Skip to content

Commit

Permalink
Parse vci request and prepare for credential endpoint call.
Browse files Browse the repository at this point in the history
Note the credential endpoint part is neither tested nor invoked for now.
  • Loading branch information
QZHelen committed Nov 21, 2024
1 parent 960053a commit c6e45da
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 12 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.devtool.ksp)
}
Expand Down Expand Up @@ -40,6 +41,8 @@ android {
}

dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.registry.digitalcredentials.mdoc)
implementation(libs.androidx.registry.digitalcredentials.preview)
Expand All @@ -61,6 +64,7 @@ dependencies {
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
testImplementation(libs.junit)
implementation(libs.kotlinx.serialization.json)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand Down
1 change: 1 addition & 0 deletions app/src/main/assets/openid4vci_request.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
],
"issuer_metadata": {
"credential_issuer": "https://credential-issuer.example.com",
"credential_endpoint": "https://credential-issuer.example.com",
"credential_configurations_supported": {
"com.emvco.payment_card": {
"format": "mso_mdoc",
Expand Down
50 changes: 39 additions & 11 deletions app/src/main/java/com/credman/cmwallet/CreateCredentialActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import androidx.activity.ComponentActivity
import androidx.credentials.CreateCustomCredentialResponse
import androidx.credentials.DigitalCredential
import androidx.credentials.ExperimentalDigitalCredentialApi
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.registry.provider.selectedEntryId
import com.credman.cmwallet.CmWalletApplication.Companion.TAG
import com.credman.cmwallet.openid4vci.DATA
import com.credman.cmwallet.openid4vci.OpenId4VCI
import com.credman.cmwallet.openid4vci.PROTOCOL
import org.json.JSONObject

@OptIn(ExperimentalDigitalCredentialApi::class)
Expand All @@ -36,17 +39,42 @@ class CreateCredentialActivity : ComponentActivity() {
) ?: ""
Log.i(TAG, "[CreateCredentialActivity] origin $origin")

val testResponse = CreateCustomCredentialResponse(
type = DigitalCredential.TYPE_DIGITAL_CREDENTIAL,
data = Bundle().apply {
putString("androidx.credentials.BUNDLE_KEY_RESPONSE_JSON", "test response")
},
)
try {
// This will eventually be replaced by a structured Jetpack property,
// as opposed to having to parse a raw data from Bundle.
val requestJsonString: String = request.callingRequest.credentialData.getString(
"androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
)!!

val resultData = Intent()
PendingIntentHandler.setCreateCredentialResponse(resultData, testResponse)
setResult(RESULT_OK, resultData)
finish()
val requestJson = JSONObject(requestJsonString)
require(requestJson.has(PROTOCOL)) { "request json missing required field $PROTOCOL" }
require(requestJson.has(DATA)) { "request json missing required field $DATA" }

Log.d(TAG, "Request json received: ${requestJson.getString(DATA)}")

val openId4VCI = OpenId4VCI(requestJson.getString(DATA))

val testResponse = CreateCustomCredentialResponse(
type = DigitalCredential.TYPE_DIGITAL_CREDENTIAL,
data = Bundle().apply {
putString("androidx.credentials.BUNDLE_KEY_RESPONSE_JSON", "test response")
},
)

val resultData = Intent()
PendingIntentHandler.setCreateCredentialResponse(resultData, testResponse)
setResult(RESULT_OK, resultData)
finish()
} catch (e: Exception) {
Log.e(TAG, "exception", e)
val resultData = Intent()
PendingIntentHandler.setCreateCredentialException(
resultData,
CreateCredentialUnknownException(),
)
setResult(RESULT_OK, resultData)
finish()
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.credman.cmwallet.openid4vci

import android.util.Log
import com.credman.cmwallet.CmWalletApplication.Companion.TAG
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

internal val jsonFormat = Json { explicitNulls = false }

fun String.toCredentialResponse(): CredentialResponse? {
return try {
jsonFormat.decodeFromString<CredentialResponse>(this)
} catch (e: Exception) {
Log.e(TAG, "credential response parsing exception", e)
null
}
}

@Serializable
data class CredentialResponse(
val credentials: List<Credential>? = null,
val transaction_id: String? = null,
val notification_id: String? = null,
)

@Serializable
data class Credential(
// Technically this could also be an object
val credential: String,
)

@Serializable
data class CredentialRequest(
val credential_configuration_id: String,
val proof: Proof? = null,
) {
fun toJson(): String {
return jsonFormat.encodeToString(this)
}
}

@Serializable
class Proof(
val proof_type: String,
val jwt: String?,
)

internal const val JWT = "jwt"
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,7 @@ internal const val BACKGROUND_IMAGE = "background_image"
internal const val BACKGROUND_COLOR = "background_color"
internal const val TEXT_COLOR = "text_color"
internal const val VALUE_TYPE = "value_type"
internal const val ALT_TEXT = "alt_text"
internal const val ALT_TEXT = "alt_text"
internal const val CREDENTIAL_ENDPOINT = "credential_endpoint"
const val PROTOCOL = "protocol"
const val DATA = "data"
57 changes: 57 additions & 0 deletions app/src/main/java/com/credman/cmwallet/openid4vci/OpenId4VCI.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
package com.credman.cmwallet.openid4vci

import android.util.Log
import com.credman.cmwallet.CmWalletApplication.Companion.TAG
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
import io.ktor.http.headers
import org.json.JSONObject

class OpenId4VCI(val request: String) {
val requestJson: JSONObject = JSONObject(request)

val credentialIssuer: String
val credentialConfigurationIds: List<String>

// Credential Issuer Metadata Parameters
val credentialEndpoint: String
val credentialConfigurationsSupportedMap: Map<String, CredConfigsSupportedItem>

init {
Expand All @@ -23,8 +37,11 @@ class OpenId4VCI(val request: String) {
}
ids
}
require(credentialConfigurationIds.isNotEmpty()) { "Credential configuration id list shouldn't be empty" }

val issuerMetadataJson = requestJson.getJSONObject(ISSUER_METADATA)
require(issuerMetadataJson.has(CREDENTIAL_ENDPOINT)) { "Issuance request must contain $CREDENTIAL_ENDPOINT" }
credentialEndpoint = issuerMetadataJson.getString(CREDENTIAL_ENDPOINT)
require(issuerMetadataJson.has(CREDENTIAL_CONFIGURATION_SUPPORTED)) { "Issuance request must contain $CREDENTIAL_CONFIGURATION_SUPPORTED" }
val credConfigSupportedJson = issuerMetadataJson.getJSONObject(CREDENTIAL_CONFIGURATION_SUPPORTED)
val itr = credConfigSupportedJson.keys()
Expand All @@ -36,4 +53,44 @@ class OpenId4VCI(val request: String) {
}
credentialConfigurationsSupportedMap = tmpMap
}

suspend fun requestCredential() {
val client = HttpClient(CIO)
val httpResponse = client.post(credentialEndpoint) {
headers {
append(HttpHeaders.Authorization, getAuthToken())
}
contentType(ContentType.Application.Json)
setBody(
CredentialRequest(
credentialConfigurationIds.first(),
proof = Proof(
JWT,
jwt = generateDeviceKeyJwt()
)
).toJson()
)
}

if (httpResponse.status.value == 202) {
Log.d(TAG, "Successful credential endpoint response." +
"Content type: ${httpResponse.headers[HttpHeaders.ContentType]}, " +
"Content body: ${httpResponse.body<String>()}")
val credResponse = httpResponse.body<String>().toCredentialResponse()
// TODO: remove !!
val credential = credResponse!!.credentials!!.first().credential
TODO()
} else {
Log.e(TAG, "Error credential endpoint code: ${httpResponse.status.value}")
TODO()
}
}

private fun generateDeviceKeyJwt(): String {
return "TODO"
}

private fun getAuthToken(): String {
return "TODO"
}
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.devtool.ksp) apply false
}
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
[versions]
agp = "8.7.1"
kotlin = "2.0.21"
kotlinxSerializationJson = "1.7.3"
ksp = "2.0.21-1.0.27"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
ktorClient = "3.0.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
Expand Down Expand Up @@ -36,6 +38,9 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClient" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClient" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx" }
androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomRuntime" }
Expand All @@ -44,5 +49,6 @@ play-services-identity-credentials = {module = "com.google.android.gms:play-serv
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
devtool-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}

0 comments on commit c6e45da

Please sign in to comment.