@veramo/did-comm
Version:
Veramo messaging plugin implementing DIDComm v2.
741 lines • 38 kB
JavaScript
import { createJWE, decryptJWE, verifyJWS, } from 'did-jwt';
import { parse as parseDidUrl, } 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 { asArray, bytesToUtf8String, decodeJoseBlob, dereferenceDidKeys, encodeJoseBlob, extractPublicKeyHex, hexToBytes, isDefined, mapIdentifierKeysToDoc, resolveDidOrThrow, stringToUtf8Bytes, } from '@veramo/utils';
import Debug from 'debug';
import { DIDCommHttpTransport } from './transports/transports.js';
import { DIDCommMessageMediaType, } from './types/message-types.js';
const debug = Debug('veramo:did-comm:action-handler');
/**
* 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 {
transports;
/** Plugin methods */
methods;
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()] } = {}) {
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, context) {
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`);
}
}
async packDIDCommMessageJWS(args, context) {
const message = args.message;
let keyRef = args.keyRef;
let kid;
// check that the message has from field that is managed
let managedSender;
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;
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 = 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) };
}
async packDIDCommMessageJWE(args, context) {
// 1. check if args.packing requires authentication and map sender key to skid
let senderECDH = null;
let keyRef = args.keyRef;
let protectedHeader = {
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 = await context.agent.didManagerGet({ did: args?.message?.from });
// 1.2 match key agreement keys from DID to managed keys
const senderKeys = 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 };
let recipients = [];
async function computeRecipients(to, resolutionOptions) {
// 2.1 resolve DID for "to"
const didDocument = await resolveDidOrThrow(to, context, resolutionOptions);
// 2.2 extract all recipient key agreement keys and normalize them
const keyAgreementKeys = (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 = 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, senderECDH, {
kid: recipient.kid,
});
}
else if (options?.alg?.endsWith('+A256KW')) {
// FIXME: the didcomm spec actually links to ECDH-1PU(v4)
return a256gcmAuthEncrypterEcdh1PuV3x25519WithA256KW(recipient.publicKeyBytes, 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, senderECDH, {
kid: recipient.kid,
});
}
else if (options?.alg?.endsWith('+A256KW')) {
// FIXME: the didcomm spec actually links to ECDH-1PU(v4)
return a256cbcHs512AuthEncrypterX25519WithA256KW(recipient.publicKeyBytes, 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, senderECDH, { kid: recipient.kid });
}
else if (options?.alg?.endsWith('+A256KW')) {
// FIXME: the didcomm spec actually links to ECDH-1PU(v4)
return xc20pAuthEncrypterEcdh1PuV3x25519WithA256KW(recipient.publicKeyBytes, 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 }) {
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, context) {
const { msgObj, mediaType } = this.decodeMessageAndMediaType(args.message);
if (mediaType === DIDCommMessageMediaType.SIGNED) {
return this.unpackDIDCommMessageJWS(msgObj, context, args.resolutionOptions);
}
else if (mediaType === DIDCommMessageMediaType.PLAIN) {
return { message: msgObj, metaData: { packing: 'none' } };
}
else if (mediaType === DIDCommMessageMediaType.ENCRYPTED) {
return this.unpackDIDCommMessageJWE({ jwe: msgObj }, context, args.resolutionOptions);
}
else {
throw Error('not_supported: ' + mediaType);
}
}
async unpackDIDCommMessageJWS(jws, context, resolutionOptions) {
// TODO: currently only supporting one signature
const signatureEncoded = isDefined(jws.signature)
? jws.signature
: jws.signatures[0]?.signature;
const headerEncoded = isDefined(jws.protected)
? jws.protected
: jws.signatures[0]?.protected;
if (!isDefined(headerEncoded) || !isDefined(signatureEncoded)) {
throw new Error('invalid_argument: could not interpret message as JWS');
}
const message = 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',
}));
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');
}
}
async unpackDIDCommMessageJWE({ jwe }, context, resolutionOptions) {
// 0 resolve skid to DID doc
// - find skid in DID doc and convert to 'X25519' byte array (if type matches)
let senderKeyBytes = 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 = 'anoncrypt';
let decrypter = null;
const recipientECDH = 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 } };
}
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');
}
decodeMessageAndMediaType(message) {
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 = null;
if (msgObj.typ === DIDCommMessageMediaType.PLAIN) {
mediaType = DIDCommMessageMediaType.PLAIN;
}
else if (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 (msgObj.signatures) {
mediaType = DIDCommMessageMediaType.SIGNED;
}
else {
throw new Error('invalid_argument: unable to determine message type');
}
return { msgObj, mediaType };
}
findPreferredDIDCommService(services) {
// FIXME: TODO: get preferred service endpoint according to configuration; now defaulting to first service
return services[0];
}
async wrapDIDCommForwardMessage(recipientDidUrl, messageId, packedMessageToForward, routingKey, context) {
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 = {
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, context) {
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) {
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),
};
}
}
// 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) => {
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 = [];
for (const service of services) {
// serviceEndpoint can be a string, a ServiceEndpoint object, or an array of strings or ServiceEndpoint objects
let routingKeys = [];
let serviceEndpointUrl = '';
if (typeof service.serviceEndpoint === 'string') {
serviceEndpointUrl = service.serviceEndpoint;
}
else if (service.serviceEndpoint.uri) {
serviceEndpointUrl = service.serviceEndpoint.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.routingKeys) {
routingKeys = service.serviceEndpoint.routingKeys;
}
}
if (routingKeys.length > 0) {
// routingKeys found, wrap forward messages
let wrappedMessage = 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) {
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 = results.filter((r) => !(r instanceof Error));
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);
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, context) {
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
: 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'));
}
}
}
//# sourceMappingURL=didcomm.js.map