Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: revocation notification event #148

Merged
merged 4 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,4 +327,16 @@ interface Pluto {
* or null if no metadata is found.
*/
fun getCredentialMetadata(linkSecretName: String): Flow<CredentialRequestMeta?>

/**
* 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<List<CredentialRecovery>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ interface Credential {
val subject: String?
val claims: Array<Claim>
val properties: Map<String, Any?>
var revoked: Boolean?
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface StorableCredential : Credential {
val credentialUpdated: String?
val credentialSchema: String?
val validUntil: String?
val revoked: Boolean?
val availableClaims: Array<String>

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is migrations, nice @cristianIOHK

"ALTER TABLE CredentialMetadata DROB COLUMN linkSecretBlindingData;",
0
)
it.execute(null, "ALTER TABLE CredentialMetadata ADD COLUMN json TEXT;", 0)
}
)
Expand Down Expand Up @@ -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
)
}
}
Expand Down Expand Up @@ -1020,4 +1025,30 @@ 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<List<CredentialRecovery>> {
return getInstance().storableCredentialQueries.observeRevokedCredential()
.asFlow()
.map {
it.executeAsList().map { credential ->
CredentialRecovery(
restorationId = credential.recoveryId,
credentialData = credential.credentialData,
revoked = true
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<W3CCredential>(credentialData.decodeToString())
cred = Json.decodeFromString<W3CCredential>(credentialData.decodeToString())
}

else -> {
throw PolluxError.InvalidCredentialError()
}
}
cred.revoked = revoked
return cred
}

/**
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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<String>
get() = c.claims.map { it.key }.toTypedArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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].
*
Expand Down Expand Up @@ -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<String>
get() = c.claims.map { it.key }.toTypedArray()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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<String>
get() = claims.map { it.key }.toTypedArray()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

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.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
Expand All @@ -36,6 +43,7 @@ class ConnectionManager(
private val pluto: Pluto,
internal val mediationHandler: MediationHandler,
private var pairings: MutableList<DIDPair>,
private val pollux: Pollux,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) : ConnectionsManager, DIDCommConnection {

Expand Down Expand Up @@ -73,22 +81,7 @@ class ConnectionManager(
mediationHandler.listenUnreadMessages(
serviceEndpointUrl
) { arrayMessages ->
// Process the received messages
val messagesIds = mutableListOf<String>()
val messages = mutableListOf<Message>()
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)
}
}

Expand All @@ -97,18 +90,7 @@ class ConnectionManager(
while (true) {
// Continuously await and process new messages
awaitMessages().collect { array ->
val messagesIds = mutableListOf<String>()
val messages = mutableListOf<Message>()
array.map { pair ->
messagesIds.add(pair.first)
messages.add(pair.second)
}
if (messagesIds.isNotEmpty()) {
mediationHandler.registerMessagesAsRead(
messagesIds.toTypedArray()
)
pluto.storeMessages(messages)
}
processMessages(array)
}
// Wait for the specified request interval before fetching new messages
delay(Duration.ofSeconds(requestInterval.toLong()).toMillis())
Expand Down Expand Up @@ -198,6 +180,48 @@ class ConnectionManager(
return null
}

internal fun processMessages(arrayMessages: Array<Pair<String, Message>>) {
scope.launch {
val messagesIds = mutableListOf<String>()
val messages = mutableListOf<Message>()
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.
*
Expand Down
Loading
Loading