Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ androidx-test-runner = "1.7.0"
androidx-test-ext-junit = "1.3.0"
androidx-test-rules = "1.7.0"
androidx-test-uiautomator = "2.3.0"
androidx-credentials-registry = "1.0.0-alpha04"

# HTTP
okio = "3.17.0"
Expand Down Expand Up @@ -273,6 +274,11 @@ androidx-test-runner = { module = "androidx.test:runner", version.ref = "android
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" }
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-test-uiautomator" }
androidx-credentials-registry-mdoc = { module = "androidx.credentials.registry:registry-digitalcredentials-mdoc", version.ref = "androidx-credentials-registry" }
androidx-credentials-registry-openid = { module = "androidx.credentials.registry:registry-digitalcredentials-openid", version.ref = "androidx-credentials-registry" }
androidx-credentials-registry-sdjwtvc = { module = "androidx.credentials.registry:registry-digitalcredentials-sdjwtvc", version.ref = "androidx-credentials-registry" }
androidx-credentials-registry-provider = { module = "androidx.credentials.registry:registry-provider", version.ref = "androidx-credentials-registry" }
androidx-credentials-registry-provider-play-services = { module = "androidx.credentials.registry:registry-provider-play-services", version.ref = "androidx-credentials-registry" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }

## HTTP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ android {
dependencies {
implementation(project(":waltid-applications:waltid-wallet-demo-compose:sharedLogic"))
implementation(project(":waltid-applications:waltid-wallet-demo-compose:sharedUI"))
implementation(project(":waltid-libraries:protocols:waltid-openid4vc-wallet-mobile"))
implementation(identityLibs.androidx.activity.compose)
implementation(identityLibs.androidx.credentials.registry.provider)
implementation(identityLibs.kotlinx.coroutines.android)
implementation(identityLibs.kotlinx.serialization.json)

androidTestImplementation(identityLibs.androidx.test.ext.junit)
androidTestImplementation(identityLibs.androidx.test.runner)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,15 @@
<data android:scheme="openid4vp" />
</intent-filter>
</activity>
<activity
android:name=".DigitalCredentialProviderActivity"
android:exported="true"
android:excludeFromRecents="true"
android:theme="@style/Theme.WalletDemoCompose">
<intent-filter>
<action android:name="androidx.credentials.registry.provider.action.GET_CREDENTIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBlzCCAT2gAwIBAgIUZFEF4iwIsLuJO7pJ9bU7vo9Dg3kwCgYIKoZIzj0EAwIw
FTETMBEGA1UEAwwKQ3VzdG9tUm9vdDAeFw0yNjAxMjIxNTU1NDJaFw0zNjAxMjAx
NTU1NDJaMB0xGzAZBgNVBAMMEkN1c3RvbUludGVybWVkaWF0ZTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABAvlBFSSRWetJJSj5rvGoXtPnfw97YRHbJj4/kspQbSw
xVN3RtofsSu0DevrISGx2MCPqqxHXdfSeu9SKgen6IOjYzBhMA8GA1UdEwEB/wQF
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlYd5tyusuYAARxiLg+4m0
0fgBujAfBgNVHSMEGDAWgBQ+D1YkeDpF+qaxAhlnb3XSkGZWCTAKBggqhkjOPQQD
AgNIADBFAiEA789kIQsGTa/GJEgYaOID9VVoO0PyeeYEwub7P0a1+ZICIHI9bYi7
2XTca9e8rqGJuYmKz8qEQodLvaXdgwCfQ4KZ
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "com.android.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.firefox",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
}
]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package id.walt.walletdemo.compose.android

import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import id.walt.wallet2.mobile.AndroidDigitalCredentialProvider
import id.walt.wallet2.mobile.MobileWallet
import id.walt.wallet2.mobile.MobileWalletAnnexCPreview
import id.walt.wallet2.mobile.MobileWalletAnnexCRequest
import id.walt.wallet2.mobile.MobileWalletAnnexCSubmission
import id.walt.wallet2.mobile.MobileWalletConfig
import id.walt.wallet2.mobile.MobileWalletDigitalCredentialProtocols
import id.walt.wallet2.mobile.MobileWalletDigitalCredentialPreview
import id.walt.wallet2.mobile.MobileWalletFactory
import id.walt.wallet2.mobile.MobileWalletPresentationCredentialOption
import id.walt.wallet2.mobile.MobileWalletPresentationCredentialSelection
import id.walt.wallet2.data.WalletX509TrustConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

