Skip to content

Commit

Permalink
feat(Agent): Logic to parse out of band invitations (#25)
Browse files Browse the repository at this point in the history
* [ATL-2998] Parse out of band invitation functionallity
  • Loading branch information
cristianIOHK authored Feb 8, 2023
1 parent 7cc738f commit 85535c5
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import io.iohk.atala.prism.domain.models.Seed
import io.iohk.atala.prism.domain.models.Signature
import io.iohk.atala.prism.walletsdk.prismagent.helpers.ApiImpl
import io.iohk.atala.prism.walletsdk.prismagent.helpers.HttpClient
import io.iohk.atala.prism.walletsdk.prismagent.protocols.prismOnboarding.PrismOnboardingInvitation
import io.iohk.atala.prism.walletsdk.prismagent.models.InvitationType
import io.iohk.atala.prism.walletsdk.prismagent.models.OutOfBandInvitation
import io.iohk.atala.prism.walletsdk.prismagent.models.PrismOnboardingInvitation
import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType
import io.iohk.atala.prism.walletsdk.prismagent.protocols.findProtocolTypeByValue
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.HttpMethod
import io.ktor.http.Url
Expand All @@ -21,17 +25,15 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject

final class PrismAgent {
enum class State {
STOPED, STARTING, RUNNING, STOPING
}

sealed class InvitationType

data class PrismOnboardingInvitation(val from: String, val endpoint: String, val ownDID: DID) : InvitationType()

val seed: Seed
var state = State.STOPED

Expand Down Expand Up @@ -112,12 +114,20 @@ final class PrismAgent {

@Throws(PrismAgentError.unknownInvitationTypeError::class)
suspend fun parseInvitation(str: String): InvitationType {
val invite = try {
parsePrismInvitation(str)
} catch (e: Throwable) {
println(e)
throw PrismAgentError.unknownInvitationTypeError()
val json = Json.decodeFromString<JsonObject>(str)
val typeString: String = if (json.containsKey("type")) {
json["type"].toString().trim('"')
} else {
""
}

val invite: InvitationType = when (findProtocolTypeByValue(typeString)) {
ProtocolType.PrismOnboarding -> parsePrismInvitation(str)
ProtocolType.Didcomminvitation -> parseOOBInvitation(str)
else ->
throw PrismAgentError.unknownInvitationTypeError()
}

return invite
}

Expand All @@ -127,10 +137,10 @@ final class PrismAgent {

var response = api.request(
HttpMethod.Post,
Url(invitation.endpoint),
Url(invitation.onboardEndpoint),
mapOf(),
mapOf(),
SendDID(invitation.ownDID.toString())
SendDID(invitation.from.toString())
)

if (response.status.value != 200) {
Expand All @@ -147,27 +157,35 @@ final class PrismAgent {
}

private suspend fun parsePrismInvitation(str: String): PrismOnboardingInvitation {
val prismOnboarding = PrismOnboardingInvitation(str)
val url = prismOnboarding.body.onboardEndpoint
val did = createNewPeerDID(
arrayOf(
DIDDocument.Service(
id = "#didcomm-1",
type = arrayOf("DIDCommMessaging"),
serviceEndpoint = DIDDocument.ServiceEndpoint(
uri = url,
accept = arrayOf("DIDCommMessaging"),
routingKeys = arrayOf()
try {
val prismOnboarding = PrismOnboardingInvitation.prismOnboardingInvitationFromJsonString(str)
val url = prismOnboarding.onboardEndpoint
val did = createNewPeerDID(
arrayOf(
DIDDocument.Service(
id = "#didcomm-1",
type = arrayOf("DIDCommMessaging"),
serviceEndpoint = DIDDocument.ServiceEndpoint(
uri = url,
accept = arrayOf("DIDCommMessaging"),
routingKeys = arrayOf()
)
)
)
),
true
)
),
true
)
prismOnboarding.from = did
return prismOnboarding
} catch (e: Exception) {
throw PrismAgentError.unknownInvitationTypeError()
}
}

return PrismOnboardingInvitation(
from = prismOnboarding.body.from,
endpoint = url,
ownDID = did
)
private fun parseOOBInvitation(str: String): OutOfBandInvitation {
try {
return Json.decodeFromString(str)
} catch (e: Exception) {
throw PrismAgentError.unknownInvitationTypeError()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.iohk.atala.prism.walletsdk.prismagent.models

import kotlinx.serialization.Serializable

@Serializable
sealed class InvitationType()
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.iohk.atala.prism.walletsdk.prismagent.models

import io.iohk.atala.prism.apollo.uuid.UUID
import io.iohk.atala.prism.domain.models.DID
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

@Serializable
data class OutOfBandInvitation(
val id: String = UUID.randomUUID4().toString(),
val body: Body,
@SerialName("from")
private val fromString: String,
@Transient
var from: DID = DID(fromString),
val type: String
) : InvitationType() {

@Serializable
data class Body(
@SerialName("goal_code")
val goalCode: String? = null,
val goal: String? = null,
val accept: Array<String>? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Body

if (goalCode != other.goalCode) return false
if (goal != other.goal) return false
if (accept != null) {
if (other.accept == null) return false
if (!accept.contentEquals(other.accept)) return false
} else if (other.accept != null) return false

return true
}

override fun hashCode(): Int {
var result = goalCode?.hashCode() ?: 0
result = 31 * result + (goal?.hashCode() ?: 0)
result = 31 * result + (accept?.contentHashCode() ?: 0)
return result
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.iohk.atala.prism.walletsdk.prismagent.models

import io.iohk.atala.prism.domain.models.DID
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

@Serializable
data class PrismOnboardingInvitation(
val onboardEndpoint: String,
@SerialName("from")
private val fromString: String,
@Transient
var from: DID? = null,
val type: String
) : InvitationType() {

init {
from = DID(fromString)
}

companion object {
fun prismOnboardingInvitationFromJsonString(string: String): PrismOnboardingInvitation {
return Json.decodeFromString(string)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.iohk.atala.prism.walletsdk.prismagent.protocols

import io.iohk.atala.prism.domain.models.PrismAgentError

enum class ProtocolType(val value: String) {
DidcommMediationRequest("https://didcomm.org/coordinate-mediation/2.0/mediate-request"),
DidcommMediationGrant("https://didcomm.org/coordinate-mediation/2.0/mediate-grant"),
Expand All @@ -22,3 +24,14 @@ enum class ProtocolType(val value: String) {
PickupStatus("https://didcomm.org/messagepickup/3.0/status"),
PickupReceived("https://didcomm.org/messagepickup/3.0/messages-received")
}

fun findProtocolTypeByValue(string: String): ProtocolType {
val it = ProtocolType.values().iterator()
while (it.hasNext()) {
val internalType = it.next()
if (internalType.value == string) {
return internalType
}
}
throw PrismAgentError.unknownInvitationTypeError()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import io.iohk.atala.prism.domain.models.PrismAgentError
import io.iohk.atala.prism.domain.models.PrivateKey
import io.iohk.atala.prism.domain.models.Seed
import io.iohk.atala.prism.domain.models.Signature
import io.iohk.atala.prism.walletsdk.prismagent.models.OutOfBandInvitation
import io.iohk.atala.prism.walletsdk.prismagent.models.PrismOnboardingInvitation
import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType
import io.ktor.http.HttpStatusCode
import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.flow.flow
Expand Down Expand Up @@ -73,13 +76,14 @@ class PrismAgentTests {
)
var invitationString = """
{
"type":"Onboarding",
"type":"${ProtocolType.PrismOnboarding.value}",
"onboardEndpoint":"http://localhost/onboarding",
"from":"did:prism:b6c0c33d701ac1b9a262a14454d1bbde3d127d697a76950963c5fd930605:Cj8KPRI7CgdtYXN0ZXIwEAFKLgoJc2VmsxEiECSTjyV7sUfCr_ArpN9rvCwR9fRMAhcsr_S7ZRiJk4p5k"
}
"""
val invitation = agent.parseInvitation(invitationString)
agent.acceptInvitation(invitation as PrismAgent.PrismOnboardingInvitation)
assertEquals(PrismOnboardingInvitation::class, invitation::class)
agent.acceptInvitation(invitation as PrismOnboardingInvitation)
}

@Test
Expand All @@ -92,14 +96,14 @@ class PrismAgentTests {
)
var invitationString = """
{
"type":"Onboarding",
"type":"${ProtocolType.PrismOnboarding.value}",
"onboardEndpoint":"http://localhost/onboarding",
"from":"did:prism:b6c0c33d701ac1b9a262a14454d1bbde3d127d697a76950963c5fd930605:Cj8KPRI7CgdtYXN0ZXIwEAFKLgoJc2VmsxEiECSTjyV7sUfCr_ArpN9rvCwR9fRMAhcsr_S7ZRiJk4p5k"
}
"""
val invitation = agent.parseInvitation(invitationString)
assertFailsWith<PrismAgentError.failedToOnboardError> {
agent.acceptInvitation(invitation as PrismAgent.PrismOnboardingInvitation)
agent.acceptInvitation(invitation as PrismOnboardingInvitation)
}
}

Expand All @@ -113,7 +117,7 @@ class PrismAgentTests {
)
var invitationString = """
{
"type":"Onboarding",
"type":"${ProtocolType.PrismOnboarding.value}",
"errorField":"http://localhost/onboarding",
"from":"did:prism:b6c0c33d701ac1b9a262a14454d1bbde3d127d697a76950963c5fd930605:Cj8KPRI7CgdtYXN0ZXIwEAFKLgoJc2VmsxEiECSTjyV7sUfCr_ArpN9rvCwR9fRMAhcsr_S7ZRiJk4p5k"
}
Expand Down Expand Up @@ -162,4 +166,65 @@ class PrismAgentTests {
assertEquals(Signature::class, agent.signWith(did, messageString.toByteArray())::class)
assertTrue { plutoMock.wasGetDIDPrivateKeysByDIDCalled }
}

@Test
fun testParseInvitation_whenOutOfBand_thenReturnsOutOfBandInvitationObject() = runTest {
val agent = PrismAgent(
apolloMock,
castorMock,
plutoMock
)

val invitationString = """
{
"type": "https://didcomm.org/out-of-band/2.0/invitation",
"id": "1234-1234-1234-1234",
"from": "did:peer:asdf42sf",
"body": {
"goal_code": "issue-vc",
"goal": "To issue a Faber College Graduate credential",
"accept": [
"didcomm/v2",
"didcomm/aip2;env=rfc587"
]
}
}
"""

val invitation = agent.parseInvitation(invitationString.trim())
assertEquals(OutOfBandInvitation::class, invitation::class)
val oobInvitation: OutOfBandInvitation = invitation as OutOfBandInvitation
assertEquals("https://didcomm.org/out-of-band/2.0/invitation", oobInvitation.type)
assertEquals(DID("did:peer:asdf42sf"), oobInvitation.from)
assertEquals(
OutOfBandInvitation.Body(
"issue-vc",
"To issue a Faber College Graduate credential",
arrayOf("didcomm/v2", "didcomm/aip2;env=rfc587")
),
oobInvitation.body
)
}

@Test
fun testParseInvitation_whenOutOfBandWrongBody_thenThrowsUnknownInvitationTypeError() = runTest {
val agent = PrismAgent(
apolloMock,
castorMock,
plutoMock
)

val invitationString = """
{
"type": "https://didcomm.org/out-of-band/2.0/invitation",
"id": "1234-1234-1234-1234",
"from": "did:peer:asdf42sf",
"wrongBody": {}
}
"""

assertFailsWith<PrismAgentError.unknownInvitationTypeError> {
agent.parseInvitation(invitationString.trim())
}
}
}

0 comments on commit 85535c5

Please sign in to comment.