From 9971eba4f91c4969b1bcddeb22ea2671dc1b9c1d Mon Sep 17 00:00:00 2001 From: Cristian G Date: Mon, 15 Apr 2024 12:07:34 -0400 Subject: [PATCH 1/4] feat: revocation credentials --- .../walletsdk/prismagent/AnoncredsTests.kt | 2 +- .../walletsdk/domain/buildingblocks/Pluto.kt | 2 + .../atala/prism/walletsdk/pluto/PlutoImpl.kt | 4 + .../walletsdk/prismagent/ConnectionManager.kt | 108 +++++++++++++----- .../prism/walletsdk/prismagent/PrismAgent.kt | 4 +- .../prismagent/protocols/ProtocolType.kt | 1 + .../revocation/RevocationNotification.kt | 57 +++++++++ .../pluto/data/StorableCredential.sq | 5 + .../prism/walletsdk/mercury/PlutoMock.kt | 4 + .../prismagent/ConnectionManagerTest.kt | 68 +++++++++++ .../prism/walletsdk/prismagent/PlutoMock.kt | 4 + .../walletsdk/prismagent/PrismAgentTests.kt | 2 +- sampleapp/src/main/res/values/strings.xml | 2 +- 13 files changed, 230 insertions(+), 33 deletions(-) create mode 100644 atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt diff --git a/atala-prism-sdk/src/androidInstrumentedTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/AnoncredsTests.kt b/atala-prism-sdk/src/androidInstrumentedTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/AnoncredsTests.kt index 5cfb1fb7f..aa26904a0 100644 --- a/atala-prism-sdk/src/androidInstrumentedTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/AnoncredsTests.kt +++ b/atala-prism-sdk/src/androidInstrumentedTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/AnoncredsTests.kt @@ -50,7 +50,7 @@ class AnoncredsTests { polluxMock = PolluxMock() mediationHandlerMock = MediationHandlerMock() // Pairing will be removed in the future - connectionManager = ConnectionManager(mercuryMock, castorMock, plutoMock, mediationHandlerMock, mutableListOf()) + connectionManager = ConnectionManager(mercuryMock, castorMock, plutoMock, mediationHandlerMock, mutableListOf(), polluxMock) json = Json { ignoreUnknownKeys = true prettyPrint = true diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt index 0f4688e36..9b2a07809 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt @@ -327,4 +327,6 @@ interface Pluto { * or null if no metadata is found. */ fun getCredentialMetadata(linkSecretName: String): Flow + + fun revokeCredential(credentialId: String) } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt index 087eb34d7..506e10034 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt @@ -1020,4 +1020,8 @@ class PlutoImpl(private val connection: DbConnection) : Pluto { ) } } + + override fun revokeCredential(credentialId: String) { + getInstance().storableCredentialQueries.revokeCredentialById(credentialId) + } } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt index 88d60befd..ce9aa22bc 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt @@ -2,15 +2,23 @@ package io.iohk.atala.prism.walletsdk.prismagent +import io.iohk.atala.prism.apollo.base64.base64UrlDecoded 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.buildingblocks.Pollux +import io.iohk.atala.prism.walletsdk.domain.models.AttachmentBase64 +import io.iohk.atala.prism.walletsdk.domain.models.CredentialType import io.iohk.atala.prism.walletsdk.domain.models.DID import io.iohk.atala.prism.walletsdk.domain.models.DIDPair import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.pollux.models.JWTCredential import io.iohk.atala.prism.walletsdk.prismagent.connectionsmanager.ConnectionsManager import io.iohk.atala.prism.walletsdk.prismagent.connectionsmanager.DIDCommConnection import io.iohk.atala.prism.walletsdk.prismagent.mediation.MediationHandler +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType +import io.iohk.atala.prism.walletsdk.prismagent.protocols.issueCredential.IssueCredential +import io.iohk.atala.prism.walletsdk.prismagent.protocols.revocation.RevocationNotification import java.time.Duration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,6 +44,7 @@ class ConnectionManager( private val pluto: Pluto, internal val mediationHandler: MediationHandler, private var pairings: MutableList, + private val pollux: Pollux, private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) ) : ConnectionsManager, DIDCommConnection { @@ -73,22 +82,23 @@ class ConnectionManager( mediationHandler.listenUnreadMessages( serviceEndpointUrl ) { arrayMessages -> - // Process the received messages - val messagesIds = mutableListOf() - val messages = mutableListOf() - arrayMessages.map { pair -> - messagesIds.add(pair.first) - messages.add(pair.second) - } - // If there are any messages, mark them as read and store them - scope.launch { - if (messagesIds.isNotEmpty()) { - mediationHandler.registerMessagesAsRead( - messagesIds.toTypedArray() - ) - pluto.storeMessages(messages) - } - } + processMessages(arrayMessages) +// // Process the received messages +// val messagesIds = mutableListOf() +// val messages = mutableListOf() +// arrayMessages.map { pair -> +// messagesIds.add(pair.first) +// messages.add(pair.second) +// } +// // If there are any messages, mark them as read and store them +// scope.launch { +// if (messagesIds.isNotEmpty()) { +// mediationHandler.registerMessagesAsRead( +// messagesIds.toTypedArray() +// ) +// pluto.storeMessages(messages) +// } +// } } } @@ -97,18 +107,19 @@ class ConnectionManager( while (true) { // Continuously await and process new messages awaitMessages().collect { array -> - val messagesIds = mutableListOf() - val messages = mutableListOf() - array.map { pair -> - messagesIds.add(pair.first) - messages.add(pair.second) - } - if (messagesIds.isNotEmpty()) { - mediationHandler.registerMessagesAsRead( - messagesIds.toTypedArray() - ) - pluto.storeMessages(messages) - } + processMessages(array) +// val messagesIds = mutableListOf() +// val messages = mutableListOf() +// array.map { pair -> +// messagesIds.add(pair.first) +// messages.add(pair.second) +// } +// if (messagesIds.isNotEmpty()) { +// mediationHandler.registerMessagesAsRead( +// messagesIds.toTypedArray() +// ) +// pluto.storeMessages(messages) +// } } // Wait for the specified request interval before fetching new messages delay(Duration.ofSeconds(requestInterval.toLong()).toMillis()) @@ -198,6 +209,47 @@ class ConnectionManager( return null } + internal fun processMessages(arrayMessages: Array>) { + scope.launch { + val messagesIds = mutableListOf() + val messages = mutableListOf() + arrayMessages.map { pair -> + messagesIds.add(pair.first) + messages.add(pair.second) + } + + val allMessages = pluto.getAllMessages().first() + + val revokedMessages = messages.filter { it.piuri == ProtocolType.PrismRevocation.value } + revokedMessages.forEach { msg -> + val revokedMessage = RevocationNotification.fromMessage(msg) + val threadId = revokedMessage.body.threadId + val matchingMessages = + allMessages.filter { it.piuri == ProtocolType.DidcommIssueCredential.value && it.thid == threadId } + if (matchingMessages.isNotEmpty()) { + matchingMessages.forEach { message -> + val issueMessage = IssueCredential.fromMessage(message) + if (pollux.extractCredentialFormatFromMessage(issueMessage.attachments) == CredentialType.JWT) { + val attachment = issueMessage.attachments.firstOrNull()?.data as? AttachmentBase64 + attachment?.let { + val credentialId = it.base64.base64UrlDecoded + pluto.revokeCredential(credentialId) + } + } + } + } + } + + // If there are any messages, mark them as read and store them + if (messagesIds.isNotEmpty()) { + mediationHandler.registerMessagesAsRead( + messagesIds.toTypedArray() + ) + pluto.storeMessages(messages) + } + } + } + /** * Awaits a response to a specified message ID from the connection. * diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt index bd3427f50..50ae8df4a 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt @@ -222,7 +222,7 @@ class PrismAgent { this.logger = logger // Pairing will be removed in the future this.connectionManager = - ConnectionManager(mercury, castor, pluto, mediatorHandler, mutableListOf()) + ConnectionManager(mercury, castor, pluto, mediatorHandler, mutableListOf(), pollux) } init { @@ -455,7 +455,7 @@ class PrismAgent { fun setupMediatorHandler(mediatorHandler: MediationHandler) { stop() this.connectionManager = - ConnectionManager(mercury, castor, pluto, mediatorHandler, mutableListOf()) + ConnectionManager(mercury, castor, pluto, mediatorHandler, mutableListOf(), pollux) } /** diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt index 389c20502..52f03aa11 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt @@ -35,6 +35,7 @@ enum class ProtocolType(val value: String) { PickupStatus("https://didcomm.org/messagepickup/3.0/status"), PickupReceived("https://didcomm.org/messagepickup/3.0/messages-received"), LiveDeliveryChange("https://didcomm.org/messagepickup/3.0/live-delivery-change"), + PrismRevocation("https://atalaprism.io/revocation_notification/1.0/revoke"), None(""); companion object { diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt new file mode 100644 index 000000000..6780c1aa0 --- /dev/null +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt @@ -0,0 +1,57 @@ +package io.iohk.atala.prism.walletsdk.prismagent.protocols.revocation + +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.prismagent.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType +import kotlinx.serialization.SerialName +import java.util.UUID +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class RevocationNotification( + val id: String = UUID.randomUUID().toString(), + val body: Body, + val from: DID, + val to: DID +) { + val type = ProtocolType.PrismRevocation + + fun makeMessage(): Message { + return Message( + id = id, + piuri = type.value, + from = from, + to = to, + body = Json.encodeToString(body) + ) + } + + @Serializable + data class Body @JvmOverloads constructor( + @SerialName("issueCredentialProtocolThreadId") + val threadId: String, + val comment: String? + ) + + companion object { + fun fromMessage(message: Message): RevocationNotification { + require( + message.piuri == ProtocolType.PrismRevocation.value && + message.from != null && + message.to != null + ) { + throw PrismAgentError.InvalidMessageType( + type = message.piuri, + shouldBe = ProtocolType.PrismRevocation.value + ) + } + return RevocationNotification( + body = Json.decodeFromString(message.body), + from = message.from, + to = message.to + ) + } + } +} \ No newline at end of file diff --git a/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq b/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq index 73ccb8817..fe8e00766 100644 --- a/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq +++ b/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq @@ -23,3 +23,8 @@ SELECT StorableCredential.*, AvailableClaims.claim AS claims FROM StorableCredential LEFT JOIN AvailableClaims ON StorableCredential.id = AvailableClaims.credentialId GROUP BY StorableCredential.id; + +revokeCredentialById: +UPDATE StorableCredential +SET revoked = 1 +WHERE id = :id; \ No newline at end of file diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt index 7c2d09d30..3d97152ba 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt @@ -180,4 +180,8 @@ class PlutoMock : Pluto { override fun getCredentialMetadata(linkSecretName: String): Flow { TODO("Not yet implemented") } + + override fun revokeCredential(credentialId: String) { + TODO("Not yet implemented") + } } diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt index bd683e94c..82b3d4f35 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt @@ -2,15 +2,21 @@ package io.iohk.atala.prism.walletsdk.prismagent +import io.iohk.atala.prism.apollo.base64.base64UrlEncoded 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.buildingblocks.Pollux +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.CredentialType import io.iohk.atala.prism.walletsdk.domain.models.Curve 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.DIDUrl import io.iohk.atala.prism.walletsdk.domain.models.Message import io.iohk.atala.prism.walletsdk.prismagent.mediation.MediationHandler +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.TestCoroutineDispatcher @@ -22,9 +28,16 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import java.util.UUID +import kotlinx.coroutines.flow.Flow +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.anyList +import org.mockito.kotlin.anyArray +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock import kotlin.test.assertNotNull import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals class ConnectionManagerTest { @@ -37,6 +50,9 @@ class ConnectionManagerTest { @Mock lateinit var plutoMock: Pluto + @Mock + lateinit var polluxMock: Pollux + @Mock lateinit var basicMediatorHandlerMock: MediationHandler @@ -53,6 +69,7 @@ class ConnectionManagerTest { pluto = plutoMock, mediationHandler = basicMediatorHandlerMock, pairings = mutableListOf(), + pollux = polluxMock, scope = CoroutineScope(testDispatcher) ) } @@ -168,4 +185,55 @@ class ConnectionManagerTest { verify(basicMediatorHandlerMock).pickupUnreadMessages(10) verify(basicMediatorHandlerMock).registerMessagesAsRead(arrayOf("1234")) } + + @Test + fun testConnectionManager_whenProcessMessageRevoke_thenAllCorrect() = runTest { + val threadId = UUID.randomUUID().toString() + val attachments: Array = + arrayOf( + AttachmentDescriptor( + mediaType = "application/json", + format = CredentialType.JWT.type, + data = AttachmentBase64(base64 = "asdfasdfasdfasdfasdfasdfasdfasdfasdf".base64UrlEncoded) + ) + ) + val listMessages = listOf( + Message( + piuri = ProtocolType.DidcommconnectionRequest.value, + body = "" + ), + Message( + piuri = ProtocolType.DidcommIssueCredential.value, + thid = threadId, + from = DID("did:peer:asdf897a6sdf"), + to = DID("did:peer:f706sg678ha"), + attachments = attachments, + body = """{}""" + ) + ) + val messageList: Flow> = flow { + emit(listMessages) + } + `when`(plutoMock.getAllMessages()).thenReturn(messageList) + `when`(polluxMock.extractCredentialFormatFromMessage(any())).thenReturn(CredentialType.JWT) + + val messages = arrayOf( + Pair( + threadId, Message( + piuri = ProtocolType.PrismRevocation.value, + from = DID("did:peer:0978aszdf7890asg"), + to = DID("did:peer:asdf9068asdf"), + body = """{"threadId":"$threadId","comment":null}""" + ) + ) + ) + + + connectionManager.processMessages(messages) + val argumentCaptor = argumentCaptor() + verify(plutoMock).revokeCredential(argumentCaptor.capture()) + assertEquals("asdfasdfasdfasdfasdfasdfasdfasdfasdf", argumentCaptor.firstValue) + verify(basicMediatorHandlerMock).registerMessagesAsRead(anyArray()) + verify(plutoMock).storeMessages(anyList()) + } } diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt index 2f8468de3..d1ea3704a 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt @@ -238,4 +238,8 @@ class PlutoMock : Pluto { override fun getCredentialMetadata(linkSecretName: String): Flow { return getCredentialMetadataReturn } + + override fun revokeCredential(credentialId: String) { + TODO("Not yet implemented") + } } diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt index 0af60048a..43768c8ec 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt @@ -61,7 +61,7 @@ class PrismAgentTests { polluxMock = PolluxMock() mediationHandlerMock = MediationHandlerMock() // Pairing will be removed in the future - connectionManager = ConnectionManager(mercuryMock, castorMock, plutoMock, mediationHandlerMock, mutableListOf()) + connectionManager = ConnectionManager(mercuryMock, castorMock, plutoMock, mediationHandlerMock, mutableListOf(), polluxMock) json = Json { ignoreUnknownKeys = true prettyPrint = true diff --git a/sampleapp/src/main/res/values/strings.xml b/sampleapp/src/main/res/values/strings.xml index 86a5f4ab2..dea961b43 100644 --- a/sampleapp/src/main/res/values/strings.xml +++ b/sampleapp/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ Mediator DID: Agent status: - did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjoiaHR0cDovLzE5Mi4xNjguNjguMTAzOjgwODAiLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19 + did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vc2l0LXByaXNtLW1lZGlhdG9yLmF0YWxhcHJpc20uaW8iLCJhIjpbImRpZGNvbW0vdjIiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3NpdC1wcmlzbS1tZWRpYXRvci5hdGFsYXByaXNtLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIl19fQ Credentials Host: From 10394f215210c0a9a3add9eb5fc986fc4c061908 Mon Sep 17 00:00:00 2001 From: Cristian G Date: Tue, 16 Apr 2024 12:02:54 -0400 Subject: [PATCH 2/4] feat: revocation notification --- .../walletsdk/domain/buildingblocks/Pollux.kt | 2 +- .../walletsdk/domain/models/Credential.kt | 1 + .../domain/models/StorableCredential.kt | 1 - .../walletsdk/pluto/CredentialRecovery.kt | 2 +- .../atala/prism/walletsdk/pluto/PlutoImpl.kt | 9 +++-- .../prism/walletsdk/pollux/PolluxImpl.kt | 20 +++++++---- .../walletsdk/pollux/models/AnonCredential.kt | 5 +-- .../walletsdk/pollux/models/JWTCredential.kt | 5 +-- .../walletsdk/pollux/models/W3CCredential.kt | 5 +-- .../walletsdk/prismagent/ConnectionManager.kt | 6 ++-- .../prism/walletsdk/prismagent/PrismAgent.kt | 2 +- .../revocation/RevocationNotification.kt | 8 +++-- .../prismagent/ConnectionManagerTest.kt | 34 ++++++++++++++++--- .../prism/walletsdk/prismagent/PolluxMock.kt | 2 +- .../ui/credentials/CredentialsAdapter.kt | 4 +++ .../res/layout/placeholder_credential.xml | 11 +++++- 16 files changed, 85 insertions(+), 32 deletions(-) diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pollux.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pollux.kt index d0c636fa4..e36ce789d 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pollux.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pollux.kt @@ -97,7 +97,7 @@ interface Pollux { * @param credentialData The byte array containing the credential data. * @return The restored credential. */ - fun restoreCredential(restorationIdentifier: String, credentialData: ByteArray): Credential + fun restoreCredential(restorationIdentifier: String, credentialData: ByteArray, revoked: Boolean): Credential /** * Converts a [Credential] object to a [StorableCredential] object of the specified [CredentialType]. diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/Credential.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/Credential.kt index f833789d8..7667da3d7 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/Credential.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/Credential.kt @@ -11,4 +11,5 @@ interface Credential { val subject: String? val claims: Array val properties: Map + var revoked: Boolean? } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/StorableCredential.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/StorableCredential.kt index 9b3a05185..e2035c3bd 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/StorableCredential.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/models/StorableCredential.kt @@ -10,7 +10,6 @@ interface StorableCredential : Credential { val credentialUpdated: String? val credentialSchema: String? val validUntil: String? - val revoked: Boolean? val availableClaims: Array /** diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/CredentialRecovery.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/CredentialRecovery.kt index 16f8d7ec7..6dcfea3a7 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/CredentialRecovery.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/CredentialRecovery.kt @@ -6,4 +6,4 @@ package io.iohk.atala.prism.walletsdk.pluto * @property restorationId The restoration ID associated with the credential recovery. * @property credentialData The credential data as a byte array. */ -class CredentialRecovery(val restorationId: String, val credentialData: ByteArray) +class CredentialRecovery(val restorationId: String, val credentialData: ByteArray, val revoked: Boolean) diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt index 506e10034..56357e26c 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt @@ -57,7 +57,11 @@ class PlutoImpl(private val connection: DbConnection) : Pluto { PrismPlutoDb.Schema.version, AfterVersion(1) { it.execute(null, "ALTER TABLE CredentialMetadata DROB COLUMN nonce;", 0) - it.execute(null, "ALTER TABLE CredentialMetadata DROB COLUMN linkSecretBlindingData;", 0) + it.execute( + null, + "ALTER TABLE CredentialMetadata DROB COLUMN linkSecretBlindingData;", + 0 + ) it.execute(null, "ALTER TABLE CredentialMetadata ADD COLUMN json TEXT;", 0) } ) @@ -926,7 +930,8 @@ class PlutoImpl(private val connection: DbConnection) : Pluto { it.executeAsList().map { credential -> CredentialRecovery( restorationId = credential.recoveryId, - credentialData = credential.credentialData + credentialData = credential.credentialData, + revoked = credential.revoked != 0 ) } } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/PolluxImpl.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/PolluxImpl.kt index f1d622335..9c7cfc2c2 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/PolluxImpl.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/PolluxImpl.kt @@ -147,25 +147,29 @@ class PolluxImpl( */ override fun restoreCredential( restorationIdentifier: String, - credentialData: ByteArray + credentialData: ByteArray, + revoked: Boolean ): Credential { - return when (restorationIdentifier) { + val cred: Credential + when (restorationIdentifier) { "jwt+credential" -> { - JWTCredential(credentialData.decodeToString()) + cred = JWTCredential(credentialData.decodeToString()) } "anon+credential" -> { - AnonCredential.fromStorableData(credentialData) + cred = AnonCredential.fromStorableData(credentialData) } "w3c+credential" -> { - Json.decodeFromString(credentialData.decodeToString()) + cred = Json.decodeFromString(credentialData.decodeToString()) } else -> { throw PolluxError.InvalidCredentialError() } } + cred.revoked = revoked + return cred } /** @@ -258,8 +262,10 @@ class PolluxImpl( val presentationRequest = PresentationRequest(attachmentBase64.base64.base64UrlDecoded) val cred = anoncreds_wrapper.Credential(credential.id) - val requestedAttributes = presentationRequest.getRequestedAttributes().toListRequestedAttribute() - val requestedPredicate = presentationRequest.getRequestedPredicates().toListRequestedPredicate() + val requestedAttributes = + presentationRequest.getRequestedAttributes().toListRequestedAttribute() + val requestedPredicate = + presentationRequest.getRequestedPredicates().toListRequestedPredicate() val credentialRequests = CredentialRequests( credential = cred, diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/AnonCredential.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/AnonCredential.kt index d8ab731ff..5886ab112 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/AnonCredential.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/AnonCredential.kt @@ -78,6 +78,8 @@ data class AnonCredential( return properties.toMap() } + override var revoked: Boolean? = null + /** * Converts the current credential object into a storable credential object. * @@ -119,8 +121,7 @@ data class AnonCredential( override val validUntil: String? get() = null - override val revoked: Boolean? - get() = null + override var revoked: Boolean? = c.revoked override val availableClaims: Array get() = c.claims.map { it.key }.toTypedArray() diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/JWTCredential.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/JWTCredential.kt index 18b51af5e..5953f17e3 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/JWTCredential.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/JWTCredential.kt @@ -77,6 +77,8 @@ data class JWTCredential(val data: String) : Credential { return properties.toMap() } + override var revoked: Boolean? = null + /** * Converts the current instance of [JWTCredential] to a [StorableCredential]. * @@ -105,8 +107,7 @@ data class JWTCredential(val data: String) : Credential { get() = c.jwtPayload.verifiableCredential.credentialSchema?.type override val validUntil: String? get() = null - override val revoked: Boolean? - get() = null + override var revoked: Boolean? = c.revoked override val availableClaims: Array get() = c.claims.map { it.key }.toTypedArray() diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/W3CCredential.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/W3CCredential.kt index d0398c6d4..1defab055 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/W3CCredential.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pollux/models/W3CCredential.kt @@ -82,6 +82,8 @@ data class W3CCredential @JvmOverloads constructor( return properties.toMap() } + override var revoked: Boolean? = null + /** * Converts the current W3CCredential object to a StorableCredential object that can be stored and retrieved from a storage system. * @@ -102,8 +104,7 @@ data class W3CCredential @JvmOverloads constructor( get() = c.credentialSchema?.type override val validUntil: String? get() = null - override val revoked: Boolean? - get() = null + override var revoked: Boolean? = c.revoked override val availableClaims: Array get() = claims.map { it.key }.toTypedArray() diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt index ce9aa22bc..0ff6ba078 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt @@ -12,7 +12,6 @@ import io.iohk.atala.prism.walletsdk.domain.models.CredentialType import io.iohk.atala.prism.walletsdk.domain.models.DID import io.iohk.atala.prism.walletsdk.domain.models.DIDPair import io.iohk.atala.prism.walletsdk.domain.models.Message -import io.iohk.atala.prism.walletsdk.pollux.models.JWTCredential import io.iohk.atala.prism.walletsdk.prismagent.connectionsmanager.ConnectionsManager import io.iohk.atala.prism.walletsdk.prismagent.connectionsmanager.DIDCommConnection import io.iohk.atala.prism.walletsdk.prismagent.mediation.MediationHandler @@ -211,7 +210,7 @@ class ConnectionManager( internal fun processMessages(arrayMessages: Array>) { scope.launch { - val messagesIds = mutableListOf() + val messagesIds = mutableListOf() val messages = mutableListOf() arrayMessages.map { pair -> messagesIds.add(pair.first) @@ -230,7 +229,8 @@ class ConnectionManager( matchingMessages.forEach { message -> val issueMessage = IssueCredential.fromMessage(message) if (pollux.extractCredentialFormatFromMessage(issueMessage.attachments) == CredentialType.JWT) { - val attachment = issueMessage.attachments.firstOrNull()?.data as? AttachmentBase64 + val attachment = + issueMessage.attachments.firstOrNull()?.data as? AttachmentBase64 attachment?.let { val credentialId = it.base64.base64UrlDecoded pluto.revokeCredential(credentialId) diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt index 50ae8df4a..0f3d11e7a 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt @@ -881,7 +881,7 @@ class PrismAgent { return pluto.getAllCredentials() .map { list -> list.map { - pollux.restoreCredential(it.restorationId, it.credentialData) + pollux.restoreCredential(it.restorationId, it.credentialData, it.revoked) } } } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt index 6780c1aa0..6a7a0a402 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/revocation/RevocationNotification.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:import-ordering") + package io.iohk.atala.prism.walletsdk.prismagent.protocols.revocation import io.iohk.atala.prism.walletsdk.domain.models.DID @@ -39,8 +41,8 @@ class RevocationNotification( fun fromMessage(message: Message): RevocationNotification { require( message.piuri == ProtocolType.PrismRevocation.value && - message.from != null && - message.to != null + message.from != null && + message.to != null ) { throw PrismAgentError.InvalidMessageType( type = message.piuri, @@ -54,4 +56,4 @@ class RevocationNotification( ) } } -} \ No newline at end of file +} diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt index 82b3d4f35..e8e52b5d1 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt @@ -29,11 +29,9 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import java.util.UUID import kotlinx.coroutines.flow.Flow -import org.mockito.ArgumentCaptor import org.mockito.Mockito.anyList import org.mockito.kotlin.anyArray import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.mock import kotlin.test.assertNotNull import kotlin.test.BeforeTest import kotlin.test.Test @@ -179,6 +177,32 @@ class ConnectionManagerTest { ) } ) + val attachments: Array = + arrayOf( + AttachmentDescriptor( + mediaType = "application/json", + format = CredentialType.JWT.type, + data = AttachmentBase64(base64 = "asdfasdfasdfasdfasdfasdfasdfasdfasdf".base64UrlEncoded) + ) + ) + val listMessages = listOf( + Message( + piuri = ProtocolType.DidcommconnectionRequest.value, + body = "" + ), + Message( + piuri = ProtocolType.DidcommIssueCredential.value, + thid = UUID.randomUUID().toString(), + from = DID("did:peer:asdf897a6sdf"), + to = DID("did:peer:f706sg678ha"), + attachments = attachments, + body = """{}""" + ) + ) + val messageList: Flow> = flow { + emit(listMessages) + } + `when`(plutoMock.getAllMessages()).thenReturn(messageList) connectionManager.startFetchingMessages() assertNotNull(connectionManager.fetchingMessagesJob) @@ -219,16 +243,16 @@ class ConnectionManagerTest { val messages = arrayOf( Pair( - threadId, Message( + threadId, + Message( piuri = ProtocolType.PrismRevocation.value, from = DID("did:peer:0978aszdf7890asg"), to = DID("did:peer:asdf9068asdf"), - body = """{"threadId":"$threadId","comment":null}""" + body = """{"issueCredentialProtocolThreadId":"$threadId","comment":null}""" ) ) ) - connectionManager.processMessages(messages) val argumentCaptor = argumentCaptor() verify(plutoMock).revokeCredential(argumentCaptor.capture()) diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PolluxMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PolluxMock.kt index c981e3581..95ab39ab1 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PolluxMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PolluxMock.kt @@ -53,7 +53,7 @@ class PolluxMock : Pollux { TODO("Not yet implemented") } - override fun restoreCredential(restorationIdentifier: String, credentialData: ByteArray): Credential { + override fun restoreCredential(restorationIdentifier: String, credentialData: ByteArray, revoked: Boolean): Credential { TODO("Not yet implemented") } diff --git a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsAdapter.kt b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsAdapter.kt index c9e0bcc52..06623e9c1 100644 --- a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsAdapter.kt +++ b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsAdapter.kt @@ -62,6 +62,7 @@ class CredentialsAdapter(private var data: MutableList = mutableList private val type: TextView = itemView.findViewById(R.id.credential_id) private val issuanceDate: TextView = itemView.findViewById(R.id.credential_issuance_date) private val expDate: TextView = itemView.findViewById(R.id.credential_expiration_date) + private val revoked: TextView = itemView.findViewById(R.id.revoked) private val typeString: String = itemView.context.getString(R.string.credential_type) private val issuanceString: String = itemView.context.getString(R.string.credential_issuance) private val expirationString: String = itemView.context.getString(R.string.credential_expiration) @@ -70,6 +71,9 @@ class CredentialsAdapter(private var data: MutableList = mutableList when (cred::class) { JWTCredential::class -> { val jwt = cred as JWTCredential + if (jwt.revoked != null && jwt.revoked!!) { + revoked.visibility = View.VISIBLE + } type.text = String.format(typeString, "JWT") // TODO: Check what else to display jwt.jwtPayload.nbf?.let { diff --git a/sampleapp/src/main/res/layout/placeholder_credential.xml b/sampleapp/src/main/res/layout/placeholder_credential.xml index fb8ae700e..03ad4b9ff 100644 --- a/sampleapp/src/main/res/layout/placeholder_credential.xml +++ b/sampleapp/src/main/res/layout/placeholder_credential.xml @@ -8,7 +8,7 @@ @@ -30,6 +30,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" /> + + \ No newline at end of file From a1be83fd305789765f1a4e53d083010c8e582c5d Mon Sep 17 00:00:00 2001 From: Cristian G Date: Wed, 17 Apr 2024 10:58:25 -0400 Subject: [PATCH 3/4] feat: revocation notification --- .../walletsdk/domain/buildingblocks/Pluto.kt | 10 ++++ .../atala/prism/walletsdk/pluto/PlutoImpl.kt | 22 +++++++ .../prism/walletsdk/prismagent/PrismAgent.kt | 58 +++++++++++++++---- .../pluto/data/StorableCredential.sq | 7 ++- .../prism/walletsdk/mercury/PlutoMock.kt | 4 ++ .../prism/walletsdk/prismagent/PlutoMock.kt | 4 ++ .../walletsdk/prismagent/PrismAgentTests.kt | 2 +- .../ui/credentials/CredentialsViewModel.kt | 5 +- .../sampleapp/ui/messages/MessagesFragment.kt | 14 ++++- .../ui/messages/MessagesViewModel.kt | 23 ++++++++ 10 files changed, 132 insertions(+), 17 deletions(-) diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt index 9b2a07809..642e66ab4 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/domain/buildingblocks/Pluto.kt @@ -328,5 +328,15 @@ interface Pluto { */ fun getCredentialMetadata(linkSecretName: String): Flow + /** + * Revokes an existing credential using the credential ID. + * + * @param credentialId The ID of the credential to be revoked + */ fun revokeCredential(credentialId: String) + + /** + * Provides a flow to listen for revoked credentials. + */ + fun observeRevokedCredentials(): Flow> } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt index 56357e26c..0e3bf5b17 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/pluto/PlutoImpl.kt @@ -1026,7 +1026,29 @@ class PlutoImpl(private val connection: DbConnection) : Pluto { } } + /** + * Revokes an existing credential using the credential ID. + * + * @param credentialId The ID of the credential to be revoked + */ override fun revokeCredential(credentialId: String) { getInstance().storableCredentialQueries.revokeCredentialById(credentialId) } + + /** + * Provides a flow to listen for revoked credentials. + */ + override fun observeRevokedCredentials(): Flow> { + return getInstance().storableCredentialQueries.observeRevokedCredential() + .asFlow() + .map { + it.executeAsList().map { credential -> + CredentialRecovery( + restorationId = credential.recoveryId, + credentialData = credential.credentialData, + revoked = true + ) + } + } + } } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt index 0f3d11e7a..553f1f057 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt @@ -230,7 +230,9 @@ class PrismAgent { if (flowState.subscriptionCount.value <= 0) { state = State.STOPPED } else { - throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Agent state only accepts one subscription.") + throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Agent state only accepts one subscription." + ) // throw Exception("Agent state only accepts one subscription.") } } @@ -427,7 +429,12 @@ class PrismAgent { verificationMethods.values.forEach { if (it.type.contains("X25519")) { - pluto.storePrivateKeys(keyAgreementKeyPair.privateKey as StorableKey, did, 0, it.id.toString()) + pluto.storePrivateKeys( + keyAgreementKeyPair.privateKey as StorableKey, + did, + 0, + it.id.toString() + ) } else if (it.type.contains("Ed25519")) { pluto.storePrivateKeys( authenticationKeyPair.privateKey as StorableKey, @@ -565,7 +572,10 @@ class PrismAgent { * @throws [PolluxError.InvalidPrismDID] if there is a problem creating the request credential. * @throws [io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError] if credential type is not supported **/ - @Throws(PolluxError.InvalidPrismDID::class, io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError::class) + @Throws( + PolluxError.InvalidPrismDID::class, + io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError::class + ) suspend fun prepareRequestCredentialWithIssuer( did: DID, offer: OfferCredential @@ -655,7 +665,9 @@ class PrismAgent { else -> { // TODO: Create new prism agent error message - throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Not supported credential type: $type") + throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Not supported credential type: $type" + ) // throw Error("Not supported credential type: $type") } } @@ -686,7 +698,9 @@ class PrismAgent { val metadata = if (credentialType == CredentialType.ANONCREDS_ISSUE) { val plutoMetadata = pluto.getCredentialMetadata(message.thid).first() - ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Invalid credential metadata") + ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Invalid credential metadata" + ) CredentialRequestMetadata( plutoMetadata.json ) @@ -709,8 +723,13 @@ class PrismAgent { ) pluto.storeCredential(storableCredential) return credential - } ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Thid should not be null") - } ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError("Cannot find attachment base64 in message") + } + ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Thid should not be null" + ) + } ?: throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + "Cannot find attachment base64 in message" + ) } // Message Events @@ -811,7 +830,10 @@ class PrismAgent { prismOnboarding.from = did return prismOnboarding } catch (e: Exception) { - throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError(e.message, e.cause) + throw io.iohk.atala.prism.walletsdk.domain.models.UnknownError.SomethingWentWrongError( + e.message, + e.cause + ) } } @@ -913,7 +935,10 @@ class PrismAgent { val privateKeyKeyPath = pluto.getPrismDIDKeyPathIndex(subjectDID).first() val keyPair = - Secp256k1KeyPair.generateKeyPair(seed, KeyCurve(Curve.SECP256K1, privateKeyKeyPath)) + Secp256k1KeyPair.generateKeyPair( + seed, + KeyCurve(Curve.SECP256K1, privateKeyKeyPath) + ) val requestData = request.attachments.mapNotNull { when (it.data) { is AttachmentJsonData -> it.data.data @@ -936,7 +961,11 @@ class PrismAgent { throw PrismAgentError.InvalidCredentialFormatError(CredentialType.ANONCREDS_PROOF_REQUEST) } val linkSecret = getLinkSecret() - val presentation = pollux.createVerifiablePresentationAnoncred(request, credential as AnonCredential, linkSecret) + val presentation = pollux.createVerifiablePresentationAnoncred( + request, + credential as AnonCredential, + linkSecret + ) presentationString = presentation.getJson() } @@ -959,6 +988,15 @@ class PrismAgent { ) } + fun observeRevokedCredentials(): Flow> { + return pluto.observeRevokedCredentials() + .map { list -> + list.map { + pollux.restoreCredential(it.restorationId, it.credentialData, it.revoked) + } + } + } + /** * This method retrieves the link secret from Pluto. * diff --git a/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq b/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq index fe8e00766..d3f5db0f2 100644 --- a/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq +++ b/atala-prism-sdk/src/commonMain/sqldelight/io/iohk/atala/prism/walletsdk/pluto/data/StorableCredential.sq @@ -27,4 +27,9 @@ GROUP BY StorableCredential.id; revokeCredentialById: UPDATE StorableCredential SET revoked = 1 -WHERE id = :id; \ No newline at end of file +WHERE id = :id; + +observeRevokedCredential: +SELECT * +FROM StorableCredential +WHERE revoked = 1; \ No newline at end of file diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt index 3d97152ba..ca1a8a0e0 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/mercury/PlutoMock.kt @@ -184,4 +184,8 @@ class PlutoMock : Pluto { override fun revokeCredential(credentialId: String) { TODO("Not yet implemented") } + + override fun observeRevokedCredentials(): Flow> { + TODO("Not yet implemented") + } } diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt index d1ea3704a..af68ac02a 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PlutoMock.kt @@ -242,4 +242,8 @@ class PlutoMock : Pluto { override fun revokeCredential(credentialId: String) { TODO("Not yet implemented") } + + override fun observeRevokedCredentials(): Flow> { + TODO("Not yet implemented") + } } diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt index 43768c8ec..99dca5cc5 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgentTests.kt @@ -357,7 +357,7 @@ class PrismAgentTests { @Test fun testStartPrismAgent_whenCalled_thenStatusIsRunning() = runTest { - val getLinkSecretReturn = flow { "linkSecret" } + val getLinkSecretReturn = flow { emit("linkSecret") } plutoMock.getLinkSecretReturn = getLinkSecretReturn val agent = PrismAgent( apollo = apolloMock, diff --git a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt index e15f9b430..ff3890219 100644 --- a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt +++ b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/credentials/CredentialsViewModel.kt @@ -14,7 +14,7 @@ class CredentialsViewModel(application: Application) : AndroidViewModel(applicat private var credentials: MutableLiveData> = MutableLiveData() - init { + fun credentialsStream(): LiveData> { viewModelScope.launch { Sdk.getInstance().agent.let { it.getAllCredentials().collect { list -> @@ -22,9 +22,6 @@ class CredentialsViewModel(application: Application) : AndroidViewModel(applicat } } } - } - - fun credentialsStream(): LiveData> { return credentials } } diff --git a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt index 8b656d557..4f4867df4 100644 --- a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt +++ b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import io.iohk.atala.prism.sampleapp.R @@ -52,7 +53,8 @@ class MessagesFragment : Fragment() { // Set up the spinner with the options context?.let { - val adapter = CustomArrayAdapter(it, android.R.layout.simple_spinner_dropdown_item, credentials) + val adapter = + CustomArrayAdapter(it, android.R.layout.simple_spinner_dropdown_item, credentials) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) dialogBinding.spinner.adapter = adapter @@ -81,6 +83,16 @@ class MessagesFragment : Fragment() { viewModel.preparePresentationProof(credential, message) } } + viewModel.revokedCredentialsStream() + .observe(this.viewLifecycleOwner) { revokedCredentials -> + if (revokedCredentials.isNotEmpty()) { + Toast.makeText( + context, + "Credential revoked ID: ${revokedCredentials.last().id}", + Toast.LENGTH_LONG + ).show() + } + } } companion object { diff --git a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt index 3661373ba..652e67970 100644 --- a/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt +++ b/sampleapp/src/main/java/io/iohk/atala/prism/sampleapp/ui/messages/MessagesViewModel.kt @@ -32,6 +32,8 @@ class MessagesViewModel(application: Application) : AndroidViewModel(application private val issuedCredentials: ArrayList = arrayListOf() private val processedOffers: ArrayList = arrayListOf() private val db: AppDatabase = DatabaseClient.getInstance() + private val revokedCredentialsNotified: MutableList = mutableListOf() + private var revokedCredentials: MutableLiveData> = MutableLiveData() init { viewModelScope.launch(Dispatchers.IO) { @@ -103,6 +105,27 @@ class MessagesViewModel(application: Application) : AndroidViewModel(application } } + fun revokedCredentialsStream(): LiveData> { + viewModelScope.launch { + Sdk.getInstance().agent.let { + it.observeRevokedCredentials().collect { list -> + val newRevokedCredentials = list.filter { newCredential -> + revokedCredentialsNotified.none { notifiedCredential -> + notifiedCredential.id == newCredential.id + } + } + if (newRevokedCredentials.isNotEmpty()) { + revokedCredentialsNotified.addAll(newRevokedCredentials) + revokedCredentials.postValue(newRevokedCredentials) + } else { + revokedCredentials.postValue(emptyList()) + } + } + } + } + return revokedCredentials + } + private suspend fun processMessages(messages: List) { val sdk = Sdk.getInstance() val messageIds: List = messages.map { it.id } From 3c1a442746aab2c347a3a4b179bf5b7f2f0bef3e Mon Sep 17 00:00:00 2001 From: Cristian G Date: Wed, 17 Apr 2024 11:05:47 -0400 Subject: [PATCH 4/4] docs: add docs to new observeRevokedCredentials --- .../walletsdk/prismagent/ConnectionManager.kt | 28 ------------------- .../prism/walletsdk/prismagent/PrismAgent.kt | 4 +++ 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt index 0ff6ba078..f0a4f7394 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt @@ -82,22 +82,6 @@ class ConnectionManager( serviceEndpointUrl ) { arrayMessages -> processMessages(arrayMessages) -// // Process the received messages -// val messagesIds = mutableListOf() -// val messages = mutableListOf() -// arrayMessages.map { pair -> -// messagesIds.add(pair.first) -// messages.add(pair.second) -// } -// // If there are any messages, mark them as read and store them -// scope.launch { -// if (messagesIds.isNotEmpty()) { -// mediationHandler.registerMessagesAsRead( -// messagesIds.toTypedArray() -// ) -// pluto.storeMessages(messages) -// } -// } } } @@ -107,18 +91,6 @@ class ConnectionManager( // Continuously await and process new messages awaitMessages().collect { array -> processMessages(array) -// val messagesIds = mutableListOf() -// val messages = mutableListOf() -// array.map { pair -> -// messagesIds.add(pair.first) -// messages.add(pair.second) -// } -// if (messagesIds.isNotEmpty()) { -// mediationHandler.registerMessagesAsRead( -// messagesIds.toTypedArray() -// ) -// pluto.storeMessages(messages) -// } } // Wait for the specified request interval before fetching new messages delay(Duration.ofSeconds(requestInterval.toLong()).toMillis()) diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt index 553f1f057..ad5d23105 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt @@ -988,6 +988,10 @@ class PrismAgent { ) } + /** + * This method provides a channel to listen for credentials that are revoked. As long as there is an + * observer collecting from this flow the updates will keep happening. + */ fun observeRevokedCredentials(): Flow> { return pluto.observeRevokedCredentials() .map { list ->