Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-16157] Support self-host servers using TLS with Client Authentication (mTLS) #4486

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading