Skip to content

Commit 571b836

Browse files
authored
[PM-16157] Support self-host servers using TLS with Client Authentication (mTLS) (#4486)
1 parent fd26472 commit 571b836

File tree

21 files changed

+1713
-26
lines changed

21 files changed

+1713
-26
lines changed

app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/EnvironmentUrlDataJson.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
77
* Represents URLs for various Bitwarden domains.
88
*
99
* @property base The overall base URL.
10+
* @property keyUri A Uri containing the alias and host of the key used for mutual TLS.
1011
* @property api Separate base URL for the "/api" domain (if applicable).
1112
* @property identity Separate base URL for the "/identity" domain (if applicable).
1213
* @property icon Separate base URL for the icon domain (if applicable).
@@ -19,6 +20,9 @@ data class EnvironmentUrlDataJson(
1920
@SerialName("base")
2021
val base: String,
2122

23+
@SerialName("keyUri")
24+
val keyUri: String? = null,
25+
2226
@SerialName("api")
2327
val api: String? = null,
2428

@@ -51,6 +55,7 @@ data class EnvironmentUrlDataJson(
5155
*/
5256
val DEFAULT_LEGACY_US: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
5357
base = "https://vault.bitwarden.com",
58+
keyUri = null,
5459
api = "https://api.bitwarden.com",
5560
identity = "https://identity.bitwarden.com",
5661
icon = "https://icons.bitwarden.net",
@@ -71,6 +76,7 @@ data class EnvironmentUrlDataJson(
7176
*/
7277
val DEFAULT_LEGACY_EU: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
7378
base = "https://vault.bitwarden.eu",
79+
keyUri = null,
7480
api = "https://api.bitwarden.eu",
7581
identity = "https://identity.bitwarden.eu",
7682
icon = "https://icons.bitwarden.eu",

app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
1414
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventServiceImpl
1515
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
1616
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushServiceImpl
17+
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
18+
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManagerImpl
19+
import com.x8bit.bitwarden.data.platform.manager.KeyManager
20+
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
1721
import dagger.Module
1822
import dagger.Provides
1923
import dagger.hilt.InstallIn
@@ -70,20 +74,33 @@ object PlatformNetworkModule {
7074
@Singleton
7175
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()
7276

77+
@Provides
78+
@Singleton
79+
fun provideSslManager(
80+
keyManager: KeyManager,
81+
environmentRepository: EnvironmentRepository,
82+
): SslManager =
83+
SslManagerImpl(
84+
keyManager = keyManager,
85+
environmentRepository = environmentRepository,
86+
)
87+
7388
@Provides
7489
@Singleton
7590
fun provideRetrofits(
7691
authTokenInterceptor: AuthTokenInterceptor,
7792
baseUrlInterceptors: BaseUrlInterceptors,
7893
headersInterceptor: HeadersInterceptor,
7994
refreshAuthenticator: RefreshAuthenticator,
95+
sslManager: SslManager,
8096
json: Json,
8197
): Retrofits =
8298
RetrofitsImpl(
8399
authTokenInterceptor = authTokenInterceptor,
84100
baseUrlInterceptors = baseUrlInterceptors,
85101
headersInterceptor = headersInterceptor,
86102
refreshAuthenticator = refreshAuthenticator,
103+
sslManager = sslManager,
87104
json = json,
88105
)
89106

app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthToke
66
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
77
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
88
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor
9+
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
910
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
1011
import kotlinx.serialization.json.Json
1112
import okhttp3.MediaType.Companion.toMediaType
@@ -14,6 +15,9 @@ import okhttp3.logging.HttpLoggingInterceptor
1415
import retrofit2.Retrofit
1516
import retrofit2.converter.kotlinx.serialization.asConverterFactory
1617
import timber.log.Timber
18+
import javax.net.ssl.SSLContext
19+
import javax.net.ssl.TrustManager
20+
import javax.net.ssl.X509TrustManager
1721

1822
/**
1923
* Primary implementation of [Retrofits].
@@ -24,6 +28,7 @@ class RetrofitsImpl(
2428
headersInterceptor: HeadersInterceptor,
2529
refreshAuthenticator: RefreshAuthenticator,
2630
json: Json,
31+
private val sslManager: SslManager,
2732
) : Retrofits {
2833
//region Authenticated Retrofits
2934

@@ -67,6 +72,10 @@ class RetrofitsImpl(
6772
baseClient
6873
.newBuilder()
6974
.addInterceptor(loggingInterceptor)
75+
.setSslSocketFactory(
76+
sslContext = sslManager.sslContext,
77+
trustManagers = sslManager.trustManagers,
78+
)
7079
.build(),
7180
)
7281
.build()
@@ -93,6 +102,10 @@ class RetrofitsImpl(
93102
.newBuilder()
94103
.authenticator(refreshAuthenticator)
95104
.addInterceptor(authTokenInterceptor)
105+
.setSslSocketFactory(
106+
sslContext = sslManager.sslContext,
107+
trustManagers = sslManager.trustManagers,
108+
)
96109
.build()
97110
}
98111

@@ -133,9 +146,22 @@ class RetrofitsImpl(
133146
.newBuilder()
134147
.addInterceptor(baseUrlInterceptor)
135148
.addInterceptor(loggingInterceptor)
149+
.setSslSocketFactory(
150+
sslContext = sslManager.sslContext,
151+
trustManagers = sslManager.trustManagers,
152+
)
136153
.build(),
137154
)
138155
.build()
139156

157+
private fun OkHttpClient.Builder.setSslSocketFactory(
158+
sslContext: SSLContext,
159+
trustManagers: Array<TrustManager>,
160+
): OkHttpClient.Builder =
161+
sslSocketFactory(
162+
sslContext.socketFactory,
163+
trustManagers.first() as X509TrustManager,
164+
)
165+
140166
//endregion Helper properties and functions
141167
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
2+
3+
import javax.net.ssl.SSLContext
4+
import javax.net.ssl.TrustManager
5+
6+
/**
7+
* Interface for managing SSL connections.
8+
*/
9+
interface SslManager {
10+
11+
/**
12+
* The SSL context to use for SSL connections.
13+
*/
14+
val sslContext: SSLContext
15+
16+
/**
17+
* The trust managers to use for SSL connections.
18+
*/
19+
val trustManagers: Array<TrustManager>
20+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
2+
3+
import android.net.Uri
4+
import androidx.annotation.VisibleForTesting
5+
import androidx.annotation.WorkerThread
6+
import androidx.core.net.toUri
7+
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
8+
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
9+
import com.x8bit.bitwarden.data.platform.manager.KeyManager
10+
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
11+
import java.net.Socket
12+
import java.security.KeyStore
13+
import java.security.Principal
14+
import java.security.PrivateKey
15+
import java.security.cert.X509Certificate
16+
import javax.net.ssl.SSLContext
17+
import javax.net.ssl.TrustManager
18+
import javax.net.ssl.TrustManagerFactory
19+
import javax.net.ssl.X509ExtendedKeyManager
20+
21+
/**
22+
* Primary implementation of [SslManager].
23+
*/
24+
class SslManagerImpl(
25+
private val keyManager: KeyManager,
26+
private val environmentRepository: EnvironmentRepository,
27+
) : SslManager {
28+
29+
/*
30+
This property must only be accessed from a background thread. Accessing this property from
31+
the main thread will result in an exception being thrown when retrieving the mutual TLS
32+
certificate from [KeyManager].
33+
*/
34+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
35+
@get:WorkerThread
36+
internal val mutualTlsCertificate: MutualTlsCertificate?
37+
get() {
38+
val keyUri = getKeyUri()
39+
?: return null
40+
41+
val host = MutualTlsKeyHost
42+
.entries
43+
.find { it.name == keyUri.authority }
44+
?: return null
45+
46+
val alias = keyUri.path
47+
?.trim('/')
48+
?.takeUnless { it.isEmpty() }
49+
?: return null
50+
51+
return keyManager.getMutualTlsCertificateChain(
52+
alias = alias,
53+
host = host,
54+
)
55+
}
56+
57+
override val trustManagers: Array<TrustManager>
58+
get() = TrustManagerFactory
59+
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
60+
.apply { init(null as KeyStore?) }
61+
.trustManagers
62+
63+
override val sslContext: SSLContext
64+
get() = SSLContext
65+
.getInstance("TLS")
66+
.apply {
67+
init(
68+
arrayOf(X509ExtendedKeyManagerImpl()),
69+
trustManagers,
70+
null,
71+
)
72+
}
73+
74+
private fun getKeyUri(): Uri? = environmentRepository
75+
.environment
76+
.environmentUrlData
77+
.keyUri
78+
?.toUri()
79+
80+
private inner class X509ExtendedKeyManagerImpl : X509ExtendedKeyManager() {
81+
override fun chooseClientAlias(
82+
keyType: Array<out String>?,
83+
issuers: Array<out Principal>?,
84+
socket: Socket?,
85+
): String = mutualTlsCertificate?.alias ?: ""
86+
87+
override fun getCertificateChain(
88+
alias: String?,
89+
): Array<X509Certificate>? =
90+
mutualTlsCertificate
91+
?.certificateChain
92+
?.toTypedArray()
93+
94+
override fun getPrivateKey(alias: String?): PrivateKey? =
95+
mutualTlsCertificate
96+
?.privateKey
97+
98+
//region Unused server side methods
99+
override fun getServerAliases(
100+
alias: String?,
101+
issuers: Array<out Principal>?,
102+
): Array<String> = arrayOf()
103+
104+
override fun getClientAliases(
105+
keyType: String?,
106+
issuers: Array<out Principal>?,
107+
): Array<String> = emptyArray()
108+
109+
override fun chooseServerAlias(
110+
alias: String?,
111+
issuers: Array<out Principal>?,
112+
socket: Socket?,
113+
): String = ""
114+
//endregion Unused server side methods
115+
}
116+
}

app/src/main/java/com/x8bit/bitwarden/data/platform/manager/KeyManager.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.x8bit.bitwarden.data.platform.manager
22

3-
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ImportPrivateKeyResult
3+
import androidx.annotation.WorkerThread
4+
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
45
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
56
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
67

@@ -29,7 +30,10 @@ interface KeyManager {
2930

3031
/**
3132
* Retrieve the certificate chain for the selected mTLS key.
33+
*
34+
* Must be called from a background thread to prevent possible deadlocks on the main thread.
3235
*/
36+
@WorkerThread
3337
fun getMutualTlsCertificateChain(
3438
alias: String,
3539
host: MutualTlsKeyHost,

app/src/main/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package com.x8bit.bitwarden.data.platform.manager
33
import android.content.Context
44
import android.security.KeyChain
55
import android.security.KeyChainException
6-
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ImportPrivateKeyResult
76
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
87
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
8+
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
99
import timber.log.Timber
1010
import java.io.IOException
1111
import java.security.KeyStore
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.x8bit.bitwarden.data.platform.datasource.disk.model
1+
package com.x8bit.bitwarden.data.platform.manager.model
22

33
/**
44
* Models the result of importing a private key.

0 commit comments

Comments
 (0)