Skip to content

Commit 42d883e

Browse files
authored
Decouple CustomCertManager from Android (#82)
* Decouple CustomCertManager from Android * Add copyright header
1 parent 670095e commit 42d883e

File tree

8 files changed

+285
-163
lines changed

8 files changed

+285
-163
lines changed

lib/src/androidTest/java/at/bitfire/cert4android/CustomCertManagerTest.kt

Lines changed: 0 additions & 89 deletions
This file was deleted.

lib/src/androidTest/java/at/bitfire/cert4android/OkhttpTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class OkhttpTest {
5858

5959
// set cert4android TrustManager and HostnameVerifier
6060
val certManager = CustomCertManager(
61-
context,
61+
CustomCertStore.getInstance(context),
6262
trustSystemCerts = true,
6363
appInForeground = null
6464
)

lib/src/androidTest/java/at/bitfire/cert4android/TestCertificates.kt

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@
1010

1111
package at.bitfire.cert4android
1212

13-
import android.net.SSLCertificateSocketFactory
14-
import org.apache.http.conn.ssl.AllowAllHostnameVerifier
15-
import java.net.URL
1613
import java.security.cert.CertificateFactory
1714
import java.security.cert.X509Certificate
18-
import javax.net.ssl.HttpsURLConnection
19-
import javax.net.ssl.X509TrustManager
2015

2116
/**
2217
* Provides certificates for testing.
@@ -71,39 +66,4 @@ object TestCertificates {
7166

7267
fun testCert() = certFactory.generateCertificate(RAW_TEST_CERT.byteInputStream()) as X509Certificate
7368

74-
75-
/**
76-
* Get the certificates of a site (bypassing all trusted checks).
77-
*
78-
* @param url the URL to get the certificates from
79-
* @return the certificates of the site
80-
*/
81-
fun getSiteCertificates(url: URL): List<X509Certificate> {
82-
val conn = url.openConnection() as HttpsURLConnection
83-
try {
84-
conn.hostnameVerifier = AllowAllHostnameVerifier()
85-
conn.sslSocketFactory = object : SSLCertificateSocketFactory(1000) {
86-
init {
87-
setTrustManagers(arrayOf(object : X509TrustManager {
88-
override fun checkClientTrusted(
89-
chain: Array<out X509Certificate?>?,
90-
authType: String?
91-
) { /* OK */ }
92-
override fun checkServerTrusted(
93-
chain: Array<out X509Certificate?>?,
94-
authType: String?
95-
) { /* OK */ }
96-
override fun getAcceptedIssuers(): Array<out X509Certificate?>? = emptyArray()
97-
}))
98-
}
99-
}
100-
conn.inputStream.read()
101-
val certs = mutableListOf<X509Certificate>()
102-
conn.serverCertificates.forEach { certs += it as X509Certificate }
103-
return certs
104-
} finally {
105-
conn.disconnect()
106-
}
107-
}
108-
10969
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* SPDX-License-Identifier: MPL-2.0
9+
*/
10+
11+
package at.bitfire.cert4android
12+
13+
import kotlinx.coroutines.flow.StateFlow
14+
import java.security.cert.X509Certificate
15+
16+
interface CertStore {
17+
18+
/**
19+
* Removes user (dis-)trust decisions for all certificates.
20+
*/
21+
fun clearUserDecisions()
22+
23+
/**
24+
* Determines whether a certificate chain is trusted.
25+
*/
26+
fun isTrusted(chain: Array<X509Certificate>, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow<Boolean>?): Boolean
27+
28+
/**
29+
* Determines whether a certificate has been explicitly accepted by the user. In this case,
30+
* we can ignore an invalid host name for that certificate.
31+
*/
32+
fun isTrustedByUser(cert: X509Certificate): Boolean
33+
34+
/**
35+
* Sets this certificate as trusted.
36+
*/
37+
fun setTrustedByUser(cert: X509Certificate)
38+
39+
/**
40+
* Sets this certificate as untrusted.
41+
*/
42+
fun setUntrustedByUser(cert: X509Certificate)
43+
44+
}

lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
package at.bitfire.cert4android
1212

1313
import android.annotation.SuppressLint
14-
import android.content.Context
1514
import kotlinx.coroutines.flow.StateFlow
1615
import java.security.cert.CertificateException
1716
import java.security.cert.X509Certificate
@@ -29,17 +28,14 @@ import javax.net.ssl.X509TrustManager
2928
*/
3029
@SuppressLint("CustomX509TrustManager")
3130
class CustomCertManager @JvmOverloads constructor(
32-
context: Context,
31+
private val certStore: CertStore,
3332
val trustSystemCerts: Boolean = true,
3433
var appInForeground: StateFlow<Boolean>?
3534
): X509TrustManager {
3635

3736
private val logger
3837
get() = Logger.getLogger(javaClass.name)
3938

40-
val certStore = CustomCertStore.getInstance(context)
41-
42-
4339
@Throws(CertificateException::class)
4440
override fun checkClientTrusted(chain: Array<X509Certificate>?, authType: String?) {
4541
throw CertificateException("cert4android doesn't validate client certificates")

lib/src/main/java/at/bitfire/cert4android/CustomCertStore.kt

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,7 @@ import javax.net.ssl.X509TrustManager
3131
class CustomCertStore internal constructor(
3232
private val context: Context,
3333
private val userTimeout: Long = 60000L
34-
) {
35-
36-
companion object {
37-
38-
private const val KEYSTORE_DIR = "KeyStore"
39-
private const val KEYSTORE_NAME = "KeyStore.bks"
40-
41-
@SuppressLint("StaticFieldLeak") // we only store the applicationContext, so this is safe
42-
private var instance: CustomCertStore? = null
43-
44-
@Synchronized
45-
fun getInstance(context: Context): CustomCertStore {
46-
instance?.let {
47-
return it
48-
}
49-
50-
val newInstance = CustomCertStore(context.applicationContext)
51-
instance = newInstance
52-
return newInstance
53-
}
54-
55-
}
34+
): CertStore {
5635

5736
private val logger
5837
get() = Logger.getLogger(javaClass.name)
@@ -82,7 +61,7 @@ class CustomCertStore internal constructor(
8261
}
8362

8463
@Synchronized
85-
fun clearUserDecisions() {
64+
override fun clearUserDecisions() {
8665
logger.info("Clearing user-(dis)trusted certificates")
8766

8867
for (alias in userKeyStore.aliases())
@@ -96,7 +75,7 @@ class CustomCertStore internal constructor(
9675
/**
9776
* Determines whether a certificate chain is trusted.
9877
*/
99-
fun isTrusted(chain: Array<X509Certificate>, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow<Boolean>?): Boolean {
78+
override fun isTrusted(chain: Array<X509Certificate>, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow<Boolean>?): Boolean {
10079
if (chain.isEmpty())
10180
throw IllegalArgumentException("Certificate chain must not be empty")
10281
val cert = chain[0]
@@ -146,11 +125,11 @@ class CustomCertStore internal constructor(
146125
* we can ignore an invalid host name for that certificate.
147126
*/
148127
@Synchronized
149-
fun isTrustedByUser(cert: X509Certificate): Boolean =
128+
override fun isTrustedByUser(cert: X509Certificate): Boolean =
150129
userKeyStore.getCertificateAlias(cert) != null
151130

152131
@Synchronized
153-
fun setTrustedByUser(cert: X509Certificate) {
132+
override fun setTrustedByUser(cert: X509Certificate) {
154133
val alias = CertUtils.getTag(cert)
155134
logger.info("Trusted by user: ${cert.subjectDN.name} ($alias)")
156135

@@ -161,7 +140,7 @@ class CustomCertStore internal constructor(
161140
}
162141

163142
@Synchronized
164-
fun setUntrustedByUser(cert: X509Certificate) {
143+
override fun setUntrustedByUser(cert: X509Certificate) {
165144
logger.info("Distrusted by user: ${cert.subjectDN.name}")
166145

167146
// find certificate
@@ -202,4 +181,25 @@ class CustomCertStore internal constructor(
202181
}
203182
}
204183

184+
companion object {
185+
186+
private const val KEYSTORE_DIR = "KeyStore"
187+
private const val KEYSTORE_NAME = "KeyStore.bks"
188+
189+
@SuppressLint("StaticFieldLeak") // we only store the applicationContext, so this is safe
190+
private var instance: CustomCertStore? = null
191+
192+
@Synchronized
193+
fun getInstance(context: Context): CustomCertStore {
194+
instance?.let {
195+
return it
196+
}
197+
198+
val newInstance = CustomCertStore(context.applicationContext)
199+
instance = newInstance
200+
return newInstance
201+
}
202+
203+
}
204+
205205
}

0 commit comments

Comments
 (0)