Skip to content

Commit a85285d

Browse files
committed
Extract and use composable from TrustCertificateActivity
1 parent 9d9bfdb commit a85285d

File tree

4 files changed

+232
-50
lines changed

4 files changed

+232
-50
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package at.bitfire.cert4android
2+
3+
import java.security.cert.X509Certificate
4+
import java.security.spec.MGF1ParameterSpec.SHA1
5+
import java.security.spec.MGF1ParameterSpec.SHA256
6+
import java.text.DateFormat
7+
8+
/**
9+
* Certificate details.
10+
* Create with [CertificateDetails.create] and use with [TrustCertificateDialog]
11+
*/
12+
data class CertificateDetails(
13+
val issuedFor: String? = null,
14+
val issuedBy: String? = null,
15+
val validFrom: String? = null,
16+
val validTo: String? = null,
17+
val sha1: String? = null,
18+
val sha256: String? = null,
19+
) {
20+
companion object {
21+
22+
/**
23+
* Creates [CertificateDetails] from [X509Certificate].
24+
*
25+
* @param cert X509Certificate
26+
* @return CertificateDetails
27+
*/
28+
fun create(cert: X509Certificate): CertificateDetails? {
29+
val subject = cert.subjectAlternativeNames?.let { altNames ->
30+
val sb = StringBuilder()
31+
for (altName in altNames) {
32+
val name = altName[1]
33+
if (name is String)
34+
sb.append("[").append(altName[0]).append("]").append(name).append(" ")
35+
}
36+
sb.toString()
37+
} ?: /* use CN if alternative names are not available */ cert.subjectDN.name
38+
39+
val timeFormatter = DateFormat.getDateInstance(DateFormat.LONG)
40+
return CertificateDetails(
41+
issuedFor = subject,
42+
issuedBy = cert.issuerDN.toString(),
43+
validFrom = timeFormatter.format(cert.notBefore),
44+
validTo = timeFormatter.format(cert.notAfter),
45+
sha1 = "SHA1: " + CertUtils.fingerprint(cert, SHA1.digestAlgorithm),
46+
sha256 = "SHA256: " + CertUtils.fingerprint(cert, SHA256.digestAlgorithm)
47+
)
48+
}
49+
}
50+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/***************************************************************************************************
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
**************************************************************************************************/
4+
5+
package at.bitfire.cert4android
6+
7+
import androidx.annotation.StringRes
8+
import androidx.compose.foundation.clickable
9+
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.Row
11+
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.material3.Card
14+
import androidx.compose.material3.Checkbox
15+
import androidx.compose.material3.MaterialTheme
16+
import androidx.compose.material3.Text
17+
import androidx.compose.material3.TextButton
18+
import androidx.compose.runtime.Composable
19+
import androidx.compose.runtime.getValue
20+
import androidx.compose.runtime.mutableStateOf
21+
import androidx.compose.runtime.remember
22+
import androidx.compose.runtime.setValue
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.res.stringResource
25+
import androidx.compose.ui.unit.dp
26+
27+
/**
28+
* Show this dialog to the user to make a decision on whether to trust the given certificate.
29+
*
30+
* @param certificateDetails Certificate details
31+
* @param onSetTrustDecision Callback to set the users trust decision for given certificate
32+
*/
33+
@Composable
34+
fun TrustCertificateDialog(
35+
certificateDetails: CertificateDetails,
36+
onSetTrustDecision: (Boolean) -> Unit,
37+
modifier: Modifier = Modifier
38+
) {
39+
Card(
40+
modifier = modifier
41+
.fillMaxWidth(),
42+
) {
43+
Column(
44+
modifier = modifier
45+
.padding(16.dp),
46+
) {
47+
Text(
48+
text = stringResource(R.string.trust_certificate_x509_certificate_details),
49+
style = MaterialTheme.typography.titleMedium,
50+
modifier = modifier
51+
.fillMaxWidth()
52+
.padding(bottom = 16.dp),
53+
)
54+
if (certificateDetails.issuedFor != null)
55+
InfoPack(R.string.trust_certificate_issued_for, certificateDetails.issuedFor)
56+
if (certificateDetails.issuedBy != null)
57+
InfoPack(R.string.trust_certificate_issued_by, certificateDetails.issuedBy)
58+
59+
val validFrom = certificateDetails.validFrom
60+
val validTo = certificateDetails.validTo
61+
if (validFrom != null && validTo != null)
62+
InfoPack(
63+
R.string.trust_certificate_validity_period,
64+
stringResource(
65+
R.string.trust_certificate_validity_period_value,
66+
validFrom,
67+
validTo
68+
)
69+
)
70+
71+
val sha1 = certificateDetails.sha1
72+
val sha256 = certificateDetails.sha256
73+
if (sha1 != null || sha256 != null) {
74+
Text(
75+
text = stringResource(R.string.trust_certificate_fingerprints).uppercase(),
76+
style = MaterialTheme.typography.bodyMedium,
77+
modifier = modifier.fillMaxWidth(),
78+
)
79+
80+
if (sha1 != null)
81+
Text(
82+
text = sha1,
83+
style = MaterialTheme.typography.bodyMedium,
84+
modifier = modifier
85+
.fillMaxWidth()
86+
.padding(bottom = 16.dp, top = 4.dp),
87+
)
88+
89+
if (sha256 != null)
90+
Text(
91+
text = sha256,
92+
style = MaterialTheme.typography.bodyMedium,
93+
modifier = modifier
94+
.fillMaxWidth()
95+
.padding(bottom = 16.dp, top = 4.dp),
96+
)
97+
}
98+
99+
var fingerprintVerified by remember { mutableStateOf(false) }
100+
Row(
101+
modifier = modifier
102+
.fillMaxWidth()
103+
.padding(8.dp),
104+
) {
105+
Checkbox(
106+
checked = fingerprintVerified,
107+
onCheckedChange = { fingerprintVerified = it }
108+
)
109+
Text(
110+
text = stringResource(R.string.trust_certificate_fingerprint_verified),
111+
modifier = modifier
112+
.clickable {
113+
fingerprintVerified = !fingerprintVerified
114+
}
115+
.weight(1f)
116+
.padding(bottom = 8.dp),
117+
style = MaterialTheme.typography.bodyMedium
118+
)
119+
}
120+
121+
Row(
122+
modifier = modifier.fillMaxWidth(),
123+
) {
124+
TextButton(
125+
enabled = fingerprintVerified,
126+
onClick = {
127+
onSetTrustDecision(true)
128+
},
129+
modifier = modifier
130+
.weight(1f)
131+
.padding(end = 16.dp)
132+
) { Text(stringResource(R.string.trust_certificate_accept).uppercase()) }
133+
TextButton(
134+
onClick = {
135+
onSetTrustDecision(false)
136+
},
137+
modifier = modifier
138+
.weight(1f)
139+
) { Text(stringResource(R.string.trust_certificate_reject).uppercase()) }
140+
}
141+
}
142+
}
143+
}
144+
145+
@Composable
146+
fun InfoPack(
147+
@StringRes labelStringRes: Int,
148+
text: String,
149+
modifier: Modifier = Modifier
150+
) {
151+
Text(
152+
text = stringResource(labelStringRes).uppercase(),
153+
style = MaterialTheme.typography.bodyMedium,
154+
modifier = modifier
155+
.fillMaxWidth(),
156+
)
157+
Text(
158+
text = text,
159+
style = MaterialTheme.typography.bodySmall,
160+
modifier = modifier
161+
.fillMaxWidth()
162+
.padding(bottom = 16.dp),
163+
)
164+
}
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33

4-
<string name="certificate_notification_connection_security">Connection security</string>
5-
<string name="certificate_notification_user_interaction">Please review the certificate</string>
6-
7-
<string name="service_rejected_temporarily">Certificate temporarily rejected</string>
8-
9-
<!-- TrustCertificateActivity -->
10-
<string name="trust_certificate_unknown_certificate_found">cert4android has encountered an unknown certificate. Do you want to trust it?</string>
4+
<!-- TrustCertificateDialog -->
115
<string name="trust_certificate_x509_certificate_details">X509 certificate details</string>
126
<string name="trust_certificate_issued_for">Issued for</string>
137
<string name="trust_certificate_issued_by">Issued by</string>
@@ -18,6 +12,5 @@
1812
<string name="trust_certificate_accept">Accept</string>
1913
<string name="trust_certificate_reject">Reject</string>
2014
<string name="trust_certificate_reset_info">You can reset all custom certificates in the app settings.</string>
21-
<string name="trust_certificate_press_back_to_reject">Press Back again to reject certificate</string>
2215

2316
</resources>

sample-app/src/main/java/at/bitfire/cert4android/demo/MainActivity.kt

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,23 @@ import androidx.compose.foundation.layout.fillMaxSize
1414
import androidx.compose.foundation.layout.padding
1515
import androidx.compose.foundation.rememberScrollState
1616
import androidx.compose.foundation.verticalScroll
17-
import androidx.compose.material3.AlertDialog
1817
import androidx.compose.material3.Button
1918
import androidx.compose.material3.SnackbarHost
2019
import androidx.compose.material3.SnackbarHostState
2120
import androidx.compose.material3.Text
22-
import androidx.compose.runtime.Composable
2321
import androidx.compose.runtime.LaunchedEffect
24-
import androidx.compose.runtime.collectAsState
2522
import androidx.compose.runtime.livedata.observeAsState
2623
import androidx.compose.runtime.remember
2724
import androidx.compose.ui.Alignment
2825
import androidx.compose.ui.Modifier
2926
import androidx.compose.ui.unit.dp
30-
import androidx.compose.ui.window.DialogProperties
3127
import androidx.lifecycle.AndroidViewModel
3228
import androidx.lifecycle.MutableLiveData
29+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3330
import androidx.lifecycle.viewModelScope
3431
import at.bitfire.cert4android.Cert4Android
32+
import at.bitfire.cert4android.TrustCertificateDialog
33+
import at.bitfire.cert4android.CertificateDetails
3534
import at.bitfire.cert4android.CustomCertManager
3635
import at.bitfire.cert4android.CustomCertStore
3736
import kotlinx.coroutines.CompletableDeferred
@@ -42,49 +41,21 @@ import kotlinx.coroutines.launch
4241
import org.apache.http.conn.ssl.AllowAllHostnameVerifier
4342
import org.apache.http.conn.ssl.StrictHostnameVerifier
4443
import java.net.URL
45-
import java.security.cert.X509Certificate
4644
import javax.net.ssl.HttpsURLConnection
4745

46+
/**
47+
* Example implementation for testing and to showcase usage of [CustomCertManager].
48+
*/
4849
class MainActivity : ComponentActivity() {
4950

5051
private val model by viewModels<Model>()
5152

52-
5353
override fun onCreate(savedInstanceState: Bundle?) {
5454
super.onCreate(savedInstanceState)
5555
setContent {
5656
Cert4Android.theme {
57-
@Composable
58-
fun TrustDecisionDialog(cert: X509Certificate, onDismiss: (Boolean) -> Unit) {
59-
AlertDialog(
60-
onDismissRequest = { onDismiss(false) },
61-
title = { Text(text = "Trust Decision") },
62-
text = { Text("Do you trust this certificate?\n\n ${cert.subjectDN.name}") },
63-
confirmButton = {
64-
Button(onClick = {
65-
onDismiss(true)
66-
}) {
67-
Text("Trust")
68-
}
69-
},
70-
dismissButton = {
71-
Button(onClick = {
72-
onDismiss(false)
73-
}) {
74-
Text("Distrust")
75-
}
76-
},
77-
properties = DialogProperties(dismissOnClickOutside = false)
78-
)
79-
}
80-
8157
val snackBarHostState = remember { SnackbarHostState() }
82-
83-
val certificateState = model.certificateFlow.collectAsState()
84-
val certificate = certificateState.value
85-
86-
if (certificate != null)
87-
TrustDecisionDialog(certificate, model::setUserDecision)
58+
val certificateDetails = model.certificateDetailsFlow.collectAsStateWithLifecycle().value
8859

8960
Box(Modifier.fillMaxSize()) {
9061
Column(
@@ -146,6 +117,10 @@ class MainActivity : ComponentActivity() {
146117
}
147118
}
148119
}
120+
121+
if (certificateDetails != null)
122+
TrustCertificateDialog(certificateDetails, model::registerUserDecision)
123+
149124
SnackbarHost(
150125
snackBarHostState,
151126
modifier = Modifier.align(Alignment.BottomCenter)
@@ -160,15 +135,15 @@ class MainActivity : ComponentActivity() {
160135

161136
val resultMessage = MutableLiveData<String>()
162137

163-
private val _certificateFlow = MutableStateFlow<X509Certificate?>(null)
164-
val certificateFlow: StateFlow<X509Certificate?> = _certificateFlow
138+
private val _certificateDetailsFlow = MutableStateFlow<CertificateDetails?>(null)
139+
val certificateDetailsFlow: StateFlow<CertificateDetails?> = _certificateDetailsFlow
165140

166141
@Volatile
167142
private var userDecision: CompletableDeferred<Boolean> = CompletableDeferred()
168143

169-
fun setUserDecision(decision: Boolean) {
144+
fun registerUserDecision(decision: Boolean) {
170145
userDecision.complete(decision)
171-
_certificateFlow.value = null
146+
_certificateDetailsFlow.value = null
172147
}
173148

174149

@@ -196,8 +171,8 @@ class MainActivity : ComponentActivity() {
196171
// Reset user decision
197172
userDecision = CompletableDeferred()
198173

199-
// Show TrustDecisionDialog with certificate to user
200-
_certificateFlow.value = cert
174+
// Show TrustDecisionDialog with certificate details to user
175+
_certificateDetailsFlow.value = CertificateDetails.create(cert)
201176

202177
// Wait for user decision and return it
203178
userDecision.await()

0 commit comments

Comments
 (0)