From 87b39d5eb9a4014c47f11007d4b86e475b7354e0 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 12 Jan 2023 20:03:02 -0300 Subject: [PATCH 01/12] feat(askar): initial import (work in progress) Signed-off-by: Ariel Gentile --- packages/askar/README.md | 31 ++ packages/askar/jest.config.ts | 14 + packages/askar/package.json | 41 ++ packages/askar/src/AskarModule.ts | 21 + packages/askar/src/AskarModuleConfig.ts | 44 ++ packages/askar/src/index.ts | 9 + .../askar/src/storage/AskarStorageService.ts | 239 +++++++++ .../__tests__/AskarStorageService.test.ts | 298 +++++++++++ packages/askar/src/storage/index.ts | 1 + packages/askar/src/types.ts | 4 + packages/askar/src/utils/assertAskarWallet.ts | 13 + packages/askar/src/wallet/AskarWallet.ts | 466 ++++++++++++++++++ .../src/wallet/__tests__/AskarWallet.test.ts | 82 +++ packages/askar/src/wallet/index.ts | 1 + packages/askar/tsconfig.build.json | 7 + packages/askar/tsconfig.json | 6 + packages/core/src/storage/FileSystem.ts | 1 + packages/node/src/NodeFileSystem.ts | 4 + .../react-native/src/ReactNativeFileSystem.ts | 4 + yarn.lock | 108 +++- 20 files changed, 1393 insertions(+), 1 deletion(-) create mode 100644 packages/askar/README.md create mode 100644 packages/askar/jest.config.ts create mode 100644 packages/askar/package.json create mode 100644 packages/askar/src/AskarModule.ts create mode 100644 packages/askar/src/AskarModuleConfig.ts create mode 100644 packages/askar/src/index.ts create mode 100644 packages/askar/src/storage/AskarStorageService.ts create mode 100644 packages/askar/src/storage/__tests__/AskarStorageService.test.ts create mode 100644 packages/askar/src/storage/index.ts create mode 100644 packages/askar/src/types.ts create mode 100644 packages/askar/src/utils/assertAskarWallet.ts create mode 100644 packages/askar/src/wallet/AskarWallet.ts create mode 100644 packages/askar/src/wallet/__tests__/AskarWallet.test.ts create mode 100644 packages/askar/src/wallet/index.ts create mode 100644 packages/askar/tsconfig.build.json create mode 100644 packages/askar/tsconfig.json diff --git a/packages/askar/README.md b/packages/askar/README.md new file mode 100644 index 0000000000..5f68099a30 --- /dev/null +++ b/packages/askar/README.md @@ -0,0 +1,31 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript Askar Module

+

+ License + typescript + @aries-framework/askar version + +

