From c752ea178c5dfb0c21c0a4c4d5befc5d9659fc6f Mon Sep 17 00:00:00 2001 From: Cody Hatfield Date: Mon, 9 Jan 2023 18:00:00 -0800 Subject: [PATCH] feat(did-comm) Routing protocol to handle forward messages --- .../__tests__/routing-message-handler.test.ts | 259 ++++++++++++++++++ .../coordinate-mediation-message-handler.ts | 12 +- packages/did-comm/src/protocols/index.ts | 3 +- .../src/protocols/routing-message-handler.ts | 82 ++++++ 4 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 packages/did-comm/src/__tests__/routing-message-handler.test.ts create mode 100644 packages/did-comm/src/protocols/routing-message-handler.ts diff --git a/packages/did-comm/src/__tests__/routing-message-handler.test.ts b/packages/did-comm/src/__tests__/routing-message-handler.test.ts new file mode 100644 index 0000000000..99dcd7f5ac --- /dev/null +++ b/packages/did-comm/src/__tests__/routing-message-handler.test.ts @@ -0,0 +1,259 @@ +import { DIDComm } from '../didcomm' +import { + createAgent, + IDIDManager, + IEventListener, + IIdentifier, + IKeyManager, + IMessageHandler, + IResolver, + TAgent, +} from '../../../core/src' +import { DIDManager, MemoryDIDStore } from '../../../did-manager/src' +import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../../../key-manager/src' +import { KeyManagementSystem } from '../../../kms-local/src' +import { DIDResolverPlugin } from '../../../did-resolver/src' +import { Resolver } from 'did-resolver' +import { DIDCommHttpTransport } from '../transports/transports' +import { IDIDComm } from '../types/IDIDComm' +import { MessageHandler } from '../../../message-handler/src' +import { + CoordinateMediationMediatorMessageHandler, + CoordinateMediationRecipientMessageHandler, + createMediateRequestMessage, + createMediateGrantMessage, +} from '../protocols/coordinate-mediation-message-handler' +import { DIDCommMessageMediaType } from '../types/message-types' +import { + RoutingMessageHandler, + FORWARD_MESSAGE_TYPE, + QUEUE_MESSAGE_TYPE, +} from '../protocols/routing-message-handler' +import { FakeDidProvider, FakeDidResolver } from '../../../test-utils/src' +import { MessagingRouter, RequestWithAgentRouter } from '../../../remote-server/src' +import { Entities, IDataStore, migrations } from '../../../data-store/src' +import express from 'express' +import { Server } from 'http' +import { DIDCommMessageHandler } from '../message-handler' +import { DataStore, DataStoreORM } from '../../../data-store/src' +import { DataSource } from 'typeorm' +import { v4 } from 'uuid' + +const DIDCommEventSniffer: IEventListener = { + eventTypes: ['DIDCommV2Message-sent', 'DIDCommV2Message-received', 'DIDCommV2Message-forwardMessageSaved'], + onEvent: jest.fn(), +} + +const databaseFile = `./tmp/local-database2-${Math.random().toPrecision(5)}.sqlite` + +describe('routing-message-handler', () => { + let recipient: IIdentifier + let mediator: IIdentifier + let agent: TAgent + let didCommEndpointServer: Server + let listeningPort = Math.round(Math.random() * 32000 + 2048) + let dbConnection: DataSource + + beforeAll(async () => { + dbConnection = new DataSource({ + name: 'test', + type: 'sqlite', + database: databaseFile, + synchronize: false, + migrations: migrations, + migrationsRun: true, + logging: false, + entities: Entities, + }) + agent = createAgent({ + plugins: [ + new KeyManager({ + store: new MemoryKeyStore(), + kms: { + // @ts-ignore + local: new KeyManagementSystem(new MemoryPrivateKeyStore()), + }, + }), + new DIDManager({ + providers: { + 'did:fake': new FakeDidProvider(), + // 'did:web': new WebDIDProvider({ defaultKms: 'local' }) + }, + store: new MemoryDIDStore(), + defaultProvider: 'did:fake', + }), + new DIDResolverPlugin({ + resolver: new Resolver({ + ...new FakeDidResolver(() => agent).getDidFakeResolver(), + }), + }), + // @ts-ignore + new DIDComm([new DIDCommHttpTransport()]), + new MessageHandler({ + messageHandlers: [ + // @ts-ignore + new DIDCommMessageHandler(), + new CoordinateMediationMediatorMessageHandler(), + new CoordinateMediationRecipientMessageHandler(), + new RoutingMessageHandler(), + ], + }), + new DataStore(dbConnection), + new DataStoreORM(dbConnection), + DIDCommEventSniffer, + ], + }) + + recipient = await agent.didManagerImport({ + did: 'did:fake:z6MkgbqNU4uF9NKSz5BqJQ4XKVHuQZYcUZP8pXGsJC8nTHwo', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-senderKey-1', + publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + privateKeyHex: + 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + kms: 'local', + }, + ], + services: [ + { + id: 'msg1', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + }, + ], + provider: 'did:fake', + alias: 'sender', + }) + + mediator = await agent.didManagerImport({ + did: 'did:fake:z6MkrPhffVLBZpxH7xvKNyD4sRVZeZsNTWJkLdHdgWbfgNu3', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverKey-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg2', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + }, + ], + provider: 'did:fake', + alias: 'receiver', + }) + // console.log('sender: ', sender) + // console.log('recipient: ', recipient) + + const requestWithAgent = RequestWithAgentRouter({ agent }) + + await new Promise((resolve) => { + //setup a server to receive HTTP messages and forward them to this agent to be processed as DIDComm messages + const app = express() + // app.use(requestWithAgent) + app.use( + '/messaging', + requestWithAgent, + MessagingRouter({ + metaData: { type: 'DIDComm', value: 'integration test' }, + }), + ) + didCommEndpointServer = app.listen(listeningPort, () => { + resolve(true) + }) + }) + }) + + afterAll(async () => { + try { + await new Promise((resolve, reject) => didCommEndpointServer?.close(resolve)) + } catch (e) { + //nop + } + }) + + it('should save forward message in queue for recipient', async () => { + expect.assertions(2) + + // 1. Coordinate mediation + const mediateRequestMessage = createMediateRequestMessage(recipient.did, mediator.did) + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + // 2. Forward message + const innerMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'test', + to: recipient.did, + from: mediator.did, + id: 'test', + body: { hello: 'world' }, + }, + }) + const msgId = v4() + const packedForwardMessage = await agent.packDIDCommMessage({ + packing: 'anoncrypt', + message: { + type: FORWARD_MESSAGE_TYPE, + to: mediator.did, + id: msgId, + body: { + next: recipient.did, + }, + attachments: [{ media_type: DIDCommMessageMediaType.ENCRYPTED, data: { json: innerMessage } }], + }, + }) + await agent.sendDIDCommMessage({ + messageId: msgId, + packedMessage: packedForwardMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { next: recipient.did }, + id: msgId, + to: mediator.did, + type: FORWARD_MESSAGE_TYPE, + attachments: [{ media_type: DIDCommMessageMediaType.ENCRYPTED, data: { json: innerMessage } }], + }, + metaData: { packing: 'anoncrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + id: expect.anything(), + to: recipient.did, + type: QUEUE_MESSAGE_TYPE, + raw: innerMessage, + createdAt: expect.anything(), + metaData: [{ type: 'didCommForwardMsgId', value: msgId }], + }, + type: 'DIDCommV2Message-forwardMessageSaved', + }, + expect.anything(), + ) + }) +}) diff --git a/packages/did-comm/src/protocols/coordinate-mediation-message-handler.ts b/packages/did-comm/src/protocols/coordinate-mediation-message-handler.ts index e9cab4c8fb..081699919a 100644 --- a/packages/did-comm/src/protocols/coordinate-mediation-message-handler.ts +++ b/packages/did-comm/src/protocols/coordinate-mediation-message-handler.ts @@ -5,13 +5,13 @@ import { v4 } from 'uuid' import { IDIDComm } from '../types/IDIDComm' import { IDIDCommMessage, DIDCommMessageMediaType } from '../types/message-types' -const debug = Debug('veramo:did-comm:trust-ping-message-handler') +const debug = Debug('veramo:did-comm:coordinate-mediation-message-handler') type IContext = IAgentContext -const MEDIATE_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-request' -const MEDIATE_GRANT_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-grant' -const MEDIATE_DENY_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-deny' +export const MEDIATE_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-request' +export const MEDIATE_GRANT_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-grant' +export const MEDIATE_DENY_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-deny' export function createMediateRequestMessage( recipientDidUrl: string, @@ -82,6 +82,10 @@ export class CoordinateMediationMediatorMessageHandler extends AbstractMessageHa contentType: DIDCommMessageMediaType.ENCRYPTED, } message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) }) + + // Save message to track recipients + // FIXME: Save IMessage + await context.agent.dataStoreSaveMessage({ message: response }) } } catch (ex) { debug(ex) diff --git a/packages/did-comm/src/protocols/index.ts b/packages/did-comm/src/protocols/index.ts index e315251d36..51eedad974 100644 --- a/packages/did-comm/src/protocols/index.ts +++ b/packages/did-comm/src/protocols/index.ts @@ -1,2 +1,3 @@ export { TrustPingMessageHandler } from "./trust-ping-message-handler" -export { CoordinateMediationMediatorMessageHandler } from "./coordinate-mediation-message-handler" \ No newline at end of file +export { CoordinateMediationMediatorMessageHandler, CoordinateMediationRecipientMessageHandler } from "./coordinate-mediation-message-handler" +export { RoutingMessageHandler } from "./routing-message-handler" \ No newline at end of file diff --git a/packages/did-comm/src/protocols/routing-message-handler.ts b/packages/did-comm/src/protocols/routing-message-handler.ts new file mode 100644 index 0000000000..67846046a1 --- /dev/null +++ b/packages/did-comm/src/protocols/routing-message-handler.ts @@ -0,0 +1,82 @@ +import { IAgentContext, IDIDManager, IKeyManager, IDataStore, IDataStoreORM } from '@veramo/core' +import { AbstractMessageHandler, Message } from '@veramo/message-handler' +import Debug from 'debug' +import { v4 } from 'uuid' +import { IDIDComm } from '../types/IDIDComm' +import { MEDIATE_GRANT_MESSAGE_TYPE, MEDIATE_DENY_MESSAGE_TYPE } from './coordinate-mediation-message-handler' + +const debug = Debug('veramo:did-comm:routing-message-handler') + +type IContext = IAgentContext + +export const FORWARD_MESSAGE_TYPE = 'https://didcomm.org/routing/2.0/forward' +export const QUEUE_MESSAGE_TYPE = 'https://didcomm.org/routing/2.0/forward/queue-message' + +/** + * A plugin for the {@link @veramo/message-handler#MessageHandler} that handles forward messages for the Routing protocol. + * @beta This API may change without a BREAKING CHANGE notice. + */ +export class RoutingMessageHandler extends AbstractMessageHandler { + constructor() { + super() + } + + /** + * Handles forward messages for Routing protocol + * https://didcomm.org/routing/2.0/ + */ + public async handle(message: Message, context: IContext): Promise { + if (message.type === FORWARD_MESSAGE_TYPE) { + debug('Forward Message Received') + try { + const { attachments, data } = message + if (!attachments) { + throw new Error('invalid_argument: Forward received without `attachments` set') + } + if (!data.next) { + throw new Error('invalid_argument: Forward received without `body.next` set') + } + + if (attachments.length > 0) { + // Check if receiver has been granted mediation + const mediationResponses = await context.agent.dataStoreORMGetMessages({ + where: [ + { + column: 'type', + value: [MEDIATE_GRANT_MESSAGE_TYPE, MEDIATE_DENY_MESSAGE_TYPE], + op: 'In', + }, + { + column: 'to', + value: [data.next], + op: 'In', + }, + ], + order: [{ column: 'createdAt', direction: 'DESC' }], + }) + + // If last mediation response was a grant (not deny) + if (mediationResponses.length > 0 && mediationResponses[0].type === MEDIATE_GRANT_MESSAGE_TYPE) { + // Save message for queue + const messageToQueue = new Message({ raw: attachments[0].data.json }) + messageToQueue.id = v4() + messageToQueue.type = QUEUE_MESSAGE_TYPE + messageToQueue.to = data.next + messageToQueue.createdAt = new Date().toISOString() + messageToQueue.addMetaData({ type: 'didCommForwardMsgId', value: message.id }) + + await context.agent.dataStoreSaveMessage({ message: messageToQueue }) + context.agent.emit('DIDCommV2Message-forwardMessageSaved', messageToQueue) + } else { + debug('Forward received for DID without granting mediation') + } + } + } catch (ex) { + debug(ex) + } + return message + } + + return super.handle(message, context) + } +}