From 960053adb7bafdeaaddff6200e1ebac69ab1a712 Mon Sep 17 00:00:00 2001 From: QZHelen Date: Thu, 21 Nov 2024 02:14:53 +0000 Subject: [PATCH] Parse a vci request into structured object --- .../credman/cmwallet/CmWalletApplication.kt | 2 + .../credman/cmwallet/openid4vci/OpenId4VCI.kt | 39 ++++ .../cmwallet/openid4vci/RequestDataModel.kt | 190 ++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 app/src/main/java/com/credman/cmwallet/openid4vci/OpenId4VCI.kt create mode 100644 app/src/main/java/com/credman/cmwallet/openid4vci/RequestDataModel.kt diff --git a/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt b/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt index 522a464..540e251 100644 --- a/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt +++ b/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt @@ -11,6 +11,8 @@ import com.credman.cmwallet.data.model.CredentialItem import com.credman.cmwallet.data.repository.CredentialRepository import com.credman.cmwallet.data.room.Credential import com.credman.cmwallet.data.room.CredentialDatabase +import com.credman.cmwallet.openid4vci.MdocCredConfigsSupportedItem +import com.credman.cmwallet.openid4vci.OpenId4VCI import com.google.android.gms.identitycredentials.IdentityCredentialClient import com.google.android.gms.identitycredentials.IdentityCredentialManager import com.google.android.gms.identitycredentials.RegisterCreationOptionsRequest diff --git a/app/src/main/java/com/credman/cmwallet/openid4vci/OpenId4VCI.kt b/app/src/main/java/com/credman/cmwallet/openid4vci/OpenId4VCI.kt new file mode 100644 index 0000000..63a7b15 --- /dev/null +++ b/app/src/main/java/com/credman/cmwallet/openid4vci/OpenId4VCI.kt @@ -0,0 +1,39 @@ +package com.credman.cmwallet.openid4vci + +import org.json.JSONObject + +class OpenId4VCI(val request: String) { + val requestJson: JSONObject = JSONObject(request) + + val credentialIssuer: String + val credentialConfigurationIds: List + val credentialConfigurationsSupportedMap: Map + + init { + require(requestJson.has(CREDENTIAL_ISSUER)) { "Issuance request must contain $CREDENTIAL_ISSUER" } + require(requestJson.has(CREDENTIAL_CONFIGURATION_IDS)) { "Issuance request must contain $CREDENTIAL_CONFIGURATION_IDS" } + // This should be required for the DC API browser profile + require(requestJson.has(ISSUER_METADATA)) { "Issuance request must contain $ISSUER_METADATA" } + + credentialIssuer = requestJson.getString(CREDENTIAL_ISSUER) + credentialConfigurationIds = requestJson.getJSONArray(CREDENTIAL_CONFIGURATION_IDS).let { + val ids = mutableListOf() + for (i in 0..() + while (itr.hasNext()) { + val configId = itr.next() + val item = credConfigSupportedJson.getJSONObject(configId) + tmpMap[configId] = CredConfigsSupportedItem.createFrom(credConfigSupportedJson.getJSONObject(configId)) + } + credentialConfigurationsSupportedMap = tmpMap + } +} \ No newline at end of file diff --git a/app/src/main/java/com/credman/cmwallet/openid4vci/RequestDataModel.kt b/app/src/main/java/com/credman/cmwallet/openid4vci/RequestDataModel.kt new file mode 100644 index 0000000..7476f79 --- /dev/null +++ b/app/src/main/java/com/credman/cmwallet/openid4vci/RequestDataModel.kt @@ -0,0 +1,190 @@ +package com.credman.cmwallet.openid4vci + +import org.json.JSONObject + +data class CredConfigsSupportedDisplay( + val name: String, + val locale: String? = null, + val logo: CredConfigsSupportedDisplayLogo? = null, + val description: String? = null, + val backgroundColor: String? = null, + // `https:` or `data:` scheme + val backgroundImage: String? = null, + val textColor: String, +) { + constructor(json: JSONObject) : this( + name = json.getString(NAME), + locale = json.optString(LOCALE), + logo = json.optJSONObject(LOGO)?.let { + CredConfigsSupportedDisplayLogo(uri = it.getString(URI), altText = it.optString(ALT_TEXT)) + }, + description = json.optString(DESCRIPTION), + backgroundColor = json.optString(BACKGROUND_COLOR), + backgroundImage = json.optJSONObject(BACKGROUND_IMAGE)?.getString(URI), + textColor = json.optString(TEXT_COLOR), + ) +} + +private fun JSONObject.getDisplays(): List? = + this.optJSONArray(DISPLAY)?.let { + val out = mutableListOf() + for (i in 0..< it.length()) { + out.add(CredConfigsSupportedDisplay(it.getJSONObject(i))) + } + out + } + +data class CredConfigsSupportedDisplayLogo( + val uri: String, + val altText: String? +) + +sealed class CredConfigsSupportedItem( + val format: String, + val cryptographicBindingMethodsSupported: List? = null, + val credentialSigningAlgValuesSupported: List? = null, + val display: List? = null, +) { + companion object { + fun createFrom(json: JSONObject): CredConfigsSupportedItem { + return if (json.has(DOCTYPE)) { + MdocCredConfigsSupportedItem(json) + } else { + UnknownCredConfigsSupportedItem(json) + } + } + } +} + +class UnknownCredConfigsSupportedItem( + format: String, + cryptographicBindingMethodsSupported: List? = null, + credentialSigningAlgValuesSupported: List? = null, + display: List? = null, +) : CredConfigsSupportedItem( + format, + cryptographicBindingMethodsSupported, + credentialSigningAlgValuesSupported, + display +) { + constructor(json: JSONObject): this( + format = json.getString(FORMAT), + cryptographicBindingMethodsSupported = json.getCryptographicBindingMethodsSupported(), + credentialSigningAlgValuesSupported = json.getCredentialSigningAlgValuesSupported(), + display = json.getDisplays(), + ) +} + +class MdocCredConfigsSupportedItem( + format: String, + cryptographicBindingMethodsSupported: List? = null, + credentialSigningAlgValuesSupported: List? = null, + display: List? = null, + val doctype: String, + // Namespace as key e.g. "org.iso.18013.5.1" + val claims: Map?, +) : CredConfigsSupportedItem( + format, + cryptographicBindingMethodsSupported, + credentialSigningAlgValuesSupported, + display +) { + constructor(json: JSONObject): this( + format = json.getString(FORMAT), + cryptographicBindingMethodsSupported = json.getCryptographicBindingMethodsSupported(), + credentialSigningAlgValuesSupported = json.getCredentialSigningAlgValuesSupported(), + display = json.getDisplays(), + doctype = json.getString(DOCTYPE), + claims = json.optJSONObject(CLAIMS)?.let { + val keys = it.keys() + val out = mutableMapOf() + while(keys.hasNext()) { + val key = keys.next() + out[key] = NamespacedClaims(it.getJSONObject(key)) + } + out + } + ) +} + +data class NamespacedClaims( + val values: Map +) { + constructor(json: JSONObject) : this( + values = json.let { + val keys = it.keys() + val out = mutableMapOf() + while(keys.hasNext()) { + val key = keys.next() + out[key] = Claim(it.getJSONObject(key)) + } + out + } + ) +} + +data class Claim( + val display: List? = null, + val mandatory: Boolean = false, + val valueType: String? = null, +) { + constructor(json: JSONObject) : this( + display = json.optJSONArray(DISPLAY)?.let { + val out = mutableListOf() + for (i in 0..< it.length()) { + out.add(ClaimDisplay(it.getJSONObject(i))) + } + out + }, + mandatory = json.optBoolean(MANDATORY, false), + valueType = json.optString(VALUE_TYPE), + ) +} + +data class ClaimDisplay( + val name: String?, + val locale: String?, +) { + constructor(json: JSONObject) : this(json.optString(NAME), json.optString(LOCALE)) +} + +private fun JSONObject.getCryptographicBindingMethodsSupported(): List? = + this.optJSONArray(CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED)?.let { + val out = mutableListOf() + for (i in 0..< it.length()) { + out.add(it.getString(i)) + } + out + } + +private fun JSONObject.getCredentialSigningAlgValuesSupported(): List? = + this.optJSONArray(CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED)?.let { + val out = mutableListOf() + for (i in 0..< it.length()) { + out.add(it.getString(i)) + } + out + } + + +internal const val CREDENTIAL_ISSUER = "credential_issuer" +internal const val CREDENTIAL_CONFIGURATION_IDS = "credential_configuration_ids" +internal const val ISSUER_METADATA = "issuer_metadata" +internal const val CREDENTIAL_CONFIGURATION_SUPPORTED = "credential_configurations_supported" +internal const val CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED = "cryptographic_binding_methods_supported" +internal const val CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED = "credential_signing_alg_values_supported" +internal const val FORMAT = "format" +internal const val DOCTYPE = "doctype" +internal const val DISPLAY = "display" +internal const val NAME = "name" +internal const val URI = "uri" +internal const val LOCALE = "locale" +internal const val MANDATORY = "mandatory" +internal const val LOGO = "logo" +internal const val DESCRIPTION = "description" +internal const val CLAIMS = "claims" +internal const val BACKGROUND_IMAGE = "background_image" +internal const val BACKGROUND_COLOR = "background_color" +internal const val TEXT_COLOR = "text_color" +internal const val VALUE_TYPE = "value_type" +internal const val ALT_TEXT = "alt_text" \ No newline at end of file