+
+ +Askar module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). diff --git a/packages/askar/jest.config.ts b/packages/askar/jest.config.ts new file mode 100644 index 0000000000..c7c5196637 --- /dev/null +++ b/packages/askar/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + // setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/askar/package.json b/packages/askar/package.json new file mode 100644 index 0000000000..37bc74023d --- /dev/null +++ b/packages/askar/package.json @@ -0,0 +1,41 @@ +{ + "name": "@aries-framework/askar", + "main": "build/index", + "types": "build/index", + "version": "0.3.2", + "private": true, + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/askar", + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "packages/askar" + }, + "scripts": { + "build": "yarn run clean && yarn run compile", + "clean": "rimraf -rf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "yarn run build", + "test": "jest" + }, + "dependencies": { + "@aries-framework/core": "0.3.2", + "aries-askar-shared": "file:../../../aries-askar/wrappers/javascript/shared/aries-askar-shared-v0.0.1.tgz", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "rxjs": "^7.2.0", + "tsyringe": "^4.7.0" + }, + "devDependencies": { + "aries-askar-nodejs": "file:../../../aries-askar/wrappers/javascript/nodejs/aries-askar-nodejs-v0.0.1.tgz", + "@aries-framework/node": "0.3.2", + "rimraf": "~3.0.2", + "typescript": "~4.3.0" + } +} diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts new file mode 100644 index 0000000000..578871412a --- /dev/null +++ b/packages/askar/src/AskarModule.ts @@ -0,0 +1,21 @@ +import type { AskarModuleConfigOptions } from './AskarModuleConfig' +import type { DependencyManager, Module } from '@aries-framework/core' + +import { registerAriesAskar } from 'aries-askar-shared' + +import { AskarModuleConfig } from './AskarModuleConfig' +import { AskarSymbol } from './types' + +export class AskarModule implements Module { + public readonly config: AskarModuleConfig + + public constructor(config: AskarModuleConfigOptions) { + this.config = new AskarModuleConfig(config) + } + + public register(dependencyManager: DependencyManager) { + registerAriesAskar({ askar: this.config.askar }) + + dependencyManager.registerInstance(AskarSymbol, this.config.askar) + } +} diff --git a/packages/askar/src/AskarModuleConfig.ts b/packages/askar/src/AskarModuleConfig.ts new file mode 100644 index 0000000000..c2104eff8e --- /dev/null +++ b/packages/askar/src/AskarModuleConfig.ts @@ -0,0 +1,44 @@ +import type { AriesAskar } from './types' + +/** + * AskarModuleConfigOptions defines the interface for the options of the AskarModuleConfig class. + */ +export interface AskarModuleConfigOptions { + /** + * Implementation of the Askar interface according to aries-askar JavaScript wrapper. + * + * + * ## Node.JS + * + * ```ts + * import { NodeJSAriesAskar } from 'aries-askar-nodejs' + * + * const askarModule = new AskarModule({ + * askar: new NodeJSAriesAskar() + * }) + * ``` + * + * ## React Native + * + * ```ts + * import { ReactNativeAriesAskar } from 'aries-askar-react-native' + * + * const askarModule = new AskarModule({ + * askar: new ReactNativeAriesAskar() + * }) + * ``` + */ + askar: AriesAskar +} + +export class AskarModuleConfig { + private options: AskarModuleConfigOptions + + public constructor(options: AskarModuleConfigOptions) { + this.options = options + } + + public get askar() { + return this.options.askar + } +} diff --git a/packages/askar/src/index.ts b/packages/askar/src/index.ts new file mode 100644 index 0000000000..d7afa60eab --- /dev/null +++ b/packages/askar/src/index.ts @@ -0,0 +1,9 @@ +// Wallet +export { AskarWallet } from './wallet' + +// Storage +export { AskarStorageService } from './storage' + +// Module +export { AskarModule } from './AskarModule' +export { AskarModuleConfig } from './AskarModuleConfig' diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts new file mode 100644 index 0000000000..31cae6cfa8 --- /dev/null +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -0,0 +1,239 @@ +import type { AskarWallet } from '../wallet/AskarWallet' +import type { + BaseRecordConstructor, + AgentContext, + BaseRecord, + TagsBase, + Query, + StorageService, +} from '@aries-framework/core' +import type { EntryObject } from 'aries-askar-shared' + +import { WalletError, RecordNotFoundError, injectable, JsonTransformer } from '@aries-framework/core' +import { Scan } from 'aries-askar-shared' + +import { assertAskarWallet } from '../utils/assertAskarWallet' + +@injectable() +export class AskarStorageService implements StorageService { + private transformToRecordTagValues(tags: Record): TagsBase { + const transformedTags: TagsBase = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is a boolean string ('1' or '0') + // use the boolean val + if (value === '1' && key?.includes(':')) { + const [tagName, tagValue] = key.split(':') + + const transformedValue = transformedTags[tagName] + + if (Array.isArray(transformedValue)) { + transformedTags[tagName] = [...transformedValue, tagValue] + } else { + transformedTags[tagName] = [tagValue] + } + } + // Transform '1' and '0' to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = value === '1' + } + // If 1 or 0 is prefixed with 'n__' we need to remove it. This is to prevent + // casting the value to a boolean + else if (value === 'n__1' || value === 'n__0') { + transformedTags[key] = value === 'n__1' ? '1' : '0' + } + // Otherwise just use the value + else { + transformedTags[key] = value as string + } + } + + return transformedTags + } + + private transformFromRecordTagValues(tags: TagsBase): { [key: string]: string | undefined } { + const transformedTags: { [key: string]: string | undefined } = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is of type null we use the value undefined + // Indy doesn't support null as a value + if (value === null) { + transformedTags[key] = undefined + } + // If the value is a boolean use the indy + // '1' or '0' syntax + else if (typeof value === 'boolean') { + transformedTags[key] = value ? '1' : '0' + } + // If the value is 1 or 0, we need to add something to the value, otherwise + // the next time we deserialize the tag values it will be converted to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = `n__${value}` + } + // If the value is an array we create a tag for each array + // item ("tagName:arrayItem" = "1") + else if (Array.isArray(value)) { + value.forEach((item) => { + const tagName = `${key}:${item}` + transformedTags[tagName] = '1' + }) + } + // Otherwise just use the value + else { + transformedTags[key] = value + } + } + + return transformedTags + } + + /** + * Transforms the search query into a wallet query compatible with indy WQL. + * + * The format used by AFJ is almost the same as the indy query, with the exception of + * the encoding of values, however this is handled by the {@link IndyStorageService.transformToRecordTagValues} + * method. + */ + // TODO: Transform to Askar format + private indyQueryFromSearchQuery(query: Query): Record { + // eslint-disable-next-line prefer-const + let { $and, $or, $not, ...tags } = query + + $and = ($and as Query[] | undefined)?.map((q) => this.indyQueryFromSearchQuery(q)) + $or = ($or as Query[] | undefined)?.map((q) => this.indyQueryFromSearchQuery(q)) + $not = $not ? this.indyQueryFromSearchQuery($not as Query) : undefined + + const indyQuery = { + ...this.transformFromRecordTagValues(tags as unknown as TagsBase), + $and, + $or, + $not, + } + + return indyQuery + } + + private recordToInstance(record: EntryObject, recordClass: BaseRecordConstructor): T { + const instance = JsonTransformer.deserialize(record.value as string, recordClass) + instance.id = record.name + + const tags = record.tags ? this.transformToRecordTagValues(record.tags) : {} + instance.replaceTags(tags) + + return instance + } + + /** @inheritDoc */ + public async save(agentContext: AgentContext, record: T) { + assertAskarWallet(agentContext.wallet) + const session = (agentContext.wallet as AskarWallet).session + + const value = JsonTransformer.serialize(record) + const tags = this.transformFromRecordTagValues(record.getTags()) as Record + + try { + await session.insert({ category: record.type, name: record.id, value, tags }) + } catch (error) { + throw new WalletError('Error saving record', { cause: error }) + } + } + + /** @inheritDoc */ + public async update(agentContext: AgentContext, record: T): Promise { + assertAskarWallet(agentContext.wallet) + const session = (agentContext.wallet as AskarWallet).session + + const value = JsonTransformer.serialize(record) + const tags = this.transformFromRecordTagValues(record.getTags()) as Record + + try { + await session.replace({ category: record.type, name: record.id, value, tags }) + } catch (error) { + throw new WalletError('Error updating record', { cause: error }) + } + } + + /** @inheritDoc */ + public async delete(agentContext: AgentContext, record: T) { + assertAskarWallet(agentContext.wallet) + const session = (agentContext.wallet as AskarWallet).session + + try { + await session.remove({ category: record.type, name: record.id }) + } catch (error) { + throw new WalletError('Error deleting record', { cause: error }) + } + } + + /** @inheritDoc */ + public async deleteById( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + id: string + ): Promise { + assertAskarWallet(agentContext.wallet) + const session = (agentContext.wallet as AskarWallet).session + + try { + await session.remove({ category: recordClass.type, name: id }) + } catch (error) { + throw new WalletError('Error deleting record', { cause: error }) + } + } + + /** @inheritDoc */ + public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { + assertAskarWallet(agentContext.wallet) + const session = (agentContext.wallet as AskarWallet).session + + const record = await session.fetch({ category: recordClass.type, name: id }) + + if (!record) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + }) + } + + return this.recordToInstance(record, recordClass) + } + + /** @inheritDoc */ + public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { + assertAskarWallet(agentContext.wallet) + const session = (agentContext.wallet as AskarWallet).session + + const records = await session.fetchAll({ category: recordClass.type }) + + const instances = [] + for (const record of records) { + instances.push(this.recordToInstance(record, recordClass)) + } + return instances + } + + /** @inheritDoc */ + public async findByQuery( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + query: Query + ): Promise { + assertAskarWallet(agentContext.wallet) + const store = agentContext.wallet.handle + + const indyQuery = this.indyQueryFromSearchQuery(query) + + const scan = new Scan({ + category: recordClass.type, + store, + tagFilter: indyQuery, + }) + + const records = await scan.fetchAll() + + const instances = [] + for (const record of records) { + instances.push(this.recordToInstance(record, recordClass)) + } + return instances + } +} diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts new file mode 100644 index 0000000000..64684e21bc --- /dev/null +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -0,0 +1,298 @@ +import type { AgentContext, TagsBase } from '@aries-framework/core' + +import { SigningProviderRegistry, RecordDuplicateError, RecordNotFoundError } from '@aries-framework/core' +import { NodeJSAriesAskar } from 'aries-askar-nodejs' +import { registerAriesAskar } from 'aries-askar-shared' + +import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { AskarWallet } from '../../wallet/AskarWallet' +import { AskarStorageService } from '../AskarStorageService' + +describe('AskarStorageService', () => { + let wallet: AskarWallet + let storageService: AskarStorageService + let agentContext: AgentContext + + beforeEach(async () => { + const agentConfig = getAgentConfig('AskarStorageServiceTest') + registerAriesAskar({ askar: new NodeJSAriesAskar() }) + + wallet = new AskarWallet(agentConfig.logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + agentContext = getAgentContext({ + wallet, + agentConfig, + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(agentConfig.walletConfig!) + storageService = new AskarStorageService() + }) + + afterEach(async () => { + await wallet.delete() + }) + + const insertRecord = async ({ id, tags }: { id?: string; tags?: TagsBase }) => { + const props = { + id, + foo: 'bar', + tags: tags ?? { myTag: 'foobar' }, + } + const record = new TestRecord(props) + await storageService.save(agentContext, record) + return record + } + + describe.skip('tag transformation', () => { + it('should correctly transform tag values to string before storing', async () => { + const record = await insertRecord({ + id: 'test-id', + tags: { + someBoolean: true, + someOtherBoolean: false, + someStringValue: 'string', + anArrayValue: ['foo', 'bar'], + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: '1', + anotherStringNumberValue: '0', + }, + }) + + const retrieveRecord = await indy.getWalletRecord(wallet.handle, record.type, record.id, { + retrieveType: true, + retrieveTags: true, + }) + + expect(retrieveRecord.tags).toEqual({ + someBoolean: '1', + someOtherBoolean: '0', + someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', + }) + }) + + it('should correctly transform tag values from string after retrieving', async () => { + await indy.addWalletRecord(wallet.handle, TestRecord.type, 'some-id', '{}', { + someBoolean: '1', + someOtherBoolean: '0', + someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', + }) + + const record = await storageService.getById(agentContext, TestRecord, 'some-id') + + expect(record.getTags()).toEqual({ + someBoolean: true, + someOtherBoolean: false, + someStringValue: 'string', + anArrayValue: expect.arrayContaining(['bar', 'foo']), + someStringNumberValue: '1', + anotherStringNumberValue: '0', + }) + }) + }) + + describe('save()', () => { + it('should throw RecordDuplicateError if a record with the id already exists', async () => { + const record = await insertRecord({ id: 'test-id' }) + + return expect(() => storageService.save(agentContext, record)).rejects.toThrowError(RecordDuplicateError) + }) + + it('should save the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + const found = await storageService.getById(agentContext, TestRecord, 'test-id') + + expect(record).toEqual(found) + }) + }) + + describe('getById()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + return expect(() => storageService.getById(agentContext, TestRecord, 'does-not-exist')).rejects.toThrowError( + RecordNotFoundError + ) + }) + + it('should return the record by id', async () => { + const record = await insertRecord({ id: 'test-id' }) + const found = await storageService.getById(agentContext, TestRecord, 'test-id') + + expect(found).toEqual(record) + }) + }) + + describe('update()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + const record = new TestRecord({ + id: 'test-id', + foo: 'test', + tags: { some: 'tag' }, + }) + + return expect(() => storageService.update(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + }) + + it('should update the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + + record.replaceTags({ ...record.getTags(), foo: 'bar' }) + record.foo = 'foobaz' + await storageService.update(agentContext, record) + + const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) + expect(retrievedRecord).toEqual(record) + }) + }) + + describe('delete()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + const record = new TestRecord({ + id: 'test-id', + foo: 'test', + tags: { some: 'tag' }, + }) + + return expect(() => storageService.delete(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + }) + + it('should delete the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + await storageService.delete(agentContext, record) + + return expect(() => storageService.getById(agentContext, TestRecord, record.id)).rejects.toThrowError( + RecordNotFoundError + ) + }) + }) + + describe('getAll()', () => { + it('should retrieve all records', async () => { + const createdRecords = await Promise.all( + Array(5) + .fill(undefined) + .map((_, index) => insertRecord({ id: `record-${index}` })) + ) + + const records = await storageService.getAll(agentContext, TestRecord) + + expect(records).toEqual(expect.arrayContaining(createdRecords)) + }) + }) + + describe.skip('findByQuery()', () => { + it('should retrieve all records that match the query', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foobar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { myTag: 'foobar' }) + + expect(records.length).toBe(1) + expect(records[0]).toEqual(expectedRecord) + }) + + it('finds records using $and statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo', anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $and: [{ myTag: 'foo' }, { anotherTag: 'bar' }], + }) + + expect(records.length).toBe(1) + expect(records[0]).toEqual(expectedRecord) + }) + + it('finds records using $or statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) + const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $or: [{ myTag: 'foo' }, { anotherTag: 'bar' }], + }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('finds records using $not statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) + const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $not: { myTag: 'notfoobar' }, + }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('correctly transforms an advanced query into a valid WQL query', async () => { + const indySpy = jest.fn() + const storageServiceWithoutIndy = new AskarStorageService({ + openWalletSearch: indySpy, + fetchWalletSearchNextRecords: jest.fn(() => ({ records: undefined })), + closeWalletSearch: jest.fn(), + } as unknown as IndySdk) + + await storageServiceWithoutIndy.findByQuery(agentContext, TestRecord, { + $and: [ + { + $or: [{ myTag: true }, { myTag: false }], + }, + { + $and: [{ theNumber: '0' }, { theNumber: '1' }], + }, + ], + $or: [ + { + aValue: ['foo', 'bar'], + }, + ], + $not: { myTag: 'notfoobar' }, + }) + + const expectedQuery = { + $and: [ + { + $and: undefined, + $not: undefined, + $or: [ + { myTag: '1', $and: undefined, $or: undefined, $not: undefined }, + { myTag: '0', $and: undefined, $or: undefined, $not: undefined }, + ], + }, + { + $or: undefined, + $not: undefined, + $and: [ + { theNumber: 'n__0', $and: undefined, $or: undefined, $not: undefined }, + { theNumber: 'n__1', $and: undefined, $or: undefined, $not: undefined }, + ], + }, + ], + $or: [ + { + 'aValue:foo': '1', + 'aValue:bar': '1', + $and: undefined, + $or: undefined, + $not: undefined, + }, + ], + $not: { myTag: 'notfoobar', $and: undefined, $or: undefined, $not: undefined }, + } + + expect(indySpy).toBeCalledWith(expect.anything(), expect.anything(), expectedQuery, expect.anything()) + }) + }) +}) diff --git a/packages/askar/src/storage/index.ts b/packages/askar/src/storage/index.ts new file mode 100644 index 0000000000..ac0265f1ea --- /dev/null +++ b/packages/askar/src/storage/index.ts @@ -0,0 +1 @@ +export * from './AskarStorageService' diff --git a/packages/askar/src/types.ts b/packages/askar/src/types.ts new file mode 100644 index 0000000000..f7ff949df9 --- /dev/null +++ b/packages/askar/src/types.ts @@ -0,0 +1,4 @@ +import type { AriesAskar } from 'aries-askar-shared' + +export const AskarSymbol = Symbol('Askar') +export type { AriesAskar } diff --git a/packages/askar/src/utils/assertAskarWallet.ts b/packages/askar/src/utils/assertAskarWallet.ts new file mode 100644 index 0000000000..37213e3d28 --- /dev/null +++ b/packages/askar/src/utils/assertAskarWallet.ts @@ -0,0 +1,13 @@ +import type { Wallet } from '@aries-framework/core' + +import { AriesFrameworkError } from '@aries-framework/core' + +import { AskarWallet } from '../wallet/AskarWallet' + +export function assertAskarWallet(wallet: Wallet): asserts wallet is AskarWallet { + if (!(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}`) + } +} diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts new file mode 100644 index 0000000000..32861f01a2 --- /dev/null +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -0,0 +1,466 @@ +import type { + EncryptedMessage, + WalletConfig, + WalletCreateKeyOptions, + DidConfig, + DidInfo, + WalletSignOptions, + UnpackedMessageContext, + WalletVerifyOptions, + Wallet, + WalletExportImportConfig, + WalletConfigRekey, +} from '@aries-framework/core' +import type { Session } from 'aries-askar-shared' + +import { + Buffer, + KeyDerivationMethod, + AriesFrameworkError, + Logger, + WalletError, + InjectionSymbols, + Key, + SigningProviderRegistry, + TypedArrayEncoder, + FileSystem, +} from '@aries-framework/core' +import { CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-shared' + +const isError = (error: unknown): error is Error => error instanceof Error + +import { inject, injectable } from 'tsyringe' +import { TextDecoder, TextEncoder } from 'util' + +import { encodeToBase58 } from '../../../core/src/utils/base58' + +@injectable() +export class AskarWallet implements Wallet { + private walletConfig?: WalletConfig + private walletHandle?: Store + private _session?: Session + + private logger: Logger + private fileSystem: FileSystem + + private signingKeyProviderRegistry: SigningProviderRegistry + private publicDidInfo: DidInfo | undefined + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, + signingKeyProviderRegistry: SigningProviderRegistry + ) { + this.logger = logger + this.fileSystem = fileSystem + this.signingKeyProviderRegistry = signingKeyProviderRegistry + } + + public get isProvisioned() { + return this.walletConfig !== undefined + } + + public get isInitialized() { + return this.walletHandle !== undefined + } + + public get publicDid() { + return this.publicDidInfo + } + + public get handle() { + if (!this.walletHandle) { + throw new AriesFrameworkError( + 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' + ) + } + + return this.walletHandle + } + + public get session() { + if (!this._session) { + throw new AriesFrameworkError('No Wallet Session is opened') + } + + return this._session + } + + public get masterSecretId() { + if (!this.isInitialized || !(this.walletConfig?.id || this.walletConfig?.masterSecretId)) { + throw new AriesFrameworkError( + 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' + ) + } + + return this.walletConfig?.masterSecretId ?? this.walletConfig.id + } + + /** + * Dispose method is called when an agent context is disposed. + */ + public async dispose() { + if (this.isInitialized) { + await this.close() + } + } + + /** + * @throws {WalletDuplicateError} if the wallet already exists + * @throws {WalletError} if another error occurs + */ + public async create(walletConfig: WalletConfig): Promise { + await this.createAndOpen(walletConfig) + await this.close() + } + + private async askarWalletConfig(walletConfig: WalletConfig) { + const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod?: KeyDerivationMethod) => { + if (!keyDerivationMethod) { + return undefined + } + + const correspondanceTable = { + [KeyDerivationMethod.Raw]: StoreKeyMethod.Raw, + [KeyDerivationMethod.Argon2IInt]: `${StoreKeyMethod.Kdf}:argon2i:int`, + [KeyDerivationMethod.Argon2IMod]: `${StoreKeyMethod.Kdf}:argon2i:mod`, + } + + return correspondanceTable[keyDerivationMethod] as StoreKeyMethod + } + + const uri = await this.getUri(walletConfig) + + return { + uri, + profile: walletConfig.id, + keyMethod: keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod), + passKey: walletConfig.key, + } + } + + private async getUri(walletConfig: WalletConfig) { + // By default use sqlite as database backend + let uri = '' + if (!walletConfig.storage) { + walletConfig.storage = { type: 'sqlite' } + } + + if (walletConfig.storage.type === 'sqlite') { + if (walletConfig.storage.inMemory) { + uri = 'sqlite://:memory:' + } else { + const path = `${(walletConfig.storage.path as string) ?? this.fileSystem.basePath + '/wallet'}/${ + walletConfig.id + }/sqlite.db` + + // Make sure path exists before creating the wallet + await this.fileSystem.createDirectory(path) + uri = `sqlite://${path}` + } + // TODO postgres + } else { + throw new WalletError(`Storage type not supported: ${walletConfig.storage.type}`) + } + + return uri + } + /** + * @throws {WalletDuplicateError} if the wallet already exists + * @throws {WalletError} if another error occurs + */ + public async createAndOpen(walletConfig: WalletConfig): Promise { + this.logger.debug(`Creating wallet '${walletConfig.id}`) + + const askarWalletConfig = await this.askarWalletConfig(walletConfig) + try { + this.walletHandle = await Store.provision({ + recreate: false, + uri: askarWalletConfig.uri, + profile: askarWalletConfig.profile, + keyMethod: askarWalletConfig.keyMethod, + passKey: askarWalletConfig.passKey, + }) + this.walletConfig = walletConfig + this._session = await this.walletHandle.openSession() + + // TODO: Master Secret creation (now part of IndyCredx/AnonCreds) + } catch (error) { + const errorMessage = `Error creating wallet '${walletConfig.id}'` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + + this.logger.debug(`Successfully created wallet '${walletConfig.id}'`) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async open(walletConfig: WalletConfig): Promise { + await this._open(walletConfig) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async rotateKey(walletConfig: WalletConfigRekey): Promise { + if (!walletConfig.rekey) { + throw new WalletError('Wallet rekey undefined!. Please specify the new wallet key') + } + await this._open( + { + id: walletConfig.id, + key: walletConfig.key, + keyDerivationMethod: walletConfig.keyDerivationMethod, + }, + walletConfig.rekey, + walletConfig.rekeyDerivationMethod + ) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + private async _open( + walletConfig: WalletConfig, + rekey?: string, + rekeyDerivation?: KeyDerivationMethod + ): Promise { + if (this.walletHandle) { + throw new WalletError( + 'Wallet instance already opened. Close the currently opened wallet before re-opening the wallet' + ) + } + + const askarWalletConfig = await this.askarWalletConfig(walletConfig) + + try { + this.walletHandle = await Store.open({ + uri: askarWalletConfig.uri, + keyMethod: askarWalletConfig.keyMethod, + passKey: askarWalletConfig.passKey, + }) + + this._session = await this.walletHandle.openSession() + + // TODO: key rotation + this.walletConfig = walletConfig + } catch (error) { + // TODO: Improve errors + throw new WalletError(error.message, { cause: error }) + } + + this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this.handle}'`) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async delete(): Promise { + 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 wallet '${this.walletConfig.id}'`) + + if (this.walletHandle) { + await this.close() + } + + try { + await Store.remove(await this.getUri(this.walletConfig)) + } catch (error) { + const errorMessage = `Error deleting wallet '${this.walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async export(exportConfig: WalletExportImportConfig) { + // TODO + throw new WalletError('Export not yet implemented') + } + + public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) { + // TODO + throw new WalletError('Import not yet implemented') + } + + /** + * @throws {WalletError} if the wallet is already closed or another error occurs + */ + public async close(): Promise { + this.logger.debug(`Closing wallet ${this.walletConfig?.id}`) + if (!this.walletHandle) { + throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no handle.') + } + + try { + await this._session?.close() + await this.walletHandle.close() + this.walletHandle = undefined + this.publicDidInfo = undefined + } catch (error) { + const errorMessage = `Error closing wallet': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async initPublicDid(didConfig: DidConfig) { + // Not implemented, as it does not work with legacy Ledger module + } + + /** + * Create a key with an optional seed and keyType. + * The keypair is also automatically stored in the wallet afterwards + * + * TODO: use signingKeyProviderRegistry to support algorithms not implemented in Askar + * + * @param seed string The seed for creating a key + * @param keyType KeyType the type of key that should be created + * + * @returns a Key instance with a publicKeyBase58 + * + * @throws {WalletError} When an unsupported keytype is requested + * @throws {WalletError} When the key could not be created + */ + public async createKey({ seed, keyType }: WalletCreateKeyOptions): Promise { + try { + const algorithm = keyAlgFromString(keyType) + + // Create key from seed + const key = AskarKey.fromSeed({ seed: new TextEncoder().encode(seed), algorithm }) + + // Store key + await this._session?.insertKey({ key, name: encodeToBase58(key.publicBytes) }) + + return Key.fromPublicKey(key.publicBytes, keyType) + } 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 creating key with key type '${keyType}': ${error.message}`, { cause: error }) + } + } + + /** + * sign a Buffer with an instance of a Key class + * + * TODO: use signingKeyProviderRegistry to support algorithms not implemented in Askar + * + * @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 { + try { + const keyEntry = await this._session?.fetchKey({ name: key.publicKeyBase58 }) + + if (!keyEntry) { + throw new WalletError('Key entry not found') + } + + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not suppirting signing of multiple messages`) + } + + const signed = keyEntry.key.signMessage({ message: data as Buffer }) + + return Buffer.from(signed) + } 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 signing data with verkey ${key.publicKeyBase58}`, { cause: error }) + } + } + + /** + * Verify the signature with the data and the used key + * + * TODO: use signingKeyProviderRegistry to support algorithms not implemented in Askar + * + * @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 { + try { + const keyEntry = await this._session?.fetchKey({ name: key.publicKeyBase58 }) + + if (!keyEntry) { + throw new WalletError('Key entry not found') + } + + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signature of multiple messages`) + } + + return keyEntry.key.verifySignature({ message: data as Buffer, signature }) + } 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 signing data with verkey ${key.publicKeyBase58}`, { cause: error }) + } + } + + public async pack( + payload: Record, + recipientKeys: string[], + senderVerkey?: string + ): Promise { + // TODO + throw new WalletError('Pack not yet implemented') + } + + public async unpack(messagePackage: EncryptedMessage): Promise { + // TODO + throw new WalletError('Unpack not yet implemented') + } + + public async generateNonce(): Promise { + try { + return new TextDecoder().decode(CryptoBox.randomNonce()) + } 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 }) + } + } +} diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts new file mode 100644 index 0000000000..be8bd7b12b --- /dev/null +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -0,0 +1,82 @@ +import type { WalletConfig } from '@aries-framework/core' + +import { KeyType, SigningProviderRegistry, TypedArrayEncoder, KeyDerivationMethod } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/core/tests/helpers' +import { NodeJSAriesAskar } from 'aries-askar-nodejs' +import { registerAriesAskar, Store } from 'aries-askar-shared' + +import testLogger from '../../../../core/tests/logger' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const walletConfig: WalletConfig = { + id: 'Wallet: askarWalletTest', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describe('askarWallet', () => { + let askarWallet: AskarWallet + + const seed = 'sample-seed' + const message = TypedArrayEncoder.fromString('sample-message') + + beforeEach(async () => { + registerAriesAskar({ askar: new NodeJSAriesAskar() }) + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('Get the Master Secret', () => { + expect(askarWallet.masterSecretId).toEqual('Wallet: askarWalletTest') + }) + + test('Get the wallet handle', () => { + expect(askarWallet.handle).toEqual(expect.any(Store)) + }) + + test('Generate Nonce', async () => { + await expect(askarWallet.generateNonce()).resolves.toEqual(expect.any(String)) + }) + + test('Create ed25519 keypair', async () => { + await expect( + askarWallet.createKey({ seed: '2103de41b4ae37e8e28586d84a342b67', keyType: KeyType.Ed25519 }) + ).resolves.toMatchObject({ + keyType: KeyType.Ed25519, + }) + }) + + test('Create x25519 keypair', async () => { + await expect(askarWallet.createKey({ seed, keyType: KeyType.X25519 })).resolves.toMatchObject({ + keyType: KeyType.X25519, + }) + }) + + test('Create a signature with a ed25519 keypair', async () => { + const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await askarWallet.sign({ + data: message, + key: ed25519Key, + }) + expect(signature.length).toStrictEqual(64) + }) + + test('Verify a signed message with a ed25519 publicKey', async () => { + const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await askarWallet.sign({ + data: message, + key: ed25519Key, + }) + await expect(askarWallet.verify({ key: ed25519Key, data: message, signature })).resolves.toStrictEqual(true) + }) + + test('masterSecretId is equal to wallet ID by default', async () => { + expect(askarWallet.masterSecretId).toEqual(walletConfig.id) + }) +}) diff --git a/packages/askar/src/wallet/index.ts b/packages/askar/src/wallet/index.ts new file mode 100644 index 0000000000..ce420d6138 --- /dev/null +++ b/packages/askar/src/wallet/index.ts @@ -0,0 +1 @@ +export { AskarWallet } from './AskarWallet' diff --git a/packages/askar/tsconfig.build.json b/packages/askar/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/askar/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/askar/tsconfig.json b/packages/askar/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/askar/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/core/src/storage/FileSystem.ts b/packages/core/src/storage/FileSystem.ts index 6673bc333c..b724e68158 100644 --- a/packages/core/src/storage/FileSystem.ts +++ b/packages/core/src/storage/FileSystem.ts @@ -2,6 +2,7 @@ export interface FileSystem { readonly basePath: string exists(path: string): Promise + createDirectory(path: string): Promise write(path: string, data: string): Promise read(path: string): Promise downloadToFile(url: string, path: string): Promise diff --git a/packages/node/src/NodeFileSystem.ts b/packages/node/src/NodeFileSystem.ts index f739c40814..240440d64c 100644 --- a/packages/node/src/NodeFileSystem.ts +++ b/packages/node/src/NodeFileSystem.ts @@ -29,6 +29,10 @@ export class NodeFileSystem implements FileSystem { } } + public async createDirectory(path: string): Promise { + await promises.mkdir(dirname(path), { recursive: true }) + } + public async write(path: string, data: string): Promise { // Make sure parent directories exist await promises.mkdir(dirname(path), { recursive: true }) diff --git a/packages/react-native/src/ReactNativeFileSystem.ts b/packages/react-native/src/ReactNativeFileSystem.ts index 331fa11a54..0eaab55429 100644 --- a/packages/react-native/src/ReactNativeFileSystem.ts +++ b/packages/react-native/src/ReactNativeFileSystem.ts @@ -21,6 +21,10 @@ export class ReactNativeFileSystem implements FileSystem { return RNFS.exists(path) } + public async createDirectory(path: string): Promise { + await RNFS.mkdir(getDirFromFilePath(path)) + } + public async write(path: string, data: string): Promise { // Make sure parent directories exist await RNFS.mkdir(getDirFromFilePath(path)) diff --git a/yarn.lock b/yarn.lock index eb7826abc9..0373b0d818 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2990,6 +2990,28 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +"aries-askar-nodejs@file:../aries-askar/wrappers/javascript/nodejs/aries-askar-nodejs-v0.0.1.tgz": + version "0.0.1" + resolved "file:../aries-askar/wrappers/javascript/nodejs/aries-askar-nodejs-v0.0.1.tgz#8fb197be27602f4054f5d03aa419acbc16a707e5" + dependencies: + aries-askar-shared "*" + ffi-napi "^4.0.3" + node-cache "^5.1.2" + ref-array-di "^1.2.2" + ref-napi "^3.0.3" + ref-struct-di "^1.1.1" + +aries-askar-shared@*: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/aries-askar-shared/-/aries-askar-shared-0.0.1-security.tgz#6a77eb60274014998b16f7f4754612c28ad7489a" + integrity sha512-VmZYUXUtJQs2dU9n7yzFjExla6KV/cZejk2iYZ4cd6hdgZ6dQtcGI/NUJnUVzmYaeOX7fHUyQQK20q54jUPZsw== + +"aries-askar-shared@file:../aries-askar/wrappers/javascript/shared/aries-askar-shared-v0.0.1.tgz": + version "0.0.1" + resolved "file:../aries-askar/wrappers/javascript/shared/aries-askar-shared-v0.0.1.tgz#b3020fd627d015629d9788926a6c28a593bf343a" + dependencies: + fast-text-encoding "^1.0.3" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -3046,6 +3068,14 @@ array-includes@^3.1.4: get-intrinsic "^1.1.3" is-string "^1.0.7" +array-index@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" + integrity sha512-jesyNbBkLQgGZMSwA1FanaFjalb1mZUGxGeUEkSDidzgrbjBGhvizJkaItdhkt8eIHFOJC7nDsrXk+BaehTdRw== + dependencies: + debug "^2.2.0" + es6-symbol "^3.0.2" + array-map@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.1.tgz#d1bf3cc8813a7daaa335e5c8eb21d9d06230c1a7" @@ -3821,6 +3851,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone@2.x: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -4271,6 +4306,14 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -4705,6 +4748,32 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.0.2, es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -5056,6 +5125,13 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -8129,6 +8205,11 @@ neon-cli@0.8.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -8144,6 +8225,13 @@ node-addon-api@^3.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-dir@^0.1.17: version "0.1.17" resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" @@ -9431,6 +9519,14 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== +ref-array-di@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ref-array-di/-/ref-array-di-1.2.2.tgz#ceee9d667d9c424b5a91bb813457cc916fb1f64d" + integrity sha512-jhCmhqWa7kvCVrWhR/d7RemkppqPUdxEil1CtTtm7FkZV8LcHHCK3Or9GinUiFP5WY3k0djUkMvhBhx49Jb2iA== + dependencies: + array-index "^1.0.0" + debug "^3.1.0" + "ref-napi@^2.0.1 || ^3.0.2", ref-napi@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/ref-napi/-/ref-napi-3.0.3.tgz#e259bfc2bbafb3e169e8cd9ba49037dd00396b22" @@ -9441,7 +9537,7 @@ reduce-flatten@^2.0.0: node-addon-api "^3.0.0" node-gyp-build "^4.2.1" -ref-struct-di@^1.1.0: +ref-struct-di@^1.1.0, ref-struct-di@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ref-struct-di/-/ref-struct-di-1.1.1.tgz#5827b1d3b32372058f177547093db1fe1602dc10" integrity sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g== @@ -10768,6 +10864,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" From ae35f4b8c43d33ed758f5e174f2dce7536e9a616 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 14 Jan 2023 10:11:13 -0300 Subject: [PATCH 02/12] refactor: using test packages Signed-off-by: Ariel Gentile --- packages/askar/package.json | 4 ++-- .../askar/src/storage/AskarStorageService.ts | 4 ++-- .../__tests__/AskarStorageService.test.ts | 4 ++-- packages/askar/src/wallet/AskarWallet.ts | 4 ++-- .../src/wallet/__tests__/AskarWallet.test.ts | 6 +++--- yarn.lock | 17 +++++++---------- 6 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/askar/package.json b/packages/askar/package.json index 37bc74023d..655b9196ea 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -26,14 +26,14 @@ }, "dependencies": { "@aries-framework/core": "0.3.2", - "aries-askar-shared": "file:../../../aries-askar/wrappers/javascript/shared/aries-askar-shared-v0.0.1.tgz", + "aries-askar-test-shared": "^0.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { - "aries-askar-nodejs": "file:../../../aries-askar/wrappers/javascript/nodejs/aries-askar-nodejs-v0.0.1.tgz", + "aries-askar-test-nodejs": "^0.0.1", "@aries-framework/node": "0.3.2", "rimraf": "~3.0.2", "typescript": "~4.3.0" diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 31cae6cfa8..18fb71e36e 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -7,10 +7,10 @@ import type { Query, StorageService, } from '@aries-framework/core' -import type { EntryObject } from 'aries-askar-shared' +import type { EntryObject } from 'aries-askar-test-shared' import { WalletError, RecordNotFoundError, injectable, JsonTransformer } from '@aries-framework/core' -import { Scan } from 'aries-askar-shared' +import { Scan } from 'aries-askar-test-shared' import { assertAskarWallet } from '../utils/assertAskarWallet' diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index 64684e21bc..db6b6c5fe3 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -1,8 +1,8 @@ import type { AgentContext, TagsBase } from '@aries-framework/core' import { SigningProviderRegistry, RecordDuplicateError, RecordNotFoundError } from '@aries-framework/core' -import { NodeJSAriesAskar } from 'aries-askar-nodejs' -import { registerAriesAskar } from 'aries-askar-shared' +import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' +import { registerAriesAskar } from 'aries-askar-test-shared' import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 32861f01a2..2fd0c4ef31 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -11,7 +11,7 @@ import type { WalletExportImportConfig, WalletConfigRekey, } from '@aries-framework/core' -import type { Session } from 'aries-askar-shared' +import type { Session } from 'aries-askar-test-shared' import { Buffer, @@ -25,7 +25,7 @@ import { TypedArrayEncoder, FileSystem, } from '@aries-framework/core' -import { CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-shared' +import { CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' const isError = (error: unknown): error is Error => error instanceof Error diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index be8bd7b12b..3c0b39b381 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -1,10 +1,10 @@ import type { WalletConfig } from '@aries-framework/core' import { KeyType, SigningProviderRegistry, TypedArrayEncoder, KeyDerivationMethod } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/core/tests/helpers' -import { NodeJSAriesAskar } from 'aries-askar-nodejs' -import { registerAriesAskar, Store } from 'aries-askar-shared' +import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' +import { registerAriesAskar, Store } from 'aries-askar-test-shared' +import { agentDependencies } from '../../../../core/tests/helpers' import testLogger from '../../../../core/tests/logger' import { AskarWallet } from '../AskarWallet' diff --git a/yarn.lock b/yarn.lock index 67e79d390a..af1b6b04f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3007,25 +3007,22 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -"aries-askar-nodejs@file:../aries-askar/wrappers/javascript/nodejs/aries-askar-nodejs-v0.0.1.tgz": +aries-askar-test-nodejs@^0.0.1: version "0.0.1" - resolved "file:../aries-askar/wrappers/javascript/nodejs/aries-askar-nodejs-v0.0.1.tgz#8fb197be27602f4054f5d03aa419acbc16a707e5" + resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.0.1.tgz#bddb73e2bc2bdb5ed12f839282879e059c768076" + integrity sha512-I0SBjisZiyFxXQtVVmQzYAKaXneFhVm5W/ZK8Hn44FFkI++IYTzT5/iT1g1H+Jsj8LSY+Boex+CL1o4B6qGfGw== dependencies: - aries-askar-shared "*" + aries-askar-test-shared "*" ffi-napi "^4.0.3" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-napi "^3.0.3" ref-struct-di "^1.1.1" -aries-askar-shared@*: - version "0.0.1-security" - resolved "https://registry.yarnpkg.com/aries-askar-shared/-/aries-askar-shared-0.0.1-security.tgz#6a77eb60274014998b16f7f4754612c28ad7489a" - integrity sha512-VmZYUXUtJQs2dU9n7yzFjExla6KV/cZejk2iYZ4cd6hdgZ6dQtcGI/NUJnUVzmYaeOX7fHUyQQK20q54jUPZsw== - -"aries-askar-shared@file:../aries-askar/wrappers/javascript/shared/aries-askar-shared-v0.0.1.tgz": +aries-askar-test-shared@*, aries-askar-test-shared@^0.0.1: version "0.0.1" - resolved "file:../aries-askar/wrappers/javascript/shared/aries-askar-shared-v0.0.1.tgz#b3020fd627d015629d9788926a6c28a593bf343a" + resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.0.1.tgz#3f7492139ef902aa1371cc532f887b5eea2dd445" + integrity sha512-EhQW2j94cV7l3fgwDxr9mdS7JTAtOrgF6Ze0vA+i2dypOJrWzQpMdmLQKWyE90vEXoEP0yxjpSTKh6qPniySsA== dependencies: fast-text-encoding "^1.0.3" From ed302c429863af11b69b2065ac6ea35737aafa82 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 14 Jan 2023 21:02:57 -0300 Subject: [PATCH 03/12] feat(askar): use published test npm packages Signed-off-by: Ariel Gentile --- packages/askar/package.json | 4 +- packages/askar/src/AskarModule.ts | 2 +- .../askar/src/storage/AskarStorageService.ts | 73 +++++++++++++++---- .../__tests__/AskarStorageService.test.ts | 50 ++++++++----- packages/askar/src/types.ts | 2 +- packages/askar/src/utils/askarError.ts | 16 ++++ packages/askar/src/wallet/AskarWallet.ts | 1 + yarn.lock | 34 ++++++--- 8 files changed, 136 insertions(+), 46 deletions(-) create mode 100644 packages/askar/src/utils/askarError.ts diff --git a/packages/askar/package.json b/packages/askar/package.json index 655b9196ea..24313172ec 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -26,14 +26,14 @@ }, "dependencies": { "@aries-framework/core": "0.3.2", - "aries-askar-test-shared": "^0.0.1", + "aries-askar-test-shared": "^0.1.0-dev.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { - "aries-askar-test-nodejs": "^0.0.1", + "aries-askar-test-nodejs": "^0.1.0-dev.2", "@aries-framework/node": "0.3.2", "rimraf": "~3.0.2", "typescript": "~4.3.0" diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts index 578871412a..e3c59c1f29 100644 --- a/packages/askar/src/AskarModule.ts +++ b/packages/askar/src/AskarModule.ts @@ -1,7 +1,7 @@ import type { AskarModuleConfigOptions } from './AskarModuleConfig' import type { DependencyManager, Module } from '@aries-framework/core' -import { registerAriesAskar } from 'aries-askar-shared' +import { registerAriesAskar } from 'aries-askar-test-shared' import { AskarModuleConfig } from './AskarModuleConfig' import { AskarSymbol } from './types' diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 18fb71e36e..993c1b7290 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -9,9 +9,16 @@ import type { } from '@aries-framework/core' import type { EntryObject } from 'aries-askar-test-shared' -import { WalletError, RecordNotFoundError, injectable, JsonTransformer } from '@aries-framework/core' +import { + RecordDuplicateError, + WalletError, + RecordNotFoundError, + injectable, + JsonTransformer, +} from '@aries-framework/core' import { Scan } from 'aries-askar-test-shared' +import { askarErrors, isAskarError } from '../utils/askarError' import { assertAskarWallet } from '../utils/assertAskarWallet' @injectable() @@ -95,13 +102,13 @@ export class AskarStorageService implements StorageService * method. */ // TODO: Transform to Askar format - private indyQueryFromSearchQuery(query: Query): Record { + private askarQueryFromSearchQuery(query: Query): Record { // eslint-disable-next-line prefer-const let { $and, $or, $not, ...tags } = query - $and = ($and as Query[] | undefined)?.map((q) => this.indyQueryFromSearchQuery(q)) - $or = ($or as Query[] | undefined)?.map((q) => this.indyQueryFromSearchQuery(q)) - $not = $not ? this.indyQueryFromSearchQuery($not as Query) : undefined + $and = ($and as Query[] | undefined)?.map((q) => this.askarQueryFromSearchQuery(q)) + $or = ($or as Query[] | undefined)?.map((q) => this.askarQueryFromSearchQuery(q)) + $not = $not ? this.askarQueryFromSearchQuery($not as Query) : undefined const indyQuery = { ...this.transformFromRecordTagValues(tags as unknown as TagsBase), @@ -134,6 +141,10 @@ export class AskarStorageService implements StorageService try { await session.insert({ category: record.type, name: record.id, value, tags }) } catch (error) { + if (isAskarError(error) && error.code === askarErrors.Duplicate) { + throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) + } + throw new WalletError('Error saving record', { cause: error }) } } @@ -149,6 +160,13 @@ export class AskarStorageService implements StorageService try { await session.replace({ category: record.type, name: record.id, value, tags }) } catch (error) { + if (isAskarError(error) && error.code === askarErrors.NotFound) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + cause: error, + }) + } + throw new WalletError('Error updating record', { cause: error }) } } @@ -161,6 +179,12 @@ export class AskarStorageService implements StorageService try { await session.remove({ category: record.type, name: record.id }) } catch (error) { + if (isAskarError(error) && error.code === askarErrors.NotFound) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + cause: error, + }) + } throw new WalletError('Error deleting record', { cause: error }) } } @@ -177,6 +201,12 @@ export class AskarStorageService implements StorageService try { await session.remove({ category: recordClass.type, name: id }) } catch (error) { + if (isAskarError(error) && error.code === askarErrors.NotFound) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + cause: error, + }) + } throw new WalletError('Error deleting record', { cause: error }) } } @@ -186,15 +216,28 @@ export class AskarStorageService implements StorageService assertAskarWallet(agentContext.wallet) const session = (agentContext.wallet as AskarWallet).session - const record = await session.fetch({ category: recordClass.type, name: id }) - - if (!record) { - throw new RecordNotFoundError(`record with id ${id} not found.`, { - recordType: recordClass.type, - }) + try { + const record = await session.fetch({ category: recordClass.type, name: id }) + if (!record) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + }) + } + return this.recordToInstance(record, recordClass) + } catch (error) { + if ( + isAskarError(error) && + (error.code === askarErrors.NotFound || + // FIXME: this is current output from askar wrapper but does not describe specifically a not found scenario + error.message === 'Received null pointer. The native library could not find the value.') + ) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + cause: error, + }) + } + throw new WalletError(`Error getting record`, { cause: error }) } - - return this.recordToInstance(record, recordClass) } /** @inheritDoc */ @@ -220,12 +263,12 @@ export class AskarStorageService implements StorageService assertAskarWallet(agentContext.wallet) const store = agentContext.wallet.handle - const indyQuery = this.indyQueryFromSearchQuery(query) + const askarQuery = this.askarQueryFromSearchQuery(query) const scan = new Scan({ category: recordClass.type, store, - tagFilter: indyQuery, + tagFilter: askarQuery, }) const records = await scan.fetchAll() diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index db6b6c5fe3..dfbc55205c 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -3,6 +3,7 @@ import type { AgentContext, TagsBase } from '@aries-framework/core' import { SigningProviderRegistry, RecordDuplicateError, RecordNotFoundError } from '@aries-framework/core' import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { registerAriesAskar } from 'aries-askar-test-shared' +import { TextEncoder } from 'util' import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' @@ -13,10 +14,11 @@ describe('AskarStorageService', () => { let wallet: AskarWallet let storageService: AskarStorageService let agentContext: AgentContext + const askar = new NodeJSAriesAskar() beforeEach(async () => { const agentConfig = getAgentConfig('AskarStorageServiceTest') - registerAriesAskar({ askar: new NodeJSAriesAskar() }) + registerAriesAskar({ askar }) wallet = new AskarWallet(agentConfig.logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) agentContext = getAgentContext({ @@ -43,7 +45,7 @@ describe('AskarStorageService', () => { return record } - describe.skip('tag transformation', () => { + describe('tag transformation', () => { it('should correctly transform tag values to string before storing', async () => { const record = await insertRecord({ id: 'test-id', @@ -58,12 +60,15 @@ describe('AskarStorageService', () => { }, }) - const retrieveRecord = await indy.getWalletRecord(wallet.handle, record.type, record.id, { - retrieveType: true, - retrieveTags: true, + const retrieveRecord = await askar.sessionFetch({ + category: record.type, + name: record.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sessionHandle: wallet.session.handle!, + forUpdate: false, }) - expect(retrieveRecord.tags).toEqual({ + expect(JSON.parse(retrieveRecord.getTags(0))).toEqual({ someBoolean: '1', someOtherBoolean: '0', someStringValue: 'string', @@ -75,15 +80,23 @@ describe('AskarStorageService', () => { }) it('should correctly transform tag values from string after retrieving', async () => { - await indy.addWalletRecord(wallet.handle, TestRecord.type, 'some-id', '{}', { - someBoolean: '1', - someOtherBoolean: '0', - someStringValue: 'string', - 'anArrayValue:foo': '1', - 'anArrayValue:bar': '1', - // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' - someStringNumberValue: 'n__1', - anotherStringNumberValue: 'n__0', + await askar.sessionUpdate({ + category: TestRecord.type, + name: 'some-id', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sessionHandle: wallet.session.handle!, + value: new Uint8Array(new TextEncoder().encode('{}')), + tags: { + someBoolean: '1', + someOtherBoolean: '0', + someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', + }, + operation: 0, // EntryOperation.Insert }) const record = await storageService.getById(agentContext, TestRecord, 'some-id') @@ -187,15 +200,16 @@ describe('AskarStorageService', () => { }) }) - describe.skip('findByQuery()', () => { + describe('findByQuery()', () => { it('should retrieve all records that match the query', async () => { const expectedRecord = await insertRecord({ tags: { myTag: 'foobar' } }) + const expectedRecord2 = await insertRecord({ tags: { myTag: 'foobar' } }) await insertRecord({ tags: { myTag: 'notfoobar' } }) const records = await storageService.findByQuery(agentContext, TestRecord, { myTag: 'foobar' }) - expect(records.length).toBe(1) - expect(records[0]).toEqual(expectedRecord) + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) }) it('finds records using $and statements', async () => { diff --git a/packages/askar/src/types.ts b/packages/askar/src/types.ts index f7ff949df9..d3e230adbf 100644 --- a/packages/askar/src/types.ts +++ b/packages/askar/src/types.ts @@ -1,4 +1,4 @@ -import type { AriesAskar } from 'aries-askar-shared' +import type { AriesAskar } from 'aries-askar-test-shared' export const AskarSymbol = Symbol('Askar') export type { AriesAskar } diff --git a/packages/askar/src/utils/askarError.ts b/packages/askar/src/utils/askarError.ts new file mode 100644 index 0000000000..7d946830f2 --- /dev/null +++ b/packages/askar/src/utils/askarError.ts @@ -0,0 +1,16 @@ +import { AriesAskarError } from 'aries-askar-test-shared' + +export enum askarErrors { + Success = 0, + Backend = 1, + Busy = 2, + Duplicate = 3, + Encryption = 4, + Input = 5, + NotFound = 6, + Unexpected = 7, + Unsupported = 8, + Custom = 100, +} + +export const isAskarError = (error: Error) => error instanceof AriesAskarError diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 2fd0c4ef31..9da90a2c43 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -25,6 +25,7 @@ import { TypedArrayEncoder, FileSystem, } from '@aries-framework/core' +// eslint-disable-next-line import/order import { CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' const isError = (error: unknown): error is Error => error instanceof Error diff --git a/yarn.lock b/yarn.lock index af1b6b04f8..33bcd83436 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1791,6 +1791,21 @@ semver "^7.3.5" tar "^6.1.11" +"@mapbox/node-pre-gyp@^1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" + integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@mattrglobal/bbs-signatures@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@mattrglobal/bbs-signatures/-/bbs-signatures-1.0.0.tgz#8ff272c6d201aadab7e08bd84dbfd6e0d48ba12d" @@ -3007,22 +3022,23 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aries-askar-test-nodejs@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.0.1.tgz#bddb73e2bc2bdb5ed12f839282879e059c768076" - integrity sha512-I0SBjisZiyFxXQtVVmQzYAKaXneFhVm5W/ZK8Hn44FFkI++IYTzT5/iT1g1H+Jsj8LSY+Boex+CL1o4B6qGfGw== +aries-askar-test-nodejs@^0.1.0-dev.2: + version "0.1.0-dev.2" + resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.2.tgz#c7d92de5cd3afa2522c772491ad3be2766e603ee" + integrity sha512-CDYMpNn3rIjZvb+LAGMmEnH9Sx7f2vws/zZWl6TW6yaMJ5QfZgVGgCLDRlpFBuX5nXpt+mDuR91N0ppD/nhSoA== dependencies: - aries-askar-test-shared "*" + "@mapbox/node-pre-gyp" "^1.0.10" + aries-askar-test-shared "0.1.0-dev.2" ffi-napi "^4.0.3" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-napi "^3.0.3" ref-struct-di "^1.1.1" -aries-askar-test-shared@*, aries-askar-test-shared@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.0.1.tgz#3f7492139ef902aa1371cc532f887b5eea2dd445" - integrity sha512-EhQW2j94cV7l3fgwDxr9mdS7JTAtOrgF6Ze0vA+i2dypOJrWzQpMdmLQKWyE90vEXoEP0yxjpSTKh6qPniySsA== +aries-askar-test-shared@0.1.0-dev.2, aries-askar-test-shared@^0.1.0-dev.2: + version "0.1.0-dev.2" + resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.2.tgz#052b5ef695609ad5c4dacafcb4e411651f97302e" + integrity sha512-P6lIHupmW42UaqUnjutDEJgbat8w/0nkndrwMYsDVk5Z4uzGSLKVnF0pwO72l8s6SH87V8wWnhNJ1DS2tBBGlA== dependencies: fast-text-encoding "^1.0.3" From 17b6a736469c5d3716f39fa32a53619a5f29d886 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 17 Jan 2023 21:35:38 -0300 Subject: [PATCH 04/12] feat(askar): first implementation of DIDComm V1 packing and E2E test with IndySDKWallet Signed-off-by: Ariel Gentile --- packages/askar/package.json | 4 +- .../askar/src/storage/AskarStorageService.ts | 19 +- .../__tests__/AskarStorageService.test.ts | 47 ++-- packages/askar/src/utils/askarKeyTypes.ts | 14 ++ packages/askar/src/wallet/AskarWallet.ts | 216 ++++++++++++++++-- packages/askar/src/wallet/JweEnvelope.ts | 57 +++++ .../src/wallet/__tests__/AskarWallet.test.ts | 36 +++ .../src/wallet/__tests__/packing.test.ts | 55 +++++ tests/e2e-wallet-subject.test.ts | 152 ++++++++++++ yarn.lock | 18 +- 10 files changed, 560 insertions(+), 58 deletions(-) create mode 100644 packages/askar/src/utils/askarKeyTypes.ts create mode 100644 packages/askar/src/wallet/JweEnvelope.ts create mode 100644 packages/askar/src/wallet/__tests__/packing.test.ts create mode 100644 tests/e2e-wallet-subject.test.ts diff --git a/packages/askar/package.json b/packages/askar/package.json index 24313172ec..088c8ce97c 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -26,14 +26,14 @@ }, "dependencies": { "@aries-framework/core": "0.3.2", - "aries-askar-test-shared": "^0.1.0-dev.2", + "aries-askar-test-shared": "^0.1.0-dev.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { - "aries-askar-test-nodejs": "^0.1.0-dev.2", + "aries-askar-test-nodejs": "^0.1.0-dev.3", "@aries-framework/node": "0.3.2", "rimraf": "~3.0.2", "typescript": "~4.3.0" diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 993c1b7290..0c71d580e8 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -271,12 +271,21 @@ export class AskarStorageService implements StorageService tagFilter: askarQuery, }) - const records = await scan.fetchAll() - const instances = [] - for (const record of records) { - instances.push(this.recordToInstance(record, recordClass)) + try { + const records = await scan.fetchAll() + for (const record of records) { + instances.push(this.recordToInstance(record, recordClass)) + } + return instances + } catch (error) { + if ( + isAskarError(error) && // FIXME: this is current output from askar wrapper but does not describe specifically a 0 length scenario + error.message === 'Received null pointer. The native library could not find the value.' + ) { + return instances + } + throw new WalletError(`Error executing query`, { cause: error }) } - return instances } } diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index dfbc55205c..18c3c2b564 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -239,6 +239,9 @@ describe('AskarStorageService', () => { it('finds records using $not statements', async () => { const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) + // FIXME: Seems to be an issue with Askar WQL implementation: + // it only takes into account records containing a myTag and returns only those different from the $not rule + // (i.e. expectedRecord2 will not be taken into account because it does not have any myTag value) const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) await insertRecord({ tags: { myTag: 'notfoobar' } }) @@ -251,29 +254,7 @@ describe('AskarStorageService', () => { }) it('correctly transforms an advanced query into a valid WQL query', async () => { - const indySpy = jest.fn() - const storageServiceWithoutIndy = new AskarStorageService({ - openWalletSearch: indySpy, - fetchWalletSearchNextRecords: jest.fn(() => ({ records: undefined })), - closeWalletSearch: jest.fn(), - } as unknown as IndySdk) - - await storageServiceWithoutIndy.findByQuery(agentContext, TestRecord, { - $and: [ - { - $or: [{ myTag: true }, { myTag: false }], - }, - { - $and: [{ theNumber: '0' }, { theNumber: '1' }], - }, - ], - $or: [ - { - aValue: ['foo', 'bar'], - }, - ], - $not: { myTag: 'notfoobar' }, - }) + const storageService = new AskarStorageService() const expectedQuery = { $and: [ @@ -306,7 +287,25 @@ describe('AskarStorageService', () => { $not: { myTag: 'notfoobar', $and: undefined, $or: undefined, $not: undefined }, } - expect(indySpy).toBeCalledWith(expect.anything(), expect.anything(), expectedQuery, expect.anything()) + expect( + //@ts-ignore + storageService.askarQueryFromSearchQuery({ + $and: [ + { + $or: [{ myTag: true }, { myTag: false }], + }, + { + $and: [{ theNumber: '0' }, { theNumber: '1' }], + }, + ], + $or: [ + { + aValue: ['foo', 'bar'], + }, + ], + $not: { myTag: 'notfoobar' }, + }) + ).toEqual(expectedQuery) }) }) }) diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts new file mode 100644 index 0000000000..8899d6f59d --- /dev/null +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -0,0 +1,14 @@ +import { KeyType } from '@aries-framework/core' +import { KeyAlgs } from 'aries-askar-test-shared' + +export function askarKeyType(keyType: KeyType) { + const table = { + [KeyType.Bls12381g1]: KeyAlgs.Bls12381G1, + [KeyType.Bls12381g1g2]: KeyAlgs.Bls12381G1, // TODO: Verify if it's valid + [KeyType.Bls12381g2]: KeyAlgs.Bls12381G2, + [KeyType.Ed25519]: KeyAlgs.Ed25519, + [KeyType.X25519]: KeyAlgs.X25519, + } + + return table[keyType] +} diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 9da90a2c43..9478248a33 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -14,6 +14,8 @@ import type { import type { Session } from 'aries-askar-test-shared' import { + JsonEncoder, + KeyType, Buffer, KeyDerivationMethod, AriesFrameworkError, @@ -24,16 +26,22 @@ import { SigningProviderRegistry, TypedArrayEncoder, FileSystem, + WalletNotFoundError, } from '@aries-framework/core' // eslint-disable-next-line import/order -import { CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' +import { KeyAlgs, CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' const isError = (error: unknown): error is Error => error instanceof Error import { inject, injectable } from 'tsyringe' import { TextDecoder, TextEncoder } from 'util' -import { encodeToBase58 } from '../../../core/src/utils/base58' +import { encodeToBase58, decodeFromBase58 } from '../../../core/src/utils/base58' +import { base64ToBase64URL } from '../../../core/src/utils/base64' +import { askarErrors, isAskarError } from '../utils/askarError' +import { askarKeyType } from '../utils/askarKeyTypes' + +import { JweEnvelope, JweRecipient } from './JweEnvelope' @injectable() export class AskarWallet implements Wallet { @@ -250,13 +258,25 @@ export class AskarWallet implements Wallet { passKey: askarWalletConfig.passKey, }) + if (rekey) { + await this.walletHandle.rekey({ passKey: rekey, keyMethod: StoreKeyMethod.Raw /* TODO */ }) + } this._session = await this.walletHandle.openSession() - // TODO: key rotation this.walletConfig = walletConfig } catch (error) { - // TODO: Improve errors - throw new WalletError(error.message, { cause: error }) + if (isAskarError(error) && error.code === askarErrors.NotFound) { + const errorMessage = `Wallet '${walletConfig.id}' not found` + this.logger.debug(errorMessage) + + throw new WalletNotFoundError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } + // TODO Access error + + throw new WalletError(`Error opening wallet ${walletConfig.id}`, { cause: error }) } this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this.handle}'`) @@ -350,7 +370,9 @@ export class AskarWallet implements Wallet { const algorithm = keyAlgFromString(keyType) // Create key from seed - const key = AskarKey.fromSeed({ seed: new TextEncoder().encode(seed), algorithm }) + const key = seed + ? AskarKey.fromSeed({ seed: new TextEncoder().encode(seed), algorithm }) + : AskarKey.generate(algorithm) // Store key await this._session?.insertKey({ key, name: encodeToBase58(key.publicBytes) }) @@ -413,37 +435,195 @@ export class AskarWallet implements Wallet { */ public async verify({ data, key, signature }: WalletVerifyOptions): Promise { try { - const keyEntry = await this._session?.fetchKey({ name: key.publicKeyBase58 }) - - if (!keyEntry) { - throw new WalletError('Key entry not found') - } + const askarKey = AskarKey.fromPublicBytes({ algorithm: askarKeyType(key.keyType), publicKey: key.publicKey }) if (!TypedArrayEncoder.isTypedArray(data)) { throw new WalletError(`Currently not supporting signature of multiple messages`) } - return keyEntry.key.verifySignature({ message: data as Buffer, signature }) + return askarKey.verifySignature({ message: data as Buffer, signature }) } 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 signing data with verkey ${key.publicKeyBase58}`, { cause: error }) + throw new WalletError( + `Error verifying signature of data signed with verkey ${key.publicKeyBase58}. ERROR ${error}`, + { + cause: error, + } + ) } } public async pack( payload: Record, recipientKeys: string[], - senderVerkey?: string + senderVerkey?: string // in base58 ): Promise { - // TODO - throw new WalletError('Pack not yet implemented') + const cek = AskarKey.generate(KeyAlgs.Chacha20C20P) + + const senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined + + const senderExchangeKey = senderKey ? senderKey.key.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined + + const recipients: JweRecipient[] = [] + + for (const recipientKey of recipientKeys) { + const targetExchangeKey = AskarKey.fromPublicBytes({ + publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, + algorithm: KeyAlgs.Ed25519, + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + if (senderVerkey && senderExchangeKey) { + const enc_sender = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: new TextEncoder().encode(senderVerkey), + }) + const nonce = CryptoBox.randomNonce() + const encryptedCek = CryptoBox.cryptoBox({ + recipientKey: targetExchangeKey, + senderKey: senderExchangeKey, + message: cek.secretBytes, + nonce, + }) + + recipients.push( + new JweRecipient({ + encrypted_key: encryptedCek, + header: { + kid: recipientKey, + sender: base64ToBase64URL(Buffer.from(enc_sender).toString('base64')), + iv: base64ToBase64URL(Buffer.from(nonce).toString('base64')), + }, + }) + ) + } else { + const encryptedCek = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: cek.secretBytes, + }) + recipients.push( + new JweRecipient({ + encrypted_key: encryptedCek, + header: { + kid: recipientKey, + }, + }) + ) + } + } + + const protectedJson = { + enc: 'xchacha20poly1305_ietf', + typ: 'JWM/1.0', + alg: senderVerkey ? 'Authcrypt' : 'Anoncrypt', + recipients, + } + + const { ciphertext, tag, nonce } = cek.aeadEncrypt({ + message: new TextEncoder().encode(JSON.stringify(payload)), + aad: new TextEncoder().encode(JsonEncoder.toBase64URL(protectedJson)), + }).parts + + return new JweEnvelope({ + ciphertext: base64ToBase64URL(Buffer.from(ciphertext).toString('base64')), + iv: base64ToBase64URL(Buffer.from(nonce).toString('base64')), + protected: JsonEncoder.toBase64URL(protectedJson), + tag: base64ToBase64URL(Buffer.from(tag).toString('base64')), + }) } public async unpack(messagePackage: EncryptedMessage): Promise { - // TODO - throw new WalletError('Unpack not yet implemented') + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + + const alg = protectedJson.alg + const isAuthcrypt = alg === 'Authcrypt' + + if (!isAuthcrypt && alg != 'Anoncrypt') { + 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 ? new Uint8Array(Buffer.from(recip.header.sender, 'base64')) : undefined + const iv = recip.header.iv ? new Uint8Array(Buffer.from(recip.header.iv, 'base64')) : 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: new Uint8Array(Buffer.from(recip.encrypted_key, 'base64')), + }) + } + + let payloadKey, senderKey, recipientKey + + for (const recipient of recipients) { + let recipientKeyEntry + try { + recipientKeyEntry = await this.session.fetchKey({ name: recipient.kid }) + } catch (error) { + // TODO: Currently Askar wrapper throws error when key is not found + // In this case we don't need to throw any error because we should + // try with other recipient keys + continue + } + if (recipientKeyEntry) { + const recip_x = recipientKeyEntry.key.convertkey({ algorithm: KeyAlgs.X25519 }) + recipientKey = recipient.kid + + if (recipient.sender && recipient.iv) { + senderKey = new TextDecoder().decode( + CryptoBox.sealOpen({ + recipientKey: recip_x, + ciphertext: recipient.sender, + }) + ) + const sender_x = AskarKey.fromPublicBytes({ + algorithm: KeyAlgs.Ed25519, + publicKey: decodeFromBase58(senderKey), + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + payloadKey = CryptoBox.open({ + recipientKey: recip_x, + senderKey: sender_x, + message: recipient.encrypted_key, + nonce: recipient.iv, + }) + } + break + } + } + if (!payloadKey) { + throw new WalletError('No corresponding recipient key found') + } + + if (!senderKey && isAuthcrypt) { + throw new WalletError('Sender public key not provided for Authcrypt') + } + + const cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) + const message = cek.aeadDecrypt({ + ciphertext: new Uint8Array(Buffer.from(messagePackage.ciphertext as any, 'base64')), + nonce: new Uint8Array(Buffer.from(messagePackage.iv as any, 'base64')), + tag: new Uint8Array(Buffer.from(messagePackage.tag as any, 'base64')), + aad: new TextEncoder().encode(messagePackage.protected), + }) + + return { + plaintextMessage: JsonEncoder.fromBuffer(message), + senderKey, + recipientKey, + } } public async generateNonce(): Promise { diff --git a/packages/askar/src/wallet/JweEnvelope.ts b/packages/askar/src/wallet/JweEnvelope.ts new file mode 100644 index 0000000000..227a3c9273 --- /dev/null +++ b/packages/askar/src/wallet/JweEnvelope.ts @@ -0,0 +1,57 @@ +import { JsonTransformer } from '@aries-framework/core' + +import { base64ToBase64URL } from '../../../core/src/utils/base64' + +export class JweRecipient { + public encrypted_key!: string + public header?: Record + + public constructor(options: { encrypted_key: Uint8Array; header?: Record }) { + if (options) { + this.encrypted_key = base64ToBase64URL(Buffer.from(options.encrypted_key).toString('base64')) + this.header = options.header + } + } +} + +export interface JweEnvelopeOptions { + protected: string + unprotected?: string + recipients?: JweRecipient[] + ciphertext: string + iv: string + tag: string + aad?: string + header?: string[] + encrypted_key?: string +} + +export class JweEnvelope { + public protected!: string + public unprotected?: string + public recipients?: JweRecipient[] + public ciphertext!: string + public iv!: string + public tag!: string + public aad?: string + public header?: string[] + public encrypted_key?: string + + public constructor(options: JweEnvelopeOptions) { + if (options) { + this.protected = options.protected + this.unprotected = options.unprotected + this.recipients = options.recipients + this.ciphertext = options.ciphertext + this.iv = options.iv + this.tag = options.tag + this.aad = options.aad + this.header = options.header + this.encrypted_key = options.encrypted_key + } + } + + public toJson() { + return JsonTransformer.toJSON(this) + } +} diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index 3c0b39b381..43f719e5e0 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -80,3 +80,39 @@ describe('askarWallet', () => { expect(askarWallet.masterSecretId).toEqual(walletConfig.id) }) }) + +describe('AskarWallet key rotation', () => { + let askarWallet: AskarWallet + + beforeEach(async () => { + registerAriesAskar({ askar: new NodeJSAriesAskar() }) + }) + + afterEach(async () => { + if (askarWallet) { + await askarWallet.delete() + } + }) + + // TODO: Open, Close (duplicate, non existant, invalid key, etc.) + + test('Rotate key', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + await askarWallet.createAndOpen({ ...walletConfig, id: 'keyRotation', key: initialKey }) + + await askarWallet.close() + + const newKey = Store.generateRawKey() + await askarWallet.rotateKey({ ...walletConfig, id: 'keyRotation', key: initialKey, rekey: newKey }) + + await askarWallet.close() + + await expect(askarWallet.open({ ...walletConfig, id: 'keyRotation', key: initialKey })).rejects.toThrowError() + + await askarWallet.open({ ...walletConfig, id: 'keyRotation', key: newKey }) + + await askarWallet.close() + }) +}) diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts new file mode 100644 index 0000000000..47b871bfe2 --- /dev/null +++ b/packages/askar/src/wallet/__tests__/packing.test.ts @@ -0,0 +1,55 @@ +import type { WalletConfig } from '@aries-framework/core' + +import { + JsonTransformer, + BasicMessage, + KeyType, + SigningProviderRegistry, + KeyDerivationMethod, +} from '@aries-framework/core' +import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' +import { registerAriesAskar } from 'aries-askar-test-shared' + +import { agentDependencies } from '../../../../core/tests/helpers' +import testLogger from '../../../../core/tests/logger' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const walletConfig: WalletConfig = { + id: 'Askar Wallet Packing', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describe('askarWallet packing', () => { + let askarWallet: AskarWallet + + beforeEach(async () => { + registerAriesAskar({ askar: new NodeJSAriesAskar() }) + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('DIDComm V1 packing and unpacking', async () => { + // Create both sender and recipient keys + const senderKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const recipientKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + + const message = new BasicMessage({ content: 'hello' }) + + const encryptedMessage = await askarWallet.pack( + message.toJSON(), + [recipientKey.publicKeyBase58], + senderKey.publicKeyBase58 + ) + + const plainTextMessage = await askarWallet.unpack(encryptedMessage) + + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) + }) +}) diff --git a/tests/e2e-wallet-subject.test.ts b/tests/e2e-wallet-subject.test.ts new file mode 100644 index 0000000000..57fb91d3bc --- /dev/null +++ b/tests/e2e-wallet-subject.test.ts @@ -0,0 +1,152 @@ +import type { SubjectMessage } from './transport/SubjectInboundTransport' + +// eslint-disable-next-line import/no-extraneous-dependencies +import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' +import { Subject } from 'rxjs' + +import { getAgentOptions, makeConnection, waitForBasicMessage } from '../packages/core/tests/helpers' + +import { SubjectInboundTransport } from './transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' + +import { AskarModule, AskarStorageService, AskarWallet } from '@aries-framework/askar' +import { Agent, DependencyManager, InjectionSymbols } from '@aries-framework/core' +import { IndySdkModule, IndySdkStorageService, IndySdkWallet } from '@aries-framework/indy-sdk' +import { agentDependencies } from '@aries-framework/node' + +describe('E2E Wallet Subject tests', () => { + let recipientAgent: Agent + let senderAgent: Agent + + afterEach(async () => { + if (recipientAgent) { + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + } + + if (senderAgent) { + await senderAgent.shutdown() + await senderAgent.wallet.delete() + } + }) + + test('Wallet Subject flow - Indy Sender / Askar Receiver ', async () => { + // Sender is an Agent using Indy SDK Wallet + const senderDependencyManager = new DependencyManager() + senderDependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) + senderDependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + senderAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Sender Indy', { endpoints: ['rxjs:sender'] }), + modules: { indySdk: new IndySdkModule({ indySdk: agentDependencies.indy }) }, + }, + senderDependencyManager + ) + + // Recipient is an Agent using Askar Wallet + const recipientDependencyManager = new DependencyManager() + recipientDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + recipientDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + recipientAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), + modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, + }, + recipientDependencyManager + ) + + await e2eWalletTest(senderAgent, recipientAgent) + }) + + test('Wallet Subject flow - Askar Sender / Askar Recipient ', async () => { + // Sender is an Agent using Indy SDK Wallet + const senderDependencyManager = new DependencyManager() + senderDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + senderDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + senderAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Sender Askar', { endpoints: ['rxjs:sender'] }), + modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, + }, + senderDependencyManager + ) + + // Recipient is an Agent using Askar Wallet + const recipientDependencyManager = new DependencyManager() + recipientDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + recipientDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + recipientAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), + modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, + }, + recipientDependencyManager + ) + + await e2eWalletTest(senderAgent, recipientAgent) + }) + + test('Wallet Subject flow - Indy Sender / Indy Recipient ', async () => { + // Sender is an Agent using Indy SDK Wallet + const senderDependencyManager = new DependencyManager() + senderDependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) + senderDependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + senderAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Sender Indy', { endpoints: ['rxjs:sender'] }), + modules: { indySdk: new IndySdkModule({ indySdk: agentDependencies.indy }) }, + }, + senderDependencyManager + ) + + // Recipient is an Agent using Indy Wallet + const recipientDependencyManager = new DependencyManager() + recipientDependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) + recipientDependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + recipientAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Recipient Indy', { endpoints: ['rxjs:recipient'] }), + modules: { indySdk: new IndySdkModule({ indySdk: agentDependencies.indy }) }, + }, + recipientDependencyManager + ) + + await e2eWalletTest(senderAgent, recipientAgent) + }) +}) + +export async function e2eWalletTest(senderAgent: Agent, recipientAgent: Agent) { + const recipientMessages = new Subject() + const senderMessages = new Subject() + + const subjectMap = { + 'rxjs:recipient': recipientMessages, + 'rxjs:sender': senderMessages, + } + + // Recipient Setup + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) + await recipientAgent.initialize() + + // Sender Setup + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) + await senderAgent.initialize() + + // Make connection between sender and recipient + const [recipientSenderConnection, senderRecipientConnection] = await makeConnection(recipientAgent, senderAgent) + expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) + + // Sender sends a basic message and Recipient waits for it + await senderAgent.basicMessages.sendMessage(senderRecipientConnection.id, 'Hello') + await waitForBasicMessage(recipientAgent, { + content: 'Hello', + }) + + // Recipient sends a basic message and Sender waits for it + await recipientAgent.basicMessages.sendMessage(recipientSenderConnection.id, 'How are you?') + await waitForBasicMessage(senderAgent, { + content: 'How are you?', + }) +} diff --git a/yarn.lock b/yarn.lock index 33bcd83436..f65cc35ffe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3022,23 +3022,23 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aries-askar-test-nodejs@^0.1.0-dev.2: - version "0.1.0-dev.2" - resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.2.tgz#c7d92de5cd3afa2522c772491ad3be2766e603ee" - integrity sha512-CDYMpNn3rIjZvb+LAGMmEnH9Sx7f2vws/zZWl6TW6yaMJ5QfZgVGgCLDRlpFBuX5nXpt+mDuR91N0ppD/nhSoA== +aries-askar-test-nodejs@^0.1.0-dev.3: + version "0.1.0-dev.3" + resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.3.tgz#e197b8579723e13f4af25824fe326884fc1f3fb8" + integrity sha512-FC+p6w8HgWtI+H9l8n78+2pJQRKDTnVoKnfNjOSg2Ns21wvMdgwGe1v/I9qiTpOMRaLGmAcoK8CcxjCs8zQPLg== dependencies: "@mapbox/node-pre-gyp" "^1.0.10" - aries-askar-test-shared "0.1.0-dev.2" + aries-askar-test-shared "0.1.0-dev.3" ffi-napi "^4.0.3" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-napi "^3.0.3" ref-struct-di "^1.1.1" -aries-askar-test-shared@0.1.0-dev.2, aries-askar-test-shared@^0.1.0-dev.2: - version "0.1.0-dev.2" - resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.2.tgz#052b5ef695609ad5c4dacafcb4e411651f97302e" - integrity sha512-P6lIHupmW42UaqUnjutDEJgbat8w/0nkndrwMYsDVk5Z4uzGSLKVnF0pwO72l8s6SH87V8wWnhNJ1DS2tBBGlA== +aries-askar-test-shared@0.1.0-dev.3, aries-askar-test-shared@^0.1.0-dev.3: + version "0.1.0-dev.3" + resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.3.tgz#ee8b2927735cd349031ec86bd68c09466258cdea" + integrity sha512-6CBjvLbKdlHPhDX21gghJGGCwGTMzUBsa/CDWfSDdITdkaBbdxCjlNQMhLkHUxd1SmgxF23GJGtGXyRb3fGHUA== dependencies: fast-text-encoding "^1.0.3" From 02167187899b2d77a361fa1567ff9771d78df610 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 18 Jan 2023 13:53:33 -0300 Subject: [PATCH 05/12] add error cases Signed-off-by: Ariel Gentile --- packages/askar/src/wallet/AskarWallet.ts | 28 +++++++-- .../src/wallet/__tests__/AskarWallet.test.ts | 61 ++++++++++++++++--- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 9478248a33..2c1c131efb 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -1,4 +1,4 @@ -import type { +import { EncryptedMessage, WalletConfig, WalletCreateKeyOptions, @@ -10,10 +10,12 @@ import type { Wallet, WalletExportImportConfig, WalletConfigRekey, + WalletInvalidKeyError, } from '@aries-framework/core' import type { Session } from 'aries-askar-test-shared' import { + WalletDuplicateError, JsonEncoder, KeyType, Buffer, @@ -28,7 +30,6 @@ import { FileSystem, WalletNotFoundError, } from '@aries-framework/core' -// eslint-disable-next-line import/order import { KeyAlgs, CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' const isError = (error: unknown): error is Error => error instanceof Error @@ -195,7 +196,19 @@ export class AskarWallet implements Wallet { // TODO: Master Secret creation (now part of IndyCredx/AnonCreds) } catch (error) { - const errorMessage = `Error creating wallet '${walletConfig.id}'` + // FIXME: Askar should throw a Duplicate error code, but is currently returning Encryption + // And if we provide the very same wallet key, it will open it without any error + if (isAskarError(error) && (error.code === askarErrors.Encryption || error.code === askarErrors.Duplicate)) { + const errorMessage = `Wallet '${walletConfig.id}' already exists` + this.logger.debug(errorMessage) + + throw new WalletDuplicateError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } + + const errorMessage = `Error creating wallet '${walletConfig.id}'. CODE ${error.code}` this.logger.error(errorMessage, { error, errorMessage: error.message, @@ -273,9 +286,14 @@ export class AskarWallet implements Wallet { walletType: 'AskarWallet', cause: error, }) + } else if (isAskarError(error) && error.code === askarErrors.Encryption) { + const errorMessage = `Incorrect key for wallet '${walletConfig.id}'` + this.logger.debug(errorMessage) + throw new WalletInvalidKeyError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) } - // TODO Access error - throw new WalletError(`Error opening wallet ${walletConfig.id}`, { cause: error }) } diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index 43f719e5e0..dc5d0a3c18 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -1,6 +1,14 @@ import type { WalletConfig } from '@aries-framework/core' -import { KeyType, SigningProviderRegistry, TypedArrayEncoder, KeyDerivationMethod } from '@aries-framework/core' +import { + WalletDuplicateError, + WalletNotFoundError, + WalletInvalidKeyError, + KeyType, + SigningProviderRegistry, + TypedArrayEncoder, + KeyDerivationMethod, +} from '@aries-framework/core' import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { registerAriesAskar, Store } from 'aries-askar-test-shared' @@ -16,7 +24,7 @@ const walletConfig: WalletConfig = { keyDerivationMethod: KeyDerivationMethod.Raw, } -describe('askarWallet', () => { +describe('AskarWallet basic operations', () => { let askarWallet: AskarWallet const seed = 'sample-seed' @@ -81,7 +89,7 @@ describe('askarWallet', () => { }) }) -describe('AskarWallet key rotation', () => { +describe('AskarWallet management', () => { let askarWallet: AskarWallet beforeEach(async () => { @@ -94,24 +102,61 @@ describe('AskarWallet key rotation', () => { } }) - // TODO: Open, Close (duplicate, non existant, invalid key, etc.) + test('Create', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + const anotherKey = Store.generateRawKey() + + // Create and open wallet + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: initialKey }) + + // Close and try to re-create it + await askarWallet.close() + await expect( + askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: anotherKey }) + ).rejects.toThrowError(WalletDuplicateError) + }) + + test('Open', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + const wrongKey = Store.generateRawKey() + + // Create and open wallet + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Open', key: initialKey }) + + // Close and try to re-opening it with a wrong key + await askarWallet.close() + await expect(askarWallet.open({ ...walletConfig, id: 'AskarWallet Open', key: wrongKey })).rejects.toThrowError( + WalletInvalidKeyError + ) + + // Try to open a non existent wallet + await expect( + askarWallet.open({ ...walletConfig, id: 'AskarWallet Open - Non existent', key: initialKey }) + ).rejects.toThrowError(WalletNotFoundError) + }) test('Rotate key', async () => { askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) const initialKey = Store.generateRawKey() - await askarWallet.createAndOpen({ ...walletConfig, id: 'keyRotation', key: initialKey }) + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) await askarWallet.close() const newKey = Store.generateRawKey() - await askarWallet.rotateKey({ ...walletConfig, id: 'keyRotation', key: initialKey, rekey: newKey }) + await askarWallet.rotateKey({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey, rekey: newKey }) await askarWallet.close() - await expect(askarWallet.open({ ...walletConfig, id: 'keyRotation', key: initialKey })).rejects.toThrowError() + await expect( + askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) + ).rejects.toThrowError(WalletInvalidKeyError) - await askarWallet.open({ ...walletConfig, id: 'keyRotation', key: newKey }) + await askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: newKey }) await askarWallet.close() }) From b8adc3b3f9104139811ca87f9c13e2efeb460815 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 19 Jan 2023 22:12:39 -0300 Subject: [PATCH 06/12] feat: add custom signing provider support Signed-off-by: Ariel Gentile --- packages/askar/package.json | 10 +- .../askar/src/storage/AskarStorageService.ts | 16 +- packages/askar/src/utils/askarKeyTypes.ts | 16 +- packages/askar/src/utils/askarWalletConfig.ts | 42 +++ packages/askar/src/utils/index.ts | 3 + packages/askar/src/wallet/AskarWallet.ts | 330 +++++++++++------- packages/askar/src/wallet/JweEnvelope.ts | 22 +- .../src/wallet/__tests__/AskarWallet.test.ts | 97 ++++- packages/core/src/utils/base64.ts | 6 + tests/e2e-wallet-subject.test.ts | 8 +- yarn.lock | 62 +--- 11 files changed, 396 insertions(+), 216 deletions(-) create mode 100644 packages/askar/src/utils/askarWalletConfig.ts create mode 100644 packages/askar/src/utils/index.ts diff --git a/packages/askar/package.json b/packages/askar/package.json index 088c8ce97c..5e834dc2de 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -19,23 +19,23 @@ }, "scripts": { "build": "yarn run clean && yarn run compile", - "clean": "rimraf -rf ./build", + "clean": "rimraf ./build", "compile": "tsc -p tsconfig.build.json", "prepublishOnly": "yarn run build", "test": "jest" }, "dependencies": { "@aries-framework/core": "0.3.2", - "aries-askar-test-shared": "^0.1.0-dev.3", + "aries-askar-test-shared": "^0.1.0-dev.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { - "aries-askar-test-nodejs": "^0.1.0-dev.3", + "aries-askar-test-nodejs": "^0.1.0-dev.4", "@aries-framework/node": "0.3.2", - "rimraf": "~3.0.2", - "typescript": "~4.3.0" + "rimraf": "^4.0.7", + "typescript": "~4.9.4" } } diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 0c71d580e8..9d6cbc8ae4 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -63,11 +63,11 @@ export class AskarStorageService implements StorageService for (const [key, value] of Object.entries(tags)) { // If the value is of type null we use the value undefined - // Indy doesn't support null as a value + // Askar doesn't support null as a value if (value === null) { transformedTags[key] = undefined } - // If the value is a boolean use the indy + // If the value is a boolean use the Askar // '1' or '0' syntax else if (typeof value === 'boolean') { transformedTags[key] = value ? '1' : '0' @@ -95,10 +95,10 @@ export class AskarStorageService implements StorageService } /** - * Transforms the search query into a wallet query compatible with indy WQL. + * Transforms the search query into a wallet query compatible with Askar WQL. * - * The format used by AFJ is almost the same as the indy query, with the exception of - * the encoding of values, however this is handled by the {@link IndyStorageService.transformToRecordTagValues} + * The format used by AFJ is almost the same as the WQL query, with the exception of + * the encoding of values, however this is handled by the {@link AskarStorageService.transformToRecordTagValues} * method. */ // TODO: Transform to Askar format @@ -110,14 +110,14 @@ export class AskarStorageService implements StorageService $or = ($or as Query[] | undefined)?.map((q) => this.askarQueryFromSearchQuery(q)) $not = $not ? this.askarQueryFromSearchQuery($not as Query) : undefined - const indyQuery = { + const askarQuery = { ...this.transformFromRecordTagValues(tags as unknown as TagsBase), $and, $or, $not, } - return indyQuery + return askarQuery } private recordToInstance(record: EntryObject, recordClass: BaseRecordConstructor): T { @@ -261,7 +261,7 @@ export class AskarStorageService implements StorageService query: Query ): Promise { assertAskarWallet(agentContext.wallet) - const store = agentContext.wallet.handle + const store = agentContext.wallet.store const askarQuery = this.askarQueryFromSearchQuery(query) diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts index 8899d6f59d..67f8c0ac8e 100644 --- a/packages/askar/src/utils/askarKeyTypes.ts +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -1,14 +1,6 @@ -import { KeyType } from '@aries-framework/core' -import { KeyAlgs } from 'aries-askar-test-shared' +import type { KeyType } from '@aries-framework/core' -export function askarKeyType(keyType: KeyType) { - const table = { - [KeyType.Bls12381g1]: KeyAlgs.Bls12381G1, - [KeyType.Bls12381g1g2]: KeyAlgs.Bls12381G1, // TODO: Verify if it's valid - [KeyType.Bls12381g2]: KeyAlgs.Bls12381G2, - [KeyType.Ed25519]: KeyAlgs.Ed25519, - [KeyType.X25519]: KeyAlgs.X25519, - } +import { KeyAlgs } from 'aries-askar-test-shared' - return table[keyType] -} +export const keyTypeSupportedByAskar = (keyType: KeyType) => + Object.entries(KeyAlgs).find(([, value]) => value === keyType.toString()) !== undefined diff --git a/packages/askar/src/utils/askarWalletConfig.ts b/packages/askar/src/utils/askarWalletConfig.ts new file mode 100644 index 0000000000..295066c60b --- /dev/null +++ b/packages/askar/src/utils/askarWalletConfig.ts @@ -0,0 +1,42 @@ +import type { WalletConfig } from '@aries-framework/core' + +import { KeyDerivationMethod, WalletError } from '@aries-framework/core' +import { StoreKeyMethod } from 'aries-askar-test-shared' + +export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod?: KeyDerivationMethod) => { + if (!keyDerivationMethod) { + return undefined + } + + const correspondanceTable = { + [KeyDerivationMethod.Raw]: StoreKeyMethod.Raw, + [KeyDerivationMethod.Argon2IInt]: `${StoreKeyMethod.Kdf}:argon2i:int`, + [KeyDerivationMethod.Argon2IMod]: `${StoreKeyMethod.Kdf}:argon2i:mod`, + } + + return correspondanceTable[keyDerivationMethod] as StoreKeyMethod +} + +export const uriFromWalletConfig = (walletConfig: WalletConfig, basePath: string): { uri: string; path?: string } => { + let uri = '' + let path + + // By default use sqlite as database backend + if (!walletConfig.storage) { + walletConfig.storage = { type: 'sqlite' } + } + + if (walletConfig.storage.type === 'sqlite') { + if (walletConfig.storage.inMemory) { + uri = 'sqlite://:memory:' + } else { + path = `${(walletConfig.storage.path as string) ?? basePath + '/wallet'}/${walletConfig.id}/sqlite.db` + uri = `sqlite://${path}` + } + } else { + // TODO posgres + throw new WalletError(`Storage type not supported: ${walletConfig.storage.type}`) + } + + return { uri, path } +} diff --git a/packages/askar/src/utils/index.ts b/packages/askar/src/utils/index.ts new file mode 100644 index 0000000000..b9f658de82 --- /dev/null +++ b/packages/askar/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './askarError' +export * from './askarKeyTypes' +export * from './askarWalletConfig' diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 2c1c131efb..977a608ea5 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -1,4 +1,4 @@ -import { +import type { EncryptedMessage, WalletConfig, WalletCreateKeyOptions, @@ -10,16 +10,20 @@ import { Wallet, WalletExportImportConfig, WalletConfigRekey, - WalletInvalidKeyError, + KeyPair, + KeyDerivationMethod, } from '@aries-framework/core' import type { Session } from 'aries-askar-test-shared' import { + JsonTransformer, + RecordNotFoundError, + RecordDuplicateError, + WalletInvalidKeyError, WalletDuplicateError, JsonEncoder, KeyType, Buffer, - KeyDerivationMethod, AriesFrameworkError, Logger, WalletError, @@ -30,7 +34,7 @@ import { FileSystem, WalletNotFoundError, } from '@aries-framework/core' -import { KeyAlgs, CryptoBox, Store, StoreKeyMethod, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' +import { StoreKeyMethod, KeyAlgs, CryptoBox, Store, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' const isError = (error: unknown): error is Error => error instanceof Error @@ -38,18 +42,24 @@ import { inject, injectable } from 'tsyringe' import { TextDecoder, TextEncoder } from 'util' import { encodeToBase58, decodeFromBase58 } from '../../../core/src/utils/base58' -import { base64ToBase64URL } from '../../../core/src/utils/base64' -import { askarErrors, isAskarError } from '../utils/askarError' -import { askarKeyType } from '../utils/askarKeyTypes' +import { uint8ArrayToBase64URL } from '../../../core/src/utils/base64' +import { + askarErrors, + isAskarError, + keyDerivationMethodToStoreKeyMethod, + keyTypeSupportedByAskar, + uriFromWalletConfig, +} from '../utils' import { JweEnvelope, JweRecipient } from './JweEnvelope' @injectable() export class AskarWallet implements Wallet { private walletConfig?: WalletConfig - private walletHandle?: Store private _session?: Session + private _store?: Store + private logger: Logger private fileSystem: FileSystem @@ -71,21 +81,21 @@ export class AskarWallet implements Wallet { } public get isInitialized() { - return this.walletHandle !== undefined + return this._store !== undefined } public get publicDid() { return this.publicDidInfo } - public get handle() { - if (!this.walletHandle) { + public get store() { + if (!this._store) { throw new AriesFrameworkError( 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' ) } - return this.walletHandle + return this._store } public get session() { @@ -124,57 +134,6 @@ export class AskarWallet implements Wallet { await this.close() } - private async askarWalletConfig(walletConfig: WalletConfig) { - const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod?: KeyDerivationMethod) => { - if (!keyDerivationMethod) { - return undefined - } - - const correspondanceTable = { - [KeyDerivationMethod.Raw]: StoreKeyMethod.Raw, - [KeyDerivationMethod.Argon2IInt]: `${StoreKeyMethod.Kdf}:argon2i:int`, - [KeyDerivationMethod.Argon2IMod]: `${StoreKeyMethod.Kdf}:argon2i:mod`, - } - - return correspondanceTable[keyDerivationMethod] as StoreKeyMethod - } - - const uri = await this.getUri(walletConfig) - - return { - uri, - profile: walletConfig.id, - keyMethod: keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod), - passKey: walletConfig.key, - } - } - - private async getUri(walletConfig: WalletConfig) { - // By default use sqlite as database backend - let uri = '' - if (!walletConfig.storage) { - walletConfig.storage = { type: 'sqlite' } - } - - if (walletConfig.storage.type === 'sqlite') { - if (walletConfig.storage.inMemory) { - uri = 'sqlite://:memory:' - } else { - const path = `${(walletConfig.storage.path as string) ?? this.fileSystem.basePath + '/wallet'}/${ - walletConfig.id - }/sqlite.db` - - // Make sure path exists before creating the wallet - await this.fileSystem.createDirectory(path) - uri = `sqlite://${path}` - } - // TODO postgres - } else { - throw new WalletError(`Storage type not supported: ${walletConfig.storage.type}`) - } - - return uri - } /** * @throws {WalletDuplicateError} if the wallet already exists * @throws {WalletError} if another error occurs @@ -182,9 +141,9 @@ export class AskarWallet implements Wallet { public async createAndOpen(walletConfig: WalletConfig): Promise { this.logger.debug(`Creating wallet '${walletConfig.id}`) - const askarWalletConfig = await this.askarWalletConfig(walletConfig) + const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) try { - this.walletHandle = await Store.provision({ + this._store = await Store.provision({ recreate: false, uri: askarWalletConfig.uri, profile: askarWalletConfig.profile, @@ -192,7 +151,7 @@ export class AskarWallet implements Wallet { passKey: askarWalletConfig.passKey, }) this.walletConfig = walletConfig - this._session = await this.walletHandle.openSession() + this._session = await this._store.openSession() // TODO: Master Secret creation (now part of IndyCredx/AnonCreds) } catch (error) { @@ -208,7 +167,7 @@ export class AskarWallet implements Wallet { }) } - const errorMessage = `Error creating wallet '${walletConfig.id}'. CODE ${error.code}` + const errorMessage = `Error creating wallet '${walletConfig.id}'` this.logger.error(errorMessage, { error, errorMessage: error.message, @@ -256,25 +215,28 @@ export class AskarWallet implements Wallet { rekey?: string, rekeyDerivation?: KeyDerivationMethod ): Promise { - if (this.walletHandle) { + if (this._store) { throw new WalletError( 'Wallet instance already opened. Close the currently opened wallet before re-opening the wallet' ) } - const askarWalletConfig = await this.askarWalletConfig(walletConfig) + const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) try { - this.walletHandle = await Store.open({ + this._store = await Store.open({ uri: askarWalletConfig.uri, keyMethod: askarWalletConfig.keyMethod, passKey: askarWalletConfig.passKey, }) if (rekey) { - await this.walletHandle.rekey({ passKey: rekey, keyMethod: StoreKeyMethod.Raw /* TODO */ }) + await this._store.rekey({ + passKey: rekey, + keyMethod: keyDerivationMethodToStoreKeyMethod(rekeyDerivation) ?? StoreKeyMethod.Raw, + }) } - this._session = await this.walletHandle.openSession() + this._session = await this._store.openSession() this.walletConfig = walletConfig } catch (error) { @@ -294,10 +256,13 @@ export class AskarWallet implements Wallet { cause: error, }) } - throw new WalletError(`Error opening wallet ${walletConfig.id}`, { cause: error }) + throw new WalletError( + `Error opening wallet ${walletConfig.id}. ERROR CODE ${error.code} MESSAGE ${error.message}`, + { cause: error } + ) } - this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this.handle}'`) + this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this._store.handle.handle}'`) } /** @@ -313,12 +278,13 @@ export class AskarWallet implements Wallet { this.logger.info(`Deleting wallet '${this.walletConfig.id}'`) - if (this.walletHandle) { + if (this._store) { await this.close() } try { - await Store.remove(await this.getUri(this.walletConfig)) + const { uri } = uriFromWalletConfig(this.walletConfig, this.fileSystem.basePath) + await Store.remove(uri) } catch (error) { const errorMessage = `Error deleting wallet '${this.walletConfig.id}': ${error.message}` this.logger.error(errorMessage, { @@ -332,12 +298,12 @@ export class AskarWallet implements Wallet { public async export(exportConfig: WalletExportImportConfig) { // TODO - throw new WalletError('Export not yet implemented') + throw new WalletError('AskarWallet Export not yet implemented') } public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) { // TODO - throw new WalletError('Import not yet implemented') + throw new WalletError('AskarWallet Import not yet implemented') } /** @@ -345,14 +311,15 @@ export class AskarWallet implements Wallet { */ public async close(): Promise { this.logger.debug(`Closing wallet ${this.walletConfig?.id}`) - if (!this.walletHandle) { + if (!this._store) { throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no handle.') } try { - await this._session?.close() - await this.walletHandle.close() - this.walletHandle = undefined + await this.session.close() + await this.store.close() + this._session = undefined + this._store = undefined this.publicDidInfo = undefined } catch (error) { const errorMessage = `Error closing wallet': ${error.message}` @@ -373,8 +340,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 * - * TODO: use signingKeyProviderRegistry to support algorithms not implemented in Askar - * * @param seed string The seed for creating a key * @param keyType KeyType the type of key that should be created * @@ -385,17 +350,28 @@ export class AskarWallet implements Wallet { */ public async createKey({ seed, keyType }: WalletCreateKeyOptions): Promise { try { - const algorithm = keyAlgFromString(keyType) + if (keyTypeSupportedByAskar(keyType)) { + const algorithm = keyAlgFromString(keyType) - // Create key from seed - const key = seed - ? AskarKey.fromSeed({ seed: new TextEncoder().encode(seed), algorithm }) - : AskarKey.generate(algorithm) + // Create key from seed + const key = seed + ? AskarKey.fromSeed({ seed: new TextEncoder().encode(seed), algorithm }) + : AskarKey.generate(algorithm) - // Store key - await this._session?.insertKey({ key, name: encodeToBase58(key.publicBytes) }) + // Store key + await this.session.insertKey({ key, name: encodeToBase58(key.publicBytes) }) + return Key.fromPublicKey(key.publicBytes, keyType) + } 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) - return Key.fromPublicKey(key.publicBytes, keyType) + const keyPair = await signingKeyProvider.createKeyPair({ seed }) + await this.storeKeyPair(keyPair) + return Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyType) + } + throw new WalletError(`Unsupported key type: '${keyType}'`) + } } catch (error) { if (!isError(error)) { throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) @@ -407,8 +383,6 @@ export class AskarWallet implements Wallet { /** * sign a Buffer with an instance of a Key class * - * TODO: use signingKeyProviderRegistry to support algorithms not implemented in Askar - * * @param data Buffer The data that needs to be signed * @param key Key The key that is used to sign the data * @@ -416,19 +390,36 @@ export class AskarWallet implements Wallet { */ public async sign({ data, key }: WalletSignOptions): Promise { try { - const keyEntry = await this._session?.fetchKey({ name: key.publicKeyBase58 }) - - if (!keyEntry) { - throw new WalletError('Key entry not found') - } - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError(`Currently not suppirting signing of multiple messages`) + throw new WalletError(`Currently not supporting signing of multiple messages`) } - const signed = keyEntry.key.signMessage({ message: data as Buffer }) + if (keyTypeSupportedByAskar(key.keyType)) { + const 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 }) - return Buffer.from(signed) + 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) { if (!isError(error)) { throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) @@ -440,8 +431,6 @@ export class AskarWallet implements Wallet { /** * Verify the signature with the data and the used key * - * TODO: use signingKeyProviderRegistry to support algorithms not implemented in Askar - * * @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 @@ -453,26 +442,49 @@ export class AskarWallet implements Wallet { */ public async verify({ data, key, signature }: WalletVerifyOptions): Promise { try { - const askarKey = AskarKey.fromPublicBytes({ algorithm: askarKeyType(key.keyType), publicKey: key.publicKey }) - if (!TypedArrayEncoder.isTypedArray(data)) { throw new WalletError(`Currently not supporting signature of multiple messages`) } - return askarKey.verifySignature({ message: data as Buffer, signature }) + if (keyTypeSupportedByAskar(key.keyType)) { + const askarKey = AskarKey.fromPublicBytes({ + algorithm: keyAlgFromString(key.keyType), + publicKey: key.publicKey, + }) + return askarKey.verifySignature({ message: data as Buffer, signature }) + } 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) { 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}. ERROR ${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[], @@ -493,7 +505,7 @@ export class AskarWallet implements Wallet { }).convertkey({ algorithm: KeyAlgs.X25519 }) if (senderVerkey && senderExchangeKey) { - const enc_sender = CryptoBox.seal({ + const encryptedSender = CryptoBox.seal({ recipientKey: targetExchangeKey, message: new TextEncoder().encode(senderVerkey), }) @@ -507,11 +519,11 @@ export class AskarWallet implements Wallet { recipients.push( new JweRecipient({ - encrypted_key: encryptedCek, + encryptedKey: encryptedCek, header: { kid: recipientKey, - sender: base64ToBase64URL(Buffer.from(enc_sender).toString('base64')), - iv: base64ToBase64URL(Buffer.from(nonce).toString('base64')), + sender: uint8ArrayToBase64URL(encryptedSender), + iv: uint8ArrayToBase64URL(nonce), }, }) ) @@ -522,7 +534,7 @@ export class AskarWallet implements Wallet { }) recipients.push( new JweRecipient({ - encrypted_key: encryptedCek, + encryptedKey: encryptedCek, header: { kid: recipientKey, }, @@ -535,7 +547,7 @@ export class AskarWallet implements Wallet { enc: 'xchacha20poly1305_ietf', typ: 'JWM/1.0', alg: senderVerkey ? 'Authcrypt' : 'Anoncrypt', - recipients, + recipients: recipients.map((item) => JsonTransformer.toJSON(item)), } const { ciphertext, tag, nonce } = cek.aeadEncrypt({ @@ -543,14 +555,22 @@ export class AskarWallet implements Wallet { aad: new TextEncoder().encode(JsonEncoder.toBase64URL(protectedJson)), }).parts - return new JweEnvelope({ - ciphertext: base64ToBase64URL(Buffer.from(ciphertext).toString('base64')), - iv: base64ToBase64URL(Buffer.from(nonce).toString('base64')), + const envelope = new JweEnvelope({ + ciphertext: uint8ArrayToBase64URL(ciphertext), + iv: uint8ArrayToBase64URL(nonce), protected: JsonEncoder.toBase64URL(protectedJson), - tag: base64ToBase64URL(Buffer.from(tag).toString('base64')), - }) + tag: uint8ArrayToBase64URL(tag), + }).toJson() + + return envelope as EncryptedMessage } + /** + * 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) @@ -662,4 +682,64 @@ export class AskarWallet implements Wallet { throw new WalletError('Error generating wallet key', { cause: error }) } } + + private async getAskarWalletConfig(walletConfig: WalletConfig) { + const { uri, path } = uriFromWalletConfig(walletConfig, this.fileSystem.basePath) + + // Make sure path exists before creating the wallet + if (path) { + await this.fileSystem.createDirectory(path) + } + + return { + uri, + profile: walletConfig.id, + // FIXME: Default derivation method should be set somewhere in either agent config or some constants + keyMethod: keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod) ?? StoreKeyMethod.Raw, + 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) { + if ( + isAskarError(error) && + (error.code === askarErrors.NotFound || + // FIXME: this is current output from askar wrapper but does not describe specifically a not found scenario + error.message === 'Received null pointer. The native library could not find the value.') + ) { + throw new RecordNotFoundError(`KeyPairRecord not found for public key: ${publicKeyBase58}.`, { + recordType: 'KeyPairRecord', + cause: 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) && error.code === askarErrors.Duplicate) { + throw new RecordDuplicateError(`Record already exists`, { recordType: 'KeyPairRecord' }) + } + throw new WalletError('Error saving KeyPair record', { cause: error }) + } + } } diff --git a/packages/askar/src/wallet/JweEnvelope.ts b/packages/askar/src/wallet/JweEnvelope.ts index 227a3c9273..c8913084d2 100644 --- a/packages/askar/src/wallet/JweEnvelope.ts +++ b/packages/askar/src/wallet/JweEnvelope.ts @@ -1,14 +1,17 @@ import { JsonTransformer } from '@aries-framework/core' +import { Expose, Type } from 'class-transformer' -import { base64ToBase64URL } from '../../../core/src/utils/base64' +import { uint8ArrayToBase64URL } from '../../../core/src//utils/base64' export class JweRecipient { - public encrypted_key!: string + @Expose({ name: 'encrypted_key' }) + public encryptedKey!: string public header?: Record - public constructor(options: { encrypted_key: Uint8Array; header?: Record }) { + public constructor(options: { encryptedKey: Uint8Array; header?: Record }) { if (options) { - this.encrypted_key = base64ToBase64URL(Buffer.from(options.encrypted_key).toString('base64')) + this.encryptedKey = uint8ArrayToBase64URL(options.encryptedKey) + this.header = options.header } } @@ -23,19 +26,24 @@ export interface JweEnvelopeOptions { tag: string aad?: string header?: string[] - encrypted_key?: string + encryptedKey?: string } export class JweEnvelope { public protected!: string public unprotected?: string + + @Expose() + @Type(() => JweRecipient) public recipients?: JweRecipient[] public ciphertext!: string public iv!: string public tag!: string public aad?: string public header?: string[] - public encrypted_key?: string + + @Expose({ name: 'encrypted_key' }) + public encryptedKey?: string public constructor(options: JweEnvelopeOptions) { if (options) { @@ -47,7 +55,7 @@ export class JweEnvelope { this.tag = options.tag this.aad = options.aad this.header = options.header - this.encrypted_key = options.encrypted_key + this.encryptedKey = options.encryptedKey } } diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index dc5d0a3c18..d7d19207cc 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -1,6 +1,14 @@ -import type { WalletConfig } from '@aries-framework/core' +import type { + SigningProvider, + WalletConfig, + CreateKeyPairOptions, + KeyPair, + SignOptions, + VerifyOptions, +} from '@aries-framework/core' import { + WalletError, WalletDuplicateError, WalletNotFoundError, WalletInvalidKeyError, @@ -8,10 +16,13 @@ import { SigningProviderRegistry, TypedArrayEncoder, KeyDerivationMethod, + Buffer, } from '@aries-framework/core' import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { registerAriesAskar, Store } from 'aries-askar-test-shared' +import { TextEncoder } from 'util' +import { encodeToBase58 } from '../../../../core/src/utils/base58' import { agentDependencies } from '../../../../core/tests/helpers' import testLogger from '../../../../core/tests/logger' import { AskarWallet } from '../AskarWallet' @@ -44,8 +55,8 @@ describe('AskarWallet basic operations', () => { expect(askarWallet.masterSecretId).toEqual('Wallet: askarWalletTest') }) - test('Get the wallet handle', () => { - expect(askarWallet.handle).toEqual(expect.any(Store)) + test('Get the wallet store', () => { + expect(askarWallet.store).toEqual(expect.any(Store)) }) test('Generate Nonce', async () => { @@ -66,6 +77,10 @@ describe('AskarWallet basic operations', () => { }) }) + test('Fail to create a Bls12381g1g2 keypair', async () => { + await expect(askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 })).rejects.toThrowError(WalletError) + }) + test('Create a signature with a ed25519 keypair', async () => { const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) const signature = await askarWallet.sign({ @@ -89,6 +104,82 @@ describe('AskarWallet basic operations', () => { }) }) +describe('AskarWallet with custom signing provider', () => { + let askarWallet: AskarWallet + + const seed = 'sample-seed' + const message = TypedArrayEncoder.fromString('sample-message') + + beforeEach(async () => { + registerAriesAskar({ askar: new NodeJSAriesAskar() }) + + class DummySigningProvider implements SigningProvider { + public keyType: KeyType = KeyType.Bls12381g1g2 + + public async createKeyPair(options: CreateKeyPairOptions): Promise { + return { + publicKeyBase58: encodeToBase58(new TextEncoder().encode(options.seed || 'publicKeyBase58')), + privateKeyBase58: 'privateKeyBase58', + keyType: KeyType.Bls12381g1g2, + } + } + + public async sign(options: SignOptions): Promise { + return new Buffer('signed') + } + + public async verify(options: VerifyOptions): Promise { + return true + } + } + askarWallet = new AskarWallet( + testLogger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([new DummySigningProvider()]) + ) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('Create custom keypair and use it for signing', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(new TextEncoder().encode(seed))) + + const signature = await askarWallet.sign({ + data: message, + key, + }) + + expect(signature).toBeInstanceOf(Buffer) + }) + + test('Create custom keypair and use it for verifying', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(new TextEncoder().encode(seed))) + + const signature = await askarWallet.verify({ + data: message, + signature: new Buffer('signature'), + key, + }) + + expect(signature).toBeTruthy() + }) + + test('Attempt to create the same custom keypair twice', async () => { + await askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 }) + + await expect(askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 })).rejects.toThrow( + WalletError + ) + }) +}) + describe('AskarWallet management', () => { let askarWallet: AskarWallet diff --git a/packages/core/src/utils/base64.ts b/packages/core/src/utils/base64.ts index b66d6ad5db..5b9feca213 100644 --- a/packages/core/src/utils/base64.ts +++ b/packages/core/src/utils/base64.ts @@ -1,3 +1,9 @@ +import { Buffer } from './buffer' + export function base64ToBase64URL(base64: string) { return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') } + +export function uint8ArrayToBase64URL(array: Uint8Array) { + return base64ToBase64URL(Buffer.from(array).toString('base64')) +} diff --git a/tests/e2e-wallet-subject.test.ts b/tests/e2e-wallet-subject.test.ts index 57fb91d3bc..da88f58ce9 100644 --- a/tests/e2e-wallet-subject.test.ts +++ b/tests/e2e-wallet-subject.test.ts @@ -6,14 +6,16 @@ import { Subject } from 'rxjs' import { getAgentOptions, makeConnection, waitForBasicMessage } from '../packages/core/tests/helpers' -import { SubjectInboundTransport } from './transport/SubjectInboundTransport' -import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' - import { AskarModule, AskarStorageService, AskarWallet } from '@aries-framework/askar' import { Agent, DependencyManager, InjectionSymbols } from '@aries-framework/core' import { IndySdkModule, IndySdkStorageService, IndySdkWallet } from '@aries-framework/indy-sdk' + +import { SubjectInboundTransport } from './transport/SubjectInboundTransport' + import { agentDependencies } from '@aries-framework/node' +import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' + describe('E2E Wallet Subject tests', () => { let recipientAgent: Agent let senderAgent: Agent diff --git a/yarn.lock b/yarn.lock index 463887cd39..46332621d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,50 +10,6 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@aries-framework/core@file:packages/core": - version "0.3.2" - dependencies: - "@digitalcredentials/jsonld" "^5.2.1" - "@digitalcredentials/jsonld-signatures" "^9.3.1" - "@digitalcredentials/vc" "^1.1.2" - "@multiformats/base-x" "^4.0.1" - "@stablelib/ed25519" "^1.0.2" - "@stablelib/random" "^1.0.1" - "@stablelib/sha256" "^1.0.1" - "@types/indy-sdk" "1.16.24" - "@types/node-fetch" "^2.5.10" - "@types/ws" "^7.4.6" - abort-controller "^3.0.0" - bn.js "^5.2.0" - borc "^3.0.0" - buffer "^6.0.3" - class-transformer "0.5.1" - class-validator "0.13.1" - did-resolver "^3.1.3" - lru_map "^0.4.1" - luxon "^1.27.0" - make-error "^1.3.6" - object-inspect "^1.10.3" - query-string "^7.0.1" - reflect-metadata "^0.1.13" - rxjs "^7.2.0" - tsyringe "^4.7.0" - uuid "^8.3.2" - varint "^6.0.0" - web-did-resolver "^2.0.8" - -"@aries-framework/node@file:packages/node": - version "0.3.2" - dependencies: - "@aries-framework/core" "0.3.2" - "@types/express" "^4.17.15" - express "^4.17.1" - ffi-napi "^4.0.3" - indy-sdk "^1.16.0-dev-1636" - node-fetch "^2.6.1" - ref-napi "^3.0.3" - ws "^7.5.3" - "@azure/core-asynciterator-polyfill@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" @@ -3090,23 +3046,23 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aries-askar-test-nodejs@^0.1.0-dev.3: - version "0.1.0-dev.3" - resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.3.tgz#e197b8579723e13f4af25824fe326884fc1f3fb8" - integrity sha512-FC+p6w8HgWtI+H9l8n78+2pJQRKDTnVoKnfNjOSg2Ns21wvMdgwGe1v/I9qiTpOMRaLGmAcoK8CcxjCs8zQPLg== +aries-askar-test-nodejs@^0.1.0-dev.4: + version "0.1.0-dev.4" + resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.4.tgz#4fa128489d697cfb0c890b8959c48018cc5d5845" + integrity sha512-0tVLQLQDXyEQpsZgTGvxDgFpRcfILLp3Lpt5vzfoN0O2Dcgff1sEiMdVucPcZ80LjZ3WlvWtvc48XaZYEUM7Og== dependencies: "@mapbox/node-pre-gyp" "^1.0.10" - aries-askar-test-shared "0.1.0-dev.3" + aries-askar-test-shared "0.1.0-dev.4" ffi-napi "^4.0.3" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-napi "^3.0.3" ref-struct-di "^1.1.1" -aries-askar-test-shared@0.1.0-dev.3, aries-askar-test-shared@^0.1.0-dev.3: - version "0.1.0-dev.3" - resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.3.tgz#ee8b2927735cd349031ec86bd68c09466258cdea" - integrity sha512-6CBjvLbKdlHPhDX21gghJGGCwGTMzUBsa/CDWfSDdITdkaBbdxCjlNQMhLkHUxd1SmgxF23GJGtGXyRb3fGHUA== +aries-askar-test-shared@0.1.0-dev.4, aries-askar-test-shared@^0.1.0-dev.4: + version "0.1.0-dev.4" + resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.4.tgz#98b8dced478641f3cb867c177cc2fc88f110665e" + integrity sha512-HsY5GGGE7IeOF4QMkh3QLyoVJ/Xt03GpbV8Q4xKyBZk46pZEexu1/XrA1iy0vIachpyYiKQJWT1cYl6MN8PApw== dependencies: fast-text-encoding "^1.0.3" From 93a96171fdf4c4a928beb3d9c800ac13b24bfb7a Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 20 Jan 2023 19:16:56 -0300 Subject: [PATCH 07/12] fix: address some feedback from PR review Signed-off-by: Ariel Gentile --- package.json | 3 ++ packages/askar/package.json | 6 +-- .../askar/src/storage/AskarStorageService.ts | 12 +++--- .../__tests__/AskarStorageService.test.ts | 3 +- .../src/utils/AskarStorageServiceUtils.ts | 0 packages/askar/src/wallet/AskarWallet.ts | 42 +++++++++---------- packages/askar/src/wallet/JweEnvelope.ts | 7 +--- .../src/wallet/__tests__/AskarWallet.test.ts | 42 +++++++++---------- packages/core/src/utils/TypedArrayEncoder.ts | 2 +- packages/core/src/utils/base64.ts | 6 --- ...e2e-askar-indy-sdk-wallet-subject.test.ts} | 3 +- yarn.lock | 2 +- 12 files changed, 58 insertions(+), 70 deletions(-) create mode 100644 packages/askar/src/utils/AskarStorageServiceUtils.ts rename tests/{e2e-wallet-subject.test.ts => e2e-askar-indy-sdk-wallet-subject.test.ts} (98%) diff --git a/package.json b/package.json index 24f487b9a2..1083c12e10 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,9 @@ "run-mediator": "ts-node ./samples/mediator.ts", "next-version-bump": "ts-node ./scripts/get-next-bump.ts" }, + "dependencies": { + "aries-askar-test-nodejs": "0.1.0-dev.4" + }, "devDependencies": { "@types/cors": "^2.8.10", "@types/eslint": "^7.2.13", diff --git a/packages/askar/package.json b/packages/askar/package.json index 5e834dc2de..df584a82a9 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -2,7 +2,7 @@ "name": "@aries-framework/askar", "main": "build/index", "types": "build/index", - "version": "0.3.2", + "version": "0.3.3", "private": true, "files": [ "build" @@ -25,7 +25,7 @@ "test": "jest" }, "dependencies": { - "@aries-framework/core": "0.3.2", + "@aries-framework/core": "0.3.3", "aries-askar-test-shared": "^0.1.0-dev.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -34,7 +34,7 @@ }, "devDependencies": { "aries-askar-test-nodejs": "^0.1.0-dev.4", - "@aries-framework/node": "0.3.2", + "@aries-framework/node": "0.3.3", "rimraf": "^4.0.7", "typescript": "~4.9.4" } diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 9d6cbc8ae4..4caabc08cd 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -133,7 +133,7 @@ export class AskarStorageService implements StorageService /** @inheritDoc */ public async save(agentContext: AgentContext, record: T) { assertAskarWallet(agentContext.wallet) - const session = (agentContext.wallet as AskarWallet).session + const session = agentContext.wallet.session const value = JsonTransformer.serialize(record) const tags = this.transformFromRecordTagValues(record.getTags()) as Record @@ -152,7 +152,7 @@ export class AskarStorageService implements StorageService /** @inheritDoc */ public async update(agentContext: AgentContext, record: T): Promise { assertAskarWallet(agentContext.wallet) - const session = (agentContext.wallet as AskarWallet).session + const session = agentContext.wallet.session const value = JsonTransformer.serialize(record) const tags = this.transformFromRecordTagValues(record.getTags()) as Record @@ -174,7 +174,7 @@ export class AskarStorageService implements StorageService /** @inheritDoc */ public async delete(agentContext: AgentContext, record: T) { assertAskarWallet(agentContext.wallet) - const session = (agentContext.wallet as AskarWallet).session + const session = agentContext.wallet.session try { await session.remove({ category: record.type, name: record.id }) @@ -196,7 +196,7 @@ export class AskarStorageService implements StorageService id: string ): Promise { assertAskarWallet(agentContext.wallet) - const session = (agentContext.wallet as AskarWallet).session + const session = agentContext.wallet.session try { await session.remove({ category: recordClass.type, name: id }) @@ -214,7 +214,7 @@ export class AskarStorageService implements StorageService /** @inheritDoc */ public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { assertAskarWallet(agentContext.wallet) - const session = (agentContext.wallet as AskarWallet).session + const session = agentContext.wallet.session try { const record = await session.fetch({ category: recordClass.type, name: id }) @@ -243,7 +243,7 @@ export class AskarStorageService implements StorageService /** @inheritDoc */ public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { assertAskarWallet(agentContext.wallet) - const session = (agentContext.wallet as AskarWallet).session + const session = agentContext.wallet.session const records = await session.fetchAll({ category: recordClass.type }) diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index 18c3c2b564..e1e151560f 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -3,7 +3,6 @@ import type { AgentContext, TagsBase } from '@aries-framework/core' import { SigningProviderRegistry, RecordDuplicateError, RecordNotFoundError } from '@aries-framework/core' import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { registerAriesAskar } from 'aries-askar-test-shared' -import { TextEncoder } from 'util' import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' @@ -85,7 +84,7 @@ describe('AskarStorageService', () => { name: 'some-id', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sessionHandle: wallet.session.handle!, - value: new Uint8Array(new TextEncoder().encode('{}')), + value: Buffer.from('{}'), tags: { someBoolean: '1', someOtherBoolean: '0', diff --git a/packages/askar/src/utils/AskarStorageServiceUtils.ts b/packages/askar/src/utils/AskarStorageServiceUtils.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 977a608ea5..a6fc30ba9d 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -34,15 +34,14 @@ import { FileSystem, WalletNotFoundError, } from '@aries-framework/core' +// eslint-disable-next-line import/order import { StoreKeyMethod, KeyAlgs, CryptoBox, Store, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' const isError = (error: unknown): error is Error => error instanceof Error import { inject, injectable } from 'tsyringe' -import { TextDecoder, TextEncoder } from 'util' import { encodeToBase58, decodeFromBase58 } from '../../../core/src/utils/base58' -import { uint8ArrayToBase64URL } from '../../../core/src/utils/base64' import { askarErrors, isAskarError, @@ -354,9 +353,7 @@ export class AskarWallet implements Wallet { const algorithm = keyAlgFromString(keyType) // Create key from seed - const key = seed - ? AskarKey.fromSeed({ seed: new TextEncoder().encode(seed), algorithm }) - : AskarKey.generate(algorithm) + const key = seed ? AskarKey.fromSeed({ seed: Buffer.from(seed), algorithm }) : AskarKey.generate(algorithm) // Store key await this.session.insertKey({ key, name: encodeToBase58(key.publicBytes) }) @@ -507,7 +504,7 @@ export class AskarWallet implements Wallet { if (senderVerkey && senderExchangeKey) { const encryptedSender = CryptoBox.seal({ recipientKey: targetExchangeKey, - message: new TextEncoder().encode(senderVerkey), + message: Buffer.from(senderVerkey), }) const nonce = CryptoBox.randomNonce() const encryptedCek = CryptoBox.cryptoBox({ @@ -522,8 +519,8 @@ export class AskarWallet implements Wallet { encryptedKey: encryptedCek, header: { kid: recipientKey, - sender: uint8ArrayToBase64URL(encryptedSender), - iv: uint8ArrayToBase64URL(nonce), + sender: TypedArrayEncoder.toBase64URL(encryptedSender), + iv: TypedArrayEncoder.toBase64URL(nonce), }, }) ) @@ -551,15 +548,15 @@ export class AskarWallet implements Wallet { } const { ciphertext, tag, nonce } = cek.aeadEncrypt({ - message: new TextEncoder().encode(JSON.stringify(payload)), - aad: new TextEncoder().encode(JsonEncoder.toBase64URL(protectedJson)), + message: Buffer.from(JSON.stringify(payload)), + aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), }).parts const envelope = new JweEnvelope({ - ciphertext: uint8ArrayToBase64URL(ciphertext), - iv: uint8ArrayToBase64URL(nonce), + ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), + iv: TypedArrayEncoder.toBase64URL(nonce), protected: JsonEncoder.toBase64URL(protectedJson), - tag: uint8ArrayToBase64URL(tag), + tag: TypedArrayEncoder.toBase64URL(tag), }).toJson() return envelope as EncryptedMessage @@ -588,8 +585,8 @@ export class AskarWallet implements Wallet { if (!kid) { throw new WalletError('Blank recipient key') } - const sender = recip.header.sender ? new Uint8Array(Buffer.from(recip.header.sender, 'base64')) : undefined - const iv = recip.header.iv ? new Uint8Array(Buffer.from(recip.header.iv, 'base64')) : undefined + 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) { @@ -599,7 +596,7 @@ export class AskarWallet implements Wallet { kid, sender, iv, - encrypted_key: new Uint8Array(Buffer.from(recip.encrypted_key, 'base64')), + encrypted_key: TypedArrayEncoder.fromBase64(recip.encrypted_key), }) } @@ -620,7 +617,7 @@ export class AskarWallet implements Wallet { recipientKey = recipient.kid if (recipient.sender && recipient.iv) { - senderKey = new TextDecoder().decode( + senderKey = TypedArrayEncoder.toUtf8String( CryptoBox.sealOpen({ recipientKey: recip_x, ciphertext: recipient.sender, @@ -651,12 +648,11 @@ export class AskarWallet implements Wallet { const cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) const message = cek.aeadDecrypt({ - ciphertext: new Uint8Array(Buffer.from(messagePackage.ciphertext as any, 'base64')), - nonce: new Uint8Array(Buffer.from(messagePackage.iv as any, 'base64')), - tag: new Uint8Array(Buffer.from(messagePackage.tag as any, 'base64')), - aad: new TextEncoder().encode(messagePackage.protected), + 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, @@ -666,7 +662,7 @@ export class AskarWallet implements Wallet { public async generateNonce(): Promise { try { - return new TextDecoder().decode(CryptoBox.randomNonce()) + return TypedArrayEncoder.toUtf8String(CryptoBox.randomNonce()) } catch (error) { if (!isError(error)) { throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) diff --git a/packages/askar/src/wallet/JweEnvelope.ts b/packages/askar/src/wallet/JweEnvelope.ts index c8913084d2..ac4d791f89 100644 --- a/packages/askar/src/wallet/JweEnvelope.ts +++ b/packages/askar/src/wallet/JweEnvelope.ts @@ -1,8 +1,6 @@ -import { JsonTransformer } from '@aries-framework/core' +import { JsonTransformer, TypedArrayEncoder } from '@aries-framework/core' import { Expose, Type } from 'class-transformer' -import { uint8ArrayToBase64URL } from '../../../core/src//utils/base64' - export class JweRecipient { @Expose({ name: 'encrypted_key' }) public encryptedKey!: string @@ -10,7 +8,7 @@ export class JweRecipient { public constructor(options: { encryptedKey: Uint8Array; header?: Record }) { if (options) { - this.encryptedKey = uint8ArrayToBase64URL(options.encryptedKey) + this.encryptedKey = TypedArrayEncoder.toBase64URL(options.encryptedKey) this.header = options.header } @@ -33,7 +31,6 @@ export class JweEnvelope { public protected!: string public unprotected?: string - @Expose() @Type(() => JweRecipient) public recipients?: JweRecipient[] public ciphertext!: string diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index d7d19207cc..a2d0577254 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -20,7 +20,6 @@ import { } from '@aries-framework/core' import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { registerAriesAskar, Store } from 'aries-askar-test-shared' -import { TextEncoder } from 'util' import { encodeToBase58 } from '../../../../core/src/utils/base58' import { agentDependencies } from '../../../../core/tests/helpers' @@ -29,7 +28,7 @@ import { AskarWallet } from '../AskarWallet' // use raw key derivation method to speed up wallet creating / opening / closing between tests const walletConfig: WalletConfig = { - id: 'Wallet: askarWalletTest', + id: 'Wallet: AskarWalletTest', // generated using indy.generateWalletKey key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', keyDerivationMethod: KeyDerivationMethod.Raw, @@ -110,28 +109,29 @@ describe('AskarWallet with custom signing provider', () => { const seed = 'sample-seed' const message = TypedArrayEncoder.fromString('sample-message') - beforeEach(async () => { - registerAriesAskar({ askar: new NodeJSAriesAskar() }) - - class DummySigningProvider implements SigningProvider { - public keyType: KeyType = KeyType.Bls12381g1g2 + class DummySigningProvider implements SigningProvider { + public keyType: KeyType = KeyType.Bls12381g1g2 - public async createKeyPair(options: CreateKeyPairOptions): Promise { - return { - publicKeyBase58: encodeToBase58(new TextEncoder().encode(options.seed || 'publicKeyBase58')), - privateKeyBase58: 'privateKeyBase58', - keyType: KeyType.Bls12381g1g2, - } + public async createKeyPair(options: CreateKeyPairOptions): Promise { + return { + publicKeyBase58: encodeToBase58(Buffer.from(options.seed || 'publicKeyBase58')), + privateKeyBase58: 'privateKeyBase58', + keyType: KeyType.Bls12381g1g2, } + } - public async sign(options: SignOptions): Promise { - return new Buffer('signed') - } + public async sign(options: SignOptions): Promise { + return new Buffer('signed') + } - public async verify(options: VerifyOptions): Promise { - return true - } + public async verify(options: VerifyOptions): Promise { + return true } + } + + beforeEach(async () => { + registerAriesAskar({ askar: new NodeJSAriesAskar() }) + askarWallet = new AskarWallet( testLogger, new agentDependencies.FileSystem(), @@ -147,7 +147,7 @@ describe('AskarWallet with custom signing provider', () => { test('Create custom keypair and use it for signing', async () => { const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) expect(key.keyType).toBe(KeyType.Bls12381g1g2) - expect(key.publicKeyBase58).toBe(encodeToBase58(new TextEncoder().encode(seed))) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) const signature = await askarWallet.sign({ data: message, @@ -160,7 +160,7 @@ describe('AskarWallet with custom signing provider', () => { test('Create custom keypair and use it for verifying', async () => { const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) expect(key.keyType).toBe(KeyType.Bls12381g1g2) - expect(key.publicKeyBase58).toBe(encodeToBase58(new TextEncoder().encode(seed))) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) const signature = await askarWallet.verify({ data: message, diff --git a/packages/core/src/utils/TypedArrayEncoder.ts b/packages/core/src/utils/TypedArrayEncoder.ts index 685eac485c..83ee5d89ca 100644 --- a/packages/core/src/utils/TypedArrayEncoder.ts +++ b/packages/core/src/utils/TypedArrayEncoder.ts @@ -17,7 +17,7 @@ export class TypedArrayEncoder { * * @param buffer the buffer to encode into base64url string */ - public static toBase64URL(buffer: Buffer) { + public static toBase64URL(buffer: Buffer | Uint8Array) { return base64ToBase64URL(TypedArrayEncoder.toBase64(buffer)) } diff --git a/packages/core/src/utils/base64.ts b/packages/core/src/utils/base64.ts index 5b9feca213..b66d6ad5db 100644 --- a/packages/core/src/utils/base64.ts +++ b/packages/core/src/utils/base64.ts @@ -1,9 +1,3 @@ -import { Buffer } from './buffer' - export function base64ToBase64URL(base64: string) { return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') } - -export function uint8ArrayToBase64URL(array: Uint8Array) { - return base64ToBase64URL(Buffer.from(array).toString('base64')) -} diff --git a/tests/e2e-wallet-subject.test.ts b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts similarity index 98% rename from tests/e2e-wallet-subject.test.ts rename to tests/e2e-askar-indy-sdk-wallet-subject.test.ts index da88f58ce9..e6d872ee84 100644 --- a/tests/e2e-wallet-subject.test.ts +++ b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts @@ -1,6 +1,5 @@ import type { SubjectMessage } from './transport/SubjectInboundTransport' -// eslint-disable-next-line import/no-extraneous-dependencies import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { Subject } from 'rxjs' @@ -16,7 +15,7 @@ import { agentDependencies } from '@aries-framework/node' import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' -describe('E2E Wallet Subject tests', () => { +describe('E2E Askar-Indy SDK Wallet Subject tests', () => { let recipientAgent: Agent let senderAgent: Agent diff --git a/yarn.lock b/yarn.lock index 46332621d1..a0267f4a57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3046,7 +3046,7 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aries-askar-test-nodejs@^0.1.0-dev.4: +aries-askar-test-nodejs@0.1.0-dev.4, aries-askar-test-nodejs@^0.1.0-dev.4: version "0.1.0-dev.4" resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.4.tgz#4fa128489d697cfb0c890b8959c48018cc5d5845" integrity sha512-0tVLQLQDXyEQpsZgTGvxDgFpRcfILLp3Lpt5vzfoN0O2Dcgff1sEiMdVucPcZ80LjZ3WlvWtvc48XaZYEUM7Og== From dd1ce8215c181f25100d0baf9c67e502825e463d Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 20 Jan 2023 19:27:39 -0300 Subject: [PATCH 08/12] fix(askar): allow multiple sign/verify for custom signing providers Signed-off-by: Ariel Gentile --- packages/askar/src/wallet/AskarWallet.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index a6fc30ba9d..05fef1e0bd 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -387,11 +387,11 @@ export class AskarWallet implements Wallet { */ public async sign({ data, key }: WalletSignOptions): Promise { try { - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError(`Currently not supporting signing of multiple messages`) - } - if (keyTypeSupportedByAskar(key.keyType)) { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + const keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) if (!keyEntry) { @@ -439,11 +439,11 @@ export class AskarWallet implements Wallet { */ public async verify({ data, key, signature }: WalletVerifyOptions): Promise { try { - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError(`Currently not supporting signature of multiple messages`) - } - if (keyTypeSupportedByAskar(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, From 44e65dba5d47e9ef77424be7644fd41632f15ed1 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 23 Jan 2023 18:52:13 -0300 Subject: [PATCH 09/12] feat(askar): support posgres backend Signed-off-by: Ariel Gentile --- package.json | 2 +- packages/askar/jest.config.ts | 2 +- packages/askar/package.json | 4 +- .../askar/src/storage/AskarStorageService.ts | 132 ++---------------- .../__tests__/AskarStorageService.test.ts | 18 +-- packages/askar/src/storage/utils.ts | 110 +++++++++++++++ .../src/utils/AskarStorageServiceUtils.ts | 0 packages/askar/src/utils/askarWalletConfig.ts | 36 ++++- packages/askar/src/wallet/AskarWallet.ts | 2 +- .../AskarWalletPostgresStorageConfig.ts | 22 +++ .../src/wallet/__tests__/AskarWallet.test.ts | 2 +- packages/askar/src/wallet/index.ts | 1 + .../askar/tests/askar-postgres.e2e.test.ts | 112 +++++++++++++++ packages/askar/tests/helpers.ts | 50 +++++++ packages/askar/tests/setup.ts | 4 + packages/core/src/types.ts | 10 +- .../e2e-askar-indy-sdk-wallet-subject.test.ts | 3 + yarn.lock | 18 +-- 18 files changed, 376 insertions(+), 152 deletions(-) create mode 100644 packages/askar/src/storage/utils.ts delete mode 100644 packages/askar/src/utils/AskarStorageServiceUtils.ts create mode 100644 packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts create mode 100644 packages/askar/tests/askar-postgres.e2e.test.ts create mode 100644 packages/askar/tests/helpers.ts create mode 100644 packages/askar/tests/setup.ts diff --git a/package.json b/package.json index 1083c12e10..609872a732 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "next-version-bump": "ts-node ./scripts/get-next-bump.ts" }, "dependencies": { - "aries-askar-test-nodejs": "0.1.0-dev.4" + "aries-askar-test-nodejs": "0.1.0-dev.5" }, "devDependencies": { "@types/cors": "^2.8.10", diff --git a/packages/askar/jest.config.ts b/packages/askar/jest.config.ts index c7c5196637..55c67d70a6 100644 --- a/packages/askar/jest.config.ts +++ b/packages/askar/jest.config.ts @@ -8,7 +8,7 @@ const config: Config.InitialOptions = { ...base, name: packageJson.name, displayName: packageJson.name, - // setupFilesAfterEnv: ['./tests/setup.ts'], + setupFilesAfterEnv: ['./tests/setup.ts'], } export default config diff --git a/packages/askar/package.json b/packages/askar/package.json index df584a82a9..ee0c318b1b 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -26,14 +26,14 @@ }, "dependencies": { "@aries-framework/core": "0.3.3", - "aries-askar-test-shared": "^0.1.0-dev.4", + "aries-askar-test-shared": "^0.1.0-dev.5", + "aries-askar-test-nodejs": "^0.1.0-dev.5", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { - "aries-askar-test-nodejs": "^0.1.0-dev.4", "@aries-framework/node": "0.3.3", "rimraf": "^4.0.7", "typescript": "~4.9.4" diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 4caabc08cd..b0e4afbbf5 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -1,13 +1,4 @@ -import type { AskarWallet } from '../wallet/AskarWallet' -import type { - BaseRecordConstructor, - AgentContext, - BaseRecord, - TagsBase, - Query, - StorageService, -} from '@aries-framework/core' -import type { EntryObject } from 'aries-askar-test-shared' +import type { BaseRecordConstructor, AgentContext, BaseRecord, Query, StorageService } from '@aries-framework/core' import { RecordDuplicateError, @@ -21,122 +12,17 @@ import { Scan } from 'aries-askar-test-shared' import { askarErrors, isAskarError } from '../utils/askarError' import { assertAskarWallet } from '../utils/assertAskarWallet' +import { askarQueryFromSearchQuery, recordToInstance, transformFromRecordTagValues } from './utils' + @injectable() export class AskarStorageService implements StorageService { - private transformToRecordTagValues(tags: Record): TagsBase { - const transformedTags: TagsBase = {} - - for (const [key, value] of Object.entries(tags)) { - // If the value is a boolean string ('1' or '0') - // use the boolean val - if (value === '1' && key?.includes(':')) { - const [tagName, tagValue] = key.split(':') - - const transformedValue = transformedTags[tagName] - - if (Array.isArray(transformedValue)) { - transformedTags[tagName] = [...transformedValue, tagValue] - } else { - transformedTags[tagName] = [tagValue] - } - } - // Transform '1' and '0' to boolean - else if (value === '1' || value === '0') { - transformedTags[key] = value === '1' - } - // If 1 or 0 is prefixed with 'n__' we need to remove it. This is to prevent - // casting the value to a boolean - else if (value === 'n__1' || value === 'n__0') { - transformedTags[key] = value === 'n__1' ? '1' : '0' - } - // Otherwise just use the value - else { - transformedTags[key] = value as string - } - } - - return transformedTags - } - - private transformFromRecordTagValues(tags: TagsBase): { [key: string]: string | undefined } { - const transformedTags: { [key: string]: string | undefined } = {} - - for (const [key, value] of Object.entries(tags)) { - // If the value is of type null we use the value undefined - // Askar doesn't support null as a value - if (value === null) { - transformedTags[key] = undefined - } - // If the value is a boolean use the Askar - // '1' or '0' syntax - else if (typeof value === 'boolean') { - transformedTags[key] = value ? '1' : '0' - } - // If the value is 1 or 0, we need to add something to the value, otherwise - // the next time we deserialize the tag values it will be converted to boolean - else if (value === '1' || value === '0') { - transformedTags[key] = `n__${value}` - } - // If the value is an array we create a tag for each array - // item ("tagName:arrayItem" = "1") - else if (Array.isArray(value)) { - value.forEach((item) => { - const tagName = `${key}:${item}` - transformedTags[tagName] = '1' - }) - } - // Otherwise just use the value - else { - transformedTags[key] = value - } - } - - return transformedTags - } - - /** - * Transforms the search query into a wallet query compatible with Askar WQL. - * - * The format used by AFJ is almost the same as the WQL query, with the exception of - * the encoding of values, however this is handled by the {@link AskarStorageService.transformToRecordTagValues} - * method. - */ - // TODO: Transform to Askar format - private askarQueryFromSearchQuery(query: Query): Record { - // eslint-disable-next-line prefer-const - let { $and, $or, $not, ...tags } = query - - $and = ($and as Query[] | undefined)?.map((q) => this.askarQueryFromSearchQuery(q)) - $or = ($or as Query[] | undefined)?.map((q) => this.askarQueryFromSearchQuery(q)) - $not = $not ? this.askarQueryFromSearchQuery($not as Query) : undefined - - const askarQuery = { - ...this.transformFromRecordTagValues(tags as unknown as TagsBase), - $and, - $or, - $not, - } - - return askarQuery - } - - private recordToInstance(record: EntryObject, recordClass: BaseRecordConstructor): T { - const instance = JsonTransformer.deserialize(record.value as string, recordClass) - instance.id = record.name - - const tags = record.tags ? this.transformToRecordTagValues(record.tags) : {} - instance.replaceTags(tags) - - return instance - } - /** @inheritDoc */ public async save(agentContext: AgentContext, record: T) { assertAskarWallet(agentContext.wallet) const session = agentContext.wallet.session const value = JsonTransformer.serialize(record) - const tags = this.transformFromRecordTagValues(record.getTags()) as Record + const tags = transformFromRecordTagValues(record.getTags()) as Record try { await session.insert({ category: record.type, name: record.id, value, tags }) @@ -155,7 +41,7 @@ export class AskarStorageService implements StorageService const session = agentContext.wallet.session const value = JsonTransformer.serialize(record) - const tags = this.transformFromRecordTagValues(record.getTags()) as Record + const tags = transformFromRecordTagValues(record.getTags()) as Record try { await session.replace({ category: record.type, name: record.id, value, tags }) @@ -223,7 +109,7 @@ export class AskarStorageService implements StorageService recordType: recordClass.type, }) } - return this.recordToInstance(record, recordClass) + return recordToInstance(record, recordClass) } catch (error) { if ( isAskarError(error) && @@ -249,7 +135,7 @@ export class AskarStorageService implements StorageService const instances = [] for (const record of records) { - instances.push(this.recordToInstance(record, recordClass)) + instances.push(recordToInstance(record, recordClass)) } return instances } @@ -263,7 +149,7 @@ export class AskarStorageService implements StorageService assertAskarWallet(agentContext.wallet) const store = agentContext.wallet.store - const askarQuery = this.askarQueryFromSearchQuery(query) + const askarQuery = askarQueryFromSearchQuery(query) const scan = new Scan({ category: recordClass.type, @@ -275,7 +161,7 @@ export class AskarStorageService implements StorageService try { const records = await scan.fetchAll() for (const record of records) { - instances.push(this.recordToInstance(record, recordClass)) + instances.push(recordToInstance(record, recordClass)) } return instances } catch (error) { diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index e1e151560f..e99825bc38 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -1,6 +1,11 @@ import type { AgentContext, TagsBase } from '@aries-framework/core' -import { SigningProviderRegistry, RecordDuplicateError, RecordNotFoundError } from '@aries-framework/core' +import { + TypedArrayEncoder, + SigningProviderRegistry, + RecordDuplicateError, + RecordNotFoundError, +} from '@aries-framework/core' import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { registerAriesAskar } from 'aries-askar-test-shared' @@ -8,6 +13,7 @@ import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' import { AskarWallet } from '../../wallet/AskarWallet' import { AskarStorageService } from '../AskarStorageService' +import { askarQueryFromSearchQuery } from '../utils' describe('AskarStorageService', () => { let wallet: AskarWallet @@ -84,7 +90,7 @@ describe('AskarStorageService', () => { name: 'some-id', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sessionHandle: wallet.session.handle!, - value: Buffer.from('{}'), + value: TypedArrayEncoder.fromString('{}'), tags: { someBoolean: '1', someOtherBoolean: '0', @@ -238,9 +244,6 @@ describe('AskarStorageService', () => { it('finds records using $not statements', async () => { const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) - // FIXME: Seems to be an issue with Askar WQL implementation: - // it only takes into account records containing a myTag and returns only those different from the $not rule - // (i.e. expectedRecord2 will not be taken into account because it does not have any myTag value) const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) await insertRecord({ tags: { myTag: 'notfoobar' } }) @@ -253,8 +256,6 @@ describe('AskarStorageService', () => { }) it('correctly transforms an advanced query into a valid WQL query', async () => { - const storageService = new AskarStorageService() - const expectedQuery = { $and: [ { @@ -287,8 +288,7 @@ describe('AskarStorageService', () => { } expect( - //@ts-ignore - storageService.askarQueryFromSearchQuery({ + askarQueryFromSearchQuery({ $and: [ { $or: [{ myTag: true }, { myTag: false }], diff --git a/packages/askar/src/storage/utils.ts b/packages/askar/src/storage/utils.ts new file mode 100644 index 0000000000..45f330359c --- /dev/null +++ b/packages/askar/src/storage/utils.ts @@ -0,0 +1,110 @@ +import type { BaseRecord, BaseRecordConstructor, Query, TagsBase } from '@aries-framework/core' +import type { EntryObject } from 'aries-askar-test-shared' + +import { JsonTransformer } from '@aries-framework/core' + +export function recordToInstance(record: EntryObject, recordClass: BaseRecordConstructor): T { + const instance = JsonTransformer.deserialize(record.value as string, recordClass) + instance.id = record.name + + const tags = record.tags ? transformToRecordTagValues(record.tags) : {} + instance.replaceTags(tags) + + return instance +} + +export function transformToRecordTagValues(tags: Record): TagsBase { + const transformedTags: TagsBase = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is a boolean string ('1' or '0') + // use the boolean val + if (value === '1' && key?.includes(':')) { + const [tagName, tagValue] = key.split(':') + + const transformedValue = transformedTags[tagName] + + if (Array.isArray(transformedValue)) { + transformedTags[tagName] = [...transformedValue, tagValue] + } else { + transformedTags[tagName] = [tagValue] + } + } + // Transform '1' and '0' to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = value === '1' + } + // If 1 or 0 is prefixed with 'n__' we need to remove it. This is to prevent + // casting the value to a boolean + else if (value === 'n__1' || value === 'n__0') { + transformedTags[key] = value === 'n__1' ? '1' : '0' + } + // Otherwise just use the value + else { + transformedTags[key] = value as string + } + } + + return transformedTags +} + +export function transformFromRecordTagValues(tags: TagsBase): { [key: string]: string | undefined } { + const transformedTags: { [key: string]: string | undefined } = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is of type null we use the value undefined + // Askar doesn't support null as a value + if (value === null) { + transformedTags[key] = undefined + } + // If the value is a boolean use the Askar + // '1' or '0' syntax + else if (typeof value === 'boolean') { + transformedTags[key] = value ? '1' : '0' + } + // If the value is 1 or 0, we need to add something to the value, otherwise + // the next time we deserialize the tag values it will be converted to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = `n__${value}` + } + // If the value is an array we create a tag for each array + // item ("tagName:arrayItem" = "1") + else if (Array.isArray(value)) { + value.forEach((item) => { + const tagName = `${key}:${item}` + transformedTags[tagName] = '1' + }) + } + // Otherwise just use the value + else { + transformedTags[key] = value + } + } + + return transformedTags +} + +/** + * Transforms the search query into a wallet query compatible with Askar WQL. + * + * The format used by AFJ is almost the same as the WQL query, with the exception of + * the encoding of values, however this is handled by the {@link AskarStorageServiceUtil.transformToRecordTagValues} + * method. + */ +export function askarQueryFromSearchQuery(query: Query): Record { + // eslint-disable-next-line prefer-const + let { $and, $or, $not, ...tags } = query + + $and = ($and as Query[] | undefined)?.map((q) => askarQueryFromSearchQuery(q)) + $or = ($or as Query[] | undefined)?.map((q) => askarQueryFromSearchQuery(q)) + $not = $not ? askarQueryFromSearchQuery($not as Query) : undefined + + const askarQuery = { + ...transformFromRecordTagValues(tags as unknown as TagsBase), + $and, + $or, + $not, + } + + return askarQuery +} diff --git a/packages/askar/src/utils/AskarStorageServiceUtils.ts b/packages/askar/src/utils/AskarStorageServiceUtils.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/askar/src/utils/askarWalletConfig.ts b/packages/askar/src/utils/askarWalletConfig.ts index 295066c60b..a43de5a2e6 100644 --- a/packages/askar/src/utils/askarWalletConfig.ts +++ b/packages/askar/src/utils/askarWalletConfig.ts @@ -1,3 +1,4 @@ +import type { AskarWalletPostgresStorageConfig } from '../wallet/AskarWalletPostgresStorageConfig' import type { WalletConfig } from '@aries-framework/core' import { KeyDerivationMethod, WalletError } from '@aries-framework/core' @@ -33,8 +34,41 @@ export const uriFromWalletConfig = (walletConfig: WalletConfig, basePath: string path = `${(walletConfig.storage.path as string) ?? basePath + '/wallet'}/${walletConfig.id}/sqlite.db` uri = `sqlite://${path}` } + } else if (walletConfig.storage.type === 'postgres') { + const storageConfig = walletConfig.storage as unknown as AskarWalletPostgresStorageConfig + + if (!storageConfig.config || !storageConfig.credentials) { + throw new WalletError('Invalid storage configuration for postgres wallet') + } + + const urlParams = [] + if (storageConfig.config.connectTimeout !== undefined) { + urlParams.push(`connect_timeout=${encodeURIComponent(storageConfig.config.connectTimeout)}`) + } + if (storageConfig.config.idleTimeout !== undefined) { + urlParams.push(`idle_timeout=${encodeURIComponent(storageConfig.config.idleTimeout)}`) + } + if (storageConfig.config.maxConnections !== undefined) { + urlParams.push(`max_connections=${encodeURIComponent(storageConfig.config.maxConnections)}`) + } + if (storageConfig.config.minConnections !== undefined) { + urlParams.push(`min_connections=${encodeURIComponent(storageConfig.config.minConnections)}`) + } + if (storageConfig.credentials.adminAccount !== undefined) { + urlParams.push(`admin_account=${encodeURIComponent(storageConfig.credentials.adminAccount)}`) + } + if (storageConfig.credentials.adminPassword !== undefined) { + urlParams.push(`admin_password=${encodeURIComponent(storageConfig.credentials.adminPassword)}`) + } + + uri = `postgres://${encodeURIComponent(storageConfig.credentials.account)}:${encodeURIComponent( + storageConfig.credentials.password + )}@${storageConfig.config.host}/${encodeURIComponent(walletConfig.id)}` + + if (urlParams.length > 0) { + uri = `${uri}?${urlParams.join('&')}` + } } else { - // TODO posgres throw new WalletError(`Storage type not supported: ${walletConfig.storage.type}`) } diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 05fef1e0bd..ab4e566435 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -691,7 +691,7 @@ export class AskarWallet implements Wallet { uri, profile: walletConfig.id, // FIXME: Default derivation method should be set somewhere in either agent config or some constants - keyMethod: keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod) ?? StoreKeyMethod.Raw, + keyMethod: keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod) ?? StoreKeyMethod.None, passKey: walletConfig.key, } } diff --git a/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts b/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts new file mode 100644 index 0000000000..a9a9aab91f --- /dev/null +++ b/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts @@ -0,0 +1,22 @@ +import type { WalletStorageConfig } from '../../../core/src/types' + +export interface AskarWalletPostgresConfig { + host: string + connectTimeout?: number + idleTimeout?: number + maxConnections?: number + minConnections?: number +} + +export interface AskarWalletPostgresCredentials { + account: string + password: string + adminAccount?: string + adminPassword?: string +} + +export interface AskarWalletPostgresStorageConfig extends WalletStorageConfig { + type: 'postgres' + config: AskarWalletPostgresConfig + credentials: AskarWalletPostgresCredentials +} diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index a2d0577254..78fd0c9adc 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -51,7 +51,7 @@ describe('AskarWallet basic operations', () => { }) test('Get the Master Secret', () => { - expect(askarWallet.masterSecretId).toEqual('Wallet: askarWalletTest') + expect(askarWallet.masterSecretId).toEqual('Wallet: AskarWalletTest') }) test('Get the wallet store', () => { diff --git a/packages/askar/src/wallet/index.ts b/packages/askar/src/wallet/index.ts index ce420d6138..8d569fdf4c 100644 --- a/packages/askar/src/wallet/index.ts +++ b/packages/askar/src/wallet/index.ts @@ -1 +1,2 @@ export { AskarWallet } from './AskarWallet' +export * from './AskarWalletPostgresStorageConfig' diff --git a/packages/askar/tests/askar-postgres.e2e.test.ts b/packages/askar/tests/askar-postgres.e2e.test.ts new file mode 100644 index 0000000000..e07926a9df --- /dev/null +++ b/packages/askar/tests/askar-postgres.e2e.test.ts @@ -0,0 +1,112 @@ +/* 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 { DependencyManager, InjectionSymbols, Agent, HandshakeProtocol } from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { waitForBasicMessage } from '../../core/tests/helpers' +import { AskarStorageService } from '../src/storage' +import { AskarWallet } from '../src/wallet' + +import { getPostgresAgentOptions } from './helpers' + +// FIXME: Remove when Askar JS Wrapper performance issues are solved +jest.setTimeout(120000) + +const storageConfig: AskarWalletPostgresStorageConfig = { + type: 'postgres', + config: { + host: 'localhost:5432', + }, + credentials: { + account: 'postgres', + password: 'postgres', + }, +} + +const alicePostgresAgentOptions = getPostgresAgentOptions('AgentsAlice', storageConfig, { + endpoints: ['rxjs:alice'], +}) +const bobPostgresAgentOptions = getPostgresAgentOptions('AgentsBob', storageConfig, { + endpoints: ['rxjs:bob'], +}) + +describe('Askar Postgres agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + let aliceConnection: ConnectionRecord + let bobConnection: ConnectionRecord + + afterAll(async () => { + if (bobAgent) { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + } + + if (aliceAgent) { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + } + }) + + test('make a connection between postgres agents', async () => { + const aliceMessages = new Subject() + const bobMessages = new Subject() + + const subjectMap = { + 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, + } + + const aliceDependencyManager = new DependencyManager() + aliceDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + aliceDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + aliceAgent = new Agent(alicePostgresAgentOptions, aliceDependencyManager) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + const bobDependencyManager = new DependencyManager() + bobDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + bobDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + bobAgent = new Agent(bobPostgresAgentOptions, bobDependencyManager) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + 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) + }) +}) diff --git a/packages/askar/tests/helpers.ts b/packages/askar/tests/helpers.ts new file mode 100644 index 0000000000..cf33bd0d0d --- /dev/null +++ b/packages/askar/tests/helpers.ts @@ -0,0 +1,50 @@ +import type { AskarWalletPostgresStorageConfig } from '../src/wallet' +import type { InitConfig } from '@aries-framework/core' + +import { LogLevel } from '@aries-framework/core' +import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' +import path from 'path' + +import { TestLogger } from '../../core/tests/logger' +import { agentDependencies } from '../../node/src' +import { AskarModule } from '../src/AskarModule' + +export const genesisPath = process.env.GENESIS_TXN_PATH + ? path.resolve(process.env.GENESIS_TXN_PATH) + : path.join(__dirname, '../../../../network/genesis/local-genesis.txn') + +export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' + +export function getPostgresAgentOptions( + name: string, + storageConfig: AskarWalletPostgresStorageConfig, + extraConfig: Partial = {} +) { + const config: InitConfig = { + label: `Agent: ${name}`, + walletConfig: { + id: `Wallet${name}`, + key: `Key${name}`, + storage: storageConfig, + }, + connectToIndyLedgersOnStartup: false, + publicDidSeed, + autoAcceptConnections: true, + autoUpdateStorageOnStartup: false, + indyLedgers: [ + { + id: `pool-${name}`, + indyNamespace: `pool:localtest`, + isProduction: false, + genesisPath, + }, + ], + logger: new TestLogger(LogLevel.off, name), + ...extraConfig, + } + return { + config, + dependencies: agentDependencies, + modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, + } as const +} diff --git a/packages/askar/tests/setup.ts b/packages/askar/tests/setup.ts new file mode 100644 index 0000000000..9a096e4aa1 --- /dev/null +++ b/packages/askar/tests/setup.ts @@ -0,0 +1,4 @@ +import 'reflect-metadata' + +// FIXME: Remove when Askar JS Wrapper performance issues are solved +jest.setTimeout(60000) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d2f5a21c8f..b454c7963e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -13,14 +13,16 @@ export enum KeyDerivationMethod { Raw = 'RAW', } +export interface WalletStorageConfig { + type: string + [key: string]: unknown +} + export interface WalletConfig { id: string key: string keyDerivationMethod?: KeyDerivationMethod - storage?: { - type: string - [key: string]: unknown - } + storage?: WalletStorageConfig masterSecretId?: string } diff --git a/tests/e2e-askar-indy-sdk-wallet-subject.test.ts b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts index e6d872ee84..e23a3584d6 100644 --- a/tests/e2e-askar-indy-sdk-wallet-subject.test.ts +++ b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts @@ -15,6 +15,9 @@ import { agentDependencies } from '@aries-framework/node' import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' +// FIXME: Remove when Askar JS Wrapper performance issues are solved +jest.setTimeout(180000) + describe('E2E Askar-Indy SDK Wallet Subject tests', () => { let recipientAgent: Agent let senderAgent: Agent diff --git a/yarn.lock b/yarn.lock index a0267f4a57..d1d1d8ebdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3046,23 +3046,23 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aries-askar-test-nodejs@0.1.0-dev.4, aries-askar-test-nodejs@^0.1.0-dev.4: - version "0.1.0-dev.4" - resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.4.tgz#4fa128489d697cfb0c890b8959c48018cc5d5845" - integrity sha512-0tVLQLQDXyEQpsZgTGvxDgFpRcfILLp3Lpt5vzfoN0O2Dcgff1sEiMdVucPcZ80LjZ3WlvWtvc48XaZYEUM7Og== +aries-askar-test-nodejs@0.1.0-dev.5, aries-askar-test-nodejs@^0.1.0-dev.5: + version "0.1.0-dev.5" + resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.5.tgz#dd2b804cec69f78cdf51521d30b509afa5354ac0" + integrity sha512-Veotp1nUmH0SjmJaNChuRte1YydvMzhE6u18Ht9JwugwjbF+BH9aE/qkMWCa8s6GKkqsxrr7AUUsKDDe8XmBYw== dependencies: "@mapbox/node-pre-gyp" "^1.0.10" - aries-askar-test-shared "0.1.0-dev.4" + aries-askar-test-shared "0.1.0-dev.5" ffi-napi "^4.0.3" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-napi "^3.0.3" ref-struct-di "^1.1.1" -aries-askar-test-shared@0.1.0-dev.4, aries-askar-test-shared@^0.1.0-dev.4: - version "0.1.0-dev.4" - resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.4.tgz#98b8dced478641f3cb867c177cc2fc88f110665e" - integrity sha512-HsY5GGGE7IeOF4QMkh3QLyoVJ/Xt03GpbV8Q4xKyBZk46pZEexu1/XrA1iy0vIachpyYiKQJWT1cYl6MN8PApw== +aries-askar-test-shared@0.1.0-dev.5, aries-askar-test-shared@^0.1.0-dev.5: + version "0.1.0-dev.5" + resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.5.tgz#5a7c358a5749a1a7d3a96be5eb36e8939902f571" + integrity sha512-dYg+YdeVy1hEOxhKUmssbSnkTQ2fe7AVZ4dG9YqFMeD9+DkJ7ThNsAasR54sgXmd3Aa1G7inu2oRveJxlETXuw== dependencies: fast-text-encoding "^1.0.3" From 27c3c475c19947651880b7bc96c5a14402065fc2 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 23 Jan 2023 21:27:04 -0300 Subject: [PATCH 10/12] chore: update askar test packages Signed-off-by: Ariel Gentile --- package.json | 2 +- packages/askar/package.json | 6 +++--- yarn.lock | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 609872a732..8eaa83edc2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "next-version-bump": "ts-node ./scripts/get-next-bump.ts" }, "dependencies": { - "aries-askar-test-nodejs": "0.1.0-dev.5" + "aries-askar-test-nodejs": "^0.1.0-dev.6" }, "devDependencies": { "@types/cors": "^2.8.10", diff --git a/packages/askar/package.json b/packages/askar/package.json index ee0c318b1b..19de41fd37 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -26,15 +26,15 @@ }, "dependencies": { "@aries-framework/core": "0.3.3", - "aries-askar-test-shared": "^0.1.0-dev.5", - "aries-askar-test-nodejs": "^0.1.0-dev.5", + "aries-askar-test-shared": "^0.1.0-dev.6", + "aries-askar-test-nodejs": "^0.1.0-dev.6", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { - "@aries-framework/node": "0.3.3", + "reflect-metadata": "^0.1.13", "rimraf": "^4.0.7", "typescript": "~4.9.4" } diff --git a/yarn.lock b/yarn.lock index d1d1d8ebdd..11f32ffa15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3046,23 +3046,23 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aries-askar-test-nodejs@0.1.0-dev.5, aries-askar-test-nodejs@^0.1.0-dev.5: - version "0.1.0-dev.5" - resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.5.tgz#dd2b804cec69f78cdf51521d30b509afa5354ac0" - integrity sha512-Veotp1nUmH0SjmJaNChuRte1YydvMzhE6u18Ht9JwugwjbF+BH9aE/qkMWCa8s6GKkqsxrr7AUUsKDDe8XmBYw== +aries-askar-test-nodejs@^0.1.0-dev.6: + version "0.1.0-dev.6" + resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.6.tgz#ad63f37a5c7ff7c2c77e063d3113e48a56a71c78" + integrity sha512-SMF34Ntd20XeL0gFqMLFRRLp+Wsk0U6kTYcZNJQ5FYJR7H2qAIbPp9t3TyyTT2cpRk98bNqcG0xHrXjTnAbtcQ== dependencies: "@mapbox/node-pre-gyp" "^1.0.10" - aries-askar-test-shared "0.1.0-dev.5" + aries-askar-test-shared "0.1.0-dev.6" ffi-napi "^4.0.3" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-napi "^3.0.3" ref-struct-di "^1.1.1" -aries-askar-test-shared@0.1.0-dev.5, aries-askar-test-shared@^0.1.0-dev.5: - version "0.1.0-dev.5" - resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.5.tgz#5a7c358a5749a1a7d3a96be5eb36e8939902f571" - integrity sha512-dYg+YdeVy1hEOxhKUmssbSnkTQ2fe7AVZ4dG9YqFMeD9+DkJ7ThNsAasR54sgXmd3Aa1G7inu2oRveJxlETXuw== +aries-askar-test-shared@0.1.0-dev.6, aries-askar-test-shared@^0.1.0-dev.6: + version "0.1.0-dev.6" + resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.6.tgz#409d60067b8772b154f4b549261d317979b05a39" + integrity sha512-PKAUqxaqDTAgYxvORZr2ugulmu8KRJ2PKRb45fwjWvNRUgkfWXSEnGCogTZ8Y1Xs4c2gpypnilVbMmUy5fhDhQ== dependencies: fast-text-encoding "^1.0.3" From b44647f254c66204d8bf8c3ae02a2407569ba9e4 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sun, 5 Feb 2023 20:21:11 -0300 Subject: [PATCH 11/12] update to hyperledger packages and skip askar e2e tests due to performance issues Signed-off-by: Ariel Gentile --- package.json | 3 - packages/askar/package.json | 4 +- packages/askar/src/AskarModule.ts | 36 +++-- .../askar/src/storage/AskarStorageService.ts | 2 +- .../__tests__/AskarStorageService.test.ts | 9 +- packages/askar/src/storage/utils.ts | 2 +- packages/askar/src/types.ts | 3 +- packages/askar/src/utils/askarError.ts | 2 +- packages/askar/src/utils/askarKeyTypes.ts | 2 +- packages/askar/src/utils/askarWalletConfig.ts | 2 +- packages/askar/src/wallet/AskarWallet.ts | 11 +- .../src/wallet/__tests__/AskarWallet.test.ts | 124 +++++++++--------- .../src/wallet/__tests__/packing.test.ts | 3 - .../askar/tests/askar-postgres.e2e.test.ts | 20 +-- packages/askar/tests/helpers.ts | 3 +- packages/askar/tests/setup.ts | 7 + packages/core/src/agent/Agent.ts | 6 +- .../e2e-askar-indy-sdk-wallet-subject.test.ts | 53 +++----- yarn.lock | 40 +++--- 19 files changed, 156 insertions(+), 176 deletions(-) diff --git a/package.json b/package.json index 8eaa83edc2..24f487b9a2 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,6 @@ "run-mediator": "ts-node ./samples/mediator.ts", "next-version-bump": "ts-node ./scripts/get-next-bump.ts" }, - "dependencies": { - "aries-askar-test-nodejs": "^0.1.0-dev.6" - }, "devDependencies": { "@types/cors": "^2.8.10", "@types/eslint": "^7.2.13", diff --git a/packages/askar/package.json b/packages/askar/package.json index 19de41fd37..5ed1b8b150 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -26,14 +26,14 @@ }, "dependencies": { "@aries-framework/core": "0.3.3", - "aries-askar-test-shared": "^0.1.0-dev.6", - "aries-askar-test-nodejs": "^0.1.0-dev.6", + "@hyperledger/aries-askar-shared": "^0.1.0-dev.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { + "@hyperledger/aries-askar-nodejs": "^0.1.0-dev.1", "reflect-metadata": "^0.1.13", "rimraf": "^4.0.7", "typescript": "~4.9.4" diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts index e3c59c1f29..5eccb13b3d 100644 --- a/packages/askar/src/AskarModule.ts +++ b/packages/askar/src/AskarModule.ts @@ -1,21 +1,33 @@ -import type { AskarModuleConfigOptions } from './AskarModuleConfig' import type { DependencyManager, Module } from '@aries-framework/core' -import { registerAriesAskar } from 'aries-askar-test-shared' +import { AriesFrameworkError, InjectionSymbols } from '@aries-framework/core' -import { AskarModuleConfig } from './AskarModuleConfig' -import { AskarSymbol } from './types' +import { AskarStorageService } from './storage' +import { AskarWallet } from './wallet' export class AskarModule implements Module { - public readonly config: AskarModuleConfig - - public constructor(config: AskarModuleConfigOptions) { - this.config = new AskarModuleConfig(config) - } - public register(dependencyManager: DependencyManager) { - registerAriesAskar({ askar: this.config.askar }) + try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('@hyperledger/aries-askar-nodejs') + } catch (error) { + try { + require('@hyperledger/aries-askar-react-native') + } catch (error) { + throw new Error('Could not load aries-askar bindings') + } + } + + if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { + throw new AriesFrameworkError('There is an instance of Wallet already registered') + } else { + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + } - dependencyManager.registerInstance(AskarSymbol, this.config.askar) + if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { + throw new AriesFrameworkError('There is an instance of StorageService already registered') + } else { + dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + } } } diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index b0e4afbbf5..e7c96399c2 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -7,7 +7,7 @@ import { injectable, JsonTransformer, } from '@aries-framework/core' -import { Scan } from 'aries-askar-test-shared' +import { Scan } from '@hyperledger/aries-askar-shared' import { askarErrors, isAskarError } from '../utils/askarError' import { assertAskarWallet } from '../utils/assertAskarWallet' diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index e99825bc38..1ba1bf329f 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -6,8 +6,7 @@ import { RecordDuplicateError, RecordNotFoundError, } from '@aries-framework/core' -import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' -import { registerAriesAskar } from 'aries-askar-test-shared' +import { ariesAskar } from '@hyperledger/aries-askar-shared' import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' @@ -19,11 +18,9 @@ describe('AskarStorageService', () => { let wallet: AskarWallet let storageService: AskarStorageService let agentContext: AgentContext - const askar = new NodeJSAriesAskar() beforeEach(async () => { const agentConfig = getAgentConfig('AskarStorageServiceTest') - registerAriesAskar({ askar }) wallet = new AskarWallet(agentConfig.logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) agentContext = getAgentContext({ @@ -65,7 +62,7 @@ describe('AskarStorageService', () => { }, }) - const retrieveRecord = await askar.sessionFetch({ + const retrieveRecord = await ariesAskar.sessionFetch({ category: record.type, name: record.id, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -85,7 +82,7 @@ describe('AskarStorageService', () => { }) it('should correctly transform tag values from string after retrieving', async () => { - await askar.sessionUpdate({ + await ariesAskar.sessionUpdate({ category: TestRecord.type, name: 'some-id', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/packages/askar/src/storage/utils.ts b/packages/askar/src/storage/utils.ts index 45f330359c..381bd98dd7 100644 --- a/packages/askar/src/storage/utils.ts +++ b/packages/askar/src/storage/utils.ts @@ -1,5 +1,5 @@ import type { BaseRecord, BaseRecordConstructor, Query, TagsBase } from '@aries-framework/core' -import type { EntryObject } from 'aries-askar-test-shared' +import type { EntryObject } from '@hyperledger/aries-askar-shared' import { JsonTransformer } from '@aries-framework/core' diff --git a/packages/askar/src/types.ts b/packages/askar/src/types.ts index d3e230adbf..bc0baa2947 100644 --- a/packages/askar/src/types.ts +++ b/packages/askar/src/types.ts @@ -1,4 +1,3 @@ -import type { AriesAskar } from 'aries-askar-test-shared' +import type { AriesAskar } from '@hyperledger/aries-askar-shared' -export const AskarSymbol = Symbol('Askar') export type { AriesAskar } diff --git a/packages/askar/src/utils/askarError.ts b/packages/askar/src/utils/askarError.ts index 7d946830f2..2cfcbd90cf 100644 --- a/packages/askar/src/utils/askarError.ts +++ b/packages/askar/src/utils/askarError.ts @@ -1,4 +1,4 @@ -import { AriesAskarError } from 'aries-askar-test-shared' +import { AriesAskarError } from '@hyperledger/aries-askar-shared' export enum askarErrors { Success = 0, diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts index 67f8c0ac8e..bb837f962e 100644 --- a/packages/askar/src/utils/askarKeyTypes.ts +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -1,6 +1,6 @@ import type { KeyType } from '@aries-framework/core' -import { KeyAlgs } from 'aries-askar-test-shared' +import { KeyAlgs } from '@hyperledger/aries-askar-shared' export const keyTypeSupportedByAskar = (keyType: KeyType) => Object.entries(KeyAlgs).find(([, value]) => value === keyType.toString()) !== undefined diff --git a/packages/askar/src/utils/askarWalletConfig.ts b/packages/askar/src/utils/askarWalletConfig.ts index a43de5a2e6..dcf1d15ab1 100644 --- a/packages/askar/src/utils/askarWalletConfig.ts +++ b/packages/askar/src/utils/askarWalletConfig.ts @@ -2,7 +2,7 @@ import type { AskarWalletPostgresStorageConfig } from '../wallet/AskarWalletPost import type { WalletConfig } from '@aries-framework/core' import { KeyDerivationMethod, WalletError } from '@aries-framework/core' -import { StoreKeyMethod } from 'aries-askar-test-shared' +import { StoreKeyMethod } from '@hyperledger/aries-askar-shared' export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod?: KeyDerivationMethod) => { if (!keyDerivationMethod) { diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index ab4e566435..432e50cdda 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -13,7 +13,7 @@ import type { KeyPair, KeyDerivationMethod, } from '@aries-framework/core' -import type { Session } from 'aries-askar-test-shared' +import type { Session } from '@hyperledger/aries-askar-shared' import { JsonTransformer, @@ -35,7 +35,14 @@ import { WalletNotFoundError, } from '@aries-framework/core' // eslint-disable-next-line import/order -import { StoreKeyMethod, KeyAlgs, CryptoBox, Store, Key as AskarKey, keyAlgFromString } from 'aries-askar-test-shared' +import { + StoreKeyMethod, + KeyAlgs, + CryptoBox, + Store, + Key as AskarKey, + keyAlgFromString, +} from '@hyperledger/aries-askar-shared' const isError = (error: unknown): error is Error => error instanceof Error diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index 78fd0c9adc..9386dd9d9e 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -18,8 +18,7 @@ import { KeyDerivationMethod, Buffer, } from '@aries-framework/core' -import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' -import { registerAriesAskar, Store } from 'aries-askar-test-shared' +import { Store } from '@hyperledger/aries-askar-shared' import { encodeToBase58 } from '../../../../core/src/utils/base58' import { agentDependencies } from '../../../../core/tests/helpers' @@ -41,7 +40,6 @@ describe('AskarWallet basic operations', () => { const message = TypedArrayEncoder.fromString('sample-message') beforeEach(async () => { - registerAriesAskar({ askar: new NodeJSAriesAskar() }) askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) await askarWallet.createAndOpen(walletConfig) }) @@ -76,8 +74,10 @@ describe('AskarWallet basic operations', () => { }) }) - test('Fail to create a Bls12381g1g2 keypair', async () => { - await expect(askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 })).rejects.toThrowError(WalletError) + describe.skip('Currently, all KeyTypes are supported by Askar natively', () => { + test('Fail to create a Bls12381g1g2 keypair', async () => { + await expect(askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 })).rejects.toThrowError(WalletError) + }) }) test('Create a signature with a ed25519 keypair', async () => { @@ -103,90 +103,86 @@ describe('AskarWallet basic operations', () => { }) }) -describe('AskarWallet with custom signing provider', () => { - let askarWallet: AskarWallet +describe.skip('Currently, all KeyTypes are supported by Askar natively', () => { + describe('AskarWallet with custom signing provider', () => { + let askarWallet: AskarWallet - const seed = 'sample-seed' - const message = TypedArrayEncoder.fromString('sample-message') + const seed = 'sample-seed' + const message = TypedArrayEncoder.fromString('sample-message') - class DummySigningProvider implements SigningProvider { - public keyType: KeyType = KeyType.Bls12381g1g2 + class DummySigningProvider implements SigningProvider { + public keyType: KeyType = KeyType.Bls12381g1g2 - public async createKeyPair(options: CreateKeyPairOptions): Promise { - return { - publicKeyBase58: encodeToBase58(Buffer.from(options.seed || 'publicKeyBase58')), - privateKeyBase58: 'privateKeyBase58', - keyType: KeyType.Bls12381g1g2, + public async createKeyPair(options: CreateKeyPairOptions): Promise { + return { + publicKeyBase58: encodeToBase58(Buffer.from(options.seed || 'publicKeyBase58')), + privateKeyBase58: 'privateKeyBase58', + keyType: KeyType.Bls12381g1g2, + } } - } - public async sign(options: SignOptions): Promise { - return new Buffer('signed') - } + public async sign(options: SignOptions): Promise { + return new Buffer('signed') + } - public async verify(options: VerifyOptions): Promise { - return true + public async verify(options: VerifyOptions): Promise { + return true + } } - } - beforeEach(async () => { - registerAriesAskar({ askar: new NodeJSAriesAskar() }) + beforeEach(async () => { + askarWallet = new AskarWallet( + testLogger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([new DummySigningProvider()]) + ) + await askarWallet.createAndOpen(walletConfig) + }) - askarWallet = new AskarWallet( - testLogger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([new DummySigningProvider()]) - ) - await askarWallet.createAndOpen(walletConfig) - }) + afterEach(async () => { + await askarWallet.delete() + }) - afterEach(async () => { - await askarWallet.delete() - }) + test('Create custom keypair and use it for signing', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) - test('Create custom keypair and use it for signing', async () => { - const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) - expect(key.keyType).toBe(KeyType.Bls12381g1g2) - expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) + const signature = await askarWallet.sign({ + data: message, + key, + }) - const signature = await askarWallet.sign({ - data: message, - key, + expect(signature).toBeInstanceOf(Buffer) }) - expect(signature).toBeInstanceOf(Buffer) - }) + test('Create custom keypair and use it for verifying', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) - test('Create custom keypair and use it for verifying', async () => { - const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) - expect(key.keyType).toBe(KeyType.Bls12381g1g2) - expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) + const signature = await askarWallet.verify({ + data: message, + signature: new Buffer('signature'), + key, + }) - const signature = await askarWallet.verify({ - data: message, - signature: new Buffer('signature'), - key, + expect(signature).toBeTruthy() }) - expect(signature).toBeTruthy() - }) - - test('Attempt to create the same custom keypair twice', async () => { - await askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 }) + test('Attempt to create the same custom keypair twice', async () => { + await askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 }) - await expect(askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 })).rejects.toThrow( - WalletError - ) + await expect(askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 })).rejects.toThrow( + WalletError + ) + }) }) }) describe('AskarWallet management', () => { let askarWallet: AskarWallet - beforeEach(async () => { - registerAriesAskar({ askar: new NodeJSAriesAskar() }) - }) - afterEach(async () => { if (askarWallet) { await askarWallet.delete() diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts index 47b871bfe2..2a27e18678 100644 --- a/packages/askar/src/wallet/__tests__/packing.test.ts +++ b/packages/askar/src/wallet/__tests__/packing.test.ts @@ -7,8 +7,6 @@ import { SigningProviderRegistry, KeyDerivationMethod, } from '@aries-framework/core' -import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' -import { registerAriesAskar } from 'aries-askar-test-shared' import { agentDependencies } from '../../../../core/tests/helpers' import testLogger from '../../../../core/tests/logger' @@ -26,7 +24,6 @@ describe('askarWallet packing', () => { let askarWallet: AskarWallet beforeEach(async () => { - registerAriesAskar({ askar: new NodeJSAriesAskar() }) askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) await askarWallet.createAndOpen(walletConfig) }) diff --git a/packages/askar/tests/askar-postgres.e2e.test.ts b/packages/askar/tests/askar-postgres.e2e.test.ts index e07926a9df..dfbc6db600 100644 --- a/packages/askar/tests/askar-postgres.e2e.test.ts +++ b/packages/askar/tests/askar-postgres.e2e.test.ts @@ -3,20 +3,15 @@ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTran import type { AskarWalletPostgresStorageConfig } from '../src/wallet' import type { ConnectionRecord } from '@aries-framework/core' -import { DependencyManager, InjectionSymbols, Agent, HandshakeProtocol } from '@aries-framework/core' +import { Agent, HandshakeProtocol } from '@aries-framework/core' import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' import { waitForBasicMessage } from '../../core/tests/helpers' -import { AskarStorageService } from '../src/storage' -import { AskarWallet } from '../src/wallet' import { getPostgresAgentOptions } from './helpers' -// FIXME: Remove when Askar JS Wrapper performance issues are solved -jest.setTimeout(120000) - const storageConfig: AskarWalletPostgresStorageConfig = { type: 'postgres', config: { @@ -35,7 +30,8 @@ const bobPostgresAgentOptions = getPostgresAgentOptions('AgentsBob', storageConf endpoints: ['rxjs:bob'], }) -describe('Askar Postgres agents', () => { +// FIXME: Re-include in tests when Askar NodeJS wrapper performance is improved +describe.skip('Askar Postgres agents', () => { let aliceAgent: Agent let bobAgent: Agent let aliceConnection: ConnectionRecord @@ -62,18 +58,12 @@ describe('Askar Postgres agents', () => { 'rxjs:bob': bobMessages, } - const aliceDependencyManager = new DependencyManager() - aliceDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) - aliceDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) - aliceAgent = new Agent(alicePostgresAgentOptions, aliceDependencyManager) + aliceAgent = new Agent(alicePostgresAgentOptions) aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() - const bobDependencyManager = new DependencyManager() - bobDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) - bobDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) - bobAgent = new Agent(bobPostgresAgentOptions, bobDependencyManager) + bobAgent = new Agent(bobPostgresAgentOptions) bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await bobAgent.initialize() diff --git a/packages/askar/tests/helpers.ts b/packages/askar/tests/helpers.ts index cf33bd0d0d..17a521a1af 100644 --- a/packages/askar/tests/helpers.ts +++ b/packages/askar/tests/helpers.ts @@ -2,7 +2,6 @@ import type { AskarWalletPostgresStorageConfig } from '../src/wallet' import type { InitConfig } from '@aries-framework/core' import { LogLevel } from '@aries-framework/core' -import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import path from 'path' import { TestLogger } from '../../core/tests/logger' @@ -45,6 +44,6 @@ export function getPostgresAgentOptions( return { config, dependencies: agentDependencies, - modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, + modules: { askar: new AskarModule() }, } as const } diff --git a/packages/askar/tests/setup.ts b/packages/askar/tests/setup.ts index 9a096e4aa1..6e37c5e96c 100644 --- a/packages/askar/tests/setup.ts +++ b/packages/askar/tests/setup.ts @@ -1,4 +1,11 @@ import 'reflect-metadata' +try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('@hyperledger/aries-askar-nodejs') +} catch (error) { + throw new Error('Could not load aries-askar bindings') +} + // FIXME: Remove when Askar JS Wrapper performance issues are solved jest.setTimeout(60000) diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index 2909c3536d..3785d00f4a 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -74,6 +74,9 @@ export class Agent extends BaseAge dependencyManager.registerInstance(InjectionSymbols.Stop$, new Subject()) dependencyManager.registerInstance(InjectionSymbols.FileSystem, new agentConfig.agentDependencies.FileSystem()) + // Register all modules. This will also include the default modules + dependencyManager.registerModules(modulesWithDefaultModules) + // Register possibly already defined services if (!dependencyManager.isRegistered(InjectionSymbols.Wallet)) { dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndyWallet) @@ -88,9 +91,6 @@ export class Agent extends BaseAge dependencyManager.registerSingleton(InjectionSymbols.MessageRepository, InMemoryMessageRepository) } - // Register all modules. This will also include the default modules - dependencyManager.registerModules(modulesWithDefaultModules) - // TODO: contextCorrelationId for base wallet // Bind the default agent context to the container for use in modules etc. dependencyManager.registerInstance( diff --git a/tests/e2e-askar-indy-sdk-wallet-subject.test.ts b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts index e23a3584d6..b7d4233738 100644 --- a/tests/e2e-askar-indy-sdk-wallet-subject.test.ts +++ b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts @@ -1,11 +1,10 @@ import type { SubjectMessage } from './transport/SubjectInboundTransport' -import { NodeJSAriesAskar } from 'aries-askar-test-nodejs' import { Subject } from 'rxjs' import { getAgentOptions, makeConnection, waitForBasicMessage } from '../packages/core/tests/helpers' -import { AskarModule, AskarStorageService, AskarWallet } from '@aries-framework/askar' +import { AskarModule } from '@aries-framework/askar' import { Agent, DependencyManager, InjectionSymbols } from '@aries-framework/core' import { IndySdkModule, IndySdkStorageService, IndySdkWallet } from '@aries-framework/indy-sdk' @@ -15,10 +14,8 @@ import { agentDependencies } from '@aries-framework/node' import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' -// FIXME: Remove when Askar JS Wrapper performance issues are solved -jest.setTimeout(180000) - -describe('E2E Askar-Indy SDK Wallet Subject tests', () => { +// FIXME: Re-include in tests when Askar NodeJS wrapper performance is improved +describe.skip('E2E Askar-Indy SDK Wallet Subject tests', () => { let recipientAgent: Agent let senderAgent: Agent @@ -48,44 +45,26 @@ describe('E2E Askar-Indy SDK Wallet Subject tests', () => { ) // Recipient is an Agent using Askar Wallet - const recipientDependencyManager = new DependencyManager() - recipientDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) - recipientDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) - recipientAgent = new Agent( - { - ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), - modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, - }, - recipientDependencyManager - ) + recipientAgent = new Agent({ + ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), + modules: { askar: new AskarModule() }, + }) await e2eWalletTest(senderAgent, recipientAgent) }) test('Wallet Subject flow - Askar Sender / Askar Recipient ', async () => { - // Sender is an Agent using Indy SDK Wallet - const senderDependencyManager = new DependencyManager() - senderDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) - senderDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) - senderAgent = new Agent( - { - ...getAgentOptions('E2E Wallet Subject Sender Askar', { endpoints: ['rxjs:sender'] }), - modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, - }, - senderDependencyManager - ) + // Sender is an Agent using Askar Wallet + senderAgent = new Agent({ + ...getAgentOptions('E2E Wallet Subject Sender Askar', { endpoints: ['rxjs:sender'] }), + modules: { askar: new AskarModule() }, + }) // Recipient is an Agent using Askar Wallet - const recipientDependencyManager = new DependencyManager() - recipientDependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) - recipientDependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) - recipientAgent = new Agent( - { - ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), - modules: { askar: new AskarModule({ askar: new NodeJSAriesAskar() }) }, - }, - recipientDependencyManager - ) + recipientAgent = new Agent({ + ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), + modules: { askar: new AskarModule() }, + }) await e2eWalletTest(senderAgent, recipientAgent) }) diff --git a/yarn.lock b/yarn.lock index 140d4ed79d..f182ddd068 100644 --- a/yarn.lock +++ b/yarn.lock @@ -858,6 +858,26 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@hyperledger/aries-askar-nodejs@^0.1.0-dev.1": + version "0.1.0-dev.1" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.1.0-dev.1.tgz#b384d422de48f0ce5918e1612d2ca32ebd160520" + integrity sha512-XrRskQ0PaNAerItvfxKkS8YaVg+iuImguoqfyQ4ZSaePKZQnTqZpkxo6faKS+GlsaubRXz/6yz3YndVRIxPO+w== + dependencies: + "@hyperledger/aries-askar-shared" "0.1.0-dev.1" + "@mapbox/node-pre-gyp" "^1.0.10" + ffi-napi "^4.0.3" + node-cache "^5.1.2" + ref-array-di "^1.2.2" + ref-napi "^3.0.3" + ref-struct-di "^1.1.1" + +"@hyperledger/aries-askar-shared@0.1.0-dev.1", "@hyperledger/aries-askar-shared@^0.1.0-dev.1": + version "0.1.0-dev.1" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-shared/-/aries-askar-shared-0.1.0-dev.1.tgz#4e4e494c3a44c7c82f7b95ad4f06149f2a3a9b6c" + integrity sha512-Pt525M6CvnE3N6jxMpSqLy7RpOsc4oqa2Q+hc2UdCHuSYwmM/aeqt6wiA5dpghvl8g/78lCi1Dz74pzp7Dmm3w== + dependencies: + fast-text-encoding "^1.0.3" + "@hyperledger/indy-vdr-nodejs@^0.1.0-dev.4": version "0.1.0-dev.4" resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.1.0-dev.4.tgz#b5d2090b30c4a51e4e4f15a024054aada0d3550e" @@ -3063,26 +3083,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aries-askar-test-nodejs@^0.1.0-dev.6: - version "0.1.0-dev.6" - resolved "https://registry.yarnpkg.com/aries-askar-test-nodejs/-/aries-askar-test-nodejs-0.1.0-dev.6.tgz#ad63f37a5c7ff7c2c77e063d3113e48a56a71c78" - integrity sha512-SMF34Ntd20XeL0gFqMLFRRLp+Wsk0U6kTYcZNJQ5FYJR7H2qAIbPp9t3TyyTT2cpRk98bNqcG0xHrXjTnAbtcQ== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.10" - aries-askar-test-shared "0.1.0-dev.6" - ffi-napi "^4.0.3" - node-cache "^5.1.2" - ref-array-di "^1.2.2" - ref-napi "^3.0.3" - ref-struct-di "^1.1.1" - -aries-askar-test-shared@0.1.0-dev.6, aries-askar-test-shared@^0.1.0-dev.6: - version "0.1.0-dev.6" - resolved "https://registry.yarnpkg.com/aries-askar-test-shared/-/aries-askar-test-shared-0.1.0-dev.6.tgz#409d60067b8772b154f4b549261d317979b05a39" - integrity sha512-PKAUqxaqDTAgYxvORZr2ugulmu8KRJ2PKRb45fwjWvNRUgkfWXSEnGCogTZ8Y1Xs4c2gpypnilVbMmUy5fhDhQ== - dependencies: - fast-text-encoding "^1.0.3" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" From c06f1ef186fbad1114529736fcbd1e2f4ed55fea Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 6 Feb 2023 08:54:58 -0300 Subject: [PATCH 12/12] add eslint ignores and increase timeout for askar tests Signed-off-by: Ariel Gentile --- packages/askar/src/wallet/__tests__/AskarWallet.test.ts | 2 ++ packages/askar/tests/setup.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index 9386dd9d9e..15bbf174cd 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -121,10 +121,12 @@ describe.skip('Currently, all KeyTypes are supported by Askar natively', () => { } } + // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sign(options: SignOptions): Promise { return new Buffer('signed') } + // eslint-disable-next-line @typescript-eslint/no-unused-vars public async verify(options: VerifyOptions): Promise { return true } diff --git a/packages/askar/tests/setup.ts b/packages/askar/tests/setup.ts index 6e37c5e96c..a09e05318c 100644 --- a/packages/askar/tests/setup.ts +++ b/packages/askar/tests/setup.ts @@ -8,4 +8,4 @@ try { } // FIXME: Remove when Askar JS Wrapper performance issues are solved -jest.setTimeout(60000) +jest.setTimeout(180000)