Skip to content

Commit

Permalink
Add room database for storing credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
Helen Qin committed Nov 19, 2024
1 parent 3dd0a26 commit 4d14d78
Show file tree
Hide file tree
Showing 13 changed files with 224 additions and 31 deletions.
4 changes: 4 additions & 0 deletions app/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)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.devtool.ksp)
}

android {
Expand Down Expand Up @@ -55,6 +56,9 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.lifecycle.runtime.compose.android)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/assets/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"format": "mso_mdoc",
"credential": {
"docType": "org.iso.18013.5.1.mDL",
"nameSpaces": {
"org.iso.18013.5.1": {
"family_name": {
"value": "James",
"display": "Family Name",
"display_value": "Bond"
},
"given_name": {
"value": "Jon",
"display": "Given Name",
"display_value": "Jon"
},
"age_over_21": {
"value": true,
"display": "Age Over 21",
"display_value": "True"
}
}
}
},
"metadata": {
"verification": {
"title": "James Bond's Driving License",
"subtitle": "Gotham City DMV"
}
},
"deviceKey": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkAHk14S-CcPohE9v9rsVV8j7KRxrEqYYp1XuidZD2b6hRANCAAR07xfD2-9IWUn1YLsl3r0D-Nnx9toOLnhf3aMhuQmbGgxMzD6Kl_nY_UykJ-qdykIqCacRdWEGu42EotRizdWF",
"issuerSigned": "ompuYW1lU3BhY2VzoXFvcmcuaXNvLjE4MDEzLjUuMYPYGFhUpGhkaWdlc3RJRABmcmFuZG9tUKRsGD3aPLpwu_wGZyvuvdxxZWxlbWVudElkZW50aWZpZXJrZmFtaWx5X25hbWVsZWxlbWVudFZhbHVlZVNtaXRo2BhYUaRoZGlnZXN0SUQBZnJhbmRvbVAQwZXPLt5ybFSqRvFVCnPocWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlY0pvbtgYWE-kaGRpZ2VzdElEAmZyYW5kb21QPNysOvdkUbmuOPhvyXsrAHFlbGVtZW50SWRlbnRpZmllcmthZ2Vfb3Zlcl8yMWxlbGVtZW50VmFsdWX1amlzc3VlckF1dGiEQ6EBJqEYIVkCSzCCAkcwggHtoAMCAQICFHStD_3VcEOVnxRIW57aoGfaMp7FMAoGCCqGSM49BAMCMHkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRwwGgYDVQQKDBNEaWdpdGFsIENyZWRlbnRpYWxzMR8wHQYDVQQDDBZkaWdpdGFsY3JlZGVudGlhbHMuZGV2MB4XDTI0MTExMDAxMDgwM1oXDTM0MTAyOTAxMDgwM1oweTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxHDAaBgNVBAoME0RpZ2l0YWwgQ3JlZGVudGlhbHMxHzAdBgNVBAMMFmRpZ2l0YWxjcmVkZW50aWFscy5kZXYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATrQ6h60nar2xgrGpTMbRRYLBtWyfkHw2k4QzZc40EsBJNeDp-WXKz85dJjNloCsC7Ckb1spirxQdKVPWy2eRBpo1MwUTAdBgNVHQ4EFgQUCyxw_AMcbG8Lp1EwUuOaRBk527AwHwYDVR0jBBgwFoAUCyxw_AMcbG8Lp1EwUuOaRBk527AwDwYDVR0TAQH_BAUwAwEB_zAKBggqhkjOPQQDAgNIADBFAiEA_JW68hhRYz9l2scu8yW55xi7yyq7ycHg6arTH4b75zMCIG5DADVEbdGnoh6rzTKUdXEh2EnsgjERk6vH6u25Y4fLWQG62BhZAbWmZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2Z2RvY1R5cGV1b3JnLmlzby4xODAxMy41LjEubURMbHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGjAFgg-TGk78sfX6xxEfdjckEmDSfiVWzOGIIwTqm0oQetoR8BWCAcX3iJNwCyYOy1Bfl9sAjv1lEuD7iXI5dJbkwPUB6-RwJYIGFOQ5HGtkmhrJWuJ6eTdM2PC_lAIDR5_9pWUiRogpWwbWRldmljZUtleUluZm-haWRldmljZUtleaQBAiABIVggdO8Xw9vvSFlJ9WC7Jd69A_jZ8fbaDi54X92jIbkJmxoiWCAMTMw-ipf52P1MpCfqncpCKgmnEXVhBruNhKLUYs3VhWx2YWxpZGl0eUluZm-jZnNpZ25lZMB4GzIwMjQtMTEtMTdUMjA6NTI6MjIuOTE5NzgyWml2YWxpZEZyb23AeBsyMDI0LTExLTE3VDIwOjUyOjIyLjkxOTc4OVpqdmFsaWRVbnRpbMB4GzIwMzQtMTEtMDVUMjA6NTI6MjIuOTE5Nzg5WlhAl1Lt2d0SSsbuMizlTkVeLR7wucamVyUhyHm6PdG1W0YWXIxfLGwP0rG7Zhpuomh5kpItM7lRdR_FdkJHXO81MQ=="
}
35 changes: 32 additions & 3 deletions app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,47 @@ import android.app.Application
import android.util.Log
import androidx.credentials.registry.provider.RegisterCredentialsRequest
import androidx.credentials.registry.provider.RegistryManager
import androidx.room.Room
import com.credman.cmwallet.data.model.CredentialItem
import com.credman.cmwallet.data.repository.CredentialRepository
import com.credman.cmwallet.data.room.Credential
import com.credman.cmwallet.data.room.CredentialDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject

