From de24ae8ae8840e1e9e39e06486c44b9692da797b Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Sat, 5 Oct 2024 08:21:04 -0700 Subject: [PATCH] wip: oid4vp integration Signed-off-by: Ryan Tate --- .../mobile/sdk/oid4vp/OID4VPHolder.kt | 30 ++----- .../spruceid/mobilesdkexample/MainActivity.kt | 21 ++++- .../navigation/SetupNavGraph.kt | 2 +- .../spruceid/mobilesdkexample/utils/Utils.kt | 7 +- .../wallet/AchievementCredentialItem.kt | 8 +- .../mobilesdkexample/wallet/DispatchQRView.kt | 85 ++++++++++++++----- .../mobilesdkexample/wallet/WalletHomeView.kt | 2 + 7 files changed, 108 insertions(+), 47 deletions(-) diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/oid4vp/OID4VPHolder.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/oid4vp/OID4VPHolder.kt index 42d78fc..2c3ec5f 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/oid4vp/OID4VPHolder.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/oid4vp/OID4VPHolder.kt @@ -2,32 +2,18 @@ package com.spruceid.mobile.sdk.oid4vp import com.spruceid.mobile.sdk.rs.Credential import com.spruceid.mobile.sdk.rs.Holder -import com.spruceid.mobile.sdk.rs.HolderInterface -import com.spruceid.mobile.sdk.rs.RequestSignerInterface import com.spruceid.mobile.sdk.rs.Url import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext class OID4VPHolder( - keyId: String, - credentials: List, - trustedDids: List + private val keyId: String, + private val credentials: List, + private val trustedDids: List ) { - public val keyId = keyId - public val credentials = credentials - public val trustedDids = trustedDids - - suspend fun handle_url( - url: Url, - credentialPresentation: CredentialPresentation - ) : Url? { - return runBlocking { - Holder.newWithCredentials( - credentials, - RequestSigner(keyId), - trustedDids - ).handleOid4vpRequest(url, credentialPresentation) - } - } + suspend fun handleUrl(url: Url, credentialPresentation: CredentialPresentation): Url? = + withContext(Dispatchers.Default) { + Holder.newWithCredentials(credentials, RequestSigner(keyId), trustedDids) + .handleOid4vpRequest(url, credentialPresentation) + } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt index 52a178d..1e50fc4 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.lifecycle.coroutineScope import androidx.navigation.NavHostController @@ -21,6 +22,7 @@ import com.spruceid.mobilesdkexample.db.RawCredentialsRepository import com.spruceid.mobilesdkexample.navigation.SetupNavGraph import com.spruceid.mobilesdkexample.ui.theme.Bg import com.spruceid.mobilesdkexample.ui.theme.MobileSdkTheme +import com.spruceid.mobilesdkexample.utils.exampleSdJwt import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel import com.spruceid.mobilesdkexample.viewmodels.RawCredentialsViewModelFactory import kotlinx.coroutines.launch @@ -33,8 +35,9 @@ class MainActivity : ComponentActivity() { val deepLinkUri: Uri? = intent.data if (deepLinkUri != null) { + // Remove? TBD if (deepLinkUri.scheme == "oid4vp://") { - // NOTE: See ScanOID4VPQR.kt for handling OID4VP QR code scanning, + // NOTE: See DispatchQRView.kt for handling OID4VP QR code scanning, // and credential selection. } } @@ -52,6 +55,22 @@ class MainActivity : ComponentActivity() { val credentialsViewModel: IRawCredentialsViewModel by viewModels { RawCredentialsViewModelFactory((application as MainApplication).rawCredentialsRepository) } + + // Insert a raw credential into the rawCredentialsRepository, + // using a suspend / async method. + LaunchedEffect(credentialsViewModel) { + lifecycle.coroutineScope.launch { + // Clear the raw credentials table. +// credentialsViewModel.deleteAllRawCredentials() +// // Load the exampleSdJwt into the raw credentials table. +// credentialsViewModel.saveRawCredential( +// com.spruceid.mobilesdkexample.db.RawCredentials( +// rawCredential = exampleSdJwt +// ) +// ) + } + } + SetupNavGraph(navController, credentialsViewModel) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt index a3db7a5..b5ff357 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt @@ -74,7 +74,7 @@ fun SetupNavGraph( } ) ) { - DispatchQRView(navController) + DispatchQRView(navController, rawCredentialsViewModel) } } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt index b67c0a1..28f7376 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt @@ -6,4 +6,9 @@ val keyPEM = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHB val keyBase64 = - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEAqKZdZQgPVtjlEBfz2ItHG8oXIONenOxRePtqOQ42yhRANCAATA43gI2Ib8+qKK4YEOfNCRiNOhyHaCLgAvKdhHS+y6wpG3oJ2xudXagzKKbcfvUda4x0j8zR1/oD56mpm85GbO" \ No newline at end of file + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEAqKZdZQgPVtjlEBfz2ItHG8oXIONenOxRePtqOQ42yhRANCAATA43gI2Ib8+qKK4YEOfNCRiNOhyHaCLgAvKdhHS+y6wpG3oJ2xudXagzKKbcfvUda4x0j8zR1/oD56mpm85GbO" + +//const val exampleSdJwt = "eyJhbGciOiJFUzI1NiJ9.eyJfc2RfYWxnIjoic2hhMjU2IiwiY3JlZGVudGlhbFN1YmplY3QiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiJdLCJhd2FyZGVkRGF0ZSI6IjIwMjQtMDktMjNUMTg6MTI6MTIrMDAwMCIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkZW50aXR5IjpbeyJoYXNoZWQiOmZhbHNlLCJpZGVudGl0eUhhc2giOiJKb2huIFNtaXRoIiwiaWRlbnRpdHlUeXBlIjoibmFtZSIsInNhbHQiOiJub3QtdXNlZCIsInR5cGUiOiJJZGVudGl0eU9iamVjdCJ9LHsiaGFzaGVkIjpmYWxzZSwiaWRlbnRpdHlIYXNoIjoiam9obi5zbWl0aEBleGFtcGxlLmNvbSIsImlkZW50aXR5VHlwZSI6ImVtYWlsQWRkcmVzcyIsInNhbHQiOiJub3QtdXNlZCIsInR5cGUiOiJJZGVudGl0eU9iamVjdCJ9XSwiYWNoaWV2ZW1lbnQiOnsibmFtZSI6IkNvbG9yYWRvRldEIFRlYW0gTWVtYmVyc2hpcCIsInR5cGUiOiJBY2hpZXZlbWVudCJ9fSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0ltTnlkaUk2SWxBdE1qVTJJaXdpYTNSNUlqb2lSVU1pTENKNElqb2liV0pVTTJkcU9XRnZPR051UzI4ME0wcHJjVlJQVW1OSlFWSTRNRmd3VFVGWFFXTkdZelp2UjFKTVl5SXNJbmtpT2lKaU9GVk9ZMGhETW1GSFEzSjFTVFowUWxSV1NWWTBkVzVaV0VWeVMwTTRaRFJuUlRGR1owczBRMDVKSW4wIzAiLCJuYW1lIjoiQ29sb3JhZG8gV29ya2ZvcmNlIERldmVsb3BtZW50IENvdW5jaWwiLCJ0eXBlIjoiUHJvZmlsZSJ9LCJuYW1lIjoiQ29sb3JhZG9GV0RUZWFtTWVtYmVyc2hpcCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl19fQ.Aj40fUgmPygWVULtPgVWbpIsgbZjBp-KE_ZudRYpZ90PPGSbRf3hLEHZ9nkCKhYX7xCmTOtdYHnjhEwIFN1Xng~"; + +const val exampleSdJwt = "eyJhbGciOiJFZERTQSJ9.eyJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsIl9zZCI6WyJ4U1poOVJaU0RQOEhnNXllZ3Vlck9TbnZ2UXFDcjF4RmdQc1g4U1A4Qkk4Il19.5rxobR0a1hT8WUCN86p8rbQuUsBEcLLuE7p6UBSfAzCgbaF0ZxbXcnAPPwGnBQVcETx7QAmxCJNudfQ5n4tADg~WyIyVmpiY3FEd3RtdTNiWHlZaUJIbjZRIiwidmMiLHsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiJdLCJhd2FyZGVkRGF0ZSI6IjIwMjQtMDktMjNUMTg6MTI6MTIrMDAwMCIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkZW50aXR5IjpbeyJoYXNoZWQiOmZhbHNlLCJpZGVudGl0eUhhc2giOiJKb2huIFNtaXRoIiwiaWRlbnRpdHlUeXBlIjoibmFtZSIsInNhbHQiOiJub3QtdXNlZCIsInR5cGUiOiJJZGVudGl0eU9iamVjdCJ9LHsiaGFzaGVkIjpmYWxzZSwiaWRlbnRpdHlIYXNoIjoiam9obi5zbWl0aEBleGFtcGxlLmNvbSIsImlkZW50aXR5VHlwZSI6ImVtYWlsQWRkcmVzcyIsInNhbHQiOiJub3QtdXNlZCIsInR5cGUiOiJJZGVudGl0eU9iamVjdCJ9XSwiYWNoaWV2ZW1lbnQiOnsibmFtZSI6IkNvbG9yYWRvRldEIFRlYW0gTWVtYmVyc2hpcCIsInR5cGUiOiJBY2hpZXZlbWVudCJ9fSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0ltTnlkaUk2SWxBdE1qVTJJaXdpYTNSNUlqb2lSVU1pTENKNElqb2liV0pVTTJkcU9XRnZPR051UzI4ME0wcHJjVlJQVW1OSlFWSTRNRmd3VFVGWFFXTkdZelp2UjFKTVl5SXNJbmtpT2lKaU9GVk9ZMGhETW1GSFEzSjFTVFowUWxSV1NWWTBkVzVaV0VWeVMwTTRaRFJuUlRGR1owczBRMDVKSW4wIzAiLCJuYW1lIjoiQ29sb3JhZG8gV29ya2ZvcmNlIERldmVsb3BtZW50IENvdW5jaWwiLCJ0eXBlIjoiUHJvZmlsZSJ9LCJuYW1lIjoiQ29sb3JhZG9GV0RUZWFtTWVtYmVyc2hpcCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl19XQ~" + diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt index a93f2c9..fd00a5a 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt @@ -66,6 +66,7 @@ class AchievementCredentialItem { constructor(rawCredential: String, onDelete: (() -> Unit)? = null) { val decodedSdJwt = decodeRevealSdJwt(rawCredential) + this.credential = JSONObject(decodedSdJwt) this.onDelete = onDelete } @@ -244,12 +245,15 @@ class AchievementCredentialItem { @Composable fun detailsComponent() { - val awardedDate = keyPathFinder(credential, mutableListOf("awardedDate")).toString() + println("credential: $credential") + // NOTE: The credential contains a `vc` property with the verifiable credential payload. + val awardedDate = keyPathFinder(credential, mutableListOf("vc", "awardedDate")).toString() + println("awardedDate: $awardedDate") val ISO8601DateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]Z") val parsedDate = OffsetDateTime.parse(awardedDate, ISO8601DateFormat) val dateTimeFormatter = DateTimeFormatter.ofPattern("MMM dd, yyyy 'at' h:mm a") - val identity = keyPathFinder(credential, mutableListOf("credentialSubject", "identity")) as JSONArray + val identity = keyPathFinder(credential, mutableListOf("vc", "credentialSubject", "identity")) as JSONArray val details = MutableList(identity.length()) { i -> val obj = identity.get(i) as JSONObject Pair(obj["identityType"].toString(), obj["identityHash"].toString()) diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt index 64914b0..6e6683c 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt @@ -2,9 +2,11 @@ package com.spruceid.mobilesdkexample.wallet import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.navigation.NavController import com.spruceid.mobilesdkexample.ScanningComponent @@ -12,18 +14,29 @@ import com.spruceid.mobilesdkexample.ScanningType import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.spruceid.mobile.sdk.oid4vp.CredentialPresentation import com.spruceid.mobile.sdk.oid4vp.OID4VPHolder +import com.spruceid.mobile.sdk.oid4vp.RequestSigner import com.spruceid.mobile.sdk.oid4vp.SelectCredentialsView import com.spruceid.mobile.sdk.rs.Credential +import com.spruceid.mobile.sdk.rs.Holder import com.spruceid.mobile.sdk.rs.ParsedCredential +import com.spruceid.mobile.sdk.rs.SdJwt import com.spruceid.mobile.sdk.rs.Uuid +import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun DispatchQRView( - navController: NavController + navController: NavController, + rawCredentialsViewModel: IRawCredentialsViewModel, ) { + val scope = rememberCoroutineScope() + + val rawCredentials by rawCredentialsViewModel.rawCredentials.collectAsState() + + var credentials by remember { mutableStateOf(emptyList()) } + // maintain of state of credentials that once loaded will be used // to lazy load the component view. var parsedCredentials by remember { mutableStateOf(emptyList()) } @@ -31,6 +44,8 @@ fun DispatchQRView( // Selected Credential ID to be used in the future. var selectedCredentialId by remember { mutableStateOf(null) } + var holder by remember { mutableStateOf(null) } + // A function that sets the parsed credentials based on the Rust callback. fun onCredentials(credentialList: List) { parsedCredentials = credentialList @@ -42,26 +57,56 @@ fun DispatchQRView( } fun onRead(url: String) { + println("Reading URL: $url") + + if (credentials.isEmpty()) { + credentials = rawCredentials.map { rawCredential -> + ParsedCredential + .newSdJwt(SdJwt.newFromCompactSdJwt(rawCredential.rawCredential)) + .intoGenericForm() + } + } + + println("Credentials: $credentials") + // TODO: Should this be a different scope? - GlobalScope.launch { - // TODO: Need to find the key from the key manager. - val keyId = "INSERT KEY ID HERE" - - // TODO: Change this to a VDC collection in the future. - // TODO: Use the credential datastore to retrieve credentials. - val credentials = emptyList() - - // TODO: Add an entry in utils file for trusted DIDs. - val trustedDids = emptyList() - - OID4VPHolder( - keyId, - credentials, - trustedDids - ).handle_url(url, CredentialPresentation( - ::onCredentials, - ::onSelectedCredential - )) + scope.launch { +// // Convert rawCredentials to a list of ParsedCredentials + + + +// +// val keyId = "INSERT KEY ID HERE" +// val trustedDIDs = emptyList() +// +// +// holder = Holder.newWithCredentials( +// credentials, +// RequestSigner(keyId), +// trustedDIDs +// ) +// +//// // TODO: Need to find the key from the key manager. +//// val keyId = "INSERT KEY ID HERE" +//// +//// // TODO: Change this to a VDC collection in the future. +//// // TODO: Use the credential datastore to retrieve credentials. +//// val credentials = emptyList() +//// +//// // TODO: Add an entry in utils file for trusted DIDs. +//// val trustedDids = emptyList() +//// +//// // TODO: +//// val response = OID4VPHolder( +//// keyId, +//// credentials, +//// trustedDids +//// ).handleUrl(url, CredentialPresentation( +//// ::onCredentials, +//// ::onSelectedCredential +//// )) +//// +//// // TODO: dispatchUrl(response) } } 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 1ed162f..d5e4c93 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt @@ -34,11 +34,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.spruceid.mobilesdkexample.R +import com.spruceid.mobilesdkexample.db.RawCredentials import com.spruceid.mobilesdkexample.navigation.Screen 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.exampleSdJwt import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel import kotlinx.coroutines.launch