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