Skip to content

Commit

Permalink
bitwarden#4416 add possibility to select a client certificate for mTLS
Browse files Browse the repository at this point in the history
  • Loading branch information
rohm1 committed Dec 18, 2024
1 parent e2b93ec commit 61b9170
Show file tree
Hide file tree
Showing 20 changed files with 339 additions and 3 deletions.
11 changes: 11 additions & 0 deletions app/src/main/java/com/x8bit/bitwarden/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityComp
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.repository.ChoosePrivateKeyAliasCallback
import com.x8bit.bitwarden.data.platform.repository.KeyChainRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
Expand Down Expand Up @@ -53,6 +55,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager

@Inject
lateinit var keyChainRepository: KeyChainRepository

override fun onCreate(savedInstanceState: Bundle?) {
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
Expand All @@ -66,6 +71,9 @@ class MainActivity : AppCompatActivity() {
)
}

// inject current activity in keychain repository for later use
keyChainRepository.configure(this)

// Within the app the language will change dynamically and will be managed
// by the OS, but we need to ensure we properly set the language when
// upgrading from older versions that handle this differently.
Expand Down Expand Up @@ -102,6 +110,9 @@ class MainActivity : AppCompatActivity() {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },
navController = navController,
choosePrivateKeyAlias = { callback: ChoosePrivateKeyAliasCallback ->
keyChainRepository.choosePrivateKeyAlias(callback)
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
* Represents URLs for various Bitwarden domains.
*
* @property base The overall base URL.
* @property keyAlias A key alias to use for connections with the server.
* @property api Separate base URL for the "/api" domain (if applicable).
* @property identity Separate base URL for the "/identity" domain (if applicable).
* @property icon Separate base URL for the icon domain (if applicable).
Expand All @@ -19,6 +20,9 @@ data class EnvironmentUrlDataJson(
@SerialName("base")
val base: String,

@SerialName("keyAlias")
val keyAlias: String? = null,

@SerialName("api")
val api: String? = null,

Expand Down Expand Up @@ -51,6 +55,7 @@ data class EnvironmentUrlDataJson(
*/
val DEFAULT_LEGACY_US: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
base = "https://vault.bitwarden.com",
keyAlias = null,
api = "https://api.bitwarden.com",
identity = "https://identity.bitwarden.com",
icon = "https://icons.bitwarden.net",
Expand All @@ -71,6 +76,7 @@ data class EnvironmentUrlDataJson(
*/
val DEFAULT_LEGACY_EU: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
base = "https://vault.bitwarden.eu",
keyAlias = null,
api = "https://api.bitwarden.eu",
identity = "https://identity.bitwarden.eu",
icon = "https://icons.bitwarden.eu",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.di

import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
Expand All @@ -14,6 +15,9 @@ import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.util.TLSHelper
import com.x8bit.bitwarden.data.platform.repository.KeyChainRepository
import com.x8bit.bitwarden.data.platform.repository.KeyChainRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -70,6 +74,16 @@ object PlatformNetworkModule {
@Singleton
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()

@Provides
@Singleton
fun providesKeyChainRepository(environmentDiskSource: EnvironmentDiskSource): KeyChainRepository =
KeyChainRepositoryImpl(environmentDiskSource = environmentDiskSource)

@Provides
@Singleton
fun providesTlsHelper(keyChainRepository: KeyChainRepository): TLSHelper =
TLSHelper(keyChainRepository = keyChainRepository)

@Provides
@Singleton
fun provideRetrofits(
Expand All @@ -78,13 +92,15 @@ object PlatformNetworkModule {
headersInterceptor: HeadersInterceptor,
refreshAuthenticator: RefreshAuthenticator,
json: Json,
tlsHelper: TLSHelper,
): Retrofits =
RetrofitsImpl(
authTokenInterceptor = authTokenInterceptor,
baseUrlInterceptors = baseUrlInterceptors,
headersInterceptor = headersInterceptor,
refreshAuthenticator = refreshAuthenticator,
json = json,
tlsHelper = tlsHelper
)

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlI
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import com.x8bit.bitwarden.data.platform.datasource.network.util.TLSHelper
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
Expand All @@ -24,6 +25,7 @@ class RetrofitsImpl(
headersInterceptor: HeadersInterceptor,
refreshAuthenticator: RefreshAuthenticator,
json: Json,
tlsHelper: TLSHelper
) : Retrofits {
//region Authenticated Retrofits

Expand Down Expand Up @@ -84,7 +86,7 @@ class RetrofitsImpl(
}

private val baseOkHttpClient: OkHttpClient =
OkHttpClient.Builder()
tlsHelper.setupOkHttpClientSSLSocketFactory(OkHttpClient.Builder())
.addInterceptor(headersInterceptor)
.build()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.x8bit.bitwarden.data.platform.datasource.network.util

import com.x8bit.bitwarden.data.platform.repository.KeyChainRepository
import java.net.Socket
import java.security.KeyStore
import java.security.Principal
import java.security.PrivateKey
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Named
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttpClient

class TLSHelper @Inject constructor(
@Named("keyChainRepository") private val keyChainRepository: KeyChainRepository,
) {
fun setupOkHttpClientSSLSocketFactory(builder: OkHttpClient.Builder): OkHttpClient.Builder {
val trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers

val sslContext = SSLContext.getInstance("TLS")
sslContext.init(arrayOf(getMTLSKeyManagerForOKHTTP()), trustManagers, null)

builder.sslSocketFactory(sslContext.socketFactory, trustManagers[0] as X509TrustManager)

return builder
}

private fun getMTLSKeyManagerForOKHTTP(): X509ExtendedKeyManager {
return object : X509ExtendedKeyManager() {
override fun getClientAliases(
p0: String?,
p1: Array<out Principal>?,
): Array<String> {
return emptyArray()
}

override fun chooseClientAlias(
p0: Array<out String>?,
p1: Array<out Principal>?,
p2: Socket?,
): String {
return ""
}

override fun getServerAliases(
p0: String?,
p1: Array<out Principal>?,
): Array<String> {
return arrayOf()
}

override fun chooseServerAlias(
p0: String?,
p1: Array<out Principal>?,
p2: Socket?,
): String {
return ""
}

override fun getCertificateChain(p0: String?): Array<X509Certificate>? {
return keyChainRepository.getCertificateChain()
}

override fun getPrivateKey(p0: String?): PrivateKey? {
return keyChainRepository.getPrivateKey()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.x8bit.bitwarden.data.platform.repository

import android.security.KeyChainAliasCallback

interface ChoosePrivateKeyAliasCallback {
fun getCallback(): KeyChainAliasCallback
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.platform.repository

import android.security.KeyChainAliasCallback

class ChoosePrivateKeyAliasCallbackImpl constructor(val callback: (String?) -> Unit) :
ChoosePrivateKeyAliasCallback {
override fun getCallback(): KeyChainAliasCallback {
return KeyChainAliasCallback {
callback(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.platform.repository

import android.app.Activity
import java.security.PrivateKey
import java.security.cert.X509Certificate

interface KeyChainRepository {
fun configure(activity: Activity)

fun choosePrivateKeyAlias(callback: ChoosePrivateKeyAliasCallback)

fun getPrivateKey(): PrivateKey?

fun getCertificateChain(): Array<X509Certificate>?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.x8bit.bitwarden.data.platform.repository

import android.app.Activity
import android.security.KeyChain
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
import timber.log.Timber
import java.security.PrivateKey
import java.security.cert.X509Certificate
import javax.inject.Inject

class KeyChainRepositoryImpl @Inject constructor(
environmentDiskSource: EnvironmentDiskSource,
) : KeyChainRepository {
private var activity: Activity? = null
private var alias: String? = null
private var key: PrivateKey? = null
private var chain: Array<X509Certificate>? = null

init {
alias =
environmentDiskSource.preAuthEnvironmentUrlData.toEnvironmentUrlsOrDefault().environmentUrlData.keyAlias
}

override fun configure(activity: Activity) {
this.activity = activity
}

override fun choosePrivateKeyAlias(callback: ChoosePrivateKeyAliasCallback) {
if (activity == null) {
Timber.tag(this.javaClass.name)
.d("activity is not set yet -- trying to choose private key alias")
return
}

KeyChain.choosePrivateKeyAlias(activity!!, { a ->
callback.getCallback().alias(a)
alias = a
}, null, null, null, alias)
}

override fun getPrivateKey(): PrivateKey? {
if (key == null && activity != null && !alias.isNullOrEmpty()) {
key = try {
KeyChain.getPrivateKey(activity!!, alias!!)
} catch (e: Exception) {
null
}
}

return key
}

override fun getCertificateChain(): Array<X509Certificate>? {
if (chain == null && activity != null && !alias.isNullOrEmpty()) {
chain = try {
KeyChain.getCertificateChain(activity!!, alias!!)
} catch (e: Exception) {
null
}
}

return chain
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.navOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.data.platform.repository.ChoosePrivateKeyAliasCallback
import com.x8bit.bitwarden.ui.auth.feature.checkemail.checkEmailDestination
import com.x8bit.bitwarden.ui.auth.feature.checkemail.navigateToCheckEmail
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.completeRegistrationDestination
Expand Down Expand Up @@ -50,6 +51,7 @@ const val AUTH_GRAPH_ROUTE: String = "auth_graph"
@Suppress("LongMethod")
fun NavGraphBuilder.authGraph(
navController: NavHostController,
choosePrivateKeyAlias: (ChoosePrivateKeyAliasCallback) -> Unit,
) {
navigation(
startDestination = LANDING_ROUTE,
Expand Down Expand Up @@ -173,6 +175,7 @@ fun NavGraphBuilder.authGraph(
)
environmentDestination(
onNavigateBack = { navController.popBackStack() },
choosePrivateKeyAlias = choosePrivateKeyAlias,
)
masterPasswordHintDestination(
onNavigateBack = { navController.popBackStack() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.environment
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.data.platform.repository.ChoosePrivateKeyAliasCallback
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions

private const val ENVIRONMENT_ROUTE = "environment"
Expand All @@ -12,11 +13,12 @@ private const val ENVIRONMENT_ROUTE = "environment"
*/
fun NavGraphBuilder.environmentDestination(
onNavigateBack: () -> Unit,
choosePrivateKeyAlias: (ChoosePrivateKeyAliasCallback) -> Unit,
) {
composableWithSlideTransitions(
route = ENVIRONMENT_ROUTE,
) {
EnvironmentScreen(onNavigateBack = onNavigateBack)
EnvironmentScreen(onNavigateBack = onNavigateBack, choosePrivateKeyAlias = choosePrivateKeyAlias)
}
}

Expand Down
Loading

0 comments on commit 61b9170

Please sign in to comment.