/** Credential Manager provider UI owned by the demo app, not by the reusable KMP SDK. */
class DigitalCredentialProviderActivity : ComponentActivity() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val resultIntent = Intent()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
scope.launch {
runCatching {
val allowlist = assets.open("privileged_apps.json").bufferedReader().use { it.readText() }
val demoTrustAnchor = assets.open("dc_api_demo_trust_anchor.pem").bufferedReader().use { it.readText() }
val input = AndroidDigitalCredentialProvider.extract(intent, allowlist)
val wallet = MobileWalletFactory(applicationContext).create(
MobileWalletConfig(
requestObjectX509Trust = WalletX509TrustConfig(
trustAnchorPemCertificates = listOf(demoTrustAnchor),
allowedRequestObjectAlgorithms = setOf("ES256", "ES384", "ES512"),
)
)
)
wallet.bootstrap()
if (input.request.protocol == MobileWalletDigitalCredentialProtocols.ISO_MDOC_ANNEX_C) {
val data = Json.parseToJsonElement(input.request.dataJson).jsonObject
val deviceRequest = requireNotNull(data["deviceRequest"]?.jsonPrimitive?.content) {
"Annex C deviceRequest is required"
}
val encryptionInfo = requireNotNull(data["encryptionInfo"]?.jsonPrimitive?.content) {
"Annex C encryptionInfo is required"
}
val preview = wallet.previewAnnexCPresentation(
MobileWalletAnnexCRequest(
parsedRequest = wallet.parseAnnexCDeviceRequest(deviceRequest),
verifiedOrigin = input.request.verifiedOrigin,
selectedRegistryEntryIds = input.request.selectedRegistryEntryIds,
deviceRequestBase64Url = deviceRequest,
encryptionInfoBase64Url = encryptionInfo,
)
)
selectCredentials(preview.credentialOptions) { selections, selectedOptions ->
showAnnexCConsent(
wallet,
preview,
deviceRequest,
encryptionInfo,
selections,
selectedOptions,
)
}
} else {
val preview = wallet.previewDigitalCredentialPresentation(input.request)
selectCredentials(preview.credentialOptions) { selections, selectedOptions ->
showConsent(wallet, preview, selections, selectedOptions)
}
}
}.onFailure {
reportFailure(it)
}
}
}

private fun showAnnexCConsent(
wallet: MobileWallet,
preview: MobileWalletAnnexCPreview,
deviceRequest: String,
encryptionInfo: String,
selections: List<MobileWalletPresentationCredentialSelection>,
selectedOptions: List<MobileWalletPresentationCredentialOption>,
) {
val trust = when (val value = preview.readerTrust) {
is id.walt.wallet2.mobile.MobileWalletReaderTrust.Trusted -> "Trusted reader: ${value.certificateSubject}"
is id.walt.wallet2.mobile.MobileWalletReaderTrust.Unverified -> "Unverified reader: ${value.reason}"
id.walt.wallet2.mobile.MobileWalletReaderTrust.NotApplicable -> "Reader trust not applicable"
}
val claimLines = selectedOptions.flatMap { option ->
option.disclosures.map { disclosure ->
"${disclosure.name ?: disclosure.path}: ${disclosure.displayValue ?: disclosure.valueJson}"
}
}.distinct()
val credentialLines = selectedOptions.map(::credentialTitle).distinct()
AlertDialog.Builder(this)
.setTitle("Share mobile document?")
.setMessage(
buildString {
append("Requester: ${preview.verifiedOrigin}\n")
append("$trust\n")
append("\nCredential${if (credentialLines.size == 1) "" else "s"}:\n${credentialLines.joinToString("\n")}\n")
if (claimLines.isNotEmpty()) append("\nData to share:\n${claimLines.joinToString("\n")}")
}
)
.setCancelable(false)
.setNegativeButton("Cancel") { _, _ ->
AndroidDigitalCredentialProvider.setCancellation(resultIntent)
finishProviderResult()
}
.setPositiveButton("Share") { _, _ ->
scope.launch {
runCatching {
wallet.submitAnnexCPresentation(
MobileWalletAnnexCSubmission(
requestId = preview.requestId,
verifiedOrigin = preview.verifiedOrigin,
deviceRequestBase64Url = deviceRequest,
encryptionInfoBase64Url = encryptionInfo,
selectedCredentialOptions = selections,
)
)
}.onSuccess { response ->
AndroidDigitalCredentialProvider.setResponse(resultIntent, response)
finishProviderResult()
}.onFailure {
reportFailure(it)
}
}
}
.show()
}

