From a89f635ac3a4c1177054c9e72ea7a2d1e2dba1d9 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Tue, 3 Aug 2021 14:16:34 +0200 Subject: [PATCH] feat(key-manager): move private key storage to kms-local fixes #539 fixes #540 BREAKING CHANGE: `keyManagetGet` no longer includes private key data BREAKING CHANGE: `KeyStore` no longer requires a `SecretBox` BREAKING CHANGE: `KeyManagementSystem` needs a `PrivateKeyStore` BREAKING CHANGE: @veramo/cli configuration version update to 3.0 --- __tests__/localAgent.test.ts | 5 +- __tests__/localMemoryStoreAgent.test.ts | 4 +- __tests__/restAgent.test.ts | 5 +- __tests__/shared/didManager.ts | 97 ++++----- __tests__/shared/keyManager.ts | 38 +++- package.json | 2 +- packages/cli/default/default.yml | 28 +-- .../src/migrations/SecretBox1588075773000.ts | 8 +- packages/cli/src/setup.ts | 2 +- packages/core/plugin.schema.json | 178 +++++++++++++++- packages/core/src/types/IDIDManager.ts | 4 +- packages/core/src/types/IIdentifier.ts | 10 + packages/core/src/types/IKeyManager.ts | 20 +- packages/data-store/src/data-store-orm.ts | 7 +- packages/data-store/src/entities/key.ts | 4 +- .../data-store/src/entities/private-key.ts | 20 ++ .../data-store/src/identifier/did-store.ts | 8 +- .../data-store/src/identifier/key-store.ts | 32 +-- .../src/identifier/private-key-store.ts | 56 +++++ packages/data-store/src/index.ts | 6 +- packages/did-manager/src/id-manager.ts | 22 +- packages/did-manager/src/memory-did-store.ts | 10 +- packages/key-manager/package.json | 8 +- .../src/__tests__/abstract-key-store.test.ts | 12 +- .../src/abstract-key-management-system.ts | 28 ++- .../key-manager/src/abstract-key-store.ts | 5 +- .../src/abstract-private-key-store.ts | 16 ++ packages/key-manager/src/index.ts | 3 +- packages/key-manager/src/key-manager.ts | 34 +-- packages/key-manager/src/memory-key-store.ts | 48 +++++ .../kms-local/src/__tests__/kms-local.test.ts | 63 +++--- .../kms-local/src/key-management-system.ts | 200 +++++++++++++----- yarn.lock | 9 +- 33 files changed, 748 insertions(+), 244 deletions(-) create mode 100644 packages/data-store/src/entities/private-key.ts create mode 100644 packages/data-store/src/identifier/private-key-store.ts create mode 100644 packages/key-manager/src/abstract-private-key-store.ts diff --git a/__tests__/localAgent.test.ts b/__tests__/localAgent.test.ts index ed07eff731..fc13890440 100644 --- a/__tests__/localAgent.test.ts +++ b/__tests__/localAgent.test.ts @@ -34,6 +34,7 @@ import { DataStore, DataStoreORM, ProfileDiscoveryProvider, + PrivateKeyStore, } from '../packages/data-store/src' import { getDidKeyResolver } from '../packages/did-provider-key/src' import { IDIDDiscovery, DIDDiscovery } from '../packages/did-discovery/src' @@ -105,9 +106,9 @@ const setup = async (options?: IAgentOptions): Promise => { }, plugins: [ new KeyManager({ - store: new KeyStore(dbConnection, new SecretBox(secretKey)), + store: new KeyStore(dbConnection), kms: { - local: new KeyManagementSystem(), + local: new KeyManagementSystem({ keyStore: new PrivateKeyStore(dbConnection, new SecretBox(secretKey)) }), }, }), new DIDManager({ diff --git a/__tests__/localMemoryStoreAgent.test.ts b/__tests__/localMemoryStoreAgent.test.ts index 836bf2bfc0..e8cf260646 100644 --- a/__tests__/localMemoryStoreAgent.test.ts +++ b/__tests__/localMemoryStoreAgent.test.ts @@ -9,7 +9,7 @@ import { IAgentOptions, } from '../packages/core/src' import { MessageHandler } from '../packages/message-handler/src' -import { KeyManager, MemoryKeyStore } from '../packages/key-manager/src' +import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../packages/key-manager/src' import { DIDManager, MemoryDIDStore } from '../packages/did-manager/src' import { createConnection, Connection } from 'typeorm' import { DIDResolverPlugin } from '../packages/did-resolver/src' @@ -93,7 +93,7 @@ const setup = async (options?: IAgentOptions): Promise => { new KeyManager({ store: new MemoryKeyStore(), kms: { - local: new KeyManagementSystem(), + local: new KeyManagementSystem({ keyStore: new MemoryPrivateKeyStore() }), }, }), new DIDManager({ diff --git a/__tests__/restAgent.test.ts b/__tests__/restAgent.test.ts index b25b51bf33..118831630b 100644 --- a/__tests__/restAgent.test.ts +++ b/__tests__/restAgent.test.ts @@ -37,6 +37,7 @@ import { DataStore, DataStoreORM, ProfileDiscoveryProvider, + PrivateKeyStore, } from '../packages/data-store/src' import { AgentRestClient } from '../packages/remote-client/src' import { AgentRouter, RequestWithAgentRouter, MessagingRouter } from '../packages/remote-server/src' @@ -113,9 +114,9 @@ const setup = async (options?: IAgentOptions): Promise => { ...options, plugins: [ new KeyManager({ - store: new KeyStore(dbConnection, new SecretBox(secretKey)), + store: new KeyStore(dbConnection), kms: { - local: new KeyManagementSystem(), + local: new KeyManagementSystem({ keyStore: new PrivateKeyStore(dbConnection, new SecretBox(secretKey)) }), }, }), new DIDManager({ diff --git a/__tests__/shared/didManager.ts b/__tests__/shared/didManager.ts index 9093b1255a..cdccf415d8 100644 --- a/__tests__/shared/didManager.ts +++ b/__tests__/shared/didManager.ts @@ -261,56 +261,57 @@ export default (testContext: { }) it('should import identifier', async () => { - const identifier = await agent.didManagerGetOrCreate({ - alias: 'example.org', + expect.assertions(1) + const did = 'did:web:imported.example' + const imported = await agent.didManagerImport({ + did, provider: 'did:web', + services: [ + { + id: `${did}#msg`, + type: 'Messaging', + serviceEndpoint: 'https://example.org/messaging', + description: 'Handles incoming messages', + }, + ], + keys: [ + { + kms: 'local', + privateKeyHex: 'e63886b5ba367dc2aff9acea6d955ee7c39115f12eaf2aa6b1a2eaa852036668', + type: 'Secp256k1', + }, + ], + }) + expect(imported).toEqual({ + did, + keys: [ + { + kid: '04dd467afb12bdb797303e7f3f0c8cd0ba80d518dc4e339e0e2eb8f2d99a9415cac537854a30d31a854b7af0b4fcb54c3954047390fa9500d3cc2e15a3e09017bb', + kms: 'local', + meta: { + algorithms: [ + 'ES256K', + 'ES256K-R', + 'eth_signTransaction', + 'eth_signTypedData', + 'eth_signMessage', + ], + }, + publicKeyHex: + '04dd467afb12bdb797303e7f3f0c8cd0ba80d518dc4e339e0e2eb8f2d99a9415cac537854a30d31a854b7af0b4fcb54c3954047390fa9500d3cc2e15a3e09017bb', + type: 'Secp256k1', + }, + ], + provider: 'did:web', + services: [ + { + description: 'Handles incoming messages', + id: `${did}#msg`, + serviceEndpoint: 'https://example.org/messaging', + type: 'Messaging', + } as Service, + ], }) - - await agent.didManagerAddService({ - did: identifier.did, - service: { - id: 'did:web:example.org#msg', - type: 'Messaging', - serviceEndpoint: 'https://example.org/messaging', - description: 'Handles incoming messages', - }, - }) - - const signingKeyFull = await agent.keyManagerGet({ - kid: identifier.keys[0].kid, - }) - - const encryptionKey = await agent.keyManagerCreate({ - kms: 'local', - type: 'Ed25519', - }) - - const encryptionKeyFull = await agent.keyManagerGet({ - kid: encryptionKey.kid, - }) - - await agent.didManagerAddKey({ - did: identifier.did, - key: encryptionKey, - }) - - const exportedIdentifier = await agent.didManagerGet({ - did: identifier.did, - }) - - await agent.didManagerDelete({ - did: identifier.did, - }) - - await agent.didManagerImport({ - ...exportedIdentifier, - keys: [signingKeyFull, encryptionKeyFull], - }) - - const importedIdentifier = await agent.didManagerGet({ - did: identifier.did, - }) - expect(importedIdentifier).toEqual(exportedIdentifier) }) it('should set alias for identifier', async () => { diff --git a/__tests__/shared/keyManager.ts b/__tests__/shared/keyManager.ts index 83759de910..53de3f7cda 100644 --- a/__tests__/shared/keyManager.ts +++ b/__tests__/shared/keyManager.ts @@ -69,7 +69,9 @@ export default (testContext: { kms: 'foobar', type: 'Secp256k1', }), - ).rejects.toThrow('KMS does not exist: foobar') + ).rejects.toThrow( + `invalid_argument: This agent has no registered KeyManagementSystem with name='foobar'`, + ) }) it('should throw an error for unsupported key type', async () => { @@ -115,8 +117,12 @@ export default (testContext: { kid: key.kid, }) - expect(key2).toHaveProperty('privateKeyHex') - expect(key2.publicKeyHex).toEqual(key.publicKeyHex) + expect(key2).toHaveProperty('kid') + expect(key2).toHaveProperty('kms') + expect(key2).toHaveProperty('publicKeyHex') + expect(key2).toHaveProperty('type') + expect(key2).not.toHaveProperty('privateKeyHex') + expect(key2).toEqual(key) }) it('should delete key', async () => { @@ -145,24 +151,34 @@ export default (testContext: { }) it('should import key', async () => { - const fullKey: IKey = { - kid: '04dd467afb12bdb797303e7f3f0c8cd0ba80d518dc4e339e0e2eb8f2d99a9415cac537854a30d31a854b7af0b4fcb54c3954047390fa9500d3cc2e15a3e09017bb', + const keyData = { + kid: 'myImportedKey', + kms: 'local', + type: 'Secp256k1', + privateKeyHex: 'e63886b5ba367dc2aff9acea6d955ee7c39115f12eaf2aa6b1a2eaa852036668', + meta: { foo: 'bar' }, + } + + const expectedImport = { + kid: 'myImportedKey', kms: 'local', type: 'Secp256k1', publicKeyHex: '04dd467afb12bdb797303e7f3f0c8cd0ba80d518dc4e339e0e2eb8f2d99a9415cac537854a30d31a854b7af0b4fcb54c3954047390fa9500d3cc2e15a3e09017bb', - privateKeyHex: 'e63886b5ba367dc2aff9acea6d955ee7c39115f12eaf2aa6b1a2eaa852036668', - meta: { foo: 'bar' }, + meta: { + algorithms: ['ES256K', 'ES256K-R', 'eth_signTransaction', 'eth_signTypedData', 'eth_signMessage'], + foo: 'bar', + }, } - const result = await agent.keyManagerImport(fullKey) - expect(result).toEqual(true) + const result = await agent.keyManagerImport(keyData) + expect(result).toEqual(expectedImport) const key2 = await agent.keyManagerGet({ - kid: fullKey.kid, + kid: keyData.kid, }) - expect(key2).toEqual(fullKey) + expect(key2).toEqual(expectedImport) }) it('should sign JWT', async () => { diff --git a/package.json b/package.json index aea9eaf07b..70f68121c5 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "test:integration-prepare": "ts-node --project packages/tsconfig.settings.json ./scripts/prepare-integration-tests.ts", "test:integration-pretty": "prettier --write __tests__/shared/documentationExamples.ts", "test:integration": "yarn test:integration-build && yarn test:ci", - "test:ci": "jest --config=jest.json --maxWorkers=2", + "test:ci": "jest --config=jest.json", "test": "jest --config=jest.json --coverage=false", "test:watch": "yarn test --watch --verbose", "veramo": "./packages/cli/bin/veramo.js", diff --git a/packages/cli/default/default.yml b/packages/cli/default/default.yml index 4e708495bb..ebf036f34e 100644 --- a/packages/cli/default/default.yml +++ b/packages/cli/default/default.yml @@ -1,4 +1,4 @@ -version: 2.0 +version: 3.0 constants: baseUrl: http://localhost:3332 @@ -173,20 +173,7 @@ didResolver: ethr-did-resolver: $require: ethr-did-resolver?t=function&p=/ethr#getResolver $args: - - networks: - - name: mainnet - rpcUrl: https://mainnet.infura.io/v3/5ffc47f65c4042ce847ef66a3fa70d4c - - name: rinkeby - rpcUrl: https://rinkeby.infura.io/v3/5ffc47f65c4042ce847ef66a3fa70d4c - - name: ropsten - rpcUrl: https://ropsten.infura.io/v3/5ffc47f65c4042ce847ef66a3fa70d4c - - name: kovan - rpcUrl: https://kovan.infura.io/v3/5ffc47f65c4042ce847ef66a3fa70d4c - - name: goerli - rpcUrl: https://goerli.infura.io/v3/5ffc47f65c4042ce847ef66a3fa70d4c - - name: private - rpcUrl: http://localhost:8545/ - registry: '0x05cc574b19a3c11308f761b3d7263bd8608bc532' + - infuraProjectId: 5ffc47f65c4042ce847ef66a3fa70d4c web-did-resolver: $require: web-did-resolver?t=function&p=/web#getResolver @@ -207,12 +194,17 @@ keyManager: $require: '@veramo/data-store#KeyStore' $args: - $ref: /dbConnection - - $require: '@veramo/kms-local#SecretBox' - $args: - - $ref: /constants/secretKey kms: local: $require: '@veramo/kms-local#KeyManagementSystem' + $args: + - keyStore: + $require: '@veramo/data-store#PrivateKeyStore' + $args: + - $ref: /dbConnection + - $require: '@veramo/kms-local#SecretBox' + $args: + - $ref: /constants/secretKey # DID Manager didManager: diff --git a/packages/cli/src/migrations/SecretBox1588075773000.ts b/packages/cli/src/migrations/SecretBox1588075773000.ts index 5da40981ba..7693cb4b27 100644 --- a/packages/cli/src/migrations/SecretBox1588075773000.ts +++ b/packages/cli/src/migrations/SecretBox1588075773000.ts @@ -9,8 +9,8 @@ export class SecretBox1588075773000 implements MigrationInterface { const secretBox = new SecretBox(process.env.VERAMO_SECRET_KEY) const keys = await queryRunner.connection.getRepository(Key).find() for (const key of keys) { - if (key.privateKeyHex) { - key.privateKeyHex = await secretBox.encrypt(key.privateKeyHex) + if ((key).privateKeyHex) { + (key).privateKeyHex = await secretBox.encrypt((key).privateKeyHex) } await key.save() } @@ -23,8 +23,8 @@ export class SecretBox1588075773000 implements MigrationInterface { const secretBox = new SecretBox(process.env.VERAMO_SECRET_KEY) const keys = await queryRunner.connection.getRepository(Key).find() for (const key of keys) { - if (key.privateKeyHex) { - key.privateKeyHex = await secretBox.decrypt(key.privateKeyHex) + if ((key).privateKeyHex) { + (key).privateKeyHex = await secretBox.decrypt((key).privateKeyHex) } await key.save() } diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts index 7e52d99d64..190b73f3c8 100644 --- a/packages/cli/src/setup.ts +++ b/packages/cli/src/setup.ts @@ -19,7 +19,7 @@ export const getConfig = (fileName: string): any => { const config = yaml.parse(fs.readFileSync(fileName).toString()) - if (config?.version != 2) { + if (config?.version != 3) { console.log('Unsupported configuration file version:', config.version) process.exit(1) } diff --git a/packages/core/plugin.schema.json b/packages/core/plugin.schema.json index 6f243ca487..09655d5e2c 100644 --- a/packages/core/plugin.schema.json +++ b/packages/core/plugin.schema.json @@ -434,7 +434,7 @@ } } }, - "IKey": { + "ManagedKeyInfo": { "type": "object", "properties": { "kid": { @@ -453,10 +453,6 @@ "type": "string", "description": "Public key" }, - "privateKeyHex": { - "type": "string", - "description": "Optional. Private key" - }, "meta": { "anyOf": [ { @@ -475,7 +471,7 @@ "type", "publicKeyHex" ], - "description": "Cryptographic key" + "description": "Represents information about a managed key. Private or secret key material is not present." }, "IKeyManagerDecryptJWEArgs": { "type": "object", @@ -578,6 +574,89 @@ ], "description": "Input arguments for {@link IKeyManager.keyManagerGet | keyManagerGet }" }, + "IKey": { + "type": "object", + "properties": { + "kid": { + "type": "string", + "description": "Key ID" + }, + "kms": { + "type": "string", + "description": "Key Management System" + }, + "type": { + "$ref": "#/components/schemas/TKeyType", + "description": "Key type" + }, + "publicKeyHex": { + "type": "string", + "description": "Public key" + }, + "privateKeyHex": { + "type": "string", + "description": "Optional. Private key" + }, + "meta": { + "anyOf": [ + { + "$ref": "#/components/schemas/KeyMetadata" + }, + { + "type": "null" + } + ], + "description": "Optional. Key metadata. This should be used to determine which algorithms are supported." + } + }, + "required": [ + "kid", + "kms", + "type", + "publicKeyHex" + ], + "description": "Cryptographic key" + }, + "MinimalImportableKey": { + "$ref": "#/components/schemas/RequireOnly", + "description": "Represents the properties required to import a key." + }, + "RequireOnly": { + "type": "object", + "properties": { + "kid": { + "type": "string", + "description": "Key ID" + }, + "kms": { + "type": "string", + "description": "Key Management System" + }, + "type": { + "$ref": "#/components/schemas/TKeyType", + "description": "Key type" + }, + "publicKeyHex": { + "type": "string", + "description": "Public key" + }, + "privateKeyHex": { + "type": "string", + "description": "Optional. Private key" + }, + "meta": { + "anyOf": [ + { + "$ref": "#/components/schemas/KeyMetadata" + }, + { + "type": "null" + } + ], + "description": "Optional. Key metadata. This should be used to determine which algorithms are supported." + } + } + }, "IKeyManagerSharedSecretArgs": { "type": "object", "properties": { @@ -733,7 +812,7 @@ "$ref": "#/components/schemas/IKeyManagerCreateArgs" }, "returnType": { - "$ref": "#/components/schemas/IKey" + "$ref": "#/components/schemas/ManagedKeyInfo" } }, "keyManagerDecryptJWE": { @@ -787,10 +866,10 @@ "keyManagerImport": { "description": "Imports a created key", "arguments": { - "$ref": "#/components/schemas/IKey" + "$ref": "#/components/schemas/MinimalImportableKey" }, "returnType": { - "type": "boolean" + "$ref": "#/components/schemas/ManagedKeyInfo" } }, "keyManagerSharedSecret": { @@ -1115,6 +1194,85 @@ ], "description": "Input arguments for {@link IDIDManager.didManagerGetOrCreate | didManagerGetOrCreate }" }, + "MinimalImportableIdentifier": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "Decentralized identifier" + }, + "alias": { + "type": "string", + "description": "Optional. Identifier alias. Can be used to reference an object in an external system" + }, + "provider": { + "type": "string", + "description": "Identifier provider name" + }, + "controllerKeyId": { + "type": "string", + "description": "Controller key id" + }, + "keys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MinimalImportableKey" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IService" + } + } + }, + "required": [ + "did", + "keys", + "provider" + ], + "description": "Represents the minimum amount of information needed to import an {@link IIdentifier }" + }, + "MinimalImportableKey": { + "$ref": "#/components/schemas/RequireOnly", + "description": "Represents the properties required to import a key." + }, + "RequireOnly": { + "type": "object", + "properties": { + "kid": { + "type": "string", + "description": "Key ID" + }, + "kms": { + "type": "string", + "description": "Key Management System" + }, + "type": { + "$ref": "#/components/schemas/TKeyType", + "description": "Key type" + }, + "publicKeyHex": { + "type": "string", + "description": "Public key" + }, + "privateKeyHex": { + "type": "string", + "description": "Optional. Private key" + }, + "meta": { + "anyOf": [ + { + "$ref": "#/components/schemas/KeyMetadata" + }, + { + "type": "null" + } + ], + "description": "Optional. Key metadata. This should be used to determine which algorithms are supported." + } + } + }, "IDIDManagerRemoveKeyArgs": { "type": "object", "properties": { @@ -1269,7 +1427,7 @@ "didManagerImport": { "description": "Imports identifier", "arguments": { - "$ref": "#/components/schemas/IIdentifier" + "$ref": "#/components/schemas/MinimalImportableIdentifier" }, "returnType": { "$ref": "#/components/schemas/IIdentifier" diff --git a/packages/core/src/types/IDIDManager.ts b/packages/core/src/types/IDIDManager.ts index 5290d4fb55..61dca4b125 100644 --- a/packages/core/src/types/IDIDManager.ts +++ b/packages/core/src/types/IDIDManager.ts @@ -1,5 +1,5 @@ import { IPluginMethodMap, IAgentContext } from './IAgent' -import { IIdentifier, IService, IKey } from './IIdentifier' +import { IIdentifier, IService, IKey, MinimalImportableIdentifier } from './IIdentifier' import { IKeyManager } from './IKeyManager' /** @@ -303,7 +303,7 @@ export interface IDIDManager extends IPluginMethodMap { /** * Imports identifier */ - didManagerImport(args: IIdentifier, context: IAgentContext): Promise + didManagerImport(args: MinimalImportableIdentifier, context: IAgentContext): Promise /** * Deletes identifier diff --git a/packages/core/src/types/IIdentifier.ts b/packages/core/src/types/IIdentifier.ts index b19d2b6066..4e6734fb25 100644 --- a/packages/core/src/types/IIdentifier.ts +++ b/packages/core/src/types/IIdentifier.ts @@ -1,3 +1,5 @@ +import { MinimalImportableKey } from './IKeyManager' + /** * Identifier interface * @public @@ -34,6 +36,14 @@ export interface IIdentifier { services: IService[] } +/** + * Represents the minimum amount of information needed to import an {@link IIdentifier} + */ +export type MinimalImportableIdentifier = { + keys: MinimalImportableKey[] + services?: IService[] +} & Omit + /** * Cryptographic key type * @public diff --git a/packages/core/src/types/IKeyManager.ts b/packages/core/src/types/IKeyManager.ts index 3a903f8a50..ab47e61359 100644 --- a/packages/core/src/types/IKeyManager.ts +++ b/packages/core/src/types/IKeyManager.ts @@ -1,6 +1,22 @@ import { IPluginMethodMap } from './IAgent' import { TKeyType, IKey, KeyMetadata } from './IIdentifier' +/** + * Represents an object type where a subset of keys are required and everything else is optional. + */ +export type RequireOnly = Required> & Partial + +/** + * Represents the properties required to import a key. + */ +export type MinimalImportableKey = RequireOnly + +/** + * Represents information about a managed key. + * Private or secret key material is not present. + */ +export type ManagedKeyInfo = Omit + /** * Input arguments for {@link IKeyManager.keyManagerCreate | keyManagerCreate} * @public @@ -175,7 +191,7 @@ export interface IKeyManager extends IPluginMethodMap { /** * Creates and returns a new key */ - keyManagerCreate(args: IKeyManagerCreateArgs): Promise + keyManagerCreate(args: IKeyManagerCreateArgs): Promise /** * Returns an existing key @@ -190,7 +206,7 @@ export interface IKeyManager extends IPluginMethodMap { /** * Imports a created key */ - keyManagerImport(args: IKey): Promise + keyManagerImport(args: MinimalImportableKey): Promise /** * Generates a signature according to the algorithm specified. diff --git a/packages/data-store/src/data-store-orm.ts b/packages/data-store/src/data-store-orm.ts index 241b215c3b..70f9d3bf7b 100644 --- a/packages/data-store/src/data-store-orm.ts +++ b/packages/data-store/src/data-store-orm.ts @@ -5,6 +5,7 @@ import { VerifiablePresentation, IPluginMethodMap, IIdentifier, + IKey, } from '@veramo/core' import { Message, createMessage } from './entities/message' import { Claim } from './entities/claim' @@ -37,7 +38,7 @@ import { FindArgs, } from './types' -import { schema } from './' +import { Key, schema } from './' interface IContext { authenticatedDid?: string @@ -130,7 +131,7 @@ export class DataStoreORM implements IAgentPlugin { ): Promise { const identifiers = await (await this.identifiersQuery(args, context)).getMany() return identifiers.map((i) => { - const identifier: PartialIdentifier = i + const identifier: PartialIdentifier = i as PartialIdentifier if (identifier.controllerKeyId === null) { delete identifier.controllerKeyId } @@ -140,7 +141,7 @@ export class DataStoreORM implements IAgentPlugin { if (identifier.provider === null) { delete identifier.provider } - return identifier + return identifier as IIdentifier }) } diff --git a/packages/data-store/src/entities/key.ts b/packages/data-store/src/entities/key.ts index ef70f1d576..d856939e67 100644 --- a/packages/data-store/src/entities/key.ts +++ b/packages/data-store/src/entities/key.ts @@ -22,8 +22,8 @@ export class Key extends BaseEntity { //@ts-ignore publicKeyHex: string - @Column({ nullable: true }) - privateKeyHex?: string + // @Column({ nullable: true }) + // privateKeyHex?: string @Column({ type: 'simple-json', diff --git a/packages/data-store/src/entities/private-key.ts b/packages/data-store/src/entities/private-key.ts new file mode 100644 index 0000000000..3e2662f0b9 --- /dev/null +++ b/packages/data-store/src/entities/private-key.ts @@ -0,0 +1,20 @@ +import { KeyMetadata, TKeyType } from '@veramo/core' +import { Entity, Column, PrimaryColumn, BaseEntity, ManyToOne, ManyToMany } from 'typeorm' +import { Identifier } from './identifier' + +export type KeyType = TKeyType + +@Entity('private-key') +export class PrivateKey extends BaseEntity { + @PrimaryColumn() + //@ts-ignore + alias: string + + @Column() + //@ts-ignore + type: KeyType + + @Column() + //@ts-ignore + privateKeyHex: string +} diff --git a/packages/data-store/src/identifier/did-store.ts b/packages/data-store/src/identifier/did-store.ts index 15b16651e5..94027e95d1 100644 --- a/packages/data-store/src/identifier/did-store.ts +++ b/packages/data-store/src/identifier/did-store.ts @@ -1,4 +1,4 @@ -import { IIdentifier } from '@veramo/core' +import { IIdentifier, IKey } from '@veramo/core' import { AbstractDIDStore } from '@veramo/did-manager' import { Identifier } from '../entities/identifier' import { Key } from '../entities/key' @@ -48,7 +48,7 @@ export class DIDStore extends AbstractDIDStore { kms: k.kms, publicKeyHex: k.publicKeyHex, meta: k.meta, - })), + } as IKey)), } if (identifier.alias) { result.alias = identifier.alias @@ -81,7 +81,7 @@ export class DIDStore extends AbstractDIDStore { const key = new Key() key.kid = argsKey.kid key.publicKeyHex = argsKey.publicKeyHex - key.privateKeyHex = argsKey.privateKeyHex + // key.privateKeyHex = argsKey.privateKeyHex key.kms = argsKey.kms key.meta = argsKey.meta identifier.keys.push(key) @@ -117,7 +117,7 @@ export class DIDStore extends AbstractDIDStore { if (i.alias === null) { delete i.alias } - return i + return i as IIdentifier }) } } diff --git a/packages/data-store/src/identifier/key-store.ts b/packages/data-store/src/identifier/key-store.ts index 64aed60ca8..05ce3779e8 100644 --- a/packages/data-store/src/identifier/key-store.ts +++ b/packages/data-store/src/identifier/key-store.ts @@ -1,4 +1,4 @@ -import { IKey } from '@veramo/core' +import { IKey, ManagedKeyInfo } from '@veramo/core' import { AbstractKeyStore, AbstractSecretBox } from '@veramo/key-manager' import { Connection } from 'typeorm' @@ -8,20 +8,17 @@ import Debug from 'debug' const debug = Debug('veramo:typeorm:key-store') export class KeyStore extends AbstractKeyStore { - constructor(private dbConnection: Promise, private secretBox?: AbstractSecretBox) { + constructor(private dbConnection: Promise) { super() - if (!secretBox) { - console.warn('Please provide SecretBox to the KeyStore') - } } async get({ kid }: { kid: string }): Promise { const key = await (await this.dbConnection).getRepository(Key).findOne(kid) if (!key) throw Error('Key not found') - if (this.secretBox && key.privateKeyHex) { - key.privateKeyHex = await this.secretBox.decrypt(key.privateKeyHex) - } - return key + // if (this.secretBox && key.privateKeyHex) { + // key.privateKeyHex = await this.secretBox.decrypt(key.privateKeyHex) + // } + return key as IKey } async delete({ kid }: { kid: string }) { @@ -35,10 +32,10 @@ export class KeyStore extends AbstractKeyStore { async import(args: IKey) { const key = new Key() key.kid = args.kid - key.privateKeyHex = args.privateKeyHex - if (this.secretBox && key.privateKeyHex) { - key.privateKeyHex = await this.secretBox.encrypt(key.privateKeyHex) - } + // key.privateKeyHex = args.privateKeyHex + // if (this.secretBox && key.privateKeyHex) { + // key.privateKeyHex = await this.secretBox.encrypt(key.privateKeyHex) + // } key.publicKeyHex = args.publicKeyHex key.type = args.type key.kms = args.kms @@ -47,4 +44,13 @@ export class KeyStore extends AbstractKeyStore { await (await this.dbConnection).getRepository(Key).save(key) return true } + + async list(args: {} = {}): Promise { + const keys = await (await this.dbConnection).getRepository(Key).find() + const managedKeys: ManagedKeyInfo[] = keys.map((key) => { + const { kid, publicKeyHex, type, meta, kms } = key + return { kid, publicKeyHex, type, meta, kms } as IKey + }) + return managedKeys + } } diff --git a/packages/data-store/src/identifier/private-key-store.ts b/packages/data-store/src/identifier/private-key-store.ts new file mode 100644 index 0000000000..d986c83cec --- /dev/null +++ b/packages/data-store/src/identifier/private-key-store.ts @@ -0,0 +1,56 @@ +import { AbstractSecretBox, AbstractPrivateKeyStore } from '@veramo/key-manager' +import { Connection } from 'typeorm' +import { ImportablePrivateKey, ManagedPrivateKey } from '@veramo/key-manager/src/abstract-private-key-store' +import { PrivateKey } from '../entities/private-key' +import { v4 as uuid4} from 'uuid' +import Debug from 'debug' +const debug = Debug('veramo:typeorm:key-store') + +export class PrivateKeyStore extends AbstractPrivateKeyStore { + constructor(private dbConnection: Promise, private secretBox?: AbstractSecretBox) { + super() + if (!secretBox) { + console.warn('Please provide SecretBox to the KeyStore') + } + } + + async get({ alias }: { alias: string }): Promise { + const key = await (await this.dbConnection).getRepository(PrivateKey).findOne(alias) + if (!key) throw Error('Key not found') + if (this.secretBox && key.privateKeyHex) { + key.privateKeyHex = await this.secretBox.decrypt(key.privateKeyHex) + } + return key as ManagedPrivateKey + } + + async delete({ alias }: { alias: string }) { + const key = await (await this.dbConnection).getRepository(PrivateKey).findOne(alias) + if (!key) throw Error(`not_found: Private Key data not found for alias=${alias}`) + debug('Deleting private key data', alias) + await (await this.dbConnection).getRepository(PrivateKey).remove(key) + return true + } + + async import(args: ImportablePrivateKey): Promise { + const key = new PrivateKey() + key.alias = args.alias || uuid4() + key.privateKeyHex = args.privateKeyHex + if (this.secretBox && key.privateKeyHex) { + key.privateKeyHex = await this.secretBox.encrypt(key.privateKeyHex) + } + key.type = args.type + debug('Saving private key data', args.alias) + const keyRepo = await (await this.dbConnection).getRepository(PrivateKey) + const existingKey = await keyRepo.findOne(key.alias) + if (existingKey && existingKey.privateKeyHex !== key.privateKeyHex) { + throw new Error(`key_already_exists: A key with this alias exists but with different data. Please use a different alias.`) + } + await keyRepo.save(key) + return key + } + + async list(): Promise> { + const keys = await (await this.dbConnection).getRepository(PrivateKey).find() + return keys + } +} diff --git a/packages/data-store/src/index.ts b/packages/data-store/src/index.ts index 8c1cc5f154..90c7439a56 100644 --- a/packages/data-store/src/index.ts +++ b/packages/data-store/src/index.ts @@ -11,6 +11,7 @@ export { DIDStore } from './identifier/did-store' export { KeyStore } from './identifier/key-store' +export { PrivateKeyStore } from './identifier/private-key-store' export { DataStore } from './data-store' export { DataStoreORM, @@ -32,8 +33,9 @@ import { Credential } from './entities/credential' import { Presentation } from './entities/presentation' import { Service } from './entities/service' import { Message, MetaData } from './entities/message' -export const Entities = [Key, Identifier, Message, Claim, Credential, Presentation, Service] -export { KeyType, Key, Identifier, Message, Claim, Credential, Presentation, MetaData, Service } +import { PrivateKey } from './entities/private-key' +export const Entities = [Key, Identifier, Message, Claim, Credential, Presentation, Service, PrivateKey] +export { KeyType, Key, Identifier, Message, Claim, Credential, Presentation, MetaData, Service, PrivateKey } export { migrations } from './migrations' const schema = require('../plugin.schema.json') export { schema } diff --git a/packages/did-manager/src/id-manager.ts b/packages/did-manager/src/id-manager.ts index d5bf1036cd..997873ab8d 100644 --- a/packages/did-manager/src/id-manager.ts +++ b/packages/did-manager/src/id-manager.ts @@ -17,6 +17,9 @@ import { IDIDManagerFindArgs, IDIDManagerSetAliasArgs, schema, + MinimalImportableIdentifier, + IKey, + IService, } from '@veramo/core' import { AbstractDIDStore } from './abstract-identifier-store' @@ -140,12 +143,23 @@ export class DIDManager implements IAgentPlugin { return await this.store.import(identifier) } /** {@inheritDoc @veramo/core#IDIDManager.didManagerImport} */ - async didManagerImport(identifier: IIdentifier, context: IAgentContext): Promise { + async didManagerImport( + identifier: MinimalImportableIdentifier, + context: IAgentContext, + ): Promise { + const keys: IKey[] = [] for (const key of identifier.keys) { - await context.agent.keyManagerImport(key) + const importedKey = await context.agent.keyManagerImport(key) + keys.push(importedKey) } - await this.store.import(identifier) - return identifier + const services: IService[] = [...(identifier?.services || [])] + const importedDID = { + ...identifier, + keys, + services, + } + await this.store.import(importedDID) + return importedDID } /** {@inheritDoc @veramo/core#IDIDManager.didManagerDelete} */ diff --git a/packages/did-manager/src/memory-did-store.ts b/packages/did-manager/src/memory-did-store.ts index bd76bd452a..94c2396fdc 100644 --- a/packages/did-manager/src/memory-did-store.ts +++ b/packages/did-manager/src/memory-did-store.ts @@ -13,19 +13,19 @@ export class MemoryDIDStore extends AbstractDIDStore { alias: string provider: string }): Promise { - if (did !== undefined && alias === undefined) { - if (!this.identifiers[did]) throw Error('Identifier not found') + if (did && !alias) { + if (!this.identifiers[did]) throw Error(`not_found: IIdentifier not found with did=${did}`) return this.identifiers[did] - } else if (did === undefined && alias !== undefined && provider !== undefined) { + } else if (!did && alias && provider) { for (const key of Object.keys(this.identifiers)) { if (this.identifiers[key].alias === alias && this.identifiers[key].provider === provider) { return this.identifiers[key] } } } else { - throw Error('Get requires did or (alias and provider)') + throw Error('invalid_argument: Get requires did or (alias and provider)') } - throw Error('Identifier not found') + throw Error(`not_found: IIdentifier not found with alias=${alias} provider=${provider}`) } async delete({ did }: { did: string }) { diff --git a/packages/key-manager/package.json b/packages/key-manager/package.json index af1b4ffb61..e079d6f1b6 100644 --- a/packages/key-manager/package.json +++ b/packages/key-manager/package.json @@ -9,7 +9,13 @@ "extract-api": "yarn veramo dev generate-plugin-schema" }, "dependencies": { - "@veramo/core": "^2.1.0" + "@ethersproject/bytes": "^5.4.0", + "@ethersproject/strings": "^5.4.0", + "@ethersproject/transactions": "^5.4.0", + "@stablelib/ed25519": "^1.0.2", + "@veramo/core": "^2.1.0", + "did-jwt": "^5.6.2", + "uint8arrays": "2.1.8" }, "devDependencies": { "typescript": "4.3.5" diff --git a/packages/key-manager/src/__tests__/abstract-key-store.test.ts b/packages/key-manager/src/__tests__/abstract-key-store.test.ts index d270033330..fa7ecb4275 100644 --- a/packages/key-manager/src/__tests__/abstract-key-store.test.ts +++ b/packages/key-manager/src/__tests__/abstract-key-store.test.ts @@ -1,7 +1,17 @@ import { AbstractKeyStore } from '../abstract-key-store' -import { IKey } from '@veramo/core' +import { IKey, ManagedKeyInfo } from '@veramo/core' class MockKeyStore extends AbstractKeyStore { + async list(args: {}): Promise { + return [ + { + kid: '', + kms: '', + type: 'Ed25519', + publicKeyHex: '', + }, + ] + } async get({ kid }: { kid: string }): Promise { return { kid: '', diff --git a/packages/key-manager/src/abstract-key-management-system.ts b/packages/key-manager/src/abstract-key-management-system.ts index 2b535eb59d..a07a2bc99c 100644 --- a/packages/key-manager/src/abstract-key-management-system.ts +++ b/packages/key-manager/src/abstract-key-management-system.ts @@ -1,23 +1,25 @@ -import { IKey, TKeyType } from '@veramo/core' -import { arrayify, hexlify } from '@ethersproject/bytes' +import { IKey, ManagedKeyInfo, MinimalImportableKey, TKeyType } from '@veramo/core' +import { arrayify } from '@ethersproject/bytes' import { serialize } from '@ethersproject/transactions' import * as u8a from 'uint8arrays' export abstract class AbstractKeyManagementSystem { - abstract createKey(args: { type: TKeyType; meta?: any }): Promise> + abstract importKey(args: Exclude): Promise + abstract listKeys(): Promise> + abstract createKey(args: { type: TKeyType; meta?: any }): Promise abstract deleteKey(args: { kid: string }): Promise /**@deprecated please use `sign({key, alg: 'eth_signTransaction', data: arrayify(serialize(transaction))})` instead */ - async signEthTX({ key, transaction }: { key: IKey; transaction: object }): Promise { + async signEthTX({ key, transaction }: { key: Pick; transaction: object }): Promise { const { v, r, s, from, ...tx } = transaction const data = arrayify(serialize(tx)) const algorithm = 'eth_signTransaction' - const signedTxHexString = this.sign({ key, data, algorithm }) + const signedTxHexString = this.sign({ keyRef: key, data, algorithm }) return signedTxHexString } /**@deprecated please use `sign({key, data})` instead, with `Uint8Array` data */ - async signJWT({ key, data }: { key: IKey; data: string | Uint8Array }): Promise { + async signJWT({ key, data }: { key: Pick; data: string | Uint8Array }): Promise { let dataBytes: Uint8Array if (typeof data === 'string') { try { @@ -28,10 +30,18 @@ export abstract class AbstractKeyManagementSystem { } else { dataBytes = data } - return this.sign({ key, data: dataBytes }) + return this.sign({ keyRef: key, data: dataBytes }) } - abstract sign(args: { key: IKey; algorithm?: string; data: Uint8Array; [x: string]: any }): Promise + abstract sign(args: { + keyRef: Pick + algorithm?: string + data: Uint8Array + [x: string]: any + }): Promise - abstract sharedSecret(args: { myKey: IKey; theirKey: Pick }): Promise + abstract sharedSecret(args: { + myKeyRef: Pick + theirKey: Pick + }): Promise } diff --git a/packages/key-manager/src/abstract-key-store.ts b/packages/key-manager/src/abstract-key-store.ts index 23053278de..a238f9fd74 100644 --- a/packages/key-manager/src/abstract-key-store.ts +++ b/packages/key-manager/src/abstract-key-store.ts @@ -1,7 +1,8 @@ -import { IKey } from '@veramo/core' +import { IKey, ManagedKeyInfo } from '@veramo/core' export abstract class AbstractKeyStore { - abstract import(args: IKey): Promise + abstract import(args: Partial): Promise abstract get(args: { kid: string }): Promise abstract delete(args: { kid: string }): Promise + abstract list(args: {}): Promise> } diff --git a/packages/key-manager/src/abstract-private-key-store.ts b/packages/key-manager/src/abstract-private-key-store.ts new file mode 100644 index 0000000000..f036e188ff --- /dev/null +++ b/packages/key-manager/src/abstract-private-key-store.ts @@ -0,0 +1,16 @@ +import { ManagedKeyInfo, RequireOnly, TKeyType } from '@veramo/core' + +export interface ManagedPrivateKey { + alias: string + privateKeyHex: string + type: TKeyType +} + +export type ImportablePrivateKey = RequireOnly + +export abstract class AbstractPrivateKeyStore { + abstract import(args: ImportablePrivateKey): Promise + abstract get(args: { alias: string }): Promise + abstract delete(args: { alias: string }): Promise + abstract list(args: {}): Promise> +} diff --git a/packages/key-manager/src/index.ts b/packages/key-manager/src/index.ts index f6f0451d09..a02677e7bb 100644 --- a/packages/key-manager/src/index.ts +++ b/packages/key-manager/src/index.ts @@ -7,5 +7,6 @@ export { KeyManager } from './key-manager' export { AbstractKeyManagementSystem } from './abstract-key-management-system' export { AbstractKeyStore } from './abstract-key-store' +export { AbstractPrivateKeyStore } from './abstract-private-key-store' export { AbstractSecretBox } from './abstract-secret-box' -export { MemoryKeyStore } from './memory-key-store' +export { MemoryKeyStore, MemoryPrivateKeyStore } from './memory-key-store' diff --git a/packages/key-manager/src/key-manager.ts b/packages/key-manager/src/key-manager.ts index 31f3329d0d..0417becb31 100644 --- a/packages/key-manager/src/key-manager.ts +++ b/packages/key-manager/src/key-manager.ts @@ -15,6 +15,8 @@ import { IKeyManagerSignArgs, IKeyManagerSharedSecretArgs, TKeyType, + MinimalImportableKey, + ManagedKeyInfo, } from '@veramo/core' import * as u8a from 'uint8arrays' import { JWE, createAnonDecrypter, createAnonEncrypter, createJWE, decryptJWE, ECDH } from 'did-jwt' @@ -61,7 +63,9 @@ export class KeyManager implements IAgentPlugin { private getKms(name: string): AbstractKeyManagementSystem { const kms = this.kms[name] - if (!kms) throw Error('KMS does not exist: ' + name) + if (!kms) { + throw Error(`invalid_argument: This agent has no registered KeyManagementSystem with name='${name}'`) + } return kms } @@ -71,7 +75,7 @@ export class KeyManager implements IAgentPlugin { } /** {@inheritDoc @veramo/core#IKeyManager.keyManagerCreate} */ - async keyManagerCreate(args: IKeyManagerCreateArgs): Promise { + async keyManagerCreate(args: IKeyManagerCreateArgs): Promise { const kms = this.getKms(args.kms) const partialKey = await kms.createKey({ type: args.type, meta: args.meta }) const key: IKey = { ...partialKey, kms: args.kms } @@ -99,9 +103,13 @@ export class KeyManager implements IAgentPlugin { } /** {@inheritDoc @veramo/core#IKeyManager.keyManagerImport} */ - async keyManagerImport(key: IKey): Promise { - //FIXME: check proper key properties and ask the actual KMS to import and fill in the missing meta data - return this.store.import(key) + async keyManagerImport(key: MinimalImportableKey): Promise { + const kms = this.getKms(key.kms) + const managedKey = await kms.importKey(key) + const { meta } = key + const importedKey = { ...managedKey, meta: { ...meta, ...managedKey.meta }, kms: key.kms } + await this.store.import(importedKey) + return importedKey } /** {@inheritDoc @veramo/core#IKeyManager.keyManagerEncryptJWE} */ @@ -152,7 +160,7 @@ export class KeyManager implements IAgentPlugin { /** {@inheritDoc @veramo/core#IKeyManager.keyManagerSign} */ async keyManagerSign(args: IKeyManagerSignArgs): Promise { const { keyRef, data, algorithm, encoding, ...extras } = { encoding: 'utf-8', ...args } - const key = await this.store.get({ kid: keyRef }) + const keyInfo: ManagedKeyInfo = await this.store.get({ kid: keyRef }) let dataBytes if (typeof data === 'string') { if (encoding === 'base16' || encoding === 'hex') { @@ -164,8 +172,8 @@ export class KeyManager implements IAgentPlugin { } else { dataBytes = data } - const kms = this.getKms(key.kms) - return kms.sign({ key, algorithm, data: dataBytes, ...extras }) + const kms = this.getKms(keyInfo.kms) + return kms.sign({ keyRef: keyInfo, algorithm, data: dataBytes, ...extras }) } /** {@inheritDoc @veramo/core#IKeyManager.keyManagerSignEthTX} */ @@ -192,14 +200,14 @@ export class KeyManager implements IAgentPlugin { /** {@inheritDoc @veramo/core#IKeyManager.keyManagerSharedKey} */ async keyManagerSharedSecret(args: IKeyManagerSharedSecretArgs): Promise { const { secretKeyRef, publicKey } = args - const myKey = await this.store.get({ kid: secretKeyRef }) + const myKeyRef = await this.store.get({ kid: secretKeyRef }) const theirKey = publicKey if ( - myKey.type === theirKey.type || - (['Ed25519', 'X25519'].includes(myKey.type) && ['Ed25519', 'X25519'].includes(theirKey.type)) + myKeyRef.type === theirKey.type || + (['Ed25519', 'X25519'].includes(myKeyRef.type) && ['Ed25519', 'X25519'].includes(theirKey.type)) ) { - const kms = this.getKms(myKey.kms) - return kms.sharedSecret({ myKey, theirKey }) + const kms = this.getKms(myKeyRef.kms) + return kms.sharedSecret({ myKeyRef, theirKey }) } else { throw new Error('invalid_argument: the key types have to match to be able to compute a shared secret') } diff --git a/packages/key-manager/src/memory-key-store.ts b/packages/key-manager/src/memory-key-store.ts index 0483232245..c3f139fb69 100644 --- a/packages/key-manager/src/memory-key-store.ts +++ b/packages/key-manager/src/memory-key-store.ts @@ -1,5 +1,11 @@ import { IKey } from '@veramo/core' import { AbstractKeyStore } from './abstract-key-store' +import { + AbstractPrivateKeyStore, + ImportablePrivateKey, + ManagedPrivateKey, +} from './abstract-private-key-store' +import { v4 as uuidv4 } from 'uuid' export class MemoryKeyStore extends AbstractKeyStore { private keys: Record = {} @@ -19,4 +25,46 @@ export class MemoryKeyStore extends AbstractKeyStore { this.keys[args.kid] = { ...args } return true } + + async list(args: {}): Promise[]> { + const safeKeys = Object.values(this.keys).map((key) => { + const { privateKeyHex, ...safeKey } = key + return safeKey + }) + return safeKeys + } +} + +/** + * An implementation of {@link AbstractPrivateKeyStore} that holds everything in memory. + * + * This is usable by {@link @veramo/kms-local} to hold the private key data. + */ +export class MemoryPrivateKeyStore extends AbstractPrivateKeyStore { + private privateKeys: Record = {} + + async get({ alias }: { alias: string }): Promise { + const key = this.privateKeys[alias] + if (!key) throw Error(`not_found: PrivateKey not found for alias=${alias}`) + return key + } + + async delete({ alias }: { alias: string }) { + delete this.privateKeys[alias] + return true + } + + async import(args: ImportablePrivateKey) { + const alias = args.alias || uuidv4() + const existingEntry = this.privateKeys[alias] + if (existingEntry && existingEntry.privateKeyHex !== args.privateKeyHex) { + throw new Error('key_already_exists: key exists with different data, please use a different alias') + } + this.privateKeys[alias] = { ...args, alias } + return this.privateKeys[alias] + } + + async list(): Promise> { + return [...Object.values(this.privateKeys)] + } } diff --git a/packages/kms-local/src/__tests__/kms-local.test.ts b/packages/kms-local/src/__tests__/kms-local.test.ts index 2325e24a90..f5f8e5c371 100644 --- a/packages/kms-local/src/__tests__/kms-local.test.ts +++ b/packages/kms-local/src/__tests__/kms-local.test.ts @@ -1,100 +1,101 @@ import { KeyManagementSystem } from '../key-management-system' -import { IKey, TKeyType } from '@veramo/core' +import { TKeyType } from '@veramo/core' +import { MemoryPrivateKeyStore } from '@veramo/key-manager/src' describe('@veramo/kms-local', () => { it('should compute a shared secret Ed+Ed', async () => { - const kms = new KeyManagementSystem() + const kms = new KeyManagementSystem({ keyStore: new MemoryPrivateKeyStore() }) const myKey = { - type: 'Ed25519', + type: 'Ed25519', privateKeyHex: 'ed3991fa33d4df22c88b78249e4d73c509c640a873a66808ad5dce774334ce94ee5072bc20355b4cd5499e04ee70853591bffa1874b1b5467dedd648d5b89ecb', - } as IKey + } const theirKey = { type: 'Ed25519', publicKeyHex: 'e1d1dc2afe59bb054c44ba23ba07561d15ba83f9d1c42568ac11351fbdfd87c6', } - - const secret = await kms.sharedSecret({ myKey, theirKey }) + const myKeyRef = await kms.importKey(myKey) + const secret = await kms.sharedSecret({ myKeyRef, theirKey }) expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') }) it('should compute a shared secret Ed+X', async () => { - const kms = new KeyManagementSystem() + const kms = new KeyManagementSystem({ keyStore: new MemoryPrivateKeyStore() }) const myKey = { - type: 'Ed25519', + type: 'Ed25519', privateKeyHex: 'ed3991fa33d4df22c88b78249e4d73c509c640a873a66808ad5dce774334ce94ee5072bc20355b4cd5499e04ee70853591bffa1874b1b5467dedd648d5b89ecb', - } as IKey + } const theirKey = { type: 'X25519', publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', } - - const secret = await kms.sharedSecret({ myKey, theirKey }) + const myKeyRef = await kms.importKey(myKey) + const secret = await kms.sharedSecret({ myKeyRef, theirKey }) expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') }) it('should compute a shared secret X+Ed', async () => { - const kms = new KeyManagementSystem() + const kms = new KeyManagementSystem({ keyStore: new MemoryPrivateKeyStore() }) const myKey = { - type: 'X25519', + type: 'X25519', privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', - } as IKey + } const theirKey = { type: 'Ed25519', publicKeyHex: 'e1d1dc2afe59bb054c44ba23ba07561d15ba83f9d1c42568ac11351fbdfd87c6', } - - const secret = await kms.sharedSecret({ myKey, theirKey }) + const myKeyRef = await kms.importKey(myKey) + const secret = await kms.sharedSecret({ myKeyRef, theirKey }) expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') }) it('should compute a shared secret X+X', async () => { - const kms = new KeyManagementSystem() + const kms = new KeyManagementSystem({ keyStore: new MemoryPrivateKeyStore() }) const myKey = { - type: 'X25519', + type: 'X25519', privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', - } as IKey + } const theirKey = { type: 'X25519', publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', } - - const secret = await kms.sharedSecret({ myKey, theirKey }) + const myKeyRef = await kms.importKey(myKey) + const secret = await kms.sharedSecret({ myKeyRef, theirKey }) expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') }) it('should throw on invalid myKey type', async () => { expect.assertions(1) - const kms = new KeyManagementSystem() + const kms = new KeyManagementSystem({ keyStore: new MemoryPrivateKeyStore() }) const myKey = { - type: 'Secp256k1', + type: 'Secp256k1', privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', - } as IKey + } const theirKey = { type: 'X25519', publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', } - - expect(kms.sharedSecret({ myKey, theirKey })).rejects.toThrow('not_supported') + const myKeyRef = await kms.importKey(myKey) + expect(kms.sharedSecret({ myKeyRef, theirKey })).rejects.toThrow('not_supported') }) it('should throw on invalid theirKey type', async () => { expect.assertions(1) - const kms = new KeyManagementSystem() + const kms = new KeyManagementSystem({ keyStore: new MemoryPrivateKeyStore() }) const myKey = { - type: 'X25519', + type: 'X25519', privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', - } as IKey - + } + const myKeyRef = await kms.importKey(myKey) const theirKey = { type: 'Secp256k1', publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', } - expect(kms.sharedSecret({ myKey, theirKey })).rejects.toThrow('not_supported') + expect(kms.sharedSecret({ myKeyRef, theirKey })).rejects.toThrow('not_supported') }) }) diff --git a/packages/kms-local/src/key-management-system.ts b/packages/kms-local/src/key-management-system.ts index 2848dedb52..65acaae410 100644 --- a/packages/kms-local/src/key-management-system.ts +++ b/packages/kms-local/src/key-management-system.ts @@ -1,8 +1,20 @@ -import { TKeyType, IKey } from '@veramo/core' -import { AbstractKeyManagementSystem } from '@veramo/key-manager' +import { TKeyType, IKey, ManagedKeyInfo, MinimalImportableKey, RequireOnly } from '@veramo/core' +import { AbstractKeyManagementSystem, AbstractPrivateKeyStore } from '@veramo/key-manager' +// FIXME: weird import +import { ManagedPrivateKey } from '@veramo/key-manager/src/abstract-private-key-store' + import { EdDSASigner, ES256KSigner } from 'did-jwt' -import { generateKeyPair, convertPublicKeyToX25519, convertSecretKeyToX25519 } from '@stablelib/ed25519' -import { generateKeyPair as generateEncryptionKeypair, sharedKey } from '@stablelib/x25519' +import { + generateKeyPair as generateSigningKeyPair, + convertPublicKeyToX25519, + convertSecretKeyToX25519, + extractPublicKeyFromSecretKey, +} from '@stablelib/ed25519' +import { + generateKeyPair as generateEncryptionKeypair, + generateKeyPairFromSeed as generateEncryptionKeyPairFromSeed, + sharedKey, +} from '@stablelib/x25519' import { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' import { TransactionRequest } from '@ethersproject/abstract-provider' import { toUtf8String } from '@ethersproject/strings' @@ -11,54 +23,60 @@ import { Wallet } from '@ethersproject/wallet' import { SigningKey } from '@ethersproject/signing-key' import { randomBytes } from '@ethersproject/random' import { arrayify, hexlify } from '@ethersproject/bytes' +import * as u8a from 'uint8arrays' import Debug from 'debug' const debug = Debug('veramo:kms:local') export class KeyManagementSystem extends AbstractKeyManagementSystem { - async createKey({ type }: { type: TKeyType }): Promise> { - let key: Omit + private readonly keyStore: AbstractPrivateKeyStore + + constructor(args: { keyStore: AbstractPrivateKeyStore }) { + super() + this.keyStore = args.keyStore + } + + async importKey(args: Omit): Promise { + if (!args.type || !args.privateKeyHex) { + throw new Error('invalid_argument: type and privateKeyHex are required to import a key') + } + const managedKey = this.asManagedKeyInfo({ alias: args.kid, ...args }) + await this.keyStore.import({ alias: managedKey.kid, ...args }) + debug('imported key', managedKey.type, managedKey.publicKeyHex) + return managedKey + } + + async listKeys(): Promise { + const privateKeys = await this.keyStore.list({}) + const managedKeys = privateKeys.map((key) => this.asManagedKeyInfo(key)) + return managedKeys + } + + async createKey({ type }: { type: TKeyType }): Promise { + let key: ManagedKeyInfo switch (type) { case 'Ed25519': { - const keyPairEd25519 = generateKeyPair() - key = { + const keyPairEd25519 = generateSigningKeyPair() + key = await this.importKey({ type, - kid: Buffer.from(keyPairEd25519.publicKey).toString('hex'), - publicKeyHex: Buffer.from(keyPairEd25519.publicKey).toString('hex'), - privateKeyHex: Buffer.from(keyPairEd25519.secretKey).toString('hex'), - meta: { - algorithms: ['Ed25519', 'EdDSA'], - }, - } + privateKeyHex: u8a.toString(keyPairEd25519.secretKey, 'base16'), + }) break } case 'Secp256k1': { const privateBytes = randomBytes(32) - const keyPair = new SigningKey(privateBytes) - const publicKeyHex = keyPair.publicKey.substring(2) - const privateKeyHex = keyPair.privateKey.substring(2) - key = { + key = await this.importKey({ type, - kid: publicKeyHex, - publicKeyHex, - privateKeyHex, - meta: { - algorithms: ['ES256K', 'ES256K-R', 'eth_signTransaction', 'eth_signTypedData', 'eth_signMessage'], - }, - } + privateKeyHex: u8a.toString(privateBytes, 'base16'), + }) break } case 'X25519': { const keyPairX25519 = generateEncryptionKeypair() - key = { + key = await this.importKey({ type, - kid: Buffer.from(keyPairX25519.publicKey).toString('hex'), - publicKeyHex: Buffer.from(keyPairX25519.publicKey).toString('hex'), - privateKeyHex: Buffer.from(keyPairX25519.secretKey).toString('hex'), - meta: { - algorithms: ['ECDH', 'ECDH-ES', 'ECDH-1PU'], - }, - } + privateKeyHex: u8a.toString(keyPairX25519.secretKey, 'base16'), + }) break } default: @@ -71,35 +89,66 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { } async deleteKey(args: { kid: string }) { - // this kms doesn't need to delete keys - return true + return await this.keyStore.delete({ alias: args.kid }) } - async sign({ key, algorithm, data }: { key: IKey; algorithm?: string; data: Uint8Array }): Promise { - //FIXME: KMS implementation should not rely on private keys being provided, but rather manage their own keys - if (!key.privateKeyHex) throw Error('No private key for kid: ' + key.kid) + async sign({ + keyRef, + algorithm, + data, + }: { + keyRef: Pick + algorithm?: string + data: Uint8Array + }): Promise { + let managedKey: ManagedPrivateKey + try { + managedKey = await this.keyStore.get({ alias: keyRef.kid }) + } catch (e) { + throw new Error(`key_not_found: No key entry found for kid=${keyRef.kid}`) + } if ( - key.type === 'Ed25519' && + managedKey.type === 'Ed25519' && (typeof algorithm === 'undefined' || ['Ed25519', 'EdDSA'].includes(algorithm)) ) { - return await this.signEdDSA(key.privateKeyHex, data) - } else if (key.type === 'Secp256k1') { + return await this.signEdDSA(managedKey.privateKeyHex, data) + } else if (managedKey.type === 'Secp256k1') { if (typeof algorithm === 'undefined' || ['ES256K', 'ES256K-R'].includes(algorithm)) { - return await this.signES256K(key.privateKeyHex, algorithm, data) + return await this.signES256K(managedKey.privateKeyHex, algorithm, data) } else if (['eth_signTransaction', 'signTransaction', 'signTx'].includes(algorithm)) { - return await this.eth_signTransaction(key.privateKeyHex, data) + return await this.eth_signTransaction(managedKey.privateKeyHex, data) } else if (algorithm === 'eth_signMessage') { - return await this.eth_signMessage(key.privateKeyHex, data) + return await this.eth_signMessage(managedKey.privateKeyHex, data) } else if (['eth_signTypedData', 'EthereumEip712Signature2021'].includes(algorithm)) { - return await this.eth_signTypedData(key.privateKeyHex, data) + return await this.eth_signTypedData(managedKey.privateKeyHex, data) } } - throw Error(`not_supported: Cannot sign ${algorithm} using key of type ${key.type}`) + throw Error(`not_supported: Cannot sign ${algorithm} using key of type ${managedKey.type}`) } - async sharedSecret(args: { myKey: IKey; theirKey: Pick }): Promise { - const { myKey, theirKey } = args + async sharedSecret(args: { + myKeyRef: Pick + theirKey: Pick + }): Promise { + let myKey: ManagedPrivateKey + try { + myKey = await this.keyStore.get({ alias: args.myKeyRef.kid }) + } catch (e) { + throw new Error(`key_not_found: No key entry found for kid=${args.myKeyRef.kid}`) + } + if (!myKey.privateKeyHex) { + throw Error('key_not_managed: No private key is available for kid: ' + myKey.alias) + } + let theirKey: Pick = args.theirKey + if ( + !theirKey.type || + typeof theirKey.type !== 'string' || + !theirKey.publicKeyHex || + typeof theirKey.publicKeyHex !== 'string' + ) { + throw new Error(`invalid_argument: args.theirKey must contain 'type' and 'publicKeyHex'`) + } let myKeyBytes = arrayify('0x' + myKey.privateKeyHex) if (myKey.type === 'Ed25519') { myKeyBytes = convertSecretKeyToX25519(myKeyBytes) @@ -199,6 +248,59 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { //base64url encoded string return signature as string } + + /** + * Converts a {@link ManagedPrivateKey} to {@link ManagedKeyInfo} + */ + private asManagedKeyInfo(args: RequireOnly): ManagedKeyInfo { + let key: Partial + switch (args.type) { + case 'Ed25519': { + const secretKey = u8a.fromString(args.privateKeyHex.toLowerCase(), 'base16') + const publicKeyHex = u8a.toString(extractPublicKeyFromSecretKey(secretKey), 'base16') + key = { + type: args.type, + kid: args.alias || publicKeyHex, + publicKeyHex, + meta: { + algorithms: ['Ed25519', 'EdDSA'], + }, + } + break + } + case 'Secp256k1': { + const privateBytes = u8a.fromString(args.privateKeyHex.toLowerCase(), 'base16') + const keyPair = new SigningKey(privateBytes) + const publicKeyHex = keyPair.publicKey.substring(2) + key = { + type: args.type, + kid: args.alias || publicKeyHex, + publicKeyHex, + meta: { + algorithms: ['ES256K', 'ES256K-R', 'eth_signTransaction', 'eth_signTypedData', 'eth_signMessage'], + }, + } + break + } + case 'X25519': { + const secretKeyBytes = u8a.fromString(args.privateKeyHex.toLowerCase(), 'base16') + const keyPairX25519 = generateEncryptionKeyPairFromSeed(secretKeyBytes) + const publicKeyHex = u8a.toString(keyPairX25519.publicKey, 'base16') + key = { + type: args.type, + kid: args.alias || publicKeyHex, + publicKeyHex: publicKeyHex, + meta: { + algorithms: ['ECDH', 'ECDH-ES', 'ECDH-1PU'], + }, + } + break + } + default: + throw Error('not_supported: Key type not supported: ' + args.type) + } + return key as ManagedKeyInfo + } } type Eip712Payload = { diff --git a/yarn.lock b/yarn.lock index a390a6994a..b0274d4d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4333,7 +4333,7 @@ did-jwt-vc@2.1.6: did-jwt "^5.6.1" did-resolver "^3.1.0" -did-jwt@5.6.2, did-jwt@^5.4.0, did-jwt@^5.6.1: +did-jwt@5.6.2, did-jwt@^5.4.0, did-jwt@^5.6.1, did-jwt@^5.6.2: version "5.6.2" resolved "https://registry.yarnpkg.com/did-jwt/-/did-jwt-5.6.2.tgz#6b84f91db614bd844b58b45e94fb93ac748bdcd1" integrity sha512-WNX6haTfgNZZrOLxyeGdxKUh7tQJ07jJCcWIvXaq+wms0UxA7XGbdcEuz9h3uJsuIE+/7a3HgAy1FjD1DTcfww== @@ -10129,6 +10129,13 @@ uid-number@0.0.6: resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= +uint8arrays@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.8.tgz#79394390ba93c7d858ce5703705dcf9012f0c9d4" + integrity sha512-qpZ/B88mSea11W3LvoimtnGWIC2i3gGuXby5wBkn8jY+OFulbaQwyjpOYSyrASqgcNEvKdAkLiOwiUt5cPSdcQ== + dependencies: + multiformats "^9.4.2" + uint8arrays@^2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.5.tgz#9e6e6377a9463d5eba4620a3f0450f7eb389a351"