Skip to content

Commit

Permalink
feat: contactless presentation request (#192)
Browse files Browse the repository at this point in the history
Signed-off-by: Cristian G <[email protected]>
  • Loading branch information
cristianIOHK authored Aug 26, 2024
1 parent 0437927 commit e03ebbc
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ constructor(
/**
* Represents an error that occurs when a field is null but should not be.
*/
class NonNullableError(val field: String) : PolluxError("Field $field are non nullable.") {
class NonNullableError(val field: String) : PolluxError("Field $field is non nullable.") {
override val code: Int
get() = 516
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -321,6 +322,10 @@ object AttachmentDataSerializer : KSerializer<AttachmentData> {
jsonSerializable.decodeFromJsonElement(AttachmentData.AttachmentJsonData.serializer(), json)
}

json.containsKey("json") -> {
AttachmentData.AttachmentJsonData(data = Json.encodeToString(json["json"]!!.jsonObject))
}

else -> throw SerializationException("Unknown AttachmentData type")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.http.Url
import io.ktor.serialization.kotlinx.json.json
import io.ktor.util.date.getTimeMillis
import java.net.UnknownHostException
import java.security.SecureRandom
import java.util.*
Expand All @@ -34,12 +35,15 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.hyperledger.identus.apollo.base64.base64UrlEncoded
import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519KeyPair
import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519PrivateKey
Expand Down Expand Up @@ -902,14 +906,91 @@ open class EdgeAgent {
}

/**
* Accepts an Out-of-Band (DIDComm) invitation and establishes a new connection
* Accepts an Out-of-Band (DIDComm), verifies if it contains attachments or not. If it does contain attachments
* means is a contactless request presentation. If it does not, it just creates a pair and establishes a connection.
* @param invitation The Out-of-Band invitation to accept
* @throws [EdgeAgentError.NoMediatorAvailableError] if there is no mediator available or other errors occur during the acceptance process
*/
suspend fun acceptOutOfBandInvitation(invitation: OutOfBandInvitation) {
val ownDID = createNewPeerDID(updateMediator = true)
val pair = DIDCommConnectionRunner(invitation, pluto, ownDID, connectionManager).run()
connectionManager.addConnection(pair)
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)
}
}

/**
Expand Down Expand Up @@ -1060,8 +1141,10 @@ open class EdgeAgent {
data = AttachmentBase64(presentationString.base64UrlEncoded)
)

val fromDID = request.to ?: createNewPeerDID(updateMediator = true)

return Presentation(
from = request.to,
from = fromDID,
to = request.from,
thid = request.thid,
body = Presentation.Body(request.body.goalCode, request.body.comment),
Expand Down Expand Up @@ -1172,11 +1255,13 @@ open class EdgeAgent {
format = CredentialType.PRESENTATION_EXCHANGE_SUBMISSION.type,
data = AttachmentBase64(presentationSubmissionProof.base64UrlEncoded)
)

val fromDID = requestPresentation.to ?: createNewPeerDID(updateMediator = true)
return Presentation(
body = Presentation.Body(),
attachments = arrayOf(attachmentDescriptor),
thid = requestPresentation.thid ?: requestPresentation.id,
from = requestPresentation.to,
from = fromDID,
to = requestPresentation.from
)
} else {
Expand All @@ -1192,11 +1277,12 @@ open class EdgeAgent {
format = CredentialType.PRESENTATION_EXCHANGE_SUBMISSION.type,
data = AttachmentBase64(presentationSubmissionProof.base64UrlEncoded)
)
val fromDID = requestPresentation.to ?: createNewPeerDID(updateMediator = true)
return Presentation(
body = Presentation.Body(),
attachments = arrayOf(attachmentDescriptor),
thid = requestPresentation.thid ?: requestPresentation.id,
from = requestPresentation.to,
from = fromDID,
to = requestPresentation.from
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,13 @@ sealed class EdgeAgentError : KnownPrismError() {
override val message: String
get() = "This credential does not fulfill the criteria required by the request."
}

class ExpiredInvitation() :
EdgeAgentError() {
override val code: Int
get() = 615

override val message: String
get() = "This invitation has expired."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.hyperledger.identus.walletsdk.domain.models.AttachmentDescriptor
import org.hyperledger.identus.walletsdk.edgeagent.GOAL_CODE
import org.hyperledger.identus.walletsdk.edgeagent.protocols.ProtocolType
import java.util.UUID
Expand All @@ -23,7 +24,12 @@ constructor(
@EncodeDefault
val type: ProtocolType = ProtocolType.Didcomminvitation,
@EncodeDefault
val typ: String? = null
val typ: String? = null,
val attachments: Array<AttachmentDescriptor> = arrayOf(),
@SerialName("created_time")
val createdTime: Long = 0,
@SerialName("expires_time")
val expiresTime: Long = 0
) : InvitationType() {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,28 +182,6 @@ class Presentation {
)
}

/**
* Converts a message into a Presentation object.
*
* @param msg The input message to convert.
* @return The converted Presentation object.
* @throws EdgeAgentError.InvalidMessageType If the message type is invalid.
*/
@Throws(EdgeAgentError.InvalidMessageType::class)
fun makePresentationFromRequest(msg: Message): Presentation {
val requestPresentation = RequestPresentation.fromMessage(msg)
return Presentation(
body = Body(
goalCode = requestPresentation.body.goalCode,
comment = requestPresentation.body.comment
),
attachments = requestPresentation.attachments,
thid = requestPresentation.id,
from = requestPresentation.to,
to = requestPresentation.from
)
}

/**
* Compares this Presentation object with the specified object for equality.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,6 @@ class ProposePresentation {
)
}

/**
* Creates a proposal presentation from a request message.
*
* @param msg The request message.
* @return The created `ProposePresentation` object.
* @throws EdgeAgentError.InvalidMessageType If the message type does not represent the expected protocol.
*/
@Throws(EdgeAgentError.InvalidMessageType::class)
fun makeProposalFromRequest(msg: Message): ProposePresentation {
val request = RequestPresentation.fromMessage(msg)

return ProposePresentation(
body = Body(
goalCode = request.body.goalCode,
comment = request.body.comment,
proofTypes = request.body.proofTypes
),
attachments = request.attachments,
thid = msg.id,
from = request.to,
to = request.from
)
}

/**
* Compares this object with the specified object for equality.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ data class RequestPresentation(
val attachments: Array<AttachmentDescriptor>,
val thid: String? = null,
val from: DID,
val to: DID,
val to: DID? = null,
val direction: Message.Direction = Message.Direction.RECEIVED
) {

Expand Down
Loading

0 comments on commit e03ebbc

Please sign in to comment.