From a9c911012b4acd54d63df666c939cdd8c5514d1a Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 25 Jan 2026 11:02:15 +0100 Subject: [PATCH 1/5] Add `SettingsProvider` interface for settings - Modify `CustomCertManager` to use `SettingsProvider` for settings - Remove `Flow` for setting that can change --- .../java/at/bitfire/cert4android/CertStore.kt | 3 +- .../bitfire/cert4android/CustomCertManager.kt | 26 ++++++++----- .../bitfire/cert4android/CustomCertStore.kt | 5 +-- .../bitfire/cert4android/SettingsProvider.kt | 37 +++++++++++++++++++ 4 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt 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..e0966b4 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..a4e012f --- /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 + +interface SettingsProvider { + + /* + * @param trustSystemCerts + * @param appInForeground + + */ + + /** + * Returns 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 + */ + fun appInForeground(): Boolean? + + /** + * Returns whether system certificates shall be trusted. + * + * @return `true` if system certificates are considered trustworthy, `false` otherwise + */ + fun trustSystemCerts(): Boolean + +} \ No newline at end of file From a54c5041b0db4b590246d94c547f05e7f217301f Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 25 Jan 2026 11:06:56 +0100 Subject: [PATCH 2/5] KDoc --- .../java/at/bitfire/cert4android/SettingsProvider.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt b/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt index a4e012f..fb6db13 100644 --- a/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt +++ b/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt @@ -10,14 +10,14 @@ package at.bitfire.cert4android +/** + * Provides settings for cert4android. + * + * Usually implemented by the app which uses cert4android, and then passed to cert4android classes + * which need it. + */ interface SettingsProvider { - /* - * @param trustSystemCerts - * @param appInForeground - - */ - /** * Returns the app foreground status: * From d86e6ee13fdbe27aa51cf712076faabbc20d7e64 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 25 Jan 2026 11:17:11 +0100 Subject: [PATCH 3/5] Refactor SettingsProvider methods to properties - Change `appInForeground()` to `appInForeground` property - Change `trustSystemCerts()` to `trustSystemCerts` property - Update usages in CustomCertManager and MainActivity --- .../bitfire/cert4android/CustomCertManager.kt | 6 ++--- .../bitfire/cert4android/SettingsProvider.kt | 10 +++---- .../bitfire/cert4android/demo/MainActivity.kt | 26 +++++++++++++++---- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt b/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt index e0966b4..36bb3ba 100644 --- a/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt +++ b/lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt @@ -50,8 +50,8 @@ class CustomCertManager @JvmOverloads constructor( if (!certStore.isTrusted( chain, authType, - trustSystemCerts = settings.trustSystemCerts(), - appInForeground = settings.appInForeground() + trustSystemCerts = settings.trustSystemCerts, + appInForeground = settings.appInForeground ) ) throw CertificateException("Certificate chain not trusted") @@ -81,7 +81,7 @@ class CustomCertManager @JvmOverloads constructor( arrayOf(cert), "RSA", trustSystemCerts = false, - appInForeground = settings.appInForeground() + appInForeground = settings.appInForeground ) ) return true diff --git a/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt b/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt index fb6db13..5c7fe6a 100644 --- a/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt +++ b/lib/src/main/java/at/bitfire/cert4android/SettingsProvider.kt @@ -11,7 +11,7 @@ package at.bitfire.cert4android /** - * Provides settings for 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. @@ -19,19 +19,19 @@ package at.bitfire.cert4android interface SettingsProvider { /** - * Returns the app foreground status: + * 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 */ - fun appInForeground(): Boolean? + val appInForeground: Boolean? /** - * Returns whether system certificates shall be trusted. + * Whether system certificates shall be trusted. * * @return `true` if system certificates are considered trustworthy, `false` otherwise */ - fun trustSystemCerts(): Boolean + val trustSystemCerts: Boolean } \ No newline at end of file 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") From 6aa17b1581f547da8e8417cbb4c4d0ba5d46de59 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 25 Jan 2026 11:41:12 +0100 Subject: [PATCH 4/5] Fix tests --- .../androidTest/java/at/bitfire/cert4android/OkhttpTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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") From 9dd481172680ea7af4ca7f8851abb15848c3bd78 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 25 Jan 2026 11:45:50 +0100 Subject: [PATCH 5/5] Fix tests (2) --- .../at/bitfire/cert4android/CustomCertManagerTest.kt | 10 ++++++++-- .../test/java/at/bitfire/cert4android/TestCertStore.kt | 3 +-- 2 files changed, 9 insertions(+), 4 deletions(-) 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]