diff --git a/.dockerignore b/.dockerignore index dd87e2d73f..2399fcb20b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ +# Skip unncecessary folders node_modules build +.github diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml deleted file mode 100644 index 27fe8d108d..0000000000 --- a/.github/actions/setup-node/action.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Setup NodeJS -description: Setup NodeJS with caching -author: 'timo@animo.id' - -inputs: - node-version: - description: Node version to use - required: true - -runs: - using: composite - steps: - - name: Get yarn cache directory path - id: yarn-cache-dir-path - shell: bash - run: echo "::set-output name=dir::$(yarn cache dir)" - - - uses: actions/cache@v2 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Setup node v${{ inputs.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ inputs.node-version }} - registry-url: 'https://registry.npmjs.org/' - -branding: - icon: scissors - color: purple diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..eb9c53f64c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +################################# +# GitHub Dependabot Config info # +################################# + +version: 2 +updates: + # Maintain dependencies for NPM + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'monthly' + allow: + # Focus on main dependencies, not devDependencies + - dependency-type: 'production' + + # Maintain dependencies for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'monthly' + + # Maintain dependencies for Docker + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'monthly' + + # Maintain dependencies for Cargo + - package-ecosystem: 'cargo' + directory: '/' + schedule: + interval: 'monthly' diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml new file mode 100644 index 0000000000..4b4ecd5bd5 --- /dev/null +++ b/.github/workflows/cleanup-cache.yml @@ -0,0 +1,16 @@ +# Repositories have 10 GB of cache storage per repository +# Documentation: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy +name: 'Cleanup - Cache' +on: + schedule: + - cron: '0 0 * * 0/3' + workflow_dispatch: + +jobs: + delete-caches: + name: 'Delete Actions caches' + runs-on: ubuntu-latest + + steps: + - name: 'Wipe Github Actions cache' + uses: easimon/wipe-cache@v2 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 470451a5c8..0000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: 'CodeQL' - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: '45 0 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ['javascript'] - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 82d66fb8f7..5e7d654556 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -5,6 +5,9 @@ on: branches: - main +env: + NODE_OPTIONS: --max_old_space_size=6144 + jobs: release-canary: runs-on: aries-ubuntu-2004 @@ -12,7 +15,7 @@ jobs: if: "!startsWith(github.event.head_commit.message, 'chore(release): v')" steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # pulls all commits (needed for lerna to correctly version) fetch-depth: 0 @@ -22,9 +25,11 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' + registry-url: 'https://registry.npmjs.org/' - name: Install dependencies run: yarn install --frozen-lockfile @@ -43,7 +48,7 @@ jobs: run: | LAST_RELEASED_VERSION=$(npm view @aries-framework/core@alpha version) - echo "::set-output name=version::$LAST_RELEASED_VERSION" + echo version="${LAST_RELEASED_VERSION}" >> "$GITHUB_OUTPUT" - name: Setup git user run: | @@ -62,16 +67,18 @@ jobs: if: "startsWith(github.event.head_commit.message, 'chore(release): v')" steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 # setup dependencies - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' + registry-url: 'https://registry.npmjs.org/' - name: Install dependencies run: yarn install --frozen-lockfile @@ -82,10 +89,10 @@ jobs: NEW_VERSION=$(node -p "require('./lerna.json').version") echo $NEW_VERSION - echo "::set-output name=version::$NEW_VERSION" + echo version="${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Create Tag - uses: mathieudutour/github-tag-action@v6.0 + uses: mathieudutour/github-tag-action@v6.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} custom_tag: ${{ steps.new-version.outputs.version }} diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 19ea4923ba..3d9afd40f7 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -13,13 +13,14 @@ env: ENDORSER_AGENT_PUBLIC_DID_SEED: 00000000000000000000000Endorser9 GENESIS_TXN_PATH: network/genesis/local-genesis.txn LIB_INDY_STRG_POSTGRES: /home/runner/work/aries-framework-javascript/indy-sdk/experimental/plugins/postgres_storage/target/release # for Linux - NODE_OPTIONS: --max_old_space_size=4096 + NODE_OPTIONS: --max_old_space_size=6144 # Make sure we're not running multiple release steps at the same time as this can give issues with determining the next npm version to release. # Ideally we only add this to the 'release' job so it doesn't limit PR runs, but github can't guarantee the job order in that case: # "When concurrency is specified at the job level, order is not guaranteed for jobs or runs that queue within 5 minutes of each other." concurrency: - group: aries-framework-${{ github.ref }}-${{ github.repository }}-${{ github.event_name }} + # Cancel previous runs that are not completed yet + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -43,26 +44,27 @@ jobs: fi echo "SHOULD_RUN: ${SHOULD_RUN}" - echo "::set-output name=triggered::${SHOULD_RUN}" + echo triggered="${SHOULD_RUN}" >> "$GITHUB_OUTPUT" validate: runs-on: aries-ubuntu-2004 name: Validate steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 # setup dependencies - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' - name: Install dependencies - run: yarn install + run: yarn install --frozen-lockfile - name: Linting run: yarn lint @@ -86,7 +88,7 @@ jobs: steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 # setup dependencies @@ -109,21 +111,22 @@ jobs: uses: ./.github/actions/setup-postgres-wallet-plugin - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: 'yarn' - name: Add ref-napi resolution in Node18 - run: node ./scripts/add-ref-napi-resolution.js if: matrix.node-version == '18.x' + run: node ./scripts/add-ref-napi-resolution.js - name: Install dependencies - run: yarn install + run: yarn install --frozen-lockfile - name: Run tests run: TEST_AGENT_PUBLIC_DID_SEED=${TEST_AGENT_PUBLIC_DID_SEED} ENDORSER_AGENT_PUBLIC_DID_SEED=${ENDORSER_AGENT_PUBLIC_DID_SEED} GENESIS_TXN_PATH=${GENESIS_TXN_PATH} yarn test --coverage --forceExit --bail - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 if: always() version-stable: @@ -133,19 +136,21 @@ jobs: if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # pulls all commits (needed for lerna to correctly version) fetch-depth: 0 + persist-credentials: false # setup dependencies - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile @@ -174,11 +179,10 @@ jobs: run: | NEW_VERSION=$(node -p "require('./lerna.json').version") echo $NEW_VERSION - - echo "::set-output name=version::$NEW_VERSION" + echo version="${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Create Pull Request - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v5 with: commit-message: | chore(release): v${{ steps.new-version.outputs.version }} diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index d5af138f96..a14c516acb 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -14,7 +14,7 @@ jobs: steps: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - - uses: amannn/action-semantic-pull-request@v3.4.6 + - uses: amannn/action-semantic-pull-request@v5.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/repolinter.yml b/.github/workflows/repolinter.yml index f33a30eb1d..4a4501a8a8 100644 --- a/.github/workflows/repolinter.yml +++ b/.github/workflows/repolinter.yml @@ -12,6 +12,7 @@ jobs: container: ghcr.io/todogroup/repolinter:v0.10.1 steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - name: Lint Repo run: bundle exec /app/bin/repolinter.js --rulesetUrl https://raw.githubusercontent.com/hyperledger-labs/hyperledger-community-management-tools/master/repo_structure/repolint.json diff --git a/Dockerfile b/Dockerfile index cd68166f9e..9514936098 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,41 @@ -FROM ubuntu:20.04 as base +## Stage 1: Build indy-sdk and postgres plugin -ENV DEBIAN_FRONTEND noninteractive +FROM ubuntu:22.04 as base -RUN apt-get update -y && apt-get install -y \ - software-properties-common \ - apt-transport-https \ - curl \ - # Only needed to build indy-sdk - build-essential \ - git \ - libzmq3-dev libsodium-dev pkg-config libssl-dev +# Set this value only during build +ARG DEBIAN_FRONTEND noninteractive -# libindy -RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 -RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" +# Define packages to install +ENV PACKAGES software-properties-common ca-certificates \ + curl build-essential git \ + libzmq3-dev libsodium-dev pkg-config -# nodejs 16x LTS Debian -RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - +# Combined update and install to ensure Docker caching works correctly +RUN apt-get update -y \ + && apt-get install -y $PACKAGES -# yarn -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +RUN curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl1.1.deb \ + # libssl1.1 (required by libindy) + && dpkg -i libssl1.1.deb \ + && curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl-dev_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl-dev1.1.deb \ + # libssl-dev1.1 (required to compile libindy with posgres plugin) + && dpkg -i libssl-dev1.1.deb -# install depdencies -RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ - libindy \ - nodejs +# Add APT sources +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 \ + && add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" \ + && curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list -# Install yarn seperately due to `no-install-recommends` to skip nodejs install -RUN apt-get install -y --no-install-recommends yarn +# Install libindy, NodeJS and yarn +RUN apt-get update -y \ + # Install libindy + && apt-get install -y --allow-unauthenticated libindy \ + && apt-get install -y nodejs \ + && apt-get install -y --no-install-recommends yarn \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean -y # postgres plugin setup # install rust and set up rustup @@ -46,14 +53,19 @@ RUN cargo build --release # set up library path for postgres plugin ENV LIB_INDY_STRG_POSTGRES="/indy-sdk/experimental/plugins/postgres_storage/target/release" +## Stage 2: Build Aries Framework JavaScript + FROM base as final -# AFJ specifc setup -WORKDIR /www +# Set environment variables ENV RUN_MODE="docker" -# Copy dependencies +# Set working directory +WORKDIR /www + +# Copy repository files COPY . . -RUN yarn install -RUN yarn build \ No newline at end of file +# Run yarn install and build +RUN yarn install --frozen-lockfile \ + && yarn build diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index 8374e27238..2de378d8c1 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -46,11 +46,6 @@ export class Alice extends BaseAgent { } 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, }) diff --git a/packages/action-menu/src/ActionMenuApi.ts b/packages/action-menu/src/ActionMenuApi.ts index 6abe1b3fac..086ba1115b 100644 --- a/packages/action-menu/src/ActionMenuApi.ts +++ b/packages/action-menu/src/ActionMenuApi.ts @@ -11,8 +11,8 @@ import { AriesFrameworkError, ConnectionService, MessageSender, - OutboundMessageContext, injectable, + getOutboundMessageContext, } from '@aries-framework/core' import { ActionMenuRole } from './ActionMenuRole' @@ -66,10 +66,10 @@ export class ActionMenuApi { connection, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: record, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -92,10 +92,10 @@ export class ActionMenuApi { menu: options.menu, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: record, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -126,10 +126,10 @@ export class ActionMenuApi { performedAction: options.performedAction, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: record, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) diff --git a/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts b/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts index 2d676b4d52..d47fd3b905 100644 --- a/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts +++ b/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts @@ -40,6 +40,12 @@ export interface AnonCredsRsModuleConfigOptions { * ``` */ anoncreds: Anoncreds + + /** + * Create a default link secret if there are no created link secrets. + * @defaultValue true + */ + autoCreateLinkSecret?: boolean } /** @@ -55,4 +61,9 @@ export class AnonCredsRsModuleConfig { public get anoncreds() { return this.options.anoncreds } + + /** See {@link AnonCredsModuleConfigOptions.autoCreateLinkSecret} */ + public get autoCreateLinkSecret() { + return this.options.autoCreateLinkSecret ?? true + } } diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts index 20b9c5a74d..7249da2662 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts @@ -34,6 +34,7 @@ import { AnonCredsRestrictionWrapper, unqualifiedCredentialDefinitionIdRegex, AnonCredsRegistryService, + storeLinkSecret, } from '@aries-framework/anoncreds' import { AriesFrameworkError, JsonTransformer, TypedArrayEncoder, injectable, utils } from '@aries-framework/core' import { @@ -47,6 +48,7 @@ import { anoncreds, } from '@hyperledger/anoncreds-shared' +import { AnonCredsRsModuleConfig } from '../AnonCredsRsModuleConfig' import { AnonCredsRsError } from '../errors/AnonCredsRsError' @injectable() @@ -198,15 +200,20 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) // If a link secret is specified, use it. Otherwise, attempt to use default link secret - const linkSecretRecord = options.linkSecretId + let linkSecretRecord = options.linkSecretId ? await linkSecretRepository.getByLinkSecretId(agentContext, options.linkSecretId) : await linkSecretRepository.findDefault(agentContext) + // No default link secret. Automatically create one if set on module config if (!linkSecretRecord) { - // No default link secret - throw new AnonCredsRsError( - 'No link secret provided to createCredentialRequest and no default link secret has been found' - ) + const moduleConfig = agentContext.dependencyManager.resolve(AnonCredsRsModuleConfig) + if (!moduleConfig.autoCreateLinkSecret) { + throw new AnonCredsRsError( + 'No link secret provided to createCredentialRequest and no default link secret has been found' + ) + } + const { linkSecretId, linkSecretValue } = await this.createLinkSecret(agentContext, {}) + linkSecretRecord = await storeLinkSecret(agentContext, { linkSecretId, linkSecretValue, setAsDefault: true }) } if (!linkSecretRecord.value) { diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index a4beea9b20..5659f5a813 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -25,7 +25,6 @@ import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRecord, AnonCredsKeyCorrectnessProofRepository, - AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository, } from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' @@ -40,6 +39,7 @@ import { AnonCredsIssuerServiceSymbol, } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' +import { storeLinkSecret } from './utils' @injectable() export class AnonCredsApi { @@ -81,30 +81,21 @@ export class AnonCredsApi { /** * Create a Link Secret, optionally indicating its ID and if it will be the default one - * If there is no default Link Secret, this will be set as default (even if setAsDefault is true). + * If there is no default Link Secret, this will be set as default (even if setAsDefault is false). * */ - public async createLinkSecret(options?: AnonCredsCreateLinkSecretOptions) { + public async createLinkSecret(options?: AnonCredsCreateLinkSecretOptions): Promise { const { linkSecretId, linkSecretValue } = await this.anonCredsHolderService.createLinkSecret(this.agentContext, { linkSecretId: options?.linkSecretId, }) - // In some cases we don't have the linkSecretValue. However we still want a record so we know which link secret ids are valid - const linkSecretRecord = new AnonCredsLinkSecretRecord({ linkSecretId, value: linkSecretValue }) - - // If it is the first link secret registered, set as default - const defaultLinkSecretRecord = await this.anonCredsLinkSecretRepository.findDefault(this.agentContext) - if (!defaultLinkSecretRecord || options?.setAsDefault) { - linkSecretRecord.setTag('isDefault', true) - } - - // Set the current default link secret as not default - if (defaultLinkSecretRecord && options?.setAsDefault) { - defaultLinkSecretRecord.setTag('isDefault', false) - await this.anonCredsLinkSecretRepository.update(this.agentContext, defaultLinkSecretRecord) - } + await storeLinkSecret(this.agentContext, { + linkSecretId, + linkSecretValue, + setAsDefault: options?.setAsDefault, + }) - await this.anonCredsLinkSecretRepository.save(this.agentContext, linkSecretRecord) + return linkSecretId } /** diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts index e3e6b25275..7ca8053a38 100644 --- a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -17,6 +17,7 @@ import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../ import { IndySdkHolderService, IndySdkIssuerService, + IndySdkModuleConfig, IndySdkStorageService, IndySdkVerifierService, IndySdkWallet, @@ -63,6 +64,7 @@ const agentContext = getAgentContext({ [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [AnonCredsLinkSecretRepository, anonCredsLinkSecretRepository], + [IndySdkModuleConfig, new IndySdkModuleConfig({ indySdk, autoCreateLinkSecret: false })], ], agentConfig, wallet, diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index edc9883578..fad5355d54 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -14,3 +14,4 @@ export * from './AnonCredsApiOptions' export { generateLegacyProverDidLikeString } from './utils/proverDid' export * from './utils/indyIdentifiers' export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' +export { storeLinkSecret } from './utils/linkSecret' diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index 0e0ae355c9..5213153ff9 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -87,7 +87,7 @@ export interface AnonCredsProof { > self_attested_attrs: Record - requested_predicates: Record + predicates: Record } // TODO: extend types for proof property proof: any diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts index 12f464d3f3..775c47aff5 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -215,18 +215,18 @@ export class V1CredentialProtocol credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.OfferSent) - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1ProposeCredentialMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1OfferCredentialMessage, }) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) await this.indyCredentialFormat.processProposal(messageContext.agentContext, { @@ -257,7 +257,7 @@ export class V1CredentialProtocol }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) // Save record await credentialRepository.save(messageContext.agentContext, credentialRecord) @@ -506,11 +506,11 @@ export class V1CredentialProtocol } if (credentialRecord) { - const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1ProposeCredentialMessage, }) - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1OfferCredentialMessage, }) @@ -518,9 +518,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.ProposalSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) await this.indyCredentialFormat.processOffer(messageContext.agentContext, { @@ -546,7 +546,7 @@ export class V1CredentialProtocol }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) await this.indyCredentialFormat.processOffer(messageContext.agentContext, { credentialRecord, @@ -750,9 +750,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.OfferSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage ?? undefined, - previousSentMessage: offerMessage ?? undefined, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage ?? undefined, + lastSentMessage: offerMessage ?? undefined, }) const requestAttachment = requestMessage.getRequestAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) @@ -886,9 +886,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.RequestSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: offerCredentialMessage, - previousSentMessage: requestCredentialMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: offerCredentialMessage, + lastSentMessage: requestCredentialMessage, }) const issueAttachment = issueMessage.getCredentialAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) @@ -981,9 +981,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.CredentialIssued) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: requestCredentialMessage, - previousSentMessage: issueCredentialMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: requestCredentialMessage, + lastSentMessage: issueCredentialMessage, }) // Update record diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts index eb255070cc..0abcbba515 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts @@ -126,7 +126,7 @@ const credentialIssueMessage = new V1IssueCredentialMessage({ const didCommMessageRecord = new DidCommMessageRecord({ associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', - message: {}, + message: { '@id': '123', '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential' }, role: DidCommMessageRole.Receiver, }) diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts index 5825f0610c..d19d391ddf 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts @@ -59,13 +59,13 @@ describe('V1 Connectionless Credentials', () => { protocolVersion: 'v1', }) - const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ recordId: faberCredentialRecord.id, message, domain: 'https://a-domain.com', }) - await aliceAgent.receiveMessage(offerMessage.toJSON()) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, @@ -162,7 +162,6 @@ describe('V1 Connectionless Credentials', () => { }) const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ - recordId: faberCredentialRecord.id, message, domain: 'https://a-domain.com', }) diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts index e828fb2258..de3835c170 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts @@ -1,9 +1,9 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { MessageHandler, MessageHandlerInboundMessage, CredentialExchangeRecord } from '@aries-framework/core' -import { DidCommMessageRepository, OutboundMessageContext } from '@aries-framework/core' +import { AriesFrameworkError, getOutboundMessageContext } from '@aries-framework/core' -import { V1IssueCredentialMessage, V1RequestCredentialMessage } from '../messages' +import { V1IssueCredentialMessage } from '../messages' export class V1IssueCredentialHandler implements MessageHandler { private credentialProtocol: V1CredentialProtocol @@ -36,31 +36,20 @@ export class V1IssueCredentialHandler implements MessageHandler { credentialRecord, }) - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { - associatedRecordId: credentialRecord.id, - messageClass: V1RequestCredentialMessage, - }) - - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service && requestMessage.service) { - const recipientService = messageContext.message.service - const ourService = requestMessage.service - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) + const requestMessage = await this.credentialProtocol.findRequestMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } - messageContext.agentContext.config.logger.error(`Could not automatically create credential ack`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) } } diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts index 8d2d847e96..483516736d 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts @@ -1,13 +1,7 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { MessageHandler, MessageHandlerInboundMessage, CredentialExchangeRecord } from '@aries-framework/core' -import { - OutboundMessageContext, - RoutingService, - DidCommMessageRepository, - DidCommMessageRole, - ServiceDecorator, -} from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { V1OfferCredentialMessage } from '../messages' @@ -37,47 +31,13 @@ export class V1OfferCredentialHandler implements MessageHandler { messageContext: MessageHandlerInboundMessage ) { messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) - if (messageContext.connection) { - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) + const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service) { - const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) - const routing = await routingService.getRouting(messageContext.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service - - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { - credentialRecord, - }) - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create credential request`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + }) } } diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts index d4fdbec98f..5711a0b4c8 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts @@ -1,7 +1,7 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { CredentialExchangeRecord, MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' -import { OutboundMessageContext } from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { V1ProposeCredentialMessage } from '../messages' @@ -44,9 +44,9 @@ export class V1ProposeCredentialHandler implements MessageHandler { credentialRecord, }) - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, + return getOutboundMessageContext(messageContext.agentContext, { + message, + connectionRecord: messageContext.connection, associatedRecord: credentialRecord, }) } diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts index 00154fc8a4..c79c993f64 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts @@ -1,7 +1,7 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { CredentialExchangeRecord, MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' -import { DidCommMessageRepository, DidCommMessageRole, OutboundMessageContext } from '@aries-framework/core' +import { AriesFrameworkError, getOutboundMessageContext } from '@aries-framework/core' import { V1RequestCredentialMessage } from '../messages' @@ -36,40 +36,20 @@ export class V1RequestCredentialHandler implements MessageHandler { messageContext.agentContext, credentialRecord.id ) + if (!offerMessage) { + throw new AriesFrameworkError(`Could not find offer message for credential record with id ${credentialRecord.id}`) + } const { message } = await this.credentialProtocol.acceptRequest(messageContext.agentContext, { credentialRecord, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service && offerMessage?.service) { - const recipientService = messageContext.message.service - const ourService = offerMessage.service - - // Set ~service, update message in record (for later use) - message.setService(ourService) - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create credential request`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: offerMessage, + }) } } diff --git a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts index 5e27debbfb..606dc6e7ce 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts @@ -179,17 +179,17 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< proofRecord.assertState(ProofState.RequestSent) proofRecord.assertProtocolVersion('v1') - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1ProposePresentationMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1RequestPresentationMessage, }) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) // Update record @@ -212,7 +212,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { agentMessage: proposalMessage, @@ -425,11 +425,11 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // proof record already exists, this means we are the message is sent as reply to a proposal we sent if (proofRecord) { - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1RequestPresentationMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1ProposePresentationMessage, }) @@ -437,9 +437,9 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // Assert proofRecord.assertProtocolVersion('v1') proofRecord.assertState(ProofState.ProposalSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) await this.indyProofFormat.processRequest(agentContext, { @@ -475,7 +475,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) // Save in repository await proofRepository.save(agentContext, proofRecord) @@ -764,9 +764,9 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // Assert proofRecord.assertState(ProofState.RequestSent) proofRecord.assertProtocolVersion('v1') - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage, - previousSentMessage: requestMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage, + lastSentMessage: requestMessage, }) const presentationAttachment = presentationMessage.getPresentationAttachmentById(INDY_PROOF_ATTACHMENT_ID) @@ -839,12 +839,12 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< connection?.id ) - const previousReceivedMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastReceivedMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1RequestPresentationMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1PresentationMessage, }) @@ -852,9 +852,9 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // Assert proofRecord.assertProtocolVersion('v1') proofRecord.assertState(ProofState.PresentationSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) // Update record diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts index 47bb04e2d4..57127b9269 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts @@ -119,12 +119,11 @@ describe('V1 Proofs - Connectionless - Indy', () => { }, }) - const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, - message, - domain: 'https://a-domain.com', + const outOfBandRecord = await faberAgent.oob.createInvitation({ + messages: [message], + handshake: false, }) - await aliceAgent.receiveMessage(requestMessage.toJSON()) + await aliceAgent.oob.receiveInvitation(outOfBandRecord.outOfBandInvitation) testLogger.test('Alice waits for presentation request from Faber') let aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { @@ -295,7 +294,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { agents = [aliceAgent, faberAgent] - const { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + const { message } = await faberAgent.proofs.createRequest({ protocolVersion: 'v1', proofFormats: { indy: { @@ -328,8 +327,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, + const { invitationUrl, message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ message, domain: 'https://a-domain.com', }) @@ -338,7 +336,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { await faberAgent.unregisterOutboundTransport(transport) } - await aliceAgent.receiveMessage(requestMessage.toJSON()) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) await waitForProofExchangeRecordSubject(aliceReplay, { state: ProofState.Done, @@ -448,11 +446,6 @@ describe('V1 Proofs - Connectionless - Indy', () => { expect(faberConnection.isReady).toBe(true) expect(aliceConnection.isReady).toBe(true) - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - await issueLegacyAnonCredsCredential({ issuerAgent: faberAgent as AnonCredsTestsAgent, issuerReplay: faberReplay, diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts index 41bcd9b4ae..38b7a97a78 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts @@ -1,9 +1,9 @@ import type { V1ProofProtocol } from '../V1ProofProtocol' import type { MessageHandler, MessageHandlerInboundMessage, ProofExchangeRecord } from '@aries-framework/core' -import { OutboundMessageContext, DidCommMessageRepository } from '@aries-framework/core' +import { AriesFrameworkError, getOutboundMessageContext } from '@aries-framework/core' -import { V1PresentationMessage, V1RequestPresentationMessage } from '../messages' +import { V1PresentationMessage } from '../messages' export class V1PresentationHandler implements MessageHandler { private proofProtocol: V1ProofProtocol @@ -32,47 +32,21 @@ export class V1PresentationHandler implements MessageHandler { ) { messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) - if (messageContext.connection) { - const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { - proofRecord, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: proofRecord, - }) - } else if (messageContext.message.service) { - const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { - proofRecord, - }) - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - const requestMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - const recipientService = messageContext.message.service - const ourService = requestMessage?.service - - if (!ourService) { - messageContext.agentContext.config.logger.error( - `Could not automatically create presentation ack. Missing ourService on request message` - ) - return - } - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) + const requestMessage = await this.proofProtocol.findRequestMessage(messageContext.agentContext, proofRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) } - messageContext.agentContext.config.logger.error(`Could not automatically create presentation ack`) + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) } } diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts index a697f0da90..49dc7f95ba 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts @@ -1,13 +1,7 @@ import type { V1ProofProtocol } from '../V1ProofProtocol' import type { MessageHandler, MessageHandlerInboundMessage, ProofExchangeRecord } from '@aries-framework/core' -import { - OutboundMessageContext, - RoutingService, - ServiceDecorator, - DidCommMessageRepository, - DidCommMessageRole, -} from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { V1RequestPresentationMessage } from '../messages' @@ -38,50 +32,15 @@ export class V1RequestPresentationHandler implements MessageHandler { ) { messageContext.agentContext.config.logger.info(`Automatically sending presentation with autoAccept on`) - if (messageContext.connection) { - const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { - proofRecord, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: proofRecord, - }) - } else if (messageContext.message.service) { - const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { - proofRecord, - }) - - const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) - const routing = await routingService.getRouting(messageContext.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: message.service.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - } + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, + }) - messageContext.agentContext.config.logger.error(`Could not automatically create presentation`) + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) } } diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index 7f2d7763fe..d995623bb0 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -9,6 +9,7 @@ export { encodeCredentialValue, checkValidCredentialValueEncoding } from './cred export { IsMap } from './isMap' export { composeCredentialAutoAccept, composeProofAutoAccept } from './composeAutoAccept' export { areCredentialPreviewAttributesEqual } from './credentialPreviewAttributes' +export { storeLinkSecret } from './linkSecret' export { unqualifiedCredentialDefinitionIdRegex, unqualifiedIndyDidRegex, diff --git a/packages/anoncreds/src/utils/linkSecret.ts b/packages/anoncreds/src/utils/linkSecret.ts new file mode 100644 index 0000000000..b301c9717d --- /dev/null +++ b/packages/anoncreds/src/utils/linkSecret.ts @@ -0,0 +1,30 @@ +import type { AgentContext } from '@aries-framework/core' + +import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../repository' + +export async function storeLinkSecret( + agentContext: AgentContext, + options: { linkSecretId: string; linkSecretValue?: string; setAsDefault?: boolean } +) { + const { linkSecretId, linkSecretValue, setAsDefault } = options + const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) + + // In some cases we don't have the linkSecretValue. However we still want a record so we know which link secret ids are valid + const linkSecretRecord = new AnonCredsLinkSecretRecord({ linkSecretId, value: linkSecretValue }) + + // If it is the first link secret registered, set as default + const defaultLinkSecretRecord = await linkSecretRepository.findDefault(agentContext) + if (!defaultLinkSecretRecord || setAsDefault) { + linkSecretRecord.setTag('isDefault', true) + } + + // Set the current default link secret as not default + if (defaultLinkSecretRecord && setAsDefault) { + defaultLinkSecretRecord.setTag('isDefault', false) + await linkSecretRepository.update(agentContext, defaultLinkSecretRecord) + } + + await linkSecretRepository.save(agentContext, linkSecretRecord) + + return linkSecretRecord +} diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index d5590ca4ba..d56dc4b630 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -83,6 +83,7 @@ const agent = new Agent({ modules: { indySdk: new IndySdkModule({ indySdk, + autoCreateLinkSecret: false, }), anoncreds: new AnonCredsModule({ registries: [ diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 5517779900..39e9b53c47 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -402,12 +402,6 @@ export async function setupAnonCredsTests< await holderAgent.initialize() if (verifierAgent) await verifierAgent.initialize() - // Create default link secret for holder - await holderAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition, schema } = await prepareForAnonCredsIssuance(issuerAgent, { attributeNames, }) diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts index c9f8c07973..440676fce8 100644 --- a/packages/askar/src/AskarModule.ts +++ b/packages/askar/src/AskarModule.ts @@ -1,11 +1,13 @@ import type { AskarModuleConfigOptions } from './AskarModuleConfig' -import type { DependencyManager, Module } from '@aries-framework/core' +import type { AgentContext, DependencyManager, Module } from '@aries-framework/core' import { AgentConfig, AriesFrameworkError, InjectionSymbols } from '@aries-framework/core' +import { Store } from '@hyperledger/aries-askar-shared' -import { AskarModuleConfig } from './AskarModuleConfig' +import { AskarMultiWalletDatabaseScheme, AskarModuleConfig } from './AskarModuleConfig' import { AskarStorageService } from './storage' -import { AskarWallet } from './wallet' +import { assertAskarWallet } from './utils/assertAskarWallet' +import { AskarProfileWallet, AskarWallet } from './wallet' export class AskarModule implements Module { public readonly config: AskarModuleConfig @@ -28,6 +30,11 @@ export class AskarModule implements Module { throw new AriesFrameworkError('There is an instance of Wallet already registered') } else { dependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + + // If the multiWalletDatabaseScheme is set to ProfilePerWallet, we want to register the AskarProfileWallet + if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) { + dependencyManager.registerContextScoped(AskarProfileWallet) + } } if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { @@ -36,4 +43,32 @@ export class AskarModule implements Module { dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) } } + + public async initialize(agentContext: AgentContext): Promise { + // We MUST use an askar wallet here + assertAskarWallet(agentContext.wallet) + + const wallet = agentContext.wallet + + // Register the Askar store instance on the dependency manager + // This allows it to be re-used for tenants + agentContext.dependencyManager.registerInstance(Store, agentContext.wallet.store) + + // If the multiWalletDatabaseScheme is set to ProfilePerWallet, we want to register the AskarProfileWallet + // and return that as the wallet for all tenants, but not for the main agent, that should use the AskarWallet + if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) { + agentContext.dependencyManager.container.register(InjectionSymbols.Wallet, { + useFactory: (container) => { + // If the container is the same as the root dependency manager container + // it means we are in the main agent, and we should use the root wallet + if (container === agentContext.dependencyManager.container) { + return wallet + } + + // Otherwise we want to return the AskarProfileWallet + return container.resolve(AskarProfileWallet) + }, + }) + } + } } diff --git a/packages/askar/src/AskarModuleConfig.ts b/packages/askar/src/AskarModuleConfig.ts index 38eebdde86..91ec72ed5b 100644 --- a/packages/askar/src/AskarModuleConfig.ts +++ b/packages/askar/src/AskarModuleConfig.ts @@ -1,5 +1,17 @@ import type { AriesAskar } from '@hyperledger/aries-askar-shared' +export enum AskarMultiWalletDatabaseScheme { + /** + * Each wallet get its own database and uses a separate store. + */ + DatabasePerWallet = 'DatabasePerWallet', + + /** + * All wallets are stored in a single database, but each wallet uses a separate profile. + */ + ProfilePerWallet = 'ProfilePerWallet', +} + export interface AskarModuleConfigOptions { /** * @@ -36,6 +48,16 @@ export interface AskarModuleConfigOptions { * ``` */ ariesAskar: AriesAskar + + /** + * Determine the strategy for storing wallets if multiple wallets are used in a single agent. + * This is mostly the case in multi-tenancy, and determines whether each tenant will get a separate + * database, or whether all wallets will be stored in a single database, using a different profile + * for each wallet. + * + * @default {@link AskarMultiWalletDatabaseScheme.DatabasePerWallet} (for backwards compatibility) + */ + multiWalletDatabaseScheme?: AskarMultiWalletDatabaseScheme } /** @@ -48,7 +70,13 @@ export class AskarModuleConfig { this.options = options } + /** See {@link AskarModuleConfigOptions.ariesAskar} */ public get ariesAskar() { return this.options.ariesAskar } + + /** See {@link AskarModuleConfigOptions.multiWalletDatabaseScheme} */ + public get multiWalletDatabaseScheme() { + return this.options.multiWalletDatabaseScheme ?? AskarMultiWalletDatabaseScheme.DatabasePerWallet + } } diff --git a/packages/askar/src/index.ts b/packages/askar/src/index.ts index 438ae1d7f9..532ef7c842 100644 --- a/packages/askar/src/index.ts +++ b/packages/askar/src/index.ts @@ -4,6 +4,7 @@ export { AskarWalletPostgresStorageConfig, AskarWalletPostgresConfig, AskarWalletPostgresCredentials, + AskarProfileWallet, } from './wallet' // Storage @@ -11,3 +12,4 @@ export { AskarStorageService } from './storage' // Module export { AskarModule } from './AskarModule' +export { AskarModuleConfigOptions, AskarMultiWalletDatabaseScheme } from './AskarModuleConfig' diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 2174291c81..17bec8917a 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -140,15 +140,16 @@ export class AskarStorageService implements StorageService recordClass: BaseRecordConstructor, query: Query ): Promise { - assertAskarWallet(agentContext.wallet) - const store = agentContext.wallet.store + const wallet = agentContext.wallet + assertAskarWallet(wallet) const askarQuery = askarQueryFromSearchQuery(query) const scan = new Scan({ category: recordClass.type, - store, + store: wallet.store, tagFilter: askarQuery, + profile: wallet.profile, }) const instances = [] diff --git a/packages/askar/src/utils/assertAskarWallet.ts b/packages/askar/src/utils/assertAskarWallet.ts index 37213e3d28..3c01f51a7e 100644 --- a/packages/askar/src/utils/assertAskarWallet.ts +++ b/packages/askar/src/utils/assertAskarWallet.ts @@ -2,12 +2,14 @@ import type { Wallet } from '@aries-framework/core' import { AriesFrameworkError } from '@aries-framework/core' -import { AskarWallet } from '../wallet/AskarWallet' +import { AskarWallet, AskarProfileWallet } from '../wallet' -export function assertAskarWallet(wallet: Wallet): asserts wallet is AskarWallet { - if (!(wallet instanceof AskarWallet)) { +export function assertAskarWallet(wallet: Wallet): asserts wallet is AskarProfileWallet | AskarWallet { + if (!(wallet instanceof AskarProfileWallet) && !(wallet instanceof AskarWallet)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const walletClassName = (wallet as any).constructor?.name ?? 'unknown' - throw new AriesFrameworkError(`Expected wallet to be instance of AskarWallet, found ${walletClassName}`) + throw new AriesFrameworkError( + `Expected wallet to be instance of AskarProfileWallet or AskarWallet, found ${walletClassName}` + ) } } diff --git a/packages/askar/src/wallet/AskarBaseWallet.ts b/packages/askar/src/wallet/AskarBaseWallet.ts new file mode 100644 index 0000000000..46c01a9cae --- /dev/null +++ b/packages/askar/src/wallet/AskarBaseWallet.ts @@ -0,0 +1,515 @@ +import type { + EncryptedMessage, + WalletConfig, + WalletCreateKeyOptions, + WalletSignOptions, + UnpackedMessageContext, + WalletVerifyOptions, + Wallet, + WalletConfigRekey, + KeyPair, + WalletExportImportConfig, + Logger, + SigningProviderRegistry, +} from '@aries-framework/core' +import type { KeyEntryObject, Session } from '@hyperledger/aries-askar-shared' + +import { + WalletKeyExistsError, + isValidSeed, + isValidPrivateKey, + JsonTransformer, + JsonEncoder, + KeyType, + Buffer, + AriesFrameworkError, + WalletError, + Key, + TypedArrayEncoder, +} from '@aries-framework/core' +import { KeyAlgs, CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared' +// eslint-disable-next-line import/order +import BigNumber from 'bn.js' + +const isError = (error: unknown): error is Error => error instanceof Error + +import { AskarErrorCode, isAskarError, isKeyTypeSupportedByAskar, keyTypesSupportedByAskar } from '../utils' + +import { JweEnvelope, JweRecipient } from './JweEnvelope' + +export abstract class AskarBaseWallet implements Wallet { + protected _session?: Session + + protected logger: Logger + protected signingKeyProviderRegistry: SigningProviderRegistry + + public constructor(logger: Logger, signingKeyProviderRegistry: SigningProviderRegistry) { + this.logger = logger + this.signingKeyProviderRegistry = signingKeyProviderRegistry + } + + /** + * Abstract methods that need to be implemented by subclasses + */ + public abstract isInitialized: boolean + public abstract isProvisioned: boolean + public abstract create(walletConfig: WalletConfig): Promise + public abstract createAndOpen(walletConfig: WalletConfig): Promise + public abstract open(walletConfig: WalletConfig): Promise + public abstract rotateKey(walletConfig: WalletConfigRekey): Promise + public abstract close(): Promise + public abstract delete(): Promise + public abstract export(exportConfig: WalletExportImportConfig): Promise + public abstract import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise + public abstract dispose(): void | Promise + public abstract profile: string + + public get session() { + if (!this._session) { + throw new AriesFrameworkError('No Wallet Session is opened') + } + + return this._session + } + + public get supportedKeyTypes() { + const signingKeyProviderSupportedKeyTypes = this.signingKeyProviderRegistry.supportedKeyTypes + + return Array.from(new Set([...keyTypesSupportedByAskar, ...signingKeyProviderSupportedKeyTypes])) + } + + /** + * Create a key with an optional seed and keyType. + * The keypair is also automatically stored in the wallet afterwards + */ + public async createKey({ seed, privateKey, keyType }: WalletCreateKeyOptions): Promise { + try { + if (seed && privateKey) { + throw new WalletError('Only one of seed and privateKey can be set') + } + + if (seed && !isValidSeed(seed, keyType)) { + throw new WalletError('Invalid seed provided') + } + + if (privateKey && !isValidPrivateKey(privateKey, keyType)) { + throw new WalletError('Invalid private key provided') + } + + if (isKeyTypeSupportedByAskar(keyType)) { + const algorithm = keyAlgFromString(keyType) + + // Create key + let key: AskarKey | undefined + try { + const key = privateKey + ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) + : seed + ? AskarKey.fromSeed({ seed, algorithm }) + : AskarKey.generate(algorithm) + + const keyPublicBytes = key.publicBytes + // Store key + await this.session.insertKey({ key, name: TypedArrayEncoder.toBase58(keyPublicBytes) }) + key.handle.free() + return Key.fromPublicKey(keyPublicBytes, keyType) + } catch (error) { + key?.handle.free() + // Handle case where key already exists + if (isAskarError(error, AskarErrorCode.Duplicate)) { + throw new WalletKeyExistsError('Key already exists') + } + + // Otherwise re-throw error + throw error + } + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(keyType) + + const keyPair = await signingKeyProvider.createKeyPair({ seed, privateKey }) + await this.storeKeyPair(keyPair) + return Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyType) + } + throw new WalletError(`Unsupported key type: '${keyType}'`) + } + } catch (error) { + // If already instance of `WalletError`, re-throw + if (error instanceof WalletError) throw error + + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) + } + } + + /** + * sign a Buffer with an instance of a Key class + * + * @param data Buffer The data that needs to be signed + * @param key Key The key that is used to sign the data + * + * @returns A signature for the data + */ + public async sign({ data, key }: WalletSignOptions): Promise { + let keyEntry: KeyEntryObject | null | undefined + try { + if (isKeyTypeSupportedByAskar(key.keyType)) { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) + + if (!keyEntry) { + throw new WalletError('Key entry not found') + } + + const signed = keyEntry.key.signMessage({ message: data as Buffer }) + + keyEntry.key.handle.free() + + return Buffer.from(signed) + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) + + const keyPair = await this.retrieveKeyPair(key.publicKeyBase58) + const signed = await signingKeyProvider.sign({ + data, + privateKeyBase58: keyPair.privateKeyBase58, + publicKeyBase58: key.publicKeyBase58, + }) + + return signed + } + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + } catch (error) { + keyEntry?.key.handle.free() + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}. ${error.message}`, { cause: error }) + } + } + + /** + * Verify the signature with the data and the used key + * + * @param data Buffer The data that has to be confirmed to be signed + * @param key Key The key that was used in the signing process + * @param signature Buffer The signature that was created by the signing process + * + * @returns A boolean whether the signature was created with the supplied data and key + * + * @throws {WalletError} When it could not do the verification + * @throws {WalletError} When an unsupported keytype is used + */ + public async verify({ data, key, signature }: WalletVerifyOptions): Promise { + let askarKey: AskarKey | undefined + try { + if (isKeyTypeSupportedByAskar(key.keyType)) { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting verification of multiple messages`) + } + + const askarKey = AskarKey.fromPublicBytes({ + algorithm: keyAlgFromString(key.keyType), + publicKey: key.publicKey, + }) + const verified = askarKey.verifySignature({ message: data as Buffer, signature }) + askarKey.handle.free() + return verified + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) + + const signed = await signingKeyProvider.verify({ + data, + signature, + publicKeyBase58: key.publicKeyBase58, + }) + + return signed + } + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + } catch (error) { + askarKey?.handle.free() + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error verifying signature of data signed with verkey ${key.publicKeyBase58}`, { + cause: error, + }) + } + } + + /** + * Pack a message using DIDComm V1 algorithm + * + * @param payload message to send + * @param recipientKeys array containing recipient keys in base58 + * @param senderVerkey sender key in base58 + * @returns JWE Envelope to send + */ + public async pack( + payload: Record, + recipientKeys: string[], + senderVerkey?: string // in base58 + ): Promise { + let cek: AskarKey | undefined + let senderKey: KeyEntryObject | null | undefined + let senderExchangeKey: AskarKey | undefined + + try { + cek = AskarKey.generate(KeyAlgs.Chacha20C20P) + + senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined + if (senderVerkey && !senderKey) { + throw new WalletError(`Unable to pack message. Sender key ${senderVerkey} not found in wallet.`) + } + + senderExchangeKey = senderKey ? senderKey.key.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined + + const recipients: JweRecipient[] = [] + + for (const recipientKey of recipientKeys) { + let targetExchangeKey: AskarKey | undefined + try { + targetExchangeKey = AskarKey.fromPublicBytes({ + publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, + algorithm: KeyAlgs.Ed25519, + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + if (senderVerkey && senderExchangeKey) { + const encryptedSender = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: Buffer.from(senderVerkey), + }) + const nonce = CryptoBox.randomNonce() + const encryptedCek = CryptoBox.cryptoBox({ + recipientKey: targetExchangeKey, + senderKey: senderExchangeKey, + message: cek.secretBytes, + nonce, + }) + + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + sender: TypedArrayEncoder.toBase64URL(encryptedSender), + iv: TypedArrayEncoder.toBase64URL(nonce), + }, + }) + ) + } else { + const encryptedCek = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: cek.secretBytes, + }) + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + }, + }) + ) + } + } finally { + targetExchangeKey?.handle.free() + } + } + + const protectedJson = { + enc: 'xchacha20poly1305_ietf', + typ: 'JWM/1.0', + alg: senderVerkey ? 'Authcrypt' : 'Anoncrypt', + recipients: recipients.map((item) => JsonTransformer.toJSON(item)), + } + + const { ciphertext, tag, nonce } = cek.aeadEncrypt({ + message: Buffer.from(JSON.stringify(payload)), + aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), + }).parts + + const envelope = new JweEnvelope({ + ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), + iv: TypedArrayEncoder.toBase64URL(nonce), + protected: JsonEncoder.toBase64URL(protectedJson), + tag: TypedArrayEncoder.toBase64URL(tag), + }).toJson() + + return envelope as EncryptedMessage + } finally { + cek?.handle.free() + senderKey?.key.handle.free() + senderExchangeKey?.handle.free() + } + } + + /** + * Unpacks a JWE Envelope coded using DIDComm V1 algorithm + * + * @param messagePackage JWE Envelope + * @returns UnpackedMessageContext with plain text message, sender key and recipient key + */ + public async unpack(messagePackage: EncryptedMessage): Promise { + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + + const alg = protectedJson.alg + if (!['Anoncrypt', 'Authcrypt'].includes(alg)) { + throw new WalletError(`Unsupported pack algorithm: ${alg}`) + } + + const recipients = [] + + for (const recip of protectedJson.recipients) { + const kid = recip.header.kid + if (!kid) { + throw new WalletError('Blank recipient key') + } + const sender = recip.header.sender ? TypedArrayEncoder.fromBase64(recip.header.sender) : undefined + const iv = recip.header.iv ? TypedArrayEncoder.fromBase64(recip.header.iv) : undefined + if (sender && !iv) { + throw new WalletError('Missing IV') + } else if (!sender && iv) { + throw new WalletError('Unexpected IV') + } + recipients.push({ + kid, + sender, + iv, + encrypted_key: TypedArrayEncoder.fromBase64(recip.encrypted_key), + }) + } + + let payloadKey, senderKey, recipientKey + + for (const recipient of recipients) { + let recipientKeyEntry: KeyEntryObject | null | undefined + let sender_x: AskarKey | undefined + let recip_x: AskarKey | undefined + + try { + recipientKeyEntry = await this.session.fetchKey({ name: recipient.kid }) + if (recipientKeyEntry) { + const recip_x = recipientKeyEntry.key.convertkey({ algorithm: KeyAlgs.X25519 }) + recipientKey = recipient.kid + + if (recipient.sender && recipient.iv) { + senderKey = TypedArrayEncoder.toUtf8String( + CryptoBox.sealOpen({ + recipientKey: recip_x, + ciphertext: recipient.sender, + }) + ) + const sender_x = AskarKey.fromPublicBytes({ + algorithm: KeyAlgs.Ed25519, + publicKey: TypedArrayEncoder.fromBase58(senderKey), + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + payloadKey = CryptoBox.open({ + recipientKey: recip_x, + senderKey: sender_x, + message: recipient.encrypted_key, + nonce: recipient.iv, + }) + } else { + payloadKey = CryptoBox.sealOpen({ ciphertext: recipient.encrypted_key, recipientKey: recip_x }) + } + break + } + } finally { + recipientKeyEntry?.key.handle.free() + sender_x?.handle.free() + recip_x?.handle.free() + } + } + if (!payloadKey) { + throw new WalletError('No corresponding recipient key found') + } + + if (!senderKey && alg === 'Authcrypt') { + throw new WalletError('Sender public key not provided for Authcrypt') + } + + let cek: AskarKey | undefined + try { + cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) + const message = cek.aeadDecrypt({ + ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext), + nonce: TypedArrayEncoder.fromBase64(messagePackage.iv), + tag: TypedArrayEncoder.fromBase64(messagePackage.tag), + aad: TypedArrayEncoder.fromString(messagePackage.protected), + }) + return { + plaintextMessage: JsonEncoder.fromBuffer(message), + senderKey, + recipientKey, + } + } finally { + cek?.handle.free() + } + } + + public async generateNonce(): Promise { + try { + // generate an 80-bit nonce suitable for AnonCreds proofs + const nonce = CryptoBox.randomNonce().slice(0, 10) + return new BigNumber(nonce).toString() + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError('Error generating nonce', { cause: error }) + } + } + + public async generateWalletKey() { + try { + return Store.generateRawKey() + } catch (error) { + throw new WalletError('Error generating wallet key', { cause: error }) + } + } + + private async retrieveKeyPair(publicKeyBase58: string): Promise { + try { + const entryObject = await this.session.fetch({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }) + + if (entryObject?.value) { + return JsonEncoder.fromString(entryObject?.value as string) as KeyPair + } else { + throw new WalletError(`No content found for record with public key: ${publicKeyBase58}`) + } + } catch (error) { + throw new WalletError('Error retrieving KeyPair record', { cause: error }) + } + } + + private async storeKeyPair(keyPair: KeyPair): Promise { + try { + await this.session.insert({ + category: 'KeyPairRecord', + name: `key-${keyPair.publicKeyBase58}`, + value: JSON.stringify(keyPair), + tags: { + keyType: keyPair.keyType, + }, + }) + } catch (error) { + if (isAskarError(error, AskarErrorCode.Duplicate)) { + throw new WalletKeyExistsError('Key already exists') + } + throw new WalletError('Error saving KeyPair record', { cause: error }) + } + } +} diff --git a/packages/askar/src/wallet/AskarProfileWallet.ts b/packages/askar/src/wallet/AskarProfileWallet.ts new file mode 100644 index 0000000000..aaeb79bda9 --- /dev/null +++ b/packages/askar/src/wallet/AskarProfileWallet.ts @@ -0,0 +1,196 @@ +import type { WalletConfig } from '@aries-framework/core' + +import { + WalletDuplicateError, + WalletNotFoundError, + InjectionSymbols, + Logger, + SigningProviderRegistry, + WalletError, +} from '@aries-framework/core' +import { Store } from '@hyperledger/aries-askar-shared' +import { inject, injectable } from 'tsyringe' + +import { AskarErrorCode, isAskarError } from '../utils' + +import { AskarBaseWallet } from './AskarBaseWallet' + +@injectable() +export class AskarProfileWallet extends AskarBaseWallet { + private walletConfig?: WalletConfig + public readonly store: Store + + public constructor( + store: Store, + @inject(InjectionSymbols.Logger) logger: Logger, + signingKeyProviderRegistry: SigningProviderRegistry + ) { + super(logger, signingKeyProviderRegistry) + + this.store = store + } + + public get isInitialized() { + return this._session !== undefined + } + + public get isProvisioned() { + return this.walletConfig !== undefined + } + + public get profile() { + if (!this.walletConfig) { + throw new WalletError('No profile configured.') + } + + return this.walletConfig.id + } + + /** + * Dispose method is called when an agent context is disposed. + */ + public async dispose() { + if (this.isInitialized) { + await this.close() + } + } + + public async create(walletConfig: WalletConfig): Promise { + this.logger.debug(`Creating wallet for profile '${walletConfig.id}'`) + + try { + await this.store.createProfile(walletConfig.id) + } catch (error) { + if (isAskarError(error, AskarErrorCode.Duplicate)) { + const errorMessage = `Wallet for profile '${walletConfig.id}' already exists` + this.logger.debug(errorMessage) + + throw new WalletDuplicateError(errorMessage, { + walletType: 'AskarProfileWallet', + cause: error, + }) + } + + const errorMessage = `Error creating wallet for profile '${walletConfig.id}'` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + + this.logger.debug(`Successfully created wallet for profile '${walletConfig.id}'`) + } + + public async open(walletConfig: WalletConfig): Promise { + this.logger.debug(`Opening wallet for profile '${walletConfig.id}'`) + + try { + this.walletConfig = walletConfig + + this._session = await this.store.session(walletConfig.id).open() + + // FIXME: opening a session for a profile that does not exist, will not throw an error until + // the session is actually used. We can check if the profile exists by doing something with + // the session, which will throw a not found error if the profile does not exists, + // but that is not very efficient as it needs to be done on every open. + // See: https://github.com/hyperledger/aries-askar/issues/163 + await this._session.fetch({ + category: 'fetch-to-see-if-profile-exists', + name: 'fetch-to-see-if-profile-exists', + forUpdate: false, + isJson: false, + }) + } catch (error) { + // Profile does not exist + if (isAskarError(error, AskarErrorCode.NotFound)) { + const errorMessage = `Wallet for profile '${walletConfig.id}' not found` + this.logger.debug(errorMessage) + + throw new WalletNotFoundError(errorMessage, { + walletType: 'AskarProfileWallet', + cause: error, + }) + } + + const errorMessage = `Error opening wallet for profile '${walletConfig.id}'` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + + this.logger.debug(`Successfully opened wallet for profile '${walletConfig.id}'`) + } + + public async createAndOpen(walletConfig: WalletConfig): Promise { + await this.create(walletConfig) + await this.open(walletConfig) + } + + public async delete() { + if (!this.walletConfig) { + throw new WalletError( + 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' + ) + } + + this.logger.info(`Deleting profile '${this.walletConfig.id}'`) + + if (this._session) { + await this.close() + } + + try { + await this.store.removeProfile(this.walletConfig.id) + } catch (error) { + const errorMessage = `Error deleting wallet for profile '${this.walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async export() { + // This PR should help with this: https://github.com/hyperledger/aries-askar/pull/159 + throw new WalletError('Exporting a profile is not supported.') + } + + public async import() { + // This PR should help with this: https://github.com/hyperledger/aries-askar/pull/159 + throw new WalletError('Importing a profile is not supported.') + } + + public async rotateKey(): Promise { + throw new WalletError( + 'Rotating a key is not supported for a profile. You can rotate the key on the main askar wallet.' + ) + } + + public async close() { + this.logger.debug(`Closing wallet for profile ${this.walletConfig?.id}`) + + if (!this._session) { + throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no handle.') + } + + try { + await this.session.close() + this._session = undefined + } catch (error) { + const errorMessage = `Error closing wallet for profile ${this.walletConfig?.id}: ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } +} diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 06d927ec38..7a46016027 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -1,78 +1,45 @@ -import type { - EncryptedMessage, - WalletConfig, - WalletCreateKeyOptions, - WalletSignOptions, - UnpackedMessageContext, - WalletVerifyOptions, - Wallet, - WalletConfigRekey, - KeyPair, - WalletExportImportConfig, -} from '@aries-framework/core' -import type { KeyEntryObject, Session } from '@hyperledger/aries-askar-shared' +import type { WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '@aries-framework/core' import { WalletExportPathExistsError, - WalletKeyExistsError, - isValidSeed, - isValidPrivateKey, - JsonTransformer, WalletInvalidKeyError, WalletDuplicateError, - JsonEncoder, - KeyType, - Buffer, AriesFrameworkError, Logger, WalletError, InjectionSymbols, - Key, SigningProviderRegistry, - TypedArrayEncoder, FileSystem, WalletNotFoundError, KeyDerivationMethod, } from '@aries-framework/core' -import { KeyAlgs, CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared' // eslint-disable-next-line import/order -import BigNumber from 'bn.js' - -const isError = (error: unknown): error is Error => error instanceof Error +import { Store } from '@hyperledger/aries-askar-shared' import { inject, injectable } from 'tsyringe' -import { - AskarErrorCode, - isAskarError, - keyDerivationMethodToStoreKeyMethod, - isKeyTypeSupportedByAskar, - uriFromWalletConfig, - keyTypesSupportedByAskar, -} from '../utils' +import { AskarErrorCode, isAskarError, keyDerivationMethodToStoreKeyMethod, uriFromWalletConfig } from '../utils' -import { JweEnvelope, JweRecipient } from './JweEnvelope' +import { AskarBaseWallet } from './AskarBaseWallet' +import { AskarProfileWallet } from './AskarProfileWallet' +/** + * @todo: rename after 0.5.0, as we now have multiple types of AskarWallet + */ @injectable() -export class AskarWallet implements Wallet { - private walletConfig?: WalletConfig - private _session?: Session - - private _store?: Store - - private logger: Logger +export class AskarWallet extends AskarBaseWallet { private fileSystem: FileSystem - private signingKeyProviderRegistry: SigningProviderRegistry + private walletConfig?: WalletConfig + private _store?: Store public constructor( @inject(InjectionSymbols.Logger) logger: Logger, @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, signingKeyProviderRegistry: SigningProviderRegistry ) { - this.logger = logger + super(logger, signingKeyProviderRegistry) this.fileSystem = fileSystem - this.signingKeyProviderRegistry = signingKeyProviderRegistry } public get isProvisioned() { @@ -93,18 +60,12 @@ export class AskarWallet implements Wallet { return this._store } - public get session() { - if (!this._session) { - throw new AriesFrameworkError('No Wallet Session is opened') + public get profile() { + if (!this.walletConfig) { + throw new WalletError('No profile configured.') } - return this._session - } - - public get supportedKeyTypes() { - const signingKeyProviderSupportedKeyTypes = this.signingKeyProviderRegistry.supportedKeyTypes - - return Array.from(new Set([...keyTypesSupportedByAskar, ...signingKeyProviderSupportedKeyTypes])) + return this.walletConfig.id } /** @@ -125,6 +86,14 @@ export class AskarWallet implements Wallet { await this.close() } + /** + * TODO: we can add this method, and add custom logic in the tenants module + * or we can try to register the store on the agent context + */ + public async getProfileWallet() { + return new AskarProfileWallet(this.store, this.logger, this.signingKeyProviderRegistry) + } + /** * @throws {WalletDuplicateError} if the wallet already exists * @throws {WalletError} if another error occurs @@ -240,7 +209,11 @@ export class AskarWallet implements Wallet { this.walletConfig = walletConfig } catch (error) { - if (isAskarError(error) && error.code === AskarErrorCode.NotFound) { + if ( + isAskarError(error) && + (error.code === AskarErrorCode.NotFound || + (error.code === AskarErrorCode.Backend && walletConfig.storage?.inMemory)) + ) { const errorMessage = `Wallet '${walletConfig.id}' not found` this.logger.debug(errorMessage) @@ -411,409 +384,6 @@ export class AskarWallet implements Wallet { } } - /** - * Create a key with an optional seed and keyType. - * The keypair is also automatically stored in the wallet afterwards - */ - public async createKey({ seed, privateKey, keyType }: WalletCreateKeyOptions): Promise { - try { - if (seed && privateKey) { - throw new WalletError('Only one of seed and privateKey can be set') - } - - if (seed && !isValidSeed(seed, keyType)) { - throw new WalletError('Invalid seed provided') - } - - if (privateKey && !isValidPrivateKey(privateKey, keyType)) { - throw new WalletError('Invalid private key provided') - } - - if (isKeyTypeSupportedByAskar(keyType)) { - const algorithm = keyAlgFromString(keyType) - - // Create key - let key: AskarKey | undefined - try { - const key = privateKey - ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) - : seed - ? AskarKey.fromSeed({ seed, algorithm }) - : AskarKey.generate(algorithm) - - const keyPublicBytes = key.publicBytes - // Store key - await this.session.insertKey({ key, name: TypedArrayEncoder.toBase58(keyPublicBytes) }) - key.handle.free() - return Key.fromPublicKey(keyPublicBytes, keyType) - } catch (error) { - key?.handle.free() - // Handle case where key already exists - if (isAskarError(error, AskarErrorCode.Duplicate)) { - throw new WalletKeyExistsError('Key already exists') - } - - // Otherwise re-throw error - throw error - } - } else { - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(keyType) - - const keyPair = await signingKeyProvider.createKeyPair({ seed, privateKey }) - await this.storeKeyPair(keyPair) - return Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyType) - } - throw new WalletError(`Unsupported key type: '${keyType}'`) - } - } catch (error) { - // If already instance of `WalletError`, re-throw - if (error instanceof WalletError) throw error - - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) - } - } - - /** - * sign a Buffer with an instance of a Key class - * - * @param data Buffer The data that needs to be signed - * @param key Key The key that is used to sign the data - * - * @returns A signature for the data - */ - public async sign({ data, key }: WalletSignOptions): Promise { - let keyEntry: KeyEntryObject | null | undefined - try { - if (isKeyTypeSupportedByAskar(key.keyType)) { - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError(`Currently not supporting signing of multiple messages`) - } - keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) - - if (!keyEntry) { - throw new WalletError('Key entry not found') - } - - const signed = keyEntry.key.signMessage({ message: data as Buffer }) - - keyEntry.key.handle.free() - - return Buffer.from(signed) - } else { - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - - const keyPair = await this.retrieveKeyPair(key.publicKeyBase58) - const signed = await signingKeyProvider.sign({ - data, - privateKeyBase58: keyPair.privateKeyBase58, - publicKeyBase58: key.publicKeyBase58, - }) - - return signed - } - throw new WalletError(`Unsupported keyType: ${key.keyType}`) - } - } catch (error) { - keyEntry?.key.handle.free() - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}. ${error.message}`, { cause: error }) - } - } - - /** - * Verify the signature with the data and the used key - * - * @param data Buffer The data that has to be confirmed to be signed - * @param key Key The key that was used in the signing process - * @param signature Buffer The signature that was created by the signing process - * - * @returns A boolean whether the signature was created with the supplied data and key - * - * @throws {WalletError} When it could not do the verification - * @throws {WalletError} When an unsupported keytype is used - */ - public async verify({ data, key, signature }: WalletVerifyOptions): Promise { - let askarKey: AskarKey | undefined - try { - if (isKeyTypeSupportedByAskar(key.keyType)) { - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError(`Currently not supporting verification of multiple messages`) - } - - const askarKey = AskarKey.fromPublicBytes({ - algorithm: keyAlgFromString(key.keyType), - publicKey: key.publicKey, - }) - const verified = askarKey.verifySignature({ message: data as Buffer, signature }) - askarKey.handle.free() - return verified - } else { - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - - const signed = await signingKeyProvider.verify({ - data, - signature, - publicKeyBase58: key.publicKeyBase58, - }) - - return signed - } - throw new WalletError(`Unsupported keyType: ${key.keyType}`) - } - } catch (error) { - askarKey?.handle.free() - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error verifying signature of data signed with verkey ${key.publicKeyBase58}`, { - cause: error, - }) - } - } - - /** - * Pack a message using DIDComm V1 algorithm - * - * @param payload message to send - * @param recipientKeys array containing recipient keys in base58 - * @param senderVerkey sender key in base58 - * @returns JWE Envelope to send - */ - public async pack( - payload: Record, - recipientKeys: string[], - senderVerkey?: string // in base58 - ): Promise { - let cek: AskarKey | undefined - let senderKey: KeyEntryObject | null | undefined - let senderExchangeKey: AskarKey | undefined - - try { - cek = AskarKey.generate(KeyAlgs.Chacha20C20P) - - senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined - if (senderVerkey && !senderKey) { - throw new WalletError(`Unable to pack message. Sender key ${senderVerkey} not found in wallet.`) - } - - senderExchangeKey = senderKey ? senderKey.key.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined - - const recipients: JweRecipient[] = [] - - for (const recipientKey of recipientKeys) { - let targetExchangeKey: AskarKey | undefined - try { - targetExchangeKey = AskarKey.fromPublicBytes({ - publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, - algorithm: KeyAlgs.Ed25519, - }).convertkey({ algorithm: KeyAlgs.X25519 }) - - if (senderVerkey && senderExchangeKey) { - const encryptedSender = CryptoBox.seal({ - recipientKey: targetExchangeKey, - message: Buffer.from(senderVerkey), - }) - const nonce = CryptoBox.randomNonce() - const encryptedCek = CryptoBox.cryptoBox({ - recipientKey: targetExchangeKey, - senderKey: senderExchangeKey, - message: cek.secretBytes, - nonce, - }) - - recipients.push( - new JweRecipient({ - encryptedKey: encryptedCek, - header: { - kid: recipientKey, - sender: TypedArrayEncoder.toBase64URL(encryptedSender), - iv: TypedArrayEncoder.toBase64URL(nonce), - }, - }) - ) - } else { - const encryptedCek = CryptoBox.seal({ - recipientKey: targetExchangeKey, - message: cek.secretBytes, - }) - recipients.push( - new JweRecipient({ - encryptedKey: encryptedCek, - header: { - kid: recipientKey, - }, - }) - ) - } - } finally { - targetExchangeKey?.handle.free() - } - } - - const protectedJson = { - enc: 'xchacha20poly1305_ietf', - typ: 'JWM/1.0', - alg: senderVerkey ? 'Authcrypt' : 'Anoncrypt', - recipients: recipients.map((item) => JsonTransformer.toJSON(item)), - } - - const { ciphertext, tag, nonce } = cek.aeadEncrypt({ - message: Buffer.from(JSON.stringify(payload)), - aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), - }).parts - - const envelope = new JweEnvelope({ - ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), - iv: TypedArrayEncoder.toBase64URL(nonce), - protected: JsonEncoder.toBase64URL(protectedJson), - tag: TypedArrayEncoder.toBase64URL(tag), - }).toJson() - - return envelope as EncryptedMessage - } finally { - cek?.handle.free() - senderKey?.key.handle.free() - senderExchangeKey?.handle.free() - } - } - - /** - * Unpacks a JWE Envelope coded using DIDComm V1 algorithm - * - * @param messagePackage JWE Envelope - * @returns UnpackedMessageContext with plain text message, sender key and recipient key - */ - public async unpack(messagePackage: EncryptedMessage): Promise { - const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) - - const alg = protectedJson.alg - if (!['Anoncrypt', 'Authcrypt'].includes(alg)) { - throw new WalletError(`Unsupported pack algorithm: ${alg}`) - } - - const recipients = [] - - for (const recip of protectedJson.recipients) { - const kid = recip.header.kid - if (!kid) { - throw new WalletError('Blank recipient key') - } - const sender = recip.header.sender ? TypedArrayEncoder.fromBase64(recip.header.sender) : undefined - const iv = recip.header.iv ? TypedArrayEncoder.fromBase64(recip.header.iv) : undefined - if (sender && !iv) { - throw new WalletError('Missing IV') - } else if (!sender && iv) { - throw new WalletError('Unexpected IV') - } - recipients.push({ - kid, - sender, - iv, - encrypted_key: TypedArrayEncoder.fromBase64(recip.encrypted_key), - }) - } - - let payloadKey, senderKey, recipientKey - - for (const recipient of recipients) { - let recipientKeyEntry: KeyEntryObject | null | undefined - let sender_x: AskarKey | undefined - let recip_x: AskarKey | undefined - - try { - recipientKeyEntry = await this.session.fetchKey({ name: recipient.kid }) - if (recipientKeyEntry) { - const recip_x = recipientKeyEntry.key.convertkey({ algorithm: KeyAlgs.X25519 }) - recipientKey = recipient.kid - - if (recipient.sender && recipient.iv) { - senderKey = TypedArrayEncoder.toUtf8String( - CryptoBox.sealOpen({ - recipientKey: recip_x, - ciphertext: recipient.sender, - }) - ) - const sender_x = AskarKey.fromPublicBytes({ - algorithm: KeyAlgs.Ed25519, - publicKey: TypedArrayEncoder.fromBase58(senderKey), - }).convertkey({ algorithm: KeyAlgs.X25519 }) - - payloadKey = CryptoBox.open({ - recipientKey: recip_x, - senderKey: sender_x, - message: recipient.encrypted_key, - nonce: recipient.iv, - }) - } else { - payloadKey = CryptoBox.sealOpen({ ciphertext: recipient.encrypted_key, recipientKey: recip_x }) - } - break - } - } finally { - recipientKeyEntry?.key.handle.free() - sender_x?.handle.free() - recip_x?.handle.free() - } - } - if (!payloadKey) { - throw new WalletError('No corresponding recipient key found') - } - - if (!senderKey && alg === 'Authcrypt') { - throw new WalletError('Sender public key not provided for Authcrypt') - } - - let cek: AskarKey | undefined - try { - cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) - const message = cek.aeadDecrypt({ - ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext as any), - nonce: TypedArrayEncoder.fromBase64(messagePackage.iv as any), - tag: TypedArrayEncoder.fromBase64(messagePackage.tag as any), - aad: TypedArrayEncoder.fromString(messagePackage.protected), - }) - return { - plaintextMessage: JsonEncoder.fromBuffer(message), - senderKey, - recipientKey, - } - } finally { - cek?.handle.free() - } - } - - public async generateNonce(): Promise { - try { - // generate an 80-bit nonce suitable for AnonCreds proofs - const nonce = CryptoBox.randomNonce().slice(0, 10) - return new BigNumber(nonce).toString() - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error generating nonce', { cause: error }) - } - } - - public async generateWalletKey() { - try { - return Store.generateRawKey() - } catch (error) { - throw new WalletError('Error generating wallet key', { cause: error }) - } - } - private async getAskarWalletConfig(walletConfig: WalletConfig) { const { uri, path } = uriFromWalletConfig(walletConfig, this.fileSystem.dataPath) @@ -832,36 +402,4 @@ export class AskarWallet implements Wallet { passKey: walletConfig.key, } } - - private async retrieveKeyPair(publicKeyBase58: string): Promise { - try { - const entryObject = await this.session.fetch({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }) - - if (entryObject?.value) { - return JsonEncoder.fromString(entryObject?.value as string) as KeyPair - } else { - throw new WalletError(`No content found for record with public key: ${publicKeyBase58}`) - } - } catch (error) { - throw new WalletError('Error retrieving KeyPair record', { cause: error }) - } - } - - private async storeKeyPair(keyPair: KeyPair): Promise { - try { - await this.session.insert({ - category: 'KeyPairRecord', - name: `key-${keyPair.publicKeyBase58}`, - value: JSON.stringify(keyPair), - tags: { - keyType: keyPair.keyType, - }, - }) - } catch (error) { - if (isAskarError(error, AskarErrorCode.Duplicate)) { - throw new WalletKeyExistsError('Key already exists') - } - throw new WalletError('Error saving KeyPair record', { cause: error }) - } - } } diff --git a/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts new file mode 100644 index 0000000000..21484d1955 --- /dev/null +++ b/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts @@ -0,0 +1,64 @@ +import type { WalletConfig } from '@aries-framework/core' + +import { + SigningProviderRegistry, + WalletDuplicateError, + WalletNotFoundError, + KeyDerivationMethod, +} from '@aries-framework/core' + +import { describeRunInNodeVersion } from '../../../../../tests/runInVersion' +import { testLogger, agentDependencies } from '../../../../core/tests' +import { AskarProfileWallet } from '../AskarProfileWallet' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const rootWalletConfig: WalletConfig = { + id: 'Wallet: AskarProfileWalletTest', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describeRunInNodeVersion([18], 'AskarWallet management', () => { + let rootAskarWallet: AskarWallet + let profileAskarWallet: AskarProfileWallet + + afterEach(async () => { + if (profileAskarWallet) { + await profileAskarWallet.delete() + } + + if (rootAskarWallet) { + await rootAskarWallet.delete() + } + }) + + test('Create, open, close, delete', async () => { + const signingProviderRegistry = new SigningProviderRegistry([]) + rootAskarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), signingProviderRegistry) + + // Create and open wallet + await rootAskarWallet.createAndOpen(rootWalletConfig) + + profileAskarWallet = new AskarProfileWallet(rootAskarWallet.store, testLogger, signingProviderRegistry) + + // Create, open and close profile + await profileAskarWallet.create({ ...rootWalletConfig, id: 'profile-id' }) + await profileAskarWallet.open({ ...rootWalletConfig, id: 'profile-id' }) + await profileAskarWallet.close() + + // try to re-create it + await expect(profileAskarWallet.createAndOpen({ ...rootWalletConfig, id: 'profile-id' })).rejects.toThrowError( + WalletDuplicateError + ) + + // Re-open profile + await profileAskarWallet.open({ ...rootWalletConfig, id: 'profile-id' }) + + // try to open non-existent wallet + await expect(profileAskarWallet.open({ ...rootWalletConfig, id: 'non-existent-profile-id' })).rejects.toThrowError( + WalletNotFoundError + ) + }) +}) diff --git a/packages/askar/src/wallet/index.ts b/packages/askar/src/wallet/index.ts index 8d569fdf4c..fb71764d57 100644 --- a/packages/askar/src/wallet/index.ts +++ b/packages/askar/src/wallet/index.ts @@ -1,2 +1,3 @@ export { AskarWallet } from './AskarWallet' +export { AskarProfileWallet } from './AskarProfileWallet' export * from './AskarWalletPostgresStorageConfig' diff --git a/packages/askar/tests/askar-inmemory.e2e.test.ts b/packages/askar/tests/askar-inmemory.e2e.test.ts new file mode 100644 index 0000000000..2261fa7efb --- /dev/null +++ b/packages/askar/tests/askar-inmemory.e2e.test.ts @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' + +import { Agent } from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { describeRunInNodeVersion } from '../../../tests/runInVersion' +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' + +import { e2eTest, getSqliteAgentOptions } from './helpers' + +const aliceInMemoryAgentOptions = getSqliteAgentOptions( + 'AgentsAlice', + { + endpoints: ['rxjs:alice'], + }, + true +) +const bobInMemoryAgentOptions = getSqliteAgentOptions( + 'AgentsBob', + { + endpoints: ['rxjs:bob'], + }, + true +) + +// FIXME: Re-include in tests when Askar NodeJS wrapper performance is improved +describeRunInNodeVersion([18], 'Askar In Memory agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + + afterAll(async () => { + if (bobAgent) { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + } + + if (aliceAgent) { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + } + }) + + test('In memory Askar wallets E2E test', async () => { + const aliceMessages = new Subject() + const bobMessages = new Subject() + + const subjectMap = { + 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, + } + + aliceAgent = new Agent(aliceInMemoryAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + bobAgent = new Agent(bobInMemoryAgentOptions) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + + await e2eTest(aliceAgent, bobAgent) + }) +}) diff --git a/packages/askar/tests/askar-postgres.e2e.test.ts b/packages/askar/tests/askar-postgres.e2e.test.ts index c25f6d16c3..14219b6515 100644 --- a/packages/askar/tests/askar-postgres.e2e.test.ts +++ b/packages/askar/tests/askar-postgres.e2e.test.ts @@ -1,17 +1,15 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' import type { AskarWalletPostgresStorageConfig } from '../src/wallet' -import type { ConnectionRecord } from '@aries-framework/core' -import { Agent, HandshakeProtocol } from '@aries-framework/core' +import { Agent } from '@aries-framework/core' import { Subject } from 'rxjs' import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { waitForBasicMessage } from '../../core/tests/helpers' -import { getPostgresAgentOptions } from './helpers' +import { e2eTest, getPostgresAgentOptions } from './helpers' const storageConfig: AskarWalletPostgresStorageConfig = { type: 'postgres', @@ -35,8 +33,6 @@ const bobPostgresAgentOptions = getPostgresAgentOptions('AgentsBob', storageConf describeRunInNodeVersion([18], 'Askar Postgres agents', () => { let aliceAgent: Agent let bobAgent: Agent - let aliceConnection: ConnectionRecord - let bobConnection: ConnectionRecord afterAll(async () => { if (bobAgent) { @@ -50,7 +46,7 @@ describeRunInNodeVersion([18], 'Askar Postgres agents', () => { } }) - test('make a connection between postgres agents', async () => { + test('Postgres Askar wallets E2E test', async () => { const aliceMessages = new Subject() const bobMessages = new Subject() @@ -69,35 +65,6 @@ describeRunInNodeVersion([18], 'Askar Postgres agents', () => { bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await bobAgent.initialize() - const aliceBobOutOfBandRecord = await aliceAgent.oob.createInvitation({ - handshakeProtocols: [HandshakeProtocol.Connections], - }) - - const { connectionRecord: bobConnectionAtBobAlice } = await bobAgent.oob.receiveInvitation( - aliceBobOutOfBandRecord.outOfBandInvitation - ) - bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice!.id) - - const [aliceConnectionAtAliceBob] = await aliceAgent.connections.findAllByOutOfBandId(aliceBobOutOfBandRecord.id) - aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob!.id) - }) - - test('send a message to connection', async () => { - const message = 'hello, world' - await aliceAgent.basicMessages.sendMessage(aliceConnection.id, message) - - const basicMessage = await waitForBasicMessage(bobAgent, { - content: message, - }) - - expect(basicMessage.content).toBe(message) - }) - - test('can shutdown and re-initialize the same postgres agent', async () => { - expect(aliceAgent.isInitialized).toBe(true) - await aliceAgent.shutdown() - expect(aliceAgent.isInitialized).toBe(false) - await aliceAgent.initialize() - expect(aliceAgent.isInitialized).toBe(true) + await e2eTest(aliceAgent, bobAgent) }) }) diff --git a/packages/askar/tests/helpers.ts b/packages/askar/tests/helpers.ts index 8a71cd9bff..e02e5cd542 100644 --- a/packages/askar/tests/helpers.ts +++ b/packages/askar/tests/helpers.ts @@ -1,11 +1,12 @@ import type { AskarWalletPostgresStorageConfig } from '../src/wallet' -import type { InitConfig } from '@aries-framework/core' +import type { Agent, InitConfig } from '@aries-framework/core' -import { ConnectionsModule, LogLevel, utils } from '@aries-framework/core' +import { ConnectionsModule, HandshakeProtocol, LogLevel, utils } from '@aries-framework/core' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { registerAriesAskar } from '@hyperledger/aries-askar-shared' import path from 'path' +import { waitForBasicMessage } from '../../core/tests/helpers' import { TestLogger } from '../../core/tests/logger' import { agentDependencies } from '../../node/src' import { AskarModule } from '../src/AskarModule' @@ -54,14 +55,14 @@ export function getPostgresAgentOptions( } as const } -export function getSqliteAgentOptions(name: string, extraConfig: Partial = {}) { +export function getSqliteAgentOptions(name: string, extraConfig: Partial = {}, inMemory?: boolean) { const random = utils.uuid().slice(0, 4) const config: InitConfig = { label: `SQLiteAgent: ${name} - ${random}`, walletConfig: { id: `SQLiteWallet${name} - ${random}`, key: `Key${name}`, - storage: { type: 'sqlite' }, + storage: { type: 'sqlite', inMemory }, }, autoUpdateStorageOnStartup: false, logger: new TestLogger(LogLevel.off, name), @@ -78,3 +79,41 @@ export function getSqliteAgentOptions(name: string, extraConfig: Partial export const getCheqdModules = (seed?: string, rpcUrl?: string) => ({ cheqdSdk: new CheqdModule(getCheqdModuleConfig(seed, rpcUrl)), dids: new DidsModule({ - registrars: [new CheqdDidRegistrar(), new KeyDidRegistrar()], - resolvers: [new CheqdDidResolver(), new KeyDidResolver()], + registrars: [new CheqdDidRegistrar()], + resolvers: [new CheqdDidResolver()], }), indySdk: new IndySdkModule(getIndySdkModuleConfig()), }) diff --git a/packages/core/src/agent/AgentMessage.ts b/packages/core/src/agent/AgentMessage.ts index 9f081f3d18..320be216c4 100644 --- a/packages/core/src/agent/AgentMessage.ts +++ b/packages/core/src/agent/AgentMessage.ts @@ -1,3 +1,4 @@ +import type { PlaintextMessage } from '../types' import type { ParsedMessageType } from '../utils/messageType' import type { Constructor } from '../utils/mixins' @@ -31,10 +32,7 @@ export class AgentMessage extends Decorated { @Exclude() public readonly allowDidSovPrefix: boolean = false - public toJSON({ useDidSovPrefixWhereAllowed }: { useDidSovPrefixWhereAllowed?: boolean } = {}): Record< - string, - unknown - > { + public toJSON({ useDidSovPrefixWhereAllowed }: { useDidSovPrefixWhereAllowed?: boolean } = {}): PlaintextMessage { const json = JsonTransformer.toJSON(this) // If we have `useDidSovPrefixWhereAllowed` enabled, we want to replace the new https://didcomm.org prefix with the legacy did:sov prefix. @@ -44,7 +42,7 @@ export class AgentMessage extends Decorated { replaceNewDidCommPrefixWithLegacyDidSovOnMessage(json) } - return json + return json as PlaintextMessage } public is(Class: C): this is InstanceType { diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index ae9e6656ba..39ea8d521d 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -175,9 +175,10 @@ export abstract class BaseAgent { + agentContext.config.logger.debug( + `No previous sent message in thread for outbound message ${message.id}, setting up routing` + ) + + let routing: Routing | undefined = undefined + + // Extract routing from out of band record if possible + const oobRecordRecipientRouting = outOfBandRecord?.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (oobRecordRecipientRouting) { + routing = { + recipientKey: Key.fromFingerprint(oobRecordRecipientRouting.recipientKeyFingerprint), + routingKeys: oobRecordRecipientRouting.routingKeyFingerprints.map((fingerprint) => + Key.fromFingerprint(fingerprint) + ), + endpoints: oobRecordRecipientRouting.endpoints, + mediatorId: oobRecordRecipientRouting.mediatorId, + } + } + + if (!routing) { + const routingService = agentContext.dependencyManager.resolve(RoutingService) + routing = await routingService.getRouting(agentContext, { + mediatorId: outOfBandRecord?.mediatorId, + }) + } + + return { + id: uuid(), + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + } +} + +async function addExchangeDataToMessage( + agentContext: AgentContext, + { + message, + ourService, + outOfBandRecord, + associatedRecord, + }: { + message: AgentMessage + ourService: ResolvedDidCommService + outOfBandRecord?: OutOfBandRecord + associatedRecord: BaseRecordAny + } +) { + // Set the parentThreadId on the message from the oob invitation + if (outOfBandRecord) { + if (!message.thread) { + message.setThread({ + parentThreadId: outOfBandRecord.outOfBandInvitation.id, + }) + } else { + message.thread.parentThreadId = outOfBandRecord.outOfBandInvitation.id + } + } + + // Set the service on the message and save service decorator to record (to remember our verkey) + // TODO: we should store this in the OOB record, but that would be a breaking change for now. + // We can change this in 0.5.0 + message.service = ServiceDecorator.fromResolvedDidCommService(ourService) + + await agentContext.dependencyManager.resolve(DidCommMessageRepository).saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: associatedRecord.id, + }) +} diff --git a/packages/core/src/decorators/service/ServiceDecorator.ts b/packages/core/src/decorators/service/ServiceDecorator.ts index 0a105c4831..793509d678 100644 --- a/packages/core/src/decorators/service/ServiceDecorator.ts +++ b/packages/core/src/decorators/service/ServiceDecorator.ts @@ -46,4 +46,12 @@ export class ServiceDecorator { serviceEndpoint: this.serviceEndpoint, } } + + public static fromResolvedDidCommService(service: ResolvedDidCommService): ServiceDecorator { + return new ServiceDecorator({ + recipientKeys: service.recipientKeys.map((k) => k.publicKeyBase58), + routingKeys: service.routingKeys.map((k) => k.publicKeyBase58), + serviceEndpoint: service.serviceEndpoint, + }) + } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9a159f0eb7..4d99d06980 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,6 +16,7 @@ export { AgentMessage } from './agent/AgentMessage' export { Dispatcher } from './agent/Dispatcher' export { MessageSender } from './agent/MessageSender' export type { AgentDependencies } from './agent/AgentDependencies' +export { getOutboundMessageContext } from './agent/getOutboundMessageContext' export type { InitConfig, OutboundPackage, diff --git a/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts b/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts index 72498db91a..bdd17d49d1 100644 --- a/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts +++ b/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts @@ -4,7 +4,7 @@ import type { Cache } from '../Cache' import { LRUMap } from 'lru_map' -import { AriesFrameworkError } from '../../../error' +import { AriesFrameworkError, RecordDuplicateError } from '../../../error' import { SingleContextLruCacheRecord } from './SingleContextLruCacheRecord' import { SingleContextLruCacheRepository } from './SingleContextLruCacheRepository' @@ -114,7 +114,20 @@ export class SingleContextStorageLruCache implements Cache { entries: new Map(), }) - await cacheRepository.save(agentContext, cacheRecord) + try { + await cacheRepository.save(agentContext, cacheRecord) + } catch (error) { + // This addresses some race conditions issues where we first check if the record exists + // then we create one if it doesn't, but another process has created one in the meantime + // Although not the most elegant solution, it addresses the issues + if (error instanceof RecordDuplicateError) { + // the record already exists, which is our intended end state + // we can ignore this error and fetch the existing record + return cacheRepository.getById(agentContext, CONTEXT_STORAGE_LRU_CACHE_ID) + } else { + throw error + } + } } return cacheRecord diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index 2a1b2fea96..b9da15c304 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -98,7 +98,7 @@ export class DidExchangeProtocol { alias, state: DidExchangeState.InvitationReceived, theirLabel: outOfBandInvitation.label, - mediatorId: routing.mediatorId ?? outOfBandRecord.mediatorId, + mediatorId: routing.mediatorId, autoAcceptConnection: outOfBandRecord.autoAcceptConnection, outOfBandId: outOfBandRecord.id, invitationDid, diff --git a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts index c07c94a893..00e46a001d 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts @@ -30,8 +30,10 @@ import { DidCommV1Service } from '../../dids/domain/service/DidCommV1Service' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' import { DidRecord, DidRepository } from '../../dids/repository' import { DidRegistrarService } from '../../dids/services/DidRegistrarService' +import { OutOfBandService } from '../../oob/OutOfBandService' import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { OutOfBandRepository } from '../../oob/repository/OutOfBandRepository' import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' import { Connection, @@ -48,9 +50,13 @@ import { ConnectionService } from '../services/ConnectionService' import { convertToNewDidDocument } from '../services/helpers' jest.mock('../repository/ConnectionRepository') +jest.mock('../../oob/repository/OutOfBandRepository') +jest.mock('../../oob/OutOfBandService') jest.mock('../../dids/services/DidRegistrarService') jest.mock('../../dids/repository/DidRepository') const ConnectionRepositoryMock = ConnectionRepository as jest.Mock +const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock +const OutOfBandServiceMock = OutOfBandService as jest.Mock const DidRepositoryMock = DidRepository as jest.Mock const DidRegistrarServiceMock = DidRegistrarService as jest.Mock @@ -72,9 +78,13 @@ const agentConfig = getAgentConfig('ConnectionServiceTest', { connectionImageUrl, }) +const outOfBandRepository = new OutOfBandRepositoryMock() +const outOfBandService = new OutOfBandServiceMock() + describe('ConnectionService', () => { let wallet: Wallet let connectionRepository: ConnectionRepository + let didRepository: DidRepository let connectionService: ConnectionService let eventEmitter: EventEmitter @@ -83,7 +93,14 @@ describe('ConnectionService', () => { beforeAll(async () => { wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) - agentContext = getAgentContext({ wallet, agentConfig }) + agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [ + [OutOfBandRepository, outOfBandRepository], + [OutOfBandService, outOfBandService], + ], + }) await wallet.createAndOpen(agentConfig.walletConfig) }) @@ -95,13 +112,7 @@ describe('ConnectionService', () => { eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) connectionRepository = new ConnectionRepositoryMock() didRepository = new DidRepositoryMock() - connectionService = new ConnectionService( - agentConfig.logger, - connectionRepository, - didRepository, - didRegistrarService, - eventEmitter - ) + connectionService = new ConnectionService(agentConfig.logger, connectionRepository, didRepository, eventEmitter) myRouting = { recipientKey: Key.fromFingerprint('z6MkwFkSP4uv5PhhKJCGehtjuZedkotC7VF64xtMsxuM8R3W'), endpoints: agentConfig.endpoints ?? [], @@ -755,8 +766,8 @@ describe('ConnectionService', () => { }) }) - describe('assertConnectionOrServiceDecorator', () => { - it('should not throw an error when a connection record with state complete is present in the messageContext', () => { + describe('assertConnectionOrOutOfBandExchange', () => { + it('should not throw an error when a connection record with state complete is present in the messageContext', async () => { expect.assertions(1) const messageContext = new InboundMessageContext(new AgentMessage(), { @@ -764,10 +775,10 @@ describe('ConnectionService', () => { connection: getMockConnection({ state: DidExchangeState.Completed }), }) - expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).not.toThrow() + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).resolves.not.toThrow() }) - it('should throw an error when a connection record is present and state not complete in the messageContext', () => { + it('should throw an error when a connection record is present and state not complete in the messageContext', async () => { expect.assertions(1) const messageContext = new InboundMessageContext(new AgentMessage(), { @@ -775,14 +786,16 @@ describe('ConnectionService', () => { connection: getMockConnection({ state: DidExchangeState.InvitationReceived }), }) - expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).toThrowError( + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).rejects.toThrowError( 'Connection record is not ready to be used' ) }) - it('should not throw an error when no connection record is present in the messageContext and no additional data, but the message has a ~service decorator', () => { + it('should not throw an error when no connection record is present in the messageContext and no additional data, but the message has a ~service decorator', async () => { expect.assertions(1) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(null) + const message = new AgentMessage() message.setService({ recipientKeys: [], @@ -791,24 +804,24 @@ describe('ConnectionService', () => { }) const messageContext = new InboundMessageContext(message, { agentContext }) - expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).not.toThrow() + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).resolves.not.toThrow() }) - it('should not throw when a fully valid connection-less input is passed', () => { + it('should not throw when a fully valid connection-less input is passed', async () => { expect.assertions(1) const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) - const previousSentMessage = new AgentMessage() - previousSentMessage.setService({ + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ recipientKeys: [recipientKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], }) - const previousReceivedMessage = new AgentMessage() - previousReceivedMessage.setService({ + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ recipientKeys: [senderKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], @@ -816,69 +829,80 @@ describe('ConnectionService', () => { const message = new AgentMessage() message.setService({ - recipientKeys: [], + recipientKeys: [senderKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], }) const messageContext = new InboundMessageContext(message, { agentContext, recipientKey, senderKey }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) - ).not.toThrow() + ).resolves.not.toThrow() }) - it('should throw an error when previousSentMessage is present, but recipientVerkey is not ', () => { + it('should throw an error when lastSentMessage is present, but recipientVerkey is not ', async () => { expect.assertions(1) - const previousSentMessage = new AgentMessage() - previousSentMessage.setService({ + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ recipientKeys: [], serviceEndpoint: '', routingKeys: [], }) const message = new AgentMessage() + message.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) const messageContext = new InboundMessageContext(message, { agentContext }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousSentMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastSentMessage, }) - ).toThrowError('Cannot verify service without recipientKey on incoming message') + ).rejects.toThrowError( + 'Incoming message must have recipientKey and senderKey (so cannot be AuthCrypt or unpacked) if there are lastSentMessage or lastReceivedMessage.' + ) }) - it('should throw an error when previousSentMessage and recipientKey are present, but recipient key is not present in recipientKeys of previously sent message ~service decorator', () => { + it('should throw an error when lastSentMessage and recipientKey are present, but recipient key is not present in recipientKeys of previously sent message ~service decorator', async () => { expect.assertions(1) const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const senderKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) - const previousSentMessage = new AgentMessage() - previousSentMessage.setService({ + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ recipientKeys: ['anotherKey'], serviceEndpoint: '', routingKeys: [], }) const message = new AgentMessage() - const messageContext = new InboundMessageContext(message, { agentContext, recipientKey }) + message.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) + const messageContext = new InboundMessageContext(message, { agentContext, recipientKey, senderKey }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousSentMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastSentMessage, }) - ).toThrowError( - 'Previously sent message ~service recipientKeys does not include current received message recipient key' - ) + ).rejects.toThrowError('Recipient key 8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K not found in our service') }) - it('should throw an error when previousReceivedMessage is present, but senderVerkey is not ', () => { + it('should throw an error when lastReceivedMessage is present, but senderVerkey is not ', async () => { expect.assertions(1) - const previousReceivedMessage = new AgentMessage() - previousReceivedMessage.setService({ + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ recipientKeys: [], serviceEndpoint: '', routingKeys: [], @@ -887,38 +911,47 @@ describe('ConnectionService', () => { const message = new AgentMessage() const messageContext = new InboundMessageContext(message, { agentContext }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, }) - ).toThrowError('Cannot verify service without senderKey on incoming message') + ).rejects.toThrowError( + 'No keys on our side to use for encrypting messages, and previous messages found (in which case our keys MUST also be present).' + ) }) - it('should throw an error when previousReceivedMessage and senderKey are present, but sender key is not present in recipientKeys of previously received message ~service decorator', () => { + it('should throw an error when lastReceivedMessage and senderKey are present, but sender key is not present in recipientKeys of previously received message ~service decorator', async () => { expect.assertions(1) const senderKey = 'senderKey' - const previousReceivedMessage = new AgentMessage() - previousReceivedMessage.setService({ + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ recipientKeys: ['anotherKey'], serviceEndpoint: '', routingKeys: [], }) + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ + recipientKeys: [senderKey], + serviceEndpoint: '', + routingKeys: [], + }) + const message = new AgentMessage() const messageContext = new InboundMessageContext(message, { agentContext, - senderKey: Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('randomKey', KeyType.Ed25519), + recipientKey: Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519), }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) - ).toThrowError( - 'Previously received message ~service recipientKeys does not include current received message sender key' - ) + ).rejects.toThrowError('Sender key randomKey not found in their service') }) }) diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 00461217ae..059c785452 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -23,14 +23,16 @@ import { Logger } from '../../../logger' import { inject, injectable } from '../../../plugins' import { JsonTransformer } from '../../../utils/JsonTransformer' import { indyDidFromPublicKeyBase58 } from '../../../utils/did' -import { DidKey, DidRegistrarService, IndyAgentService } from '../../dids' +import { DidKey, IndyAgentService } from '../../dids' import { DidDocumentRole } from '../../dids/domain/DidDocumentRole' import { didKeyToVerkey } from '../../dids/helpers' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' import { DidRecord, DidRepository } from '../../dids/repository' import { DidRecordMetadataKeys } from '../../dids/repository/didRecordMetadataTypes' +import { OutOfBandService } from '../../oob/OutOfBandService' import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { OutOfBandRepository } from '../../oob/repository' import { ConnectionEventTypes } from '../ConnectionEvents' import { ConnectionProblemReportError, ConnectionProblemReportReason } from '../errors' import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' @@ -61,7 +63,6 @@ export interface ConnectionRequestParams { export class ConnectionService { private connectionRepository: ConnectionRepository private didRepository: DidRepository - private didRegistrarService: DidRegistrarService private eventEmitter: EventEmitter private logger: Logger @@ -69,12 +70,10 @@ export class ConnectionService { @inject(InjectionSymbols.Logger) logger: Logger, connectionRepository: ConnectionRepository, didRepository: DidRepository, - didRegistrarService: DidRegistrarService, eventEmitter: EventEmitter ) { this.connectionRepository = connectionRepository this.didRepository = didRepository - this.didRegistrarService = didRegistrarService this.eventEmitter = eventEmitter this.logger = logger } @@ -441,19 +440,18 @@ export class ConnectionService { /** * Assert that an inbound message either has a connection associated with it, - * or has everything correctly set up for connection-less exchange. + * or has everything correctly set up for connection-less exchange (optionally with out of band) * * @param messageContext - the inbound message context - * @param previousRespondence - previous sent and received message to determine if a valid service decorator is present */ - public assertConnectionOrServiceDecorator( + public async assertConnectionOrOutOfBandExchange( messageContext: InboundMessageContext, { - previousSentMessage, - previousReceivedMessage, + lastSentMessage, + lastReceivedMessage, }: { - previousSentMessage?: AgentMessage | null - previousReceivedMessage?: AgentMessage | null + lastSentMessage?: AgentMessage | null + lastReceivedMessage?: AgentMessage | null } = {} ) { const { connection, message } = messageContext @@ -472,43 +470,70 @@ export class ConnectionService { const recipientKey = messageContext.recipientKey && messageContext.recipientKey.publicKeyBase58 const senderKey = messageContext.senderKey && messageContext.senderKey.publicKeyBase58 - if (previousSentMessage) { - // If we have previously sent a message, it is not allowed to receive an OOB/unpacked message - if (!recipientKey) { - throw new AriesFrameworkError( - 'Cannot verify service without recipientKey on incoming message (received unpacked message)' - ) - } + // set theirService to the value of lastReceivedMessage.service + let theirService = + messageContext.message?.service?.resolvedDidCommService ?? lastReceivedMessage?.service?.resolvedDidCommService + let ourService = lastSentMessage?.service?.resolvedDidCommService - // Check if the inbound message recipient key is present - // in the recipientKeys of previously sent message ~service decorator - if (!previousSentMessage?.service || !previousSentMessage.service.recipientKeys.includes(recipientKey)) { - throw new AriesFrameworkError( - 'Previously sent message ~service recipientKeys does not include current received message recipient key' - ) - } + // 1. check if there's an oob record associated. + const outOfBandRepository = messageContext.agentContext.dependencyManager.resolve(OutOfBandRepository) + const outOfBandService = messageContext.agentContext.dependencyManager.resolve(OutOfBandService) + const outOfBandRecord = await outOfBandRepository.findSingleByQuery(messageContext.agentContext, { + invitationRequestsThreadIds: [message.threadId], + }) + + // If we have an out of band record, we can extract the service for our/the other party from the oob record + if (outOfBandRecord?.role === OutOfBandRole.Sender) { + ourService = await outOfBandService.getResolvedServiceForOutOfBandServices( + messageContext.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + } else if (outOfBandRecord?.role === OutOfBandRole.Receiver) { + theirService = await outOfBandService.getResolvedServiceForOutOfBandServices( + messageContext.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) } - if (previousReceivedMessage) { - // If we have previously received a message, it is not allowed to receive an OOB/unpacked/AnonCrypt message - if (!senderKey) { - throw new AriesFrameworkError( - 'Cannot verify service without senderKey on incoming message (received AnonCrypt or unpacked message)' - ) - } + // theirService can be null when we receive an oob invitation and process the message. + // In this case there MUST be an oob record, otherwise there is no way for us to reply + // to the message + if (!theirService && !outOfBandRecord) { + throw new AriesFrameworkError( + 'No service for incoming connection-less message and no associated out of band record found.' + ) + } - // Check if the inbound message sender key is present - // in the recipientKeys of previously received message ~service decorator - if (!previousReceivedMessage.service || !previousReceivedMessage.service.recipientKeys.includes(senderKey)) { - throw new AriesFrameworkError( - 'Previously received message ~service recipientKeys does not include current received message sender key' - ) + // ourService can be null when we receive an oob invitation or legacy connectionless message and process the message. + // In this case lastSentMessage and lastReceivedMessage MUST be null, because there shouldn't be any previous exchange + if (!ourService && (lastReceivedMessage || lastSentMessage)) { + throw new AriesFrameworkError( + 'No keys on our side to use for encrypting messages, and previous messages found (in which case our keys MUST also be present).' + ) + } + + // If the message is unpacked or AuthCrypt, there cannot be any previous exchange (this must be the first message). + // All exchange after the first unpacked oob exchange MUST be encrypted. + if ((!senderKey || !recipientKey) && (lastSentMessage || lastReceivedMessage)) { + throw new AriesFrameworkError( + 'Incoming message must have recipientKey and senderKey (so cannot be AuthCrypt or unpacked) if there are lastSentMessage or lastReceivedMessage.' + ) + } + + // Check if recipientKey is in ourService + if (recipientKey && ourService) { + const recipientKeyFound = ourService.recipientKeys.some((key) => key.publicKeyBase58 === recipientKey) + if (!recipientKeyFound) { + throw new AriesFrameworkError(`Recipient key ${recipientKey} not found in our service`) } } - // If message is received unpacked/, we need to make sure it included a ~service decorator - if (!message.service && !recipientKey) { - throw new AriesFrameworkError('Message recipientKey must have ~service decorator') + // Check if senderKey is in theirService + if (senderKey && theirService) { + const senderKeyFound = theirService.recipientKeys.some((key) => key.publicKeyBase58 === senderKey) + if (!senderKeyFound) { + throw new AriesFrameworkError(`Sender key ${senderKey} not found in their service.`) + } } } } diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 6e60a807ba..c8eba61ae9 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -24,13 +24,11 @@ import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' -import { OutboundMessageContext } from '../../agent/models' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { InjectionSymbols } from '../../constants' -import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' -import { DidCommMessageRole } from '../../storage' import { DidCommMessageRepository } from '../../storage/didcomm/DidCommMessageRepository' import { ConnectionService } from '../connections/services' import { RoutingService } from '../routing/services/RoutingService' @@ -158,11 +156,10 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - // send the message here - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -202,10 +199,10 @@ export class CredentialsApi implements Credent }) // send the message - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -239,11 +236,11 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -271,10 +268,10 @@ export class CredentialsApi implements Credent }) this.logger.debug('Offer Message successfully created; message= ', message) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -295,75 +292,32 @@ export class CredentialsApi implements Credent this.logger.debug(`Got a credentialProtocol object for this version; version = ${protocol.version}`) const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } // Use connection if present - if (credentialRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - - // Assert - connectionRecord.assertReady() - - const { message } = await protocol.acceptOffer(this.agentContext, { - credentialRecord, - credentialFormats: options.credentialFormats, - comment: options.comment, - autoAcceptCredential: options.autoAcceptCredential, - }) - - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: credentialRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) - - return credentialRecord - } - // Use ~service decorator otherwise - else if (offerMessage?.service) { - // Create ~service decorator - const routing = await this.routingService.getRouting(this.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = offerMessage.service - - const { message } = await protocol.acceptOffer(this.agentContext, { - credentialRecord, - credentialFormats: options.credentialFormats, - comment: options.comment, - autoAcceptCredential: options.autoAcceptCredential, - }) - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - ) + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() - return credentialRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept offer for credential record without connectionId or ~service decorator on credential offer.` - ) - } + const { message } = await protocol.acceptOffer(this.agentContext, { + credentialRecord, + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: offerMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord } public async declineOffer(credentialRecordId: string): Promise { @@ -399,10 +353,10 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -428,7 +382,7 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - this.logger.debug('Offer Message successfully created; message= ', message) + this.logger.debug('Offer Message successfully created', { message }) return { message, credentialRecord } } @@ -448,6 +402,21 @@ export class CredentialsApi implements Credent this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new AriesFrameworkError(`No offer message found for proof record with id '${credentialRecord.id}'`) + } + const { message } = await protocol.acceptRequest(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, @@ -456,52 +425,16 @@ export class CredentialsApi implements Credent }) this.logger.debug('We have a credential message (sending outbound): ', message) - const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) - const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) - - // Use connection if present - if (credentialRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, - associatedRecord: credentialRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) - - return credentialRecord - } - // Use ~service decorator otherwise - else if (requestMessage?.service && offerMessage?.service) { - const recipientService = requestMessage.service - const ourService = offerMessage.service - - message.service = ourService - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - ) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: requestMessage, + lastSentMessage: offerMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) - return credentialRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept request for credential record without connectionId or ~service decorator on credential offer / request.` - ) - } + return credentialRecord } /** @@ -520,49 +453,36 @@ export class CredentialsApi implements Credent this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) - const { message } = await protocol.acceptCredential(this.agentContext, { - credentialRecord, - }) + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) - const credentialMessage = await protocol.findCredentialMessage(this.agentContext, credentialRecord.id) - - if (credentialRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, - associatedRecord: credentialRecord, - }) - - await this.messageSender.sendMessage(outboundMessageContext) - - return credentialRecord + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } - // Use ~service decorator otherwise - else if (credentialMessage?.service && requestMessage?.service) { - const recipientService = credentialMessage.service - const ourService = requestMessage.service - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: false, // hard wire to be false since it's the end of the protocol so not needed here - }, - }) - ) - - return credentialRecord - } - // Cannot send message without connectionId or ~service decorator - else { + const credentialMessage = await protocol.findCredentialMessage(this.agentContext, credentialRecord.id) + if (!credentialMessage) { throw new AriesFrameworkError( - `Cannot accept credential without connectionId or ~service decorator on credential message.` + `No credential message found for credential record with id '${credentialRecord.id}'` ) } + + const { message } = await protocol.acceptCredential(this.agentContext, { + credentialRecord, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: credentialMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord } /** @@ -576,7 +496,7 @@ export class CredentialsApi implements Credent if (!credentialRecord.connectionId) { throw new AriesFrameworkError(`No connectionId found for credential record '${credentialRecord.id}'.`) } - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) const protocol = this.getProtocol(credentialRecord.protocolVersion) const { message } = await protocol.createProblemReport(this.agentContext, { @@ -586,10 +506,10 @@ export class CredentialsApi implements Credent message.setThread({ threadId: credentialRecord.threadId, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts index 901834a106..2ab8eddc4a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts @@ -189,9 +189,9 @@ export class V2CredentialProtocol { aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() - // Create link secret for alice - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age'], }) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts index de3aee8612..860b29a43e 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts @@ -34,7 +34,7 @@ import { KeyType } from '../../../../../crypto' import { TypedArrayEncoder } from '../../../../../utils' import { JsonTransformer } from '../../../../../utils/JsonTransformer' import { CacheModule, InMemoryLruCache } from '../../../../cache' -import { DidsModule, KeyDidRegistrar, KeyDidResolver } from '../../../../dids' +import { DidsModule } from '../../../../dids' import { ProofEventTypes, ProofsModule, V2ProofProtocol } from '../../../../proofs' import { W3cCredentialsModule } from '../../../../vc' import { customDocumentLoader } from '../../../../vc/data-integrity/__tests__/documentLoader' @@ -108,8 +108,8 @@ const getIndyJsonLdModules = () => registries: [new IndySdkAnonCredsRegistry()], }), dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver(), new KeyDidResolver()], - registrars: [new IndySdkIndyDidRegistrar(), new KeyDidRegistrar()], + resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver()], + registrars: [new IndySdkIndyDidRegistrar()], }), indySdk: new IndySdkModule({ indySdk, @@ -169,12 +169,6 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { await aliceAgent.initialize() ;[, { id: aliceConnectionId }] = await makeConnection(faberAgent, aliceAgent) - // Create link secret for alice - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age', 'profile_picture', 'x-ray'], }) diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts index c6815be71c..9f1ee870f2 100644 --- a/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts @@ -3,10 +3,9 @@ import type { InboundMessageContext } from '../../../../../agent/models/InboundM import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' import type { V2CredentialProtocol } from '../V2CredentialProtocol' -import { OutboundMessageContext } from '../../../../../agent/models' -import { DidCommMessageRepository } from '../../../../../storage' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { AriesFrameworkError } from '../../../../../error' import { V2IssueCredentialMessage } from '../messages/V2IssueCredentialMessage' -import { V2RequestCredentialMessage } from '../messages/V2RequestCredentialMessage' export class V2IssueCredentialHandler implements MessageHandler { private credentialProtocol: V2CredentialProtocol @@ -34,36 +33,24 @@ export class V2IssueCredentialHandler implements MessageHandler { messageContext: MessageHandlerInboundMessage ) { messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - - const requestMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: credentialRecord.id, - messageClass: V2RequestCredentialMessage, - }) - const { message } = await this.credentialProtocol.acceptCredential(messageContext.agentContext, { credentialRecord, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (requestMessage?.service && messageContext.message.service) { - const recipientService = messageContext.message.service - const ourService = requestMessage.service - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) + const requestMessage = await this.credentialProtocol.findRequestMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } - messageContext.agentContext.config.logger.error(`Could not automatically create credential ack`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) } } diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts index 8320314f0a..ff2d08716a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts @@ -3,10 +3,7 @@ import type { InboundMessageContext } from '../../../../../agent/models/InboundM import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' import type { V2CredentialProtocol } from '../V2CredentialProtocol' -import { OutboundMessageContext } from '../../../../../agent/models' -import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' -import { DidCommMessageRepository, DidCommMessageRole } from '../../../../../storage' -import { RoutingService } from '../../../../routing/services/RoutingService' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' export class V2OfferCredentialHandler implements MessageHandler { @@ -36,48 +33,13 @@ export class V2OfferCredentialHandler implements MessageHandler { ) { messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) - if (messageContext.connection) { - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { - credentialRecord, - }) - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message?.service) { - const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) - const routing = await routingService.getRouting(messageContext.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service + const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { - credentialRecord, - }) - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create credential request`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + }) } } diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts index 244485182f..98f04deb1a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts @@ -3,9 +3,8 @@ import type { InboundMessageContext } from '../../../../../agent/models/InboundM import type { CredentialExchangeRecord } from '../../../repository' import type { V2CredentialProtocol } from '../V2CredentialProtocol' -import { OutboundMessageContext } from '../../../../../agent/models' -import { DidCommMessageRepository, DidCommMessageRole } from '../../../../../storage' -import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { AriesFrameworkError } from '../../../../../error' import { V2RequestCredentialMessage } from '../messages/V2RequestCredentialMessage' export class V2RequestCredentialHandler implements MessageHandler { @@ -35,44 +34,25 @@ export class V2RequestCredentialHandler implements MessageHandler { messageContext: InboundMessageContext ) { messageContext.agentContext.config.logger.info(`Automatically sending credential with autoAccept`) - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - const offerMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: credentialRecord.id, - messageClass: V2OfferCredentialMessage, - }) + const offerMessage = await this.credentialProtocol.findOfferMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!offerMessage) { + throw new AriesFrameworkError(`Could not find offer message for credential record with id ${credentialRecord.id}`) + } const { message } = await this.credentialProtocol.acceptRequest(messageContext.agentContext, { credentialRecord, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service && offerMessage?.service) { - const recipientService = messageContext.message.service - const ourService = offerMessage.service - - // Set ~service, update message in record (for later use) - message.setService(ourService) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - associatedRecordId: credentialRecord.id, - role: DidCommMessageRole.Sender, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically issue credential`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: offerMessage, + }) } } diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts index 4877113fa0..43b517f9a4 100644 --- a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -1,8 +1,6 @@ import type { AgentContext } from '../../../agent' -import type { Logger } from '../../../logger' import type { ResolvedDidCommService } from '../types' -import { AgentConfig } from '../../../agent/AgentConfig' import { KeyType } from '../../../crypto' import { injectable } from '../../../plugins' import { DidResolverService } from '../../dids' @@ -12,11 +10,9 @@ import { findMatchingEd25519Key } from '../util/matchingEd25519Key' @injectable() export class DidCommDocumentService { - private logger: Logger private didResolverService: DidResolverService - public constructor(agentConfig: AgentConfig, didResolverService: DidResolverService) { - this.logger = agentConfig.logger + public constructor(didResolverService: DidResolverService) { this.didResolverService = didResolverService } diff --git a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts index ad57ed6372..8019274c46 100644 --- a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts +++ b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts @@ -1,7 +1,7 @@ import type { AgentContext } from '../../../../agent' import type { VerificationMethod } from '../../../dids' -import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { getAgentContext, mockFunction } from '../../../../../tests/helpers' import { Key, KeyType } from '../../../../crypto' import { DidCommV1Service, DidDocument, IndyAgentService } from '../../../dids' import { verkeyToInstanceOfKey } from '../../../dids/helpers' @@ -12,14 +12,13 @@ jest.mock('../../../dids/services/DidResolverService') const DidResolverServiceMock = DidResolverService as jest.Mock describe('DidCommDocumentService', () => { - const agentConfig = getAgentConfig('DidCommDocumentService') let didCommDocumentService: DidCommDocumentService let didResolverService: DidResolverService let agentContext: AgentContext beforeEach(async () => { didResolverService = new DidResolverServiceMock() - didCommDocumentService = new DidCommDocumentService(agentConfig, didResolverService) + didCommDocumentService = new DidCommDocumentService(didResolverService) agentContext = getAgentContext() }) diff --git a/packages/core/src/modules/dids/DidsModuleConfig.ts b/packages/core/src/modules/dids/DidsModuleConfig.ts index 8e065657b4..91c574caff 100644 --- a/packages/core/src/modules/dids/DidsModuleConfig.ts +++ b/packages/core/src/modules/dids/DidsModuleConfig.ts @@ -19,9 +19,9 @@ export interface DidsModuleConfigOptions { * List of did registrars that should be used by the dids module. The registrar must * be an instance of the {@link DidRegistrar} interface. * - * If no registrars are provided, the default registrars will be used. The `PeerDidRegistrar` will ALWAYS be - * registered, as it is needed for the connections and out of band module to function. Other did methods can be - * disabled. + * If no registrars are provided, the default registrars will be used. `PeerDidRegistrar` and `KeyDidRegistrar` + * will ALWAYS be registered, as they are needed for connections, mediation and out of band modules to function. + * Other did methods can be disabled. * * @default [KeyDidRegistrar, PeerDidRegistrar, JwkDidRegistrar] */ @@ -31,9 +31,9 @@ export interface DidsModuleConfigOptions { * List of did resolvers that should be used by the dids module. The resolver must * be an instance of the {@link DidResolver} interface. * - * If no resolvers are provided, the default resolvers will be used. The `PeerDidResolver` will ALWAYS be - * registered, as it is needed for the connections and out of band module to function. Other did methods can be - * disabled. + * If no resolvers are provided, the default resolvers will be used. `PeerDidResolver` and `KeyDidResolver` + * will ALWAYS be registered, as they are needed for connections, mediation and out of band modules to function. + * Other did methods can be disabled. * * @default [WebDidResolver, KeyDidResolver, PeerDidResolver, JwkDidResolver] */ @@ -62,6 +62,12 @@ export class DidsModuleConfig { registrars = [...registrars, new PeerDidRegistrar()] } + // Add key did registrar if it is not included yet + if (!registrars.find((registrar) => registrar instanceof KeyDidRegistrar)) { + // Do not modify original options array + registrars = [...registrars, new KeyDidRegistrar()] + } + this._registrars = registrars return registrars } @@ -88,6 +94,12 @@ export class DidsModuleConfig { resolvers = [...resolvers, new PeerDidResolver()] } + // Add key did resolver if it is not included yet + if (!resolvers.find((resolver) => resolver instanceof KeyDidResolver)) { + // Do not modify original options array + resolvers = [...resolvers, new KeyDidResolver()] + } + this._resolvers = resolvers return resolvers } diff --git a/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts index ef3dc3dc66..cf1d4a2e59 100644 --- a/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts @@ -29,8 +29,8 @@ describe('DidsModuleConfig', () => { }) test('sets values', () => { - const registrars = [new PeerDidRegistrar(), {} as DidRegistrar] - const resolvers = [new PeerDidResolver(), {} as DidResolver] + const registrars = [new PeerDidRegistrar(), new KeyDidRegistrar(), {} as DidRegistrar] + const resolvers = [new PeerDidResolver(), new KeyDidResolver(), {} as DidResolver] const config = new DidsModuleConfig({ registrars, resolvers, @@ -40,7 +40,7 @@ describe('DidsModuleConfig', () => { expect(config.resolvers).toEqual(resolvers) }) - test('adds peer did resolver and registrar if not provided in config', () => { + test('adds peer and key did resolvers and registrars if not provided in config', () => { const registrar = {} as DidRegistrar const resolver = {} as DidResolver const config = new DidsModuleConfig({ @@ -48,8 +48,8 @@ describe('DidsModuleConfig', () => { resolvers: [resolver], }) - expect(config.registrars).toEqual([registrar, expect.any(PeerDidRegistrar)]) - expect(config.resolvers).toEqual([resolver, expect.any(PeerDidResolver)]) + expect(config.registrars).toEqual([registrar, expect.any(PeerDidRegistrar), expect.any(KeyDidRegistrar)]) + expect(config.resolvers).toEqual([resolver, expect.any(PeerDidResolver), expect.any(KeyDidResolver)]) }) test('add resolver and registrar after creation', () => { diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index d96bfe2f16..0d6449d97c 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -20,14 +20,13 @@ import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' -import { DidCommMessageRepository, DidCommMessageRole } from '../../storage' +import { DidCommMessageRepository } from '../../storage' import { JsonEncoder, JsonTransformer } from '../../utils' import { parseMessageType, supportsIncomingMessageType } from '../../utils/messageType' import { parseInvitationShortUrl } from '../../utils/parseInvitation' import { ConnectionsApi, DidExchangeState, HandshakeProtocol } from '../connections' import { DidCommDocumentService } from '../didcomm' import { DidKey } from '../dids' -import { didKeyToVerkey } from '../dids/helpers' import { RoutingService } from '../routing/services/RoutingService' import { OutOfBandService } from './OutOfBandService' @@ -40,6 +39,7 @@ import { HandshakeReuseAcceptedHandler } from './handlers/HandshakeReuseAccepted import { convertToNewInvitation, convertToOldInvitation } from './helpers' import { OutOfBandInvitation } from './messages' import { OutOfBandRecord } from './repository/OutOfBandRecord' +import { OutOfBandRecordMetadataKeys } from './repository/outOfBandRecordMetadataTypes' const didCommProfiles = ['didcomm/aip1', 'didcomm/aip2;env=rfc19'] @@ -253,32 +253,32 @@ export class OutOfBandApi { } public async createLegacyConnectionlessInvitation(config: { - recordId: string + /** + * @deprecated this value is not used anymore, as the legacy connection-less exchange is now + * integrated with the out of band protocol. The value is kept to not break the API, but will + * be removed in a future version, and has no effect. + */ + recordId?: string message: Message domain: string routing?: Routing - }): Promise<{ message: Message; invitationUrl: string }> { - // Create keys (and optionally register them at the mediator) - const routing = config.routing ?? (await this.routingService.getRouting(this.agentContext)) - - // Set the service on the message - config.message.service = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey].map((key) => key.publicKeyBase58), - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), + }): Promise<{ message: Message; invitationUrl: string; outOfBandRecord: OutOfBandRecord }> { + const outOfBandRecord = await this.createInvitation({ + messages: [config.message], + routing: config.routing, }) - // We need to update the message with the new service, so we can - // retrieve it from storage later on. - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: config.message, - associatedRecordId: config.recordId, - role: DidCommMessageRole.Sender, - }) + // Resolve the service and set it on the message + const resolvedService = await this.outOfBandService.getResolvedServiceForOutOfBandServices( + this.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + config.message.service = ServiceDecorator.fromResolvedDidCommService(resolvedService) return { message: config.message, invitationUrl: `${config.domain}?d_m=${JsonEncoder.toBase64URL(JsonTransformer.toJSON(config.message))}`, + outOfBandRecord, } } @@ -385,6 +385,8 @@ export class OutOfBandApi { const messages = outOfBandInvitation.getRequests() + const isConnectionless = handshakeProtocols === undefined || handshakeProtocols.length === 0 + if ((!handshakeProtocols || handshakeProtocols.length === 0) && (!messages || messages?.length === 0)) { throw new AriesFrameworkError( 'One or both of handshake_protocols and requests~attach MUST be included in the message.' @@ -392,7 +394,7 @@ export class OutOfBandApi { } // Make sure we haven't received this invitation before - // It's fine if we created it (means that we are connnecting to ourselves) or if it's an implicit + // It's fine if we created it (means that we are connecting to ourselves) or if it's an implicit // invitation (it allows to connect multiple times to the same public did) if (!config.isImplicit) { const existingOobRecordsFromThisId = await this.outOfBandService.findAllByQuery(this.agentContext, { @@ -431,8 +433,21 @@ export class OutOfBandApi { outOfBandInvitation: outOfBandInvitation, autoAcceptConnection, tags: { recipientKeyFingerprints }, + mediatorId: routing?.mediatorId, }) + // If we have routing, and this is a connectionless exchange, or we are not auto accepting the connection + // we need to store the routing, so it can be used when we send the first message in response to this invitation + if (routing && (isConnectionless || !autoAcceptInvitation)) { + this.logger.debug('Storing routing for out of band invitation.') + outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.RecipientRouting, { + recipientKeyFingerprint: routing.recipientKey.fingerprint, + routingKeyFingerprints: routing.routingKeys.map((key) => key.fingerprint), + endpoints: routing.endpoints, + mediatorId: routing.mediatorId, + }) + } + await this.outOfBandService.save(this.agentContext, outOfBandRecord) this.outOfBandService.emitStateChangedEvent(this.agentContext, outOfBandRecord, null) @@ -473,6 +488,11 @@ export class OutOfBandApi { label?: string alias?: string imageUrl?: string + /** + * Routing for the exchange (either connection or connection-less exchange). + * + * If a connection is reused, the routing WILL NOT be used. + */ routing?: Routing timeoutMs?: number } @@ -480,11 +500,24 @@ export class OutOfBandApi { const outOfBandRecord = await this.outOfBandService.getById(this.agentContext, outOfBandId) const { outOfBandInvitation } = outOfBandRecord - const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, routing } = config + const { label, alias, imageUrl, autoAcceptConnection, reuseConnection } = config const services = outOfBandInvitation.getServices() const messages = outOfBandInvitation.getRequests() const timeoutMs = config.timeoutMs ?? 20000 + let routing = config.routing + + // recipient routing from the receiveInvitation method. + const recipientRouting = outOfBandRecord.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (!routing && recipientRouting) { + routing = { + recipientKey: Key.fromFingerprint(recipientRouting.recipientKeyFingerprint), + routingKeys: recipientRouting.routingKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint)), + endpoints: recipientRouting.endpoints, + mediatorId: recipientRouting.mediatorId, + } + } + const { handshakeProtocols } = outOfBandInvitation const existingConnection = await this.findExistingConnection(outOfBandInvitation) @@ -747,39 +780,6 @@ export class OutOfBandApi { this.logger.debug(`Message with type ${plaintextMessage['@type']} can be processed.`) - let serviceEndpoint: string | undefined - let recipientKeys: string[] | undefined - let routingKeys: string[] = [] - - // The framework currently supports only older OOB messages with `~service` decorator. - // TODO: support receiving messages with other services so we don't have to transform the service - // to ~service decorator - const [service] = services - - if (typeof service === 'string') { - const [didService] = await this.didCommDocumentService.resolveServicesFromDid(this.agentContext, service) - if (didService) { - serviceEndpoint = didService.serviceEndpoint - recipientKeys = didService.recipientKeys.map((key) => key.publicKeyBase58) - routingKeys = didService.routingKeys.map((key) => key.publicKeyBase58) || [] - } - } else { - serviceEndpoint = service.serviceEndpoint - recipientKeys = service.recipientKeys.map(didKeyToVerkey) - routingKeys = service.routingKeys?.map(didKeyToVerkey) || [] - } - - if (!serviceEndpoint || !recipientKeys) { - throw new AriesFrameworkError('Service not found') - } - - const serviceDecorator = new ServiceDecorator({ - recipientKeys, - routingKeys, - serviceEndpoint, - }) - - plaintextMessage['~service'] = JsonTransformer.toJSON(serviceDecorator) this.eventEmitter.emit(this.agentContext, { type: AgentEventTypes.AgentMessageReceived, payload: { diff --git a/packages/core/src/modules/oob/OutOfBandService.ts b/packages/core/src/modules/oob/OutOfBandService.ts index bdf9fb131c..1884cb694a 100644 --- a/packages/core/src/modules/oob/OutOfBandService.ts +++ b/packages/core/src/modules/oob/OutOfBandService.ts @@ -1,3 +1,4 @@ +import type { OutOfBandDidCommService } from './domain' import type { HandshakeReusedEvent, OutOfBandStateChangedEvent } from './domain/OutOfBandEvents' import type { AgentContext } from '../../agent' import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' @@ -9,7 +10,7 @@ import type { HandshakeProtocol } from '../connections/models' import { EventEmitter } from '../../agent/EventEmitter' import { AriesFrameworkError } from '../../error' import { injectable } from '../../plugins' -import { JsonTransformer } from '../../utils' +import { DidCommDocumentService } from '../didcomm/services/DidCommDocumentService' import { DidsApi } from '../dids' import { parseDid } from '../dids/domain/parse' @@ -32,10 +33,16 @@ export interface CreateFromImplicitInvitationConfig { export class OutOfBandService { private outOfBandRepository: OutOfBandRepository private eventEmitter: EventEmitter + private didCommDocumentService: DidCommDocumentService - public constructor(outOfBandRepository: OutOfBandRepository, eventEmitter: EventEmitter) { + public constructor( + outOfBandRepository: OutOfBandRepository, + eventEmitter: EventEmitter, + didCommDocumentService: DidCommDocumentService + ) { this.outOfBandRepository = outOfBandRepository this.eventEmitter = eventEmitter + this.didCommDocumentService = didCommDocumentService } /** @@ -252,4 +259,26 @@ export class OutOfBandService { const outOfBandRecord = await this.getById(agentContext, outOfBandId) return this.outOfBandRepository.delete(agentContext, outOfBandRecord) } + + /** + * Extract a resolved didcomm service from an out of band invitation. + * + * Currently the first service that can be resolved is returned. + */ + public async getResolvedServiceForOutOfBandServices( + agentContext: AgentContext, + services: Array + ) { + for (const service of services) { + if (typeof service === 'string') { + const [didService] = await this.didCommDocumentService.resolveServicesFromDid(agentContext, service) + + if (didService) return didService + } else { + return service.resolvedDidCommService + } + } + + throw new AriesFrameworkError('Could not extract a service from the out of band invitation.') + } } diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts index 4d89be6eaf..dafa6c5055 100644 --- a/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts +++ b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts @@ -1,3 +1,5 @@ +import type { DidCommDocumentService } from '../../didcomm' + import { Subject } from 'rxjs' import { @@ -30,12 +32,14 @@ const agentContext = getAgentContext() describe('OutOfBandService', () => { let outOfBandRepository: OutOfBandRepository let outOfBandService: OutOfBandService + let didCommDocumentService: DidCommDocumentService let eventEmitter: EventEmitter beforeEach(async () => { eventEmitter = new EventEmitter(agentDependencies, new Subject()) outOfBandRepository = new OutOfBandRepositoryMock() - outOfBandService = new OutOfBandService(outOfBandRepository, eventEmitter) + didCommDocumentService = {} as DidCommDocumentService + outOfBandService = new OutOfBandService(outOfBandRepository, eventEmitter, didCommDocumentService) }) describe('processHandshakeReuse', () => { diff --git a/packages/core/src/modules/oob/__tests__/helpers.test.ts b/packages/core/src/modules/oob/__tests__/helpers.test.ts index cc501335b6..8ecba2a69a 100644 --- a/packages/core/src/modules/oob/__tests__/helpers.test.ts +++ b/packages/core/src/modules/oob/__tests__/helpers.test.ts @@ -1,7 +1,7 @@ import { Attachment } from '../../../decorators/attachment/Attachment' import { JsonTransformer } from '../../../utils' import { ConnectionInvitationMessage } from '../../connections' -import { DidCommV1Service } from '../../dids' +import { OutOfBandDidCommService } from '../domain' import { convertToNewInvitation, convertToOldInvitation } from '../helpers' import { OutOfBandInvitation } from '../messages' @@ -120,7 +120,7 @@ describe('convertToOldInvitation', () => { imageUrl: 'https://my-image.com', label: 'a-label', services: [ - new DidCommV1Service({ + new OutOfBandDidCommService({ id: '#inline', recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], routingKeys: ['did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'], diff --git a/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts index ecfd87c4d3..8c747523f1 100644 --- a/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts +++ b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts @@ -1,9 +1,10 @@ +import type { ResolvedDidCommService } from '../../didcomm' import type { ValidationOptions } from 'class-validator' import { ArrayNotEmpty, buildMessage, IsOptional, isString, IsString, ValidateBy } from 'class-validator' import { isDid } from '../../../utils' -import { DidDocumentService } from '../../dids' +import { DidDocumentService, DidKey } from '../../dids' export class OutOfBandDidCommService extends DidDocumentService { public constructor(options: { @@ -35,6 +36,24 @@ export class OutOfBandDidCommService extends DidDocumentService { @IsString({ each: true }) @IsOptional() public accept?: string[] + + public get resolvedDidCommService(): ResolvedDidCommService { + return { + id: this.id, + recipientKeys: this.recipientKeys.map((didKey) => DidKey.fromDid(didKey).key), + routingKeys: this.routingKeys?.map((didKey) => DidKey.fromDid(didKey).key) ?? [], + serviceEndpoint: this.serviceEndpoint, + } + } + + public static fromResolvedDidCommService(service: ResolvedDidCommService) { + return new OutOfBandDidCommService({ + id: service.id, + recipientKeys: service.recipientKeys.map((key) => new DidKey(key).did), + routingKeys: service.routingKeys.map((key) => new DidKey(key).did), + serviceEndpoint: service.serviceEndpoint, + }) + } } /** diff --git a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts index 202e6a3886..a4dd0c670f 100644 --- a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts +++ b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts @@ -1,3 +1,4 @@ +import type { OutOfBandRecordMetadata } from './outOfBandRecordMetadataTypes' import type { TagsBase } from '../../../storage/BaseRecord' import type { OutOfBandRole } from '../domain/OutOfBandRole' import type { OutOfBandState } from '../domain/OutOfBandState' @@ -6,6 +7,7 @@ import { Type } from 'class-transformer' import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' +import { getThreadIdFromPlainTextMessage } from '../../../utils/thread' import { uuid } from '../../../utils/uuid' import { OutOfBandInvitation } from '../messages' @@ -14,6 +16,11 @@ type DefaultOutOfBandRecordTags = { state: OutOfBandState invitationId: string threadId?: string + /** + * The thread ids from the attached request messages from the out + * of band invitation. + */ + invitationRequestsThreadIds?: string[] } interface CustomOutOfBandRecordTags extends TagsBase { @@ -36,7 +43,11 @@ export interface OutOfBandRecordProps { threadId?: string } -export class OutOfBandRecord extends BaseRecord { +export class OutOfBandRecord extends BaseRecord< + DefaultOutOfBandRecordTags, + CustomOutOfBandRecordTags, + OutOfBandRecordMetadata +> { @Type(() => OutOfBandInvitation) public outOfBandInvitation!: OutOfBandInvitation public role!: OutOfBandRole @@ -75,6 +86,9 @@ export class OutOfBandRecord extends BaseRecord getThreadIdFromPlainTextMessage(r)), } } diff --git a/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts b/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts new file mode 100644 index 0000000000..f092807324 --- /dev/null +++ b/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts @@ -0,0 +1,12 @@ +export enum OutOfBandRecordMetadataKeys { + RecipientRouting = '_internal/recipientRouting', +} + +export type OutOfBandRecordMetadata = { + [OutOfBandRecordMetadataKeys.RecipientRouting]: { + recipientKeyFingerprint: string + routingKeyFingerprints: string[] + endpoints: string[] + mediatorId?: string + } +} diff --git a/packages/core/src/modules/proofs/ProofsApi.ts b/packages/core/src/modules/proofs/ProofsApi.ts index ed78afec2d..483ad5e2c1 100644 --- a/packages/core/src/modules/proofs/ProofsApi.ts +++ b/packages/core/src/modules/proofs/ProofsApi.ts @@ -29,13 +29,9 @@ import { injectable } from 'tsyringe' import { MessageSender } from '../../agent/MessageSender' import { AgentContext } from '../../agent/context/AgentContext' -import { OutboundMessageContext } from '../../agent/models' -import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { AriesFrameworkError } from '../../error' -import { DidCommMessageRepository } from '../../storage' -import { DidCommMessageRole } from '../../storage/didcomm/DidCommMessageRole' import { ConnectionService } from '../connections/services/ConnectionService' -import { RoutingService } from '../routing/services/RoutingService' import { ProofsModuleConfig } from './ProofsModuleConfig' import { ProofState } from './models/ProofState' @@ -98,9 +94,7 @@ export class ProofsApi implements ProofsApi { private connectionService: ConnectionService private messageSender: MessageSender - private routingService: RoutingService private proofRepository: ProofRepository - private didCommMessageRepository: DidCommMessageRepository private agentContext: AgentContext public constructor( @@ -108,16 +102,12 @@ export class ProofsApi implements ProofsApi { connectionService: ConnectionService, agentContext: AgentContext, proofRepository: ProofRepository, - routingService: RoutingService, - didCommMessageRepository: DidCommMessageRepository, config: ProofsModuleConfig ) { this.messageSender = messageSender this.connectionService = connectionService this.proofRepository = proofRepository this.agentContext = agentContext - this.routingService = routingService - this.didCommMessageRepository = didCommMessageRepository this.config = config } @@ -155,10 +145,10 @@ export class ProofsApi implements ProofsApi { parentThreadId: options.parentThreadId, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -198,10 +188,10 @@ export class ProofsApi implements ProofsApi { }) // send the message - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -240,10 +230,10 @@ export class ProofsApi implements ProofsApi { willConfirm: options.willConfirm, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -274,10 +264,10 @@ export class ProofsApi implements ProofsApi { willConfirm: options.willConfirm, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -298,75 +288,33 @@ export class ProofsApi implements ProofsApi { const protocol = this.getProtocol(proofRecord.protocolVersion) const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) + } // Use connection if present - if (proofRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) - - // Assert - connectionRecord.assertReady() - - const { message } = await protocol.acceptRequest(this.agentContext, { - proofFormats: options.proofFormats, - proofRecord, - comment: options.comment, - autoAcceptProof: options.autoAcceptProof, - goalCode: options.goalCode, - }) - - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: proofRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) - - return proofRecord - } + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() - // Use ~service decorator otherwise - else if (requestMessage?.service) { - // Create ~service decorator - const routing = await this.routingService.getRouting(this.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = requestMessage.service - - const { message } = await protocol.acceptRequest(this.agentContext, { - proofFormats: options.proofFormats, - proofRecord, - comment: options.comment, - autoAcceptProof: options.autoAcceptProof, - goalCode: options.goalCode, - }) - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: proofRecord.id, - }) - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: options.useReturnRoute ?? true, // defaults to true if missing - }, - }) - ) - return proofRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept presentation request without connectionId or ~service decorator on presentation request.` - ) - } + const { message } = await protocol.acceptRequest(this.agentContext, { + proofFormats: options.proofFormats, + proofRecord, + comment: options.comment, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: proofRecord, + lastReceivedMessage: requestMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord } public async declineRequest(options: DeclineProofRequestOptions): Promise { @@ -414,13 +362,13 @@ export class ProofsApi implements ProofsApi { comment: options.comment, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, associatedRecord: proofRecord, }) - await this.messageSender.sendMessage(outboundMessageContext) + return proofRecord } @@ -460,56 +408,36 @@ export class ProofsApi implements ProofsApi { const protocol = this.getProtocol(proofRecord.protocolVersion) const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) + } + const presentationMessage = await protocol.findPresentationMessage(this.agentContext, proofRecord.id) + if (!presentationMessage) { + throw new AriesFrameworkError(`No presentation message found for proof record with id '${proofRecord.id}'`) + } // Use connection if present - if (proofRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() - // Assert - connectionRecord.assertReady() - - const { message } = await protocol.acceptPresentation(this.agentContext, { - proofRecord, - }) - - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: proofRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) + const { message } = await protocol.acceptPresentation(this.agentContext, { + proofRecord, + }) - return proofRecord - } - // Use ~service decorator otherwise - else if (requestMessage?.service && presentationMessage?.service) { - const recipientService = presentationMessage.service - const ourService = requestMessage.service - - const { message } = await protocol.acceptPresentation(this.agentContext, { - proofRecord, - }) - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: false, // hard wire to be false since it's the end of the protocol so not needed here - }, - }) - ) + // FIXME: returnRoute: false + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: proofRecord, + lastSentMessage: requestMessage, + lastReceivedMessage: presentationMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) - return proofRecord - } - // Cannot send message without credentialId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept presentation without connectionId or ~service decorator on presentation message.` - ) - } + return proofRecord } /** @@ -570,50 +498,30 @@ export class ProofsApi implements ProofsApi { description: options.description, }) - if (proofRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) - - // Assert - connectionRecord.assertReady() - - const outboundMessageContext = new OutboundMessageContext(problemReport, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: proofRecord, - }) + // Use connection if present + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() - await this.messageSender.sendMessage(outboundMessageContext) - return proofRecord - } else if (requestMessage?.service) { + // If there's no connection (so connection-less, we require the state to be request received) + if (!connectionRecord) { proofRecord.assertState(ProofState.RequestReceived) - // Create ~service decorator - const routing = await this.routingService.getRouting(this.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = requestMessage.service - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(problemReport, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - ) - - return proofRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot send problem report without connectionId or ~service decorator on presentation request.` - ) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) + } } + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: problemReport, + connectionRecord, + associatedRecord: proofRecord, + lastReceivedMessage: requestMessage ?? undefined, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord } public async getFormatData(proofRecordId: string): Promise>> { diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts index 7c3bfbc88c..733fbc9f5c 100644 --- a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts @@ -172,11 +172,11 @@ export class V2ProofProtocol { ) agents = [aliceAgent, faberAgent, mediatorAgent] - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age', 'image_0', 'image_1'], }) diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts index 73216d17c1..e74d153a3e 100644 --- a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts @@ -2,7 +2,7 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../. import type { ProofExchangeRecord } from '../../../repository' import type { V2ProofProtocol } from '../V2ProofProtocol' -import { OutboundMessageContext } from '../../../../../agent/models' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' import { DidCommMessageRepository } from '../../../../../storage' import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' @@ -38,31 +38,17 @@ export class V2PresentationHandler implements MessageHandler { }) const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - const requestMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { associatedRecordId: proofRecord.id, messageClass: V2RequestPresentationMessage, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: proofRecord, - }) - } else if (requestMessage?.service && messageContext.message?.service) { - const recipientService = messageContext.message?.service - const ourService = requestMessage?.service - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create presentation ack`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: proofRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) } } diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts index 0eaad0f2d0..394a4ff2a9 100644 --- a/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts @@ -2,11 +2,7 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../. import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' import type { V2ProofProtocol } from '../V2ProofProtocol' -import { OutboundMessageContext } from '../../../../../agent/models' -import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' -import { DidCommMessageRole } from '../../../../../storage' -import { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' -import { RoutingService } from '../../../../routing' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' import { V2RequestPresentationMessage } from '../messages/V2RequestPresentationMessage' export class V2RequestPresentationHandler implements MessageHandler { @@ -42,40 +38,11 @@ export class V2RequestPresentationHandler implements MessageHandler { proofRecord, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: proofRecord, - }) - } else if (messageContext.message.service) { - const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - - const routing = await routingService.getRouting(messageContext.agentContext) - message.service = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service - - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: message.service.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create presentation`) + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) } } diff --git a/packages/core/src/modules/routing/services/MediationRecipientService.ts b/packages/core/src/modules/routing/services/MediationRecipientService.ts index 2d12fc96fe..c61e50bd54 100644 --- a/packages/core/src/modules/routing/services/MediationRecipientService.ts +++ b/packages/core/src/modules/routing/services/MediationRecipientService.ts @@ -18,7 +18,6 @@ import { OutboundMessageContext } from '../../../agent/models' import { Key, KeyType } from '../../../crypto' import { AriesFrameworkError } from '../../../error' import { injectable } from '../../../plugins' -import { JsonTransformer } from '../../../utils' import { ConnectionType } from '../../connections/models/ConnectionType' import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes' import { ConnectionService } from '../../connections/services/ConnectionService' diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts index 206e6d6c41..7b3df9b861 100644 --- a/packages/core/src/modules/routing/services/MediatorService.ts +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -9,7 +9,7 @@ import type { ForwardMessage, MediationRequestMessage } from '../messages' import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' import { KeyType } from '../../../crypto' -import { AriesFrameworkError } from '../../../error' +import { AriesFrameworkError, RecordDuplicateError } from '../../../error' import { Logger } from '../../../logger' import { injectable, inject } from '../../../plugins' import { ConnectionService } from '../../connections' @@ -38,7 +38,6 @@ export class MediatorService { private mediatorRoutingRepository: MediatorRoutingRepository private eventEmitter: EventEmitter private connectionService: ConnectionService - private _mediatorRoutingRecord?: MediatorRoutingRecord public constructor( mediationRepository: MediationRepository, @@ -209,18 +208,33 @@ export class MediatorService { routingKeys: [routingKey.publicKeyBase58], }) - await this.mediatorRoutingRepository.save(agentContext, routingRecord) - - this.eventEmitter.emit(agentContext, { - type: RoutingEventTypes.RoutingCreatedEvent, - payload: { - routing: { - endpoints: agentContext.config.endpoints, - routingKeys: [], - recipientKey: routingKey, + try { + await this.mediatorRoutingRepository.save(agentContext, routingRecord) + this.eventEmitter.emit(agentContext, { + type: RoutingEventTypes.RoutingCreatedEvent, + payload: { + routing: { + endpoints: agentContext.config.endpoints, + routingKeys: [], + recipientKey: routingKey, + }, }, - }, - }) + }) + } catch (error) { + // This addresses some race conditions issues where we first check if the record exists + // then we create one if it doesn't, but another process has created one in the meantime + // Although not the most elegant solution, it addresses the issues + if (error instanceof RecordDuplicateError) { + // the record already exists, which is our intended end state + // we can ignore this error and fetch the existing record + return this.mediatorRoutingRepository.getById( + agentContext, + this.mediatorRoutingRepository.MEDIATOR_ROUTING_RECORD_ID + ) + } else { + throw error + } + } return routingRecord } diff --git a/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts index a5ac4eff54..15831a8a54 100644 --- a/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts +++ b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts @@ -12,7 +12,6 @@ import { ConnectionMetadataKeys } from '../../../connections/repository/Connecti import { ConnectionRepository } from '../../../connections/repository/ConnectionRepository' import { ConnectionService } from '../../../connections/services/ConnectionService' import { DidRepository } from '../../../dids/repository/DidRepository' -import { DidRegistrarService } from '../../../dids/services/DidRegistrarService' import { RoutingEventTypes } from '../../RoutingEvents' import { KeylistUpdateAction, @@ -40,9 +39,6 @@ const EventEmitterMock = EventEmitter as jest.Mock jest.mock('../../../../agent/MessageSender') const MessageSenderMock = MessageSender as jest.Mock -jest.mock('../../../dids/services/DidRegistrarService') -const DidRegistrarServiceMock = DidRegistrarService as jest.Mock - const connectionImageUrl = 'https://example.com/image.png' describe('MediationRecipientService', () => { @@ -53,7 +49,6 @@ describe('MediationRecipientService', () => { let mediationRepository: MediationRepository let didRepository: DidRepository - let didRegistrarService: DidRegistrarService let eventEmitter: EventEmitter let connectionService: ConnectionService let connectionRepository: ConnectionRepository @@ -72,14 +67,7 @@ describe('MediationRecipientService', () => { eventEmitter = new EventEmitterMock() connectionRepository = new ConnectionRepositoryMock() didRepository = new DidRepositoryMock() - didRegistrarService = new DidRegistrarServiceMock() - connectionService = new ConnectionService( - config.logger, - connectionRepository, - didRepository, - didRegistrarService, - eventEmitter - ) + connectionService = new ConnectionService(config.logger, connectionRepository, didRepository, eventEmitter) mediationRepository = new MediationRepositoryMock() messageSender = new MessageSenderMock() diff --git a/packages/core/src/storage/BaseRecord.ts b/packages/core/src/storage/BaseRecord.ts index 10c16e8d5d..7f26952200 100644 --- a/packages/core/src/storage/BaseRecord.ts +++ b/packages/core/src/storage/BaseRecord.ts @@ -15,6 +15,13 @@ export type Tags = Cu export type RecordTags = ReturnType +// The BaseRecord requires a DefaultTags and CustomTags type, but we want to be +// able to use the BaseRecord without specifying these types. If we don't specify +// these types, the default TagsBase will be used, but this is not compatible +// with records that have specified a custom type. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type BaseRecordAny = BaseRecord + export abstract class BaseRecord< DefaultTags extends TagsBase = TagsBase, CustomTags extends TagsBase = TagsBase, diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts index 9f234bb15b..4de5321396 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -1,6 +1,6 @@ import type { DidCommMessageRole } from './DidCommMessageRole' import type { ConstructableAgentMessage } from '../../agent/AgentMessage' -import type { JsonObject } from '../../types' +import type { PlaintextMessage } from '../../types' import { AriesFrameworkError } from '../../error' import { JsonTransformer } from '../../utils/JsonTransformer' @@ -25,14 +25,14 @@ export type DefaultDidCommMessageTags = { export interface DidCommMessageRecordProps { role: DidCommMessageRole - message: JsonObject + message: PlaintextMessage id?: string createdAt?: Date associatedRecordId?: string } export class DidCommMessageRecord extends BaseRecord { - public message!: JsonObject + public message!: PlaintextMessage public role!: DidCommMessageRole /** diff --git a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts index cffa511e3a..245b621790 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts @@ -1,7 +1,6 @@ import type { DidCommMessageRole } from './DidCommMessageRole' import type { AgentContext } from '../../agent' import type { AgentMessage, ConstructableAgentMessage } from '../../agent/AgentMessage' -import type { JsonObject } from '../../types' import { EventEmitter } from '../../agent/EventEmitter' import { InjectionSymbols } from '../../constants' @@ -26,7 +25,7 @@ export class DidCommMessageRepository extends Repository { { role, agentMessage, associatedRecordId }: SaveAgentMessageOptions ) { const didCommMessageRecord = new DidCommMessageRecord({ - message: agentMessage.toJSON() as JsonObject, + message: agentMessage.toJSON(), role, associatedRecordId, }) @@ -45,7 +44,7 @@ export class DidCommMessageRepository extends Repository { }) if (record) { - record.message = options.agentMessage.toJSON() as JsonObject + record.message = options.agentMessage.toJSON() record.role = options.role await this.update(agentContext, record) return diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap index 0b66feb17b..543720be21 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap @@ -783,6 +783,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "1-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "d56fd7af-852e-458e-b750-7a4f4e53d6e6", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MkfiPMPxCQeSDZGMkCvm1Y2rBoPsmw4ZHMv71jXtcWRRiM", ], @@ -849,6 +850,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "2-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "d939d371-3155-4d9c-87d1-46447f624f44", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MktCZAQNGvWb4WHAjwBqPtXhZdDYorbSJkGW9vj1uhw1HD", ], @@ -915,6 +917,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "3-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "21ef606f-b25b-48c6-bafa-e79193732413", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6Mkt1tsp15cnDD7wBCFgehiR2SxHX1aPxt4sueE24twH9Bd", ], @@ -981,6 +984,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "4-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "08eb8d8b-67cf-4ce2-9aca-c7d260a5c143", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6Mkmod8vp2nURVktVC5ceQeyr2VUz26iu2ZANLNVg9pMawa", ], @@ -1047,6 +1051,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "5-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "cc67fb5e-1414-4ba6-9030-7456ccd2aaea", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MkjDJL4X7YGoH6gjamhZR2NzowPZqtJfX5kPuNuWiVdjMr", ], @@ -1113,6 +1118,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "6-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "f0ca03d8-2e11-4ff2-a5fc-e0137a434b7e", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6Mko31DNE3gqMRZj1JNhv2BHb1caQshcd9njgKkEQXsgFRp", ], @@ -1174,6 +1180,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "7-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "1f516e35-08d3-43d8-900c-99d5239f54da", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MkuWTEmH1mUo6W96zSWyH612hFHowRzNEscPYBL2CCMyC2", ], diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts b/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts index 61c2ddb3af..25c751bd5f 100644 --- a/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts +++ b/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts @@ -1,6 +1,6 @@ import type { BaseAgent } from '../../../../agent/BaseAgent' import type { CredentialExchangeRecord } from '../../../../modules/credentials' -import type { JsonObject } from '../../../../types' +import type { JsonObject, PlaintextMessage } from '../../../../types' import { CredentialState } from '../../../../modules/credentials/models/CredentialState' import { CredentialRepository } from '../../../../modules/credentials/repository/CredentialRepository' @@ -224,7 +224,7 @@ export async function moveDidCommMessages( `Starting move of ${messageKey} from credential record with id ${credentialRecord.id} to DIDCommMessageRecord` ) const credentialRecordJson = credentialRecord as unknown as JsonObject - const message = credentialRecordJson[messageKey] as JsonObject | undefined + const message = credentialRecordJson[messageKey] as PlaintextMessage | undefined if (message) { const credentialRole = getCredentialRole(credentialRecord) diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts b/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts index 7e923a3d48..b5eb0ec98d 100644 --- a/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts +++ b/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts @@ -1,6 +1,6 @@ import type { BaseAgent } from '../../../../agent/BaseAgent' import type { ProofExchangeRecord } from '../../../../modules/proofs' -import type { JsonObject } from '../../../../types' +import type { JsonObject, PlaintextMessage } from '../../../../types' import { ProofState } from '../../../../modules/proofs/models' import { ProofRepository } from '../../../../modules/proofs/repository/ProofRepository' @@ -131,7 +131,7 @@ export async function moveDidCommMessages(agent: Agent, `Starting move of ${messageKey} from proof record with id ${proofRecord.id} to DIDCommMessageRecord` ) const proofRecordJson = proofRecord as unknown as JsonObject - const message = proofRecordJson[messageKey] as JsonObject | undefined + const message = proofRecordJson[messageKey] as PlaintextMessage | undefined if (message) { const proofRole = getProofRole(proofRecord) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 0143c5c406..b42891fe9a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -35,10 +35,33 @@ export interface WalletExportImportConfig { } export type EncryptedMessage = { + /** + * The "protected" member MUST be present and contain the value + * BASE64URL(UTF8(JWE Protected Header)) when the JWE Protected + * Header value is non-empty; otherwise, it MUST be absent. These + * Header Parameter values are integrity protected. + */ protected: string - iv: unknown - ciphertext: unknown - tag: unknown + + /** + * The "iv" member MUST be present and contain the value + * BASE64URL(JWE Initialization Vector) when the JWE Initialization + * Vector value is non-empty; otherwise, it MUST be absent. + */ + iv: string + + /** + * The "ciphertext" member MUST be present and contain the value + * BASE64URL(JWE Ciphertext). + */ + ciphertext: string + + /** + * The "tag" member MUST be present and contain the value + * BASE64URL(JWE Authentication Tag) when the JWE Authentication Tag + * value is non-empty; otherwise, it MUST be absent. + */ + tag: string } export enum DidCommMimeType { diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index 3a908af7bd..68db977e62 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -4,11 +4,14 @@ import type { Response } from 'node-fetch' import { AbortController } from 'abort-controller' import { parseUrl } from 'query-string' +import { AgentMessage } from '../agent/AgentMessage' import { AriesFrameworkError } from '../error' import { ConnectionInvitationMessage } from '../modules/connections' +import { OutOfBandDidCommService } from '../modules/oob/domain/OutOfBandDidCommService' import { convertToNewInvitation } from '../modules/oob/helpers' import { OutOfBandInvitation } from '../modules/oob/messages' +import { JsonEncoder } from './JsonEncoder' import { JsonTransformer } from './JsonTransformer' import { MessageValidator } from './MessageValidator' import { parseMessageType, supportsIncomingMessageType } from './messageType' @@ -102,9 +105,36 @@ export const parseInvitationShortUrl = async ( if (parsedUrl['oob']) { const outOfBandInvitation = OutOfBandInvitation.fromUrl(invitationUrl) return outOfBandInvitation - } else if (parsedUrl['c_i'] || parsedUrl['d_m']) { + } else if (parsedUrl['c_i']) { const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) return convertToNewInvitation(invitation) + } + // Legacy connectionless invitation + else if (parsedUrl['d_m']) { + const messageJson = JsonEncoder.fromBase64(parsedUrl['d_m'] as string) + const agentMessage = JsonTransformer.fromJSON(messageJson, AgentMessage) + + // ~service is required for legacy connectionless invitations + if (!agentMessage.service) { + throw new AriesFrameworkError('Invalid legacy connectionless invitation url. Missing ~service decorator.') + } + + // This destructuring removes the ~service property from the message, and + // we can can use messageWithoutService to create the out of band invitation + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { '~service': service, ...messageWithoutService } = messageJson + + // transform into out of band invitation + const invitation = new OutOfBandInvitation({ + // The label is currently required by the OutOfBandInvitation class, but not according to the specification. + // FIXME: In 0.5.0 we will make this optional: https://github.com/hyperledger/aries-framework-javascript/issues/1524 + label: '', + services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], + }) + + invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) + + return invitation } else { try { return oobInvitationFromShortUrl(await fetchShortUrl(invitationUrl, dependencies)) diff --git a/packages/core/src/utils/thread.ts b/packages/core/src/utils/thread.ts new file mode 100644 index 0000000000..a8dd1a668a --- /dev/null +++ b/packages/core/src/utils/thread.ts @@ -0,0 +1,5 @@ +import type { PlaintextMessage } from '../types' + +export function getThreadIdFromPlainTextMessage(message: PlaintextMessage) { + return message['~thread']?.thid ?? message['@id'] +} diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 8537d5c35f..517c53a274 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -28,6 +28,7 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { + OutOfBandDidCommService, ConnectionsModule, ConnectionEventTypes, TypedArrayEncoder, @@ -45,7 +46,6 @@ import { TrustPingEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' -import { DidCommV1Service } from '../src/modules/dids' import { DidKey } from '../src/modules/dids/methods/key' import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' @@ -506,9 +506,8 @@ export function getMockOutOfBand({ accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], handshakeProtocols: [HandshakeProtocol.DidExchange], services: [ - new DidCommV1Service({ + new OutOfBandDidCommService({ id: `#inline-0`, - priority: 0, serviceEndpoint: serviceEndpoint ?? 'http://example.com', recipientKeys, routingKeys: [], diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index c05cb5c0a2..fece27bda0 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -17,8 +17,7 @@ import { OutOfBandEventTypes } from '../src/modules/oob/domain/OutOfBandEvents' import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' import { OutOfBandInvitation } from '../src/modules/oob/messages' -import { DidCommMessageRepository, DidCommMessageRole } from '../src/storage' -import { JsonEncoder } from '../src/utils' +import { JsonEncoder, JsonTransformer } from '../src/utils' import { TestMessage } from './TestMessage' import { getAgentOptions, waitForCredentialRecord } from './helpers' @@ -87,6 +86,8 @@ describe('out of band', () => { aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() + await aliceAgent.modules.anoncreds.createLinkSecret() + const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age', 'profile_picture', 'x-ray'], }) @@ -720,50 +721,213 @@ describe('out of band', () => { }) }) - describe('createLegacyConnectionlessInvitation', () => { - test('add ~service decorator to the message and returns invitation url', async () => { - const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + describe('connection-less exchange', () => { + test('oob exchange without handshake where response is received to invitation', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshake: false, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord - const { message: offerMessage, invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('oob exchange without handshake where response is received and custom routing is used on recipient', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshake: false, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord + + const routing = await aliceAgent.mediationRecipient.getRouting({}) + + await aliceAgent.oob.receiveInvitation(outOfBandInvitation, { + routing, + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + const faberCredentialRecord = await faberCredentialRecordPromise + + const faberCredentialRequest = await faberAgent.credentials.findRequestMessage(faberCredentialRecord.id) + + expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ + recipientKeys: [routing.recipientKey.publicKeyBase58], + serviceEndpoint: routing.endpoints[0], + routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), + }) + }) + + test('legacy connectionless exchange where response is received to invitation', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', + message, recordId: credentialRecord.id, - domain: 'https://test.com', + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('legacy connectionless exchange where response is received to invitation and custom routing is used on recipient', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', message, + recordId: credentialRecord.id, }) - expect(offerMessage.service).toMatchObject({ - serviceEndpoint: expect.any(String), - recipientKeys: [expect.any(String)], - routingKeys: [], + const routing = await aliceAgent.mediationRecipient.getRouting({}) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, }) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl, { routing }) - expect(invitationUrl).toEqual(expect.stringContaining('https://test.com?d_m=')) + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) - const messageBase64 = invitationUrl.split('https://test.com?d_m=')[1] + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) - expect(JsonEncoder.fromBase64(messageBase64)).toMatchObject({ - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + const faberCredentialRecord = await faberCredentialRecordPromise + + const faberCredentialRequest = await faberAgent.credentials.findRequestMessage(faberCredentialRecord.id) + + expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ + recipientKeys: [routing.recipientKey.publicKeyBase58], + serviceEndpoint: routing.endpoints[0], + routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), }) }) - test('updates the message in the didCommMessageRepository', async () => { + test('legacy connectionless exchange without receiving message through oob receiveInvitation, where response is received to invitation', async () => { const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { message: messageWithService } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', + message, + recordId: credentialRecord.id, + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + await aliceAgent.receiveMessage(messageWithService.toJSON()) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) - const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + await faberCredentialRecordPromise + }) - const saveOrUpdateSpy = jest.spyOn(didCommMessageRepository, 'saveOrUpdateAgentMessage') - saveOrUpdateSpy.mockResolvedValue() + test('add ~service decorator to the message and returns invitation url in createLegacyConnectionlessInvitation', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) - await faberAgent.oob.createLegacyConnectionlessInvitation({ + const { message: offerMessage, invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ recordId: credentialRecord.id, domain: 'https://test.com', message, }) - expect(saveOrUpdateSpy).toHaveBeenCalledWith(expect.anything(), { - agentMessage: message, - associatedRecordId: credentialRecord.id, - role: DidCommMessageRole.Sender, + expect(offerMessage.service).toMatchObject({ + serviceEndpoint: expect.any(String), + recipientKeys: [expect.any(String)], + routingKeys: [], + }) + + expect(invitationUrl).toEqual(expect.stringContaining('https://test.com?d_m=')) + + const messageBase64 = invitationUrl.split('https://test.com?d_m=')[1] + + expect(JsonEncoder.fromBase64(messageBase64)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', }) }) }) diff --git a/packages/indy-sdk/src/IndySdkModuleConfig.ts b/packages/indy-sdk/src/IndySdkModuleConfig.ts index 5bb066bebb..65478f507c 100644 --- a/packages/indy-sdk/src/IndySdkModuleConfig.ts +++ b/packages/indy-sdk/src/IndySdkModuleConfig.ts @@ -50,6 +50,12 @@ export interface IndySdkModuleConfigOptions { * ``` */ networks?: IndySdkPoolConfig[] + + /** + * Create a default link secret if there are no created link secrets. + * @defaultValue true + */ + autoCreateLinkSecret?: boolean } export class IndySdkModuleConfig { @@ -67,4 +73,9 @@ export class IndySdkModuleConfig { public get networks() { return this.options.networks ?? [] } + + /** See {@link AnonCredsModuleConfigOptions.autoCreateLinkSecret} */ + public get autoCreateLinkSecret() { + return this.options.autoCreateLinkSecret ?? true + } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts index ef44edb2e2..0557305ea7 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts @@ -28,9 +28,11 @@ import { parseIndyCredentialDefinitionId, AnonCredsLinkSecretRepository, generateLegacyProverDidLikeString, + storeLinkSecret, } from '@aries-framework/anoncreds' import { AriesFrameworkError, injectable, inject, utils } from '@aries-framework/core' +import { IndySdkModuleConfig } from '../../IndySdkModuleConfig' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' @@ -154,7 +156,8 @@ export class IndySdkHolderService implements AnonCredsHolderService { indyProof, }) - return indyProof + // FIXME IndyProof if badly typed in indy-sdk. It contains a `requested_predicates` property, which should be `predicates`. + return indyProof as unknown as AnonCredsProof } catch (error) { agentContext.config.logger.error(`Error creating Indy Proof`, { error, @@ -283,15 +286,20 @@ export class IndySdkHolderService implements AnonCredsHolderService { const proverDid = generateLegacyProverDidLikeString() // If a link secret is specified, use it. Otherwise, attempt to use default link secret - const linkSecretRecord = options.linkSecretId + let linkSecretRecord = options.linkSecretId ? await linkSecretRepository.getByLinkSecretId(agentContext, options.linkSecretId) : await linkSecretRepository.findDefault(agentContext) + // No default link secret. Automatically create one if set on module config if (!linkSecretRecord) { - // No default link secret - throw new AriesFrameworkError( - 'No link secret provided to createCredentialRequest and no default link secret has been found' - ) + const moduleConfig = agentContext.dependencyManager.resolve(IndySdkModuleConfig) + if (!moduleConfig.autoCreateLinkSecret) { + throw new AriesFrameworkError( + 'No link secret provided to createCredentialRequest and no default link secret has been found' + ) + } + const { linkSecretId } = await this.createLinkSecret(agentContext, {}) + linkSecretRecord = await storeLinkSecret(agentContext, { linkSecretId, setAsDefault: true }) } try { diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts index 5d03e7e18c..80aee7be6f 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts @@ -1,4 +1,4 @@ -import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' +import type { AnonCredsProof, AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefs, Schemas, RevocRegDefs, RevRegs, IndyProofRequest, IndyProof } from 'indy-sdk' @@ -82,7 +82,8 @@ export class IndySdkVerifierService implements AnonCredsVerifierService { return await this.indySdk.verifierVerifyProof( options.proofRequest as IndyProofRequest, - options.proof as IndyProof, + // FIXME IndyProof if badly typed in indy-sdk. It contains a `requested_predicates` property, which should be `predicates`. + options.proof as unknown as IndyProof, indySchemas, indyCredentialDefinitions, indyRevocationDefinitions, diff --git a/packages/indy-sdk/tests/setupIndySdkModule.ts b/packages/indy-sdk/tests/setupIndySdkModule.ts index b4a30f799a..f4a2ca8c59 100644 --- a/packages/indy-sdk/tests/setupIndySdkModule.ts +++ b/packages/indy-sdk/tests/setupIndySdkModule.ts @@ -1,4 +1,4 @@ -import { DidsModule, KeyDidRegistrar, KeyDidResolver, utils } from '@aries-framework/core' +import { DidsModule, utils } from '@aries-framework/core' import indySdk from 'indy-sdk' import { genesisPath, taaVersion, taaAcceptanceMechanism } from '../../core/tests/helpers' @@ -29,7 +29,7 @@ export const getIndySdkModuleConfig = () => export const getIndySdkModules = () => ({ indySdk: new IndySdkModule(getIndySdkModuleConfig()), dids: new DidsModule({ - registrars: [new IndySdkIndyDidRegistrar(), new KeyDidRegistrar()], - resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver(), new KeyDidResolver()], + registrars: [new IndySdkIndyDidRegistrar()], + resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver()], }), }) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index 7f998e247d..09e2c4e4a1 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -319,6 +319,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { verificationKey, alias, diddocContent, + role: options.options.role, }) if (services && useEndpointAttrib) { @@ -388,6 +389,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { signingKey?: Key alias: string | undefined diddocContent?: Record + role?: NymRequestRole }) { const { agentContext, @@ -397,6 +399,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { verificationKey, alias, signingKey, + role, } = options // FIXME: Add diddocContent when supported by indy-vdr @@ -408,7 +411,8 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { submitterDid: submitterNamespaceIdentifier, dest: namespaceIdentifier, verkey: verificationKey.publicKeyBase58, - alias: alias, + alias, + role, }) if (!signingKey) return request @@ -487,7 +491,7 @@ interface IndyVdrDidCreateOptionsBase extends DidCreateOptions { didDocument?: never // Not yet supported options: { alias?: string - role?: string + role?: NymRequestRole services?: DidDocumentService[] useEndpointAttrib?: boolean verkey?: string @@ -566,3 +570,5 @@ export interface EndorseDidTxAction extends DidOperationStateActionBase { } export type IndyVdrDidCreateResult = DidCreateResult + +export type NymRequestRole = 'STEWARD' | 'TRUSTEE' | 'ENDORSER' | 'NETWORK_MONITOR' diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts index 1783f13ee3..a3762dc96b 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts @@ -241,6 +241,7 @@ describe('IndyVdrIndyDidRegistrar', () => { verificationKey: expect.any(Key), alias: 'Hello', diddocContent: undefined, + role: 'STEWARD', }) expect(registerPublicDidSpy).toHaveBeenCalledWith(agentContext, poolMock, undefined) @@ -307,6 +308,7 @@ describe('IndyVdrIndyDidRegistrar', () => { verificationKey: expect.any(Key), alias: 'Hello', diddocContent: undefined, + role: 'STEWARD', }) expect(registerPublicDidSpy).toHaveBeenCalledWith( @@ -402,6 +404,7 @@ describe('IndyVdrIndyDidRegistrar', () => { namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', verificationKey: expect.any(Key), alias: 'Hello', + role: 'STEWARD', diddocContent: { '@context': [], authentication: [], @@ -583,6 +586,7 @@ describe('IndyVdrIndyDidRegistrar', () => { verificationKey: expect.any(Key), alias: 'Hello', diddocContent: undefined, + role: 'STEWARD', }) expect(registerPublicDidSpy).toHaveBeenCalledWith( diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index 95e8742dd6..526391871f 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -155,91 +155,17 @@ describe('Indy VDR Indy Did Registrar', () => { }) }) - test('can register a did:indy without services with endorser', async () => { - const didCreateTobeEndorsedResult = (await agent.dids.create({ + test('cannot create a did with TRUSTEE role', async () => { + const didRegistrationResult = await endorser.dids.create({ method: 'indy', options: { - endorserMode: 'external', endorserDid, - }, - })) as IndyVdrDidCreateResult - - const didState = didCreateTobeEndorsedResult.didState - if (didState.state !== 'action' || didState.action !== 'endorseIndyTransaction') throw Error('unexpected did state') - - const signedNymRequest = await endorser.modules.indyVdr.endorseTransaction( - didState.nymRequest, - didState.endorserDid - ) - const didCreateSubmitResult = await agent.dids.create({ - did: didState.did, - options: { - endorserMode: 'external', - endorsedTransaction: { - nymRequest: signedNymRequest, - }, - }, - secret: didState.secret, - }) - - expect(JsonTransformer.toJSON(didCreateSubmitResult)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: expect.stringMatching(didIndyRegex), - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: expect.stringMatching(didIndyRegex), - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: expect.stringMatching(didIndyRegex), - id: expect.stringContaining('#verkey'), - publicKeyBase58: expect.any(String), - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [expect.stringContaining('#verkey')], - service: undefined, - }, + endorserMode: 'internal', + role: 'TRUSTEE', }, }) - const did = didCreateSubmitResult.didState.did - if (!did) throw Error('did not defined') - - // Wait some time pass to let ledger settle the object - await sleep(1000) - - const didResolutionResult = await endorser.dids.resolve(did) - expect(JsonTransformer.toJSON(didResolutionResult)).toMatchObject({ - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: did, - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: did, - id: `${did}#verkey`, - publicKeyBase58: expect.any(String), - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [`${did}#verkey`], - service: undefined, - }, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, - }) + expect(JsonTransformer.toJSON(didRegistrationResult.didState.state)).toBe('failed') }) test('can register an endorsed did:indy without services - did and verkey specified', async () => { diff --git a/packages/node/src/transport/HttpInboundTransport.ts b/packages/node/src/transport/HttpInboundTransport.ts index 000d5e22ff..701effb385 100644 --- a/packages/node/src/transport/HttpInboundTransport.ts +++ b/packages/node/src/transport/HttpInboundTransport.ts @@ -24,18 +24,6 @@ export class HttpInboundTransport implements InboundTransport { this.app = app ?? express() this.path = path ?? '/' - this.app.use((req, res, next) => { - const contentType = req.headers['content-type'] - - if (!contentType || !supportedContentTypes.includes(contentType)) { - return res - .status(415) - .send('Unsupported content-type. Supported content-types are: ' + supportedContentTypes.join(', ')) - } - - return next() - }) - this.app.use(text({ type: supportedContentTypes, limit: '5mb' })) } @@ -48,6 +36,14 @@ export class HttpInboundTransport implements InboundTransport { }) this.app.post(this.path, async (req, res) => { + const contentType = req.headers['content-type'] + + if (!contentType || !supportedContentTypes.includes(contentType)) { + return res + .status(415) + .send('Unsupported content-type. Supported content-types are: ' + supportedContentTypes.join(', ')) + } + const session = new HttpTransportSession(utils.uuid(), req, res) try { const message = req.body diff --git a/packages/question-answer/src/QuestionAnswerApi.ts b/packages/question-answer/src/QuestionAnswerApi.ts index 97ea98c143..5c732a8b70 100644 --- a/packages/question-answer/src/QuestionAnswerApi.ts +++ b/packages/question-answer/src/QuestionAnswerApi.ts @@ -2,9 +2,9 @@ import type { QuestionAnswerRecord } from './repository' import type { Query } from '@aries-framework/core' import { + getOutboundMessageContext, AgentContext, ConnectionService, - OutboundMessageContext, injectable, MessageSender, } from '@aries-framework/core' @@ -65,10 +65,10 @@ export class QuestionAnswerApi { detail: config?.detail, } ) - const outboundMessageContext = new OutboundMessageContext(questionMessage, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: questionMessage, associatedRecord: questionAnswerRecord, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -94,10 +94,10 @@ export class QuestionAnswerApi { const connection = await this.connectionService.getById(this.agentContext, questionRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(answerMessage, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: answerMessage, associatedRecord: questionAnswerRecord, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) diff --git a/packages/tenants/tests/tenants-askar-profiles.e2e.test.ts b/packages/tenants/tests/tenants-askar-profiles.e2e.test.ts new file mode 100644 index 0000000000..cfee1c07d0 --- /dev/null +++ b/packages/tenants/tests/tenants-askar-profiles.e2e.test.ts @@ -0,0 +1,128 @@ +import type { InitConfig } from '@aries-framework/core' + +import { Agent } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' + +import { describeRunInNodeVersion } from '../../../tests/runInVersion' +import { AskarModule, AskarMultiWalletDatabaseScheme, AskarProfileWallet, AskarWallet } from '../../askar/src' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { testLogger } from '../../core/tests' + +import { TenantsModule } from '@aries-framework/tenants' + +// FIXME: Re-include in tests when Askar NodeJS wrapper performance is improved +describeRunInNodeVersion([18], 'Tenants Askar database schemes E2E', () => { + test('uses AskarWallet for all wallets and tenants when database schema is DatabasePerWallet', async () => { + const agentConfig: InitConfig = { + label: 'Tenant Agent 1', + walletConfig: { + id: 'Wallet: askar tenants without profiles e2e agent 1', + key: 'Wallet: askar tenants without profiles e2e agent 1', + }, + logger: testLogger, + } + + // Create multi-tenant agent + const agent = new Agent({ + config: agentConfig, + modules: { + tenants: new TenantsModule(), + askar: new AskarModule({ + ariesAskar: askarModuleConfig.ariesAskar, + // Database per wallet + multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.DatabasePerWallet, + }), + }, + dependencies: agentDependencies, + }) + + await agent.initialize() + + // main wallet should use AskarWallet + expect(agent.context.wallet).toBeInstanceOf(AskarWallet) + const mainWallet = agent.context.wallet as AskarWallet + + // Create tenant + const tenantRecord = await agent.modules.tenants.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + + // Get tenant agent + const tenantAgent = await agent.modules.tenants.getTenantAgent({ + tenantId: tenantRecord.id, + }) + + expect(tenantAgent.context.wallet).toBeInstanceOf(AskarWallet) + const tenantWallet = tenantAgent.context.wallet as AskarWallet + + // By default, profile is the same as the wallet id + expect(tenantWallet.profile).toEqual(`tenant-${tenantRecord.id}`) + // But the store should be different + expect(tenantWallet.store).not.toBe(mainWallet.store) + + // Insert and end + await tenantAgent.genericRecords.save({ content: { name: 'hello' }, id: 'hello' }) + await tenantAgent.endSession() + + const tenantAgent2 = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) + expect(await tenantAgent2.genericRecords.findById('hello')).not.toBeNull() + + await agent.wallet.delete() + await agent.shutdown() + }) + + test('uses AskarWallet for main agent, and ProfileAskarWallet for tenants', async () => { + const agentConfig: InitConfig = { + label: 'Tenant Agent 1', + walletConfig: { + id: 'Wallet: askar tenants with profiles e2e agent 1', + key: 'Wallet: askar tenants with profiles e2e agent 1', + }, + logger: testLogger, + } + + // Create multi-tenant agent + const agent = new Agent({ + config: agentConfig, + modules: { + tenants: new TenantsModule(), + askar: new AskarModule({ + ariesAskar: askarModuleConfig.ariesAskar, + // Profile per wallet + multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.ProfilePerWallet, + }), + }, + dependencies: agentDependencies, + }) + + await agent.initialize() + + // main wallet should use AskarWallet + expect(agent.context.wallet).toBeInstanceOf(AskarWallet) + const mainWallet = agent.context.wallet as AskarWallet + + // Create tenant + const tenantRecord = await agent.modules.tenants.createTenant({ + config: { + label: 'Tenant 1', + }, + }) + + // Get tenant agent + const tenantAgent = await agent.modules.tenants.getTenantAgent({ + tenantId: tenantRecord.id, + }) + + expect(tenantAgent.context.wallet).toBeInstanceOf(AskarProfileWallet) + const tenantWallet = tenantAgent.context.wallet as AskarProfileWallet + + expect(tenantWallet.profile).toEqual(`tenant-${tenantRecord.id}`) + // When using profile, the wallets should share the same store + expect(tenantWallet.store).toBe(mainWallet.store) + + await agent.wallet.delete() + await agent.shutdown() + }) +}) diff --git a/samples/extension-module/dummy/DummyApi.ts b/samples/extension-module/dummy/DummyApi.ts index 9d4aa765d3..b99ae9a3b6 100644 --- a/samples/extension-module/dummy/DummyApi.ts +++ b/samples/extension-module/dummy/DummyApi.ts @@ -2,7 +2,7 @@ import type { DummyRecord } from './repository/DummyRecord' import type { Query } from '@aries-framework/core' import { - OutboundMessageContext, + getOutboundMessageContext, AgentContext, ConnectionService, injectable, @@ -48,7 +48,11 @@ export class DummyApi { const { record, message } = await this.dummyService.createRequest(this.agentContext, connection) await this.messageSender.sendMessage( - new OutboundMessageContext(message, { agentContext: this.agentContext, connection }) + await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) ) await this.dummyService.updateState(this.agentContext, record, DummyState.RequestSent) @@ -69,7 +73,11 @@ export class DummyApi { const message = await this.dummyService.createResponse(this.agentContext, record) await this.messageSender.sendMessage( - new OutboundMessageContext(message, { agentContext: this.agentContext, connection, associatedRecord: record }) + await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) ) await this.dummyService.updateState(this.agentContext, record, DummyState.ResponseSent) diff --git a/samples/extension-module/dummy/handlers/DummyRequestHandler.ts b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts index b19394239f..320dd45184 100644 --- a/samples/extension-module/dummy/handlers/DummyRequestHandler.ts +++ b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts @@ -1,7 +1,7 @@ import type { DummyService } from '../services' import type { MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' -import { OutboundMessageContext } from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { DummyRequestMessage } from '../messages' @@ -14,11 +14,14 @@ export class DummyRequestHandler implements MessageHandler { } public async handle(inboundMessage: MessageHandlerInboundMessage) { - const connection = inboundMessage.assertReadyConnection() + const connectionRecord = inboundMessage.assertReadyConnection() const responseMessage = await this.dummyService.processRequest(inboundMessage) if (responseMessage) { - return new OutboundMessageContext(responseMessage, { agentContext: inboundMessage.agentContext, connection }) + return getOutboundMessageContext(inboundMessage.agentContext, { + connectionRecord, + message: responseMessage, + }) } } } diff --git a/samples/mediator.ts b/samples/mediator.ts index b76727fe4d..4213015af1 100644 --- a/samples/mediator.ts +++ b/samples/mediator.ts @@ -16,6 +16,7 @@ import type { InitConfig } from '@aries-framework/core' import type { Socket } from 'net' import express from 'express' +import * as indySdk from 'indy-sdk' import { Server } from 'ws' import { TestLogger } from '../packages/core/tests/logger' @@ -29,6 +30,7 @@ import { LogLevel, WsOutboundTransport, } from '@aries-framework/core' +import { IndySdkModule } from '@aries-framework/indy-sdk' import { HttpInboundTransport, agentDependencies, WsInboundTransport } from '@aries-framework/node' const port = process.env.AGENT_PORT ? Number(process.env.AGENT_PORT) : 3001 @@ -58,6 +60,7 @@ const agent = new Agent({ config: agentConfig, dependencies: agentDependencies, modules: { + indySdk: new IndySdkModule({ indySdk }), mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), diff --git a/tests/e2e-test.ts b/tests/e2e-test.ts index ffacd8be0a..b1958e1b8a 100644 --- a/tests/e2e-test.ts +++ b/tests/e2e-test.ts @@ -50,9 +50,6 @@ export async function e2eTest({ const [recipientSenderConnection, senderRecipientConnection] = await makeConnection(recipientAgent, senderAgent) expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) - // Create link secret with default options. This should create a default link secret. - await recipientAgent.modules.anoncreds.createLinkSecret() - // Issue credential from sender to recipient const { credentialDefinition } = await prepareForAnonCredsIssuance(senderAgent, { attributeNames: ['name', 'age', 'dateOfBirth'], diff --git a/yarn.lock b/yarn.lock index 970e218139..0449f185fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -799,6 +799,16 @@ "@cosmjs/math" "^0.29.5" "@cosmjs/utils" "^0.29.5" +"@cosmjs/amino@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.31.0.tgz#49b33047295002804ad51bdf7ec0c2c97f1b553d" + integrity sha512-xJ5CCEK7H79FTpOuEmlpSzVI+ZeYESTVvO3wHDgbnceIyAne3C68SvyaKqLUR4uJB0Z4q4+DZHbqW6itUiv4lA== + dependencies: + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + "@cosmjs/crypto@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.29.5.tgz#ab99fc382b93d8a8db075780cf07487a0f9519fd" @@ -812,6 +822,19 @@ elliptic "^6.5.4" libsodium-wrappers "^0.7.6" +"@cosmjs/crypto@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.31.0.tgz#0be3867ada0155da19c45a51f5fde08e84f9ec4b" + integrity sha512-UaqCe6Tgh0pe1QlZ66E13t6FlIF86QrnBXXq+EN7Xe1Rouza3fJ1ojGlPleJZkBoq3tAyYVIOOqdZIxtVj/sIQ== + dependencies: + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + "@noble/hashes" "^1" + bn.js "^5.2.0" + elliptic "^6.5.4" + libsodium-wrappers-sumo "^0.7.11" + "@cosmjs/encoding@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.29.5.tgz#009a4b1c596cdfd326f30ccfa79f5e56daa264f2" @@ -821,6 +844,15 @@ bech32 "^1.1.4" readonly-date "^1.0.0" +"@cosmjs/encoding@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.31.0.tgz#9a6fd80b59c35fc20638a6436128ad0be681eafc" + integrity sha512-NYGQDRxT7MIRSlcbAezwxK0FqnaSPKCH7O32cmfpHNWorFxhy9lwmBoCvoe59Kd0HmArI4h+NGzLEfX3OLnA4Q== + dependencies: + base64-js "^1.3.0" + bech32 "^1.1.4" + readonly-date "^1.0.0" + "@cosmjs/json-rpc@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.29.5.tgz#5e483a9bd98a6270f935adf0dfd8a1e7eb777fe4" @@ -836,6 +868,13 @@ dependencies: bn.js "^5.2.0" +"@cosmjs/math@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.31.0.tgz#c9fc5f8191df7c2375945d2eacce327dfbf26414" + integrity sha512-Sb/8Ry/+gKJaYiV6X8q45kxXC9FoV98XCY1WXtu0JQwOi61VCG2VXsURQnVvZ/EhR/CuT/swOlNKrqEs3da0fw== + dependencies: + bn.js "^5.2.0" + "@cosmjs/proto-signing@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.29.5.tgz#af3b62a46c2c2f1d2327d678b13b7262db1fe87c" @@ -849,6 +888,19 @@ cosmjs-types "^0.5.2" long "^4.0.0" +"@cosmjs/proto-signing@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.31.0.tgz#7056963457cd967f53f56c2ab4491638e5ade2c0" + integrity sha512-JNlyOJRkn8EKB9mCthkjr6lVX6eyVQ09PFdmB4/DR874E62dFTvQ+YvyKMAgN7K7Dcjj26dVlAD3f6Xs7YOGDg== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" + long "^4.0.0" + "@cosmjs/socket@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.29.5.tgz#a48df6b4c45dc6a6ef8e47232725dd4aa556ac2d" @@ -905,6 +957,11 @@ resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.29.5.tgz#3fed1b3528ae8c5f1eb5d29b68755bebfd3294ee" integrity sha512-m7h+RXDUxOzEOGt4P+3OVPX7PuakZT3GBmaM/Y2u+abN3xZkziykD/NvedYFvvCCdQo714XcGl33bwifS9FZPQ== +"@cosmjs/utils@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.31.0.tgz#3a7ac16856dcff63bbf1bb11e31f975f71ef4f21" + integrity sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -926,12 +983,12 @@ ky-universal "^0.8.2" "@digitalcredentials/jsonld-signatures@^9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld-signatures/-/jsonld-signatures-9.3.1.tgz#e00175ab4199c580c9b308effade021da805c695" - integrity sha512-YMh1e1GpTeHDqq2a2Kd+pLcHsMiPeKyE2Zs17NSwqckij7UMRVDQ54S5VQhHvoXZ1mlkpVaI2xtj5M5N6rzylw== + version "9.3.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld-signatures/-/jsonld-signatures-9.3.2.tgz#2c8141e7dfec2228b54ebd1f94d925df250351bb" + integrity sha512-auubZrr3D7et5O6zCdqoXsLhI8/F26HqneE94gIoZYVuxNHBNaFoDQ1Z71RfddRqwJonHkfkWgeZSzqjv6aUmg== dependencies: "@digitalbazaar/security-context" "^1.0.0" - "@digitalcredentials/jsonld" "^5.2.1" + "@digitalcredentials/jsonld" "^6.0.0" fast-text-encoding "^1.0.3" isomorphic-webcrypto "^2.3.8" serialize-error "^8.0.1" @@ -946,6 +1003,16 @@ canonicalize "^1.0.1" lru-cache "^6.0.0" +"@digitalcredentials/jsonld@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld/-/jsonld-6.0.0.tgz#05d34cb1d81c4bbdfacf61f8958bbaede33be598" + integrity sha512-5tTakj0/GsqAJi8beQFVMQ97wUJZnuxViW9xRuAATL6eOBIefGBwHkVryAgEq2I4J/xKgb/nEyw1ZXX0G8wQJQ== + dependencies: + "@digitalcredentials/http-client" "^1.0.0" + "@digitalcredentials/rdf-canonize" "^1.0.0" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + "@digitalcredentials/rdf-canonize@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@digitalcredentials/rdf-canonize/-/rdf-canonize-1.0.0.tgz#6297d512072004c2be7f280246383a9c4b0877ff" @@ -2842,9 +2909,9 @@ "@types/node" "*" "@types/ws@^8.5.4": - version "8.5.4" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" - integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== + version "8.5.5" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" + integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== dependencies: "@types/node" "*" @@ -4512,6 +4579,14 @@ cosmjs-types@^0.5.2: long "^4.0.0" protobufjs "~6.11.2" +cosmjs-types@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.8.0.tgz#2ed78f3e990f770229726f95f3ef5bf9e2b6859b" + integrity sha512-Q2Mj95Fl0PYMWEhA2LuGEIhipF7mQwd9gTQ85DdP9jjjopeoGaDxvmPa5nakNzsq7FnO1DMTatXTAx6bxMH7Lg== + dependencies: + long "^4.0.0" + protobufjs "~6.11.2" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -4529,6 +4604,13 @@ cross-fetch@^3.1.5: dependencies: node-fetch "2.6.7" +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -5496,9 +5578,9 @@ fast-text-encoding@^1.0.3: integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== fast-xml-parser@^4.0.12: - version "4.2.4" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.4.tgz#6e846ede1e56ad9e5ef07d8720809edf0ed07e9b" - integrity sha512-fbfMDvgBNIdDJLdLOwacjFAPYt67tr31H9ZhWSm45CDAxvd0I6WTlSOUo7K2P/K5sA5JgMKG64PI3DMcaFdWpQ== + version "4.2.6" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.6.tgz#30ad37b014c16e31eec0e01fbf90a85cedb4eacf" + integrity sha512-Xo1qV++h/Y3Ng8dphjahnYe+rGHaaNdsYOBWL9Y9GCPKpNKilJtilvWkLcI9f9X2DoKTLsZsGYAls5+JL5jfLA== dependencies: strnum "^1.0.5" @@ -7800,6 +7882,18 @@ libphonenumber-js@^1.10.14: resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.28.tgz#cae7e929cad96cee5ecc9449027192ecba39ee72" integrity sha512-1eAgjLrZA0+2Wgw4hs+4Q/kEBycxQo8ZLYnmOvZ3AlM8ImAVAJgDPlZtISLEzD1vunc2q8s2Pn7XwB7I8U3Kzw== +libsodium-sumo@^0.7.11: + version "0.7.11" + resolved "https://registry.yarnpkg.com/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz#ab0389e2424fca5c1dc8c4fd394906190da88a11" + integrity sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA== + +libsodium-wrappers-sumo@^0.7.11: + version "0.7.11" + resolved "https://registry.yarnpkg.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz#d96329ee3c0e7ec7f5fcf4cdde16cc3a1ae91d82" + integrity sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ== + dependencies: + libsodium-sumo "^0.7.11" + libsodium-wrappers@^0.7.6: version "0.7.11" resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.11.tgz#53bd20606dffcc54ea2122133c7da38218f575f7" @@ -8884,6 +8978,13 @@ node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.6.12" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" + integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.2.1, node-gyp-build@^4.3.0: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" @@ -9935,9 +10036,9 @@ proto-list@~1.2.1: integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== protobufjs@^6.8.8, protobufjs@~6.11.2, protobufjs@~6.11.3: - version "6.11.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" - integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== + version "6.11.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" + integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -9954,9 +10055,9 @@ protobufjs@^6.8.8, protobufjs@~6.11.2, protobufjs@~6.11.3: long "^4.0.0" protobufjs@^7.2.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.3.tgz#01af019e40d9c6133c49acbb3ff9e30f4f0f70b2" - integrity sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg== + version "7.2.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" + integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -10628,9 +10729,9 @@ scheduler@^0.23.0: loose-envify "^1.1.0" "semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@7.3.4: version "7.3.4" @@ -10647,16 +10748,16 @@ semver@7.3.8: lru-cache "^6.0.0" semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: - version "7.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" - integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== send@0.18.0: version "0.18.0" @@ -11547,9 +11648,9 @@ tsutils@^3.21.0: tslib "^1.8.1" tsyringe@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.7.0.tgz#aea0a9d565385deebb6def60cda342b15016f283" - integrity sha512-ncFDM1jTLsok4ejMvSW5jN1VGPQD48y2tfAR0pdptWRKYX4bkbqPt92k7KJ5RFJ1KV36JEs/+TMh7I6OUgj74g== + version "4.8.0" + resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.8.0.tgz#d599651b36793ba872870fee4f845bd484a5cac1" + integrity sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA== dependencies: tslib "^1.9.3" @@ -11953,11 +12054,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: defaults "^1.0.3" web-did-resolver@^2.0.21: - version "2.0.23" - resolved "https://registry.yarnpkg.com/web-did-resolver/-/web-did-resolver-2.0.23.tgz#59806a8bc6f5709403929a3d2b49c06279580632" - integrity sha512-7yOKnY9E322cVFfVkpV6g2j7QWB3H32aezGn2VagBmTAQr74zf0hxRN0p/PzK/kcgnc/oDCIRuiWUGwJEJAh0w== + version "2.0.27" + resolved "https://registry.yarnpkg.com/web-did-resolver/-/web-did-resolver-2.0.27.tgz#21884a41d64c2042c307acb2d6e2061244e09806" + integrity sha512-YxQlNdeYBXLhVpMW62+TPlc6sSOiWyBYq7DNvY6FXmXOD9g0zLeShpq2uCKFFQV/WlSrBi/yebK/W5lMTDxMUQ== dependencies: - cross-fetch "^3.1.5" + cross-fetch "^4.0.0" did-resolver "^4.0.0" webcrypto-core@^1.7.7: @@ -12051,9 +12152,9 @@ wide-align@^1.1.0, wide-align@^1.1.2, wide-align@^1.1.5: string-width "^1.0.2 || 2 || 3 || 4" word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wordwrap@^1.0.0: version "1.0.0"