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:
Kirill Kamakin
2023-07-04 22:53:05 +02:00
committed by GitHub
parent 2375be0329
commit 79dee6a9ad
17 changed files with 279 additions and 15 deletions

View File

@@ -16,8 +16,8 @@ plugins {
android { android {
defaultConfig { defaultConfig {
applicationId = "gq.kirmanak.mealient" applicationId = "gq.kirmanak.mealient"
versionCode = 28 versionCode = 29
versionName = "0.3.13" versionName = "0.4.0"
testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner" testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner"
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true") testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de") resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de")

View File

@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.ui.baseurl
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@@ -11,13 +12,16 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
import gq.kirmanak.mealient.datasource.CertificateCombinedException
import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.extensions.collectWhenResumed
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.CheckableMenuItem import gq.kirmanak.mealient.ui.CheckableMenuItem
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentDirections.Companion.actionBaseURLFragmentToRecipesListFragment import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentDirections.Companion.actionBaseURLFragmentToRecipesListFragment
import java.security.cert.X509Certificate
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -36,6 +40,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
binding.button.setOnClickListener(::onProceedClick) binding.button.setOnClickListener(::onProceedClick)
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
collectWhenResumed(viewModel.invalidCertificatesFlow, ::onInvalidCertificate)
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy( it.copy(
navigationVisible = !args.isOnboarding, 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) { private fun onProceedClick(view: View) {
logger.v { "onProceedClick() called with: view = $view" } logger.v { "onProceedClick() called with: view = $view" }
saveEnteredUrl()
}
private fun saveEnteredUrl() {
logger.v { "saveEnteredUrl() called" }
val url = binding.urlInput.checkIfInputIsEmpty( val url = binding.urlInput.checkIfInputIsEmpty(
inputLayout = binding.urlInputLayout, inputLayout = binding.urlInputLayout,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
@@ -69,6 +101,8 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
val cause = exception.cause?.message ?: exception.message val cause = exception.cause?.message ?: exception.message
getString(R.string.fragment_base_url_malformed_url, cause) getString(R.string.fragment_base_url_malformed_url, cause)
} }
is CertificateCombinedException -> getString(R.string.fragment_base_url_invalid_certificate_title)
null -> null null -> null
else -> getString(R.string.fragment_base_url_unknown_error) else -> getString(R.string.fragment_base_url_unknown_error)
} }

View File

@@ -8,11 +8,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.CertificateCombinedException
import gq.kirmanak.mealient.datasource.NetworkError 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.logging.Logger
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.security.cert.X509Certificate
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -21,12 +26,15 @@ class BaseURLViewModel @Inject constructor(
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val recipeRepo: RecipeRepo, private val recipeRepo: RecipeRepo,
private val logger: Logger, private val logger: Logger,
private val shoppingListsRepo: ShoppingListsRepo, private val trustedCertificatesStore: TrustedCertificatesStore,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial()) private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
private val invalidCertificatesChannel = Channel<X509Certificate>(Channel.UNLIMITED)
val invalidCertificatesFlow = invalidCertificatesChannel.receiveAsFlow()
fun saveBaseUrl(baseURL: String) { fun saveBaseUrl(baseURL: String) {
logger.v { "saveBaseUrl() called with: baseURL = $baseURL" } logger.v { "saveBaseUrl() called with: baseURL = $baseURL" }
_uiState.value = OperationUiState.Progress() _uiState.value = OperationUiState.Progress()
@@ -49,7 +57,11 @@ class BaseURLViewModel @Inject constructor(
val result: Result<Unit> = serverInfoRepo.tryBaseURL(url).recoverCatching { val result: Result<Unit> = serverInfoRepo.tryBaseURL(url).recoverCatching {
logger.e(it) { "checkBaseURL: trying to recover, had prefix = $hasPrefix" } 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 throw it
} else { } else {
val unencryptedUrl = url.replace("https", "http") val unencryptedUrl = url.replace("https", "http")
@@ -66,4 +78,8 @@ class BaseURLViewModel @Inject constructor(
_uiState.value = OperationUiState.fromResult(result) _uiState.value = OperationUiState.fromResult(result)
} }
fun acceptInvalidCertificate(certificate: X509Certificate) {
logger.v { "acceptInvalidCertificate() called with: certificate = $certificate" }
trustedCertificatesStore.addTrustedCertificate(certificate)
}
} }

View File

@@ -17,6 +17,10 @@
<string name="fragment_base_url_malformed_url">Check URL format: %s</string> <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_save">Proceed</string>
<string name="fragment_base_url_unknown_error" translatable="false">@string/fragment_authentication_unknown_error</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="menu_navigation_drawer_login">Login</string>
<string name="fragment_disclaimer_button_okay">Okay</string> <string name="fragment_disclaimer_button_okay">Okay</string>
<string name="view_holder_recipe_instructions_step">Step: %d</string> <string name="view_holder_recipe_instructions_step">Step: %d</string>

View File

@@ -5,7 +5,7 @@ import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.NetworkError 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.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
@@ -33,7 +33,7 @@ class BaseURLViewModelTest : BaseUnitTest() {
lateinit var recipeRepo: RecipeRepo lateinit var recipeRepo: RecipeRepo
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var shoppingListsRepo: ShoppingListsRepo lateinit var trustedCertificatesStore: TrustedCertificatesStore
lateinit var subject: BaseURLViewModel lateinit var subject: BaseURLViewModel
@@ -45,7 +45,7 @@ class BaseURLViewModelTest : BaseUnitTest() {
authRepo = authRepo, authRepo = authRepo,
recipeRepo = recipeRepo, recipeRepo = recipeRepo,
logger = logger, logger = logger,
shoppingListsRepo = shoppingListsRepo, trustedCertificatesStore = trustedCertificatesStore,
) )
} }

View File

@@ -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
}
}

View File

@@ -20,3 +20,13 @@ inline fun <T> runCatchingExceptCancel(block: () -> T): Result<T> = try {
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
inline fun <reified R> ResponseBody.decode(json: Json): R = json.decodeFromStream(byteStream()) 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
}

View File

@@ -13,6 +13,7 @@ import gq.kirmanak.mealient.datasource.impl.CacheBuilderImpl
import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl
import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl
import gq.kirmanak.mealient.datasource.impl.RetrofitBuilder 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.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0 import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
@@ -30,7 +31,7 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface DataSourceModule { internal interface DataSourceModule {
companion object { companion object {
@@ -94,4 +95,7 @@ interface DataSourceModule {
@Binds @Binds
@IntoSet @IntoSet
fun bindBaseUrlInterceptor(baseUrlInterceptor: BaseUrlInterceptor): LocalInterceptor fun bindBaseUrlInterceptor(baseUrlInterceptor: BaseUrlInterceptor): LocalInterceptor
@Binds
fun bindTrustedCertificatesStore(impl: TrustedCertificatesStoreImpl): TrustedCertificatesStore
} }

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -10,7 +10,7 @@ import okhttp3.Response
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
class AuthInterceptor @Inject constructor( internal class AuthInterceptor @Inject constructor(
private val logger: Logger, private val logger: Logger,
private val authenticationProviderProvider: Provider<AuthenticationProvider>, private val authenticationProviderProvider: Provider<AuthenticationProvider>,
) : LocalInterceptor { ) : LocalInterceptor {

View File

@@ -11,7 +11,7 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
class BaseUrlInterceptor @Inject constructor( internal class BaseUrlInterceptor @Inject constructor(
private val logger: Logger, private val logger: Logger,
private val serverUrlProviderProvider: Provider<ServerUrlProvider>, private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
) : LocalInterceptor { ) : LocalInterceptor {

View File

@@ -9,7 +9,7 @@ import okhttp3.Cache
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class CacheBuilderImpl @Inject constructor( internal class CacheBuilderImpl @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val logger: Logger, private val logger: Logger,
) : CacheBuilder { ) : CacheBuilder {

View File

@@ -7,7 +7,7 @@ import gq.kirmanak.mealient.logging.Logger
import retrofit2.HttpException import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class NetworkRequestWrapperImpl @Inject constructor( internal class NetworkRequestWrapperImpl @Inject constructor(
private val logger: Logger, private val logger: Logger,
) : NetworkRequestWrapper { ) : NetworkRequestWrapper {

View File

@@ -6,22 +6,50 @@ import gq.kirmanak.mealient.datasource.OkHttpBuilder
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.TlsVersion
import javax.inject.Inject 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, private val cacheBuilder: CacheBuilder,
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382) // Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
private val interceptors: Set<@JvmSuppressWildcards Interceptor>, private val interceptors: Set<@JvmSuppressWildcards Interceptor>,
private val localInterceptors: Set<@JvmSuppressWildcards LocalInterceptor>, private val localInterceptors: Set<@JvmSuppressWildcards LocalInterceptor>,
private val advancedX509TrustManager: AdvancedX509TrustManager,
private val logger: Logger, private val logger: Logger,
) : OkHttpBuilder { ) : OkHttpBuilder {
override fun buildOkHttp(): OkHttpClient { override fun buildOkHttp(): OkHttpClient {
logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors, localInterceptors = $localInterceptors" } 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 { return OkHttpClient.Builder().apply {
localInterceptors.forEach(::addInterceptor) localInterceptors.forEach(::addInterceptor)
interceptors.forEach(::addNetworkInterceptor) interceptors.forEach(::addNetworkInterceptor)
sslSocketFactory(sslSocketFactory, advancedX509TrustManager)
cache(cacheBuilder.buildCache()) cache(cacheBuilder.buildCache())
}.build() }.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()
}
} }

View File

@@ -6,7 +6,7 @@ import retrofit2.Converter.Factory
import retrofit2.Retrofit import retrofit2.Retrofit
import javax.inject.Inject import javax.inject.Inject
class RetrofitBuilder @Inject constructor( internal class RetrofitBuilder @Inject constructor(
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val converterFactory: Factory, private val converterFactory: Factory,
private val logger: Logger, private val logger: Logger,

View File

@@ -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"
}
}