diff --git a/lib/src/androidTest/java/at/bitfire/cert4android/OkhttpTest.kt b/lib/src/androidTest/java/at/bitfire/cert4android/OkhttpTest.kt index f60097e..6efcdcb 100644 --- a/lib/src/androidTest/java/at/bitfire/cert4android/OkhttpTest.kt +++ b/lib/src/androidTest/java/at/bitfire/cert4android/OkhttpTest.kt @@ -59,8 +59,10 @@ class OkhttpTest { // set cert4android TrustManager and HostnameVerifier val certManager = CustomCertManager( CustomCertStore.getInstance(context), - trustSystemCerts = true, - appInForeground = null + object : SettingsProvider { + override val appInForeground = null + override val trustSystemCerts = true + } ) val sslContext = SSLContext.getInstance("TLS") diff --git a/lib/src/main/java/at/bitfire/cert4android/CertStore.kt b/lib/src/main/java/at/bitfire/cert4android/CertStore.kt index 0295ecf..39cf77b 100644 --- a/lib/src/main/java/at/bitfire/cert4android/CertStore.kt +++ b/lib/src/main/java/at/bitfire/cert4android/CertStore.kt @@ -10,7 +10,6 @@ package at.bitfire.cert4android -import kotlinx.coroutines.flow.StateFlow import java.security.cert.X509Certificate interface CertStore { @@ -23,7 +22,7 @@ interface CertStore { /** * Determines whether a certificate chain is trusted. */ - fun isTrusted(chain: Array, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow?): Boolean + fun isTrusted(chain: Array, authType: String, trustSystemCerts: Boolean, appInForeground: Boolean?): Boolean /** * Determines whether a certificate has been explicitly accepted by the user. In this case, diff --git a/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt b/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt index 218334b..36bb3ba 100644 --- a/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt +++ b/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt @@ -11,7 +11,6 @@ package at.bitfire.cert4android import android.annotation.SuppressLint -import kotlinx.coroutines.flow.StateFlow import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.util.logging.Logger @@ -21,16 +20,13 @@ import javax.net.ssl.X509TrustManager /** * TrustManager to handle custom certificates. * - * @param trustSystemCerts whether system certificates will be trusted - * @param appInForeground - `true`: if needed, directly launches [TrustCertificateActivity] and shows notification (if possible) - * - `false`: if needed, shows notification (if possible) - * - `null`: non-interactive mode: does not show notification or launch activity + * @param certStore certificate store with (un)trusted certificates + * @param settings settings provider to get settings from */ @SuppressLint("CustomX509TrustManager") class CustomCertManager @JvmOverloads constructor( private val certStore: CertStore, - val trustSystemCerts: Boolean = true, - var appInForeground: StateFlow? + private val settings: SettingsProvider ): X509TrustManager { private val logger @@ -51,7 +47,13 @@ class CustomCertManager @JvmOverloads constructor( */ @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array, authType: String) { - if (!certStore.isTrusted(chain, authType, trustSystemCerts, appInForeground)) + if (!certStore.isTrusted( + chain, + authType, + trustSystemCerts = settings.trustSystemCerts, + appInForeground = settings.appInForeground + ) + ) throw CertificateException("Certificate chain not trusted") } @@ -75,7 +77,13 @@ class CustomCertManager @JvmOverloads constructor( // Allow users to explicitly accept certificates that have a bad hostname here (session.peerCertificates.firstOrNull() as? X509Certificate)?.let { cert -> // Check without trusting system certificates so that the user will be asked even for system-trusted certificates - if (certStore.isTrusted(arrayOf(cert), "RSA", false, appInForeground)) + if (certStore.isTrusted( + arrayOf(cert), + "RSA", + trustSystemCerts = false, + appInForeground = settings.appInForeground + ) + ) return true } diff --git a/lib/src/main/java/at/bitfire/cert4android/CustomCertStore.kt b/lib/src/main/java/at/bitfire/cert4android/CustomCertStore.kt index 0f4508e..df579fb 100644 --- a/lib/src/main/java/at/bitfire/cert4android/CustomCertStore.kt +++ b/lib/src/main/java/at/bitfire/cert4android/CustomCertStore.kt @@ -14,7 +14,6 @@ import android.annotation.SuppressLint import android.content.Context import androidx.annotation.VisibleForTesting import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import java.io.File @@ -75,7 +74,7 @@ class CustomCertStore internal constructor( /** * Determines whether a certificate chain is trusted. */ - override fun isTrusted(chain: Array, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow?): Boolean { + override fun isTrusted(chain: Array, authType: String, trustSystemCerts: Boolean, appInForeground: Boolean?): Boolean { if (chain.isEmpty()) throw IllegalArgumentException("Certificate chain must not be empty") val cert = chain[0] @@ -111,7 +110,7 @@ class CustomCertStore internal constructor( try { withTimeout(userTimeout) { - ui.check(cert, appInForeground.value) + ui.check(cert, appInForeground) } } catch (_: TimeoutCancellationException) { logger.log(Level.WARNING, "User timeout while waiting for certificate decision, rejecting") diff --git a/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt b/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt new file mode 100644 index 0000000..5c7fe6a --- /dev/null +++ b/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt @@ -0,0 +1,37 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package at.bitfire.cert4android + +/** + * Provides settings for cert4android. Implementations can override the getters. + * + * Usually implemented by the app which uses cert4android, and then passed to cert4android classes + * which need it. + */ +interface SettingsProvider { + + /** + * The app foreground status: + * + * - `true`: foreground – directly launch UI ([TrustCertificateActivity]) and show notification (if possible) + * - `false`: background – only show notification (if possible) + * - `null`: non-interactive mode – don't show notification or launch activity + */ + val appInForeground: Boolean? + + /** + * Whether system certificates shall be trusted. + * + * @return `true` if system certificates are considered trustworthy, `false` otherwise + */ + val trustSystemCerts: Boolean + +} \ No newline at end of file diff --git a/lib/src/test/java/at/bitfire/cert4android/CustomCertManagerTest.kt b/lib/src/test/java/at/bitfire/cert4android/CustomCertManagerTest.kt index 43df22c..f5edb4d 100644 --- a/lib/src/test/java/at/bitfire/cert4android/CustomCertManagerTest.kt +++ b/lib/src/test/java/at/bitfire/cert4android/CustomCertManagerTest.kt @@ -36,8 +36,14 @@ class CustomCertManagerTest { @Before fun createCertManager() { certStore = TestCertStore() - certManager = CustomCertManager(certStore, true, null) - paranoidCertManager = CustomCertManager(certStore, false, null) + certManager = CustomCertManager(certStore, object : SettingsProvider { + override val appInForeground = null + override val trustSystemCerts = true + }) + paranoidCertManager = CustomCertManager(certStore, object : SettingsProvider { + override val appInForeground = null + override val trustSystemCerts = false + }) } diff --git a/lib/src/test/java/at/bitfire/cert4android/TestCertStore.kt b/lib/src/test/java/at/bitfire/cert4android/TestCertStore.kt index 3b4bef0..e2779e9 100644 --- a/lib/src/test/java/at/bitfire/cert4android/TestCertStore.kt +++ b/lib/src/test/java/at/bitfire/cert4android/TestCertStore.kt @@ -11,7 +11,6 @@ package at.bitfire.cert4android import androidx.annotation.VisibleForTesting -import kotlinx.coroutines.flow.StateFlow import java.security.cert.X509Certificate import java.util.logging.Level import java.util.logging.Logger @@ -43,7 +42,7 @@ class TestCertStore: CertStore { /** * Determines whether a certificate chain is trusted. */ - override fun isTrusted(chain: Array, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow?): Boolean { + override fun isTrusted(chain: Array, authType: String, trustSystemCerts: Boolean, appInForeground: Boolean?): Boolean { if (chain.isEmpty()) throw IllegalArgumentException("Certificate chain must not be empty") val cert = chain[0] diff --git a/sample-app/src/main/java/at/bitfire/cert4android/demo/MainActivity.kt b/sample-app/src/main/java/at/bitfire/cert4android/demo/MainActivity.kt index 246a20a..6aae1cd 100644 --- a/sample-app/src/main/java/at/bitfire/cert4android/demo/MainActivity.kt +++ b/sample-app/src/main/java/at/bitfire/cert4android/demo/MainActivity.kt @@ -1,3 +1,13 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + package at.bitfire.cert4android.demo import android.Manifest @@ -33,6 +43,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import at.bitfire.cert4android.CustomCertManager import at.bitfire.cert4android.CustomCertStore +import at.bitfire.cert4android.SettingsProvider import at.bitfire.cert4android.ThemeManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -62,7 +73,7 @@ class MainActivity : ComponentActivity() { .padding(8.dp) .verticalScroll(rememberScrollState())) { Row { - Checkbox(model.appInForeground.collectAsState().value, onCheckedChange = { foreground -> + Checkbox(model.foreground.collectAsState().value, onCheckedChange = { foreground -> model.setInForeground(foreground) }) Text("App in foreground") @@ -131,7 +142,8 @@ class MainActivity : ComponentActivity() { val context: Context get() = getApplication() - val appInForeground = MutableStateFlow(true) + val foreground = MutableStateFlow(true) + val resultMessage = MutableLiveData() init { @@ -145,7 +157,7 @@ class MainActivity : ComponentActivity() { } fun setInForeground(foreground: Boolean) { - appInForeground.value = foreground + this.foreground.value = foreground } fun testAccess(url: String, trustSystemCerts: Boolean = true) = viewModelScope.launch(Dispatchers.IO) { @@ -177,8 +189,12 @@ class MainActivity : ComponentActivity() { // set cert4android TrustManager and HostnameVerifier val certManager = CustomCertManager( certStore = CustomCertStore.getInstance(context), - trustSystemCerts = trustSystemCerts, - appInForeground = appInForeground + settings = object : SettingsProvider { + override val appInForeground + get() = foreground.value + override val trustSystemCerts + get() = trustSystemCerts + } ) val sslContext = SSLContext.getInstance("TLS")