From dd62b790f741ac4d83a21bb1ef9927db84fee49f Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Wed, 5 Nov 2025 17:19:03 +0000 Subject: [PATCH 1/3] Add mTLS support for Glide image loading --- app/build.gradle.kts | 1 + .../platform/glide/BitwardenAppGlideModule.kt | 248 ++++++++++++++++++ .../glide/BitwardenAppGlideModuleTest.kt | 67 +++++ gradle/libs.versions.toml | 2 + 4 files changed, 318 insertions(+) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 39df639fe6f..746f7f1d765 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -259,6 +259,7 @@ dependencies { implementation(libs.androidx.work.runtime.ktx) implementation(libs.bitwarden.sdk) implementation(libs.bumptech.glide) + ksp(libs.bumptech.glide.compiler) implementation(libs.google.hilt.android) ksp(libs.google.hilt.compiler) implementation(libs.kotlinx.collections.immutable) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt new file mode 100644 index 00000000000..42528a3934c --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt @@ -0,0 +1,248 @@ +package com.x8bit.bitwarden.ui.platform.glide + +import android.content.Context +import com.bitwarden.network.ssl.CertificateProvider +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.module.AppGlideModule +import com.x8bit.bitwarden.data.platform.manager.CertificateManager +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.IOException +import java.io.InputStream +import java.net.Socket +import java.security.KeyStore +import java.security.Principal +import java.security.PrivateKey +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509TrustManager + +/** + * Custom Glide module for the Bitwarden app that configures Glide to use an OkHttpClient + * with mTLS (mutual TLS) support. + * + * This ensures that all icon/image loading requests through Glide present the client certificate + * for mutual TLS authentication, allowing them to pass through Cloudflare's mTLS checks. + * + * The configuration mirrors the SSL setup used in RetrofitsImpl for API calls. + */ +@GlideModule +class BitwardenAppGlideModule : AppGlideModule() { + + /** + * Entry point to access Hilt-provided dependencies from non-Hilt managed classes. + */ + @EntryPoint + @InstallIn(SingletonComponent::class) + interface BitwardenGlideEntryPoint { + /** + * Provides access to [CertificateManager] for mTLS certificate management. + */ + fun certificateManager(): CertificateManager + } + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + // Get CertificateManager from Hilt + val entryPoint = EntryPointAccessors.fromApplication( + context.applicationContext, + BitwardenGlideEntryPoint::class.java, + ) + val certificateManager = entryPoint.certificateManager() + + // Create OkHttpClient with mTLS configuration + val okHttpClient = createMtlsOkHttpClient(certificateManager) + + // Register custom ModelLoader that uses our mTLS OkHttpClient + registry.replace( + GlideUrl::class.java, + InputStream::class.java, + OkHttpModelLoaderFactory(okHttpClient), + ) + } + + /** + * Custom ModelLoaderFactory for Glide 5.x that uses our mTLS-configured OkHttpClient. + */ + private class OkHttpModelLoaderFactory( + private val client: OkHttpClient, + ) : ModelLoaderFactory { + + override fun build( + multiFactory: MultiModelLoaderFactory, + ): ModelLoader = OkHttpModelLoader(client) + + override fun teardown() { + // No-op + } + } + + /** + * Custom ModelLoader that uses OkHttpClient to load images. + */ + private class OkHttpModelLoader( + private val client: OkHttpClient, + ) : ModelLoader { + + override fun buildLoadData( + model: GlideUrl, + width: Int, + height: Int, + options: Options, + ): ModelLoader.LoadData? { + return ModelLoader.LoadData(model, OkHttpDataFetcher(client, model)) + } + + override fun handles(model: GlideUrl): Boolean = true + } + + /** + * DataFetcher that uses OkHttpClient to execute HTTP requests. + */ + private class OkHttpDataFetcher( + private val client: OkHttpClient, + private val url: GlideUrl, + ) : com.bumptech.glide.load.data.DataFetcher { + + private var call: Call? = null + + override fun loadData( + priority: com.bumptech.glide.Priority, + callback: com.bumptech.glide.load.data.DataFetcher.DataCallback, + ) { + val request = Request.Builder() + .url(url.toStringUrl()) + .build() + + call = client.newCall(request) + + try { + val response = call?.execute() + if (response?.isSuccessful == true) { + callback.onDataReady(response.body?.byteStream()) + } else { + callback.onLoadFailed(Exception("HTTP ${response?.code}: ${response?.message}")) + } + } catch (e: IOException) { + callback.onLoadFailed(e) + } + } + + override fun cleanup() { + // Response body cleanup is handled by Glide + } + + override fun cancel() { + call?.cancel() + } + + override fun getDataClass(): Class = InputStream::class.java + + override fun getDataSource(): com.bumptech.glide.load.DataSource = + com.bumptech.glide.load.DataSource.REMOTE + } + + /** + * Creates an OkHttpClient configured with mTLS using the same SSL setup as RetrofitsImpl. + * + * This client will present the client certificate stored in the Android KeyStore during + * the TLS handshake. + */ + private fun createMtlsOkHttpClient(certificateProvider: CertificateProvider): OkHttpClient { + val sslContext = createSslContext(certificateProvider) + val trustManagers = createSslTrustManagers() + + return OkHttpClient.Builder() + .sslSocketFactory( + sslContext.socketFactory, + trustManagers.first() as X509TrustManager, + ) + .build() + } + + /** + * Creates an SSLContext configured with a custom X509ExtendedKeyManager. + * + * This wraps our CertificateProvider to handle client certificate selection during + * the TLS handshake. + */ + private fun createSslContext(certificateProvider: CertificateProvider): SSLContext = + SSLContext.getInstance("TLS").apply { + init( + arrayOf( + CertificateProviderKeyManager(certificateProvider = certificateProvider), + ), + createSslTrustManagers(), + null, + ) + } + + /** + * X509ExtendedKeyManager implementation that delegates to a CertificateProvider. + * + * This is equivalent to BitwardenX509ExtendedKeyManager but defined locally since + * that class is internal to the :network module. + */ + private class CertificateProviderKeyManager( + private val certificateProvider: CertificateProvider, + ) : X509ExtendedKeyManager() { + override fun chooseClientAlias( + keyType: Array?, + issuers: Array?, + socket: Socket?, + ): String = certificateProvider.chooseClientAlias( + keyType = keyType, + issuers = issuers, + socket = socket, + ) + + override fun getCertificateChain( + alias: String?, + ): Array? = certificateProvider.getCertificateChain(alias) + + override fun getPrivateKey(alias: String?): PrivateKey? = + certificateProvider.getPrivateKey(alias) + + // Unused server side methods + override fun getServerAliases( + alias: String?, + issuers: Array?, + ): Array = emptyArray() + + override fun getClientAliases( + keyType: String?, + issuers: Array?, + ): Array = emptyArray() + + override fun chooseServerAlias( + alias: String?, + issuers: Array?, + socket: Socket?, + ): String = "" + } + + /** + * Creates default TrustManagers for verifying server certificates. + * + * This uses the system's default trust anchors (trusted CA certificates). + */ + private fun createSslTrustManagers(): Array = + TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()) + .apply { init(null as KeyStore?) } + .trustManagers +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt new file mode 100644 index 00000000000..7c94c985327 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt @@ -0,0 +1,67 @@ +package com.x8bit.bitwarden.ui.platform.glide + +import com.bumptech.glide.module.AppGlideModule +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Test class for [BitwardenAppGlideModule] to verify mTLS configuration is properly applied + * to Glide without requiring a real mTLS server. + * + * These tests verify the module's structure and that it can be instantiated. + * Full integration testing requires running the app and checking logcat for + * "BitwardenGlide" logs when images are loaded. + */ +class BitwardenAppGlideModuleTest { + + @Test + fun `BitwardenAppGlideModule should be instantiable`() { + // Verify the module can be created + val module = BitwardenAppGlideModule() + + assertNotNull("BitwardenAppGlideModule should be instantiable", module) + } + + @Test + fun `BitwardenAppGlideModule should extend AppGlideModule`() { + // Verify the module properly extends AppGlideModule for Glide integration + val module = BitwardenAppGlideModule() + + assertTrue( + "BitwardenAppGlideModule must extend AppGlideModule", + module is AppGlideModule, + ) + } + + @Test + fun `BitwardenAppGlideModule should have EntryPoint interface for Hilt dependency injection`() { + // Verify the Hilt EntryPoint interface exists for accessing CertificateManager + val entryPointInterface = BitwardenAppGlideModule::class.java + .declaredClasses + .firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" } + + assertNotNull( + "BitwardenAppGlideModule must define BitwardenGlideEntryPoint interface for Hilt", + entryPointInterface, + ) + } + + @Test + fun `BitwardenGlideEntryPoint should declare certificateManager method`() { + // Verify the EntryPoint has the required method to access CertificateManager + val entryPointInterface = BitwardenAppGlideModule::class.java + .declaredClasses + .firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" } + + assertNotNull("BitwardenGlideEntryPoint must exist", entryPointInterface) + + val methods = entryPointInterface!!.declaredMethods + val hasCertificateManagerMethod = methods.any { it.name == "certificateManager" } + + assertTrue( + "BitwardenGlideEntryPoint must have certificateManager() method", + hasCertificateManagerMethod, + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f83812cd642..19bb8115329 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,7 @@ crashlytics = "3.0.6" detekt = "1.23.8" firebaseBom = "34.4.0" glide = "1.0.0-beta01" +glideKsp = "5.0.0-rc01" googleGuava = "33.5.0-jre" googleProtoBufJava = "4.33.0" googleProtoBufPlugin = "0.9.5" @@ -98,6 +99,7 @@ androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.re androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" } bitwarden-sdk = { module = "com.bitwarden:sdk-android", version.ref = "bitwardenSdk" } bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } +bumptech-glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glideKsp" } detekt-detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } From 9421e94219b66d33722bcc069bca2751c289f991 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Thu, 13 Nov 2025 13:02:44 +0000 Subject: [PATCH 2/3] Added CertificateProviderExtensions to deduplicate code Fixed tests --- .../platform/glide/BitwardenAppGlideModule.kt | 113 ++---------------- .../glide/BitwardenAppGlideModuleTest.kt | 32 ++--- .../network/retrofit/RetrofitsImpl.kt | 28 ++--- .../ssl/CertificateProviderExtensions.kt | 54 +++++++++ 4 files changed, 76 insertions(+), 151 deletions(-) create mode 100644 network/src/main/kotlin/com/bitwarden/network/ssl/CertificateProviderExtensions.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt index 42528a3934c..31898a29a6f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt @@ -1,11 +1,13 @@ package com.x8bit.bitwarden.ui.platform.glide import android.content.Context -import com.bitwarden.network.ssl.CertificateProvider +import com.bitwarden.network.ssl.createMtlsOkHttpClient import com.bumptech.glide.Glide +import com.bumptech.glide.Priority import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory @@ -21,16 +23,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import java.io.IOException import java.io.InputStream -import java.net.Socket -import java.security.KeyStore -import java.security.Principal -import java.security.PrivateKey -import java.security.cert.X509Certificate -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509ExtendedKeyManager -import javax.net.ssl.X509TrustManager /** * Custom Glide module for the Bitwarden app that configures Glide to use an OkHttpClient @@ -64,8 +56,7 @@ class BitwardenAppGlideModule : AppGlideModule() { ) val certificateManager = entryPoint.certificateManager() - // Create OkHttpClient with mTLS configuration - val okHttpClient = createMtlsOkHttpClient(certificateManager) + val okHttpClient = certificateManager.createMtlsOkHttpClient() // Register custom ModelLoader that uses our mTLS OkHttpClient registry.replace( @@ -116,13 +107,13 @@ class BitwardenAppGlideModule : AppGlideModule() { private class OkHttpDataFetcher( private val client: OkHttpClient, private val url: GlideUrl, - ) : com.bumptech.glide.load.data.DataFetcher { + ) : DataFetcher { private var call: Call? = null override fun loadData( - priority: com.bumptech.glide.Priority, - callback: com.bumptech.glide.load.data.DataFetcher.DataCallback, + priority: Priority, + callback: DataFetcher.DataCallback, ) { val request = Request.Builder() .url(url.toStringUrl()) @@ -155,94 +146,4 @@ class BitwardenAppGlideModule : AppGlideModule() { override fun getDataSource(): com.bumptech.glide.load.DataSource = com.bumptech.glide.load.DataSource.REMOTE } - - /** - * Creates an OkHttpClient configured with mTLS using the same SSL setup as RetrofitsImpl. - * - * This client will present the client certificate stored in the Android KeyStore during - * the TLS handshake. - */ - private fun createMtlsOkHttpClient(certificateProvider: CertificateProvider): OkHttpClient { - val sslContext = createSslContext(certificateProvider) - val trustManagers = createSslTrustManagers() - - return OkHttpClient.Builder() - .sslSocketFactory( - sslContext.socketFactory, - trustManagers.first() as X509TrustManager, - ) - .build() - } - - /** - * Creates an SSLContext configured with a custom X509ExtendedKeyManager. - * - * This wraps our CertificateProvider to handle client certificate selection during - * the TLS handshake. - */ - private fun createSslContext(certificateProvider: CertificateProvider): SSLContext = - SSLContext.getInstance("TLS").apply { - init( - arrayOf( - CertificateProviderKeyManager(certificateProvider = certificateProvider), - ), - createSslTrustManagers(), - null, - ) - } - - /** - * X509ExtendedKeyManager implementation that delegates to a CertificateProvider. - * - * This is equivalent to BitwardenX509ExtendedKeyManager but defined locally since - * that class is internal to the :network module. - */ - private class CertificateProviderKeyManager( - private val certificateProvider: CertificateProvider, - ) : X509ExtendedKeyManager() { - override fun chooseClientAlias( - keyType: Array?, - issuers: Array?, - socket: Socket?, - ): String = certificateProvider.chooseClientAlias( - keyType = keyType, - issuers = issuers, - socket = socket, - ) - - override fun getCertificateChain( - alias: String?, - ): Array? = certificateProvider.getCertificateChain(alias) - - override fun getPrivateKey(alias: String?): PrivateKey? = - certificateProvider.getPrivateKey(alias) - - // Unused server side methods - override fun getServerAliases( - alias: String?, - issuers: Array?, - ): Array = emptyArray() - - override fun getClientAliases( - keyType: String?, - issuers: Array?, - ): Array = emptyArray() - - override fun chooseServerAlias( - alias: String?, - issuers: Array?, - socket: Socket?, - ): String = "" - } - - /** - * Creates default TrustManagers for verifying server certificates. - * - * This uses the system's default trust anchors (trusted CA certificates). - */ - private fun createSslTrustManagers(): Array = - TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()) - .apply { init(null as KeyStore?) } - .trustManagers } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt index 7c94c985327..26e13ad0d26 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModuleTest.kt @@ -1,9 +1,8 @@ package com.x8bit.bitwarden.ui.platform.glide -import com.bumptech.glide.module.AppGlideModule -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test /** * Test class for [BitwardenAppGlideModule] to verify mTLS configuration is properly applied @@ -20,18 +19,7 @@ class BitwardenAppGlideModuleTest { // Verify the module can be created val module = BitwardenAppGlideModule() - assertNotNull("BitwardenAppGlideModule should be instantiable", module) - } - - @Test - fun `BitwardenAppGlideModule should extend AppGlideModule`() { - // Verify the module properly extends AppGlideModule for Glide integration - val module = BitwardenAppGlideModule() - - assertTrue( - "BitwardenAppGlideModule must extend AppGlideModule", - module is AppGlideModule, - ) + assertNotNull(module) } @Test @@ -41,10 +29,7 @@ class BitwardenAppGlideModuleTest { .declaredClasses .firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" } - assertNotNull( - "BitwardenAppGlideModule must define BitwardenGlideEntryPoint interface for Hilt", - entryPointInterface, - ) + assertNotNull(entryPointInterface) } @Test @@ -54,14 +39,11 @@ class BitwardenAppGlideModuleTest { .declaredClasses .firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" } - assertNotNull("BitwardenGlideEntryPoint must exist", entryPointInterface) + assertNotNull(entryPointInterface, "BitwardenGlideEntryPoint must exist") val methods = entryPointInterface!!.declaredMethods val hasCertificateManagerMethod = methods.any { it.name == "certificateManager" } - assertTrue( - "BitwardenGlideEntryPoint must have certificateManager() method", - hasCertificateManagerMethod, - ) + assertTrue(hasCertificateManagerMethod) } } diff --git a/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt b/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt index 8d0a6efe8eb..4046170faa0 100644 --- a/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt @@ -5,8 +5,8 @@ import com.bitwarden.network.interceptor.AuthTokenManager import com.bitwarden.network.interceptor.BaseUrlInterceptor import com.bitwarden.network.interceptor.BaseUrlInterceptors import com.bitwarden.network.interceptor.HeadersInterceptor -import com.bitwarden.network.ssl.BitwardenX509ExtendedKeyManager import com.bitwarden.network.ssl.CertificateProvider +import com.bitwarden.network.ssl.createSslContext import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType @@ -16,8 +16,6 @@ import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory import timber.log.Timber import java.security.KeyStore -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager @@ -149,28 +147,18 @@ internal class RetrofitsImpl( ) .build() - private fun createSslTrustManagers(): Array = - TrustManagerFactory + private fun OkHttpClient.Builder.configureSsl(): OkHttpClient.Builder { + val sslContext = certificateProvider.createSslContext() + val trustManagers = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()) .apply { init(null as KeyStore?) } .trustManagers - private fun createSslContext(certificateProvider: CertificateProvider): SSLContext = SSLContext - .getInstance("TLS").apply { - init( - arrayOf( - BitwardenX509ExtendedKeyManager(certificateProvider = certificateProvider), - ), - createSslTrustManagers(), - null, - ) - } - - private fun OkHttpClient.Builder.configureSsl(): OkHttpClient.Builder = - sslSocketFactory( - createSslContext(certificateProvider = certificateProvider).socketFactory, - createSslTrustManagers().first() as X509TrustManager, + return sslSocketFactory( + sslContext.socketFactory, + trustManagers.first() as X509TrustManager, ) + } //endregion Helper properties and functions } diff --git a/network/src/main/kotlin/com/bitwarden/network/ssl/CertificateProviderExtensions.kt b/network/src/main/kotlin/com/bitwarden/network/ssl/CertificateProviderExtensions.kt new file mode 100644 index 00000000000..c25e688ac76 --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/ssl/CertificateProviderExtensions.kt @@ -0,0 +1,54 @@ +package com.bitwarden.network.ssl + +import okhttp3.OkHttpClient +import java.security.KeyStore +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +/** + * Creates an [SSLContext] configured with mTLS support using this [CertificateProvider]. + * + * The returned SSLContext will present the client certificate from this provider during + * TLS handshakes, enabling mutual TLS authentication. + */ +fun CertificateProvider.createSslContext(): SSLContext = + SSLContext.getInstance("TLS").apply { + init( + arrayOf( + BitwardenX509ExtendedKeyManager(certificateProvider = this@createSslContext), + ), + createSslTrustManagers(), + null, + ) + } + +/** + * Creates an [OkHttpClient] configured with mTLS support using this [CertificateProvider]. + * + * The returned client will present the client certificate from this provider during TLS + * handshakes, allowing requests to pass through mTLS checks. + */ +fun CertificateProvider.createMtlsOkHttpClient(): OkHttpClient { + val sslContext = createSslContext() + val trustManagers = createSslTrustManagers() + + return OkHttpClient.Builder() + .sslSocketFactory( + sslContext.socketFactory, + trustManagers.first() as X509TrustManager, + ) + .build() +} + +/** + * Creates default [TrustManager]s for verifying server certificates. + * + * Uses the system's default trust anchors (trusted CA certificates). + */ +private fun createSslTrustManagers(): Array = + TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()) + .apply { init(null as KeyStore?) } + .trustManagers From 1bab24c7a89b8f24e2cf52341c68cbd4b3eb8ac3 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Mon, 17 Nov 2025 11:29:40 +0000 Subject: [PATCH 3/3] improved code readibility --- .../platform/glide/BitwardenAppGlideModule.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt index 31898a29a6f..723595edbef 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/glide/BitwardenAppGlideModule.kt @@ -6,6 +6,8 @@ import com.bumptech.glide.Glide import com.bumptech.glide.Priority import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.HttpException import com.bumptech.glide.load.Options import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.model.GlideUrl @@ -94,7 +96,7 @@ class BitwardenAppGlideModule : AppGlideModule() { width: Int, height: Int, options: Options, - ): ModelLoader.LoadData? { + ): ModelLoader.LoadData { return ModelLoader.LoadData(model, OkHttpDataFetcher(client, model)) } @@ -121,15 +123,19 @@ class BitwardenAppGlideModule : AppGlideModule() { call = client.newCall(request) - try { - val response = call?.execute() - if (response?.isSuccessful == true) { - callback.onDataReady(response.body?.byteStream()) - } else { - callback.onLoadFailed(Exception("HTTP ${response?.code}: ${response?.message}")) - } + val localCall = client.newCall(request).also { call = it } + + val response = try { + localCall.execute() } catch (e: IOException) { callback.onLoadFailed(e) + return + } + + if (response.isSuccessful) { + callback.onDataReady(response.body.byteStream()) + } else { + callback.onLoadFailed(HttpException(response.message, response.code)) } } @@ -143,7 +149,6 @@ class BitwardenAppGlideModule : AppGlideModule() { override fun getDataClass(): Class = InputStream::class.java - override fun getDataSource(): com.bumptech.glide.load.DataSource = - com.bumptech.glide.load.DataSource.REMOTE + override fun getDataSource(): DataSource = DataSource.REMOTE } }