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)