|
1 | 1 | package com.credman.cmwallet.data.repository |
2 | 2 |
|
3 | | -import android.graphics.Bitmap |
4 | | -import android.os.Build |
| 3 | +import android.graphics.BitmapFactory |
5 | 4 | import android.util.Log |
6 | | -import androidx.core.graphics.drawable.toBitmap |
7 | 5 | import androidx.credentials.DigitalCredential |
8 | 6 | import androidx.credentials.ExperimentalDigitalCredentialApi |
| 7 | +import androidx.credentials.registry.digitalcredentials.mdoc.MdocEntry |
| 8 | +import androidx.credentials.registry.digitalcredentials.mdoc.MdocField |
| 9 | +import androidx.credentials.registry.digitalcredentials.mdoc.MdocInlineIssuanceEntry |
| 10 | +import androidx.credentials.registry.digitalcredentials.openid4vp.OpenId4VpRegistry |
| 11 | +import androidx.credentials.registry.digitalcredentials.sdjwt.SdJwtClaim |
| 12 | +import androidx.credentials.registry.digitalcredentials.sdjwt.SdJwtEntry |
| 13 | +import androidx.credentials.registry.digitalcredentials.sdjwt.SdJwtInlineIssuanceEntry |
9 | 14 | import androidx.credentials.registry.provider.RegisterCredentialsRequest |
10 | 15 | import androidx.credentials.registry.provider.RegistryManager |
11 | | -import com.credman.cmwallet.R |
| 16 | +import androidx.credentials.registry.provider.digitalcredentials.DigitalCredentialEntry |
| 17 | +import androidx.credentials.registry.provider.digitalcredentials.InlineIssuanceEntry |
| 18 | +import androidx.credentials.registry.provider.digitalcredentials.VerificationEntryDisplayProperties |
| 19 | +import androidx.credentials.registry.provider.digitalcredentials.VerificationFieldDisplayProperties |
| 20 | +import com.credman.cmwallet.CmWalletApplication |
12 | 21 | import com.credman.cmwallet.data.model.CredentialDisplayData |
13 | 22 | import com.credman.cmwallet.data.model.CredentialItem |
14 | 23 | import com.credman.cmwallet.data.model.CredentialKeySoftware |
@@ -58,10 +67,10 @@ class CredentialRepository { |
58 | 67 |
|
59 | 68 | val credentials: Flow<List<CredentialItem>> = combinedCredentials() |
60 | 69 |
|
61 | | - val credentialRegistryDatabase: Flow<ByteArray> = flow { |
| 70 | + val credentialRegistryDatabase: Flow<OpenId4VpRegistry> = flow { |
62 | 71 | emitAll(combinedCredentials().map { credentials -> |
63 | 72 | Log.i("CredentialRepository", "Updating flow with ${credentials.size}") |
64 | | - createRegistryDatabase(credentials) |
| 73 | + createRegistry(credentials) |
65 | 74 | }) |
66 | 75 | } |
67 | 76 |
|
@@ -163,151 +172,137 @@ class CredentialRepository { |
163 | 172 | put(ICON, iconJson) |
164 | 173 | } |
165 | 174 |
|
166 | | - private fun constructJwtForRegistry( |
| 175 | + private fun constructJwtClaims( |
167 | 176 | rawJwt: JSONObject, |
168 | 177 | displayConfig: CredentialConfigurationSdJwtVc?, |
169 | | - path: JSONArray, |
170 | | - ): JSONObject { |
171 | | - val result = JSONObject() |
| 178 | + claims: MutableList<SdJwtClaim>, |
| 179 | + path: List<String> |
| 180 | + ) { |
172 | 181 | for (key in rawJwt.keys()) { |
173 | 182 | val v = rawJwt[key] |
174 | | - val currPath = JSONArray(path.toString()) // Make a copy |
175 | | - currPath.put(key) |
| 183 | + val currPath = path.toMutableList() // Make a copy |
| 184 | + currPath.add(key) |
176 | 185 | if (v is JSONObject) { |
177 | | - result.put( |
178 | | - key, |
179 | | - constructJwtForRegistry(v, displayConfig, currPath) |
| 186 | + constructJwtClaims( |
| 187 | + v, |
| 188 | + displayConfig, |
| 189 | + claims, |
| 190 | + currPath |
180 | 191 | ) |
181 | 192 | } else { |
182 | | - result.put( |
183 | | - key, |
184 | | - JSONObject().apply { |
185 | | - val displayName = displayConfig?.claims?.firstOrNull{ |
186 | | - JSONArray(it.path) == currPath |
187 | | - }?.display?.first()?.name ?: key |
188 | | - putOpt(DISPLAY, displayName) |
189 | | - putOpt(VALUE, v) |
190 | | - } |
| 193 | + val displayName = displayConfig?.claims?.firstOrNull{ |
| 194 | + JSONArray(it.path) == currPath |
| 195 | + }?.display?.first()?.name ?: currPath.joinToString(separator = ".") |
| 196 | + claims.add( |
| 197 | + SdJwtClaim( |
| 198 | + path = currPath, |
| 199 | + value = v, |
| 200 | + fieldDisplayPropertySet = setOf(VerificationFieldDisplayProperties( |
| 201 | + displayName = displayName, |
| 202 | + )), |
| 203 | +// isSelectivelyDisclosable = TODO() |
| 204 | + ) |
191 | 205 | ) |
192 | 206 | } |
193 | 207 | } |
194 | | - return result |
195 | 208 | } |
196 | 209 |
|
197 | | - /** |
198 | | - * Credential Registry has the following format: |
199 | | - * |
200 | | - * |---------------------------------------| |
201 | | - * |--- (Int) offset of credential json ---| |
202 | | - * |--------- (Byte Array) Icon 1 ---------| |
203 | | - * |--------- (Byte Array) Icon 2 ---------| |
204 | | - * |------------- More Icons... -----------| |
205 | | - * |----------- Credential Json -----------| // See assets/paymentcreds.json as an example |
206 | | - * |---------------------------------------| |
207 | | - */ |
208 | 210 | @OptIn(ExperimentalEncodingApi::class) |
209 | | - private fun createRegistryDatabase(items: List<CredentialItem>): ByteArray { |
210 | | - val out = ByteArrayOutputStream() |
211 | | - |
212 | | - val iconMap: Map<String, RegistryIcon> = items.associate { |
213 | | - Pair( |
214 | | - it.id, |
215 | | - RegistryIcon(it.displayData.icon?.decodeBase64() ?: ByteArray(0)) |
216 | | - ) |
217 | | - } |
218 | | - // Write the offset to the json |
219 | | - val jsonOffset = 4 + iconMap.values.sumOf { it.iconValue.size } |
220 | | - val buffer = ByteBuffer.allocate(4) |
221 | | - buffer.order(ByteOrder.LITTLE_ENDIAN) |
222 | | - buffer.putInt(jsonOffset) |
223 | | - out.write(buffer.array()) |
| 211 | + private fun createRegistry(items: List<CredentialItem>): OpenId4VpRegistry { |
| 212 | + val credentialEntries: MutableList<DigitalCredentialEntry> = mutableListOf() |
224 | 213 |
|
225 | | - // Write the icons |
226 | | - var currIconOffset = 4 |
227 | | - iconMap.values.forEach { |
228 | | - it.iconOffset = currIconOffset |
229 | | - out.write(it.iconValue) |
230 | | - currIconOffset += it.iconValue.size |
231 | | - } |
232 | | - |
233 | | - val mdocCredentials = JSONObject() |
234 | | - val sdJwtCredentials = JSONObject() |
235 | 214 | items.forEach { item -> |
236 | 215 | when (item.config) { |
237 | 216 | is CredentialConfigurationSdJwtVc -> { |
238 | | - val credJson = JSONObject() |
239 | | - credJson.putCommon(item.id, item.displayData, iconMap) |
240 | 217 | val sdJwtVc = SdJwt(item.credentials.first().credential, (item.credentials.first().key as CredentialKeySoftware).privateKey) |
241 | 218 | val rawJwt = sdJwtVc.verifiedResult.processedJwt |
242 | | - val jwtWithDisplay = constructJwtForRegistry(rawJwt, item.config, JSONArray()) |
243 | | - // TODO: what do we do with non-user-friendly claims such as iss, aud? |
244 | | - credJson.put(PATHS, jwtWithDisplay) |
245 | | - val vctType = rawJwt["vct"] as String |
246 | | - when (val current = sdJwtCredentials.opt(vctType) ?: JSONArray()) { |
247 | | - is JSONArray -> sdJwtCredentials.put(vctType, current.put(credJson)) |
248 | | - else -> throw IllegalStateException("Unexpected type ${current::class.java}") |
249 | | - } |
250 | | - |
| 219 | + val claims = mutableListOf<SdJwtClaim>() |
| 220 | + constructJwtClaims(rawJwt, item.config, claims, emptyList()) |
| 221 | + credentialEntries.add(SdJwtEntry( |
| 222 | + verifiableCredentialType = rawJwt["vct"] as String, |
| 223 | + claims = claims, |
| 224 | + entryDisplayPropertySet = setOf(VerificationEntryDisplayProperties( |
| 225 | + title = item.displayData.title, |
| 226 | + subtitle = item.displayData.subtitle, |
| 227 | + icon = item.displayData.icon?.decodeBase64()?.let { |
| 228 | + BitmapFactory.decodeByteArray(it, 0, it.size) |
| 229 | + } ?: CmWalletApplication.walletIcon |
| 230 | + )), |
| 231 | + id = item.id, |
| 232 | + )) |
251 | 233 | } |
252 | 234 | is CredentialConfigurationMDoc -> { |
253 | | - val credJson = JSONObject() |
254 | | - credJson.putCommon(item.id, item.displayData, iconMap) |
255 | 235 | val mdoc = MDoc(item.credentials.first().credential.decodeBase64UrlNoPadding()) |
| 236 | + val mdocFields = mutableListOf<MdocField>() |
256 | 237 | if (mdoc.issuerSignedNamespaces.isNotEmpty()) { |
257 | | - val pathJson = JSONObject() |
258 | 238 | mdoc.issuerSignedNamespaces.forEach { (namespace, elements) -> |
259 | | - val namespaceJson = JSONObject() |
260 | 239 | elements.forEach { (element, value) -> |
261 | | - val namespaceDataJson = JSONObject() |
262 | | - namespaceDataJson.putOpt(VALUE, value) |
263 | 240 | val displayName = item.config.claims?.firstOrNull{ |
264 | 241 | it.path[0] == namespace && it.path[1] == element |
265 | 242 | }?.display?.first()?.name!! |
266 | | - namespaceDataJson.put(DISPLAY, displayName) |
267 | | -// namespaceDataJson.putOpt( |
268 | | -// DISPLAY_VALUE, |
269 | | -// namespaceData.value.displayValue |
270 | | -// ) |
271 | | - namespaceJson.put(element, namespaceDataJson) |
272 | | - } |
273 | | - pathJson.put(namespace, namespaceJson) |
274 | | - } |
275 | | - credJson.put(PATHS, pathJson) |
276 | | - } |
277 | | - if (Build.VERSION.SDK_INT >= 33) { |
278 | | - mdocCredentials.append(item.config.doctype, credJson) |
279 | | - } else { |
280 | | - when (val current = mdocCredentials.opt(item.config.doctype)) { |
281 | | - is JSONArray -> { |
282 | | - mdocCredentials.put(item.config.doctype, current.put(credJson)) |
283 | | - } |
284 | | - |
285 | | - null -> { |
286 | | - mdocCredentials.put( |
287 | | - item.config.doctype, |
288 | | - JSONArray().put(credJson) |
| 243 | + mdocFields.add( |
| 244 | + MdocField( |
| 245 | + namespace = namespace, |
| 246 | + identifier = element, |
| 247 | + fieldValue = value, |
| 248 | + fieldDisplayPropertySet = setOf( |
| 249 | + VerificationFieldDisplayProperties( |
| 250 | + displayName = displayName, |
| 251 | +// displayValue = namespaceData.value.displayValue |
| 252 | + ) |
| 253 | + ) |
| 254 | + ) |
289 | 255 | ) |
290 | 256 | } |
291 | | - |
292 | | - else -> throw IllegalStateException( |
293 | | - "Unexpected namespaced data that's" + |
294 | | - " not a JSONArray. Instead it is ${current::class.java}" |
295 | | - ) |
296 | 257 | } |
297 | 258 | } |
| 259 | + credentialEntries.add(MdocEntry( |
| 260 | + docType = item.config.doctype, |
| 261 | + fields = mdocFields, |
| 262 | + entryDisplayPropertySet = setOf(VerificationEntryDisplayProperties( |
| 263 | + title = item.displayData.title, |
| 264 | + subtitle = item.displayData.subtitle, |
| 265 | + icon = item.displayData.icon?.decodeBase64()?.let { |
| 266 | + BitmapFactory.decodeByteArray(it, 0, it.size) |
| 267 | + } ?: CmWalletApplication.walletIcon |
| 268 | + )), |
| 269 | + id = item.id, |
| 270 | + )) |
298 | 271 | } |
299 | 272 |
|
300 | 273 | is CredentialConfigurationUnknownFormat -> TODO() |
301 | 274 | } |
302 | 275 | } |
303 | | - val registryCredentials = JSONObject() |
304 | | - registryCredentials.put("mso_mdoc", mdocCredentials) |
305 | | - registryCredentials.put("dc+sd-jwt", sdJwtCredentials) |
306 | | - val registryJson = JSONObject() |
307 | | - registryJson.put(CREDENTIALS, registryCredentials) |
308 | | - Log.d(TAG, "Credential to be registered: ${registryJson.toString(2)}") |
309 | | - out.write(registryJson.toString().toByteArray()) |
310 | | - return out.toByteArray() |
| 276 | + return OpenId4VpRegistry( |
| 277 | + credentialEntries = credentialEntries, |
| 278 | + inlineIssuanceEntries = emptyList(), |
| 279 | +// listOf( |
| 280 | +// MdocInlineIssuanceEntry( |
| 281 | +// id = "Issuance", |
| 282 | +// display = InlineIssuanceEntry.InlineIssuanceDisplayProperties( |
| 283 | +// subtitle = "Mobile Drivers License, State Id, and Others", |
| 284 | +// ), |
| 285 | +// supportedMdocs = setOf( |
| 286 | +// MdocInlineIssuanceEntry.SupportedMdoc( |
| 287 | +// "eu.europa.ec.eudi.pid.1" |
| 288 | +// ), |
| 289 | +// MdocInlineIssuanceEntry.SupportedMdoc( |
| 290 | +// "org.iso.18013.5.1.mDL1" |
| 291 | +// ), |
| 292 | +// ) |
| 293 | +// ), |
| 294 | +// SdJwtInlineIssuanceEntry( |
| 295 | +// id = "sd-jwt-issuance", |
| 296 | +// display = InlineIssuanceEntry.InlineIssuanceDisplayProperties( |
| 297 | +// subtitle = "Mobile Drivers License, State Id, and Others", |
| 298 | +// ), |
| 299 | +// supportedSdJwts = setOf( |
| 300 | +// SdJwtInlineIssuanceEntry.SupportedSdJwt("urn:openid:interop:id:1") |
| 301 | +// ) |
| 302 | +// ) |
| 303 | +// ), |
| 304 | + id = "openid4vp1.0", |
| 305 | + ) |
311 | 306 | } |
312 | 307 |
|
313 | 308 | companion object { |
|
0 commit comments