Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial OID4VCI integration #41

Merged
merged 3 commits into from
Oct 9, 2024
Merged
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
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
Loading