Added CertificateProviderExtensions to deduplicate code

Fixed tests
This commit is contained in:
Andre Rosado 2025-11-13 13:02:44 +00:00
parent dd62b790f7
commit 9421e94219
No known key found for this signature in database
GPG Key ID: 99F68267CCD45AA9
4 changed files with 76 additions and 151 deletions

View File

@ -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<InputStream> {
) : DataFetcher<InputStream> {
private var call: Call? = null
override fun loadData(
priority: com.bumptech.glide.Priority,
callback: com.bumptech.glide.load.data.DataFetcher.DataCallback<in InputStream>,
priority: Priority,
callback: DataFetcher.DataCallback<in InputStream>,
) {
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<out String>?,
issuers: Array<out Principal>?,
socket: Socket?,
): String = certificateProvider.chooseClientAlias(
keyType = keyType,
issuers = issuers,
socket = socket,
)
override fun getCertificateChain(
alias: String?,
): Array<X509Certificate>? = certificateProvider.getCertificateChain(alias)
override fun getPrivateKey(alias: String?): PrivateKey? =
certificateProvider.getPrivateKey(alias)
// Unused server side methods
override fun getServerAliases(
alias: String?,
issuers: Array<out Principal>?,
): Array<String> = emptyArray()
override fun getClientAliases(
keyType: String?,
issuers: Array<out Principal>?,
): Array<String> = emptyArray()
override fun chooseServerAlias(
alias: String?,
issuers: Array<out Principal>?,
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<TrustManager> =
TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
.apply { init(null as KeyStore?) }
.trustManagers
}

View File

@ -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)
}
}

View File

@ -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<TrustManager> =
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
}

View File

@ -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<TrustManager> =
TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
.apply { init(null as KeyStore?) }
.trustManagers