class CmWalletApplication : Application() {
companion object {
lateinit var database: CredentialDatabase
lateinit var credentialRepo: CredentialRepository

const val TAG = "CmWalletApplication"
}

private val registryManager = RegistryManager.create(this)
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

override fun onCreate() {
super.onCreate()
database = Room.databaseBuilder(
applicationContext,
CredentialDatabase::class.java, "credential-database"
).allowMainThreadQueries().fallbackToDestructiveMigration().build()
credentialRepo = CredentialRepository()

val openId4VPMatcher = loadOpenId4VPMatcher()
val testCredentialsJson = loadTestCredentials().toString(Charsets.UTF_8)

// Add the test credentials from the included json
CredentialRepository.addCredentialsFromJson(testCredentialsJson)
CredentialRepository.setPrivAppsJson(loadAppsJson().toString(Charsets.UTF_8))
credentialRepo.addCredentialsFromJson(testCredentialsJson)
credentialRepo.setPrivAppsJson(loadAppsJson().toString(Charsets.UTF_8))

// Listen for new credentials and update the registry.
applicationScope.launch {
CredentialRepository.credentialRegistryDatabase.collect { credentialDatabase ->
credentialRepo.credentialRegistryDatabase.collect { credentialDatabase ->
Log.i("CmWalletApplication", "Credentials changed $credentialDatabase")
registryManager.registerCredentials(
request = object : RegisterCredentialsRequest(
Expand All @@ -37,6 +56,16 @@ class CmWalletApplication : Application() {
)
}
}

// TODO: delete: this is only for testing.
CoroutineScope(Dispatchers.IO).launch {
delay(5000)
val json = readAsset("test.json").toString(Charsets.UTF_8)
val cred = Credential(2000L, json)
database.credentialDao().insertAll(Credential(2000L, json))
// delay(5000)
// database.credentialDao().delete(Credential(2000L, json))
}
}

private fun readAsset(fileName: String): ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.registry.provider.selectedEntryId
import com.credman.cmwallet.data.model.MdocCredential
import com.credman.cmwallet.data.repository.CredentialRepository
import com.credman.cmwallet.mdoc.createSessionTranscript
import com.credman.cmwallet.mdoc.filterIssuerSigned
import com.credman.cmwallet.mdoc.generateDeviceResponse
Expand All @@ -29,7 +28,9 @@ class GetCredentialActivity : ComponentActivity() {
if (request != null) {
Log.i("GetCredentialActivity", "selectedEntryId ${request.selectedEntryId}")
val selectedEntryId = JSONObject(request.selectedEntryId)
val origin = request.callingAppInfo.getOrigin(CredentialRepository.privAppsJson) ?: ""
val origin = request.callingAppInfo.getOrigin(
CmWalletApplication.credentialRepo.privAppsJson
) ?: ""
Log.i("GetCredentialActivity", "origin $origin")

request.credentialOptions.forEach {
Expand Down Expand Up @@ -72,7 +73,7 @@ class GetCredentialActivity : ComponentActivity() {
selectedID: String,
origin: String
): String {
val selectedCredential = CredentialRepository.getCredential(selectedID)
val selectedCredential = CmWalletApplication.credentialRepo.getCredential(selectedID)
?: throw RuntimeException("Selected credential not found")

val request = JSONObject(requestJson)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ class CredentialItem(
CredentialMetadata.fromJson(metadataKey, it.getJSONObject(metadataKey))
}
)

// Returns the credential that should be persisted in the app database, i.e.
// [com.credman.cmwallet.data.room.Credential.credJson]
fun toJson(): String = JSONObject()
.put(CREDENTIAL, credential.toJson())
.put(METADATA, metadata.toJson())
.toString()
}

sealed class Credential(
val format: String
) {
internal abstract fun toJson(): JSONObject

companion object {
fun fromJson(json: JSONObject): Credential = when (json.getString(FORMAT)) {
MSO_MDOC -> MdocCredential(json)
Expand Down Expand Up @@ -64,6 +73,30 @@ data class MdocCredential(
deviceKey = loadECPrivateKey(Base64.decode(json.getString(DEVICE_KEY), Base64.URL_SAFE)),
issuerSigned = Base64.decode(json.getString(ISSUER_SIGNED), Base64.URL_SAFE)
)

override fun toJson(): JSONObject {
val result = JSONObject()
result.put(FORMAT, format)
val credential = JSONObject()
credential.put(DOCTYPE, docType)
val namespacesJson = JSONObject()
for ((namespace, namespacedData) in nameSpaces) {
val namespacedDataJson = JSONObject()
for ((fieldKey, field) in namespacedData.data) {
val fieldJson = JSONObject()
fieldJson.putOpt(VALUE, field.value)
fieldJson.put(DISPLAY, field.display)
fieldJson.putOpt(DISPLAY_VALUE, field.displayValue)
namespacedDataJson.put(fieldKey, fieldJson)
}
namespacesJson.put(namespace, namespacedDataJson)
}
credential.put(NAMESPACES, namespacesJson)
result.put(CREDENTIAL, credential)
result.put(DEVICE_KEY, Base64.encodeToString(deviceKey.encoded, Base64.URL_SAFE or Base64.NO_WRAP))
result.put(ISSUER_SIGNED, Base64.encodeToString(issuerSigned, Base64.URL_SAFE or Base64.NO_WRAP))
return result
}
}

data class MdocNameSpace(
Expand All @@ -81,6 +114,7 @@ sealed class CredentialMetadata(
val subtitle: String?,
val icon: String?
) {
internal abstract fun toJson(): JSONObject
companion object {
fun fromJson(type: String, json: JSONObject): CredentialMetadata {
return when (type) {
Expand Down Expand Up @@ -108,13 +142,24 @@ class PaymentMetadata(
subtitle: String?,
icon: String?,
val cardNetworkArt: String?, // b64 encoding
) : CredentialMetadata(title, subtitle, icon)
) : CredentialMetadata(title, subtitle, icon) {
override fun toJson(): JSONObject = JSONObject()
.put(TITLE, title)
.putOpt(SUBTITLE, subtitle)
.putOpt(CARD_ART, icon)
.putOpt(CARD_NETWORK_ART, cardNetworkArt)
}

class VerificationMetadata(
title: String,
subtitle: String?,
icon: String?,
) : CredentialMetadata(title, subtitle, icon)
) : CredentialMetadata(title, subtitle, icon) {
override fun toJson(): JSONObject = JSONObject()
.put(TITLE, title)
.putOpt(SUBTITLE, subtitle)
.putOpt(CARD_ICON, icon)
}

const val MSO_MDOC = "mso_mdoc"
private const val DEVICE_KEY = "deviceKey"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder

object CredentialRepository {
class CredentialRepository {
var privAppsJson = "{}"
private set

Expand Down Expand Up @@ -50,8 +50,8 @@ object CredentialRepository {
}

fun getCredential(id: String): CredentialItem? {
// TODO: check credentialDatabaseDataSource too
return testCredentialsDataSource.getCredential(id)
?: credentialDatabaseDataSource.getCredential(id)
}

fun setPrivAppsJson(appsJson: String) {
Expand Down Expand Up @@ -150,20 +150,24 @@ object CredentialRepository {
registryCredentials.put(MSO_MDOC, mdocCredentials)
val registryJson = JSONObject()
registryJson.put(CREDENTIALS, registryCredentials)
Log.d(TAG, "Credential to be registered: ${registryJson.toString(2)}")
out.write(registryJson.toString().toByteArray())
return out.toByteArray()
}

// Wasm database json keys
const val CREDENTIALS = "credentials"
const val ID = "id"
const val TITLE = "title"
const val SUBTITLE = "subtitle"
const val ICON = "icon"
const val START = "start"
const val LENGTH = "length"
const val NAMESPACES = "namespaces"
const val VALUE = "value"
const val DISPLAY = "display"
const val DISPLAY_VALUE = "display_value"
companion object {
const val TAG = "CredentialRepository"
// Wasm database json keys
const val CREDENTIALS = "credentials"
const val ID = "id"
const val TITLE = "title"
const val SUBTITLE = "subtitle"
const val ICON = "icon"
const val START = "start"
const val LENGTH = "length"
const val NAMESPACES = "namespaces"
const val VALUE = "value"
const val DISPLAY = "display"
const val DISPLAY_VALUE = "display_value"
}
}
19 changes: 19 additions & 0 deletions app/src/main/java/com/credman/cmwallet/data/room/Credential.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.credman.cmwallet.data.room

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.credman.cmwallet.data.model.CredentialItem
import org.json.JSONObject

@Entity
data class Credential(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0,
@ColumnInfo(name = "credJson") val credJson: String,
) {
fun toCredentialItem(): CredentialItem = CredentialItem(
id = id.toString(),
json = JSONObject(credJson),
)
}

27 changes: 27 additions & 0 deletions app/src/main/java/com/credman/cmwallet/data/room/CredentialDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.credman.cmwallet.data.room

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface CredentialDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(vararg creds: Credential)

@Update
suspend fun updateUsers(vararg creds: Credential)

@Delete
suspend fun delete(cred: Credential)

@Query("SELECT * FROM credential")
fun getAll(): Flow<List<Credential>>

@Query("SELECT * FROM credential WHERE id = :id")
fun loadCredById(id: Long): Credential?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.credman.cmwallet.data.room

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [Credential::class], version = 1)
abstract class CredentialDatabase : RoomDatabase() {
abstract fun credentialDao(): CredentialDao
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
package com.credman.cmwallet.data.source

import android.util.Log
import com.credman.cmwallet.CmWalletApplication
import com.credman.cmwallet.data.model.CredentialItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.transform

class CredentialDatabaseDataSource {
val credentialDao = CmWalletApplication
.database
.credentialDao()

// TODO: Make this a Room database, for now just return an empty list
private val _credentials = MutableStateFlow(emptyList<CredentialItem>())
val credentials: StateFlow<List<CredentialItem>> = _credentials.asStateFlow()
val credentials: Flow<List<CredentialItem>> = credentialDao
.getAll()
.transform { list ->
emit(list.map { it.toCredentialItem() })
}

fun getCredential(id: String): CredentialItem? {
return try {
credentialDao.loadCredById(id.toLong())?.toCredentialItem()
} catch (e: Exception) {
Log.e(CmWalletApplication.TAG, "database retrieval error", e)
null
}
}
}
Loading

0 comments on commit 4d14d78

Please sign in to comment.