Skip to content

Commit

Permalink
Merge branch 'main' into feature/ATL-6862
Browse files Browse the repository at this point in the history
Signed-off-by: Cristian G <cristian.castro@iohk.io>
  • Loading branch information
cristianIOHK committed Sep 25, 2024
2 parents 51f4948 + fce300e commit 9b4c0b9
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ import org.hyperledger.identus.walletsdk.domain.buildingblocks.Pollux
import org.hyperledger.identus.walletsdk.domain.models.Api
import org.hyperledger.identus.walletsdk.domain.models.ApiImpl
import org.hyperledger.identus.walletsdk.domain.models.ApolloError
import org.hyperledger.identus.walletsdk.domain.models.AttachmentData
import org.hyperledger.identus.walletsdk.domain.models.AttachmentData.AttachmentBase64
import org.hyperledger.identus.walletsdk.domain.models.AttachmentData.AttachmentJsonData

import org.hyperledger.identus.walletsdk.domain.models.AttachmentDescriptor
import org.hyperledger.identus.walletsdk.domain.models.Credential
import org.hyperledger.identus.walletsdk.domain.models.CredentialOperationsOptions
Expand Down Expand Up @@ -94,12 +95,15 @@ import org.hyperledger.identus.walletsdk.domain.models.keyManagement.TypeKey
import org.hyperledger.identus.walletsdk.edgeagent.helpers.AgentOptions
import org.hyperledger.identus.walletsdk.edgeagent.mediation.BasicMediatorHandler
import org.hyperledger.identus.walletsdk.edgeagent.mediation.MediationHandler
import org.hyperledger.identus.walletsdk.edgeagent.models.ConnectionlessMessageData
import org.hyperledger.identus.walletsdk.edgeagent.protocols.ProtocolType
import org.hyperledger.identus.walletsdk.edgeagent.protocols.connection.DIDCommConnectionRunner
import org.hyperledger.identus.walletsdk.edgeagent.protocols.findProtocolTypeByValue
import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.IssueCredential
import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.OfferCredential
import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.RequestCredential
import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessCredentialOffer
import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessRequestPresentation
import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.DIDCommInvitationRunner
import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.InvitationType
import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.OutOfBandInvitation
Expand Down Expand Up @@ -824,7 +828,12 @@ open class EdgeAgent {
@Throws(EdgeAgentError.UnknownInvitationTypeError::class, SerializationException::class)
suspend fun parseInvitation(str: String): InvitationType {
Url.parse(str)?.let {
return parseOOBInvitation(it)
val outOfBandInvitation = parseOOBInvitation(it)
if (outOfBandInvitation.attachments.isNotEmpty()) {
return connectionlessInvitation(outOfBandInvitation)
} else {
return outOfBandInvitation
}
} ?: run {
try {
val json = Json.decodeFromString<JsonObject>(str)
Expand Down Expand Up @@ -904,7 +913,7 @@ open class EdgeAgent {
* @return The parsed Out-of-Band invitation
* @throws [EdgeAgentError.UnknownInvitationTypeError] if the URL is not a valid Out-of-Band invitation
*/
private suspend fun parseOOBInvitation(url: Url): OutOfBandInvitation {
private fun parseOOBInvitation(url: Url): OutOfBandInvitation {
return DIDCommInvitationRunner(url).run()
}

Expand All @@ -916,84 +925,9 @@ open class EdgeAgent {
*/
suspend fun acceptOutOfBandInvitation(invitation: OutOfBandInvitation) {
val ownDID = createNewPeerDID(updateMediator = true)
if (invitation.attachments.isNotEmpty()) {
// If attachments not empty, means connectionless presentation
val now = Instant.fromEpochMilliseconds(getTimeMillis())
val expiryDate = Instant.fromEpochSeconds(invitation.expiresTime)
if (now > expiryDate) {
throw EdgeAgentError.ExpiredInvitation()
}

val jsonString = invitation.attachments.firstNotNullOf { it.data.getDataAsJsonString() }
val requestPresentationJson = Json.parseToJsonElement(jsonString).jsonObject
if (!requestPresentationJson.containsKey("id")) {
throw EdgeAgentError.MissingOrNullFieldError("id", "Request")
}
if (!requestPresentationJson.containsKey("body")) {
throw EdgeAgentError.MissingOrNullFieldError("body", "Request")
}
if (!requestPresentationJson.containsKey("attachments")) {
throw EdgeAgentError.MissingOrNullFieldError("attachments", "Request")
}
if (!requestPresentationJson.containsKey("thid")) {
throw EdgeAgentError.MissingOrNullFieldError("thid", "Request")
}
if (!requestPresentationJson.containsKey("from")) {
throw EdgeAgentError.MissingOrNullFieldError("from", "Request")
}

val requestId = requestPresentationJson["id"]!!
val requestBody = requestPresentationJson["body"]!!
val requestAttachments = requestPresentationJson["attachments"]!!
val requestThid = requestPresentationJson["thid"]!!
val requestFrom = requestPresentationJson["from"]!!

if (requestAttachments.jsonArray.size == 0) {
throw EdgeAgentError.MissingOrNullFieldError("attachments", "Request")
}
val attachmentJsonObject = requestAttachments.jsonArray[0]
if (!attachmentJsonObject.jsonObject.containsKey("id")) {
throw EdgeAgentError.MissingOrNullFieldError("id", "Request attachments")
}
if (!attachmentJsonObject.jsonObject.containsKey("media_type")) {
throw EdgeAgentError.MissingOrNullFieldError("media_type", "Request attachments")
}
if (!attachmentJsonObject.jsonObject.containsKey("data")) {
if (!attachmentJsonObject.jsonObject["data"]!!.jsonObject.containsKey("json")) {
throw EdgeAgentError.MissingOrNullFieldError("json", "Request attachments data")
}
throw EdgeAgentError.MissingOrNullFieldError("data", "Request attachments")
}
if (!attachmentJsonObject.jsonObject.containsKey("format")) {
throw EdgeAgentError.MissingOrNullFieldError("format", "Request attachments")
}
val attachmentId = attachmentJsonObject.jsonObject["id"]!!
val attachmentMediaType = attachmentJsonObject.jsonObject["media_type"]!!
val attachmentData = attachmentJsonObject.jsonObject["data"]!!.jsonObject["json"]!!
val attachmentFormat = attachmentJsonObject.jsonObject["format"]!!

val attachmentDescriptor = AttachmentDescriptor(
id = attachmentId.jsonPrimitive.content,
mediaType = attachmentMediaType.jsonPrimitive.content,
data = AttachmentJsonData(attachmentData.toString()),
format = attachmentFormat.jsonPrimitive.content
)

val requestPresentation = RequestPresentation(
id = requestId.jsonPrimitive.content,
body = Json.decodeFromString(requestBody.jsonObject.toString()),
attachments = arrayOf(attachmentDescriptor),
thid = requestThid.jsonPrimitive.content,
from = DID(requestFrom.jsonPrimitive.content),
to = ownDID
)

pluto.storeMessage(requestPresentation.makeMessage())
} else {
// Regular OOB invitation
val pair = DIDCommConnectionRunner(invitation, pluto, ownDID, connectionManager).run()
connectionManager.addConnection(pair)
}
// Regular OOB invitation
val pair = DIDCommConnectionRunner(invitation, pluto, ownDID, connectionManager).run()
connectionManager.addConnection(pair)
}

/**
Expand Down Expand Up @@ -1616,6 +1550,175 @@ open class EdgeAgent {
return nonce.toString()
}

/**
* Parses and validates a connectionless message from a JSON object. The method checks for the existence
* of required fields (e.g., id, body, attachments, thid, from) and throws errors if any are missing.
* It extracts necessary information from the message, including the attachment details, and returns
* a ConnectionlessMessageData object containing the parsed information.
*
* @param messageJson The JsonObject representing the connectionless message.
* @return A ConnectionlessMessageData object containing the parsed message data.
* @throws EdgeAgentError.MissingOrNullFieldError if any required field is missing or null.
*/
private fun parseAndValidateMessage(messageJson: JsonObject): ConnectionlessMessageData {
// Perform validation
if (!messageJson.containsKey("id")) throw EdgeAgentError.MissingOrNullFieldError("id", "Request")
if (!messageJson.containsKey("body")) throw EdgeAgentError.MissingOrNullFieldError("body", "Request")
if (!messageJson.containsKey("attachments")) {
throw EdgeAgentError.MissingOrNullFieldError(
"attachments",
"Request"
)
}
if (!messageJson.containsKey("thid")) throw EdgeAgentError.MissingOrNullFieldError("thid", "Request")
if (!messageJson.containsKey("from")) throw EdgeAgentError.MissingOrNullFieldError("from", "Request")

val messageId = messageJson["id"]!!.jsonPrimitive.content
val messageBody = messageJson["body"]!!.toString()
val messageThid = messageJson["thid"]!!.jsonPrimitive.content
val messageFrom = messageJson["from"]!!.jsonPrimitive.content

// Validate and parse the first attachment
val attachmentJsonObject = messageJson["attachments"]!!.jsonArray.first().jsonObject
if (!attachmentJsonObject.containsKey("id")) {
throw EdgeAgentError.MissingOrNullFieldError(
"id",
"Request attachments"
)
}
if (!attachmentJsonObject.containsKey("media_type")) {
throw EdgeAgentError.MissingOrNullFieldError(
"media_type",
"Request attachments"
)
}
if (!attachmentJsonObject.containsKey("data")) {
throw EdgeAgentError.MissingOrNullFieldError(
"data",
"Request attachments"
)
}
if (!attachmentJsonObject.containsKey("format")) {
throw EdgeAgentError.MissingOrNullFieldError(
"format",
"Request attachments"
)
}

val attachmentId = attachmentJsonObject["id"]!!.jsonPrimitive.content
val attachmentMediaType = attachmentJsonObject["media_type"]!!.jsonPrimitive.content
val attachmentData = attachmentJsonObject["data"]!!.jsonObject["json"]!!.toString()
val attachmentFormat = attachmentJsonObject["format"]!!.jsonPrimitive.content

val attachmentDescriptor = AttachmentDescriptor(
id = attachmentId,
mediaType = attachmentMediaType,
data = AttachmentData.AttachmentJsonData(attachmentData),
format = attachmentFormat
)

// Return the extracted data
return ConnectionlessMessageData(
messageId = messageId,
messageBody = messageBody,
attachmentDescriptor = attachmentDescriptor,
messageThid = messageThid,
messageFrom = messageFrom
)
}

/**
* Handles a connectionless invitation by parsing the invitation string, extracting the necessary
* message data, and invoking the appropriate handler based on the type of the message.
*
* @param did The DID (Decentralized Identifier) associated with the invitation.
* @param invitationString The JSON string representing the invitation.
* @throws EdgeAgentError.MissingOrNullFieldError if any required field is missing or null.
* @throws EdgeAgentError.UnknownInvitationTypeError if the invitation type is unknown.
*/
private suspend fun connectionlessInvitation(outOfBandInvitation: OutOfBandInvitation): InvitationType {
val now = Instant.fromEpochMilliseconds(getTimeMillis())
val expiryDate = Instant.fromEpochSeconds(outOfBandInvitation.expiresTime)
if (now > expiryDate) {
throw EdgeAgentError.ExpiredInvitation()
}

val jsonString = outOfBandInvitation.attachments.firstNotNullOf { it.data.getDataAsJsonString() }
val invitation = Json.parseToJsonElement(jsonString)
if (invitation.jsonObject.containsKey("type")) {
val type = invitation.jsonObject["type"]?.jsonPrimitive?.content
val attachments = invitation.jsonObject["attachments"]!!.jsonArray
if (attachments.isEmpty()) {
throw Exception()
}
val connectionLessMessageData = parseAndValidateMessage(invitation.jsonObject)
return when (type) {
ProtocolType.DidcommOfferCredential.value -> {
handleConnectionlessOfferCredential(connectionLessMessageData)
}

ProtocolType.DidcommRequestPresentation.value -> {
handleConnectionlessRequestPresentation(connectionLessMessageData)
}

else -> {
throw EdgeAgentError.UnknownInvitationTypeError(type ?: "null")
}
}
} else {
throw EdgeAgentError.MissingOrNullFieldError("type", "connectionless invitation")
}
}

/**
* Handles a connectionless Offer Credential message by extracting the necessary data
* from the ConnectionlessMessageData and storing the message using the Pluto service.
*
* @param connectionlessMessageData The parsed data from the connectionless message.
*/
private suspend fun handleConnectionlessOfferCredential(
connectionlessMessageData: ConnectionlessMessageData
): InvitationType {
val did = createNewPeerDID(updateMediator = true)
val offerCredential = OfferCredential(
id = connectionlessMessageData.messageId,
body = Json.decodeFromString(connectionlessMessageData.messageBody),
attachments = arrayOf(connectionlessMessageData.attachmentDescriptor),
thid = connectionlessMessageData.messageThid,
from = DID(connectionlessMessageData.messageFrom),
to = did
)
pluto.storeMessage(offerCredential.makeMessage())
return ConnectionlessCredentialOffer(
offerCredential = offerCredential
)
}

/**
* Handles a connectionless Request Presentation message by extracting the necessary data
* from the ConnectionlessMessageData and storing the message using the Pluto service.
*
* @param connectionlessMessageData The parsed data from the connectionless message.
*/
private suspend fun handleConnectionlessRequestPresentation(
connectionlessMessageData: ConnectionlessMessageData
): InvitationType {
val did = createNewPeerDID(updateMediator = true)
val requestPresentation = RequestPresentation(
id = connectionlessMessageData.messageId,
body = Json.decodeFromString(connectionlessMessageData.messageBody),
attachments = arrayOf(connectionlessMessageData.attachmentDescriptor),
thid = connectionlessMessageData.messageThid,
from = DID(connectionlessMessageData.messageFrom),
to = did
)

// pluto.storeMessage(requestPresentation.makeMessage())
return ConnectionlessRequestPresentation(
requestPresentation = requestPresentation
)
}

/**
* Enumeration representing the current state of the agent.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.hyperledger.identus.walletsdk.edgeagent.models

import org.hyperledger.identus.walletsdk.domain.models.AttachmentDescriptor

data class ConnectionlessMessageData(
val messageId: String,
val messageBody: String,
val attachmentDescriptor: AttachmentDescriptor,
val messageThid: String,
val messageFrom: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,6 @@ constructor(
val name: String,
val value: String,
@SerialName("media_type")
val mediaType: String?
val mediaType: String? = null
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand

import kotlinx.serialization.Serializable
import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.OfferCredential
import java.util.*

/**
* Represents a connectionless credential offer.
*/
@Serializable
data class ConnectionlessCredentialOffer(
val offerCredential: OfferCredential
) : InvitationType()
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand

import kotlinx.serialization.Serializable
import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.RequestPresentation

@Serializable
data class ConnectionlessRequestPresentation(
val requestPresentation: RequestPresentation
) : InvitationType()
Loading

0 comments on commit 9b4c0b9

Please sign in to comment.