Skip to content

Commit

Permalink
feat: Create DIDComm JWE with multiple recipients
Browse files Browse the repository at this point in the history
  • Loading branch information
inevolin committed May 21, 2022
1 parent 2374c71 commit 79591c8
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 17 deletions.
69 changes: 69 additions & 0 deletions __tests__/shared/didCommPacking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
})
})
}
14 changes: 14 additions & 0 deletions packages/did-comm/plugin.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
},
"keyRef": {
"type": "string"
},
"options": {
"$ref": "#/components/schemas/IDIDCommOptions"
}
},
"required": [
Expand Down Expand Up @@ -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": {
Expand Down
50 changes: 34 additions & 16 deletions packages/did-comm/src/didcomm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
DIDCommMessageMediaType,
DIDCommMessagePacking,
IDIDCommMessage,
IDIDCommOptions,
IPackedDIDCommMessage,
IUnpackedDIDCommMessage,
} from './types/message-types'
Expand Down Expand Up @@ -98,6 +99,7 @@ export interface IPackDIDCommMessageArgs {
message: IDIDCommMessage
packing: DIDCommMessagePacking
keyRef?: string
options?: IDIDCommOptions
}

/**
Expand Down Expand Up @@ -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<IRecipient[]> {
// 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
Expand Down
8 changes: 7 additions & 1 deletion packages/did-comm/src/types/IDIDComm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions packages/did-comm/src/types/message-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export interface IDIDCommMessage {
from_prior?: string
body: any
}
export interface IDIDCommOptions {
bcc?: string[]
}

/**
* Represents different DIDComm v2 message encapsulation
Expand Down

0 comments on commit 79591c8

Please sign in to comment.