Skip to content

Commit dee1a72

Browse files
committed
fix(MacOS): Combine KeychainStore with default certificate store
1 parent 05b3113 commit dee1a72

File tree

3 files changed

+93
-3
lines changed

3 files changed

+93
-3
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.artemchep.keyguard.crypto.ssl
2+
3+
import com.artemchep.keyguard.platform.CurrentPlatform
4+
import okhttp3.OkHttpClient
5+
import okhttp3.internal.platform.Platform
6+
import java.security.KeyStore
7+
import java.security.cert.CertificateException
8+
import java.security.cert.X509Certificate
9+
import javax.net.ssl.TrustManagerFactory
10+
import javax.net.ssl.X509TrustManager
11+
12+
fun OkHttpClient.Builder.installPlatformTrustManager() =
13+
when (CurrentPlatform) {
14+
is com.artemchep.keyguard.platform.Platform.Desktop.MacOS -> installMacOsTrustManager()
15+
else -> this
16+
}
17+
18+
fun OkHttpClient.Builder.installMacOsTrustManager() = installHybridTrustManager {
19+
getMacOsTrustManager()
20+
}
21+
22+
private inline fun OkHttpClient.Builder.installHybridTrustManager(
23+
fallback: () -> X509TrustManager = { getDefaultTrustManager() },
24+
primary: () -> X509TrustManager,
25+
): OkHttpClient.Builder {
26+
val primaryTm = runCatching {
27+
primary()
28+
}.getOrElse { e ->
29+
// Could not get the platform specific
30+
// trust manager.
31+
e.printStackTrace()
32+
return this
33+
}
34+
val fallbackTm = fallback()
35+
36+
// Combine with a new trust manager and set it
37+
// as the OkHTTPs socket factory.
38+
val hybridTm = HybridTrustManager(primaryTm, fallbackTm)
39+
val hybridSslSocketFactory = Platform.get().newSslSocketFactory(hybridTm)
40+
return sslSocketFactory(
41+
sslSocketFactory = hybridSslSocketFactory,
42+
trustManager = hybridTm,
43+
)
44+
}
45+
46+
private fun getDefaultTrustManager() = Platform.get().platformTrustManager()
47+
48+
private fun getMacOsTrustManager() = run {
49+
val appleFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
50+
val appleKeyStore = KeyStore.getInstance("KeychainStore")
51+
appleKeyStore.load(null, null)
52+
appleFactory.init(appleKeyStore)
53+
val appleTm = appleFactory.trustManagers
54+
.first { it is X509TrustManager } as X509TrustManager
55+
appleTm
56+
}
57+
58+
/**
59+
* A TrustManager that delegates to a primary manager, and falls back
60+
* to a secondary manager if the primary fails validation.
61+
*/
62+
private class HybridTrustManager(
63+
private val primary: X509TrustManager,
64+
private val secondary: X509TrustManager,
65+
) : X509TrustManager {
66+
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
67+
try {
68+
primary.checkServerTrusted(chain, authType)
69+
} catch (_: CertificateException) {
70+
secondary.checkServerTrusted(chain, authType)
71+
}
72+
}
73+
74+
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
75+
try {
76+
primary.checkClientTrusted(chain, authType)
77+
} catch (_: CertificateException) {
78+
secondary.checkClientTrusted(chain, authType)
79+
}
80+
}
81+
82+
override fun getAcceptedIssuers(): Array<X509Certificate> {
83+
// Combine the list of trusted CAs from both sources so the server
84+
// knows we accept certificates from either.
85+
return primary.acceptedIssuers + secondary.acceptedIssuers
86+
}
87+
}

common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ import com.artemchep.keyguard.crypto.CipherEncryptorImpl
416416
import com.artemchep.keyguard.crypto.CryptoGeneratorJvm
417417
import com.artemchep.keyguard.crypto.FileEncryptorImpl
418418
import com.artemchep.keyguard.crypto.KeyPairGeneratorJvm
419+
import com.artemchep.keyguard.crypto.ssl.installPlatformTrustManager
419420
import com.artemchep.keyguard.platform.CurrentPlatform
420421
import com.artemchep.keyguard.platform.util.isRelease
421422
import com.artemchep.keyguard.provider.bitwarden.api.BitwardenPersona
@@ -1575,6 +1576,7 @@ fun globalModuleJvm() = DI.Module(
15751576
bindSingleton<OkHttpClient> {
15761577
OkHttpClient
15771578
.Builder()
1579+
.installPlatformTrustManager()
15781580
.apply {
15791581
if (!isRelease) {
15801582
val logRepository: LogRepository = instance()

desktopApp/build.gradle.kts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,14 @@ compose.desktop {
102102
"-Dapple.awt.application.appearance=system",
103103
)
104104

105-
// Support system Keychain as thrust store:
105+
// Support system Keychain as trust store:
106106
// https://github.com/AChep/keyguard-app/issues/1227
107107
val platformJvmArgs = when {
108108
Os.isFamily(Os.FAMILY_MAC) -> {
109+
// We resort to runtime trust manager:
110+
// - KeychainStore is empty.
111+
// - KeychainStore-ROOT does not exist.
109112
arrayOf(
110-
"-Djavax.net.ssl.trustStore=/dev/null",
111-
"-Djavax.net.ssl.trustStoreType=KeychainStore",
112113
)
113114
}
114115
Os.isFamily(Os.FAMILY_WINDOWS) -> {

0 commit comments

Comments
 (0)