Skip to content

Commit

Permalink
Initial OID4VCI integration (#41)
Browse files Browse the repository at this point in the history
This starts integrating the OID4VCI flow into the wallet.
  • Loading branch information
Juliano1612 authored Oct 9, 2024
1 parent 8b2a6be commit 9505a1c
Show file tree
Hide file tree
Showing 15 changed files with 1,179 additions and 1 deletion.
2 changes: 1 addition & 1 deletion MobileSdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ android {
}

dependencies {
api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.32")
api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.36")
//noinspection GradleCompatible
implementation("com.android.support:appcompat-v7:28.0.0")
/* Begin UI dependencies */
Expand Down
2 changes: 2 additions & 0 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
implementation(project(mapOf("path" to ":MobileSdk")))
implementation("com.google.zxing:core:3.5.1")
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("io.ktor:ktor-client-cio:2.3.12")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const val VERIFIER_SETTINGS_HOME_PATH = "verifier_settings_home"
const val WALLET_SETTINGS_HOME_PATH = "wallet_settings_home"
const val ADD_TO_WALLET_PATH = "add_to_wallet/{rawCredential}"
const val OID4VP_PATH = "oid4vp/{params}"
const val OID4VCI_PATH = "oid4vci"



sealed class Screen(val route: String) {
Expand All @@ -19,4 +21,5 @@ sealed class Screen(val route: String) {
object WalletSettingsHomeScreen : Screen(WALLET_SETTINGS_HOME_PATH)
object AddToWalletScreen : Screen(ADD_TO_WALLET_PATH)
object ScanQRScreen : Screen(OID4VP_PATH)
object OID4VCIScreen : Screen(OID4VCI_PATH)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.spruceid.mobilesdkexample.verifier.VerifyVCView
import com.spruceid.mobilesdkexample.verifiersettings.VerifierSettingsHomeView
import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel
import com.spruceid.mobilesdkexample.wallet.AddToWalletView
import com.spruceid.mobilesdkexample.wallet.OID4VCIView
import com.spruceid.mobilesdkexample.walletsettings.WalletSettingsHomeView
import com.spruceid.mobilesdkexample.wallet.DispatchQRView

Expand Down Expand Up @@ -76,5 +77,10 @@ fun SetupNavGraph(
) {
DispatchQRView(navController)
}
composable(
route = Screen.OID4VCIScreen.route,
) {
OID4VCIView(navController)
}
}
}
236 changes: 236 additions & 0 deletions example/src/main/java/com/spruceid/mobilesdkexample/wallet/OID4VCI.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package com.spruceid.mobilesdkexample.wallet

import android.content.Context
import android.util.Base64
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.spruceid.mobile.sdk.KeyManager
import com.spruceid.mobile.sdk.rs.AsyncHttpClient
import com.spruceid.mobile.sdk.rs.DidMethod
import com.spruceid.mobile.sdk.rs.HttpRequest
import com.spruceid.mobile.sdk.rs.HttpResponse
import com.spruceid.mobile.sdk.rs.Oid4vci
import com.spruceid.mobile.sdk.rs.generatePopComplete
import com.spruceid.mobile.sdk.rs.generatePopPrepare
import com.spruceid.mobilesdkexample.R
import com.spruceid.mobilesdkexample.ScanningComponent
import com.spruceid.mobilesdkexample.ScanningType
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.client.statement.readBytes
import io.ktor.http.HttpMethod
import io.ktor.util.toMap
import kotlinx.coroutines.*
import kotlin.math.min

@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
fun OID4VCIView(
navController: NavController
) {
var loading by remember {
mutableStateOf(false)
}
var err by remember {
mutableStateOf<String?>(null)
}
var credential by remember {
mutableStateOf<String?>(null)
}
val ctx = LocalContext.current

fun getCredential(credentialOffer: String) {
loading = true
val client = HttpClient(CIO)
val oid4vciSession = Oid4vci.newWithAsyncClient(client = object : AsyncHttpClient {
override suspend fun httpClient(request: HttpRequest): HttpResponse {
val res = client.request(request.url) {
method = HttpMethod(request.method)
for ((k, v) in request.headers) {
headers[k] = v
}
setBody(request.body)
}

return HttpResponse(
statusCode = res.status.value.toUShort(),
headers = res.headers.toMap().mapValues { it.value.joinToString() },
body = res.readBytes()
)
}

})

GlobalScope.async {
try {
oid4vciSession.initiateWithOffer(
credentialOffer = credentialOffer,
clientId = "skit-demo-wallet",
redirectUrl = "https://spruceid.com"
)

val nonce = oid4vciSession.exchangeToken()

val metadata = oid4vciSession.getMetadata()

val keyManager = KeyManager()
keyManager.generateSigningKey(id = "reference-app/default-signing")
val jwk = keyManager.getJwk(id = "reference-app/default-signing")

val signingInput = jwk?.let {
generatePopPrepare(
audience = metadata.issuer(),
nonce = nonce,
didMethod = DidMethod.JWK,
publicJwk = jwk,
durationInSecs = null
)
}

val signature = signingInput?.let {
keyManager.signPayload(
id = "reference-app/default-signing",
payload = signingInput
)
}

val pop = signingInput?.let {
signature?.let {
generatePopComplete(
signingInput = signingInput,
signature = Base64.encodeToString(
signature,
Base64.URL_SAFE
or Base64.NO_PADDING
or Base64.NO_WRAP
).toByteArray()
)
}
}

oid4vciSession.setContextMap(getVCPlaygroundOID4VCIContext(ctx = ctx))

val credentials = pop?.let {
oid4vciSession.exchangeCredential(proofsOfPossession = listOf(pop))
}

credentials?.forEach { cred ->
cred.payload.toString(Charsets.UTF_8).let {
credential = it.substring(0, min(1500, it.length))
// TODO: add to credentialPack
}

}
} catch (e: Exception) {
err = e.localizedMessage
e.printStackTrace()
}
loading = false
}
}

if (loading) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Loading...")
}
} else if (err != null) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(err!!)
}
} else if (credential == null) {
ScanningComponent(
title = "Scan to Add Credential",
navController = navController,
scanningType = ScanningType.QRCODE,
onRead = ::getCredential
)
} else {
Text(credential!!)
}
}


