diff --git a/demo/README.md b/demo/README.md index 93f14ee99f..96911eca80 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,15 +1,18 @@

DEMO

-This is the Aries Framework Javascript demo. Walk through the AFJ flow yourself together with agents Alice and Faber. +This is the Aries Framework Javascript demos. Walk through the available AFJ flows yourself together with agents Alice and Faber. -Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof. +## Flows -## Features - -- ✅ Creating a connection -- ✅ Offering a credential -- ✅ Requesting a proof -- ✅ Sending basic messages +- `Main` - General flow: Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof. + - ✅ Creating a connection + - ✅ Offering a credential + - ✅ Requesting a proof + - ✅ Sending basic messages +- `DidComm V2` - [DidComm v2 massaging](https://identity.foundation/didcomm-messaging/spec/) usage. In contrast to the `Main` this demo provides functionality limited to sending `ping` message after accepting out-of-band invitation from the invitee. + - ✅ Creating a connection + - ✅ Ping + > Integration of DidComm V2 protocols is currently under development! In the future, it will cover the same features as the `Main` flow. ## Getting Started @@ -19,7 +22,7 @@ In order to use Aries Framework JavaScript some platform specific dependencies a - [NodeJS](https://aries.js.org/guides/getting-started/installation/nodejs) -### Run the demo +### Preparation These are the steps for running the AFJ demo: @@ -41,6 +44,10 @@ Install the project in one of the terminals: yarn install ``` +### Run the demo + +#### Main demo + In the left terminal run Alice: ```sh @@ -53,7 +60,7 @@ In the right terminal run Faber: yarn faber ``` -### Usage +##### Usage To set up a connection: @@ -87,3 +94,40 @@ Exit: Restart: - Select 'restart', to shutdown the current agent and start a new one + +#### DidComm V2 demo + +In the left terminal run Alice: + +```sh +yarn alice-didcommv2 +``` + +In the right terminal run Faber: + +```sh +yarn faber-didcommv2 +``` + +##### Usage + +To set up a connection: + +- Select 'receive connection invitation' in Alice and 'create connection invitation' in Faber +- Faber will print a invitation link which you then copy and paste to Alice +- You have now set up a connection! + +To send a ping message: + +- Establish the connection first +- Select 'ping' in the Alice Agent +- Message sent! +- Faber Agent should print notification about received `ping` message and response with `ping response` message back to Alice. + +Exit: + +- Select 'exit' to shutdown the agent. + +Restart: + +- Select 'restart', to shutdown the current agent and start a new one diff --git a/demo/package.json b/demo/package.json index 16ae0447fe..41b0b4c4f9 100644 --- a/demo/package.json +++ b/demo/package.json @@ -9,8 +9,10 @@ }, "license": "Apache-2.0", "scripts": { - "alice": "ts-node src/AliceInquirer.ts", - "faber": "ts-node src/FaberInquirer.ts", + "alice": "ts-node src/main/AliceInquirer.ts", + "faber": "ts-node src/main/FaberInquirer.ts", + "alice-didcommv2": "ts-node src/didcomm-v2/AliceInquirer.ts", + "faber-didcommv2": "ts-node src/didcomm-v2/FaberInquirer.ts", "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts deleted file mode 100644 index 86b09fc6a1..0000000000 --- a/demo/src/Alice.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { ConnectionRecord, CredentialExchangeRecord, ProofExchangeRecord } from '@aries-framework/core' - -import { BaseAgent } from './BaseAgent' -import { greenText, Output, redText } from './OutputClass' - -export class Alice extends BaseAgent { - public connected: boolean - public connectionRecordFaberId?: string - - public constructor(port: number, name: string) { - super({ port, name, useLegacyIndySdk: true }) - this.connected = false - } - - public static async build(): Promise { - const alice = new Alice(9000, 'alice') - await alice.initializeAgent() - return alice - } - - private async getConnectionRecord() { - if (!this.connectionRecordFaberId) { - throw Error(redText(Output.MissingConnectionRecord)) - } - return await this.agent.connections.getById(this.connectionRecordFaberId) - } - - private async receiveConnectionRequest(invitationUrl: string) { - const { connectionRecord } = await this.agent.oob.receiveInvitationFromUrl(invitationUrl) - if (!connectionRecord) { - throw new Error(redText(Output.NoConnectionRecordFromOutOfBand)) - } - return connectionRecord - } - - private async waitForConnection(connectionRecord: ConnectionRecord) { - connectionRecord = await this.agent.connections.returnWhenIsConnected(connectionRecord.id) - this.connected = true - console.log(greenText(Output.ConnectionEstablished)) - return connectionRecord.id - } - - public async acceptConnection(invitation_url: string) { - const connectionRecord = await this.receiveConnectionRequest(invitation_url) - this.connectionRecordFaberId = await this.waitForConnection(connectionRecord) - } - - public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { - const linkSecretIds = await this.agent.modules.anoncreds.getLinkSecretIds() - if (linkSecretIds.length === 0) { - await this.agent.modules.anoncreds.createLinkSecret() - } - - await this.agent.credentials.acceptOffer({ - credentialRecordId: credentialRecord.id, - }) - } - - public async acceptProofRequest(proofRecord: ProofExchangeRecord) { - const requestedCredentials = await this.agent.proofs.selectCredentialsForRequest({ - proofRecordId: proofRecord.id, - }) - - await this.agent.proofs.acceptRequest({ - proofRecordId: proofRecord.id, - proofFormats: requestedCredentials.proofFormats, - }) - console.log(greenText('\nProof request accepted!\n')) - } - - public async sendMessage(message: string) { - const connectionRecord = await this.getConnectionRecord() - await this.agent.basicMessages.sendMessage(connectionRecord.id, message) - } - - public async ping() { - const connectionRecord = await this.getConnectionRecord() - await this.agent.connections.sendPing(connectionRecord.id, {}) - } - - public async exit() { - console.log(Output.Exit) - await this.agent.shutdown() - process.exit(0) - } - - public async restart() { - await this.agent.shutdown() - } -} diff --git a/demo/src/BaseAlice.ts b/demo/src/BaseAlice.ts new file mode 100644 index 0000000000..113218ac55 --- /dev/null +++ b/demo/src/BaseAlice.ts @@ -0,0 +1,51 @@ +import type { ConnectionRecord } from '@aries-framework/core' + +import { BaseAgent } from './BaseAgent' +import { greenText, Output, redText } from './OutputClass' + +export class BaseAlice extends BaseAgent { + public connected: boolean + public connectionRecordFaberId?: string + + public constructor(port: number, name: string) { + super({ port, name, useLegacyIndySdk: true }) + this.connected = false + } + + protected async getConnectionRecord() { + if (!this.connectionRecordFaberId) { + throw Error(redText(Output.MissingConnectionRecord)) + } + return await this.agent.connections.getById(this.connectionRecordFaberId) + } + + protected async receiveConnectionRequest(invitationUrl: string) { + const { connectionRecord } = await this.agent.oob.receiveInvitationFromUrl(invitationUrl) + if (!connectionRecord) { + throw new Error(redText(Output.NoConnectionRecordFromOutOfBand)) + } + return connectionRecord + } + + protected async waitForConnection(connectionRecord: ConnectionRecord) { + connectionRecord = await this.agent.connections.returnWhenIsConnected(connectionRecord.id, { timeoutMs: 200000 }) + this.connected = true + console.log(greenText(Output.ConnectionEstablished)) + return connectionRecord.id + } + + public async acceptConnection(invitation_url: string) { + const connectionRecord = await this.receiveConnectionRequest(invitation_url) + this.connectionRecordFaberId = await this.waitForConnection(connectionRecord) + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo/src/BaseFaber.ts b/demo/src/BaseFaber.ts new file mode 100644 index 0000000000..3e3e4bc6dd --- /dev/null +++ b/demo/src/BaseFaber.ts @@ -0,0 +1,87 @@ +import type { RegisterCredentialDefinitionReturnStateFinished } from '@aries-framework/anoncreds' +import type BottomBar from 'inquirer/lib/ui/bottom-bar' + +import { KeyType, TypedArrayEncoder } from '@aries-framework/core' +import { ui } from 'inquirer' + +import { BaseAgent, indyNetworkConfig } from './BaseAgent' +import { Output, redText } from './OutputClass' + +export enum RegistryOptions { + indy = 'did:indy', + cheqd = 'did:cheqd', +} + +export class BaseFaber extends BaseAgent { + public outOfBandId?: string + public credentialDefinition?: RegisterCredentialDefinitionReturnStateFinished + public anonCredsIssuerId?: string + public ui: BottomBar + + public constructor(port: number, name: string) { + super({ port, name, useLegacyIndySdk: true }) + this.ui = new ui.BottomBar() + } + + public static async build(): Promise { + const faber = new BaseFaber(9001, 'faber') + await faber.initializeAgent() + return faber + } + + public async importDid(registry: string) { + // NOTE: we assume the did is already registered on the ledger, we just store the private key in the wallet + // and store the existing did in the wallet + // indy did is based on private key (seed) + const unqualifiedIndyDid = '2jEvRuKmfBJTRa7QowDpNN' + const cheqdDid = 'did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675' + const indyDid = `did:indy:${indyNetworkConfig.indyNamespace}:${unqualifiedIndyDid}` + + const did = registry === RegistryOptions.indy ? indyDid : cheqdDid + await this.agent.dids.import({ + did, + overwrite: true, + privateKeys: [ + { + keyType: KeyType.Ed25519, + privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'), + }, + ], + }) + this.anonCredsIssuerId = did + } + + protected async getConnectionRecord() { + if (!this.outOfBandId) { + throw Error(redText(Output.MissingConnectionRecord)) + } + + const [connection] = await this.agent.connections.findAllByOutOfBandId(this.outOfBandId) + + if (!connection) { + throw Error(redText(Output.MissingConnectionRecord)) + } + + return connection + } + + protected async printConnectionInvite(version: 'v1' | 'v2') { + const outOfBandRecord = await this.agent.oob.createInvitation({ version }) + this.outOfBandId = outOfBandRecord.id + + const outOfBandInvitation = outOfBandRecord.outOfBandInvitation || outOfBandRecord.v2OutOfBandInvitation + if (outOfBandInvitation) { + console.log(Output.ConnectionLink, outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` }), '\n') + } + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo/src/BaseListener.ts b/demo/src/BaseListener.ts new file mode 100644 index 0000000000..9a1c23a404 --- /dev/null +++ b/demo/src/BaseListener.ts @@ -0,0 +1,21 @@ +import type BottomBar from 'inquirer/lib/ui/bottom-bar' + +import { ui } from 'inquirer' + +export class BaseListener { + public on: boolean + protected ui: BottomBar + + public constructor() { + this.on = false + this.ui = new ui.BottomBar() + } + + protected turnListenerOn() { + this.on = true + } + + protected turnListenerOff() { + this.on = false + } +} diff --git a/demo/src/didcomm-v2/Alice.ts b/demo/src/didcomm-v2/Alice.ts new file mode 100644 index 0000000000..7b2b70a158 --- /dev/null +++ b/demo/src/didcomm-v2/Alice.ts @@ -0,0 +1,18 @@ +import { BaseAlice } from '../BaseAlice' + +export class Alice extends BaseAlice { + public constructor(port: number, name: string) { + super(port, name) + } + + public static async build(): Promise { + const alice = new Alice(9000, 'alice') + await alice.initializeAgent() + return alice + } + + public async ping() { + const connectionRecord = await this.getConnectionRecord() + await this.agent.connections.sendPing(connectionRecord.id, {}) + } +} diff --git a/demo/src/didcomm-v2/AliceInquirer.ts b/demo/src/didcomm-v2/AliceInquirer.ts new file mode 100644 index 0000000000..5d0dcf901b --- /dev/null +++ b/demo/src/didcomm-v2/AliceInquirer.ts @@ -0,0 +1,103 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from '../BaseInquirer' +import { Title } from '../OutputClass' + +import { Alice } from './Alice' +import { Listener } from './Listener' + +export const runAlice = async () => { + clear() + console.log(textSync('Alice', { horizontalLayout: 'full' })) + const alice = await AliceInquirer.build() + await alice.processAnswer() +} + +enum PromptOptions { + ReceiveConnectionUrl = 'Receive connection invitation', + Ping = 'Ping other party', + Exit = 'Exit', + Restart = 'Restart', +} + +export class AliceInquirer extends BaseInquirer { + public alice: Alice + public promptOptionsString: string[] + public listener: Listener + + public constructor(alice: Alice) { + super() + this.alice = alice + this.listener = new Listener() + this.promptOptionsString = Object.values(PromptOptions) + this.listener.pingListener(this.alice.agent, this.alice.name) + } + + public static async build(): Promise { + const alice = await Alice.build() + return new AliceInquirer(alice) + } + + private async getPromptChoice() { + if (this.alice.connectionRecordFaberId) return prompt([this.inquireOptions(this.promptOptionsString)]) + + const reducedOption = [PromptOptions.ReceiveConnectionUrl, PromptOptions.Exit, PromptOptions.Restart] + return prompt([this.inquireOptions(reducedOption)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + if (this.listener.on) return + + switch (choice.options) { + case PromptOptions.ReceiveConnectionUrl: + await this.connection() + break + case PromptOptions.Ping: + await this.ping() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async connection() { + const title = Title.InvitationTitle + const getUrl = await prompt([this.inquireInput(title)]) + await this.alice.acceptConnection(getUrl.input) + if (!this.alice.connected) return + } + + public async ping() { + await this.alice.ping() + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.restart() + await runAlice() + } + } +} + +void runAlice() diff --git a/demo/src/didcomm-v2/Faber.ts b/demo/src/didcomm-v2/Faber.ts new file mode 100644 index 0000000000..9383735026 --- /dev/null +++ b/demo/src/didcomm-v2/Faber.ts @@ -0,0 +1,17 @@ +import { BaseFaber } from '../BaseFaber' + +export class Faber extends BaseFaber { + public constructor(port: number, name: string) { + super(port, name) + } + + public static async build(): Promise { + const faber = new Faber(9001, 'faber') + await faber.initializeAgent() + return faber + } + + public async setupConnection() { + await this.printConnectionInvite('v2') + } +} diff --git a/demo/src/didcomm-v2/FaberInquirer.ts b/demo/src/didcomm-v2/FaberInquirer.ts new file mode 100644 index 0000000000..5bd4192112 --- /dev/null +++ b/demo/src/didcomm-v2/FaberInquirer.ts @@ -0,0 +1,101 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from '../BaseInquirer' +import { Title } from '../OutputClass' + +import { Faber } from './Faber' +import { Listener } from './Listener' + +export const runFaber = async () => { + clear() + console.log(textSync('Faber', { horizontalLayout: 'full' })) + const faber = await FaberInquirer.build() + await faber.processAnswer() +} + +enum PromptOptions { + CreateConnection = 'Create connection invitation', + Exit = 'Exit', + Restart = 'Restart', +} + +export class FaberInquirer extends BaseInquirer { + public faber: Faber + public promptOptionsString: string[] + public listener: Listener + + public constructor(faber: Faber) { + super() + this.faber = faber + this.listener = new Listener() + this.promptOptionsString = Object.values(PromptOptions) + this.listener.pingListener(this.faber.agent, this.faber.name) + } + + public static async build(): Promise { + const faber = await Faber.build() + return new FaberInquirer(faber) + } + + private async getPromptChoice() { + if (this.faber.outOfBandId) return prompt([this.inquireOptions(this.promptOptionsString)]) + + const reducedOption = [PromptOptions.CreateConnection, PromptOptions.Exit, PromptOptions.Restart] + return prompt([this.inquireOptions(reducedOption)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + if (this.listener.on) return + + switch (choice.options) { + case PromptOptions.CreateConnection: + await this.connection() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async connection() { + await this.faber.setupConnection() + } + + public async exitUseCase(title: string) { + const confirm = await prompt([this.inquireConfirmation(title)]) + if (confirm.options === ConfirmOptions.No) { + return false + } else if (confirm.options === ConfirmOptions.Yes) { + return true + } + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.faber.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.faber.restart() + await runFaber() + } + } +} + +void runFaber() diff --git a/demo/src/didcomm-v2/Listener.ts b/demo/src/didcomm-v2/Listener.ts new file mode 100644 index 0000000000..7db0bcb384 --- /dev/null +++ b/demo/src/didcomm-v2/Listener.ts @@ -0,0 +1,26 @@ +import type { Agent, V2TrustPingReceivedEvent, V2TrustPingResponseReceivedEvent } from '@aries-framework/core' + +import { TrustPingEventTypes } from '@aries-framework/core' + +import { BaseListener } from '../BaseListener' +import { purpleText } from '../OutputClass' + +export class Listener extends BaseListener { + public constructor() { + super() + } + + public pingListener(agent: Agent, name: string) { + agent.events.on(TrustPingEventTypes.V2TrustPingReceivedEvent, async (event: V2TrustPingReceivedEvent) => { + this.ui.updateBottomBar(purpleText(`\n${name} received ping message from ${event.payload.message.from}\n`)) + }) + agent.events.on( + TrustPingEventTypes.V2TrustPingResponseReceivedEvent, + async (event: V2TrustPingResponseReceivedEvent) => { + this.ui.updateBottomBar( + purpleText(`\n${name} received ping response message from ${event.payload.message.from}\n`) + ) + } + ) + } +} diff --git a/demo/src/main/Alice.ts b/demo/src/main/Alice.ts new file mode 100644 index 0000000000..a6326363a5 --- /dev/null +++ b/demo/src/main/Alice.ts @@ -0,0 +1,44 @@ +import type { CredentialExchangeRecord, ProofExchangeRecord } from '@aries-framework/core' + +import { BaseAlice } from '../BaseAlice' +import { greenText } from '../OutputClass' + +export class Alice extends BaseAlice { + public constructor(port: number, name: string) { + super(port, name) + } + + public static async build(): Promise { + const alice = new Alice(9000, 'alice') + await alice.initializeAgent() + return alice + } + + public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { + const linkSecretIds = await this.agent.modules.anoncreds.getLinkSecretIds() + if (linkSecretIds.length === 0) { + await this.agent.modules.anoncreds.createLinkSecret() + } + + await this.agent.credentials.acceptOffer({ + credentialRecordId: credentialRecord.id, + }) + } + + public async acceptProofRequest(proofRecord: ProofExchangeRecord) { + const requestedCredentials = await this.agent.proofs.selectCredentialsForRequest({ + proofRecordId: proofRecord.id, + }) + + await this.agent.proofs.acceptRequest({ + proofRecordId: proofRecord.id, + proofFormats: requestedCredentials.proofFormats, + }) + console.log(greenText('\nProof request accepted!\n')) + } + + public async sendMessage(message: string) { + const connectionRecord = await this.getConnectionRecord() + await this.agent.basicMessages.sendMessage(connectionRecord.id, message) + } +} diff --git a/demo/src/AliceInquirer.ts b/demo/src/main/AliceInquirer.ts similarity index 92% rename from demo/src/AliceInquirer.ts rename to demo/src/main/AliceInquirer.ts index 9c68af67c8..ec0d4b4a70 100644 --- a/demo/src/AliceInquirer.ts +++ b/demo/src/main/AliceInquirer.ts @@ -4,10 +4,11 @@ import { clear } from 'console' import { textSync } from 'figlet' import { prompt } from 'inquirer' +import { BaseInquirer, ConfirmOptions } from '../BaseInquirer' +import { Title } from '../OutputClass' + import { Alice } from './Alice' -import { BaseInquirer, ConfirmOptions } from './BaseInquirer' import { Listener } from './Listener' -import { Title } from './OutputClass' export const runAlice = async () => { clear() @@ -19,7 +20,6 @@ export const runAlice = async () => { enum PromptOptions { ReceiveConnectionUrl = 'Receive connection invitation', SendMessage = 'Send message', - Ping = 'Ping other party', Exit = 'Exit', Restart = 'Restart', } @@ -35,7 +35,6 @@ export class AliceInquirer extends BaseInquirer { this.listener = new Listener() this.promptOptionsString = Object.values(PromptOptions) this.listener.messageListener(this.alice.agent, this.alice.name) - this.listener.pingListener(this.alice.agent, this.alice.name) } public static async build(): Promise { @@ -61,9 +60,6 @@ export class AliceInquirer extends BaseInquirer { case PromptOptions.SendMessage: await this.message() break - case PromptOptions.Ping: - await this.ping() - break case PromptOptions.Exit: await this.exit() break @@ -109,10 +105,6 @@ export class AliceInquirer extends BaseInquirer { await this.alice.sendMessage(message) } - public async ping() { - await this.alice.ping() - } - public async exit() { const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) if (confirm.options === ConfirmOptions.No) { diff --git a/demo/src/Faber.ts b/demo/src/main/Faber.ts similarity index 70% rename from demo/src/Faber.ts rename to demo/src/main/Faber.ts index a4a0d46e9e..e927f4097e 100644 --- a/demo/src/Faber.ts +++ b/demo/src/main/Faber.ts @@ -1,27 +1,13 @@ -import type { RegisterCredentialDefinitionReturnStateFinished } from '@aries-framework/anoncreds' import type { ConnectionRecord, ConnectionStateChangedEvent } from '@aries-framework/core' -import type BottomBar from 'inquirer/lib/ui/bottom-bar' -import { KeyType, TypedArrayEncoder, utils, ConnectionEventTypes } from '@aries-framework/core' -import { ui } from 'inquirer' +import { ConnectionEventTypes, utils } from '@aries-framework/core' -import { BaseAgent, indyNetworkConfig } from './BaseAgent' -import { Color, greenText, Output, purpleText, redText } from './OutputClass' - -export enum RegistryOptions { - indy = 'did:indy', - cheqd = 'did:cheqd', -} - -export class Faber extends BaseAgent { - public outOfBandId?: string - public credentialDefinition?: RegisterCredentialDefinitionReturnStateFinished - public anonCredsIssuerId?: string - public ui: BottomBar +import { BaseFaber } from '../BaseFaber' +import { Color, greenText, Output, purpleText, redText } from '../OutputClass' +export class Faber extends BaseFaber { public constructor(port: number, name: string) { - super({ port, name, useLegacyIndySdk: true }) - this.ui = new ui.BottomBar() + super(port, name) } public static async build(): Promise { @@ -30,53 +16,7 @@ export class Faber extends BaseAgent { return faber } - public async importDid(registry: string) { - // NOTE: we assume the did is already registered on the ledger, we just store the private key in the wallet - // and store the existing did in the wallet - // indy did is based on private key (seed) - const unqualifiedIndyDid = '2jEvRuKmfBJTRa7QowDpNN' - const cheqdDid = 'did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675' - const indyDid = `did:indy:${indyNetworkConfig.indyNamespace}:${unqualifiedIndyDid}` - - const did = registry === RegistryOptions.indy ? indyDid : cheqdDid - await this.agent.dids.import({ - did, - overwrite: true, - privateKeys: [ - { - keyType: KeyType.Ed25519, - privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'), - }, - ], - }) - this.anonCredsIssuerId = did - } - - private async getConnectionRecord() { - if (!this.outOfBandId) { - throw Error(redText(Output.MissingConnectionRecord)) - } - - const [connection] = await this.agent.connections.findAllByOutOfBandId(this.outOfBandId) - - if (!connection) { - throw Error(redText(Output.MissingConnectionRecord)) - } - - return connection - } - - private async printConnectionInvite(version: 'v1' | 'v2') { - const outOfBandRecord = await this.agent.oob.createInvitation({ version }) - this.outOfBandId = outOfBandRecord.id - - const outOfBandInvitation = outOfBandRecord.outOfBandInvitation || outOfBandRecord.v2OutOfBandInvitation - if (outOfBandInvitation) { - console.log(Output.ConnectionLink, outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` }), '\n') - } - } - - private async waitForConnection() { + protected async waitForConnection() { if (!this.outOfBandId) { return } @@ -116,9 +56,9 @@ export class Faber extends BaseAgent { console.log(greenText(Output.ConnectionEstablished)) } - public async setupConnection(version: 'v1' | 'v2') { - await this.printConnectionInvite(version) - if (version === 'v1') await this.waitForConnection() + public async setupConnection() { + await this.printConnectionInvite('v1') + await this.waitForConnection() } private printSchema(name: string, version: string, attributes: string[]) { @@ -264,14 +204,4 @@ export class Faber extends BaseAgent { const connectionRecord = await this.getConnectionRecord() await this.agent.basicMessages.sendMessage(connectionRecord.id, message) } - - public async exit() { - console.log(Output.Exit) - await this.agent.shutdown() - process.exit(0) - } - - public async restart() { - await this.agent.shutdown() - } } diff --git a/demo/src/FaberInquirer.ts b/demo/src/main/FaberInquirer.ts similarity index 90% rename from demo/src/FaberInquirer.ts rename to demo/src/main/FaberInquirer.ts index 5d358022fd..0e206a4279 100644 --- a/demo/src/FaberInquirer.ts +++ b/demo/src/main/FaberInquirer.ts @@ -2,10 +2,12 @@ import { clear } from 'console' import { textSync } from 'figlet' import { prompt } from 'inquirer' -import { BaseInquirer, ConfirmOptions } from './BaseInquirer' -import { Faber, RegistryOptions } from './Faber' +import { RegistryOptions } from '../BaseFaber' +import { BaseInquirer, ConfirmOptions } from '../BaseInquirer' +import { Title } from '../OutputClass' + +import { Faber } from './Faber' import { Listener } from './Listener' -import { Title } from './OutputClass' export const runFaber = async () => { clear() @@ -34,7 +36,6 @@ export class FaberInquirer extends BaseInquirer { this.listener = new Listener() this.promptOptionsString = Object.values(PromptOptions) this.listener.messageListener(this.faber.agent, this.faber.name) - this.listener.pingListener(this.faber.agent, this.faber.name) } public static async build(): Promise { @@ -77,9 +78,7 @@ export class FaberInquirer extends BaseInquirer { } public async connection() { - const title = 'What DidComm messaging version use?' - const version = await prompt([this.inquireVersion(title)]) - await this.faber.setupConnection(version.options) + await this.faber.setupConnection() } public async exitUseCase(title: string) { diff --git a/demo/src/Listener.ts b/demo/src/main/Listener.ts similarity index 65% rename from demo/src/Listener.ts rename to demo/src/main/Listener.ts index 4a4c7e383a..414ea09375 100644 --- a/demo/src/Listener.ts +++ b/demo/src/main/Listener.ts @@ -7,14 +7,9 @@ import type { BasicMessageStateChangedEvent, CredentialExchangeRecord, CredentialStateChangedEvent, - TrustPingReceivedEvent, - TrustPingResponseReceivedEvent, - V2TrustPingReceivedEvent, - V2TrustPingResponseReceivedEvent, ProofExchangeRecord, ProofStateChangedEvent, } from '@aries-framework/core' -import type BottomBar from 'inquirer/lib/ui/bottom-bar' import { BasicMessageEventTypes, @@ -23,29 +18,12 @@ import { CredentialState, ProofEventTypes, ProofState, - TrustPingEventTypes, } from '@aries-framework/core' -import { ui } from 'inquirer' -import { Color, purpleText } from './OutputClass' - -export class Listener { - public on: boolean - private ui: BottomBar - - public constructor() { - this.on = false - this.ui = new ui.BottomBar() - } - - private turnListenerOn() { - this.on = true - } - - private turnListenerOff() { - this.on = false - } +import { BaseListener } from '../BaseListener' +import { Color, purpleText } from '../OutputClass' +export class Listener extends BaseListener { private printCredentialAttributes(credentialRecord: CredentialExchangeRecord) { if (credentialRecord.credentialAttributes) { const attribute = credentialRecord.credentialAttributes @@ -83,33 +61,6 @@ export class Listener { }) } - public pingListener(agent: Agent, name: string) { - agent.events.on(TrustPingEventTypes.TrustPingReceivedEvent, async (event: TrustPingReceivedEvent) => { - this.ui.updateBottomBar( - purpleText(`\n${name} received ping message from ${event.payload.connectionRecord?.theirDid}\n`) - ) - }) - agent.events.on( - TrustPingEventTypes.TrustPingResponseReceivedEvent, - async (event: TrustPingResponseReceivedEvent) => { - this.ui.updateBottomBar( - purpleText(`\n${name} received ping response message from ${event.payload.connectionRecord?.theirDid}\n`) - ) - } - ) - agent.events.on(TrustPingEventTypes.V2TrustPingReceivedEvent, async (event: V2TrustPingReceivedEvent) => { - this.ui.updateBottomBar(purpleText(`\n${name} received ping message from ${event.payload.message.from}\n`)) - }) - agent.events.on( - TrustPingEventTypes.V2TrustPingResponseReceivedEvent, - async (event: V2TrustPingResponseReceivedEvent) => { - this.ui.updateBottomBar( - purpleText(`\n${name} received ping response message from ${event.payload.message.from}\n`) - ) - } - ) - } - private async newProofRequestPrompt(proofRecord: ProofExchangeRecord, aliceInquirer: AliceInquirer) { this.turnListenerOn() await aliceInquirer.acceptProofRequest(proofRecord) diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index e2ddee9154..988bb2d222 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -10,18 +10,27 @@ import type { WalletPackOptions, WalletSignOptions, WalletVerifyOptions, + WalletUnpackOptions, + DidDocument, + VerificationMethod, + WalletPackV1Options, + WalletPackV2Options, } from '@aries-framework/core' import type { KeyEntryObject, Session } from '@hyperledger/aries-askar-shared' import { + AnoncrypDidCommV2EncryptionAlgs, + AnoncrypDidCommV2KeyWrapAlgs, AriesFrameworkError, + AuthcryptDidCommV2EncryptionAlgs, + AuthcryptDidCommV2KeyWrapAlgs, Buffer, DidCommMessageVersion, DidCommV2EncryptionAlgs, DidCommV2KeyProtectionAlgs, DidCommV2Types, - DidKey, FileSystem, + getKeyFromVerificationMethod, InjectionSymbols, isDidCommV1EncryptedEnvelope, isValidPrivateKey, @@ -34,6 +43,7 @@ import { Key, KeyDerivationMethod, KeyProviderRegistry, + keyReferenceToKey, KeyType, Logger, TypedArrayEncoder, @@ -52,8 +62,8 @@ import { keyAlgFromString, KeyAlgs, Store, + Jwk, } from '@hyperledger/aries-askar-shared' -import { Jwk } from '@hyperledger/aries-askar-shared/build/crypto/Jwk' import BigNumber from 'bn.js' import { inject, injectable } from 'tsyringe' @@ -454,7 +464,8 @@ export class AskarWallet implements Wallet { const keyPublicBytes = key.publicBytes // Store key - await this.session.insertKey({ key, name: TypedArrayEncoder.toBase58(keyPublicBytes) }) + const id = TypedArrayEncoder.toBase58(keyPublicBytes) + await this.session.insertKey({ key, name: id }) key.handle.free() return Key.fromPublicKey(keyPublicBytes, keyType) } catch (error) { @@ -596,16 +607,16 @@ export class AskarWallet implements Wallet { /** * Pack a message using DIDComm V1 or DIDComm V2 encryption algorithms * - * @param payload message to send + * @param payload message to pack * @param params packing options specific for envelop version * @returns JWE Envelope to send */ public async pack(payload: Record, params: WalletPackOptions): Promise { if (params.didCommVersion === DidCommMessageVersion.V1) { - return this.packDidCommV1(payload, params) + return this.packDidCommV1(payload, params as WalletPackV1Options) } if (params.didCommVersion === DidCommMessageVersion.V2) { - return this.packDidCommV2(payload, params) + return this.packDidCommV2(payload, params as WalletPackV2Options) } throw new AriesFrameworkError(`Unsupported DidComm version: ${params.didCommVersion}`) } @@ -617,7 +628,10 @@ export class AskarWallet implements Wallet { * @param params packing options specific for envelop version * @returns JWE Envelope to send */ - private async packDidCommV1(payload: Record, params: WalletPackOptions): Promise { + private async packDidCommV1( + payload: Record, + params: WalletPackV1Options + ): Promise { const { senderKey: senderVerkey, recipientKeys } = params let cek: AskarKey | undefined @@ -720,17 +734,31 @@ export class AskarWallet implements Wallet { * * @param payload message to send * @param params packing options specific for envelop version - * @returns JWE Envelope to send + * @returns Packed JWE Envelope */ - private async packDidCommV2(payload: Record, params: WalletPackOptions): Promise { - if (params.senderKey) { - return this.encryptEcdh1Pu(payload, params.senderKey, params.recipientKeys) + private async packDidCommV2( + payload: Record, + params: WalletPackV2Options + ): Promise { + if (params.senderDidDocument) { + return this.encryptEcdh1Pu(payload, params.senderDidDocument, params.recipientDidDocuments) } else { - return this.encryptEcdhEs(payload, params.recipientKeys) + return this.encryptEcdhEs(payload, params.recipientDidDocuments) } } - private async encryptEcdhEs(payload: Record, recipientKeys: Key[]): Promise { + /** + * Create a JWE Envelope with using ECDH-ES+A256KW + * + * @param payload Payload to encrypt + * @param recipientDidDocs Did Documents of the recipient + * + * @returns Packed JWE + * */ + private async encryptEcdhEs( + payload: Record, + recipientDidDocs: DidDocument[] + ): Promise { const wrapId = DidCommV2KeyProtectionAlgs.EcdhEsA256Kw const wrapAlg = KeyAlgs.AesA256Kw const encId = DidCommV2EncryptionAlgs.XC20P @@ -741,6 +769,13 @@ export class AskarWallet implements Wallet { let cek: AskarKey | undefined let epk: AskarKey | undefined + const { recipientsVerificationMethods } = this.findCommonSupportedEncryptionKeys(recipientDidDocs, undefined) + if (!recipientsVerificationMethods?.length) { + throw new AriesFrameworkError( + `Unable to pack message because there is no any commonly supported key types to encrypt message` + ) + } + try { // Generated once for all recipients // https://identity.foundation/didcomm-messaging/spec/#ecdh-es-key-wrapping-and-common-protected-headers @@ -751,8 +786,8 @@ export class AskarWallet implements Wallet { enc: encId, alg: wrapId, }) - .setEpk(JsonEncoder.toString(epk.jwkPublic)) - .setApv([...recipientKeys].map((recipientKey) => recipientKey.fingerprint)) + .setEpk({ ...epk.jwkPublic }) + .setApv(recipientsVerificationMethods.map((recipientVerificationMethod) => recipientVerificationMethod.id)) // As per spec we firstly need to encrypt the payload and then use tag as part of the key derivation process // https://identity.foundation/didcomm-messaging/spec/#ecdh-es-key-wrapping-and-common-protected-headers @@ -763,8 +798,9 @@ export class AskarWallet implements Wallet { aad: jweBuilder.aad(), }).parts - for (const recipientKey of recipientKeys) { + for (const recipientVerificationMethod of recipientsVerificationMethods) { try { + const recipientKey = getKeyFromVerificationMethod(recipientVerificationMethod) recipientX25519Key = AskarKey.fromPublicBytes({ publicKey: recipientKey.publicKey, algorithm: keyAlg, @@ -772,12 +808,9 @@ export class AskarWallet implements Wallet { // According to the spec `kid` MUST be a DID URI // https://identity.foundation/didcomm-messaging/spec/#construction - const recipientDidKey = new DidKey(recipientKey).did - const recipientKid = `${recipientDidKey}#${recipientKey.fingerprint}` + const recipientKid = recipientVerificationMethod.id // Wrap the recipient key using ECDH-ES - // FIXME: according to the spec `tag` must be used for the wrapping but there is not such parameter - // https://identity.foundation/didcomm-messaging/spec/#ecdh-es-key-wrapping-and-common-protected-headers const encryptedKey = new EcdhEs({ algId: jweBuilder.alg(), apu: jweBuilder.apu(), @@ -810,30 +843,53 @@ export class AskarWallet implements Wallet { } } + /** + * Create a JWE Envelope with using ECDH-1PU+A256KW + * + * @param payload Payload to encrypt + * @param senderDidDoc Did Document of the sender + * @param recipientDidDocs Did Documents of the recipient + * + * @returns Packed JWE + * */ private async encryptEcdh1Pu( payload: Record, - senderKey: Key, - recipientKeys: Key[] + senderDidDoc: DidDocument, + recipientDidDocs: DidDocument[] ): Promise { const wrapAlg = KeyAlgs.AesA256Kw const encAlg = KeyAlgs.AesA256CbcHs512 - const keyAlg = keyAlgFromString(senderKey.keyType) let senderAskarKey: KeyEntryObject | undefined | null let recipientAskarKey: AskarKey | undefined let cek: AskarKey | undefined let epk: AskarKey | undefined + const { senderVerificationMethod, recipientsVerificationMethods } = this.findCommonSupportedEncryptionKeys( + recipientDidDocs, + senderDidDoc + ) + if (!recipientsVerificationMethods?.length) { + throw new AriesFrameworkError( + `Unable to pack message because there is no any commonly supported key types to encrypt message` + ) + } + if (!senderVerificationMethod) { + throw new AriesFrameworkError(`Unable to pack message: Sender key not found`) + } + try { // currently, keys are stored in the wallet by their base58 representation + const senderKey = getKeyFromVerificationMethod(senderVerificationMethod) + const keyAlg = keyAlgFromString(senderKey.keyType) + senderAskarKey = await this.session.fetchKey({ name: senderKey.publicKeyBase58 }) if (!senderAskarKey) { throw new WalletError(`Unable to pack message. Sender key ${senderKey} not found in wallet.`) } // According to the spec `skid` MUST be a DID URI - const senderDidKey = new DidKey(senderKey).did - const senderKid = `${senderDidKey}#${senderKey.fingerprint}` + const senderKid = senderVerificationMethod.id // Generated once for all recipients // https://identity.foundation/didcomm-messaging/spec/#ecdh-1pu-key-wrapping-and-common-protected-headers @@ -845,22 +901,22 @@ export class AskarWallet implements Wallet { alg: DidCommV2KeyProtectionAlgs.Ecdh1PuA256Kw, }) .setSkid(senderKid) - .setEpk(JsonEncoder.toString(epk.jwkPublic)) + .setEpk({ ...epk.jwkPublic }) .setApu(senderKid) - .setApv([...recipientKeys].map((recipientKey) => recipientKey.fingerprint)) + .setApv(recipientsVerificationMethods.map((recipientsVerificationMethod) => recipientsVerificationMethod.id)) // As per spec we firstly need to encrypt the payload and then use tag as part of the key derivation process // https://identity.foundation/didcomm-messaging/spec/#ecdh-1pu-key-wrapping-and-common-protected-headers cek = AskarKey.generate(encAlg) - const message = Buffer.from(JSON.stringify(payload)) const { ciphertext, tag, nonce } = cek.aeadEncrypt({ - message, + message: Buffer.from(JSON.stringify(payload)), aad: jweBuilder.aad(), }).parts - for (const recipientKey of recipientKeys) { + for (const recipientVerificationMethod of recipientsVerificationMethods) { try { + const recipientKey = getKeyFromVerificationMethod(recipientVerificationMethod) recipientAskarKey = AskarKey.fromPublicBytes({ publicKey: recipientKey.publicKey, algorithm: keyAlg, @@ -868,8 +924,7 @@ export class AskarWallet implements Wallet { // According to the spec `kid` MUST be a DID URI // https://identity.foundation/didcomm-messaging/spec/#construction - const recipientDidKey = new DidKey(recipientKey).did - const recipientKid = `${recipientDidKey}#${recipientKey.fingerprint}` + const recipientKid = recipientVerificationMethod.id // Wrap the recipient key using ECDH-1PU const encryptedCek = new Ecdh1PU({ @@ -910,15 +965,19 @@ export class AskarWallet implements Wallet { /** * Unpacks a JWE Envelope coded using DIDComm V1 of DIDComm V2 encryption algorithms * - * @param messagePackage JWE Envelope + * @param messagePackage Json Web Envelope + * @param params In order to unpack DidComm V2 JWE we need Did Document of sender and recipients * * @returns UnpackedMessageContext with plain text message, sender key, recipient key, and didcomm message version */ - public async unpack(messagePackage: EncryptedMessage): Promise { + public async unpack(messagePackage: EncryptedMessage, params?: WalletUnpackOptions): Promise { if (isDidCommV1EncryptedEnvelope(messagePackage)) { return this.unpackDidCommV1(messagePackage) } else { - return this.unpackDidCommV2(messagePackage) + if (!params) { + throw new AriesFrameworkError(`Unable unpack DidComm V2 JWE: Missing sender/recipient Did Documents`) + } + return this.unpackDidCommV2(messagePackage, params) } } @@ -929,7 +988,7 @@ export class AskarWallet implements Wallet { * * @returns UnpackedMessageContext with plain text message, sender key, recipient key, and didcomm message version */ - private async unpackDidCommV1(messagePackage: EncryptedMessage): Promise { + public async unpackDidCommV1(messagePackage: EncryptedMessage): Promise { // Decode a message using DIDComm v1 encryption. const protected_ = JsonEncoder.fromBase64(messagePackage.protected) @@ -1029,63 +1088,91 @@ export class AskarWallet implements Wallet { * Unpacks a JWE Envelope coded using DIDComm V2 encryption algorithms * * @param messagePackage JWE Envelope + * @param params Resolved Did Documents of the sender and recipients * * @returns UnpackedMessageContext with plain text message, sender key, recipient key, and didcomm message version */ - private async unpackDidCommV2(messagePackage: EncryptedMessage): Promise { + private async unpackDidCommV2( + messagePackage: EncryptedMessage, + params: WalletUnpackOptions + ): Promise { const protected_ = JsonEncoder.fromBase64(messagePackage.protected) - if ( - protected_.alg === DidCommV2KeyProtectionAlgs.EcdhEsA128Kw || - protected_.alg === DidCommV2KeyProtectionAlgs.EcdhEsA256Kw - ) { - return this.decryptEcdhEs(messagePackage, protected_) + + if (AnoncrypDidCommV2KeyWrapAlgs.includes(protected_.alg)) { + if (!params.recipientDidDocuments) { + throw new AriesFrameworkError( + `Unable to unpack DidComm V2 anoncrypted JWE. Recipients Did Documents must be provided.` + ) + } + return this.decryptEcdhEs(messagePackage, protected_, params.recipientDidDocuments) } - if ( - protected_.alg === DidCommV2KeyProtectionAlgs.Ecdh1PuA128Kw || - protected_.alg === DidCommV2KeyProtectionAlgs.Ecdh1PuA256Kw - ) { - return this.decryptEcdh1Pu(messagePackage, protected_) + if (AuthcryptDidCommV2KeyWrapAlgs.includes(protected_.alg)) { + if (!params.senderDidDocument || !params.senderDidDocument) { + throw new AriesFrameworkError( + `Unable to unpack DidComm V2 anoncrypted JWE. Sender and Recipients Did Documents must be provided.` + ) + } + return this.decryptEcdh1Pu(messagePackage, protected_, params.senderDidDocument, params.recipientDidDocuments) } - throw new AriesFrameworkError(`Unsupported JWE algorithm: ${protected_.alg}`) + throw new AriesFrameworkError( + `Unable to unpack DidComm V2 anoncrypted JWE. Unsupported wrapping algorithm: ${protected_.alg}` + ) } - private async decryptEcdhEs(jwe: EncryptedMessage, protected_: any): Promise { + /** + * Unpacks a JWE Envelope with using ECDH-ES+A256KW + * + * @param jwe JWE Envelope + * @param protected_ Decoded protected payload (extracted from jwe) + * @param recipientDidDocuments Did Documents of the recipients + * + * @returns UnpackedMessageContext with plain text message, sender key, recipient key, and didcomm message version + */ + private async decryptEcdhEs( + jwe: EncryptedMessage, + protected_: any, + recipientDidDocuments: DidDocument[] + ): Promise { const { alg, apu, apv, enc } = protected_ const wrapAlg = alg.slice(8) - if (![DidCommV2KeyProtectionAlgs.EcdhEsA128Kw, DidCommV2KeyProtectionAlgs.EcdhEsA256Kw].includes(alg)) { + if (!AnoncrypDidCommV2KeyWrapAlgs.includes(alg)) { throw new AriesFrameworkError(`Unsupported ECDH-ES algorithm: ${alg}`) } - if (!['A128GCM', 'A256GCM', 'A128CBC-HS256', 'A256CBC-HS512', 'XC20P'].includes(enc)) { + if (!AnoncrypDidCommV2EncryptionAlgs.includes(enc)) { throw new AriesFrameworkError(`Unsupported ECDH-ES content encryption: ${alg}`) } - let recipientAskarKey: KeyEntryObject | null | undefined + let recipientAskarKey: AskarKey | undefined let cek: AskarKey | undefined let epk: AskarKey | undefined try { // Generated once for all recipients // https://identity.foundation/didcomm-messaging/spec/#ecdh-es-key-wrapping-and-common-protected-headers - epk = AskarKey.fromJwk({ jwk: Jwk.fromString(protected_.epk) }) + epk = AskarKey.fromJwk({ jwk: Jwk.fromJson(protected_.epk) }) for (const recipient of jwe.recipients) { try { - // currently, keys are stored in the wallet by their base58 representation - const recipientKey = Key.fromPublicKeyId(recipient.header.kid) - recipientAskarKey = await this.session.fetchKey({ name: recipientKey.publicKeyBase58 }) + const resolvedRecipientKeys = await this.resolveRecipientKey({ + kid: recipient.header.kid, + recipientDidDocuments, + }) + recipientAskarKey = resolvedRecipientKeys.recipientAskarKey + const recipientKey = resolvedRecipientKeys.recipientKey + if (!recipientAskarKey) continue // unwrap the key using ECDH-ES cek = new EcdhEs({ algId: Uint8Array.from(Buffer.from(alg)), - apv: Uint8Array.from(Buffer.from(apv ?? [])), - apu: Uint8Array.from(Buffer.from(apu ?? [])), + apv: apv ? TypedArrayEncoder.fromBase64(apv) : new Buffer([]), + apu: apu ? TypedArrayEncoder.fromBase64(apu) : new Buffer([]), }).receiverUnwrapKey({ wrapAlg, encAlg: enc, ephemeralKey: epk, - recipientKey: recipientAskarKey.key, + recipientKey: recipientAskarKey, ciphertext: TypedArrayEncoder.fromBase64(recipient.encrypted_key), // tag: TypedArrayEncoder.fromBase64(jwe.tag), }) @@ -1104,7 +1191,7 @@ export class AskarWallet implements Wallet { recipientKey, } } finally { - recipientAskarKey?.key.handle.free() + recipientAskarKey?.handle.free() cek?.handle.free() } } @@ -1112,69 +1199,73 @@ export class AskarWallet implements Wallet { epk?.handle.free() } - throw new AriesFrameworkError('Unable to decrypt message') + throw new AriesFrameworkError('Unable to open jwe: recipient key not found in the wallet') } - private async decryptEcdh1Pu(jwe: EncryptedMessage, protected_: any): Promise { + /** + * Unpacks a JWE Envelope with using ECDH-1PU+A256KW + * + * @param jwe JWE Envelope + * @param protected_ Decoded protected payload (extracted from jwe) + * @param senderDidDocument Did Document of the JWE sender + * @param recipientDidDocuments Did Documents of the JWE recipients + * + * @returns UnpackedMessageContext with plain text message, sender key, recipient key, and didcomm message version + */ + private async decryptEcdh1Pu( + jwe: EncryptedMessage, + protected_: any, + senderDidDocument: DidDocument, + recipientDidDocuments: DidDocument[] + ): Promise { const { alg, enc, apu, apv, skid } = protected_ const wrapAlg = alg.slice(9) - if (![DidCommV2KeyProtectionAlgs.Ecdh1PuA128Kw, DidCommV2KeyProtectionAlgs.Ecdh1PuA256Kw].includes(alg)) { + if (!AuthcryptDidCommV2KeyWrapAlgs.includes(alg)) { throw new AriesFrameworkError(`Unsupported ECDH-1PU algorithm: ${alg}`) } - if (!['A128CBC-HS256', 'A256CBC-HS512'].includes(enc)) { - throw new AriesFrameworkError(`Unsupported ECDH-1PU content encryption: ${alg}`) + if (!AuthcryptDidCommV2EncryptionAlgs.includes(enc)) { + throw new AriesFrameworkError(`Unsupported ECDH-1PU content encryption: ${enc}`) } - let recipientAskarKey: KeyEntryObject | null | undefined + let recipientAskarKey: AskarKey | undefined let senderAskarKey: AskarKey | undefined let cek: AskarKey | undefined let epk: AskarKey | undefined try { - // Validate the `apu` filed is similar to `skid` - // https://identity.foundation/didcomm-messaging/spec/#ecdh-1pu-key-wrapping-and-common-protected-headers - const senderKidApu = TypedArrayEncoder.fromBase64(apu).toString('utf-8') - if (senderKidApu && skid && senderKidApu !== skid) { - throw new AriesFrameworkError('Mismatch between skid and apu') - } - const senderKid = skid ?? senderKidApu - if (!senderKid) { - throw new AriesFrameworkError('Sender key ID not provided') + const resolvedSenderKeys = this.resolveSenderKeys({ skid, apu, didDocument: senderDidDocument }) + senderAskarKey = resolvedSenderKeys.senderAskarKey + const senderKey = resolvedSenderKeys.senderKey + if (!senderAskarKey) { + throw new WalletError(`Unable to unpack message. Cannot resolve sender key.`) } - // FIXME: Properly, we need to properly resolve sender key doing the following steps: - // 1. Extract a DID from DID URL - // 2. Resolve DID Doc for sender - // 3. Get matching the ID - // So it looks like we need to use DidResolver inside of the wallet - const senderKey = Key.fromPublicKeyId(senderKid) - senderAskarKey = AskarKey.fromPublicBytes({ - publicKey: senderKey.publicKey, - algorithm: keyAlgFromString(senderKey.keyType), - }) - // Generated once for all recipients - epk = AskarKey.fromJwk({ jwk: Jwk.fromString(protected_.epk) }) + epk = AskarKey.fromJwk({ jwk: Jwk.fromJson(protected_.epk) }) for (const recipient of jwe.recipients) { try { - // currently, keys are stored in the wallet by their base58 representation - const recipientKey = Key.fromPublicKeyId(recipient.header.kid) - recipientAskarKey = await this.session.fetchKey({ name: recipientKey.publicKeyBase58 }) + const resolvedRecipientKeys = await this.resolveRecipientKey({ + kid: recipient.header.kid, + recipientDidDocuments, + }) + recipientAskarKey = resolvedRecipientKeys.recipientAskarKey + const recipientKey = resolvedRecipientKeys.recipientKey + if (!recipientAskarKey) continue // unwrap the key using ECDH-1PU cek = new Ecdh1PU({ - apv: Uint8Array.from(Buffer.from(apv)), - apu: Uint8Array.from(Buffer.from(apu)), algId: Uint8Array.from(Buffer.from(alg)), + apv: apv ? TypedArrayEncoder.fromBase64(apv) : new Buffer([]), + apu: apu ? TypedArrayEncoder.fromBase64(apu) : new Buffer([]), }).receiverUnwrapKey({ wrapAlg: wrapAlg, encAlg: enc, ephemeralKey: epk, senderKey: senderAskarKey, - recipientKey: recipientAskarKey.key, + recipientKey: recipientAskarKey, ccTag: TypedArrayEncoder.fromBase64(jwe.tag), ciphertext: TypedArrayEncoder.fromBase64(recipient.encrypted_key), }) @@ -1195,14 +1286,69 @@ export class AskarWallet implements Wallet { } } finally { cek?.handle.free() - recipientAskarKey?.key?.handle.free() + recipientAskarKey?.handle.free() } } } finally { senderAskarKey?.handle.free() epk?.handle.free() } - throw new AriesFrameworkError('Unable to decrypt didcomm v2 envelop') + throw new AriesFrameworkError('Unable to open jwe: recipient key not found in the wallet') + } + + private async resolveRecipientKey({ + kid, + recipientDidDocuments, + }: { + kid: string + recipientDidDocuments: DidDocument[] + }): Promise<{ recipientKey: Key | undefined; recipientAskarKey: AskarKey | undefined }> { + const recipientDidDocument = recipientDidDocuments.find((didDocument) => keyReferenceToKey(didDocument, kid)) + if (!recipientDidDocument) { + throw new AriesFrameworkError(`Unable to resolve recipient Did Document for kid: ${kid}`) + } + const recipientKey = keyReferenceToKey(recipientDidDocument, kid) + const recipientAskarKey = await this.session.fetchKey({ + name: recipientKey.publicKeyBase58, + }) + return { + recipientKey, + recipientAskarKey: recipientAskarKey?.key, + } + } + + private resolveSenderKeys({ + skid, + didDocument, + apu, + }: { + skid: string + didDocument: DidDocument + apu?: string | null + }): { + senderKey: Key | undefined + senderAskarKey: AskarKey | undefined + } { + // Validate the `apu` filed is similar to `skid` + // https://identity.foundation/didcomm-messaging/spec/#ecdh-1pu-key-wrapping-and-common-protected-headers + const senderKidApu = apu ? TypedArrayEncoder.fromBase64(apu).toString('utf-8') : undefined + if (senderKidApu && skid && senderKidApu !== skid) { + throw new AriesFrameworkError('Mismatch between skid and apu') + } + const senderKid = skid ?? senderKidApu + if (!senderKid) { + throw new AriesFrameworkError('Sender key ID not provided') + } + + const senderKey = keyReferenceToKey(didDocument, senderKid) + const senderAskarKey = AskarKey.fromPublicBytes({ + publicKey: senderKey.publicKey, + algorithm: keyAlgFromString(senderKey.keyType), + }) + return { + senderKey, + senderAskarKey, + } } public async generateNonce(): Promise { @@ -1276,4 +1422,40 @@ export class AskarWallet implements Wallet { throw new WalletError('Error saving KeyPair record', { cause: error }) } } + + private findCommonSupportedEncryptionKeys(recipientDidDocuments: DidDocument[], senderDidDocument?: DidDocument) { + const recipients = recipientDidDocuments.map((recipientDidDocument) => recipientDidDocument.agreementKeys) + + if (!senderDidDocument) { + return { + senderVerificationMethod: undefined, + recipientsVerificationMethods: recipients.map((recipient) => recipient[0]), + } + } + + const senderAgreementKeys = senderDidDocument.agreementKeys + + let senderVerificationMethod: VerificationMethod | undefined + const recipientsVerificationMethods: VerificationMethod[] = [] + + for (const senderAgreementKey of senderAgreementKeys) { + senderVerificationMethod = senderAgreementKey + for (const recipient of recipients) { + const recipientKey = recipient.find((r) => r.type === senderAgreementKey.type) + if (recipientKey) { + recipientsVerificationMethods.push(recipientKey) + break + } + } + if (senderVerificationMethod && recipientsVerificationMethods.length === recipients.length) { + // found appropriate keys + break + } + } + + return { + senderVerificationMethod, + recipientsVerificationMethods, + } + } } diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts index 7c6bc0606c..a7c51427d4 100644 --- a/packages/askar/src/wallet/__tests__/packing.test.ts +++ b/packages/askar/src/wallet/__tests__/packing.test.ts @@ -1,4 +1,4 @@ -import type { WalletConfig, WalletPackOptions } from '@aries-framework/core' +import type { WalletConfig, WalletPackOptions, WalletUnpackOptions } from '@aries-framework/core' import { BasicMessage, @@ -7,13 +7,30 @@ import { KeyDerivationMethod, KeyProviderRegistry, KeyType, + AriesFrameworkError, + DidKey, + DidDocument, + Buffer, } from '@aries-framework/core' +import { Jwk, Key as AskarKey } from '@hyperledger/aries-askar-shared' import { describeRunInNodeVersion } from '../../../../../tests/runInVersion' +import { TrustPingMessage } from '../../../../core/src/modules/connections/protocols/trust-ping/v2' import { agentDependencies } from '../../../../core/tests/helpers' import testLogger from '../../../../core/tests/logger' import { AskarWallet } from '../AskarWallet' +import { + aliceDidDocument, + bobDidDocument, + bobX25519Secret1, + bobX25519Secret2, + bobX25519Secret3, + jweEcdh1PuA256CbcHs512_1, + jweEcdhEsX25519Xc20P_1, + message, +} from './testVectors' + // use raw key derivation method to speed up wallet creating / opening / closing between tests const walletConfig: WalletConfig = { id: 'Askar Wallet Packing', @@ -22,11 +39,14 @@ const walletConfig: WalletConfig = { keyDerivationMethod: KeyDerivationMethod.Raw, } -const message = new BasicMessage({ content: 'hello' }) - describeRunInNodeVersion([18], 'askarWallet packing', () => { let askarWallet: AskarWallet + async function createDidDocument(): Promise { + const key = await askarWallet.createKey({ keyType: KeyType.X25519 }) + return new DidKey(key).didDocument + } + beforeEach(async () => { askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new KeyProviderRegistry([])) await askarWallet.createAndOpen(walletConfig) @@ -37,6 +57,8 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { }) describe('DIDComm V1 packing and unpacking', () => { + const message = new BasicMessage({ content: 'hello' }) + test('Authcrypt', async () => { // Create both sender and recipient keys const senderKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) @@ -70,35 +92,113 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { }) describe('DIDComm V2 packing and unpacking', () => { - test('Authcrypt', async () => { + const message = new TrustPingMessage({ + body: { + responseRequested: false, + }, + }) + + test('Authcrypt pack/unpack works', async () => { // Create both sender and recipient keys - const senderKey = await askarWallet.createKey({ keyType: KeyType.X25519 }) - const recipientKey = await askarWallet.createKey({ keyType: KeyType.X25519 }) + const senderDidDocument = await createDidDocument() + const recipientDidDocument = await createDidDocument() - const params: WalletPackOptions = { + const packParams: WalletPackOptions = { didCommVersion: DidCommMessageVersion.V2, - recipientKeys: [recipientKey], - senderKey: senderKey, + recipientDidDocuments: [recipientDidDocument], + senderDidDocument, } + const encryptedMessage = await askarWallet.pack(message.toJSON(), packParams) - const encryptedMessage = await askarWallet.pack(message.toJSON(), params) - const plainTextMessage = await askarWallet.unpack(encryptedMessage) - expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) + const unpackParams: WalletUnpackOptions = { + senderDidDocument, + recipientDidDocuments: [recipientDidDocument], + } + const plainTextMessage = await askarWallet.unpack(encryptedMessage, unpackParams) + + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, TrustPingMessage)).toEqual(message) }) - test('Anoncrypt', async () => { + test('Anoncrypt pack/unpack works', async () => { // Create recipient keys only - const recipientKey = await askarWallet.createKey({ keyType: KeyType.X25519 }) + const recipientDidDocument = await createDidDocument() - const params: WalletPackOptions = { + const packParams: WalletPackOptions = { didCommVersion: DidCommMessageVersion.V2, - recipientKeys: [recipientKey], - senderKey: null, + recipientDidDocuments: [recipientDidDocument], + senderDidDocument: null, } + const encryptedMessage = await askarWallet.pack(message.toJSON(), packParams) - const encryptedMessage = await askarWallet.pack(message.toJSON(), params) - const plainTextMessage = await askarWallet.unpack(encryptedMessage) - expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) + const unpackParams: WalletUnpackOptions = { + recipientDidDocuments: [recipientDidDocument], + } + const plainTextMessage = await askarWallet.unpack(encryptedMessage, unpackParams) + + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, TrustPingMessage)).toEqual(message) + }) + }) + + describe('DIDComm V2 test vectors', () => { + describe('Anocrypt', () => { + const unpackParams = { + // @ts-ignore + recipientDidDocuments: [new DidDocument(bobDidDocument)], + senderDidDocument: undefined, + } + + test.each([bobX25519Secret1, bobX25519Secret2, bobX25519Secret3])( + 'Unpack anoncrypted EcdhEsX25519Xc20P test vector works', + async (bobX25519Secret) => { + await askarWallet.createKey({ + keyType: KeyType.X25519, + privateKey: Buffer.from(AskarKey.fromJwk({ jwk: Jwk.fromJson(bobX25519Secret.value) }).secretBytes), + }) + + const unpackedMessage = await askarWallet.unpack(jweEcdhEsX25519Xc20P_1, unpackParams) + expect(unpackedMessage.plaintextMessage).toEqual(message) + } + ) + + test('Unpack fails when there is not recipient key in the wallet', async () => { + return expect(() => askarWallet.unpack(jweEcdhEsX25519Xc20P_1, unpackParams)).rejects.toThrowError( + AriesFrameworkError + ) + }) + }) + + describe('Authcrypt', () => { + const unpackParams = { + // @ts-ignore + recipientDidDocuments: [new DidDocument(bobDidDocument)], + // @ts-ignore + senderDidDocument: new DidDocument(aliceDidDocument), + } + + test.each([bobX25519Secret1, bobX25519Secret2, bobX25519Secret3])( + 'Unpack authcrypted Ecdh1PuA256CbcHs512 test vector works', + async (bobX25519Secret) => { + await askarWallet.createKey({ + keyType: KeyType.X25519, + privateKey: Buffer.from(AskarKey.fromJwk({ jwk: Jwk.fromJson(bobX25519Secret.value) }).secretBytes), + }) + + const unpackedMessage = await askarWallet.unpack(jweEcdh1PuA256CbcHs512_1, unpackParams) + expect(unpackedMessage.plaintextMessage).toEqual(message) + } + ) + + test('Unpack fails when there is not recipient key in the wallet', async () => { + await expect(() => askarWallet.unpack(jweEcdh1PuA256CbcHs512_1, unpackParams)).rejects.toThrowError( + AriesFrameworkError + ) + }) + + test('Unpack fails when unable to resolve sender', async () => { + await expect(() => + askarWallet.unpack(jweEcdh1PuA256CbcHs512_1, { ...unpackParams, senderDidDocument: undefined }) + ).rejects.toThrowError(AriesFrameworkError) + }) }) }) }) diff --git a/packages/askar/src/wallet/__tests__/testVectors.ts b/packages/askar/src/wallet/__tests__/testVectors.ts new file mode 100644 index 0000000000..81c2363d5b --- /dev/null +++ b/packages/askar/src/wallet/__tests__/testVectors.ts @@ -0,0 +1,286 @@ +/* + * Test vectors from https://identity.foundation/didcomm-messaging/spec/#appendix + * */ +export const jweEcdhEsX25519Xc20P_1 = { + ciphertext: + 'KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p', + protected: + 'eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0', + recipients: [ + { + encrypted_key: '3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A', + header: { + kid: 'did:example:bob#key-x25519-1', + }, + }, + { + encrypted_key: 'j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ', + header: { + kid: 'did:example:bob#key-x25519-2', + }, + }, + { + encrypted_key: 'TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A', + header: { + kid: 'did:example:bob#key-x25519-3', + }, + }, + ], + tag: '6ylC_iAs4JvDQzXeY6MuYQ', + iv: 'ESpmcyGiZpRjc5urDela21TOOTW8Wqd1', +} + +export const jweEcdh1PuA256CbcHs512_1 = { + ciphertext: + 'MJezmxJ8DzUB01rMjiW6JViSaUhsZBhMvYtezkhmwts1qXWtDB63i4-FHZP6cJSyCI7eU-gqH8lBXO_UVuviWIqnIUrTRLaumanZ4q1dNKAnxNL-dHmb3coOqSvy3ZZn6W17lsVudjw7hUUpMbeMbQ5W8GokK9ZCGaaWnqAzd1ZcuGXDuemWeA8BerQsfQw_IQm-aUKancldedHSGrOjVWgozVL97MH966j3i9CJc3k9jS9xDuE0owoWVZa7SxTmhl1PDetmzLnYIIIt-peJtNYGdpd-FcYxIFycQNRUoFEr77h4GBTLbC-vqbQHJC1vW4O2LEKhnhOAVlGyDYkNbA4DSL-LMwKxenQXRARsKSIMn7z-ZIqTE-VCNj9vbtgR', + protected: + 'eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkdGY01vcEpsamY0cExaZmNoNGFfR2hUTV9ZQWY2aU5JMWRXREd5VkNhdzAifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXkteDI1NTE5LTEiLCJhcHUiOiJaR2xrT21WNFlXMXdiR1U2WVd4cFkyVWphMlY1TFhneU5UVXhPUzB4IiwidHlwIjoiYXBwbGljYXRpb24vZGlkY29tbS1lbmNyeXB0ZWQranNvbiIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJhbGciOiJFQ0RILTFQVStBMjU2S1cifQ', + recipients: [ + { + encrypted_key: 'o0FJASHkQKhnFo_rTMHTI9qTm_m2mkJp-wv96mKyT5TP7QjBDuiQ0AMKaPI_RLLB7jpyE-Q80Mwos7CvwbMJDhIEBnk2qHVB', + header: { + kid: 'did:example:bob#key-x25519-1', + }, + }, + { + encrypted_key: 'rYlafW0XkNd8kaXCqVbtGJ9GhwBC3lZ9AihHK4B6J6V2kT7vjbSYuIpr1IlAjvxYQOw08yqEJNIwrPpB0ouDzKqk98FVN7rK', + header: { + kid: 'did:example:bob#key-x25519-2', + }, + }, + { + encrypted_key: 'aqfxMY2sV-njsVo-_9Ke9QbOf6hxhGrUVh_m-h_Aq530w3e_4IokChfKWG1tVJvXYv_AffY7vxj0k5aIfKZUxiNmBwC_QsNo', + header: { + kid: 'did:example:bob#key-x25519-3', + }, + }, + ], + tag: 'uYeo7IsZjN7AnvBjUZE5lNryNENbf6_zew_VC-d4b3U', + iv: 'o02OXDQ6_-sKz2PX_6oyJg', +} + +export const aliceX25519Secret1 = { + kid: 'did:example:alice#key-x25519-1', + value: { + kty: 'OKP', + crv: 'X25519', + x: 'avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs', + }, +} + +export const bobX25519Secret1 = { + kid: 'did:example:bob#key-x25519-1', + value: { + kty: 'OKP', + d: 'b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0', + crv: 'X25519', + x: 'GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E', + }, +} + +export const bobX25519Secret2 = { + kid: 'did:example:bob#key-x25519-2', + value: { + kty: 'OKP', + d: 'p-vteoF1gopny1HXywt76xz_uC83UUmrgszsI-ThBKk', + crv: 'X25519', + x: 'UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM', + }, +} + +export const bobX25519Secret3 = { + kid: 'did:example:bob#key-x25519-3', + value: { + kty: 'OKP', + d: 'f9WJeuQXEItkGM8shN4dqFr5fLQLBasHnWZ-8dPaSo0', + crv: 'X25519', + x: '82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY', + }, +} + +export const message = { + id: '1234567890', + typ: 'application/didcomm-plain+json', + type: 'http://example.com/protocols/lets_do_lunch/1.0/proposal', + from: 'did:example:alice', + to: ['did:example:bob'], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: 'and its value' }, +} + +export const aliceDidDocument = { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:example:alice', + authentication: [ + { + id: 'did:example:alice#key-1', + type: 'JsonWebKey2020', + controller: 'did:example:alice', + publicKeyJwk: { + kty: 'OKP', + crv: 'Ed25519', + x: 'G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww', + }, + }, + { + id: 'did:example:alice#key-2', + type: 'JsonWebKey2020', + controller: 'did:example:alice', + publicKeyJwk: { + kty: 'EC', + crv: 'P-256', + x: '2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY', + y: 'BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w', + }, + }, + { + id: 'did:example:alice#key-3', + type: 'JsonWebKey2020', + controller: 'did:example:alice', + publicKeyJwk: { + kty: 'EC', + crv: 'secp256k1', + x: 'aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk', + y: 'JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk', + }, + }, + ], + keyAgreement: [ + { + id: 'did:example:alice#key-x25519-1', + type: 'JsonWebKey2020', + controller: 'did:example:alice', + publicKeyJwk: { + kty: 'OKP', + crv: 'X25519', + x: 'avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs', + }, + }, + { + id: 'did:example:alice#key-p256-1', + type: 'JsonWebKey2020', + controller: 'did:example:alice', + publicKeyJwk: { + kty: 'EC', + crv: 'P-256', + x: 'L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE', + y: 'SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo', + }, + }, + { + id: 'did:example:alice#key-p521-1', + type: 'JsonWebKey2020', + controller: 'did:example:alice', + publicKeyJwk: { + kty: 'EC', + crv: 'P-521', + x: 'AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz', + y: 'AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk', + }, + }, + ], +} + +export const bobDidDocument = { + '@context': ['https://www.w3.org/ns/did/v2'], + id: 'did:example:bob', + keyAgreement: [ + { + id: 'did:example:bob#key-x25519-1', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'OKP', + crv: 'X25519', + x: 'GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E', + }, + }, + { + id: 'did:example:bob#key-x25519-2', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'OKP', + crv: 'X25519', + x: 'UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM', + }, + }, + { + id: 'did:example:bob#key-x25519-3', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'OKP', + crv: 'X25519', + x: '82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY', + }, + }, + { + id: 'did:example:bob#key-p256-1', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'EC', + crv: 'P-256', + x: 'FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo', + y: '6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY', + }, + }, + { + id: 'did:example:bob#key-p256-2', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'EC', + crv: 'P-256', + x: 'n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0', + y: 'ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw', + }, + }, + { + id: 'did:example:bob#key-p384-1', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'EC', + crv: 'P-384', + x: 'MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y', + y: 'X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7', + }, + }, + { + id: 'did:example:bob#key-p384-2', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'EC', + crv: 'P-384', + x: '2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3', + y: 'W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd', + }, + }, + { + id: 'did:example:bob#key-p521-1', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'EC', + crv: 'P-521', + x: 'Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi', + y: 'ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH', + }, + }, + { + id: 'did:example:bob#key-p521-2', + type: 'JsonWebKey2020', + controller: 'did:example:bob', + publicKeyJwk: { + kty: 'EC', + crv: 'P-521', + x: 'ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots', + y: 'AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH', + }, + }, + ], +} diff --git a/packages/core/src/agent/EnvelopeService.ts b/packages/core/src/agent/EnvelopeService.ts index 8e56215851..05901dfd3a 100644 --- a/packages/core/src/agent/EnvelopeService.ts +++ b/packages/core/src/agent/EnvelopeService.ts @@ -1,6 +1,5 @@ import type { AgentMessage } from './AgentMessage' import type { AgentContext } from './context' -import type { Key } from '../crypto' import type { DidCommV1PackMessageParams, DidCommV2PackMessageParams, @@ -8,17 +7,19 @@ import type { EncryptedMessage, } from '../didcomm' import type { DidDocument } from '../modules/dids' -import type { WalletPackOptions } from '../wallet/Wallet' +import type { WalletPackOptions, WalletUnpackOptions } from '../wallet/Wallet' import { InjectionSymbols } from '../constants' import { V2Attachment } from '../decorators/attachment' import { V2AttachmentData } from '../decorators/attachment/V2Attachment' +import { isDidCommV1EncryptedEnvelope } from '../didcomm' import { DidCommMessageVersion } from '../didcomm/types' import { AriesFrameworkError } from '../error' import { Logger } from '../logger' -import { DidResolverService, getAgreementKeys, keyReferenceToKey } from '../modules/dids' +import { DidResolverService } from '../modules/dids' import { ForwardMessage, V2ForwardMessage } from '../modules/routing/messages' import { inject, injectable } from '../plugins' +import { JsonEncoder } from '../utils' export type PackMessageParams = DidCommV1PackMessageParams | DidCommV2PackMessageParams @@ -50,7 +51,44 @@ export class EnvelopeService { agentContext: AgentContext, encryptedMessage: EncryptedMessage ): Promise { - return await agentContext.wallet.unpack(encryptedMessage) + if (isDidCommV1EncryptedEnvelope(encryptedMessage)) { + return this.unpackDidCommV1(agentContext, encryptedMessage) + } else { + return this.unpackDidCommV2(agentContext, encryptedMessage) + } + } + + public async unpackDidCommV1( + agentContext: AgentContext, + encryptedMessage: EncryptedMessage + ): Promise { + return agentContext.wallet.unpack(encryptedMessage) + } + + public async unpackDidCommV2( + agentContext: AgentContext, + encryptedMessage: EncryptedMessage + ): Promise { + // FIXME: Temporary workaround to extract sender/recipient keys out of JWE and resolve their DidDocuments + // In future we are going to completely rework Wallet interface to expose crypto functions and construct / parse JWE here + const protected_ = JsonEncoder.fromBase64(encryptedMessage.protected) + + const senderDidDocument = protected_.skid + ? await this.didResolverService.resolveDidDocument(agentContext, protected_.skid) + : undefined + + const recipientDidDocuments = await Promise.all( + encryptedMessage.recipients.map((recipient) => + this.didResolverService.resolveDidDocument(agentContext, recipient.header.kid) + ) + ) + + const params: WalletUnpackOptions = { + senderDidDocument, + recipientDidDocuments, + } + + return agentContext.wallet.unpack(encryptedMessage, params) } private async packDIDCommV1Message( @@ -118,18 +156,11 @@ export class EnvelopeService { message: AgentMessage, params: DidCommV2PackMessageParams ): Promise { - const { recipientDidDoc, senderDidDoc } = params - const { senderKey, recipientKey } = EnvelopeService.findCommonSupportedEncryptionKeys(recipientDidDoc, senderDidDoc) - if (!recipientKey) { - throw new AriesFrameworkError( - `Unable to pack message ${message.id} because there is no any commonly supported key types to encrypt message` - ) - } const unboundMessage = message.toJSON() const packParams: WalletPackOptions = { didCommVersion: DidCommMessageVersion.V2, - recipientKeys: [recipientKey], - senderKey, + recipientDidDocuments: [params.recipientDidDoc], + senderDidDocument: params.senderDidDoc, } const encryptedMessage = await agentContext.wallet.pack(unboundMessage, packParams) return await this.wrapDIDCommV2MessageInForward(agentContext, encryptedMessage, params) @@ -145,24 +176,21 @@ export class EnvelopeService { return encryptedMessage } - const routings: { did: string; key: Key }[] = [] - for (const routingKey of service.routingKeys ?? []) { - const routingDidDocument = await this.didResolverService.resolveDidDocument(agentContext, routingKey) - routings.push({ - did: routingDidDocument.id, - key: keyReferenceToKey(routingDidDocument, routingKey), - }) - } + const routingKeys = service.routingKeys ?? [] + const routingDidDocuments: DidDocument[] = await Promise.all( + routingKeys.map((routingKey) => this.didResolverService.resolveDidDocument(agentContext, routingKey)) + ) - if (!routings.length) { + if (!routingDidDocuments.length) { + // There is no routing keys defined -> we do not need to wrap the message into Forward return encryptedMessage } // If the message has routing keys (mediator) pack for each mediator let next = recipientDidDoc.id - for (const routing of routings) { + for (const routing of routingDidDocuments) { const forwardMessage = new V2ForwardMessage({ - to: [routing.did], + to: [routing.id], body: { next }, attachments: [ new V2Attachment({ @@ -170,7 +198,7 @@ export class EnvelopeService { }), ], }) - next = routing.did + next = routing.id this.logger.debug('Forward message created', forwardMessage) const forwardJson = forwardMessage.toJSON() @@ -178,40 +206,11 @@ export class EnvelopeService { // Forward messages are anon packed const forwardParams: WalletPackOptions = { didCommVersion: DidCommMessageVersion.V2, - recipientKeys: [routing.key], + recipientDidDocuments: [routing], } encryptedMessage = await agentContext.wallet.pack(forwardJson, forwardParams) } return encryptedMessage } - - private static findCommonSupportedEncryptionKeys(recipientDidDocument: DidDocument, senderDidDocument?: DidDocument) { - const recipientAgreementKeys = getAgreementKeys(recipientDidDocument) - - if (!senderDidDocument) { - return { senderKey: undefined, recipientKey: recipientAgreementKeys[0] } - } - - const senderAgreementKeys = getAgreementKeys(senderDidDocument) - - let senderKey: Key | undefined - let recipientKey: Key | undefined - - for (const senderAgreementKey of senderAgreementKeys) { - for (const recipientAgreementKey of recipientAgreementKeys) { - if (senderAgreementKey.keyType === recipientAgreementKey.keyType) { - senderKey = senderAgreementKey - recipientKey = recipientAgreementKey - break - } - } - if (senderKey) break - } - - return { - senderKey, - recipientKey, - } - } } diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts index 61efb67ef3..11b273bcce 100644 --- a/packages/core/src/crypto/Key.ts +++ b/packages/core/src/crypto/Key.ts @@ -25,12 +25,6 @@ export class Key { return Key.fromPublicKey(publicKeyBytes, keyType) } - public static fromPublicKeyId(kid: string) { - const key = kid.split('#')[1] ?? kid - const multibaseKey = key.startsWith('z') ? key : `z${key}` - return Key.fromFingerprint(multibaseKey) - } - public static fromFingerprint(fingerprint: string) { const { data } = MultiBaseEncoder.decode(fingerprint) const [code, byteLength] = VarintEncoder.decode(data) diff --git a/packages/core/src/didcomm/JweEnvelope.ts b/packages/core/src/didcomm/JweEnvelope.ts index 7201b53d54..b3919e051d 100644 --- a/packages/core/src/didcomm/JweEnvelope.ts +++ b/packages/core/src/didcomm/JweEnvelope.ts @@ -22,7 +22,7 @@ export interface ProtectedOptions { enc: string alg: string skid?: string - epk?: string + epk?: Record apu?: string apv?: string } @@ -32,7 +32,7 @@ export class Protected { public enc!: string public alg!: string public skid?: string - public epk?: string + public epk?: Record public apu?: string public apv?: string @@ -153,7 +153,7 @@ export class JweEnvelopeBuilder { return this } - public setEpk(epk: string): JweEnvelopeBuilder { + public setEpk(epk: Record): JweEnvelopeBuilder { this.protected.epk = epk return this } @@ -169,11 +169,11 @@ export class JweEnvelopeBuilder { } public apv(): Uint8Array { - return this.protected.apv ? Uint8Array.from(Buffer.from(this.protected.apv)) : Uint8Array.from([]) + return this.protected.apv ? TypedArrayEncoder.fromBase64(this.protected.apv) : Uint8Array.from([]) } public apu(): Uint8Array { - return this.protected.apu ? Uint8Array.from(Buffer.from(this.protected.apu)) : Uint8Array.from([]) + return this.protected.apu ? TypedArrayEncoder.fromBase64(this.protected.apu) : Uint8Array.from([]) } public alg(): Uint8Array { diff --git a/packages/core/src/didcomm/versions/v2/index.ts b/packages/core/src/didcomm/versions/v2/index.ts index 51582b019c..699b21e402 100644 --- a/packages/core/src/didcomm/versions/v2/index.ts +++ b/packages/core/src/didcomm/versions/v2/index.ts @@ -10,4 +10,13 @@ export interface DidCommV2PackMessageParams { } export { isPlaintextMessageV2, isDidCommV2Message } from './helpers' -export { PlaintextDidCommV2Message, DidCommV2Types, DidCommV2EncryptionAlgs, DidCommV2KeyProtectionAlgs } from './types' +export { + PlaintextDidCommV2Message, + DidCommV2Types, + DidCommV2EncryptionAlgs, + DidCommV2KeyProtectionAlgs, + AnoncrypDidCommV2EncryptionAlgs, + AuthcryptDidCommV2EncryptionAlgs, + AnoncrypDidCommV2KeyWrapAlgs, + AuthcryptDidCommV2KeyWrapAlgs, +} from './types' diff --git a/packages/core/src/didcomm/versions/v2/types.ts b/packages/core/src/didcomm/versions/v2/types.ts index abe8c9ffee..02dbe53437 100644 --- a/packages/core/src/didcomm/versions/v2/types.ts +++ b/packages/core/src/didcomm/versions/v2/types.ts @@ -14,6 +14,7 @@ export enum DidCommV2Types { export enum DidCommV2EncryptionAlgs { XC20P = 'XC20P', A256CbcHs512 = 'A256CBC-HS512', + A256Gcm = 'A256GCM', } export enum DidCommV2KeyProtectionAlgs { @@ -22,3 +23,19 @@ export enum DidCommV2KeyProtectionAlgs { Ecdh1PuA128Kw = 'ECDH-1PU+A128KW', Ecdh1PuA256Kw = 'ECDH-1PU+A256KW', } + +export const AnoncrypDidCommV2EncryptionAlgs = [ + DidCommV2EncryptionAlgs.A256Gcm, + DidCommV2EncryptionAlgs.XC20P, + DidCommV2EncryptionAlgs.A256CbcHs512, +] +export const AuthcryptDidCommV2EncryptionAlgs = [DidCommV2EncryptionAlgs.A256CbcHs512] + +export const AnoncrypDidCommV2KeyWrapAlgs = [ + DidCommV2KeyProtectionAlgs.EcdhEsA128Kw, + DidCommV2KeyProtectionAlgs.EcdhEsA256Kw, +] +export const AuthcryptDidCommV2KeyWrapAlgs = [ + DidCommV2KeyProtectionAlgs.Ecdh1PuA128Kw, + DidCommV2KeyProtectionAlgs.Ecdh1PuA256Kw, +] diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index c4e14c9bfb..c03c34f7cd 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -3,13 +3,13 @@ import type { DidDocumentService } from './service' import { Expose, Type } from 'class-transformer' import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator' -import { KeyType, Key } from '../../../crypto' +import { Key, KeyType } from '../../../crypto' import { JsonTransformer } from '../../../utils/JsonTransformer' import { IsStringOrStringArray } from '../../../utils/transformers' import { getKeyFromVerificationMethod } from './key-type' -import { IndyAgentService, ServiceTransformer, DidCommV1Service, DidCommV2Service } from './service' -import { VerificationMethodTransformer, VerificationMethod, IsStringOrVerificationMethod } from './verificationMethod' +import { DidCommV1Service, DidCommV2Service, IndyAgentService, ServiceTransformer } from './service' +import { IsStringOrVerificationMethod, VerificationMethod, VerificationMethodTransformer } from './verificationMethod' export type DidPurpose = | 'authentication' @@ -145,6 +145,32 @@ export class DidDocument { throw new Error(`Unable to locate verification method with id '${keyId}' in purposes ${purposes}`) } + public dereferenceVerificationMethods(allowedPurposes?: DidPurpose[]) { + const allPurposes: DidPurpose[] = [ + 'authentication', + 'keyAgreement', + 'assertionMethod', + 'capabilityInvocation', + 'capabilityDelegation', + ] + + const purposes = allowedPurposes ?? allPurposes + + const verificationMethods: VerificationMethod[] = [] + + for (const purpose of purposes) { + for (const verificationMethod of this[purpose] ?? []) { + if (typeof verificationMethod === 'string') { + verificationMethods.push(this.dereferenceVerificationMethod(verificationMethod)) + } else { + verificationMethods.push(verificationMethod) + } + } + } + + return verificationMethods + } + /** * Returns all of the service endpoints matching the given type. * @@ -200,6 +226,26 @@ export class DidDocument { return recipientKeys } + public get agreementKeys(): Array { + return this.dereferenceVerificationMethods(['keyAgreement']) + } + + public get authentications(): Array { + return this.dereferenceVerificationMethods(['authentication']) + } + + public get assertionMethods(): Array { + return this.dereferenceVerificationMethods(['assertionMethod']) + } + + public get capabilityInvocations(): Array { + return this.dereferenceVerificationMethods(['capabilityInvocation']) + } + + public get capabilityDelegations(): Array { + return this.dereferenceVerificationMethods(['capabilityDelegation']) + } + public toJSON() { return JsonTransformer.toJSON(this) } @@ -251,22 +297,12 @@ export async function findVerificationMethodByKeyType( export function getAuthenticationKeys(didDocument: DidDocument) { return ( - didDocument.authentication?.map((authentication) => { - const verificationMethod = - typeof authentication === 'string' ? didDocument.dereferenceVerificationMethod(authentication) : authentication - const key = getKeyFromVerificationMethod(verificationMethod) - return key - }) ?? [] + didDocument.authentication?.map((authentication) => getKeyFromDidDocumentKeyEntry(authentication, didDocument)) ?? + [] ) } -export function getAgreementKeys(didDocument: DidDocument) { - return ( - didDocument.keyAgreement?.map((keyAgreement) => { - const verificationMethod = - typeof keyAgreement === 'string' ? didDocument.dereferenceVerificationMethod(keyAgreement) : keyAgreement - const key = getKeyFromVerificationMethod(verificationMethod) - return key - }) ?? [] - ) +function getKeyFromDidDocumentKeyEntry(key: string | VerificationMethod, didDocument: DidDocument): Key { + const verificationMethod = typeof key === 'string' ? didDocument.dereferenceVerificationMethod(key) : key + return getKeyFromVerificationMethod(verificationMethod) } diff --git a/packages/core/src/wallet/Wallet.ts b/packages/core/src/wallet/Wallet.ts index 5d78c083df..efed12d1ab 100644 --- a/packages/core/src/wallet/Wallet.ts +++ b/packages/core/src/wallet/Wallet.ts @@ -1,5 +1,6 @@ import type { Key, KeyType } from '../crypto' import type { EncryptedMessage, PlaintextMessage, EnvelopeType, DidCommMessageVersion } from '../didcomm/types' +import type { DidDocument } from '../modules/dids/domain/DidDocument' import type { Disposable } from '../plugins' import type { WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '../types' import type { Buffer } from '../utils/buffer' @@ -39,8 +40,26 @@ export interface Wallet extends Disposable { sign(options: WalletSignOptions): Promise verify(options: WalletVerifyOptions): Promise + /** + * Pack a message using DIDComm V1 or DIDComm V2 encryption algorithms + * + * @param payload message to pack + * @param params Additional parameter to pack JWE (specific for didcomm version) + * + * @returns JWE Envelope to send + */ pack(payload: Record, params: WalletPackOptions): Promise - unpack(encryptedMessage: EncryptedMessage): Promise + + /** + * Unpacks a JWE Envelope coded using DIDComm V1 of DIDComm V2 encryption algorithms + * + * @param encryptedMessage packed Json Web Envelope + * @param params Additional parameter to unpack JWE (specific for didcomm version) + * + * @returns UnpackedMessageContext with plain text message, sender key, recipient key, and didcomm message version + */ + unpack(encryptedMessage: EncryptedMessage, params?: WalletUnpackOptions): Promise + generateNonce(): Promise generateWalletKey(): Promise } @@ -69,9 +88,23 @@ export interface UnpackedMessageContext { recipientKey?: Key } -export type WalletPackOptions = { +export type WalletPackOptions = WalletPackV1Options | WalletPackV2Options + +export type WalletPackV1Options = { didCommVersion: DidCommMessageVersion recipientKeys: Key[] senderKey?: Key | null envelopeType?: EnvelopeType } + +export type WalletPackV2Options = { + didCommVersion: DidCommMessageVersion + recipientDidDocuments: DidDocument[] + senderDidDocument?: DidDocument | null + envelopeType?: EnvelopeType +} + +export type WalletUnpackOptions = { + recipientDidDocuments: DidDocument[] + senderDidDocument?: DidDocument | null +} diff --git a/packages/indy-sdk/src/wallet/IndySdkWallet.ts b/packages/indy-sdk/src/wallet/IndySdkWallet.ts index b740484f84..ed7368c44d 100644 --- a/packages/indy-sdk/src/wallet/IndySdkWallet.ts +++ b/packages/indy-sdk/src/wallet/IndySdkWallet.ts @@ -10,6 +10,7 @@ import type { WalletCreateKeyOptions, WalletExportImportConfig, WalletPackOptions, + WalletPackV1Options, WalletSignOptions, WalletVerifyOptions, } from '@aries-framework/core' @@ -548,7 +549,7 @@ export class IndySdkWallet implements Wallet { public async pack(payload: Record, params: WalletPackOptions): Promise { if (params.didCommVersion === DidCommMessageVersion.V1) { - return this.packDidCommV1(payload, params) + return this.packDidCommV1(payload, params as WalletPackV1Options) } if (params.didCommVersion === DidCommMessageVersion.V2) { throw new AriesFrameworkError(`DidComm V2 message encryption is not supported for Indy wallet`) @@ -556,7 +557,10 @@ export class IndySdkWallet implements Wallet { throw new AriesFrameworkError(`Unsupported DidComm version: ${params.didCommVersion}`) } - private async packDidCommV1(payload: Record, params: WalletPackOptions): Promise { + private async packDidCommV1( + payload: Record, + params: WalletPackV1Options + ): Promise { try { const messageRaw = JsonEncoder.toBuffer(payload) const recipientKeys = params.recipientKeys.map((recipientKey) => recipientKey.publicKeyBase58)