private fun showConsent(
wallet: MobileWallet,
preview: MobileWalletDigitalCredentialPreview,
selections: List<MobileWalletPresentationCredentialSelection>,
selectedOptions: List<MobileWalletPresentationCredentialOption>,
) {
val claimLines = selectedOptions.flatMap { option ->
option.disclosures.filter { it.required || !it.selectable }.map { disclosure ->
"${disclosure.name ?: disclosure.path}: ${disclosure.displayValue ?: disclosure.valueJson}"
}
}.distinct()
val credentialLines = selectedOptions.map(::credentialTitle).distinct()
val message = buildString {
append("Requester: ${preview.request.verifierName ?: preview.verifiedOrigin}\n")
append("Protocol: ${preview.protocol}\n")
append("Response encryption: ${if (preview.encryption.isRequired) "required" else "not requested"}\n")
append("\nCredential${if (credentialLines.size == 1) "" else "s"}:\n${credentialLines.joinToString("\n")}\n")
if (claimLines.isNotEmpty()) append("\nData to share:\n${claimLines.joinToString("\n")}")
}
AlertDialog.Builder(this)
.setTitle("Share digital credential?")
.setMessage(message)
.setCancelable(false)
.setNegativeButton("Cancel") { _, _ ->
AndroidDigitalCredentialProvider.setCancellation(resultIntent)
finishProviderResult()
}
.setPositiveButton("Share") { _, _ -> submitAfterConsent(wallet, preview, selections) }
.show()
}

private fun submitAfterConsent(
wallet: MobileWallet,
preview: MobileWalletDigitalCredentialPreview,
selections: List<MobileWalletPresentationCredentialSelection>,
) {
scope.launch {
runCatching {
wallet.submitDigitalCredentialPresentation(
requestId = preview.requestId,
selectedCredentialOptions = selections,
selectedDisclosureOptions = emptyList(),
)
}.onSuccess { response ->
AndroidDigitalCredentialProvider.setResponse(resultIntent, response)
finishProviderResult()
}.onFailure {
reportFailure(it)
}
}
}

private fun selectCredentials(
options: List<MobileWalletPresentationCredentialOption>,
onSelected: (
List<MobileWalletPresentationCredentialSelection>,
List<MobileWalletPresentationCredentialOption>,
) -> Unit,
) {
val groups = options.groupBy { it.queryId }.toSortedMap().toList()
require(groups.isNotEmpty()) { "No credential satisfies this presentation request" }
selectCredential(groups, 0, mutableListOf(), onSelected)
}

private fun selectCredential(
groups: List<Pair<String, List<MobileWalletPresentationCredentialOption>>>,
index: Int,
selected: MutableList<MobileWalletPresentationCredentialOption>,
onSelected: (
List<MobileWalletPresentationCredentialSelection>,
List<MobileWalletPresentationCredentialOption>,
) -> Unit,
) {
if (index == groups.size) {
onSelected(
selected.map { MobileWalletPresentationCredentialSelection(it.queryId, it.credentialId) },
selected.toList(),
)
return
}

val options = groups[index].second
require(options.isNotEmpty()) { "No credential satisfies one of the presentation queries" }
if (options.size == 1) {
selected += options.single()
selectCredential(groups, index + 1, selected, onSelected)
return
}

AlertDialog.Builder(this)
.setTitle("Choose a credential")
.setItems(options.map(::credentialTitle).toTypedArray()) { _, selectedIndex ->
selected += options[selectedIndex]
selectCredential(groups, index + 1, selected, onSelected)
}
.setCancelable(false)
.setNegativeButton("Cancel") { _, _ ->
AndroidDigitalCredentialProvider.setCancellation(resultIntent)
finishProviderResult()
}
.show()
}

private fun credentialTitle(option: MobileWalletPresentationCredentialOption): String =
listOfNotNull(option.label, option.issuer, option.subject)
.distinct()
.joinToString(" · ")
.ifEmpty { option.credentialId }

private fun reportFailure(error: Throwable) {
Log.e(TAG, "Digital credential presentation failed (${error::class.simpleName})")
AndroidDigitalCredentialProvider.setFailure(resultIntent)
finishProviderResult()
}

private fun finishProviderResult() {
setResult(RESULT_OK, resultIntent)
finish()
}

override fun onDestroy() {
scope.cancel()
super.onDestroy()
}

private companion object {
private const val TAG = "WaltDigitalCredentials"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.id.walt.wallet.demo</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)id.walt.wallet.shared</string>
</array>
<key>com.apple.developer.identity-document-services.document-provider.mobile-document-types</key>
<array>
<string>org.iso.18013.5.1.mDL</string>
<string>eu.europa.ec.eudi.pid.1</string>
<string>eu.europa.ec.eudi.photoid.1</string>
</array>
</dict>
</plist>
Loading
Loading