Skip to content
Open
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
90 changes: 26 additions & 64 deletions shared/src/androidMain/kotlin/main.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,11 @@ import androidx.credentials.registry.provider.selectedEntryId
import at.asitplus.KmmResult
import at.asitplus.catching
import at.asitplus.dcapi.*
import at.asitplus.dcapi.request.ExchangeProtocolIdentifier
import at.asitplus.dcapi.request.verifier.DigitalCredentialGetRequest
import at.asitplus.dcapi.request.verifier.DigitalCredentialRequestOptions
import at.asitplus.dcapi.request.verifier.toRequestParametersFrom
import at.asitplus.openid.RequestParametersFrom
import at.asitplus.signum.indispensable.cosef.io.coseCompliantSerializer
import at.asitplus.signum.indispensable.josef.io.joseCompliantSerializer
import at.asitplus.signum.indispensable.josef.typed
import at.asitplus.wallet.app.android.dcapi.AndroidDCAPIInvocationData
import at.asitplus.wallet.app.android.dcapi.CustomRegistry
import at.asitplus.wallet.app.common.*
Expand All @@ -55,7 +53,6 @@ import org.multipaz.prompt.PromptModel
import ui.theme.darkScheme
import ui.theme.lightScheme
import java.io.File
import kotlin.io.encoding.ExperimentalEncodingApi


actual fun getPlatformName(): String = "Android"
Expand Down Expand Up @@ -312,7 +309,7 @@ public class AndroidPlatformAdapter(
)
}

@OptIn(ExperimentalDigitalCredentialApi::class, ExperimentalEncodingApi::class)
@OptIn(ExperimentalDigitalCredentialApi::class)
override fun getCurrentDCAPIVerificationData(): KmmResult<RequestParametersFrom.DcApiRequest> = catching {
(intentState.dcapiInvocationData.value as AndroidDCAPIInvocationData?)?.let { (intent, _) ->
// Adapted from https://github.com/openwallet-foundation-labs/identity-credential/blob/d7a37a5c672ed6fe1d863cbaeb1a998314d19fc5/wallet/src/main/java/com/android/identity_credential/wallet/credman/CredmanPresentationActivity.kt#L74
Expand All @@ -325,63 +322,25 @@ public class AndroidPlatformAdapter(
?: throw IllegalArgumentException("Expected GetDigitalCredentialOption object not received")

Napier.d("DC API: Got request ${option.requestJson}")
// Android's Bundle-to-JSON conversion serializes arrays as numerically-indexed objects
// (e.g. ["a","b"] becomes {"0":"a","1":"b"}), which breaks deserialization of list
// properties such as `redirect_uris` in client_metadata.
val rawRequest = joseCompliantSerializer.parseToJsonElement(option.requestJson)
.coerceIndexedObjectsToArrays()
// Android's Bundle-to-JSON conversion currently serializes arrays as numerically-indexed objects
// (e.g. ["a","b"] becomes {"0":"a","1":"b"}), which breaks deserialization of list properties.
val dcRequestOptions = joseCompliantSerializer.decodeFromJsonElement(
DigitalCredentialRequestOptions.serializer(),
rawRequest,
joseCompliantSerializer.parseToJsonElement(option.requestJson).coerceIndexedObjectsToArrays(),
)

val selectionInfo = getSetSelection(credentialRequest)
?: getSelection(credentialRequest)

val digitalCredentialGetRequest =
dcRequestOptions.requests.find { it.protocol == ExchangeProtocolIdentifier(selectionInfo.protocol) }
?: throw IllegalStateException("Unable to find suitable DC API request. Protocol may not be supported.")

Napier.d("DC API: Selection $selectionInfo")

val credentialIds = selectionInfo.documentIds

when (digitalCredentialGetRequest) {
is DigitalCredentialGetRequest.OpenId4VpSigned -> {
Napier.d("Using OpenID4VP Signed, got request $digitalCredentialGetRequest for credential IDs $credentialIds")
RequestParametersFrom.OpenId4VpDcApiSigned(
jwsTyped = digitalCredentialGetRequest.data.request.typed(),
credentialIds = credentialIds,
callingPackageName = callingPackageName,
callingOrigin = callingOrigin
)
}
is DigitalCredentialGetRequest.OpenId4VpMultiSigned -> {
TODO("OpenID4VP multisigned DC API requests are not supported yet")
}
is DigitalCredentialGetRequest.OpenId4VpUnsigned -> {
Napier.d("Using OpenID4VP Unsigned, got request $digitalCredentialGetRequest for credential IDs $credentialIds")
RequestParametersFrom.OpenId4VpDcApiUnsigned(
parameters = digitalCredentialGetRequest.data,
jsonString = joseCompliantSerializer.encodeToString(digitalCredentialGetRequest.data),
credentialIds = credentialIds,
callingPackageName = callingPackageName,
callingOrigin = callingOrigin
)
}
is DigitalCredentialGetRequest.IsoMdoc -> {
Napier.d("Using Iso 18013-7 Annex C, got request $digitalCredentialGetRequest for credential IDs $credentialIds")
RequestParametersFrom.IsoMdocDcApi(
parameters = RequestParametersFrom.IsoMdocDcApi.IsoMdocRequestWrapper(
digitalCredentialGetRequest.data
),
jsonString = joseCompliantSerializer.encodeToString(digitalCredentialGetRequest.data),
credentialIds = credentialIds,
callingPackageName = callingPackageName,
callingOrigin = callingOrigin
)
}
}
dcRequestOptions.toRequestParametersFrom(
selectedProtocol = selectionInfo.protocol,
credentialIds = credentialIds,
callingPackageName = callingPackageName,
callingOrigin = callingOrigin,
)
} ?: throw IllegalStateException("DCAPIInvocationData not set")
}

Expand All @@ -403,23 +362,25 @@ public class AndroidPlatformAdapter(
} ?: throw IllegalStateException("DCAPIInvocationData not set")
}

