From 6b039d43609885792d36a10dbdbd163c982b8c9b Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 15 Oct 2024 16:07:47 -0400 Subject: [PATCH] Update CredentialPack to use ParsedCredential (#39) * Update CredentialPack to use ParsedCredential * Update SpruceKit Demo * Small adjustments to make demo app compatible * Parse any JSON type --------- Co-authored-by: Juliano Cezar Chagas Tavares --- .idea/deploymentTargetSelector.xml | 8 - .../com/spruceid/mobile/sdk/BaseCredential.kt | 33 ---- .../com/spruceid/mobile/sdk/Credential.kt | 112 +++++++++++++ .../com/spruceid/mobile/sdk/CredentialPack.kt | 156 ++++++++---------- .../mobile/sdk/CredentialsViewModel.kt | 27 ++- .../spruceid/mobile/sdk/IsoMdlPresentation.kt | 5 +- .../main/java/com/spruceid/mobile/sdk/MDoc.kt | 17 -- .../java/com/spruceid/mobile/sdk/W3CVC.kt | 34 ---- .../com/spruceid/mobile/sdk/ui/BaseCard.kt | 42 +++-- .../wallet/GenericCredentialListItem.kt | 38 ++--- .../wallet/ShareableCredentialListItem.kt | 72 ++++++-- .../mobilesdkexample/wallet/WalletHomeView.kt | 1 - 12 files changed, 309 insertions(+), 236 deletions(-) delete mode 100644 MobileSdk/src/main/java/com/spruceid/mobile/sdk/BaseCredential.kt create mode 100644 MobileSdk/src/main/java/com/spruceid/mobile/sdk/Credential.kt delete mode 100644 MobileSdk/src/main/java/com/spruceid/mobile/sdk/MDoc.kt delete mode 100644 MobileSdk/src/main/java/com/spruceid/mobile/sdk/W3CVC.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index a563245..040567e 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,14 +4,6 @@ diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/BaseCredential.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/BaseCredential.kt deleted file mode 100644 index 79e8657..0000000 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/BaseCredential.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.spruceid.mobile.sdk - -open class BaseCredential { - private var id: String? - - constructor() { - this.id = null - } - - constructor(id: String) { - this.id = id - } - - fun getId(): String? { - return this.id - } - - fun setId(id: String) { - this.id = id - } - - override fun toString(): String { - return "Credential($id)" - } - - open fun get(keys: List): Map { - return if (keys.contains("id")) { - mapOf("id" to this.id!!) - } else { - emptyMap() - } - } -} \ No newline at end of file diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/Credential.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/Credential.kt new file mode 100644 index 0000000..f22db64 --- /dev/null +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/Credential.kt @@ -0,0 +1,112 @@ +package com.spruceid.mobile.sdk + +import com.spruceid.mobile.sdk.rs.JsonVc +import com.spruceid.mobile.sdk.rs.JwtVc +import com.spruceid.mobile.sdk.rs.Mdoc +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONTokener + +/** + * Access all of the elements in the mdoc, ignoring namespaces and missing elements that cannot be encoded as JSON. + */ +fun Mdoc.jsonEncodedDetailsAll(): JSONObject = this.jsonEncodedDetailsInternal(null) + +/** + * Access the specified elements in the mdoc, ignoring namespaces and missing elements that cannot be encoded as JSON. + */ +fun Mdoc.jsonEncodedDetailsFiltered(elementIdentifiers: List): JSONObject = this.jsonEncodedDetailsInternal(elementIdentifiers) + + +private fun Mdoc.jsonEncodedDetailsInternal(elementIdentifiers: List?): JSONObject = + JSONObject( + // Ignore the namespaces. + this.details().values.flatMap { elements -> + elements.map { element -> + val id = element.identifier + val jsonString = element.value + + // If a filter is provided, filter out non-specified ids. + if (elementIdentifiers != null) { + if (!elementIdentifiers.contains(id)) { + return@map null + } + } + + if (jsonString != null) { + try { + val jsonElement = JSONTokener(jsonString).nextValue() + return@map Pair(id, jsonElement) + } catch (e: JSONException) { + print("failed to decode '$id' as JSON: $e") + } + } + + return@map null + } + }.filterNotNull().toMap() + ) + +/** + * Access the W3C VCDM credential (not including the JWT envelope). + */ +fun JwtVc.credentialClaims(): JSONObject { + try { + return JSONObject(this.credentialAsJsonEncodedUtf8String()) + } catch (e: Error) { + print("failed to decode VCDM data from UTF-8-encoded JSON") + return JSONObject() + } +} + +/** + * Access the specified claims from the W3C VCDM credential (not including the JWT envelope). + */ +fun JwtVc.credentialClaimsFiltered(claimNames: List): JSONObject { + val old = this.credentialClaims() + val new = JSONObject() + for (name in claimNames) { + if (old.has(name)) { + new.put(name, keyPathFinder(old, name.split(".").toMutableList())) + } + } + return new +} + +/** + * Access the W3C VCDM credential. + */ +fun JsonVc.credentialClaims(): JSONObject { + try { + return JSONObject(this.credentialAsJsonEncodedUtf8String()) + } catch (e: Error) { + print("failed to decode VCDM data from UTF-8-encoded JSON") + return JSONObject() + } +} + +/** + * Access the specified claims from the W3C VCDM credential. + */ +fun JsonVc.credentialClaimsFiltered(claimNames: List): JSONObject { + val old = this.credentialClaims() + val new = JSONObject() + for (name in claimNames) { + new.put(name, keyPathFinder(old, name.split(".").toMutableList())) + } + return new +} + +private fun keyPathFinder(json: Any, path: MutableList): Any { + try { + val firstKey = path.first() + val element = (json as JSONObject)[firstKey] + path.removeAt(0) + if (path.isNotEmpty()) { + return keyPathFinder(element, path) + } + return element + } catch (e: Exception) { + return "" + } +} \ No newline at end of file diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt index a1c02e7..acb52e2 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt @@ -1,108 +1,98 @@ package com.spruceid.mobile.sdk -import java.security.KeyFactory -import java.security.KeyStore -import java.security.cert.Certificate -import java.security.cert.CertificateFactory -import java.security.spec.PKCS8EncodedKeySpec -import java.util.Base64 +import com.spruceid.mobile.sdk.rs.JsonVc +import com.spruceid.mobile.sdk.rs.JwtVc +import com.spruceid.mobile.sdk.rs.Mdoc +import com.spruceid.mobile.sdk.rs.ParsedCredential +import org.json.JSONObject /** * Collection of BaseCredentials with methods to interact with all instances */ class CredentialPack { - private val credentials: MutableList + private val credentials: MutableList constructor() { credentials = mutableListOf() } - constructor(credentialsArray: MutableList) { + constructor(credentialsArray: MutableList) { this.credentials = credentialsArray } - fun addW3CVC(credentialString: String): List { - val vc = W3CVC(credentialString = credentialString) - credentials.add(vc) + /** + * Add a JwtVc to the CredentialPack. + */ + fun addJwtVc(jwtVc: JwtVc): List { + credentials.add(ParsedCredential.newJwtVcJson(jwtVc)) return credentials } - fun addMDoc( - id: String, - mdocBase64: String, - keyPEM: String, - keyBase64: String - ): List { - try { - val decodedKey = Base64.getDecoder().decode( - keyBase64 - ) - - val privateKey = KeyFactory.getInstance( - "EC" - ).generatePrivate( - PKCS8EncodedKeySpec( - decodedKey - ) - ) - - val cert: Array = arrayOf( - CertificateFactory.getInstance( - "X.509" - ).generateCertificate( - keyPEM.byteInputStream() - ) - ) - - val ks: KeyStore = KeyStore.getInstance( - "AndroidKeyStore" - ) - - ks.load( - null - ) - - ks.setKeyEntry( - "someAlias", - privateKey, - null, - cert - ) - - credentials.add( - MDoc( - id, - Base64.getDecoder().decode(mdocBase64), - "someAlias" - ) - ) - } catch (e: Throwable) { - print( - e - ) - throw e - } + /** + * Add a JsonVc to the CredentialPack. + */ + fun addJsonVc(jsonVc: JsonVc): List { + credentials.add(ParsedCredential.newLdpVc(jsonVc)) return credentials } - fun get(keys: List): Map> { - val values = emptyMap>().toMutableMap() - - for (credential in credentials) { - values[credential.getId()!!] = credential.get(keys) - } - return values - } - - fun getCredentialsByIds(credentialsIds: List): List { - return credentials.filter { credential -> credentialsIds.contains(credential.getId()) } - } - - fun getCredentials(): List { + /** + * Add an Mdoc to the CredentialPack. + */ + fun addMdoc(mdoc: Mdoc): List { + credentials.add(ParsedCredential.newMsoMdoc(mdoc)) return credentials } - fun getCredentialById(credentialId: String): BaseCredential? { - return credentials.find { credential -> credential.getId().equals(credentialId) } - } + /** + * Find claims from all credentials in this CredentialPack. + */ + fun findCredentialClaims(claimNames: List): Map = + this.list() + .map { credential -> + var claims: JSONObject + val mdoc = credential.asMsoMdoc() + val jwtVc = credential.asJwtVc() + val jsonVc = credential.asJsonVc() + + if (mdoc != null) { + claims = mdoc.jsonEncodedDetailsFiltered(claimNames) + } else if (jwtVc != null) { + claims = jwtVc.credentialClaimsFiltered(claimNames) + } else if (jsonVc != null) { + claims = jsonVc.credentialClaimsFiltered(claimNames) + } else { + var type: String + try { + type = credential.intoGenericForm().type + } catch (e: Error) { + type = "unknown" + } + print("unsupported credential type: $type") + claims = JSONObject() + } + + return@map Pair(credential.id(), claims) + } + .toMap() + + + /** + * Get credentials by id. + */ + fun getCredentialsByIds(credentialsIds: List): List = + this.list().filter { credential -> credentialsIds.contains(credential.id()) } + + + /** + * Get a credential by id. + */ + fun getCredentialById(credentialId: String): ParsedCredential? = + this.list().find { credential -> credential.id() == credentialId } + + + /** + * List all of the credentials in the CredentialPack. + */ + fun list(): List = this.credentials } \ No newline at end of file diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialsViewModel.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialsViewModel.kt index 8bc1840..ff8a52d 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialsViewModel.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialsViewModel.kt @@ -5,16 +5,19 @@ import android.util.Log import androidx.lifecycle.ViewModel import com.spruceid.mobile.sdk.rs.ItemsRequest import com.spruceid.mobile.sdk.rs.MdlPresentationSession +import com.spruceid.mobile.sdk.rs.Mdoc +import com.spruceid.mobile.sdk.rs.ParsedCredential import com.spruceid.mobile.sdk.rs.initializeMdlPresentationFromBytes import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import java.security.KeyStore import java.security.Signature import java.util.UUID class CredentialsViewModel : ViewModel() { - private val _credentials = MutableStateFlow>(arrayListOf()) + private val _credentials = MutableStateFlow>(arrayListOf()) val credentials = _credentials.asStateFlow() private val _currState = MutableStateFlow(PresentmentState.UNINITIALIZED) @@ -37,10 +40,20 @@ class CredentialsViewModel : ViewModel() { private val _transport = MutableStateFlow(null) - fun storeCredential(credential: BaseCredential) { + fun storeCredential(credential: ParsedCredential) { _credentials.value.add(credential) } + private fun firstMdoc(): Mdoc { + val mdoc = _credentials.value + .map { credential -> credential.asMsoMdoc() } + .firstOrNull() + if (mdoc == null) { + throw Exception("no mdoc found") + } + return mdoc + } + fun toggleAllowedNamespace(docType: String, specName: String, fieldName: String) { if (_allowedNamespaces.value.isEmpty()) { _allowedNamespaces.value = mapOf(Pair(docType, mapOf(Pair(specName, listOf())))) @@ -78,8 +91,8 @@ class CredentialsViewModel : ViewModel() { suspend fun present(bluetoothManager: BluetoothManager) { Log.d("CredentialsViewModel.present", "Credentials: ${_credentials.value}") _uuid.value = UUID.randomUUID() - val first: MDoc = _credentials.value.first() as MDoc - _session.value = initializeMdlPresentationFromBytes(first.inner, _uuid.value.toString()) + val mdoc = this.firstMdoc() + _session.value = initializeMdlPresentationFromBytes(mdoc, _uuid.value.toString()) _currState.value = PresentmentState.ENGAGING_QR_CODE _transport.value = Transport(bluetoothManager) _transport.value!! @@ -102,7 +115,7 @@ class CredentialsViewModel : ViewModel() { } fun submitNamespaces(allowedNamespaces: Map>>) { - val firstMDoc: MDoc = _credentials.value.first() as MDoc + val mdoc = this.firstMdoc() if(allowedNamespaces.isEmpty()) { val e = Error("Select at least one namespace") Log.e("CredentialsViewModel.submitNamespaces", e.toString()) @@ -122,9 +135,9 @@ class CredentialsViewModel : ViewModel() { null ) - val entry = ks.getEntry(firstMDoc.keyAlias, null) + val entry = ks.getEntry(mdoc.keyAlias(), null) if (entry !is KeyStore.PrivateKeyEntry) { - throw IllegalStateException("No such private key under the alias <${firstMDoc.keyAlias}>") + throw IllegalStateException("No such private key under the alias <${mdoc.keyAlias()}>") } try { diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt index 2ff3970..2151915 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt @@ -4,6 +4,7 @@ import android.bluetooth.BluetoothManager import android.util.Log import com.spruceid.mobile.sdk.rs.ItemsRequest import com.spruceid.mobile.sdk.rs.MdlPresentationSession +import com.spruceid.mobile.sdk.rs.Mdoc import com.spruceid.mobile.sdk.rs.RequestException import com.spruceid.mobile.sdk.rs.initializeMdlPresentationFromBytes import java.security.KeyStore @@ -16,7 +17,7 @@ abstract class BLESessionStateDelegate { } class IsoMdlPresentation( - val mdoc: MDoc, + val mdoc: Mdoc, val keyAlias: String, val bluetoothManager: BluetoothManager, val callback: BLESessionStateDelegate @@ -28,7 +29,7 @@ class IsoMdlPresentation( suspend fun initialize() { try { - session = initializeMdlPresentationFromBytes(this.mdoc.inner, uuid.toString()) + session = initializeMdlPresentationFromBytes(this.mdoc, uuid.toString()) this.bleManager = Transport(this.bluetoothManager) this.bleManager!! .initialize( diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/MDoc.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/MDoc.kt deleted file mode 100644 index 7e2529e..0000000 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/MDoc.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.spruceid.mobile.sdk - -import android.util.Log -import com.spruceid.mobile.sdk.rs.Mdoc as InnerMDoc - -class MDoc(id: String, issuerAuth: ByteArray, val keyAlias: String) : BaseCredential(id) { - val inner: InnerMDoc - - init { - try { - inner = InnerMDoc.fromCborEncodedDocument(issuerAuth, keyAlias) - } catch (e: Throwable) { - Log.e("MDoc.init", e.toString()) - throw e - } - } -} \ No newline at end of file diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/W3CVC.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/W3CVC.kt deleted file mode 100644 index 19cc97e..0000000 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/W3CVC.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.spruceid.mobile.sdk - -import org.json.JSONObject - -class W3CVC(credentialString: String): BaseCredential() { - private var credential: JSONObject = JSONObject(credentialString) - - init { - super.setId(credential.getString("id")) - } - - override fun get(keys: List): Map { - val res = mutableMapOf() - - for (key in keys) { - res[key] = keyPathFinder(credential, key.split(".").toMutableList()) - } - return res - } - - private fun keyPathFinder(json: Any, path: MutableList): Any { - try { - val firstKey = path.first() - val element = (json as JSONObject)[firstKey] - path.removeAt(0) - if (path.isNotEmpty()) { - return keyPathFinder(element, path) - } - return element - } catch (e: Exception) { - return "" - } - } -} diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt index e2f4fc9..7457b76 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.spruceid.mobile.sdk.CredentialPack +import org.json.JSONObject /** * Data class with the specification to display the credential pack in a list view @@ -23,13 +24,13 @@ import com.spruceid.mobile.sdk.CredentialPack */ data class CardRenderingListView( val titleKeys: List, - val titleFormatter: @Composable ((values: Map>) -> Unit)? = null, + val titleFormatter: @Composable ((values: Map) -> Unit)? = null, val descriptionKeys: List? = null, - val descriptionFormatter: @Composable ((values: Map>) -> Unit)? = null, + val descriptionFormatter: @Composable ((values: Map) -> Unit)? = null, val leadingIconKeys: List? = null, - val leadingIconFormatter: @Composable ((values: Map>) -> Unit)? = null, + val leadingIconFormatter: @Composable ((values: Map) -> Unit)? = null, val trailingActionKeys: List? = null, - val trailingActionButton: @Composable ((values: Map>) -> Unit)? = null + val trailingActionButton: @Composable ((values: Map) -> Unit)? = null ) /** @@ -39,7 +40,7 @@ data class CardRenderingListView( */ data class CardRenderingDetailsField( val keys: List, - val formatter: @Composable ((values: Map>) -> Unit)? = null + val formatter: @Composable ((values: Map) -> Unit)? = null ) /** @@ -99,8 +100,8 @@ fun CardListView( credentialPack: CredentialPack, rendering: CardRenderingListView ) { - val titleValues = credentialPack.get(rendering.titleKeys) - val descriptionValues = credentialPack.get(rendering.descriptionKeys ?: emptyList()) + val titleValues = credentialPack.findCredentialClaims(rendering.titleKeys) + val descriptionValues = credentialPack.findCredentialClaims(rendering.descriptionKeys ?: emptyList()) Row( Modifier.height(intrinsicSize = IntrinsicSize.Max) @@ -108,7 +109,7 @@ fun CardListView( // Leading icon if(rendering.leadingIconFormatter != null) { rendering.leadingIconFormatter.invoke( - credentialPack.get(rendering.leadingIconKeys ?: emptyList()) + credentialPack.findCredentialClaims(rendering.leadingIconKeys ?: emptyList()) ) } @@ -118,8 +119,11 @@ fun CardListView( rendering.titleFormatter.invoke(titleValues) } else { Text(text = titleValues.values - .fold(emptyList()) { acc, next -> acc + next.values - .joinToString(" ") { value -> value.toString() } + .fold(emptyList()) { acc, next -> acc + + next.keys() + .asSequence() + .map { key -> next.get(key)} + .joinToString(" ") { value -> value.toString() } }.joinToString("").trim()) } @@ -128,8 +132,11 @@ fun CardListView( rendering.descriptionFormatter.invoke(descriptionValues) } else { Text(text = descriptionValues.values - .fold(emptyList()) { acc, next -> acc + next.values - .joinToString(" ") { value -> value.toString() } + .fold(emptyList()) { acc, next -> acc + + next.keys() + .asSequence() + .map { key -> next.get(key)} + .joinToString(" ") { value -> value.toString() } }.joinToString("").trim()) } } @@ -139,7 +146,7 @@ fun CardListView( // Trailing action button if(rendering.trailingActionButton != null) { rendering.trailingActionButton.invoke( - credentialPack.get(rendering.trailingActionKeys ?: emptyList()) + credentialPack.findCredentialClaims(rendering.trailingActionKeys ?: emptyList()) ) } } @@ -157,14 +164,17 @@ fun CardDetailsView( ) { Column { rendering.fields.forEach { - val values = credentialPack.get(it.keys) + val values = credentialPack.findCredentialClaims(it.keys) if(it.formatter != null) { it.formatter.invoke(values) } else { Text(text = values.values - .fold(emptyList()) { acc, next -> acc + next.values - .joinToString(" ") { value -> value.toString() } + .fold(emptyList()) { acc, next -> acc + + next.keys() + .asSequence() + .map { key -> next.get(key)} + .joinToString(" ") { value -> value.toString() } }.joinToString("").trim()) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/GenericCredentialListItem.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/GenericCredentialListItem.kt index 38abb2a..6039802 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/GenericCredentialListItem.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/GenericCredentialListItem.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.spruceid.mobile.sdk.CredentialPack -import com.spruceid.mobile.sdk.W3CVC +import com.spruceid.mobile.sdk.rs.JsonVc import com.spruceid.mobile.sdk.rs.vcToSignedVp import com.spruceid.mobile.sdk.ui.BaseCard import com.spruceid.mobile.sdk.ui.CardRenderingDetailsField @@ -64,10 +64,10 @@ import com.spruceid.mobilesdkexample.utils.small_vc @OptIn(ExperimentalMaterial3Api::class) @Composable fun GenericCredentialListItems( - vc: String + vc_json: String ) { val credentialPack = CredentialPack() - credentialPack.addW3CVC(credentialString = vc) + credentialPack.addJsonVc(JsonVc.newFromJson(vc_json)) var sheetOpen by remember { mutableStateOf(false) @@ -149,6 +149,7 @@ fun GenericCredentialDetailsItem(credentialPack: CredentialPack) { CardRenderingDetailsField( // it's also possible just request the credentialSubject and cast it to JSONObject keys = listOf( + "issuer.name", "credentialSubject.image", "credentialSubject.givenName", "credentialSubject.familyName", @@ -159,15 +160,14 @@ fun GenericCredentialDetailsItem(credentialPack: CredentialPack) { "credentialSubject.driversLicense.family_name", "credentialSubject.driversLicense.birth_date", ), - formatter = {values -> - val w3cvc = values.toList() - .first { credentialPack.getCredentialById(it.first)!! is W3CVC }.second + formatter = { values -> + val w3cvc = values.toList().first().second var portrait = "" var firstName = "" var lastName = "" var birthDate = "" - if(w3cvc["credentialSubject.driversLicense"].toString().isNotEmpty()) { + if (w3cvc["credentialSubject.driversLicense"].toString().isNotEmpty()) { portrait = w3cvc["credentialSubject.driversLicense.portrait"].toString() firstName = w3cvc["credentialSubject.driversLicense.given_name"].toString() lastName = w3cvc["credentialSubject.driversLicense.family_name"].toString() @@ -201,7 +201,7 @@ fun GenericCredentialDetailsItem(credentialPack: CredentialPack) { ) BitmapImage( byteArray, - contentDescription = w3cvc["issuer.name"].toString(), + contentDescription = w3cvc["issuer.name"].toString(), modifier = Modifier .width(90.dp) .padding(end = 12.dp) @@ -252,8 +252,7 @@ fun GenericCredentialDetailsItem(credentialPack: CredentialPack) { CardRenderingDetailsField( keys = listOf("issuanceDate"), formatter = { values -> - val w3cvc = values.toList() - .first { credentialPack.getCredentialById(it.first)!! is W3CVC }.second + val w3cvc = values.toList().first().second Row { Column { @@ -291,9 +290,8 @@ fun GenericCredentialDetailsItem(credentialPack: CredentialPack) { fun GenericCredentialListItem(credentialPack: CredentialPack) { val listRendering = CardRenderingListView( titleKeys = listOf("name"), - titleFormatter = {values -> - val w3cvc = values.toList() - .first { credentialPack.getCredentialById(it.first)!! is W3CVC }.second + titleFormatter = { values -> + val w3cvc = values.toList().first().second Text( text = w3cvc["name"].toString(), @@ -305,9 +303,8 @@ fun GenericCredentialListItem(credentialPack: CredentialPack) { ) }, descriptionKeys = listOf("description", "valid"), - descriptionFormatter = {values -> - val w3cvc = values.toList() - .first { credentialPack.getCredentialById(it.first)!! is W3CVC }.second + descriptionFormatter = { values -> + val w3cvc = values.toList().first().second Column { Text( @@ -318,7 +315,7 @@ fun GenericCredentialListItem(credentialPack: CredentialPack) { color = TextBody ) Spacer(modifier = Modifier.height(16.dp)) - if(w3cvc["valid"].toString() == "true") { + if (w3cvc["valid"].toString() == "true") { Row(verticalAlignment = Alignment.CenterVertically) { Image( painter = painterResource(id = R.drawable.valid), @@ -338,8 +335,7 @@ fun GenericCredentialListItem(credentialPack: CredentialPack) { }, leadingIconKeys = listOf("issuer.image", "issuer.name"), leadingIconFormatter = { values -> - val w3cvc = values.toList() - .first { credentialPack.getCredentialById(it.first)!! is W3CVC }.second + val w3cvc = values.toList().first().second val byteArray = Base64.decode( w3cvc["issuer.image"] .toString() @@ -355,7 +351,7 @@ fun GenericCredentialListItem(credentialPack: CredentialPack) { ) { BitmapImage( byteArray, - contentDescription = w3cvc["issuer.name"].toString(), + contentDescription = w3cvc["issuer.name"].toString(), modifier = Modifier .width(50.dp) .padding(end = 12.dp) @@ -424,7 +420,7 @@ fun GenericCredentialListItemQRCode() { modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - if(vc != null) { + if (vc != null) { Image( painter = rememberQrBitmapPainter(vc!!, size = 300.dp), contentDescription = stringResource(id = R.string.vp_qr_code), diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/ShareableCredentialListItem.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/ShareableCredentialListItem.kt index b5c068e..176880d 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/ShareableCredentialListItem.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/ShareableCredentialListItem.kt @@ -34,9 +34,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.spruceid.mobile.sdk.BaseCredential import com.spruceid.mobile.sdk.CredentialPack import com.spruceid.mobile.sdk.CredentialsViewModel +import com.spruceid.mobile.sdk.rs.Mdoc +import com.spruceid.mobile.sdk.rs.ParsedCredential import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.ui.theme.Bg import com.spruceid.mobilesdkexample.ui.theme.CredentialBorder @@ -46,7 +47,12 @@ import com.spruceid.mobilesdkexample.ui.theme.TextHeader import com.spruceid.mobilesdkexample.ui.theme.TextOnPrimary import com.spruceid.mobilesdkexample.utils.keyBase64 import com.spruceid.mobilesdkexample.utils.keyPEM -import java.util.UUID +import java.security.KeyFactory +import java.security.KeyStore +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -57,12 +63,50 @@ fun ShareableCredentialListItems( CredentialPack() } val credentials = remember { - credentialPack.addMDoc( - id = UUID.randomUUID().toString(), - mdocBase64 = mdocBase64, - keyPEM = keyPEM, - keyBase64 = keyBase64 + val keyAlias = "someAlias" + + val decodedKey = Base64.getDecoder().decode( + keyBase64 + ) + + val privateKey = KeyFactory.getInstance( + "EC" + ).generatePrivate( + PKCS8EncodedKeySpec( + decodedKey + ) + ) + + val cert: Array = arrayOf( + CertificateFactory.getInstance( + "X.509" + ).generateCertificate( + keyPEM.byteInputStream() + ) + ) + + val ks: KeyStore = KeyStore.getInstance( + "AndroidKeyStore" ) + + ks.load( + null + ) + + ks.setKeyEntry( + keyAlias, + privateKey, + null, + cert + ) + + + val mdoc = Mdoc.fromCborEncodedDocument( + Base64.getDecoder().decode(mdocBase64), + keyAlias + ) + + credentialPack.addMdoc(mdoc) } var sheetOpen by remember { @@ -127,23 +171,23 @@ fun ShareableCredentialListItems( } @Composable -fun ShareableCredentialDetailsItem(credential: BaseCredential) { +fun ShareableCredentialDetailsItem(credential: ParsedCredential) { Box( Modifier .fillMaxWidth() .padding(24.dp) ) { - Text(credential.getId().toString()) + Text(credential.id()) } } @Composable -fun ShareableCredentialListItem(credential: BaseCredential) { - Text(credential.getId().toString()) +fun ShareableCredentialListItem(credential: ParsedCredential) { + Text(credential.id()) } @Composable -fun ShareableCredentialListItemQRCode(credential: BaseCredential) { +fun ShareableCredentialListItemQRCode(credential: ParsedCredential) { var showQRCode by remember { mutableStateOf(false) } @@ -177,7 +221,7 @@ fun ShareableCredentialListItemQRCode(credential: BaseCredential) { .fillMaxWidth() .clickable { showQRCode = !showQRCode - if(!showQRCode) { + if (!showQRCode) { credentialViewModel.cancel() } } @@ -198,7 +242,7 @@ fun ShareableCredentialListItemQRCode(credential: BaseCredential) { } AnimatedVisibility(visible = showQRCode) { - if(showQRCode) { + if (showQRCode) { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt index 5b8db1a..118d430 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt @@ -39,7 +39,6 @@ import com.spruceid.mobilesdkexample.ui.theme.CTAButtonBlue import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.ui.theme.TextHeader import com.spruceid.mobilesdkexample.ui.theme.Primary -import com.spruceid.mobilesdkexample.utils.mdocBase64 import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel import kotlinx.coroutines.launch