From 467fdb23284f93d5840ac82897a0c3fa751729aa Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 9 Jan 2026 09:41:39 -0500 Subject: [PATCH] Manual reorder --- .../java/io/heckel/ntfy/ui/AddFragment.kt | 10 ++-- .../main/java/io/heckel/ntfy/util/CertUtil.kt | 51 +++++++++++-------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt index 9bd02a9c..1d7ee024 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import androidx.core.view.isVisible import androidx.core.view.isGone +import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException @@ -245,8 +246,9 @@ class AddFragment : DialogFragment(), TrustedCertificateFragment.TrustedCertific } catch (e: Exception) { Log.w(TAG, "Connection to topic failed: ${e.message}", e) - // Check if this is an SSL certificate error - if (isSSLException(e)) { + // If this is an SSL certificate error, show the trust cert dialog + // Never show the dialog for the app base URL + if (isSSLException(e) && baseUrl != appBaseUrl) { Log.d(TAG, "SSL certificate error detected, attempting to fetch certificate for user review") handleSSLException(baseUrl) } else { @@ -259,9 +261,7 @@ class AddFragment : DialogFragment(), TrustedCertificateFragment.TrustedCertific private fun isSSLException(e: Exception): Boolean { var cause: Throwable? = e while (cause != null) { - if (cause is SSLHandshakeException || - cause is SSLPeerUnverifiedException || - cause is java.security.cert.CertificateException) { + if (cause is SSLHandshakeException || cause is SSLPeerUnverifiedException || cause is CertificateException) { return true } cause = cause.cause diff --git a/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt b/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt index cf82eb47..8cbd93d9 100644 --- a/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt +++ b/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt @@ -35,6 +35,10 @@ class CertUtil private constructor(context: Context) { private val appContext: Context = context.applicationContext private val repository: Repository by lazy { Repository.getInstance(appContext) } + /** + * Configure OkHttp client with TLS config, using the pinned certificate if available as well as + * a client certificate if available. If neither are available, system trust is used. + */ suspend fun withTLSConfig(builder: OkHttpClient.Builder, baseUrl: String): OkHttpClient.Builder { try { val pinnedCert = repository.getTrustedCertificate(baseUrl) @@ -79,24 +83,12 @@ class CertUtil private constructor(context: Context) { * Fetch the server certificate without trusting it. * Used to display certificate details before user decides to trust. */ - @SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager") fun fetchServerCertificate(baseUrl: String): X509Certificate? { var capturedCert: X509Certificate? = null - - val trustManager = object : X509TrustManager { - override fun checkClientTrusted(chain: Array?, authType: String?) {} - override fun checkServerTrusted(chain: Array?, authType: String?) { - if (!chain.isNullOrEmpty()) capturedCert = chain[0] - throw SSLException("Certificate captured for inspection") - } - - override fun getAcceptedIssuers(): Array = arrayOf() - } - + val trustManager = capturingTrustManager { capturedCert = it } val sslContext = SSLContext.getInstance("TLS").apply { init(null, arrayOf(trustManager), null) } - try { val url = URL(baseUrl) val host = url.host @@ -123,7 +115,8 @@ class CertUtil private constructor(context: Context) { } /** - * mTLS client auth via PKCS#12 from DB. + * Create a key managers list using a specific client certificate (PKCS#12). + * This is used for mTLS client auth. */ private fun clientCertKeyManagers(clientCert: ClientCertificate): Array? { return try { @@ -141,6 +134,9 @@ class CertUtil private constructor(context: Context) { } } + /** + * Create a trust manager that uses the system trust store. + */ private fun systemTrustManager(): X509TrustManager { val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(null as KeyStore?) @@ -149,12 +145,19 @@ class CertUtil private constructor(context: Context) { } /** - * Hostname verifier that accepts any hostname. - * Used for pinned certificates where the fingerprint match is the trust anchor. + * Create a trust manager that captures the server certificate and then throws an exception. + * Used to inspect certificates before deciding to trust them. */ - @SuppressLint("BadHostnameVerifier") - private fun trustAllHostnameVerifier(): HostnameVerifier { - return HostnameVerifier { _, _ -> true } + @SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager") + private fun capturingTrustManager(onCertificate: (X509Certificate) -> Unit): X509TrustManager { + return object : X509TrustManager { + override fun checkClientTrusted(chain: Array?, authType: String?) {} + override fun checkServerTrusted(chain: Array?, authType: String?) { + if (!chain.isNullOrEmpty()) onCertificate(chain[0]) + throw SSLException("Certificate captured for inspection") + } + override fun getAcceptedIssuers(): Array = arrayOf() + } } /** @@ -182,12 +185,20 @@ class CertUtil private constructor(context: Context) { "Certificate fingerprint mismatch. Expected: $pinnedFingerprint, Got: $serverFingerprint" ) } - // Optionally verify certificate validity serverCert.checkValidity() } } } + /** + * Hostname verifier that accepts any hostname. Used for pinned certificates where the + * fingerprint match is the trust anchor. + */ + @SuppressLint("BadHostnameVerifier") + private fun trustAllHostnameVerifier(): HostnameVerifier { + return HostnameVerifier { _, _ -> true } + } + companion object { private const val TAG = "NtfyCertUtil"