Skip to content

Commit

Permalink
Implement 18013-7 Kotlin and Android Showcase (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
cobward authored Jan 17, 2025
1 parent 9567043 commit 6a30188
Show file tree
Hide file tree
Showing 14 changed files with 687 additions and 14 deletions.
2 changes: 1 addition & 1 deletion MobileSdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ android {
}

dependencies {
api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.5.1")
api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.7.0")
//noinspection GradleCompatible
implementation("com.android.support:appcompat-v7:28.0.0")
/* Begin UI dependencies */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.spruceid.mobile.sdk
import android.bluetooth.BluetoothManager
import android.content.Context
import android.util.Log
import com.spruceid.mobile.sdk.rs.CryptoCurveUtils
import com.spruceid.mobile.sdk.rs.ItemsRequest
import com.spruceid.mobile.sdk.rs.MdlPresentationSession
import com.spruceid.mobile.sdk.rs.Mdoc
Expand Down Expand Up @@ -73,7 +74,10 @@ class IsoMdlPresentation(
signer.update(payload)

val signature = signer.sign()
val response = session!!.submitResponse(signature)
val normalizedSignature =
CryptoCurveUtils.secp256r1().ensureRawFixedWidthSignatureEncoding(signature)
?: throw Error("unrecognized signature encoding")
val response = session!!.submitResponse(normalizedSignature)
this.bleManager!!.send(response)
} catch (e: Error) {
Log.e("CredentialsViewModel.submitNamespaces", e.toString())
Expand Down
22 changes: 21 additions & 1 deletion MobileSdk/src/main/java/com/spruceid/mobile/sdk/KeyManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import com.spruceid.mobile.sdk.rs.CryptoCurveUtils
import com.spruceid.mobile.sdk.rs.KeyAlias
import com.spruceid.mobile.sdk.rs.KeyStore as SpruceKitKeyStore
import com.spruceid.mobile.sdk.rs.SigningKey
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.Signature
Expand All @@ -19,7 +23,7 @@ import javax.crypto.spec.GCMParameterSpec
/**
* Implementation of the secure key management with Strongbox and TEE as backup.
*/
class KeyManager {
class KeyManager: SpruceKitKeyStore {

/**
* Returns the Android Keystore.
Expand Down Expand Up @@ -311,4 +315,20 @@ class KeyManager {
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
return cipher.doFinal(payload)
}

override fun getSigningKey(alias: KeyAlias): SigningKey {
val jwk = this.getJwk(alias) ?: throw Error("key not found");
return P256SigningKey(alias, jwk)
}
}

class P256SigningKey(private val alias: String, private val jwk: String) : SigningKey {

override fun jwk(): String = this.jwk

override fun sign(payload: ByteArray): ByteArray {
val derSignature = KeyManager().signPayload(alias, payload) ?: throw Error("key not found");
return CryptoCurveUtils.secp256r1().ensureRawFixedWidthSignatureEncoding(derSignature) ?:
throw Error("signature encoding not recognized");
}
}
2 changes: 1 addition & 1 deletion example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,4 @@ dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
}
1 change: 1 addition & 0 deletions example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<!-- Deep link data -->
<data android:scheme="spruceid" />
<data android:scheme="openid4vp" />
<data android:scheme="mdoc-openid4vp" />
</intent-filter>
</activity>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ class MainActivity : ComponentActivity() {
intent.data.toString().replace("openid4vp://", "")
)
)
} else if (intent.data!!.toString().startsWith("mdoc-openid4vp")) {
navController.navigate(
Screen.HandleMdocOID4VP.route.replace(
"{url}",
intent.data.toString().replace("mdoc-openid4vp://", "")
)
)
}
} else {
super.onNewIntent(intent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.spruceid.mobile.sdk.CredentialPack
import com.spruceid.mobile.sdk.CredentialStatusList
import com.spruceid.mobile.sdk.jsonEncodedDetailsAll
import com.spruceid.mobile.sdk.ui.BaseCard
import com.spruceid.mobile.sdk.ui.CardRenderingDetailsField
import com.spruceid.mobile.sdk.ui.CardRenderingDetailsView
Expand Down Expand Up @@ -88,13 +89,19 @@ class GenericCredentialItem : ICredentialView {
val statusLists by statusListViewModel.statusLists.collectAsState()
val credential = values.toList().firstNotNullOfOrNull {
val cred = credentialPack.getCredentialById(it.first)
val mdoc = cred?.asMsoMdoc()
try {
if (
cred?.asJwtVc() != null ||
cred?.asJsonVc() != null ||
cred?.asSdJwt() != null
) {
it.second
} else if (mdoc != null){
// Assume mDL.
val details = mdoc.jsonEncodedDetailsAll()
it.second.put("issuer", details.get("issuing_authority"))
it.second
} else {
null
}
Expand All @@ -116,6 +123,14 @@ class GenericCredentialItem : ICredentialView {
}
}

if (description.isBlank()) {
try {
description = credential?.getString("issuer").toString()
} catch (_: Exception) {
}
}


Column {
Text(
text = description,
Expand Down Expand Up @@ -195,12 +210,17 @@ class GenericCredentialItem : ICredentialView {
val credential = values.toList().firstNotNullOfOrNull {
val cred = credentialPack.getCredentialById(it.first)
try {
val mdoc = cred?.asMsoMdoc()
if (
cred?.asJwtVc() != null ||
cred?.asJsonVc() != null ||
cred?.asSdJwt() != null
) {
it.second
} else if (mdoc != null){
// Assume mDL.
it.second.put("name", "Mobile Drivers License")
it.second
} else {
null
}
Expand Down Expand Up @@ -259,12 +279,17 @@ class GenericCredentialItem : ICredentialView {
val credential = values.toList().firstNotNullOfOrNull {
val cred = credentialPack.getCredentialById(it.first)
try {
val mdoc = cred?.asMsoMdoc()
if (
cred?.asJwtVc() != null ||
cred?.asJsonVc() != null ||
cred?.asSdJwt() != null
) {
it.second
} else if (mdoc != null){
// Assume mDL.
it.second.put("name", "Mobile Drivers License")
it.second
} else {
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const val ADD_TO_WALLET_PATH = "add_to_wallet/{rawCredential}"
const val SCAN_QR_PATH = "scan_qr"
const val HANDLE_OID4VCI_PATH = "oid4vci/{url}"
const val HANDLE_OID4VP_PATH = "oid4vp/{url}"
const val HANDLE_MDOC_OID4VP_PATH = "mdoc_oid4vp/{url}"

sealed class Screen(val route: String) {
object HomeScreen : Screen(HOME_SCREEN_PATH)
Expand All @@ -32,4 +33,5 @@ sealed class Screen(val route: String) {
object ScanQRScreen : Screen(SCAN_QR_PATH)
object HandleOID4VCI : Screen(HANDLE_OID4VCI_PATH)
object HandleOID4VP : Screen(HANDLE_OID4VP_PATH)
object HandleMdocOID4VP : Screen(HANDLE_MDOC_OID4VP_PATH)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewMode
import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel
import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel
import com.spruceid.mobilesdkexample.wallet.DispatchQRView
import com.spruceid.mobilesdkexample.wallet.HandleMdocOID4VPView
import com.spruceid.mobilesdkexample.wallet.HandleOID4VCIView
import com.spruceid.mobilesdkexample.wallet.HandleOID4VPView
import com.spruceid.mobilesdkexample.walletsettings.WalletSettingsActivityLogScreen
Expand Down Expand Up @@ -187,5 +188,20 @@ fun SetupNavGraph(
walletActivityLogsViewModel
)
}
composable(
route = Screen.HandleMdocOID4VP.route,
deepLinks = listOf(navDeepLink { uriPattern = "mdoc-openid4vp://{url}" })
) { backStackEntry ->
var url = backStackEntry.arguments?.getString("url")!!
if (!url.startsWith("mdoc-openid4vp")) {
url = "mdoc-openid4vp://$url"
}
HandleMdocOID4VPView(
navController,
url,
credentialPacksViewModel,
walletActivityLogsViewModel
)
}
}
}
19 changes: 17 additions & 2 deletions example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,9 @@ fun getCredentialIdTitleAndIssuer(
credentialPack: CredentialPack,
credential: ParsedCredential? = null
): Triple<String, String, String> {
val claims = credentialPack.findCredentialClaims(listOf("name", "type", "issuer"))
val claims = credentialPack.findCredentialClaims(listOf("name", "type", "issuer", "issuing_authority"))

var cred = if (credential != null) {
val cred = if (credential != null) {
claims.entries.firstNotNullOf { claim ->
if (claim.key == credential.id()) {
claim
Expand All @@ -233,12 +233,20 @@ fun getCredentialIdTitleAndIssuer(
} else {
claims.entries.firstNotNullOf { claim ->
val c = credentialPack.getCredentialById(claim.key)
val mdoc = c?.asMsoMdoc()
if (
c?.asSdJwt() != null ||
c?.asJwtVc() != null ||
c?.asJsonVc() != null
) {
claim
} else if (mdoc != null) {
// Assume mDL.
val issuer = claim.value.get("issuing_authority")
claim.value.put("issuer", issuer)
val title = "Mobile Drivers License"
claim.value.put("name", title)
claim
} else {
null
}
Expand Down Expand Up @@ -276,5 +284,12 @@ fun getCredentialIdTitleAndIssuer(
}
}

if (issuer.isBlank()) {
try {
issuer = credentialValue.getString("issuer")
} catch (_: Exception) {
}
}

return Triple(credentialKey, title, issuer)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets

// The scheme for the OID4VP QR code.
const val OID4VP_SCHEME = "openid4vp://"
const val MDOC_OID4VP_SCHEME = "mdoc-openid4vp://"
// The scheme for the OID4VCI QR code.
const val OID4VCI_SCHEME = "openid-credential-offer://"
// The schemes for HTTP/HTTPS QR code.
Expand Down Expand Up @@ -67,6 +68,13 @@ fun DispatchQRView(
} else if (payload.startsWith(HTTP_SCHEME) || payload.startsWith(HTTPS_SCHEME)) {
uriHandler.openUri(payload)
back()
} else if (payload.startsWith(MDOC_OID4VP_SCHEME)) {
val encodedUrl = URLEncoder.encode(payload, StandardCharsets.UTF_8.toString())

navController.navigate("mdoc_oid4vp/$encodedUrl") {
launchSingleTop = true
restoreState = true
}
} else {
err = "The QR code you have scanned is not supported. QR code payload: $payload"
}
Expand Down
Loading

0 comments on commit 6a30188

Please sign in to comment.