|
1 | 1 | package at.bitfire.cert4android
|
2 | 2 |
|
3 | 3 | import android.annotation.SuppressLint
|
4 |
| -import android.app.PendingIntent |
5 | 4 | import android.content.Context
|
6 |
| -import android.content.Intent |
7 |
| -import androidx.core.app.NotificationCompat |
8 |
| -import androidx.core.app.TaskStackBuilder |
| 5 | +import kotlinx.coroutines.runBlocking |
9 | 6 | import kotlinx.coroutines.suspendCancellableCoroutine
|
10 | 7 | import java.security.cert.X509Certificate
|
11 | 8 | import kotlin.coroutines.Continuation
|
@@ -41,113 +38,46 @@ class UserDecisionRegistry private constructor(
|
41 | 38 | * Thread-safe, can handle multiple requests for various certificates and/or the same certificate at once.
|
42 | 39 | *
|
43 | 40 | * @param cert certificate to ask user about
|
44 |
| - * @param appInForeground whether the app is currently in foreground = whether it can directly launch an Activity |
| 41 | + * @param getUserDecision anonymous function to retrieve user decision |
45 | 42 | * @return *true* if the user explicitly trusts the certificate, *false* if unknown or untrusted
|
46 | 43 | */
|
47 |
| - suspend fun check(cert: X509Certificate, appInForeground: Boolean): Boolean = suspendCancellableCoroutine { cont -> |
48 |
| - // check whether we're able to retrieve user feedback (= start an Activity and/or show a notification) |
49 |
| - val notificationsPermitted = NotificationUtils.notificationsPermitted(context) |
50 |
| - val userDecisionPossible = appInForeground || notificationsPermitted |
51 |
| - |
52 |
| - if (userDecisionPossible) { |
53 |
| - // User decision possible → remember request in pendingDecisions so that a later decision will be applied to this request |
54 |
| - |
55 |
| - cont.invokeOnCancellation { |
56 |
| - // remove from pending decisions on cancellation |
57 |
| - synchronized(pendingDecisions) { |
58 |
| - pendingDecisions[cert]?.remove(cont) |
59 |
| - } |
60 |
| - |
61 |
| - val nm = NotificationUtils.createChannels(context) |
62 |
| - nm.cancel(CertUtils.getTag(cert), NotificationUtils.ID_CERT_DECISION) |
63 |
| - } |
64 |
| - |
65 |
| - val requestDecision: Boolean |
| 44 | + suspend fun check(cert: X509Certificate, getUserDecision: suspend (X509Certificate) -> Boolean): Boolean = suspendCancellableCoroutine { cont -> |
| 45 | + cont.invokeOnCancellation { |
| 46 | + // remove from pending decisions on cancellation |
66 | 47 | synchronized(pendingDecisions) {
|
67 |
| - if (pendingDecisions.containsKey(cert)) { |
68 |
| - // There are already pending decisions for this request, just add our request |
69 |
| - pendingDecisions[cert]!! += cont |
70 |
| - requestDecision = false |
71 |
| - } else { |
72 |
| - // First decision for this certificate, show UI |
73 |
| - pendingDecisions[cert] = mutableListOf(cont) |
74 |
| - requestDecision = true |
75 |
| - } |
| 48 | + pendingDecisions[cert]?.remove(cont) |
76 | 49 | }
|
| 50 | + } |
77 | 51 |
|
78 |
| - if (requestDecision) |
79 |
| - requestDecision(cert, launchActivity = appInForeground, showNotification = notificationsPermitted) |
80 |
| - |
81 |
| - } else { |
82 |
| - // We're not able to retrieve user feedback, directly reject request |
83 |
| - Cert4Android.log.warning("App not in foreground and missing notification permission, rejecting certificate") |
84 |
| - cont.resume(false) |
| 52 | + val requestDecision: Boolean |
| 53 | + synchronized(pendingDecisions) { |
| 54 | + if (pendingDecisions.containsKey(cert)) { |
| 55 | + // There are already pending decisions for this request, just add our request |
| 56 | + pendingDecisions[cert]!! += cont |
| 57 | + requestDecision = false |
| 58 | + } else { |
| 59 | + // First decision for this certificate, show UI |
| 60 | + pendingDecisions[cert] = mutableListOf(cont) |
| 61 | + requestDecision = true |
| 62 | + } |
85 | 63 | }
|
| 64 | + |
| 65 | + if (requestDecision) |
| 66 | + runBlocking { |
| 67 | + requestDecision(cert, getUserDecision) |
| 68 | + } |
86 | 69 | }
|
87 | 70 |
|
88 | 71 | /**
|
89 |
| - * Starts UI for retrieving feedback (accept/reject) for a certificate from the user. |
| 72 | + * ... |
90 | 73 | *
|
91 |
| - * Ensure that required permissions are granted/conditions are met before setting [launchActivity] |
92 |
| - * or [showNotification]. |
93 |
| - * |
94 |
| - * @param cert certificate to ask user about |
95 |
| - * @param launchActivity whether to launch a [TrustCertificateActivity] |
96 |
| - * @param showNotification whether to show a certificate notification (caller must check notification permissions before passing *true*) |
97 |
| - * |
98 |
| - * @throws IllegalArgumentException when both [launchActivity] and [showNotification] are *false* |
99 | 74 | */
|
100 |
| - @SuppressLint("MissingPermission") |
101 |
| - internal fun requestDecision(cert: X509Certificate, launchActivity: Boolean, showNotification: Boolean) { |
102 |
| - if (!launchActivity && !showNotification) |
103 |
| - throw IllegalArgumentException("User decision requires certificate Activity and/or notification") |
104 |
| - |
105 |
| - val rawCert = cert.encoded |
106 |
| - val decisionIntent = Intent(context, TrustCertificateActivity::class.java).apply { |
107 |
| - putExtra(TrustCertificateActivity.EXTRA_CERTIFICATE, rawCert) |
108 |
| - } |
109 |
| - |
110 |
| - if (showNotification) { |
111 |
| - val rejectIntent = Intent(context, TrustCertificateActivity::class.java).apply { |
112 |
| - putExtra(TrustCertificateActivity.EXTRA_CERTIFICATE, rawCert) |
113 |
| - putExtra(TrustCertificateActivity.EXTRA_TRUSTED, false) |
114 |
| - } |
115 |
| - |
116 |
| - val id = rawCert.contentHashCode() |
117 |
| - val notify = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_CERTIFICATES) |
118 |
| - .setPriority(NotificationCompat.PRIORITY_HIGH) |
119 |
| - .setSmallIcon(R.drawable.ic_lock_open_white) |
120 |
| - .setContentTitle(context.getString(R.string.certificate_notification_connection_security)) |
121 |
| - .setContentText(context.getString(R.string.certificate_notification_user_interaction)) |
122 |
| - .setSubText(cert.subjectDN.name) |
123 |
| - .setCategory(NotificationCompat.CATEGORY_SERVICE) |
124 |
| - .setContentIntent( |
125 |
| - TaskStackBuilder.create(context) |
126 |
| - .addNextIntent(decisionIntent) |
127 |
| - .getPendingIntent(id, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) |
128 |
| - ) |
129 |
| - .setDeleteIntent( |
130 |
| - TaskStackBuilder.create(context) |
131 |
| - .addNextIntent(rejectIntent) |
132 |
| - .getPendingIntent(id + 1, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) |
133 |
| - ) |
134 |
| - .build() |
135 |
| - |
136 |
| - val nm = NotificationUtils.createChannels(context) |
137 |
| - nm.notify(CertUtils.getTag(cert), NotificationUtils.ID_CERT_DECISION, notify) |
138 |
| - } |
139 |
| - |
140 |
| - if (launchActivity) { |
141 |
| - decisionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
142 |
| - context.startActivity(decisionIntent) |
143 |
| - } |
| 75 | + internal suspend fun requestDecision(cert: X509Certificate, getUserDecision: suspend (X509Certificate) -> Boolean) { |
| 76 | + val userDecision = getUserDecision(cert) |
| 77 | + onUserDecision(cert, userDecision) |
144 | 78 | }
|
145 | 79 |
|
146 | 80 | fun onUserDecision(cert: X509Certificate, trusted: Boolean) {
|
147 |
| - // cancel notification |
148 |
| - val nm = NotificationUtils.createChannels(context) |
149 |
| - nm.cancel(CertUtils.getTag(cert), NotificationUtils.ID_CERT_DECISION) |
150 |
| - |
151 | 81 | // save decision
|
152 | 82 | val customCertStore = CustomCertStore.getInstance(context)
|
153 | 83 | if (trusted)
|
|
0 commit comments