UNPKG

@veramo/did-comm

Version:

Veramo messaging plugin implementing DIDComm v2.

984 lines (912 loc) 37.7 kB
import type { IAgentContext, IAgentPlugin, IDIDManager, IIdentifier, IKeyManager, IMessage, IMessageHandler, IResolver, } from '@veramo/core-types' import { createJWE, type Decrypter, decryptJWE, type ECDH, type Encrypter, type JWE, verifyJWS, } from 'did-jwt' import { type DIDDocument, type DIDResolutionOptions, parse as parseDidUrl, type Service, type ServiceEndpoint, type VerificationMethod, } from 'did-resolver' import { a256cbcHs512AnonDecrypterX25519WithA256KW, a256cbcHs512AnonEncrypterX25519WithA256KW, a256cbcHs512AuthDecrypterX25519WithA256KW, a256cbcHs512AuthEncrypterX25519WithA256KW, a256gcmAnonDecrypterX25519WithA256KW, a256gcmAnonEncrypterX25519WithA256KW, a256gcmAuthDecrypterEcdh1PuV3x25519WithA256KW, a256gcmAuthEncrypterEcdh1PuV3x25519WithA256KW, xc20pAnonDecrypterX25519WithA256KW, xc20pAnonEncrypterX25519WithA256KW, xc20pAuthDecrypterEcdh1PuV3x25519WithA256KW, xc20pAuthEncrypterEcdh1PuV3x25519WithA256KW, } from './encryption/a256kw-encrypters.js' import { a256cbcHs512AnonDecrypterX25519WithXC20PKW, a256cbcHs512AnonEncrypterX25519WithXC20PKW, a256cbcHs512AuthDecrypterX25519WithXC20PKW, a256cbcHs512AuthEncrypterX25519WithXC20PKW, a256gcmAnonDecrypterX25519WithXC20PKW, a256gcmAnonEncrypterX25519WithXC20PKW, a256gcmAuthDecrypterEcdh1PuV3x25519WithXC20PKW, a256gcmAuthEncrypterEcdh1PuV3x25519WithXC20PKW, xc20pAnonDecrypterX25519WithXC20PKW, xc20pAnonEncrypterX25519WithXC20PKW, xc20pAuthDecrypterEcdh1PuV3x25519WithXC20PKW, xc20pAuthEncrypterEcdh1PuV3x25519WithXC20PKW, } from './encryption/xc20pkw-encrypters.js' import { schema } from './plugin.schema.js' import { v4 as uuidv4 } from 'uuid' import { createEcdhWrapper, extractManagedRecipients, extractSenderEncryptionKey, mapRecipientsToLocalKeys, } from './utils.js' import { _ExtendedIKey, _NormalizedVerificationMethod, asArray, bytesToUtf8String, decodeJoseBlob, dereferenceDidKeys, encodeJoseBlob, extractPublicKeyHex, hexToBytes, isDefined, mapIdentifierKeysToDoc, resolveDidOrThrow, stringToUtf8Bytes, } from '@veramo/utils' import Debug from 'debug' import { IDIDComm, IPackDIDCommMessageArgs, ISendDIDCommMessageArgs, ISendDIDCommMessageResponse, IUnpackDIDCommMessageArgs, } from './types/IDIDComm.js' import { DIDCommHttpTransport, IDIDCommTransport } from './transports/transports.js' import { DIDCommMessageMediaType, IDIDCommMessage, IPackedDIDCommMessage, IUnpackedDIDCommMessage, } from './types/message-types.js' import { _DIDCommEncryptedMessage, _DIDCommPlainMessage, _DIDCommSignedMessage, _FlattenedJWS, _GenericJWS, } from './types/utility-types.js' const debug = Debug('veramo:did-comm:action-handler') /** * @deprecated Please use {@link IDIDComm.sendDIDCommMessage} instead. This will be removed in Veramo 4.0. * Input arguments for {@link IDIDComm.sendMessageDIDCommAlpha1} * * @beta This API may change without a BREAKING CHANGE notice. */ export interface ISendMessageDIDCommAlpha1Args { url?: string save?: boolean data: { id?: string from: string to: string type: string body: object | string } headers?: Record<string, string> } /** * The config for the {@link DIDComm} DIDComm plugin. * * @beta This API may change without a BREAKING CHANGE notice. */ export interface DIDCommConfig<T extends IDIDCommTransport = DIDCommHttpTransport> { transports?: T[] } /** * DID Comm plugin for {@link @veramo/core#Agent} * * This plugin provides a method of creating an encrypted message according to the initial * {@link https://github.com/decentralized-identifier/DIDComm-js | DIDComm-js} implementation. * * @remarks Be advised that this spec is still not final and that this protocol may need to change. * * @beta This API may change without a BREAKING CHANGE notice. */ export class DIDComm implements IAgentPlugin { readonly transports: IDIDCommTransport[] /** Plugin methods */ readonly methods: IDIDComm readonly schema = schema.IDIDComm /** * Constructor that takes a list of {@link IDIDCommTransport} objects. * @param transports - A list of {@link IDIDCommTransport} objects. Defaults to * {@link @veramo/did-comm#DIDCommHttpTransport | DIDCommHttpTransport} */ constructor({ transports = [new DIDCommHttpTransport()] }: DIDCommConfig = {}) { this.transports = transports this.methods = { sendMessageDIDCommAlpha1: this.sendMessageDIDCommAlpha1.bind(this), getDIDCommMessageMediaType: this.getDidCommMessageMediaType.bind(this), unpackDIDCommMessage: this.unpackDIDCommMessage.bind(this), packDIDCommMessage: this.packDIDCommMessage.bind(this), sendDIDCommMessage: this.sendDIDCommMessage.bind(this), } } /** {@inheritdoc IDIDComm.packDIDCommMessage} */ async packDIDCommMessage( args: IPackDIDCommMessageArgs, context: IAgentContext<IDIDManager & IKeyManager & IResolver>, ): Promise<IPackedDIDCommMessage> { switch (args.packing) { case 'authcrypt': // intentionally omitting break case 'anoncrypt': return this.packDIDCommMessageJWE(args, context) case 'none': const message = { ...args.message, typ: DIDCommMessageMediaType.PLAIN, } return { message: JSON.stringify(message) } case 'jws': return this.packDIDCommMessageJWS(args, context) default: throw new Error(`not_implemented: packing messages as ${args.packing} is not supported yet`) } } private async packDIDCommMessageJWS( args: IPackDIDCommMessageArgs, context: IAgentContext<IDIDManager & IKeyManager & IResolver>, ): Promise<IPackedDIDCommMessage> { const message = args.message let keyRef: string | undefined = args.keyRef let kid: string // check that the message has from field that is managed let managedSender: IIdentifier | undefined try { managedSender = await context.agent.didManagerGet({ did: message.from || '' }) } catch (e) { debug(`message.from(${message.from}) is not managed by this agent`) } if (!message.from || !isDefined(managedSender)) { throw new Error('invalid_argument: `from` field must be a DID managed by this agent') } // obtain sender signing key(s) from authentication section const senderKeys = await mapIdentifierKeysToDoc( managedSender, 'authentication', context, args.resolutionOptions, ) // try to find a managed signing key that matches keyRef let signingKey = null if (isDefined(keyRef)) { signingKey = senderKeys.find((key) => key.kid === keyRef || key.meta.verificationMethod.id === keyRef) } // otherwise use the first available one. signingKey = signingKey ? signingKey : senderKeys[0] if (!signingKey) { throw new Error(`key_not_found: could not locate a suitable signing key for ${message.from}`) } else { kid = signingKey.meta.verificationMethod.id } let alg: string if (signingKey.type === 'Ed25519') { alg = 'EdDSA' } else if (signingKey.type === 'Secp256k1') { alg = 'ES256K' } else { throw new Error( `not_supported: key of type ${signingKey.type} is not supported for JWS didcomm message`, ) } // construct the protected header with alg, typ and kid const headerObj = { alg, kid, typ: DIDCommMessageMediaType.SIGNED } const header = encodeJoseBlob(headerObj) const payload = encodeJoseBlob(args.message) // construct signing input and obtain signature const signingInput = header + '.' + payload const signature: string = await context.agent.keyManagerSign({ data: signingInput, encoding: 'utf-8', keyRef: signingKey.kid, algorithm: alg, }) // create flattened JWS const packedMessage = { protected: header, payload, signature, } // serialize flattened JWS JSON and return return { message: JSON.stringify(packedMessage) } } private async packDIDCommMessageJWE( args: IPackDIDCommMessageArgs, context: IAgentContext<IDIDManager & IKeyManager & IResolver>, ): Promise<IPackedDIDCommMessage> { // 1. check if args.packing requires authentication and map sender key to skid let senderECDH: ECDH | null = null let keyRef: string | undefined = args.keyRef let protectedHeader: { skid?: string typ: string } = { typ: DIDCommMessageMediaType.ENCRYPTED, } if (args.packing === 'authcrypt') { // TODO: what to do about from_prior? if (!args?.message?.from) { throw new Error( `invalid_argument: cannot create authenticated did-comm message without a 'from' field`, ) } // 1.1 check that args.message.from is a managed DID const sender: IIdentifier = await context.agent.didManagerGet({ did: args?.message?.from }) // 1.2 match key agreement keys from DID to managed keys const senderKeys: _ExtendedIKey[] = await mapIdentifierKeysToDoc( sender, 'keyAgreement', context, args.resolutionOptions, ) // try to find a sender key by keyRef, otherwise pick the first one let senderKey if (isDefined(keyRef)) { senderKey = senderKeys.find((key) => key.kid === keyRef || key.meta.verificationMethod.id === keyRef) } senderKey = senderKey || senderKeys[0] // 1.3 use kid from DID doc(skid) + local IKey to bundle a sender key if (senderKey) { senderECDH = createEcdhWrapper(senderKey.kid, context) protectedHeader = { ...protectedHeader, skid: senderKey.meta.verificationMethod.id } } else { throw new Error(`key_not_found: could not map an agent key to an skid for ${args?.message?.from}`) } } const defaults = { alg: args.packing === 'authcrypt' ? 'ECDH-1PU+A256KW' : 'ECDH-ES+A256KW', enc: 'A256GCM', // 'XC20P' or 'A256CBC-HS512' can also be specified } const options = { ...defaults, ...args.options } // 2: compute recipients interface IRecipient { kid: string publicKeyBytes: Uint8Array keyType: string } let recipients: IRecipient[] = [] async function computeRecipients( to: string, resolutionOptions?: DIDResolutionOptions, ): Promise<IRecipient[]> { // 2.1 resolve DID for "to" const didDocument: DIDDocument = await resolveDidOrThrow(to, context, resolutionOptions) // 2.2 extract all recipient key agreement keys and normalize them const keyAgreementKeys: _NormalizedVerificationMethod[] = ( await dereferenceDidKeys(didDocument, 'keyAgreement', context) ) .filter((k) => k.publicKeyHex?.length! > 0) .filter((k) => (args.options?.recipientKids ? args.options?.recipientKids.includes(k.id) : true)) if (keyAgreementKeys.length === 0) { throw new Error(`key_not_found: no key agreement keys found for recipient ${to}`) } // 2.3 get public key bytes and key IDs for supported recipient keys const tempRecipients = keyAgreementKeys .map((pk) => { // FIXME: only supporting X25519 keys for now. Add support for P-256 and P-384 & others const { publicKeyHex, keyType } = extractPublicKeyHex(pk, true) if (keyType === 'X25519') { return { kid: pk.id, publicKeyBytes: hexToBytes(publicKeyHex), keyType: pk.type } } else { debug(`not_supported: key agreement key type ${pk.type} is not supported for encryption`) return null } }) .filter(isDefined) if (tempRecipients.length === 0) { throw new Error(`not_supported: no compatible key agreement keys found for recipient ${to}`) } return tempRecipients } const recipientDIDs = asArray(args.message.to).concat(asArray(args.options?.bcc)) for (const to of recipientDIDs) { recipients.push(...(await computeRecipients(to))) } // 3. create Encrypter for each recipient const encrypters: Encrypter[] = recipients .map((recipient) => { if (options.enc === 'A256GCM') { if (args.packing === 'authcrypt' && (!options.alg || options.alg?.startsWith('ECDH-1PU'))) { if (options.alg?.endsWith('+XC20PKW')) { // FIXME: the didcomm spec actually links to ECDH-1PU(v4) return a256gcmAuthEncrypterEcdh1PuV3x25519WithXC20PKW( recipient.publicKeyBytes, <ECDH>senderECDH, { kid: recipient.kid, }, ) } else if (options?.alg?.endsWith('+A256KW')) { // FIXME: the didcomm spec actually links to ECDH-1PU(v4) return a256gcmAuthEncrypterEcdh1PuV3x25519WithA256KW( recipient.publicKeyBytes, <ECDH>senderECDH, { kid: recipient.kid, }, ) } } else if (args.packing === 'anoncrypt' && (!options.alg || options.alg?.startsWith('ECDH-ES'))) { if (options.alg?.endsWith('+XC20PKW')) { return a256gcmAnonEncrypterX25519WithXC20PKW(recipient.publicKeyBytes, recipient.kid) } else if (options?.alg?.endsWith('+A256KW')) { return a256gcmAnonEncrypterX25519WithA256KW(recipient.publicKeyBytes, recipient.kid) } } } else if (options.enc === 'A256CBC-HS512') { if (args.packing === 'authcrypt' && (!options.alg || options.alg?.startsWith('ECDH-1PU'))) { if (options.alg?.endsWith('+XC20PKW')) { // FIXME: the didcomm spec actually links to ECDH-1PU(v4) return a256cbcHs512AuthEncrypterX25519WithXC20PKW(recipient.publicKeyBytes, <ECDH>senderECDH, { kid: recipient.kid, }) } else if (options?.alg?.endsWith('+A256KW')) { // FIXME: the didcomm spec actually links to ECDH-1PU(v4) return a256cbcHs512AuthEncrypterX25519WithA256KW(recipient.publicKeyBytes, <ECDH>senderECDH, { kid: recipient.kid, }) } } else if (args.packing === 'anoncrypt' && (!options.alg || options.alg?.startsWith('ECDH-ES'))) { if (options.alg?.endsWith('+XC20PKW')) { return a256cbcHs512AnonEncrypterX25519WithXC20PKW(recipient.publicKeyBytes, recipient.kid) } else if (options?.alg?.endsWith('+A256KW')) { return a256cbcHs512AnonEncrypterX25519WithA256KW(recipient.publicKeyBytes, recipient.kid) } } } else if (options.enc === 'XC20P') { if (args.packing === 'authcrypt' && (!options.alg || options.alg?.startsWith('ECDH-1PU'))) { if (options.alg?.endsWith('+XC20PKW')) { // FIXME: the didcomm spec actually links to ECDH-1PU(v4) return xc20pAuthEncrypterEcdh1PuV3x25519WithXC20PKW( recipient.publicKeyBytes, <ECDH>senderECDH, { kid: recipient.kid }, ) } else if (options?.alg?.endsWith('+A256KW')) { // FIXME: the didcomm spec actually links to ECDH-1PU(v4) return xc20pAuthEncrypterEcdh1PuV3x25519WithA256KW(recipient.publicKeyBytes, <ECDH>senderECDH, { kid: recipient.kid, }) } } else if (args.packing === 'anoncrypt' && (!options.alg || options.alg?.startsWith('ECDH-ES'))) { if (options.alg?.endsWith('+XC20PKW')) { return xc20pAnonEncrypterX25519WithXC20PKW(recipient.publicKeyBytes, recipient.kid) } else if (options?.alg?.endsWith('+A256KW')) { return xc20pAnonEncrypterX25519WithA256KW(recipient.publicKeyBytes, recipient.kid) } } } debug( `not_supported: could not create suitable ${args.packing} encrypter for recipient ${recipient.kid} with alg=${options.alg}, enc=${options.enc}`, ) return null }) .filter(isDefined) if (encrypters.length === 0) { throw new Error( `not_supported: could not create suitable ${args.packing} encrypter for recipient ${args?.message?.to} with alg=${options.alg}, enc=${options.enc}`, ) } // 4. createJWE const messageBytes = stringToUtf8Bytes(JSON.stringify(args.message)) const jwe = await createJWE(messageBytes, encrypters, protectedHeader, undefined, true) const message = JSON.stringify(jwe) return { message } } /** {@inheritdoc IDIDComm.getDIDCommMessageMediaType} */ async getDidCommMessageMediaType({ message }: IPackedDIDCommMessage): Promise<DIDCommMessageMediaType> { try { const { mediaType } = this.decodeMessageAndMediaType(message) return mediaType } catch (e) { debug(`Could not parse message as DIDComm v2 message: ${e}`) throw e } } /** {@inheritdoc IDIDComm.unpackDIDCommMessage} */ async unpackDIDCommMessage( args: IUnpackDIDCommMessageArgs, context: IAgentContext<IDIDManager & IKeyManager & IResolver & IMessageHandler>, ): Promise<IUnpackedDIDCommMessage> { const { msgObj, mediaType } = this.decodeMessageAndMediaType(args.message) if (mediaType === DIDCommMessageMediaType.SIGNED) { return this.unpackDIDCommMessageJWS(msgObj as _DIDCommSignedMessage, context, args.resolutionOptions) } else if (mediaType === DIDCommMessageMediaType.PLAIN) { return { message: <IDIDCommMessage>msgObj, metaData: { packing: 'none' } } } else if (mediaType === DIDCommMessageMediaType.ENCRYPTED) { return this.unpackDIDCommMessageJWE({ jwe: msgObj as JWE }, context, args.resolutionOptions) } else { throw Error('not_supported: ' + mediaType) } } private async unpackDIDCommMessageJWS( jws: _DIDCommSignedMessage, context: IAgentContext<IDIDManager & IKeyManager & IResolver>, resolutionOptions?: DIDResolutionOptions, ): Promise<IUnpackedDIDCommMessage> { // TODO: currently only supporting one signature const signatureEncoded: string = isDefined((<_FlattenedJWS>jws).signature) ? (<_FlattenedJWS>jws).signature : (<_GenericJWS>jws).signatures[0]?.signature const headerEncoded = isDefined((<_FlattenedJWS>jws).protected) ? (<_FlattenedJWS>jws).protected : (<_GenericJWS>jws).signatures[0]?.protected if (!isDefined(headerEncoded) || !isDefined(signatureEncoded)) { throw new Error('invalid_argument: could not interpret message as JWS') } const message = <IDIDCommMessage>decodeJoseBlob(jws.payload) const header = decodeJoseBlob(headerEncoded) const sender = parseDidUrl(header.kid)?.did if (!isDefined(sender) || sender !== message.from) { throw new Error('invalid_jws: sender is not a DID or does not match the `kid`') } const senderDoc = await resolveDidOrThrow(sender, context, resolutionOptions) const senderKey = (await context.agent.getDIDComponentById({ didDocument: senderDoc, didUrl: header.kid, section: 'authentication', })) as VerificationMethod const verifiedSenderKey = verifyJWS(`${headerEncoded}.${jws.payload}.${signatureEncoded}`, senderKey) if (isDefined(verifiedSenderKey)) { return { message, metaData: { packing: 'jws' } } } else { throw new Error('invalid_jws: sender `kid` could not be validated as the signer of the message') } } private async unpackDIDCommMessageJWE( { jwe }: { jwe: JWE }, context: IAgentContext<IDIDManager & IKeyManager & IResolver>, resolutionOptions?: DIDResolutionOptions, ): Promise<IUnpackedDIDCommMessage> { // 0 resolve skid to DID doc // - find skid in DID doc and convert to 'X25519' byte array (if type matches) let senderKeyBytes: Uint8Array | null = await extractSenderEncryptionKey(jwe, context, resolutionOptions) // 1. check whether kid is one of my DID URIs // - get recipient DID URIs // - extract DIDs from recipient DID URIs // - match DIDs against locally managed DIDs let managedRecipients = await extractManagedRecipients(jwe, context) // 1.5 distribute protected header to each recipient const protectedHeader = decodeJoseBlob(jwe.protected) managedRecipients = managedRecipients.map((mr) => { mr.recipient.header = { ...protectedHeader, ...mr.recipient.header } return mr }) // 2. get internal IKey instance for each recipient.kid // - resolve locally managed DIDs that match recipients // - filter to the keyAgreementKeys that match the recipient.kid // - match identifier.keys.publicKeyHex to (verificationMethod.publicKey*) // - return a list of `IKey` const localKeys = await mapRecipientsToLocalKeys(managedRecipients, context, resolutionOptions) // 3. for each recipient // if isAuthcrypted? (if senderKey != null) // - construct auth decrypter // else // - construct anon decrypter for (const localKey of localKeys) { let packing: string = 'anoncrypt' let decrypter: Decrypter | null = null const recipientECDH: ECDH = createEcdhWrapper(localKey.localKeyRef, context) // TODO: here's where more algorithms should be supported if (localKey.recipient?.header?.epk?.crv === 'X25519') { if (localKey.recipient?.header?.enc === 'A256GCM') { if (senderKeyBytes && localKey.recipient?.header?.alg?.includes('ECDH-1PU')) { packing = 'authcrypt' if (localKey.recipient?.header?.alg?.endsWith('+A256KW')) { decrypter = a256gcmAuthDecrypterEcdh1PuV3x25519WithA256KW(recipientECDH, senderKeyBytes) } else if (localKey.recipient?.header?.alg?.endsWith('+XC20PKW')) { decrypter = a256gcmAuthDecrypterEcdh1PuV3x25519WithXC20PKW(recipientECDH, senderKeyBytes) } } else { packing = 'anoncrypt' if (localKey.recipient?.header?.alg?.endsWith('+A256KW')) { decrypter = a256gcmAnonDecrypterX25519WithA256KW(recipientECDH) } else if (localKey.recipient?.header?.alg?.endsWith('+XC20PKW')) { decrypter = a256gcmAnonDecrypterX25519WithXC20PKW(recipientECDH) } } } else if (localKey.recipient?.header?.enc === 'A256CBC-HS512') { if (senderKeyBytes && localKey.recipient?.header?.alg?.includes('ECDH-1PU')) { packing = 'authcrypt' if (localKey.recipient?.header?.alg?.endsWith('+A256KW')) { decrypter = a256cbcHs512AuthDecrypterX25519WithA256KW(recipientECDH, senderKeyBytes) } else if (localKey.recipient?.header?.alg?.endsWith('+XC20PKW')) { decrypter = a256cbcHs512AuthDecrypterX25519WithXC20PKW(recipientECDH, senderKeyBytes) } } else { packing = 'anoncrypt' if (localKey.recipient?.header?.alg?.endsWith('+A256KW')) { decrypter = a256cbcHs512AnonDecrypterX25519WithA256KW(recipientECDH) } else if (localKey.recipient?.header?.alg?.endsWith('+XC20PKW')) { decrypter = a256cbcHs512AnonDecrypterX25519WithXC20PKW(recipientECDH) } } } else if (localKey.recipient?.header?.enc === 'XC20P') { if (senderKeyBytes && localKey.recipient?.header?.alg?.includes('ECDH-1PU')) { packing = 'authcrypt' if (localKey.recipient?.header?.alg?.endsWith('+A256KW')) { decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithA256KW(recipientECDH, senderKeyBytes) } else if (localKey.recipient?.header?.alg?.endsWith('+XC20PKW')) { decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXC20PKW(recipientECDH, senderKeyBytes) } } else { packing = 'anoncrypt' if (localKey.recipient?.header?.alg?.endsWith('+A256KW')) { decrypter = xc20pAnonDecrypterX25519WithA256KW(recipientECDH) } else if (localKey.recipient?.header?.alg?.endsWith('+XC20PKW')) { decrypter = xc20pAnonDecrypterX25519WithXC20PKW(recipientECDH) } } } } if (!decrypter) { throw new Error('unable to decrypt DIDComm message with any of the locally managed keys') } // 4. decryptJWE(jwe, decrypter) try { const decryptedBytes = await decryptJWE(jwe, decrypter) const decryptedMsg = bytesToUtf8String(decryptedBytes) const message = JSON.parse(decryptedMsg) return { message, metaData: { packing } } as IUnpackedDIDCommMessage } catch (e) { debug( `unable to decrypt DIDComm msg using ${localKey.localKeyRef} (${localKey.recipient.header.kid})`, ) } } throw new Error('unable to decrypt DIDComm message with any of the locally managed keys') } private decodeMessageAndMediaType(message: string): { msgObj: _DIDCommPlainMessage | _DIDCommSignedMessage | _DIDCommEncryptedMessage mediaType: DIDCommMessageMediaType } { let msgObj if (typeof message === 'string') { try { msgObj = JSON.parse(message) } catch (e) { throw new Error('invalid_argument: unable to parse message as JSON') // TODO: try to interpret as compact serialized JWS / JWM? } } else { msgObj = message } let mediaType: DIDCommMessageMediaType | null = null if ((<_DIDCommPlainMessage>msgObj).typ === DIDCommMessageMediaType.PLAIN) { mediaType = DIDCommMessageMediaType.PLAIN } else if ((<_FlattenedJWS | _DIDCommEncryptedMessage>msgObj).protected) { const protectedHeader = decodeJoseBlob(msgObj.protected) if (protectedHeader.typ === DIDCommMessageMediaType.SIGNED) { mediaType = DIDCommMessageMediaType.SIGNED } else if (protectedHeader.typ === DIDCommMessageMediaType.ENCRYPTED) { mediaType = DIDCommMessageMediaType.ENCRYPTED } else { throw new Error('invalid_argument: unable to determine message type') } } else if ((<_GenericJWS>msgObj).signatures) { mediaType = DIDCommMessageMediaType.SIGNED } else { throw new Error('invalid_argument: unable to determine message type') } return { msgObj, mediaType } } private findPreferredDIDCommService(services: Service[]) { // FIXME: TODO: get preferred service endpoint according to configuration; now defaulting to first service return services[0] } private async wrapDIDCommForwardMessage( recipientDidUrl: string, messageId: string, packedMessageToForward: IPackedDIDCommMessage, routingKey: string, context: IAgentContext<IDIDManager & IKeyManager & IResolver>, ): Promise<IPackedDIDCommMessage> { const splitKey = routingKey.split('#') const shouldUseSpecificKid = splitKey.length > 1 const mediatorDidUrl = splitKey[0] const msgJson = JSON.parse(packedMessageToForward.message) // 1. Create forward message const forwardMessage: IDIDCommMessage = { id: uuidv4(), type: 'https://didcomm.org/routing/2.0/forward', to: [mediatorDidUrl], body: { next: recipientDidUrl, }, attachments: [ { media_type: msgJson?.typ ?? DIDCommMessageMediaType.ENCRYPTED, data: { json: msgJson, }, }, ], } context.agent.emit('DIDCommV2Message-forwarded', { messageId, next: recipientDidUrl, routingKey: routingKey, }) // 2. Pack message for routingKey with anoncrypt if (shouldUseSpecificKid) { return this.packDIDCommMessageJWE( { message: forwardMessage, packing: 'anoncrypt', options: { recipientKids: [routingKey] } }, context, ) } else { return this.packDIDCommMessageJWE({ message: forwardMessage, packing: 'anoncrypt' }, context) } } /** {@inheritdoc IDIDComm.sendDIDCommMessage} */ async sendDIDCommMessage( args: ISendDIDCommMessageArgs, context: IAgentContext<IDIDManager & IKeyManager & IResolver & IMessageHandler>, ): Promise<ISendDIDCommMessageResponse> { const { packedMessage, returnTransportId, recipientDidUrl, messageId } = args if (returnTransportId) { // FIXME: TODO: check if previous message was ok with reusing transport? // if so, retrieve transport from transport manager //transport = this.findDIDCommTransport(returnTransportId) throw new Error(`not_supported: return routes not supported yet`) } const didDoc = await resolveDidOrThrow(recipientDidUrl, context, args.resolutionOptions) function processServiceObject(service: Service) { if (service.type === 'DIDCommMessaging') { return service } else if (service.t === 'dm') { return { type: 'DIDCommMessaging', serviceEndpoint: service.s, accept: service.a, routingKeys: service.r, id: `#dm+` + (service.id ?? service.s), } as Service } } // FIXME: only send the message if the service section either explicitly supports `didcomm/v2`, or no // `accept` property is present. const services = (didDoc.service || []) ?.map((service: any) => { if (Array.isArray(service)) { // This is a workaround for some malformed DID documents that bundle multiple service entries into an array return service.map((s) => { if (typeof s === 'object') { return processServiceObject(s) } }) } else { return processServiceObject(service) } }) .flat() .filter(isDefined) if (!services || services.length === 0) { throw new Error( `not_found: could not find DIDComm Messaging service in DID document for '${recipientDidUrl}'`, ) } // spray all endpoints and transports that match and gather results // TODO: investigate if we should queue the requests and stop when the first transport succeeds const results: (ISendDIDCommMessageResponse | Error)[] = [] for (const service of services) { // serviceEndpoint can be a string, a ServiceEndpoint object, or an array of strings or ServiceEndpoint objects let routingKeys: string[] = [] let serviceEndpointUrl = '' if (typeof service.serviceEndpoint === 'string') { serviceEndpointUrl = service.serviceEndpoint } else if ((service.serviceEndpoint as any).uri) { serviceEndpointUrl = (service.serviceEndpoint as any).uri } else if (Array.isArray(service.serviceEndpoint) && service.serviceEndpoint.length > 0) { if (typeof service.serviceEndpoint[0] === 'string') { serviceEndpointUrl = service.serviceEndpoint[0] } else if (service.serviceEndpoint[0].uri) { serviceEndpointUrl = service.serviceEndpoint[0].uri } } if (typeof service.serviceEndpoint !== 'string') { if ( Array.isArray(service.serviceEndpoint) && service.serviceEndpoint.length > 0 && service.serviceEndpoint[0].routingKeys ) { routingKeys = service.serviceEndpoint[0].routingKeys } else if ((service.serviceEndpoint as any).routingKeys) { routingKeys = (<Exclude<ServiceEndpoint, string>>service.serviceEndpoint).routingKeys } } if (routingKeys.length > 0) { // routingKeys found, wrap forward messages let wrappedMessage: IPackedDIDCommMessage = packedMessage for (let i = routingKeys.length - 1; i >= 0; i--) { const recipient = i >= routingKeys.length - 1 ? recipientDidUrl : routingKeys[i + 1].split('#')[0] wrappedMessage = await this.wrapDIDCommForwardMessage( recipient, messageId, wrappedMessage, routingKeys[i], context, ) } packedMessage.message = wrappedMessage.message } const isServiceEndpointDid = serviceEndpointUrl.startsWith('did:') if (isServiceEndpointDid) { // Final wrapping and send to mediator DID const recipient = routingKeys.length > 0 ? routingKeys[routingKeys.length - 1].split('#')[0] : recipientDidUrl const wrappedMessage = await this.wrapDIDCommForwardMessage( recipient, messageId, packedMessage, serviceEndpointUrl, context, ) try { results.push( await this.sendDIDCommMessage( { packedMessage: wrappedMessage, recipientDidUrl: serviceEndpointUrl, messageId }, context, ), ) } catch (e: any) { debug(e) results.push(e) } } const transports = this.transports.filter( (t) => t.isServiceSupported(service) && (!returnTransportId || t.id === returnTransportId), ) if (!transports || transports.length < 1) { const err = new Error('not_found: no transport type found for service: ' + JSON.stringify(service)) debug(err) results.push(err) } for (const transport of transports) { let response try { response = await transport.send(service, packedMessage.message) if (response.error) { const err = new Error( `Error when sending DIDComm message through transport with id: '${transport.id}': ${response.error}`, ) debug(err) results.push(err) } else { results.push({ transportId: transport.id, returnMessage: response.returnMessage ? { id: '', type: 'unprocessed', raw: response.returnMessage, } : undefined, }) } } catch (e) { const err = new Error( `Cannot send DIDComm message through transport with id: '${transport.id}': ${e}`, ) debug(err) results.push(err) } } } const successful: ISendDIDCommMessageResponse[] = results.filter( (r) => !(r instanceof Error), ) as ISendDIDCommMessageResponse[] if (successful.length > 0) { context.agent.emit('DIDCommV2Message-sent', messageId) for (const response of successful) { if (response.returnMessage) { const returnMessage = await context.agent.handleMessage({ raw: response.returnMessage.raw ?? '', }) return { transportId: response.transportId, returnMessage } } } return successful[0] } else { const errors = results.filter((r) => r instanceof Error) as Error[] const err = new Error('Could not send DIDComm message using any of the attepmpted transports') err.cause = new AggregateError(errors) throw err } } /** {@inheritdoc IDIDComm.sendMessageDIDCommAlpha1} */ async sendMessageDIDCommAlpha1( args: ISendMessageDIDCommAlpha1Args, context: IAgentContext<IDIDManager & IKeyManager & IResolver & IMessageHandler>, ): Promise<IMessage> { const { data, url, headers, save = true } = args debug('Resolving didDoc') const didDoc = (await context.agent.resolveDid({ didUrl: data.to })).didDocument let serviceEndpoint if (url) { serviceEndpoint = url } else { const service = didDoc && didDoc.service && didDoc.service.find((item) => item.type == 'Messaging') serviceEndpoint = service?.serviceEndpoint } if (serviceEndpoint) { try { data.id = data.id || uuidv4() let postPayload = JSON.stringify(data) try { const identifier = await context.agent.didManagerGet({ did: data.from }) const key = identifier.keys.find((k) => k.type === 'Ed25519') if (!key) throw Error('No encryption key') const publicKey = didDoc?.publicKey?.find((item) => item.type == 'Ed25519VerificationKey2018') if (!publicKey?.publicKeyHex) throw Error('Recipient does not have encryption publicKey') postPayload = await context.agent.keyManagerEncryptJWE({ kid: key.kid, to: { type: 'Ed25519', publicKeyHex: publicKey?.publicKeyHex, kid: publicKey?.publicKeyHex, }, data: postPayload, }) debug('Encrypted:', postPayload) } catch (e) {} debug('Sending to %s', serviceEndpoint) const endpointUri = typeof serviceEndpoint === 'string' ? serviceEndpoint : (<Exclude<ServiceEndpoint, string>>serviceEndpoint).uri ?? '' const res = await fetch(endpointUri, { method: 'POST', body: postPayload, headers, }) debug('Status', res.status, res.statusText) if (res.status == 200) { return await context.agent.handleMessage({ raw: JSON.stringify(data), metaData: [{ type: 'DIDComm-sent' }], save, }) } return Promise.reject(new Error('Message not sent')) } catch (e) { return Promise.reject(e) } } else { debug('No Messaging service in didDoc') return Promise.reject(new Error('No service endpoint')) } } }