Skip to content

Conversation

@aj-rosado
Copy link
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/BWA-182

📔 Objective

This PR adds mTLS (mutual TLS) support to Glide image loading.
This allows image requests to pass through Cloudflare's mTLS validation.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Logo
Checkmarx One – Scan Summary & Detailsa42a233d-ac81-4037-b06b-c3e2235e394a

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Nov 5, 2025

Codecov Report

❌ Patch coverage is 32.69231% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.94%. Comparing base (7324be0) to head (9421e94).
⚠️ Report is 28 commits behind head on main.

Files with missing lines Patch % Lines
...arden/ui/platform/glide/BitwardenAppGlideModule.kt 12.50% 28 Missing ⚠️
...arden/network/ssl/CertificateProviderExtensions.kt 53.33% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6125      +/-   ##
==========================================
- Coverage   84.96%   84.94%   -0.03%     
==========================================
  Files         724      725       +1     
  Lines       52758    52898     +140     
  Branches     7659     7681      +22     
==========================================
+ Hits        44826    44934     +108     
- Misses       5249     5281      +32     
  Partials     2683     2683              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

private class OkHttpDataFetcher(
private val client: OkHttpClient,
private val url: GlideUrl,
) : com.bumptech.glide.load.data.DataFetcher<InputStream> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an import for this instead of having the full path here?

private var call: Call? = null

override fun loadData(
priority: com.bumptech.glide.Priority,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here

* This wraps our CertificateProvider to handle client certificate selection during
* the TLS handshake.
*/
private fun createSslContext(certificateProvider: CertificateProvider): SSLContext =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exact logic already exists in the app. Can we pull it out as a common extension method on CertificateProvider and use it in multiple places. I think we can probably do that with a lot of this.

fun CertificateProvider.createSslContext(): SSLContext =
    SSLContext.getInstance("TLS").apply {
        init(
            arrayOf(
                CertificateProviderKeyManager(certificateProvider = certificateProvider),
            ),
            createSslTrustManagers(),
            null,
        )
    }

fun CertificateProvider.createMtlsOkHttpClient(): OkHttpClient {
    val sslContext = createSslContext(certificateProvider)
    val trustManagers = createSslTrustManagers()

    return OkHttpClient.Builder()
        .sslSocketFactory(
            sslContext.socketFactory,
            trustManagers.first() as X509TrustManager,
        )
        .build()
    }

private fun createSslTrustManagers(): Array<TrustManager> =
    TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm())
        .apply { init(null as KeyStore?) }
        .trustManagers

import com.bumptech.glide.module.AppGlideModule
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make sure these use Junit 5

override fun getDataClass(): Class<InputStream> = InputStream::class.java

override fun getDataSource(): com.bumptech.glide.load.DataSource =
com.bumptech.glide.load.DataSource.REMOTE
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just pull in the import for this?

val sslContext = createSslContext()
val trustManagers = createSslTrustManagers()

return OkHttpClient.Builder()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

width: Int,
height: Int,
options: Options,
): ModelLoader.LoadData<InputStream>? {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this return type nonnull.

if (response?.isSuccessful == true) {
callback.onDataReady(response.body?.byteStream())
} else {
callback.onLoadFailed(Exception("HTTP ${response?.code}: ${response?.message}"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify some of this.

  • We can make a localCall that is nonnull to avoid weird nullability issues.
  • Move the try catch around only the function that can throw.
  • We can also use the HttpException from Glide.
            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))
            }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants