Skip to content

Commit

Permalink
feat(Mercury): Orchestration and tests (#49)
Browse files Browse the repository at this point in the history
* feat(Mercury): Orchestration and tests

* updated for PR comments

* Fixing linting issues and request changes from the PR

* Fix linting issues

* Fix wildcard imports

* Add current time to didcomm message in case unpacked message does not has a created time

---------

Co-authored-by: Cristian G <[email protected]>
  • Loading branch information
curtis-h and cristianIOHK authored Apr 11, 2023
1 parent 5ac155b commit cc1313c
Show file tree
Hide file tree
Showing 17 changed files with 899 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ data class DIDDocument(
val coreProperties: Array<DIDDocumentCoreProperty>,
) {

val services: Array<Service>
get() = coreProperties.fold(arrayOf()) { acc, property ->
if (property is DIDDocument.Services) acc.plus(property.values) else acc
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ sealed class CastorError(message: String? = null) : Throwable(message) {
sealed class MercuryError(message: String? = null) : Throwable(message) {
class InvalidURLError(message: String? = null) : MercuryError(message)
class NoDIDReceiverSetError(message: String? = null) : MercuryError(message)
class NoDIDSenderSetError(message: String? = null) : MercuryError(message)
class NoValidServiceFoundError(message: String? = null) : MercuryError(message)
class FromFieldNotSetError(message: String? = null) : MercuryError(message)
class UnknownAttachmentDataError(message: String? = null) : MercuryError(message)
Expand Down
3 changes: 3 additions & 0 deletions mercury/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ kotlin {
implementation(project(":domain"))
implementation("io.iohk.atala.prism:apollo:$apolloVersion")
implementation("io.iohk.atala.prism:base64:$apolloVersion")
implementation("io.iohk.atala.prism:base64:1.6.0-alpha")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("org.didcommx:didcomm:0.3.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
}
val allButJSMain by creating {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,85 @@ package io.iohk.atala.prism.walletsdk.mercury

import io.iohk.atala.prism.walletsdk.domain.buildingBlocks.Castor
import io.iohk.atala.prism.walletsdk.domain.buildingBlocks.Mercury
import io.iohk.atala.prism.walletsdk.domain.buildingBlocks.Pluto
import io.iohk.atala.prism.walletsdk.domain.models.DID
import io.iohk.atala.prism.walletsdk.domain.models.DIDDocument
import io.iohk.atala.prism.walletsdk.domain.models.MercuryError
import io.iohk.atala.prism.walletsdk.domain.models.Message
import io.iohk.atala.prism.walletsdk.mercury.forward.ForwardMessage
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

actual class MercuryImpl actual constructor(castor: Castor, pluto: Pluto) : Mercury {
actual override fun packMessage(message: Message): String {
TODO("Not yet implemented")
class MercuryImpl constructor(
val castor: Castor,
val protocol: DIDCommProtocol,
val api: Api
) : Mercury {
@Throws(MercuryError.NoDIDReceiverSetError::class)
override fun packMessage(message: Message): String {
if (message.to !is DID) throw MercuryError.NoDIDReceiverSetError()

if (message.from !is DID) throw MercuryError.NoDIDSenderSetError()

return protocol.packEncrypted(message)
}

actual override fun unpackMessage(message: String): Message {
TODO("Not yet implemented")
override fun unpackMessage(message: String): Message {
return protocol.unpack(message)
}

@Throws(MercuryError.NoDIDReceiverSetError::class)
override suspend fun sendMessage(message: Message): ByteArray? {
TODO("Not yet implemented")
if (message.to !is DID) throw MercuryError.NoDIDReceiverSetError()

if (message.from !is DID) throw MercuryError.NoDIDSenderSetError()

val document = castor.resolveDID(message.to.toString())
val packedMessage = packMessage(message)
val service = document.services.find { it.type.contains("DIDCommMessaging") }

getMediatorDID(service)?.let { mediatorDid ->
val mediatorDocument = castor.resolveDID(mediatorDid.toString())
val mediatorUri =
mediatorDocument.services.find { it.type.contains("DIDCommMessaging") }?.serviceEndpoint?.uri
val forwardMsg = prepareForwardMessage(message, packedMessage, mediatorDid)
val packedForwardMsg = packMessage(forwardMsg.makeMessage())

return makeRequest(mediatorUri, packedForwardMsg)
}

return makeRequest(service, packedMessage)
}

override suspend fun sendMessageParseMessage(message: Message): Message? {
TODO("Not yet implemented")
val responseBody = sendMessage(message)
val responseJSON = Json.encodeToString(responseBody)
return unpackMessage(responseJSON)
}

private fun prepareForwardMessage(message: Message, encrypted: String, mediatorDid: DID): ForwardMessage {
return ForwardMessage(
body = message.to.toString(),
encryptedMessage = encrypted,
from = message.from!!,
to = mediatorDid,
)
}

@Throws(MercuryError.NoValidServiceFoundError::class)
private fun makeRequest(service: DIDDocument.Service?, message: String): ByteArray? {
if (service !is DIDDocument.Service) throw MercuryError.NoValidServiceFoundError()

return api.request("POST", service.serviceEndpoint.uri, message)
}

@Throws(MercuryError.NoValidServiceFoundError::class)
private fun makeRequest(uri: String?, message: String): ByteArray? {
if (uri !is String) throw MercuryError.NoValidServiceFoundError()

return api.request("POST", uri, message)
}

private fun getMediatorDID(service: DIDDocument.Service?): DID? {
return service?.serviceEndpoint?.uri?.let { uri -> castor.parseDID(uri) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.iohk.atala.prism.walletsdk.mercury.forward

import io.iohk.atala.prism.apollo.base64.base64UrlEncoded
import io.iohk.atala.prism.walletsdk.domain.models.AttachmentBase64
import io.iohk.atala.prism.walletsdk.domain.models.AttachmentDescriptor
import io.iohk.atala.prism.walletsdk.domain.models.DID
import io.iohk.atala.prism.walletsdk.domain.models.Message
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.UUID

class ForwardMessage @JvmOverloads constructor(
val body: String,
val from: DID,
val to: DID,
val encryptedMessage: String,
val id: String = UUID.randomUUID().toString()
) {
companion object {
const val type = "https://didcomm.org/routing/2.0/forward"
}

fun makeMessage(): Message {
val forwardBody = Json.encodeToString(ForwardBody(body)).base64UrlEncoded
val attachmentData = AttachmentBase64(encryptedMessage)
val attachment = AttachmentDescriptor(UUID.randomUUID().toString(), "application/json", attachmentData)
val message = Message(
id = id,
piuri = type,
from = from,
to = to,
body = forwardBody,
attachments = arrayOf(attachment)
)

return message
}
}

@Serializable
data class ForwardBody(val next: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.iohk.atala.prism.walletsdk.mercury.resolvers

import io.iohk.atala.prism.walletsdk.domain.buildingBlocks.Castor
import io.iohk.atala.prism.walletsdk.domain.models.Curve
import io.iohk.atala.prism.walletsdk.domain.models.DIDDocument
import kotlinx.coroutines.runBlocking
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 java.util.Optional

class DIDCommDIDResolver(val castor: Castor) : DIDDocResolver {
override fun resolve(did: String): Optional<DIDDoc> {
return runBlocking {
val doc = castor.resolveDID(did)
val authentications = mutableListOf<String>()
val keyAgreements = mutableListOf<String>()
val services = mutableListOf<DIDCommService>()
val verificationMethods = mutableListOf<VerificationMethod>()

doc.coreProperties.forEach { coreProperty ->
val methods = when (coreProperty) {
is DIDDocument.Authentication -> coreProperty.verificationMethods
is DIDDocument.AssertionMethod -> coreProperty.verificationMethods
is DIDDocument.KeyAgreement -> coreProperty.verificationMethods
is DIDDocument.CapabilityInvocation -> coreProperty.verificationMethods
is DIDDocument.CapabilityDelegation -> coreProperty.verificationMethods
else -> emptyArray()
}

methods.forEach { method ->
val curve = DIDDocument.VerificationMethod.getCurveByType(method.publicKeyJwk?.get("crv")!!)

if (curve === Curve.ED25519) {
authentications.add(method.id.string())
}

if (curve === Curve.X25519) {
keyAgreements.add(method.id.string())
}

verificationMethods.add(
VerificationMethod(
id = method.id.string(),
controller = method.controller.toString(),
type = VerificationMethodType.JSON_WEB_KEY_2020,
verificationMaterial = VerificationMaterial(
VerificationMaterialFormat.JWK,
method.publicKeyJwk.toString() ?: ""
)
)
)
}

if (coreProperty is DIDDocument.Service && coreProperty.type.contains("DIDCommMessaging")) {
services.add(
DIDCommService(
id = coreProperty.id,
serviceEndpoint = coreProperty.serviceEndpoint.uri,
routingKeys = coreProperty.serviceEndpoint.routingKeys?.toList() ?: emptyList(),
accept = coreProperty.serviceEndpoint.accept?.toList() ?: emptyList()
)
)
}
}

Optional.of(
DIDDoc(
doc.id.toString(),
keyAgreements,
authentications,
verificationMethods,
services
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.iohk.atala.prism.walletsdk.mercury.resolvers

import io.iohk.atala.prism.apollo.base64.base64UrlEncoded
import io.iohk.atala.prism.walletsdk.domain.buildingBlocks.Pluto
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.didcommx.didcomm.common.VerificationMaterial
import org.didcommx.didcomm.common.VerificationMaterialFormat
import org.didcommx.didcomm.common.VerificationMethodType
import org.didcommx.didcomm.secret.Secret
import org.didcommx.didcomm.secret.SecretResolver
import java.util.Optional

class DIDCommSecretsResolver(val pluto: Pluto) : SecretResolver {
override fun findKeys(kids: List<String>): Set<String> {
return runBlocking {
kids.filter { pluto.getDIDPrivateKeyByID(it).firstOrNull() != null }.toSet()
}
}

override fun findKey(kid: String): Optional<Secret> {
return runBlocking {
pluto.getDIDPrivateKeyByID(kid)
.firstOrNull()
?.let { privateKey ->
Optional.of(
Secret(
kid,
VerificationMethodType.JSON_WEB_KEY_2020,
VerificationMaterial(
VerificationMaterialFormat.MULTIBASE,
privateKey.value.base64UrlEncoded
)
)
)
} ?: Optional.empty()
}
}
}
Loading

0 comments on commit cc1313c

Please sign in to comment.