diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 93f14b7..bfcbb29 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -18,7 +18,7 @@ permissions: jobs: build: - + name: Compile and Test Code runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3517e35..166a42b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ permissions: jobs: build: - name: Compile and Test code + name: Compile and Test Code runs-on: ubuntu-latest steps: diff --git a/gradle.properties b/gradle.properties index 7def1ea..fac8a0f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=2.0.2-SNAPSHOT +version=2.0.3-SNAPSHOT kotlin.code.style=official diff --git a/src/main/kotlin/com/rootsid/wal/library/didcom/DIDPeer.kt b/src/main/kotlin/com/rootsid/wal/library/didcom/DIDPeer.kt index 810deb9..93d76d5 100644 --- a/src/main/kotlin/com/rootsid/wal/library/didcom/DIDPeer.kt +++ b/src/main/kotlin/com/rootsid/wal/library/didcom/DIDPeer.kt @@ -7,6 +7,7 @@ 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.SecretResolverEditable import org.didcommx.didcomm.secret.generateEd25519Keys import org.didcommx.didcomm.secret.generateX25519Keys import org.didcommx.didcomm.secret.jwkToSecret @@ -15,108 +16,101 @@ import org.didcommx.didcomm.utils.toJson import org.didcommx.peerdid.* import java.util.* -fun createPeerDID( - authKeysCount: Int = 1, - agreementKeysCount: Int = 1, - serviceEndpoint: String? = null, - serviceRoutingKeys: List, - secretResolver: SecretResolverCustom -): String { - // 1. generate keys in JWK format - val x25519keyPairs = (1..agreementKeysCount).map { generateX25519Keys() } - val ed25519keyPairs = (1..authKeysCount).map { generateEd25519Keys() } +class DIDPeer(private val secretResolver: SecretResolverEditable = SecretResolverCustom()) { - // 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 - ) - } + fun create( + authKeysCount: Int = 1, agreementKeysCount: Int = 1, + serviceEndpoint: String? = null, serviceRoutingKeys: List + ): String { + // 1. generate keys in JWK format + val x25519keyPairs = (1..agreementKeysCount).map { generateX25519Keys() } + val ed25519keyPairs = (1..authKeysCount).map { generateEd25519Keys() } - // 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() - ) - } + // 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 + ) + } - // 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 - ) + // 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() + ) + } - // 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)) + // 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(resolve(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 } - return did -} -fun resolvePeerDID(did: String, format: VerificationMaterialFormatPeerDID) = - org.didcommx.peerdid.resolvePeerDID(did, format) + fun resolve(did: String, format: VerificationMaterialFormatPeerDID) = resolvePeerDID(did, format) -fun pack( - data: String, - to: String, - from: String? = null, - signFrom: String? = null, - protectSender: Boolean = true, - secretsResolver: SecretResolverCustom -): 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 pack( + data: String, + to: String, + from: String? = null, + signFrom: String? = null, + protectSender: Boolean = true + ): PackEncryptedResult { + val didComm = DIDComm(DIDDocResolverPeerDID(), secretResolver) + 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: SecretResolverCustom): 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 - ) + fun unpack(packedMsg: String): 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/didcom/model/DidComSecret.kt b/src/main/kotlin/com/rootsid/wal/library/didcom/model/DidComSecret.kt new file mode 100644 index 0000000..c5e8577 --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/didcom/model/DidComSecret.kt @@ -0,0 +1,11 @@ +package com.rootsid.wal.library.didcom.model + +import kotlinx.serialization.Contextual + +interface DidComSecret { + val _id: String + val secret: Map +} + + + diff --git a/src/main/kotlin/com/rootsid/wal/library/didcom/storage/DidComSecretStorage.kt b/src/main/kotlin/com/rootsid/wal/library/didcom/storage/DidComSecretStorage.kt new file mode 100644 index 0000000..e5657f9 --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/didcom/storage/DidComSecretStorage.kt @@ -0,0 +1,15 @@ +package com.rootsid.wal.library.didcom.storage + +import com.rootsid.wal.library.didcom.model.DidComSecret + +interface DidComSecretStorage { + fun insert(kid: String, secretJson: Map): DidComSecret + + fun findById(kid: String): DidComSecret + + fun findIdsIn(kids: List): Set + + fun listIds(): List + + fun list(): List +} diff --git a/src/main/kotlin/com/rootsid/wal/library/didcom/storage/SecretResolverCustom.kt b/src/main/kotlin/com/rootsid/wal/library/didcom/storage/SecretResolverCustom.kt index 8335ddc..76943e8 100644 --- a/src/main/kotlin/com/rootsid/wal/library/didcom/storage/SecretResolverCustom.kt +++ b/src/main/kotlin/com/rootsid/wal/library/didcom/storage/SecretResolverCustom.kt @@ -4,47 +4,19 @@ 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.* -import kotlin.io.path.Path -import kotlin.io.path.exists -class SecretResolverCustom(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) - } +class SecretResolverCustom(private val didComSecretStorage: DidComSecretStorage = SecretResolverFileSystemStorage()) : + SecretResolverEditable { override fun addKey(secret: Secret) { - secrets.put(secret.kid, secret) - save() + didComSecretStorage.insert(secret.kid, secretToJwk(secret)) } - override fun getKids(): List = - secrets.keys.toList() - override fun findKey(kid: String): Optional = - Optional.ofNullable(secrets.get(kid)) + Optional.of(jwkToSecret(didComSecretStorage.findById(kid).secret)) + + override fun getKids(): List = didComSecretStorage.listIds() - override fun findKeys(kids: List): Set = - kids.intersect(secrets.keys) + override fun findKeys(kids: List): Set = didComSecretStorage.findIdsIn(kids) } diff --git a/src/main/kotlin/com/rootsid/wal/library/didcom/storage/SecretResolverFileSystemStorage.kt b/src/main/kotlin/com/rootsid/wal/library/didcom/storage/SecretResolverFileSystemStorage.kt new file mode 100644 index 0000000..f4c4f51 --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/didcom/storage/SecretResolverFileSystemStorage.kt @@ -0,0 +1,66 @@ +package com.rootsid.wal.library.didcom.storage + +//import com.rootsid.wal.library.didcom.model.createAnonymousObj +import com.rootsid.wal.library.didcom.model.DidComSecret +import kotlinx.serialization.Contextual +import org.didcommx.didcomm.secret.Secret +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 kotlin.collections.set +import kotlin.io.path.Path +import kotlin.io.path.exists + +class SecretResolverFileSystemStorage(private val filePath: String = "secrets.json") : DidComSecretStorage { + 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() + } + } + } + + override fun insert(kid: String, secretJson: Map): DidComSecret { + secrets[kid] = jwkToSecret(secretJson) + save() + + return createAnonymousDidComSecret(kid, secretToJwk(secrets[kid]!!)) + } + + private fun save() { + val secretJson = toJson(secrets.values.map { secretToJwk(it) }) + File(filePath).writeText(secretJson) + } + + private fun createAnonymousDidComSecret(id: String, secret: Map): DidComSecret = + object : DidComSecret { + override val _id: String + get() = id + override val secret: Map + get() = secret + } + + override fun findById(kid: String): DidComSecret { + secrets[kid]?.let { + return createAnonymousDidComSecret(kid, secretToJwk(it)) + } + + throw RuntimeException("Secret '$kid' not found.") + } + + override fun findIdsIn(kids: List): Set = kids.intersect(secrets.keys) + + override fun listIds(): List = secrets.keys.toList() + + override fun list(): List = secrets.entries.map { createAnonymousDidComSecret(it.key, secretToJwk(it.value)) } +} diff --git a/src/main/kotlin/com/rootsid/wal/library/mongoimpl/SecretResolverDocStorage.kt b/src/main/kotlin/com/rootsid/wal/library/mongoimpl/SecretResolverDocStorage.kt new file mode 100644 index 0000000..d1a9cd2 --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/mongoimpl/SecretResolverDocStorage.kt @@ -0,0 +1,41 @@ +package com.rootsid.wal.library.mongoimpl + +import com.mongodb.client.MongoCollection +import com.mongodb.client.MongoDatabase +import com.rootsid.wal.library.didcom.model.DidComSecret +import com.rootsid.wal.library.didcom.storage.DidComSecretStorage +import com.rootsid.wal.library.mongoimpl.config.DefaultMongoDbConn +import com.rootsid.wal.library.mongoimpl.document.DidComSecretDocument +import org.litote.kmongo.* + +class SecretResolverDocStorage(db: MongoDatabase? = null, collectionName: String = "didComSecrets") : DidComSecretStorage { + private val collection: MongoCollection + + init { + val mongoConn = db ?: DefaultMongoDbConn.open() + + this.collection = mongoConn.getCollection(collectionName) + } + + override fun insert(kid: String, secretJson: Map): DidComSecret { + DidComSecretDocument(kid, secretJson).let { + val result = collection.insertOne(it) + + if (result.wasAcknowledged()) { + return it + } + } + + throw RuntimeException("Failed inserting secret in storage.") + } + + override fun findById(kid: String): DidComSecret = + collection.findOne(DidComSecret::_id eq kid) ?: throw Exception("Secret '$kid' not found.") + + override fun findIdsIn(kids: List): Set = + collection.find(DidComSecret::_id `in` kids).projection(DidComSecret::_id).map { it._id }.toSet() + + override fun list(): List = collection.find().toList() + + override fun listIds(): List = collection.find().projection(DidComSecret::_id).map { it._id }.toList() +} diff --git a/src/main/kotlin/com/rootsid/wal/library/mongoimpl/document/DidComSecretDocument.kt b/src/main/kotlin/com/rootsid/wal/library/mongoimpl/document/DidComSecretDocument.kt new file mode 100644 index 0000000..53ffd50 --- /dev/null +++ b/src/main/kotlin/com/rootsid/wal/library/mongoimpl/document/DidComSecretDocument.kt @@ -0,0 +1,11 @@ +package com.rootsid.wal.library.mongoimpl.document + +import com.rootsid.wal.library.didcom.model.DidComSecret +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable + +@Serializable +data class DidComSecretDocument( + override val _id: String, + override val secret: Map = mutableMapOf() +) : DidComSecret diff --git a/src/main/kotlin/com/rootsid/wal/library/wallet/WalletService.kt b/src/main/kotlin/com/rootsid/wal/library/wallet/WalletService.kt index 4c50f52..1c1eed1 100644 --- a/src/main/kotlin/com/rootsid/wal/library/wallet/WalletService.kt +++ b/src/main/kotlin/com/rootsid/wal/library/wallet/WalletService.kt @@ -27,6 +27,10 @@ class WalletService(private val walletStorage: WalletStorage, private val dlt: D * @return a new wallet */ fun createWallet(id: String, mnemonic: String, passphrase: String): Wallet { + if (walletStorage.exists(id)) { + throw RuntimeException("Duplicated Wallet identifier") + } + val seed = generateSeed(mnemonic, passphrase) return walletStorage.insert(walletStorage.createWalletObject(id, BytesOps.bytesToHex(seed))) } @@ -74,7 +78,7 @@ class WalletService(private val walletStorage: WalletStorage, private val dlt: D mnemonicList = MnemonicCode(mnemonic.split(Constant.MNEMONIC_SEPARATOR).map { it.trim() }) seed = KeyDerivation.binarySeed(mnemonicList, passphrase) } catch (e: Exception) { - throw Exception("Invalid mnemonic phrase") + throw RuntimeException("Invalid mnemonic phrase") } } return seed @@ -91,7 +95,7 @@ class WalletService(private val walletStorage: WalletStorage, private val dlt: D findWalletById(walletId) .let { w -> if (w.dids.any { it.alias.equals(didAlias, true) }) { - throw Exception("Duplicated DID alias") + throw RuntimeException("Duplicated DID alias") } val newDid = dlt.newDid(didAlias, w.dids.size, BytesOps.hexToBytes(w.seed), issuer) @@ -130,15 +134,15 @@ class WalletService(private val walletStorage: WalletStorage, private val dlt: D val didUpdate = dlt.publishDid(did, BytesOps.hexToBytes(wallet.seed)) did.publishedStatus = AtalaOperationStatus.PENDING_SUBMISSION - did.operationHash = didUpdate.operationHash ?: throw Exception("Unable to find operation id.") - did.operationId = didUpdate.operationId ?: throw Exception("Unable to find operation id.") + did.operationHash = didUpdate.operationHash ?: throw RuntimeException("Unable to find operation id.") + did.operationId = didUpdate.operationId ?: throw RuntimeException("Unable to find operation id.") walletStorage.update(wallet) println("DID '$didAlias' published.") return did } - ?: throw Exception("Did alias '$didAlias' not found") + ?: throw RuntimeException("Did alias '$didAlias' not found") } } @@ -169,6 +173,6 @@ class WalletService(private val walletStorage: WalletStorage, private val dlt: D val publishOperationInfo = dlt.getDidPublishOperationInfo(d) d.publishedStatus = publishOperationInfo return publishOperationInfo - } ?: throw Exception("Did alias '$didAlias' not found") + } ?: throw RuntimeException("Did alias '$didAlias' not found") } } diff --git a/src/main/kotlin/com/rootsid/wal/library/wallet/model/Wallet.kt b/src/main/kotlin/com/rootsid/wal/library/wallet/model/Wallet.kt index 69b3d45..2aff1d6 100644 --- a/src/main/kotlin/com/rootsid/wal/library/wallet/model/Wallet.kt +++ b/src/main/kotlin/com/rootsid/wal/library/wallet/model/Wallet.kt @@ -15,8 +15,10 @@ interface Wallet { val _id: String // name of the wallet val seed: String var dids: MutableList + // List of imported (Issued elsewhere) var importedCredentials: MutableList + // List of credentials issued by a DID from this wallet var issuedCredentials: MutableList }