Skip to content

Commit 9f82b42

Browse files
[BWA-182] Add mTLS support for Glide image loading (#6125)
Co-authored-by: David Perez <david@livefront.com>
1 parent 5531b47 commit 9f82b42

File tree

6 files changed

+178
-32
lines changed

6 files changed

+178
-32
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ dependencies {
260260
implementation(libs.androidx.work.runtime.ktx)
261261
implementation(libs.bitwarden.sdk)
262262
implementation(libs.bumptech.glide)
263+
implementation(libs.bumptech.glide.okhttp)
264+
ksp(libs.bumptech.glide.compiler)
263265
implementation(libs.google.hilt.android)
264266
ksp(libs.google.hilt.compiler)
265267
implementation(libs.kotlinx.collections.immutable)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.x8bit.bitwarden.ui.platform.glide
2+
3+
import android.content.Context
4+
import com.bitwarden.network.ssl.createMtlsOkHttpClient
5+
import com.bumptech.glide.Glide
6+
import com.bumptech.glide.Registry
7+
import com.bumptech.glide.annotation.GlideModule
8+
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
9+
import com.bumptech.glide.load.model.GlideUrl
10+
import com.bumptech.glide.module.AppGlideModule
11+
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
12+
import dagger.hilt.EntryPoint
13+
import dagger.hilt.InstallIn
14+
import dagger.hilt.android.EntryPointAccessors
15+
import dagger.hilt.components.SingletonComponent
16+
import java.io.InputStream
17+
18+
/**
19+
* Custom Glide module for the Bitwarden app that configures Glide to use an OkHttpClient
20+
* with mTLS (mutual TLS) support.
21+
*
22+
* This ensures that all icon/image loading requests through Glide present the client certificate
23+
* for mutual TLS authentication, allowing them to pass through Cloudflare's mTLS checks.
24+
*
25+
* The configuration mirrors the SSL setup used in RetrofitsImpl for API calls.
26+
*/
27+
@GlideModule
28+
class BitwardenAppGlideModule : AppGlideModule() {
29+
30+
/**
31+
* Entry point to access Hilt-provided dependencies from non-Hilt managed classes.
32+
*/
33+
@EntryPoint
34+
@InstallIn(SingletonComponent::class)
35+
interface BitwardenGlideEntryPoint {
36+
/**
37+
* Provides access to the [CertificateManager] for mTLS certificate management.
38+
*/
39+
fun certificateManager(): CertificateManager
40+
}
41+
42+
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
43+
// Get CertificateManager from Hilt
44+
val entryPoint = EntryPointAccessors.fromApplication(
45+
context = context.applicationContext,
46+
entryPoint = BitwardenGlideEntryPoint::class.java,
47+
)
48+
val certificateManager = entryPoint.certificateManager()
49+
50+
// Register OkHttpUrlLoader that uses our mTLS OkHttpClient
51+
registry.replace(
52+
GlideUrl::class.java,
53+
InputStream::class.java,
54+
OkHttpUrlLoader.Factory(certificateManager.createMtlsOkHttpClient()),
55+
)
56+
}
57+
58+
override fun isManifestParsingEnabled(): Boolean = false
59+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.x8bit.bitwarden.ui.platform.glide
2+
3+
import org.junit.jupiter.api.Assertions.assertNotNull
4+
import org.junit.jupiter.api.Assertions.assertTrue
5+
import org.junit.jupiter.api.Test
6+
7+
/**
8+
* Test class for [BitwardenAppGlideModule] to verify mTLS configuration is properly applied
9+
* to Glide without requiring a real mTLS server.
10+
*
11+
* These tests verify the module's structure and that it can be instantiated.
12+
* Full integration testing requires running the app and checking logcat for
13+
* "BitwardenGlide" logs when images are loaded.
14+
*/
15+
class BitwardenAppGlideModuleTest {
16+
17+
@Test
18+
fun `BitwardenAppGlideModule should be instantiable`() {
19+
// Verify the module can be created
20+
val module = BitwardenAppGlideModule()
21+
22+
assertNotNull(module)
23+
}
24+
25+
@Test
26+
fun `BitwardenAppGlideModule should have EntryPoint interface for Hilt dependency injection`() {
27+
// Verify the Hilt EntryPoint interface exists for accessing CertificateManager
28+
val entryPointInterface = BitwardenAppGlideModule::class.java
29+
.declaredClasses
30+
.firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" }
31+
32+
assertNotNull(entryPointInterface)
33+
}
34+
35+
@Test
36+
fun `BitwardenGlideEntryPoint should declare certificateManager method`() {
37+
// Verify the EntryPoint has the required method to access CertificateManager
38+
val entryPointInterface = BitwardenAppGlideModule::class.java
39+
.declaredClasses
40+
.firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" }
41+
42+
val methods = requireNotNull(entryPointInterface).declaredMethods
43+
val hasCertificateManagerMethod = methods.any { it.name == "certificateManager" }
44+
45+
assertTrue(hasCertificateManagerMethod)
46+
}
47+
}

gradle/libs.versions.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ bitwardenSdk = "2.0.0-4818-c1e4bb66"
3434
crashlytics = "3.0.6"
3535
detekt = "1.23.8"
3636
firebaseBom = "34.8.0"
37-
glide = "1.0.0-beta01"
37+
glide = "5.0.5"
38+
glideCompose = "1.0.0-beta01"
3839
googleGuava = "33.5.0-jre"
3940
googleProtoBufJava = "4.33.4"
4041
googleProtoBufPlugin = "0.9.6"
@@ -98,7 +99,9 @@ androidx-security-crypto = { module = "androidx.security:security-crypto", versi
9899
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" }
99100
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" }
100101
bitwarden-sdk = { module = "com.bitwarden:sdk-android", version.ref = "bitwardenSdk" }
101-
bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" }
102+
bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" }
103+
bumptech-glide-okhttp = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" }
104+
bumptech-glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
102105
detekt-detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
103106
detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" }
104107
google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }

network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import com.bitwarden.network.interceptor.AuthTokenManager
55
import com.bitwarden.network.interceptor.BaseUrlInterceptor
66
import com.bitwarden.network.interceptor.BaseUrlInterceptors
77
import com.bitwarden.network.interceptor.HeadersInterceptor
8-
import com.bitwarden.network.ssl.BitwardenX509ExtendedKeyManager
98
import com.bitwarden.network.ssl.CertificateProvider
9+
import com.bitwarden.network.ssl.configureSsl
1010
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
1111
import kotlinx.serialization.json.Json
1212
import okhttp3.MediaType.Companion.toMediaType
@@ -15,11 +15,6 @@ import okhttp3.logging.HttpLoggingInterceptor
1515
import retrofit2.Retrofit
1616
import retrofit2.converter.kotlinx.serialization.asConverterFactory
1717
import timber.log.Timber
18-
import java.security.KeyStore
19-
import javax.net.ssl.SSLContext
20-
import javax.net.ssl.TrustManager
21-
import javax.net.ssl.TrustManagerFactory
22-
import javax.net.ssl.X509TrustManager
2318

2419
/**
2520
* Primary implementation of [Retrofits].
@@ -97,7 +92,7 @@ internal class RetrofitsImpl(
9792

9893
private val baseOkHttpClient: OkHttpClient = OkHttpClient.Builder()
9994
.addInterceptor(headersInterceptor)
100-
.configureSsl()
95+
.configureSsl(certificateProvider = certificateProvider)
10196
.build()
10297

10398
private val authenticatedOkHttpClient: OkHttpClient by lazy {
@@ -149,28 +144,5 @@ internal class RetrofitsImpl(
149144
)
150145
.build()
151146

152-
private fun createSslTrustManagers(): Array<TrustManager> =
153-
TrustManagerFactory
154-
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
155-
.apply { init(null as KeyStore?) }
156-
.trustManagers
157-
158-
private fun createSslContext(certificateProvider: CertificateProvider): SSLContext = SSLContext
159-
.getInstance("TLS").apply {
160-
init(
161-
arrayOf(
162-
BitwardenX509ExtendedKeyManager(certificateProvider = certificateProvider),
163-
),
164-
createSslTrustManagers(),
165-
null,
166-
)
167-
}
168-
169-
private fun OkHttpClient.Builder.configureSsl(): OkHttpClient.Builder =
170-
sslSocketFactory(
171-
createSslContext(certificateProvider = certificateProvider).socketFactory,
172-
createSslTrustManagers().first() as X509TrustManager,
173-
)
174-
175147
//endregion Helper properties and functions
176148
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.bitwarden.network.ssl
2+
3+
import okhttp3.OkHttpClient
4+
import java.security.KeyStore
5+
import javax.net.ssl.SSLContext
6+
import javax.net.ssl.TrustManager
7+
import javax.net.ssl.TrustManagerFactory
8+
import javax.net.ssl.X509TrustManager
9+
10+
/**
11+
* Creates an [OkHttpClient] configured with mTLS support using this [CertificateProvider].
12+
*
13+
* The returned client will present the client certificate from this provider during TLS
14+
* handshakes, allowing requests to pass through mTLS checks.
15+
*/
16+
fun CertificateProvider.createMtlsOkHttpClient(): OkHttpClient =
17+
OkHttpClient.Builder()
18+
.configureSsl(certificateProvider = this)
19+
.build()
20+
21+
/**
22+
* Configures the [OkHttpClient.Builder] to use the a `SSLSocketFactory` as provided by the
23+
* [CertificateProvider].
24+
*/
25+
fun OkHttpClient.Builder.configureSsl(
26+
certificateProvider: CertificateProvider,
27+
): OkHttpClient.Builder {
28+
val trustManagers = sslTrustManagers
29+
val sslContext = certificateProvider.createSslContext(trustManagers = trustManagers)
30+
return sslSocketFactory(
31+
sslContext.socketFactory,
32+
trustManagers.first() as X509TrustManager,
33+
)
34+
}
35+
36+
/**
37+
* Creates an [SSLContext] configured with mTLS support using this [CertificateProvider].
38+
*
39+
* The returned SSLContext will present the client certificate from this provider during
40+
* TLS handshakes, enabling mutual TLS authentication.
41+
*/
42+
private fun CertificateProvider.createSslContext(
43+
trustManagers: Array<TrustManager>,
44+
): SSLContext = SSLContext.getInstance("TLS").apply {
45+
init(
46+
arrayOf(
47+
BitwardenX509ExtendedKeyManager(certificateProvider = this@createSslContext),
48+
),
49+
trustManagers,
50+
null,
51+
)
52+
}
53+
54+
/**
55+
* Creates default [TrustManager]s for verifying server certificates.
56+
*
57+
* Uses the system's default trust anchors (trusted CA certificates).
58+
*/
59+
private val sslTrustManagers: Array<TrustManager>
60+
get() = TrustManagerFactory
61+
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
62+
.apply { init(null as KeyStore?) }
63+
.trustManagers

0 commit comments

Comments
 (0)