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/.idea/other.xml b/.idea/other.xml deleted file mode 100644 index 94c96f6..0000000 --- a/.idea/other.xml +++ /dev/null @@ -1,318 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/MobileSdk/build.gradle.kts b/MobileSdk/build.gradle.kts index 0459155..a8ae907 100644 --- a/MobileSdk/build.gradle.kts +++ b/MobileSdk/build.gradle.kts @@ -118,7 +118,7 @@ android { } dependencies { - api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.32") + api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.33") //noinspection GradleCompatible implementation("com.android.support:appcompat-v7:28.0.0") /* Begin UI dependencies */ 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..993b4f5 --- /dev/null +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/Credential.kt @@ -0,0 +1,100 @@ +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.JSONObject + +/** + * 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) { + val json: JSONObject + try { + json = JSONObject(jsonString) + } catch (e: Error) { + print("failed to decode '$id' as JSON: $e") + return@map null + } + return@map Pair(id, json) + } + + 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, old.get(name)) + } + } + 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) { + if (old.has(name)) { + new.put(name, old.get(name)) + } + } + return new +} \ 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..8f2ed2a 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialsViewModel.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialsViewModel.kt @@ -5,6 +5,7 @@ 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.ParsedCredential import com.spruceid.mobile.sdk.rs.initializeMdlPresentationFromBytes import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,7 +15,7 @@ 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,7 +38,7 @@ class CredentialsViewModel : ViewModel() { private val _transport = MutableStateFlow(null) - fun storeCredential(credential: BaseCredential) { + fun storeCredential(credential: ParsedCredential) { _credentials.value.add(credential) } 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()) } }