|
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 |
@@ -44,110 +41,43 @@ class UserDecisionRegistry private constructor( |
44 | 41 | * @param appInForeground whether the app is currently in foreground = whether it can directly launch an Activity |
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