override fun prepareDCAPICredentialResponse(response: String, success: Boolean) {
override fun prepareDCAPICredentialResponse(response: DigitalCredentialInterface) {
(intentState.dcapiInvocationData.value as AndroidDCAPIInvocationData?)?.let { (_, sendCredentialResponseToInvoker) ->
sendCredentialResponseToInvoker(response, success)
intentState.dcapiInvocationData.value = null
val serializedResponse = response.toAndroidDcApiResponseJson()
Napier.d("Returning response $serializedResponse")
try {
sendCredentialResponseToInvoker(serializedResponse, true)
} finally {
intentState.dcapiInvocationData.value = null
}
} ?: throw IllegalStateException("Callback for response not found")
}

override fun prepareIsoMdocDCAPICredentialResponse(response: EncryptedResponse, success: Boolean) {
override fun prepareDCAPICredentialError(error: String) {
(intentState.dcapiInvocationData.value as AndroidDCAPIInvocationData?)?.let { (_, sendCredentialResponseToInvoker) ->
intentState.dcapiInvocationData.value = null
Napier.d("Returning response $response to digital credentials API invoker")
val dcApiResponse = DCAPIResponse(response)
// Needs to be cast to DigitalCredentialInterface so that protocol member is serialized
val isoMdocResponse: DigitalCredentialInterface = IsoMdocResponse(dcApiResponse)
val serializedResponse = joseCompliantSerializer.encodeToString(isoMdocResponse)
Napier.d("Returning response $serializedResponse")
sendCredentialResponseToInvoker(serializedResponse, success)
try {
sendCredentialResponseToInvoker(error, false)
} finally {
intentState.dcapiInvocationData.value = null
}
} ?: throw IllegalStateException("Callback for response not found")
}

Expand Down Expand Up @@ -483,6 +444,7 @@ public class AndroidPlatformAdapter(
JsonObject(fixedChildren)
}
}

is JsonArray -> JsonArray(map { it.coerceIndexedObjectsToArrays() })
else -> this
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ package at.asitplus.wallet.app.common

import androidx.compose.runtime.Composable
import at.asitplus.catchingUnwrapped
import at.asitplus.dif.Constraint
import at.asitplus.dif.ConstraintField
import at.asitplus.dif.ConstraintFilter
import at.asitplus.dif.DifInputDescriptor
import at.asitplus.dif.FormatContainerJwt
import at.asitplus.dif.FormatHolder
import at.asitplus.dif.InputDescriptor
import at.asitplus.iso.DocRequest
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.jsonpath.core.NormalizedJsonPathSegment.IndexSegment
import at.asitplus.jsonpath.core.NormalizedJsonPathSegment.NameSegment
Expand Down Expand Up @@ -307,26 +302,6 @@ fun NormalizedJsonPath.memberName(id: Int) =
fun NormalizedJsonPath.minus(name: String) =
NormalizedJsonPath(this.segments.filter { it !is NameSegment || it.memberName != name })

fun Array<DocRequest>.toDifInputDescriptorList() = this.map {
val itemsRequest = it.itemsRequest.value
DifInputDescriptor(
id = itemsRequest.docType,
format = FormatHolder(msoMdoc = FormatContainerJwt()),
constraints = Constraint(fields = itemsRequest.namespaces.flatMap { requestedNamespace ->
requestedNamespace.value.entries.map { requestedAttribute ->
ConstraintField(
path = listOf(
NormalizedJsonPath(
NameSegment(requestedNamespace.key),
NameSegment(requestedAttribute.dataElementIdentifier),
).toString()
), intentToRetain = requestedAttribute.intentToRetain
)
}
}.toSet())
)
}

@Composable
fun Triple<CredentialRepresentation, CredentialScheme, Collection<SingleClaimReference?>?>.toCredentialQueryUiModel(): DCQLCredentialQueryUiModel {
val (representation, scheme, attributePaths) = this
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import at.asitplus.wallet.lib.cbor.SignCoseDetached
import at.asitplus.wallet.lib.data.CredentialPresentation
import at.asitplus.wallet.lib.ktor.openid.OpenId4VpWallet
import at.asitplus.wallet.lib.openid.AuthorizationResponsePreparationState
import at.asitplus.wallet.lib.openid.DcApiPreparationState
import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient
import kotlinx.serialization.builtins.ByteArraySerializer
Expand Down Expand Up @@ -46,6 +47,12 @@ class PresentationService(
preparationState: AuthorizationResponsePreparationState
) = presentationService.getMatchingCredentials(preparationState)

suspend fun prepareDcApiRequest(request: RequestParametersFrom.DcApiRequest) =
presentationService.prepareDcApiRequest(request)

suspend fun getMatchingCredentials(preparationState: DcApiPreparationState) =
presentationService.getMatchingCredentials(preparationState)

suspend fun finalizeAuthorizationResponse(
credentialPresentation: CredentialPresentation,
preparationState: AuthorizationResponsePreparationState,
Expand All @@ -54,25 +61,18 @@ class PresentationService(
preparationState = preparationState
).getOrThrow()

@OptIn(ExperimentalStdlibApi::class)
suspend fun finalizeIsoMdocDCAPIPresentation(
credentialPresentation: CredentialPresentation.PresentationExchangePresentation,
isoMdocWalletRequest: RequestParametersFrom.IsoMdocDcApi
): OpenId4VpWallet.AuthenticationSuccess {
suspend fun finalizeDcApiPresentation(
credentialPresentation: CredentialPresentation,
preparationState: DcApiPreparationState,
) {
Napier.d("Finalizing DCAPI response")
val encryptedResponse = IsoMdocDcapiResponseBuilder.buildEncryptedResponse(
val response = presentationService.finalizeDcApiResponse(
state = preparationState,
credentialPresentation = credentialPresentation,
isoMdocWalletRequest = isoMdocWalletRequest,
keyMaterial = keyMaterial,
holderAgent = holderAgent,
)
platformAdapter.prepareIsoMdocDCAPICredentialResponse(encryptedResponse, true)
return OpenId4VpWallet.AuthenticationSuccess()
).getOrThrow()
platformAdapter.prepareDCAPICredentialResponse(response)
}

fun finalizeOpenId4VpDCAPIPresentation(response: String) =
platformAdapter.prepareDCAPICredentialResponse(response, true)

@OptIn(ExperimentalStdlibApi::class)
suspend fun finalizeLocalPresentation(
credentialPresentation: CredentialPresentation.PresentationExchangePresentation,
Expand Down
Loading
Loading