mirror of
https://github.com/bitwarden/android.git
synced 2026-02-03 18:17:54 -06:00
[BWA-182] Add mTLS support for Glide image loading (#6125)
Co-authored-by: David Perez <david@livefront.com>
This commit is contained in:
parent
5531b478d3
commit
9f82b42e36
@ -260,6 +260,8 @@ dependencies {
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.bitwarden.sdk)
|
||||
implementation(libs.bumptech.glide)
|
||||
implementation(libs.bumptech.glide.okhttp)
|
||||
ksp(libs.bumptech.glide.compiler)
|
||||
implementation(libs.google.hilt.android)
|
||||
ksp(libs.google.hilt.compiler)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.network.ssl.createMtlsOkHttpClient
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
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 java.io.InputStream
|
||||
|
||||
/**
|
||||
* 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 the [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 = context.applicationContext,
|
||||
entryPoint = BitwardenGlideEntryPoint::class.java,
|
||||
)
|
||||
val certificateManager = entryPoint.certificateManager()
|
||||
|
||||
// Register OkHttpUrlLoader that uses our mTLS OkHttpClient
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
OkHttpUrlLoader.Factory(certificateManager.createMtlsOkHttpClient()),
|
||||
)
|
||||
}
|
||||
|
||||
override fun isManifestParsingEnabled(): Boolean = false
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
|
||||
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
|
||||
* 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(module)
|
||||
}
|
||||
|
||||
@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(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" }
|
||||
|
||||
val methods = requireNotNull(entryPointInterface).declaredMethods
|
||||
val hasCertificateManagerMethod = methods.any { it.name == "certificateManager" }
|
||||
|
||||
assertTrue(hasCertificateManagerMethod)
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,8 @@ bitwardenSdk = "2.0.0-4818-c1e4bb66"
|
||||
crashlytics = "3.0.6"
|
||||
detekt = "1.23.8"
|
||||
firebaseBom = "34.8.0"
|
||||
glide = "1.0.0-beta01"
|
||||
glide = "5.0.5"
|
||||
glideCompose = "1.0.0-beta01"
|
||||
googleGuava = "33.5.0-jre"
|
||||
googleProtoBufJava = "4.33.4"
|
||||
googleProtoBufPlugin = "0.9.6"
|
||||
@ -98,7 +99,9 @@ androidx-security-crypto = { module = "androidx.security:security-crypto", versi
|
||||
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" }
|
||||
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 = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" }
|
||||
bumptech-glide-okhttp = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" }
|
||||
bumptech-glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
|
||||
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" }
|
||||
|
||||
@ -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.configureSsl
|
||||
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@ -15,11 +15,6 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
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
|
||||
|
||||
/**
|
||||
* Primary implementation of [Retrofits].
|
||||
@ -97,7 +92,7 @@ internal class RetrofitsImpl(
|
||||
|
||||
private val baseOkHttpClient: OkHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(headersInterceptor)
|
||||
.configureSsl()
|
||||
.configureSsl(certificateProvider = certificateProvider)
|
||||
.build()
|
||||
|
||||
private val authenticatedOkHttpClient: OkHttpClient by lazy {
|
||||
@ -149,28 +144,5 @@ internal class RetrofitsImpl(
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun createSslTrustManagers(): Array<TrustManager> =
|
||||
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,
|
||||
)
|
||||
|
||||
//endregion Helper properties and functions
|
||||
}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
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 [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 =
|
||||
OkHttpClient.Builder()
|
||||
.configureSsl(certificateProvider = this)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Configures the [OkHttpClient.Builder] to use the a `SSLSocketFactory` as provided by the
|
||||
* [CertificateProvider].
|
||||
*/
|
||||
fun OkHttpClient.Builder.configureSsl(
|
||||
certificateProvider: CertificateProvider,
|
||||
): OkHttpClient.Builder {
|
||||
val trustManagers = sslTrustManagers
|
||||
val sslContext = certificateProvider.createSslContext(trustManagers = trustManagers)
|
||||
return sslSocketFactory(
|
||||
sslContext.socketFactory,
|
||||
trustManagers.first() as 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.
|
||||
*/
|
||||
private fun CertificateProvider.createSslContext(
|
||||
trustManagers: Array<TrustManager>,
|
||||
): SSLContext = SSLContext.getInstance("TLS").apply {
|
||||
init(
|
||||
arrayOf(
|
||||
BitwardenX509ExtendedKeyManager(certificateProvider = this@createSslContext),
|
||||
),
|
||||
trustManagers,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default [TrustManager]s for verifying server certificates.
|
||||
*
|
||||
* Uses the system's default trust anchors (trusted CA certificates).
|
||||
*/
|
||||
private val sslTrustManagers: Array<TrustManager>
|
||||
get() = TrustManagerFactory
|
||||
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
.apply { init(null as KeyStore?) }
|
||||
.trustManagers
|
||||
Loading…
x
Reference in New Issue
Block a user