fun getVCPlaygroundOID4VCIContext(ctx: Context): Map<String, String> {
val context = mutableMapOf<String, String>()

context["https://contexts.vcplayground.org/examples/alumni/v1.json"] =
ctx.resources.openRawResource(R.raw.contexts_vcplayground_org_examples_alumni_v1)
.bufferedReader()
.readLines()
.joinToString("")

context["https://w3id.org/first-responder/v1"] =
ctx.resources.openRawResource(R.raw.w3id_org_first_responder_v1)
.bufferedReader()
.readLines()
.joinToString("")

context["https://w3id.org/vdl/aamva/v1"] =
ctx.resources.openRawResource(R.raw.w3id_org_vdl_aamva_v1)
.bufferedReader()
.readLines()
.joinToString("")

context["https://w3id.org/citizenship/v3"] =
ctx.resources.openRawResource(R.raw.w3id_org_citizenship_v3)
.bufferedReader()
.readLines()
.joinToString("")

context["https://contexts.vcplayground.org/examples/movie-ticket/v1.json"] =
ctx.resources.openRawResource(R.raw.contexts_vcplayground_org_examples_movie_ticket_v1)
.bufferedReader()
.readLines()
.joinToString("")

context["https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json"] =
ctx.resources.openRawResource(R.raw.purl_imsglobal_org_spec_ob_v3p0_context_3_0_2)
.bufferedReader()
.readLines()
.joinToString("")

context["https://contexts.vcplayground.org/examples/food-safety-certification/v1.json"] =
ctx.resources.openRawResource(R.raw.contexts_vcplayground_org_examples_food_safety_certification_v1)
.bufferedReader()
.readLines()
.joinToString("")

context["https://contexts.vcplayground.org/examples/gs1-8110-coupon/v2.json"] =
ctx.resources.openRawResource(R.raw.contexts_vcplayground_org_examples_gs1_8110_coupon_v2)
.bufferedReader()
.readLines()
.joinToString("")

context["https://contexts.vcplayground.org/examples/customer-loyalty/v1.json"] =
ctx.resources.openRawResource(R.raw.contexts_vcplayground_org_examples_customer_loyalty_v1)
.bufferedReader()
.readLines()
.joinToString("")

return context
}


Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ 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.mdocBase64
import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -71,6 +72,26 @@ fun WalletHomeHeader(navController: NavController) {
color = TextHeader
)
Spacer(Modifier.weight(1f))
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(36.dp)
.height(36.dp)
.padding(start = 4.dp)
.clip(shape = RoundedCornerShape(8.dp))
.background(Primary)
.clickable {
navController.navigate(Screen.OID4VCIScreen.route)
}
) {
Image(
painter = painterResource(id = R.drawable.scan_qr_code),
contentDescription = stringResource(id = R.string.scan_qr_code),
modifier = Modifier
.width(20.dp)
.height(20.dp)
)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"@context": {
"@version": 1.1,
"@protected": true,
"name": "https://schema.org/name",
"description": "https://schema.org/description",
"identifier": "https://schema.org/identifier",
"image": {
"@id": "https://schema.org/image",
"@type": "@id"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"@context": {
"@protected": true,
"id": "@id",
"type": "@type",
"CustomerLoyaltyCredential": "https://contexts.vcplayground.org/examples/customer-loyalty/vocab/#CustomerLoyaltyCredential",
"CustomerLoyaltyCard": {
"@id": "https://contexts.vcplayground.org/examples/customer-loyalty/vocab/#CustomerLoyaltyCard",
"@context": {
"@protected": true,
"id": "@id",
"type": "@type",
"identifier": "https://schema.org/identifier",
"branchCode": "https://schema.org/branchCode"
}
},
"customerLoyaltyCard": "https://contexts.vcplayground.org/examples/customer-loyalty/vocab/#customerLoyaltyCard",
"image": {
"@id": "https://schema.org/image",
"@type": "@id"
},
"url": {
"@id": "https://schema.org/url",
"@type": "@id"
},
"name": "https://schema.org/name",
"description": "https://schema.org/description"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"@context": {
"@protected": true,
"id": "@id",
"type": "@type",
"FoodSafetyCertificationCredential": "https://contexts.vcplayground.org/examples/food-safety-certification/vocab#FoodSafetyCertificationCredential",

"name": "https://schema.org/name",
"description": "https://schema.org/description",
"image": "https://schema.org/image",
"certification": "https://contexts.vcplayground.org/examples/food-safety-certification/vocab#certification",

"FoodSafetyCertification": {
"@id": "https://contexts.vcplayground.org/examples/food-safety-certification/vocab#FoodSafetyCertification",
"@context": {
"@protected": true,
"id": "@id",
"type": "@type",

"certificateId": "https://contexts.vcplayground.org/examples/food-safety-certification/vocab#certificateId",
"examDate": {
"@id": "https://contexts.vcplayground.org/examples/food-safety-certification/vocab#examDate",
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
},
"testCode": "https://contexts.vcplayground.org/examples/food-safety-certification/vocab#testCode"
}
}
}
}
Loading

0 comments on commit 9505a1c

Please sign in to comment.