Allow users to trust self-signed certificates (#160)
* Implement CERT-Store * Trust user-added certificates * Improve code readability * Implement saving self-signed certs to storage * Create interface for TrustedCertificatesStore * Remove unused code * Make datasource implementation internal * Bump app version to 29 (0.4.0) --------- Co-authored-by: fz72 <fz72@gmx.de>
This commit is contained in:
@@ -16,8 +16,8 @@ plugins {
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId = "gq.kirmanak.mealient"
|
||||
versionCode = 28
|
||||
versionName = "0.3.13"
|
||||
versionCode = 29
|
||||
versionName = "0.4.0"
|
||||
testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner"
|
||||
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
|
||||
resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de")
|
||||
|
||||
@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.ui.baseurl
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -11,13 +12,16 @@ import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
|
||||
import gq.kirmanak.mealient.datasource.CertificateCombinedException
|
||||
import gq.kirmanak.mealient.datasource.NetworkError
|
||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||
import gq.kirmanak.mealient.extensions.collectWhenResumed
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.CheckableMenuItem
|
||||
import gq.kirmanak.mealient.ui.OperationUiState
|
||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||
import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentDirections.Companion.actionBaseURLFragmentToRecipesListFragment
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -36,6 +40,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
||||
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
|
||||
binding.button.setOnClickListener(::onProceedClick)
|
||||
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
|
||||
collectWhenResumed(viewModel.invalidCertificatesFlow, ::onInvalidCertificate)
|
||||
activityViewModel.updateUiState {
|
||||
it.copy(
|
||||
navigationVisible = !args.isOnboarding,
|
||||
@@ -45,8 +50,35 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onInvalidCertificate(certificate: X509Certificate) {
|
||||
logger.v { "onInvalidCertificate() called with: certificate = $certificate" }
|
||||
val dialogMessage = getString(
|
||||
R.string.fragment_base_url_invalid_certificate_message,
|
||||
certificate.issuerDN.toString(),
|
||||
certificate.subjectDN.toString(),
|
||||
certificate.notBefore.toString(),
|
||||
certificate.notAfter.toString(),
|
||||
)
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.fragment_base_url_invalid_certificate_title)
|
||||
.setMessage(dialogMessage)
|
||||
.setPositiveButton(R.string.fragment_base_url_invalid_certificate_accept) { _, _ ->
|
||||
viewModel.acceptInvalidCertificate(certificate)
|
||||
saveEnteredUrl()
|
||||
}.setNegativeButton(R.string.fragment_base_url_invalid_certificate_deny) { _, _ ->
|
||||
// Do nothing, let the user enter another address or try again
|
||||
}
|
||||
.create()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun onProceedClick(view: View) {
|
||||
logger.v { "onProceedClick() called with: view = $view" }
|
||||
saveEnteredUrl()
|
||||
}
|
||||
|
||||
private fun saveEnteredUrl() {
|
||||
logger.v { "saveEnteredUrl() called" }
|
||||
val url = binding.urlInput.checkIfInputIsEmpty(
|
||||
inputLayout = binding.urlInputLayout,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
@@ -69,6 +101,8 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
||||
val cause = exception.cause?.message ?: exception.message
|
||||
getString(R.string.fragment_base_url_malformed_url, cause)
|
||||
}
|
||||
|
||||
is CertificateCombinedException -> getString(R.string.fragment_base_url_invalid_certificate_title)
|
||||
null -> null
|
||||
else -> getString(R.string.fragment_base_url_unknown_error)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.datasource.CertificateCombinedException
|
||||
import gq.kirmanak.mealient.datasource.NetworkError
|
||||
import gq.kirmanak.mealient.datasource.TrustedCertificatesStore
|
||||
import gq.kirmanak.mealient.datasource.findCauseAsInstanceOf
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
|
||||
import gq.kirmanak.mealient.ui.OperationUiState
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -21,12 +26,15 @@ class BaseURLViewModel @Inject constructor(
|
||||
private val authRepo: AuthRepo,
|
||||
private val recipeRepo: RecipeRepo,
|
||||
private val logger: Logger,
|
||||
private val shoppingListsRepo: ShoppingListsRepo,
|
||||
private val trustedCertificatesStore: TrustedCertificatesStore,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
|
||||
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
|
||||
|
||||
private val invalidCertificatesChannel = Channel<X509Certificate>(Channel.UNLIMITED)
|
||||
val invalidCertificatesFlow = invalidCertificatesChannel.receiveAsFlow()
|
||||
|
||||
fun saveBaseUrl(baseURL: String) {
|
||||
logger.v { "saveBaseUrl() called with: baseURL = $baseURL" }
|
||||
_uiState.value = OperationUiState.Progress()
|
||||
@@ -49,7 +57,11 @@ class BaseURLViewModel @Inject constructor(
|
||||
|
||||
val result: Result<Unit> = serverInfoRepo.tryBaseURL(url).recoverCatching {
|
||||
logger.e(it) { "checkBaseURL: trying to recover, had prefix = $hasPrefix" }
|
||||
if (hasPrefix || it is NetworkError.NotMealie) {
|
||||
val certificateError = it.findCauseAsInstanceOf<CertificateCombinedException>()
|
||||
if (certificateError != null) {
|
||||
invalidCertificatesChannel.send(certificateError.serverCert)
|
||||
throw certificateError
|
||||
} else if (hasPrefix || it is NetworkError.NotMealie) {
|
||||
throw it
|
||||
} else {
|
||||
val unencryptedUrl = url.replace("https", "http")
|
||||
@@ -66,4 +78,8 @@ class BaseURLViewModel @Inject constructor(
|
||||
_uiState.value = OperationUiState.fromResult(result)
|
||||
}
|
||||
|
||||
fun acceptInvalidCertificate(certificate: X509Certificate) {
|
||||
logger.v { "acceptInvalidCertificate() called with: certificate = $certificate" }
|
||||
trustedCertificatesStore.addTrustedCertificate(certificate)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@
|
||||
<string name="fragment_base_url_malformed_url">Check URL format: %s</string>
|
||||
<string name="fragment_base_url_save">Proceed</string>
|
||||
<string name="fragment_base_url_unknown_error" translatable="false">@string/fragment_authentication_unknown_error</string>
|
||||
<string name="fragment_base_url_invalid_certificate_title">The identity of the server could not be verified</string>
|
||||
<string name="fragment_base_url_invalid_certificate_message">Do you trust this certificate?\n\nCertificate Information:\nIssuer: %1$s\nSubject: %2$s\nValid From: %3$s\nValid Until: %4$s</string>
|
||||
<string name="fragment_base_url_invalid_certificate_accept">Trust</string>
|
||||
<string name="fragment_base_url_invalid_certificate_deny">No</string>
|
||||
<string name="menu_navigation_drawer_login">Login</string>
|
||||
<string name="fragment_disclaimer_button_okay">Okay</string>
|
||||
<string name="view_holder_recipe_instructions_step">Step: %d</string>
|
||||
|
||||
@@ -5,7 +5,7 @@ import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.datasource.NetworkError
|
||||
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
|
||||
import gq.kirmanak.mealient.datasource.TrustedCertificatesStore
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import gq.kirmanak.mealient.ui.OperationUiState
|
||||
@@ -33,7 +33,7 @@ class BaseURLViewModelTest : BaseUnitTest() {
|
||||
lateinit var recipeRepo: RecipeRepo
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var shoppingListsRepo: ShoppingListsRepo
|
||||
lateinit var trustedCertificatesStore: TrustedCertificatesStore
|
||||
|
||||
lateinit var subject: BaseURLViewModel
|
||||
|
||||
@@ -45,7 +45,7 @@ class BaseURLViewModelTest : BaseUnitTest() {
|
||||
authRepo = authRepo,
|
||||
recipeRepo = recipeRepo,
|
||||
logger = logger,
|
||||
shoppingListsRepo = shoppingListsRepo,
|
||||
trustedCertificatesStore = trustedCertificatesStore,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateExpiredException
|
||||
import java.security.cert.CertificateNotYetValidException
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLPeerUnverifiedException
|
||||
|
||||
|
||||
class CertificateCombinedException(val serverCert: X509Certificate) : RuntimeException() {
|
||||
|
||||
var certificateExpiredException: CertificateExpiredException? = null
|
||||
var certificateNotYetValidException: CertificateNotYetValidException? = null
|
||||
var certPathValidatorException: CertPathValidatorException? = null
|
||||
var otherCertificateException: CertificateException? = null
|
||||
var sslPeerUnverifiedException: SSLPeerUnverifiedException? = null
|
||||
|
||||
fun isException(): Boolean {
|
||||
return listOf(
|
||||
certificateExpiredException,
|
||||
certificateNotYetValidException,
|
||||
certPathValidatorException,
|
||||
otherCertificateException,
|
||||
sslPeerUnverifiedException
|
||||
).any { it != null }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID: Long = -8875782030758554999L
|
||||
}
|
||||
}
|
||||
@@ -20,3 +20,13 @@ inline fun <T> runCatchingExceptCancel(block: () -> T): Result<T> = try {
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
inline fun <reified R> ResponseBody.decode(json: Json): R = json.decodeFromStream(byteStream())
|
||||
|
||||
inline fun <reified T> Throwable.findCauseAsInstanceOf(): T? {
|
||||
var cause: Throwable? = this
|
||||
var previousCause: Throwable? = null
|
||||
while (cause != null && cause != previousCause && cause !is T) {
|
||||
previousCause = cause
|
||||
cause = cause.cause
|
||||
}
|
||||
return cause as? T
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import gq.kirmanak.mealient.datasource.impl.CacheBuilderImpl
|
||||
import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl
|
||||
import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl
|
||||
import gq.kirmanak.mealient.datasource.impl.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.datasource.impl.TrustedCertificatesStoreImpl
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
|
||||
@@ -30,7 +31,7 @@ import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface DataSourceModule {
|
||||
internal interface DataSourceModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -94,4 +95,7 @@ interface DataSourceModule {
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindBaseUrlInterceptor(baseUrlInterceptor: BaseUrlInterceptor): LocalInterceptor
|
||||
|
||||
@Binds
|
||||
fun bindTrustedCertificatesStore(impl: TrustedCertificatesStoreImpl): TrustedCertificatesStore
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import java.security.cert.Certificate
|
||||
|
||||
interface TrustedCertificatesStore {
|
||||
|
||||
fun isTrustedCertificate(cert: Certificate): Boolean
|
||||
|
||||
fun addTrustedCertificate(cert: Certificate)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package gq.kirmanak.mealient.datasource.impl
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import gq.kirmanak.mealient.datasource.CertificateCombinedException
|
||||
import gq.kirmanak.mealient.datasource.TrustedCertificatesStore
|
||||
import gq.kirmanak.mealient.datasource.findCauseAsInstanceOf
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.*
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
internal class AdvancedX509TrustManager @Inject constructor(
|
||||
private val trustedCertificatesStore: TrustedCertificatesStore
|
||||
) : X509TrustManager {
|
||||
|
||||
private val standardTrustManager: X509TrustManager by lazy {
|
||||
findStandardTrustManager()
|
||||
}
|
||||
|
||||
private fun findStandardTrustManager(): X509TrustManager {
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(null as KeyStore?)
|
||||
return factory.trustManagers
|
||||
.filterIsInstance<X509TrustManager>()
|
||||
.first()
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(certificates: Array<X509Certificate?>?, authType: String?) {
|
||||
standardTrustManager.checkClientTrusted(certificates, authType)
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(certificates: Array<X509Certificate>, authType: String?) {
|
||||
if (trustedCertificatesStore.isTrustedCertificate(certificates[0])) {
|
||||
return
|
||||
}
|
||||
|
||||
val result = CertificateCombinedException(certificates[0])
|
||||
|
||||
try {
|
||||
certificates[0].checkValidity()
|
||||
} catch (c: CertificateExpiredException) {
|
||||
result.certificateExpiredException = c
|
||||
} catch (c: CertificateNotYetValidException) {
|
||||
result.certificateNotYetValidException = c
|
||||
}
|
||||
|
||||
try {
|
||||
standardTrustManager.checkServerTrusted(certificates, authType)
|
||||
} catch (c: CertificateException) {
|
||||
val cause = c.findCauseAsInstanceOf<CertPathValidatorException>()
|
||||
if (cause != null) {
|
||||
result.certPathValidatorException = cause
|
||||
} else {
|
||||
result.otherCertificateException = c
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isException()) {
|
||||
throw result
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return standardTrustManager.acceptedIssuers
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class AuthInterceptor @Inject constructor(
|
||||
internal class AuthInterceptor @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val authenticationProviderProvider: Provider<AuthenticationProvider>,
|
||||
) : LocalInterceptor {
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class BaseUrlInterceptor @Inject constructor(
|
||||
internal class BaseUrlInterceptor @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
|
||||
) : LocalInterceptor {
|
||||
|
||||
@@ -9,7 +9,7 @@ import okhttp3.Cache
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class CacheBuilderImpl @Inject constructor(
|
||||
internal class CacheBuilderImpl @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val logger: Logger,
|
||||
) : CacheBuilder {
|
||||
|
||||
@@ -7,7 +7,7 @@ import gq.kirmanak.mealient.logging.Logger
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
class NetworkRequestWrapperImpl @Inject constructor(
|
||||
internal class NetworkRequestWrapperImpl @Inject constructor(
|
||||
private val logger: Logger,
|
||||
) : NetworkRequestWrapper {
|
||||
|
||||
|
||||
@@ -6,22 +6,50 @@ import gq.kirmanak.mealient.datasource.OkHttpBuilder
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.TlsVersion
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
|
||||
class OkHttpBuilderImpl @Inject constructor(
|
||||
internal class OkHttpBuilderImpl @Inject constructor(
|
||||
private val cacheBuilder: CacheBuilder,
|
||||
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
|
||||
private val interceptors: Set<@JvmSuppressWildcards Interceptor>,
|
||||
private val localInterceptors: Set<@JvmSuppressWildcards LocalInterceptor>,
|
||||
private val advancedX509TrustManager: AdvancedX509TrustManager,
|
||||
private val logger: Logger,
|
||||
) : OkHttpBuilder {
|
||||
|
||||
override fun buildOkHttp(): OkHttpClient {
|
||||
logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors, localInterceptors = $localInterceptors" }
|
||||
|
||||
val sslContext = buildSSLContext()
|
||||
sslContext.init(null, arrayOf<TrustManager>(advancedX509TrustManager), null)
|
||||
val sslSocketFactory = sslContext.socketFactory
|
||||
|
||||
return OkHttpClient.Builder().apply {
|
||||
localInterceptors.forEach(::addInterceptor)
|
||||
interceptors.forEach(::addNetworkInterceptor)
|
||||
sslSocketFactory(sslSocketFactory, advancedX509TrustManager)
|
||||
cache(cacheBuilder.buildCache())
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildSSLContext(): SSLContext {
|
||||
return runCatching {
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_3.javaName)
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.3 is not supported in this device; falling through TLSv1.2" }
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_2.javaName)
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.2 is not supported in this device; falling through TLSv1.1" }
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_1.javaName)
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.1 is not supported in this device; falling through TLSv1.0" }
|
||||
// should be available in any device; see reference of supported protocols in
|
||||
// http://developer.android.com/reference/javax/net/ssl/SSLSocket.html
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_0.javaName)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import retrofit2.Converter.Factory
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Inject
|
||||
|
||||
class RetrofitBuilder @Inject constructor(
|
||||
internal class RetrofitBuilder @Inject constructor(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val converterFactory: Factory,
|
||||
private val logger: Logger,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package gq.kirmanak.mealient.datasource.impl
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import gq.kirmanak.mealient.datasource.TrustedCertificatesStore
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.cert.Certificate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class TrustedCertificatesStoreImpl @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val logger: Logger,
|
||||
) : TrustedCertificatesStore {
|
||||
|
||||
private val trustedCertificatesStore: KeyStore by lazy { initialiseTrustedCertificatesStore() }
|
||||
|
||||
private fun initialiseTrustedCertificatesStore(): KeyStore {
|
||||
val store = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
val localTrustStoreFile = File(context.filesDir, LOCAL_TRUSTSTORE_FILENAME)
|
||||
logger.d { "Looking for a trusted certificate store at ${localTrustStoreFile.absolutePath}" }
|
||||
if (localTrustStoreFile.exists()) {
|
||||
FileInputStream(localTrustStoreFile).use {
|
||||
store.load(it, LOCAL_TRUSTSTORE_PASSWORD.toCharArray())
|
||||
}
|
||||
} else {
|
||||
// next is necessary to initialize an empty KeyStore instance
|
||||
store.load(null, LOCAL_TRUSTSTORE_PASSWORD.toCharArray())
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
override fun isTrustedCertificate(cert: Certificate): Boolean {
|
||||
return try {
|
||||
trustedCertificatesStore.getCertificateAlias(cert) != null
|
||||
} catch (e: KeyStoreException) {
|
||||
logger.e(e) { "Fail while checking certificate in the known-servers store" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun addTrustedCertificate(cert: Certificate) {
|
||||
trustedCertificatesStore.setCertificateEntry(cert.hashCode().toString(), cert)
|
||||
context.openFileOutput(LOCAL_TRUSTSTORE_FILENAME, Context.MODE_PRIVATE).use {
|
||||
trustedCertificatesStore.store(it, LOCAL_TRUSTSTORE_PASSWORD.toCharArray())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LOCAL_TRUSTSTORE_FILENAME = "trustedCertificates.bks"
|
||||
private const val LOCAL_TRUSTSTORE_PASSWORD = "password"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user