From 06acacb400264d0e7f83fe31935a8ff52593f21f Mon Sep 17 00:00:00 2001 From: Ilya Nevolin <53948000+inevolin@users.noreply.github.com> Date: Sat, 21 May 2022 12:35:59 +0200 Subject: [PATCH] feat: create DIDComm JWE with multiple recipients (#888) --- __tests__/shared/didCommPacking.ts | 69 ++++++++++++++++++++ packages/did-comm/plugin.schema.json | 14 ++++ packages/did-comm/src/didcomm.ts | 50 +++++++++----- packages/did-comm/src/types/IDIDComm.ts | 8 ++- packages/did-comm/src/types/message-types.ts | 3 + 5 files changed, 127 insertions(+), 17 deletions(-) diff --git a/__tests__/shared/didCommPacking.ts b/__tests__/shared/didCommPacking.ts index 71136df6a..7da4106f0 100644 --- a/__tests__/shared/didCommPacking.ts +++ b/__tests__/shared/didCommPacking.ts @@ -129,5 +129,74 @@ export default (testContext: { expect(unpackedMessage.message).toEqual(message) expect(unpackedMessage.metaData).toEqual({ packing: 'authcrypt' }) }) + + it('should pack and unpack message with multiple bcc recipients', async () => { + expect.assertions(2) + + const originator = await agent.didManagerCreate({ + provider: 'did:key', + }) + const beneficiary1 = await agent.didManagerCreate({ + provider: 'did:key', + }) + const beneficiary2 = await agent.didManagerCreate({ + provider: 'did:key', + }) + + const message = { + type: 'test', + from: originator.did, + to: originator.did, + id: 'test', + body: { hello: 'world' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message, + options: { bcc: [beneficiary1.did, beneficiary2.did] }, + }) + + // delete originator's key from local KMS + await agent.didManagerDelete({ did: originator.did }) + + // bcc'd beneficiaries should be able to decrypt + const unpackedMessage = await agent.unpackDIDCommMessage(packedMessage) + expect(unpackedMessage.message).toEqual(message) + expect(unpackedMessage.metaData).toEqual({ packing: 'authcrypt' }) + }) + + it('should pack and fail unpacking message with multiple bcc recipients', async () => { + const originator = await agent.didManagerCreate({ + provider: 'did:key', + }) + const beneficiary1 = await agent.didManagerCreate({ + provider: 'did:key', + }) + const beneficiary2 = await agent.didManagerCreate({ + provider: 'did:key', + }) + + const message = { + type: 'test', + from: originator.did, + to: originator.did, + id: 'test', + body: { hello: 'world' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message, + options: { bcc: [beneficiary1.did, beneficiary2.did] }, + }) + + // delete all keys + await agent.didManagerDelete({ did: originator.did }) + await agent.didManagerDelete({ did: beneficiary1.did }) + await agent.didManagerDelete({ did: beneficiary2.did }) + + await expect(agent.unpackDIDCommMessage(packedMessage)).rejects.toThrowError( + 'unable to decrypt DIDComm message with any of the locally managed keys', + ) + }) }) } diff --git a/packages/did-comm/plugin.schema.json b/packages/did-comm/plugin.schema.json index a4416308c..4206354b4 100644 --- a/packages/did-comm/plugin.schema.json +++ b/packages/did-comm/plugin.schema.json @@ -34,6 +34,9 @@ }, "keyRef": { "type": "string" + }, + "options": { + "$ref": "#/components/schemas/IDIDCommOptions" } }, "required": [ @@ -97,6 +100,17 @@ ], "description": "The possible types of message packing.\n\n`authcrypt`, `anoncrypt`, `anoncrypt+authcrypt`, and `anoncrypt+jws` will produce `DIDCommMessageMediaType.ENCRYPTED` messages.\n\n`jws` will produce `DIDCommMessageMediaType.SIGNED` messages.\n\n`none` will produce `DIDCommMessageMediaType.PLAIN` messages." }, + "IDIDCommOptions": { + "type": "object", + "properties": { + "bcc": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "ISendDIDCommMessageArgs": { "type": "object", "properties": { diff --git a/packages/did-comm/src/didcomm.ts b/packages/did-comm/src/didcomm.ts index 8b7cbc110..d566dc0bb 100644 --- a/packages/did-comm/src/didcomm.ts +++ b/packages/did-comm/src/didcomm.ts @@ -51,6 +51,7 @@ import { DIDCommMessageMediaType, DIDCommMessagePacking, IDIDCommMessage, + IDIDCommOptions, IPackedDIDCommMessage, IUnpackedDIDCommMessage, } from './types/message-types' @@ -98,6 +99,7 @@ export interface IPackDIDCommMessageArgs { message: IDIDCommMessage packing: DIDCommMessagePacking keyRef?: string + options?: IDIDCommOptions } /** @@ -270,27 +272,43 @@ export class DIDComm implements IAgentPlugin { } } - // 2. resolve DID for args.message.to - const didDocument: DIDDocument = await resolveDidOrThrow(args?.message?.to, context) + // 2: compute recipients + interface IRecipient { + kid: string + publicKeyBytes: Uint8Array + } + let recipients: IRecipient[] = [] + + async function computeRecipients(to: string): Promise { + // 2.1 resolve DID for "to" + const didDocument: DIDDocument = await resolveDidOrThrow(to, context) + + // 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) + + if (keyAgreementKeys.length === 0) { + throw new Error(`key_not_found: no key agreement keys found for recipient ${to}`) + } - // 2.1 extract all recipient key agreement keys and normalize them - const keyAgreementKeys: _NormalizedVerificationMethod[] = ( - await dereferenceDidKeys(didDocument, 'keyAgreement', context) - ).filter((k) => k.publicKeyHex?.length! > 0) + // 2.3 get public key bytes and key IDs for supported recipient keys + const tempRecipients = keyAgreementKeys + .map((pk) => ({ kid: pk.id, publicKeyBytes: u8a.fromString(pk.publicKeyHex!, 'base16') })) + .filter(isDefined) - if (keyAgreementKeys.length === 0) { - throw new Error(`key_not_found: no key agreement keys found for recipient ${args?.message?.to}`) + if (tempRecipients.length === 0) { + throw new Error(`not_supported: no compatible key agreement keys found for recipient ${to}`) + } + return tempRecipients } - // 2.2 get public key bytes and key IDs for supported recipient keys - const recipients: { kid: string; publicKeyBytes: Uint8Array }[] = keyAgreementKeys - .map((pk) => ({ kid: pk.id, publicKeyBytes: u8a.fromString(pk.publicKeyHex!, 'base16') })) - .filter(isDefined) + // add primary recipient + recipients.push(...(await computeRecipients(args.message.to))) - if (recipients.length === 0) { - throw new Error( - `not_supported: no compatible key agreement keys found for recipient ${args?.message?.to}`, - ) + // add bcc recipients (optional) + for (const to of args.options?.bcc || []) { + recipients.push(...(await computeRecipients(to))) } // 3. create Encrypter for each recipient diff --git a/packages/did-comm/src/types/IDIDComm.ts b/packages/did-comm/src/types/IDIDComm.ts index 6909f7a2b..d835b764e 100644 --- a/packages/did-comm/src/types/IDIDComm.ts +++ b/packages/did-comm/src/types/IDIDComm.ts @@ -13,7 +13,12 @@ import { ISendMessageDIDCommAlpha1Args, IUnpackDIDCommMessageArgs, } from '../didcomm' -import { DIDCommMessageMediaType, IPackedDIDCommMessage, IUnpackedDIDCommMessage } from './message-types' +import { + DIDCommMessageMediaType, + IDIDCommOptions, + IPackedDIDCommMessage, + IUnpackedDIDCommMessage, +} from './message-types' /** * DID Comm plugin interface for {@link @veramo/core#Agent} @@ -39,6 +44,7 @@ export interface IDIDComm extends IPluginMethodMap { * * args.packing - {@link DIDCommMessagePacking} - the packing method * * args.keyRef - Optional - string - either an `id` of a {@link did-resolver#VerificationMethod} * `kid` of a {@link @veramo/core#IKey} that will be used when `packing` is `jws` or `authcrypt`. + * * args.options - {@link IDIDCommOptions} - optional options * * @param context - This method requires an agent that also has {@link @veramo/core#IDIDManager}, * {@link @veramo/core#IKeyManager} and {@link @veramo/core#IResolver} plugins in use. diff --git a/packages/did-comm/src/types/message-types.ts b/packages/did-comm/src/types/message-types.ts index 04cd0cc83..915a3db05 100644 --- a/packages/did-comm/src/types/message-types.ts +++ b/packages/did-comm/src/types/message-types.ts @@ -17,6 +17,9 @@ export interface IDIDCommMessage { from_prior?: string body: any } +export interface IDIDCommOptions { + bcc?: string[] +} /** * Represents different DIDComm v2 message encapsulation