From 8114f823c80c7ca1ac9b65a0bc2c67fc4a3c40d0 Mon Sep 17 00:00:00 2001 From: Essbante Date: Tue, 8 Mar 2022 19:03:21 -0600 Subject: [PATCH] Dev (#2) * feat(model): make data classes serializable data classes are not serializable. Serialization is needed to export Wallet to a json file - Add Serializable annotation - modify updateWallet to use upsert (update/insert) * feat(dlt): add public and private key to DID Public and private keys are not stored on the DID. Add required code to store them as hex strings * feat(didpeer): add DIDPeer functions - add createPeerDID - add resolvePeerDID - add pack - add unpack * feat: update dependencies * fix(didpeer): fix routing keys * refactor(qr): clean code * feat(dlt,model): add addKey and revokeKey functionality * refactor(dlt,storage): add comments, refactor exception handling * refactor(dlt,storage): add comments, refactor exception handling * refactor(dlt,model,storage): refactor model and credential management * refactor(DLT,Storage,Model): add comments, refactor exception handling * feat(config,storage): recfactor db name handling * feat(dlt): return prism DID document as json * refactor(Storage,Model): add comments * refactor(Storage): change exception type * fix(gradle): change env vars remove hyphen from env vars * build(gradle): Change version --- build.gradle.kts | 21 +- .../wal/library/{Constant.kt => Config.kt} | 23 +- .../wal/library/DIDDocResolverPeerDID.kt | 57 +++ .../kotlin/com/rootsid/wal/library/DIDPeer.kt | 120 ++++++ .../kotlin/com/rootsid/wal/library/DLT.kt | 372 +++++++++++++----- .../kotlin/com/rootsid/wal/library/Model.kt | 82 +++- src/main/kotlin/com/rootsid/wal/library/QR.kt | 25 -- .../com/rootsid/wal/library/SecretResolver.kt | 50 +++ .../kotlin/com/rootsid/wal/library/Storage.kt | 116 ++++-- 9 files changed, 668 insertions(+), 198 deletions(-) rename src/main/kotlin/com/rootsid/wal/library/{Constant.kt => Config.kt} (85%) create mode 100644 src/main/kotlin/com/rootsid/wal/library/DIDDocResolverPeerDID.kt create mode 100644 src/main/kotlin/com/rootsid/wal/library/DIDPeer.kt create mode 100644 src/main/kotlin/com/rootsid/wal/library/SecretResolver.kt diff --git a/build.gradle.kts b/build.gradle.kts index 9968e97..9e7b823 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "com.rootsid.wal" -version = "1.0-SNAPSHOT" +version = "1.0.0" repositories { mavenCentral() @@ -23,16 +23,15 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10") - + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") - implementation("org.litote.kmongo:kmongo:4.4.0") // needed for cryptography primitives implementation - implementation("io.iohk.atala:prism-crypto:1.2.0") - implementation("io.iohk.atala:prism-identity:1.2.0") - implementation("io.iohk.atala:prism-credentials:1.2.0") - implementation("io.iohk.atala:prism-api:1.2.0") + implementation("io.iohk.atala:prism-crypto:1.3.0") + implementation("io.iohk.atala:prism-identity:1.3.0") + implementation("io.iohk.atala:prism-credentials:1.3.0") + implementation("io.iohk.atala:prism-api:1.3.0") // Fixes a build issue implementation("com.soywiz.korlibs.krypto:krypto-jvm:2.0.6") @@ -43,6 +42,10 @@ dependencies { implementation("org.boofcv:boofcv-kotlin:0.39.1") implementation("org.boofcv:boofcv-WebcamCapture:0.39.1") + // DIDComm + implementation("org.didcommx:didcomm:0.3.0") + implementation("org.didcommx:peerdid:0.3.0") + implementation("org.junit.jupiter:junit-jupiter:5.8.2") } @@ -59,8 +62,8 @@ publishing { name = "GitHubPackages" url = uri("https://maven.pkg.github.com/roots-id/wal-library") credentials { - username = System.getenv("ROOTS-ID_USER") - password = System.getenv("ROOTS-ID_PASSWORD") + username = System.getenv("ROOTSID_USER") + password = System.getenv("ROOTSID_PASSWORD") } } } diff --git a/src/main/kotlin/com/rootsid/wal/library/Constant.kt b/src/main/kotlin/com/rootsid/wal/library/Config.kt similarity index 85% rename from src/main/kotlin/com/rootsid/wal/library/Constant.kt rename to src/main/kotlin/com/rootsid/wal/library/Config.kt index dd31a46..81eca40 100644 --- a/src/main/kotlin/com/rootsid/wal/library/Constant.kt +++ b/src/main/kotlin/com/rootsid/wal/library/Config.kt @@ -1,11 +1,12 @@ -package com.rootsid.wal.library - -/** - * Constant - * - * @constructor Create empty Constant - */ -object Constant { - const val MNEMONIC_SEPARATOR = "," - const val TESTNET_URL = "https://explorer.cardano-testnet.iohkdev.io/en/transaction?id=" -} +package com.rootsid.wal.library + +/** + * Constant + * + * @constructor Create empty Constant + */ +object Config { + const val MNEMONIC_SEPARATOR = "," + const val TESTNET_URL = "https://explorer.cardano-testnet.iohkdev.io/en/transaction?id=" + var DB_NAME = "wal" +} diff --git a/src/main/kotlin/com/rootsid/wal/library/DIDDocResolverPeerDID.kt b/src/main/kotlin/com/rootsid/wal/library/DIDDocResolverPeerDID.kt new file mode 100644 index 0000000..8f0178e --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/DIDDocResolverPeerDID.kt @@ -0,0 +1,57 @@ +package com.rootsid.wal.library + +import org.didcommx.didcomm.common.VerificationMaterial +import org.didcommx.didcomm.common.VerificationMaterialFormat +import org.didcommx.didcomm.common.VerificationMethodType +import org.didcommx.didcomm.diddoc.DIDCommService +import org.didcommx.didcomm.diddoc.DIDDoc +import org.didcommx.didcomm.diddoc.DIDDocResolver +import org.didcommx.didcomm.diddoc.VerificationMethod +import org.didcommx.didcomm.utils.toJson +import org.didcommx.peerdid.DIDCommServicePeerDID +import org.didcommx.peerdid.DIDDocPeerDID +import org.didcommx.peerdid.VerificationMaterialFormatPeerDID +import org.didcommx.peerdid.resolvePeerDID +import java.util.Optional + +class DIDDocResolverPeerDID : DIDDocResolver { + + override fun resolve(did: String): Optional { + // request DID Doc in JWK format + val didDocJson = resolvePeerDID(did, format = VerificationMaterialFormatPeerDID.JWK) + val didDoc = DIDDocPeerDID.fromJson(didDocJson) + + didDoc.keyAgreement + return Optional.ofNullable( + DIDDoc( + did = did, + keyAgreements = didDoc.agreementKids, + authentications = didDoc.authenticationKids, + verificationMethods = (didDoc.authentication + didDoc.keyAgreement).map { + VerificationMethod( + id = it.id, + type = VerificationMethodType.JSON_WEB_KEY_2020, + controller = it.controller, + verificationMaterial = VerificationMaterial( + format = VerificationMaterialFormat.JWK, + value = toJson(it.verMaterial.value) + ) + ) + }, + didCommServices = didDoc.service?.mapNotNull { + when (it) { + is DIDCommServicePeerDID -> + DIDCommService( + id = it.id, + serviceEndpoint = it.serviceEndpoint, + routingKeys = it.routingKeys, + accept = it.accept + ) + else -> null + } + } + ?: emptyList() + ) + ) + } +} diff --git a/src/main/kotlin/com/rootsid/wal/library/DIDPeer.kt b/src/main/kotlin/com/rootsid/wal/library/DIDPeer.kt new file mode 100644 index 0000000..a779e31 --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/DIDPeer.kt @@ -0,0 +1,120 @@ +package com.rootsid.wal.library + +import org.didcommx.didcomm.DIDComm +import org.didcommx.didcomm.message.Message +import org.didcommx.didcomm.model.PackEncryptedParams +import org.didcommx.didcomm.model.PackEncryptedResult +import org.didcommx.didcomm.model.UnpackParams +import org.didcommx.didcomm.secret.generateEd25519Keys +import org.didcommx.didcomm.secret.generateX25519Keys +import org.didcommx.didcomm.secret.jwkToSecret +import org.didcommx.didcomm.utils.divideDIDFragment +import org.didcommx.didcomm.utils.toJson +import org.didcommx.peerdid.* +import java.util.UUID + +fun createPeerDID( + authKeysCount: Int = 1, + agreementKeysCount: Int = 1, + serviceEndpoint: String? = null, + serviceRoutingKeys: List, + secretResolver: SecretResolver +): String { + // 1. generate keys in JWK format + val x25519keyPairs = (1..agreementKeysCount).map { generateX25519Keys() } + val ed25519keyPairs = (1..authKeysCount).map { generateEd25519Keys() } + + // 2. prepare the keys for peer DID lib + val authPublicKeys = ed25519keyPairs.map { + VerificationMaterialAuthentication( + format = VerificationMaterialFormatPeerDID.JWK, + type = VerificationMethodTypeAuthentication.JSON_WEB_KEY_2020, + value = it.public + ) + } + val agreemPublicKeys = x25519keyPairs.map { + VerificationMaterialAgreement( + format = VerificationMaterialFormatPeerDID.JWK, + type = VerificationMethodTypeAgreement.JSON_WEB_KEY_2020, + value = it.public + ) + } + + // 3. generate service + val service = serviceEndpoint?.let { + toJson( + DIDCommServicePeerDID( + id = "new-id", + type = SERVICE_DIDCOMM_MESSAGING, + serviceEndpoint = it, + routingKeys = serviceRoutingKeys, + accept = listOf("didcomm/v2") + ).toDict() + ) + } + + // 4. call peer DID lib + // if we have just one key (auth), then use numalg0 algorithm + // otherwise use numalg2 algorithm + val did = if (authPublicKeys.size == 1 && agreemPublicKeys.isEmpty() && service.isNullOrEmpty()) + createPeerDIDNumalgo0(authPublicKeys[0]) + else + createPeerDIDNumalgo2( + signingKeys = authPublicKeys, + encryptionKeys = agreemPublicKeys, + service = service + ) + + // 5. set KIDs as in DID DOC for secrets and store the secret in the secrets resolver + val didDoc = DIDDocPeerDID.fromJson(resolvePeerDID(did, VerificationMaterialFormatPeerDID.JWK)) + didDoc.agreementKids.zip(x25519keyPairs).forEach { + val privateKey = it.second.private.toMutableMap() + privateKey["kid"] = it.first + secretResolver.addKey(jwkToSecret(privateKey)) + } + didDoc.authenticationKids.zip(ed25519keyPairs).forEach { + val privateKey = it.second.private.toMutableMap() + privateKey["kid"] = it.first + secretResolver.addKey(jwkToSecret(privateKey)) + } + return did +} + +fun resolvePeerDID(did: String, format: VerificationMaterialFormatPeerDID) = + org.didcommx.peerdid.resolvePeerDID(did, format) + +fun pack( + data: String, + to: String, + from: String? = null, + signFrom: String? = null, + protectSender: Boolean = true, + secretsResolver: SecretResolver +): PackEncryptedResult { + val didComm = DIDComm(DIDDocResolverPeerDID(), secretsResolver) + val message = Message.builder( + id = UUID.randomUUID().toString(), + body = mapOf("msg" to data), + type = "my-protocol/1.0" + ).build() + var builder = PackEncryptedParams + .builder(message, to) + .forward(false) + .protectSenderId(protectSender) + builder = from?.let { builder.from(it) } ?: builder + builder = signFrom?.let { builder.signFrom(it) } ?: builder + val params = builder.build() + return didComm.packEncrypted(params) +} + +fun unpack(packedMsg: String, secretResolver: SecretResolver): UnpackResult { + val didComm = DIDComm(DIDDocResolverPeerDID(), secretResolver) + val res = didComm.unpack(UnpackParams.Builder(packedMsg).build()) + val msg = res.message.body["msg"].toString() + val to = res.metadata.encryptedTo?.let { divideDIDFragment(it.first()).first() } ?: "" + val from = res.metadata.encryptedFrom?.let { divideDIDFragment(it).first() } + return UnpackResult( + message = msg, + from = from, to = to, res = res + ) +} diff --git a/src/main/kotlin/com/rootsid/wal/library/DLT.kt b/src/main/kotlin/com/rootsid/wal/library/DLT.kt index d01b4ba..165789c 100644 --- a/src/main/kotlin/com/rootsid/wal/library/DLT.kt +++ b/src/main/kotlin/com/rootsid/wal/library/DLT.kt @@ -15,22 +15,22 @@ import io.iohk.atala.prism.crypto.Sha256Digest import io.iohk.atala.prism.crypto.derivation.KeyDerivation import io.iohk.atala.prism.crypto.derivation.MnemonicCode import io.iohk.atala.prism.crypto.keys.ECKeyPair -import io.iohk.atala.prism.identity.LongFormPrismDid -import io.iohk.atala.prism.identity.PrismDid -import io.iohk.atala.prism.identity.PrismDidDataModel -import io.iohk.atala.prism.identity.PrismKeyType +import io.iohk.atala.prism.identity.* import io.iohk.atala.prism.protos.GetOperationInfoRequest import io.iohk.atala.prism.protos.GrpcClient import io.iohk.atala.prism.protos.GrpcOptions import io.iohk.atala.prism.protos.NodeServiceCoroutine import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import pbandk.ByteArr /** * Transaction id * - * @param oid - * @return + * @param oid operation identifier + * @return transaction Id */ @OptIn(PrismSdkInternal::class) private fun transactionId(oid: AtalaOperationId): String { @@ -44,25 +44,25 @@ private fun transactionId(oid: AtalaOperationId): String { /** * Wait until confirmed * - * @param nodePublicApi - * @param operationId + * @param nodePublicApi PRISM node + * @param operationId operation Identifier */ private fun waitUntilConfirmed(nodePublicApi: NodePublicApi, operationId: AtalaOperationId) { var tid = "" var status = runBlocking { - nodePublicApi.getOperationStatus(operationId) + nodePublicApi.getOperationInfo(operationId).status } while (status != AtalaOperationStatus.CONFIRMED_AND_APPLIED && status != AtalaOperationStatus.CONFIRMED_AND_REJECTED ) { if (status == AtalaOperationStatus.AWAIT_CONFIRMATION && tid.isEmpty()) { tid = transactionId(operationId) - println("Track the transaction in:\n- ${Constant.TESTNET_URL}$tid\n") + println("Track the transaction in:\n- ${Config.TESTNET_URL}$tid\n") } println("Current operation status: ${AtalaOperationStatus.asString(status)}\n") Thread.sleep(10000) status = runBlocking { - nodePublicApi.getOperationStatus(operationId) + nodePublicApi.getOperationInfo(operationId).status } } } @@ -70,10 +70,10 @@ private fun waitUntilConfirmed(nodePublicApi: NodePublicApi, operationId: AtalaO /** * Derive key pair * - * @param keyPairs - * @param seed - * @param keyId - * @return + * @param keyPairs List containing key path information + * @param seed seed + * @param keyId Id of the key to derive + * @return Key pair */ private fun deriveKeyPair(keyPairs: MutableList, seed: ByteArray, keyId: String): ECKeyPair { val keyPathList = keyPairs.filter { it.keyId == keyId } @@ -88,22 +88,22 @@ private fun deriveKeyPair(keyPairs: MutableList, seed: ByteArray, keyId /** * New wallet * - * @param name - * @param mnemonic - * @param passphrase - * @return + * @param name Wallet name + * @param mnemonic mnemonic, leave empty to generate a random one + * @param passphrase passphrase + * @return a new wallet */ fun newWallet(name: String, mnemonic: String, passphrase: String): Wallet { return if (mnemonic.isBlank()) { Wallet(name, KeyDerivation.randomMnemonicCode().words, passphrase) } else { try { - val mnemonicList = mnemonic.split(Constant.MNEMONIC_SEPARATOR) + val mnemonicList = mnemonic.split(Config.MNEMONIC_SEPARATOR) .map { it.trim() } KeyDerivation.binarySeed(MnemonicCode(mnemonicList), passphrase) Wallet(name, mnemonicList, passphrase) } catch (e: Exception) { - throw IllegalArgumentException("Invalid mnemonic phrase") + throw Exception("Invalid mnemonic phrase") } } } @@ -111,10 +111,10 @@ fun newWallet(name: String, mnemonic: String, passphrase: String): Wallet { /** * New did * - * @param wallet - * @param didAlias - * @param issuer - * @return + * @param wallet Wallet to store the DID + * @param didAlias Alias for the new DID + * @param issuer If true issuing and holder keys are included, otherwise only a master key pair is added + * @return updated wallet */ fun newDid(wallet: Wallet, didAlias: String, issuer: Boolean): Wallet { // To keep DID index sequential @@ -174,9 +174,9 @@ fun newDid(wallet: Wallet, didAlias: String, issuer: Boolean): Wallet { /** * Get did document * - * @param wallet - * @param didAlias - * @return + * @param wallet Wallet containing the DID + * @param didAlias Alias of the DID + * @return DID document */ fun getDidDocument(wallet: Wallet, didAlias: String): PrismDidDataModel { val didList = wallet.dids.filter { it.alias == didAlias } @@ -188,15 +188,7 @@ fun getDidDocument(wallet: Wallet, didAlias: String): PrismDidDataModel { } catch (e: Exception) { throw Exception("not a Prism DID: $did") } - println("trying to retrieve document for $did\n") - try { - val model = runBlocking { nodeAuthApi.getDidDocument(prismDid) } - println("Public Keys size: ${model.publicKeys.size}\n") - println("Model: ${model.didDataModel}\n") - return model - } catch (e: Exception) { - throw NoSuchElementException("DID '$didAlias' not found.") - } + return runBlocking { nodeAuthApi.getDidDocument(prismDid) } } else { throw NoSuchElementException("DID '$didAlias' not found.") } @@ -205,9 +197,9 @@ fun getDidDocument(wallet: Wallet, didAlias: String): PrismDidDataModel { /** * Publish did * - * @param wallet - * @param didAlias - * @return + * @param wallet Wallet contining the DID + * @param didAlias Alias of the DID + * @return updated wallet */ fun publishDid(wallet: Wallet, didAlias: String): Wallet { val didList = wallet.dids.filter { it.alias == didAlias } @@ -240,27 +232,154 @@ fun publishDid(wallet: Wallet, didAlias: String): Wallet { } waitUntilConfirmed(nodeAuthApi, createDidOperationId) - val status = runBlocking { nodeAuthApi.getOperationStatus(createDidOperationId) } - require(status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { - "expected publishing to be applied" + val response = runBlocking { nodeAuthApi.getOperationInfo(createDidOperationId) } + require(response.status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { + "expected publishing to be applied: ${response.statusDetails}" } did.operationHash = createDidInfo.operationHash.hexValue - println("DID published") return wallet } else { throw NoSuchElementException("DID alias '$didAlias' not found.") } } +/** + * Add key + * + * @param wallet Wallet containing the DID + * @param didAlias Alias of DID where the key will be added + * @param keyId Key identifier for the new key + * @param keyType Type of key (master, issuing or revocation) + * @return updated wallet + */ +fun addKey(wallet: Wallet, didAlias: String, keyId: String, keyType: Int): Wallet { + val didList = wallet.dids.filter { it.alias == didAlias } + if (didList.isNotEmpty()) { + val did = didList[0] + val keyIdx = did.keyPairs.filter { it.keyType == keyType }.size + val nodeAuthApi = NodeAuthApiImpl(GrpcConfig.options) + + // Key pairs to get private keys + val seed = KeyDerivation.binarySeed(MnemonicCode(wallet.mnemonic), wallet.passphrase) + // TODO: masterKey index 0 may be revoked, do something to indicate the currently valid masterKey + val masterKeyPair = KeyGenerator.deriveKeyFromFullPath(seed, did.didIdx, PrismKeyType.MASTER_KEY, 0) + val newKeyPair = KeyGenerator.deriveKeyFromFullPath(seed, did.didIdx, keyType, keyIdx) + + val newKeyPairData = KeyPair( + keyId, + did.didIdx, + keyType, + keyIdx, + newKeyPair.privateKey.getHexEncoded(), + newKeyPair.publicKey.getHexEncoded() + ) + val nodePayloadGenerator = NodePayloadGenerator( + PrismDid.fromString(did.uriLongForm) as LongFormPrismDid, + mapOf(PrismDid.DEFAULT_MASTER_KEY_ID to masterKeyPair.privateKey) + ) + val newKeyInfo = PrismKeyInformation( + keyId, + keyType, + newKeyPair.publicKey + ) + val updateDidInfo = nodePayloadGenerator.updateDid( + previousHash = Sha256Digest.fromHex(did.operationHash), + masterKeyId = PrismDid.DEFAULT_MASTER_KEY_ID, + keysToAdd = arrayOf(newKeyInfo) + ) + val updateDidOperationId = runBlocking { + nodeAuthApi.updateDid( + payload = updateDidInfo.payload, + did = PrismDid.fromString(did.uriCanonical).asCanonical(), + masterKeyId = PrismDid.DEFAULT_MASTER_KEY_ID, + previousOperationHash = Sha256Digest.fromHex(did.operationHash), + keysToAdd = arrayOf(newKeyInfo), + keysToRevoke = arrayOf() + ) + } + waitUntilConfirmed(nodeAuthApi, updateDidOperationId) + + val response = runBlocking { nodeAuthApi.getOperationInfo(updateDidOperationId) } + require(response.status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { + "expected did to be updated: $response" + } + // Update DID last operation hash + did.operationHash = updateDidInfo.operationHash.hexValue + did.keyPairs.add(newKeyPairData) + return wallet + } else { + throw NoSuchElementException("DID alias '$didAlias' not found.") + } +} + +/** + * Revoke key + * + * @param wallet Wallet containing the DID + * @param didAlias Alias of DID containing the key + * @param keyId Identifier of the key to be revoked + * @return updated wallet + */ +fun revokeKey(wallet: Wallet, didAlias: String, keyId: String): Wallet { + val didList = wallet.dids.filter { it.alias == didAlias } + if (didList.isNotEmpty()) { + val did = didList[0] + val keyPairList = did.keyPairs.filter { it.keyId == keyId } + if (keyPairList.isNotEmpty()) { + val nodeAuthApi = NodeAuthApiImpl(GrpcConfig.options) + + // Key pairs to get private keys + val seed = KeyDerivation.binarySeed(MnemonicCode(wallet.mnemonic), wallet.passphrase) + // TODO: masterKey index 0 may be revoked, do something to indicate the currently valid masterKey + val masterKeyPair = KeyGenerator.deriveKeyFromFullPath(seed, did.didIdx, PrismKeyType.MASTER_KEY, 0) + + val nodePayloadGenerator = NodePayloadGenerator( + PrismDid.fromString(did.uriLongForm) as LongFormPrismDid, + mapOf(PrismDid.DEFAULT_MASTER_KEY_ID to masterKeyPair.privateKey) + ) + val updateDidInfo = nodePayloadGenerator.updateDid( + previousHash = Sha256Digest.fromHex(did.operationHash), + masterKeyId = PrismDid.DEFAULT_MASTER_KEY_ID, + keysToRevoke = arrayOf(keyId) + ) + val updateDidOperationId = runBlocking { + nodeAuthApi.updateDid( + payload = updateDidInfo.payload, + did = PrismDid.fromString(did.uriCanonical).asCanonical(), + masterKeyId = PrismDid.DEFAULT_MASTER_KEY_ID, + previousOperationHash = Sha256Digest.fromHex(did.operationHash), + keysToAdd = arrayOf(), + keysToRevoke = arrayOf(keyId) + ) + } + waitUntilConfirmed(nodeAuthApi, updateDidOperationId) + + val response = runBlocking { nodeAuthApi.getOperationInfo(updateDidOperationId) } + require(response.status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { + "expected did to be updated: $response" + } + // Key revocation flag + keyPairList[0].revoked = true + // Update DID last operation hash + did.operationHash = updateDidInfo.operationHash.hexValue + return wallet + } else { + throw NoSuchElementException("Key identifier '$keyId' not found.") + } + } else { + throw NoSuchElementException("DID alias '$didAlias' not found.") + } +} + /** * Issue credential * - * @param wallet - * @param didAlias - * @param credential - * @return + * @param wallet Wallet issuing the credential + * @param didAlias Issuer DID + * @param issuedCredential Credential data + * @return updated wallet */ -fun issueCredential(wallet: Wallet, didAlias: String, credential: Credential): Pair { +fun issueCredential(wallet: Wallet, didAlias: String, issuedCredential: IssuedCredential): Wallet { val didList = wallet.dids.filter { it.alias == didAlias } if (didList.isNotEmpty()) { val issuerDid = didList[0] @@ -270,7 +389,7 @@ fun issueCredential(wallet: Wallet, didAlias: String, credential: Credential): P val seed = KeyDerivation.binarySeed(MnemonicCode(wallet.mnemonic), wallet.passphrase) val issuingKeyPair = deriveKeyPair(issuerDid.keyPairs, seed, PrismDid.DEFAULT_ISSUING_KEY_ID) - claims.add(credential.claim.toCredentialClaim()) + claims.add(issuedCredential.claim.toCredentialClaim()) val nodePayloadGenerator = NodePayloadGenerator( PrismDid.fromString(issuerDid.uriLongForm) as LongFormPrismDid, @@ -281,13 +400,14 @@ fun issueCredential(wallet: Wallet, didAlias: String, credential: Credential): P claims.toTypedArray() ) // credential batchId and hash are required for revocation - credential.batchId = credentialsInfo.batchId.id + issuedCredential.batchId = credentialsInfo.batchId.id val info = credentialsInfo.credentialsAndProofs[0] - credential.credentialHash = info.signedCredential.hash().hexValue - credential.operationHash = credentialsInfo.operationHash.hexValue - credential.verifiedCredential = VerifiedCredential( + issuedCredential.credentialHash = info.signedCredential.hash().hexValue + issuedCredential.operationHash = credentialsInfo.operationHash.hexValue + issuedCredential.issuingDidAlias = didAlias + issuedCredential.verifiedCredential = VerifiedCredential( info.signedCredential.canonicalForm, - info.inclusionProof.encode() + Json.decodeFromString(info.inclusionProof.encode()) ) val issueCredentialsOperationId = runBlocking { nodeAuthApi.issueCredentials( @@ -299,79 +419,131 @@ fun issueCredential(wallet: Wallet, didAlias: String, credential: Credential): P } waitUntilConfirmed(nodeAuthApi, issueCredentialsOperationId) - val status = runBlocking { nodeAuthApi.getOperationStatus(issueCredentialsOperationId) } - require(status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { + val response = runBlocking { nodeAuthApi.getOperationInfo(issueCredentialsOperationId) } + require(response.status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { "expected credentials to be issued" } // Update DID last operation hash issuerDid.operationHash = credentialsInfo.operationHash.hexValue - return Pair(wallet, credential) + wallet.issuedCredentials.add(issuedCredential) + return wallet } else { throw NoSuchElementException("DID alias '$didAlias' not found.") } } -fun revokeCredential(wallet: Wallet, didAlias: String, credential: Credential) { - val didList = wallet.dids.filter { it.alias == didAlias } - if (didList.isNotEmpty()) { - val issuerDid = didList[0] - val nodeAuthApi = NodeAuthApiImpl(GrpcConfig.options) - // Key pairs to get private keys - val seed = KeyDerivation.binarySeed(MnemonicCode(wallet.mnemonic), wallet.passphrase) - val revocationKeyPair = deriveKeyPair(issuerDid.keyPairs, seed, PrismDid.DEFAULT_REVOCATION_KEY_ID) - - val nodePayloadGenerator = NodePayloadGenerator( - PrismDid.fromString(issuerDid.uriLongForm) as LongFormPrismDid, - mapOf(PrismDid.DEFAULT_REVOCATION_KEY_ID to revocationKeyPair.privateKey) - ) - - val revokeInfo = nodePayloadGenerator.revokeCredentials( - PrismDid.DEFAULT_REVOCATION_KEY_ID, - Sha256Digest.fromHex(credential.operationHash), - credential.batchId, - // Pass empty array to revoke all credentials from the batch - arrayOf(Sha256Digest.fromHex(credential.credentialHash)) - ) - - val revokeOperationId = runBlocking { - nodeAuthApi.revokeCredentials( - revokeInfo.payload, - PrismDid.fromString(issuerDid.uriCanonical).asCanonical(), +/** + * Revoke credential + * + * @param wallet Wallet containing the credential + * @param credentialAlias Alias of credential to revoke + * @return Updated wallet + */ +fun revokeCredential(wallet: Wallet, credentialAlias: String): Wallet { + val credentials = wallet.issuedCredentials.filter { it.alias == credentialAlias } + if (credentials.isNotEmpty()) { + val credential = credentials[0] + val didList = wallet.dids.filter { it.alias == credential.issuingDidAlias } + if (didList.isNotEmpty()) { + val issuerDid = didList[0] + val nodeAuthApi = NodeAuthApiImpl(GrpcConfig.options) + // Key pairs to get private keys + val seed = KeyDerivation.binarySeed(MnemonicCode(wallet.mnemonic), wallet.passphrase) + val revocationKeyPair = deriveKeyPair(issuerDid.keyPairs, seed, PrismDid.DEFAULT_REVOCATION_KEY_ID) + val nodePayloadGenerator = NodePayloadGenerator( + PrismDid.fromString(issuerDid.uriLongForm) as LongFormPrismDid, + mapOf(PrismDid.DEFAULT_REVOCATION_KEY_ID to revocationKeyPair.privateKey) + ) + val revokeInfo = nodePayloadGenerator.revokeCredentials( PrismDid.DEFAULT_REVOCATION_KEY_ID, Sha256Digest.fromHex(credential.operationHash), credential.batchId, + // Pass empty array to revoke all credentials from the batch arrayOf(Sha256Digest.fromHex(credential.credentialHash)) ) + val revokeOperationId = runBlocking { + nodeAuthApi.revokeCredentials( + revokeInfo.payload, + PrismDid.fromString(issuerDid.uriCanonical).asCanonical(), + PrismDid.DEFAULT_REVOCATION_KEY_ID, + Sha256Digest.fromHex(credential.operationHash), + credential.batchId, + arrayOf(Sha256Digest.fromHex(credential.credentialHash)) + ) + } + waitUntilConfirmed(nodeAuthApi, revokeOperationId) + + val status = runBlocking { nodeAuthApi.getOperationInfo(revokeOperationId).status } + require(status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { + "expected credential to be revoked" + } + credential.revoked = true + return wallet + } else { + throw NoSuchElementException("Issuing DID not found.") } - waitUntilConfirmed(nodeAuthApi, revokeOperationId) + } else { + throw NoSuchElementException("Credential '$credentialAlias' not found.") + } +} + +/** + * Verify issued credential + * + * @param wallet Wallet containing the credential + * @param credentialAlias Alias of Credential to verify + * @return Verification result + */ +// TODO: refactor to a single verifyCredential function +fun verifyIssuedCredential(wallet: Wallet, credentialAlias: String): VerificationResult { + val credentials = wallet.issuedCredentials.filter { it.alias == credentialAlias } + if (credentials.isNotEmpty()) { + val credential = credentials[0] + val nodeAuthApi = NodeAuthApiImpl(GrpcConfig.options) + val signed = JsonBasedCredential.fromString(credential.verifiedCredential.encodedSignedCredential) + // Use encodeDefaults to generate empty siblings field on proof + val format = Json { encodeDefaults = true } + val proof = MerkleInclusionProof.decode(format.encodeToString(credential.verifiedCredential.proof)) - val status = runBlocking { nodeAuthApi.getOperationStatus(revokeOperationId) } - require(status == AtalaOperationStatus.CONFIRMED_AND_APPLIED) { - "expected credential to be revoked" + return runBlocking { + nodeAuthApi.verify(signed, proof) } - // Update DID last operation hash TODO: Ask IOG why there is no operation hash for revocation - // issuerDid.hash = revokeInfo.operationHash.hexValue } else { - throw NoSuchElementException("DID alias '$didAlias' not found.") + throw Exception("Credential '$credentialAlias' not found.") } } /** - * Verify credential + * Verify imported credential * - * @param credential - * @return + * @param wallet Wallet containing the credential + * @param credentialAlias Alias of credential to verify + * @return Verification result */ -fun verifyCredential(credential: Credential): VerificationResult { - val nodeAuthApi = NodeAuthApiImpl(GrpcConfig.options) - val signed = JsonBasedCredential.fromString(credential.verifiedCredential.encodedSignedCredential) - val proof = MerkleInclusionProof.decode(credential.verifiedCredential.proof) +// TODO: refactor to a single verifyCredential function +fun verifyImportedCredential(wallet: Wallet, credentialAlias: String): VerificationResult { + val credentials = wallet.importedCredentials.filter { it.alias == credentialAlias } + if (credentials.isNotEmpty()) { + val credential = credentials[0] + val nodeAuthApi = NodeAuthApiImpl(GrpcConfig.options) + val signed = JsonBasedCredential.fromString(credential.verifiedCredential.encodedSignedCredential) + // Use encodeDefaults to generate empty siblings field on proof + val format = Json { encodeDefaults = true } + val proof = MerkleInclusionProof.decode(format.encodeToString(credential.verifiedCredential.proof)) - return runBlocking { - nodeAuthApi.verify(signed, proof) + return runBlocking { + nodeAuthApi.verify(signed, proof) + } + } else { + throw Exception("Credential '$credentialAlias' not found.") } } +/** + * Grpc config + * + * @constructor Create empty Grpc config + */ class GrpcConfig { companion object { private val host: String = System.getenv("PRISM_NODE_HOST") diff --git a/src/main/kotlin/com/rootsid/wal/library/Model.kt b/src/main/kotlin/com/rootsid/wal/library/Model.kt index 6e00edc..90d2497 100644 --- a/src/main/kotlin/com/rootsid/wal/library/Model.kt +++ b/src/main/kotlin/com/rootsid/wal/library/Model.kt @@ -14,14 +14,20 @@ import kotlinx.serialization.json.Json * @property mnemonic * @property passphrase * @property dids + * @property importedCredentials + * @property issuedCredentials * @constructor Create empty Wallet */ @Serializable data class Wallet( - val _id: String, // name + var _id: String, // name val mnemonic: List, val passphrase: String, - var dids: MutableList = mutableListOf() + var dids: MutableList = mutableListOf(), + // List of imported (Issued elsewhere) + var importedCredentials: MutableList = mutableListOf(), + // List of credentials issued by a DID from this wallet + var issuedCredentials: MutableList = mutableListOf() ) /** @@ -46,13 +52,16 @@ data class DID( ) /** - * Key path + * Key pair * * @property keyId * @property didIdx * @property keyType * @property keyIdx - * @constructor Create empty Key path + * @property privateKey + * @property publicKey + * @property revoked + * @constructor Create empty Key pair */ @Serializable data class KeyPair( @@ -61,7 +70,8 @@ data class KeyPair( val keyType: Int, val keyIdx: Int, val privateKey: String, - val publicKey: String + val publicKey: String, + var revoked: Boolean = false ) /** @@ -74,7 +84,22 @@ data class KeyPair( @Serializable data class VerifiedCredential( val encodedSignedCredential: String, - val proof: String + val proof: Proof +) + +/** + * Proof + * + * @property hash + * @property index + * @property siblings + * @constructor Create empty Proof + */ +@Serializable +data class Proof( + var hash: String, + var index: Int, + var siblings: MutableList = mutableListOf() ) /** @@ -104,14 +129,20 @@ fun Claim.toCredentialClaim() = CredentialClaim( /** * Credential * - * @property _id + * @property alias + * @property issuingDidAlias * @property claim * @property verifiedCredential + * @property batchId + * @property credentialHash + * @property operationHash + * @property revoked * @constructor Create empty Credential */ @Serializable -data class Credential( - val _id: String, +data class IssuedCredential( + val alias: String, + var issuingDidAlias: String, // Plain json claim val claim: Claim, // Signed VC and proof (This is the real VC) @@ -121,5 +152,36 @@ data class Credential( // Required for revocation var credentialHash: String, // Required for revocation - var operationHash: String + var operationHash: String, + var revoked: Boolean +) + +/** + * Imported credential + * + * @property alias + * @property verifiedCredential + * @constructor Create empty Imported credential + */ +@Serializable +data class ImportedCredential( + val alias: String, + // Signed VC and proof (This is the real VC) + var verifiedCredential: VerifiedCredential, +) + +/** + * Unpack result + * + * @property message + * @property from + * @property to + * @property res + * @constructor Create empty Unpack result + */ +data class UnpackResult( + val message: String, + val from: String?, + val to: String, + val res: org.didcommx.didcomm.model.UnpackResult ) diff --git a/src/main/kotlin/com/rootsid/wal/library/QR.kt b/src/main/kotlin/com/rootsid/wal/library/QR.kt index 03dfd40..6eb50be 100644 --- a/src/main/kotlin/com/rootsid/wal/library/QR.kt +++ b/src/main/kotlin/com/rootsid/wal/library/QR.kt @@ -73,28 +73,3 @@ fun webCamQRScan(seconds: Long): String { frame.dispose() return message } - -// TODO: Implement read qr image file -// fun readQRfile() { -// // Opens a dialog and let's you select a directory -// val directory = BoofSwingUtil.openFileChooser("QR Disk Scanning",BoofSwingUtil.FileTypes.DIRECTORIES) ?: return -// -// // Create the scanner class -// val detector = FactoryFiducial.qrcode(null,GrayU8::class.java) -// -// // Walk through the path recursively, finding all image files, load them, scan for QR codes, add results to a map -// val imageToMessages = mutableMapOf>() -// val elapsedTime = measureTimeMillis { -// directory.walk().filter {UtilImageIO.isImage(it)}.forEach { f -> -// val image = f.absoluteFile.loadImage(ImageType.SB_U8) -// detector.process(image) -// imageToMessages[f.absolutePath] = detector.detections.map { it.message } -// println(f.name) // print so we can see something is happening -// } -// } -// -// // Print a results summary -// val totalMessages = imageToMessages.values.sumBy{it.size} -// println("\nFound ${imageToMessages.size} images with $totalMessages messages averaging %.2f img/s". -// format(imageToMessages.size/(elapsedTime*1e-3))) -// } diff --git a/src/main/kotlin/com/rootsid/wal/library/SecretResolver.kt b/src/main/kotlin/com/rootsid/wal/library/SecretResolver.kt new file mode 100644 index 0000000..3199f71 --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/SecretResolver.kt @@ -0,0 +1,50 @@ +package com.rootsid.wal.library + +import org.didcommx.didcomm.secret.Secret +import org.didcommx.didcomm.secret.SecretResolverEditable +import org.didcommx.didcomm.secret.jwkToSecret +import org.didcommx.didcomm.secret.secretToJwk +import org.didcommx.didcomm.utils.fromJsonToList +import org.didcommx.didcomm.utils.toJson +import java.io.File +import java.util.Optional +import kotlin.io.path.Path +import kotlin.io.path.exists + +class SecretResolver(private val filePath: String = "secrets.json") : SecretResolverEditable { + + private val secrets: MutableMap + + init { + if (!Path(filePath).exists()) { + secrets = mutableMapOf() + save() + } else { + val secretsJson = File(filePath).readText() + secrets = if (secretsJson.isNotEmpty()) { + fromJsonToList(secretsJson).map { jwkToSecret(it) }.associate { it.kid to it }.toMutableMap() + } else { + mutableMapOf() + } + } + } + + private fun save() { + val secretJson = toJson(secrets.values.map { secretToJwk(it) }) + File(filePath).writeText(secretJson) + } + + override fun addKey(secret: Secret) { + secrets.put(secret.kid, secret) + save() + } + + override fun getKids(): List = + secrets.keys.toList() + + override fun findKey(kid: String): Optional = + Optional.ofNullable(secrets.get(kid)) + + override fun findKeys(kids: List): Set = + kids.intersect(secrets.keys) +} diff --git a/src/main/kotlin/com/rootsid/wal/library/Storage.kt b/src/main/kotlin/com/rootsid/wal/library/Storage.kt index df208f1..7924c81 100644 --- a/src/main/kotlin/com/rootsid/wal/library/Storage.kt +++ b/src/main/kotlin/com/rootsid/wal/library/Storage.kt @@ -5,20 +5,22 @@ import org.litote.kmongo.* /** * Open db - * TODO: Add Parameters for Client configuration. - * @return + * + * @return a MongoDatabase representing 'wal' database */ fun openDb(): MongoDatabase { + // TODO: make this configurable so it can use other connection settings val client = KMongo.createClient() // get com.mongodb.MongoClient new instance - return client.getDatabase("wal") // normal java driver usage + // TODO: make databaseName configurable + return client.getDatabase(Config.DB_NAME) // normal java driver usage } /** * Insert wallet * - * @param db - * @param wallet - * @return + * @param db MongoDB Client + * @param wallet Wallet data object to add into the database + * @return true if the operation was acknowledged */ fun insertWallet(db: MongoDatabase, wallet: Wallet): Boolean { val collection = db.getCollection("wallet") @@ -26,63 +28,50 @@ fun insertWallet(db: MongoDatabase, wallet: Wallet): Boolean { return result.wasAcknowledged() } -/** - * Insert credential - * - * @param db - * @param credential - * @return - */ -fun insertCredential(db: MongoDatabase, credential: Credential): Boolean { - val collection = db.getCollection("credential") - val result = collection.insertOne(credential) - return result.wasAcknowledged() -} - /** * Find wallet * - * @param db - * @param walletName - * @return + * @param db MongoDB Client + * @param walletName name of the wallet to find + * @return wallet data object */ fun findWallet(db: MongoDatabase, walletName: String): Wallet { val collection = db.getCollection("wallet") return collection.findOne(Wallet::_id eq walletName) - ?: throw NoSuchElementException("Wallet '$walletName' not found.") + ?: throw Exception("Wallet '$walletName' not found.") } /** - * Find credential + * List wallets * - * @param db - * @param credentialAlias - * @return + * @param db MongoDB Client + * @return list of stored wallet names */ -fun findCredential(db: MongoDatabase, credentialAlias: String): Credential { - val collection = db.getCollection("credential") - return collection.findOne(Wallet::_id eq credentialAlias) - ?: throw NoSuchElementException("Credential '$credentialAlias' not found.") +fun listWallets(db: MongoDatabase): List { + val collection = db.getCollection("wallet") + return collection.find().toList() } /** - * Find wallets + * Wallet exists * - * @param db - * @return + * @param db MongoDB Client + * @param walletName name of the wallet to find + * @return true if the wallet was found */ -fun findWallets(db: MongoDatabase): List { +fun walletExists(db: MongoDatabase, walletName: String): Boolean { val collection = db.getCollection("wallet") - return collection.find().toList() + val wallet = collection.findOne("{_id:'$walletName'}") + return wallet != null } /** * Did alias exists * - * @param db - * @param walletName - * @param didAlias - * @return + * @param db MongoDB Client + * @param walletName name of the wallet storing the did + * @param didAlias alias of the did + * @return true if the did was found */ fun didAliasExists(db: MongoDatabase, walletName: String, didAlias: String): Boolean { val collection = db.getCollection("wallet") @@ -90,12 +79,53 @@ fun didAliasExists(db: MongoDatabase, walletName: String, didAlias: String): Boo return wallet != null } +/** + * Key id exists + * + * @param db MongoDB Client + * @param walletName name of the wallet storing the did + * @param didAlias alias of the did + * @param keyId key identifier + * @return true if the keyId was found + */ +fun keyIdExists(db: MongoDatabase, walletName: String, didAlias: String, keyId: String): Boolean { + val collection = db.getCollection("wallet") + val wallet = collection.findOne("{_id:'$walletName','dids':{${MongoOperator.elemMatch}: {'alias':'$didAlias'}}, 'dids.keyPairs.keyId':'$keyId'}") + return wallet != null +} + +/** + * Issued credential alias exists + * + * @param db MongoDB Client + * @param issuedCredentialAlias credential alias to find + * @return true if the did was found + */ +fun issuedCredentialAliasExists(db: MongoDatabase, walletName: String, issuedCredentialAlias: String): Boolean { + val collection = db.getCollection("wallet") + val wallet = collection.findOne("{_id:'$walletName','issuedCredentials':{${MongoOperator.elemMatch}: {'alias':'$issuedCredentialAlias'}}}") + return wallet != null +} + +/** + * Credential alias exists + * + * @param db MongoDB Client + * @param credentialAlias credential alias to find + * @return true if the did was found + */ +fun credentialAliasExists(db: MongoDatabase, walletName: String, credentialAlias: String): Boolean { + val collection = db.getCollection("wallet") + val wallet = collection.findOne("{_id:'$walletName','credentials':{${MongoOperator.elemMatch}: {'alias':'$credentialAlias'}}}") + return wallet != null +} + /** * Update wallet * - * @param db - * @param wallet - * @return + * @param db MongoDB Client + * @param wallet updated Wallet data object + * @return true if the operation was acknowledged */ fun updateWallet(db: MongoDatabase, wallet: Wallet): Boolean { val collection = db.getCollection("wallet")