diff --git a/__tests__/fixtures/local-database-before-3.0.sqlite b/__tests__/fixtures/local-database-before-3.0.sqlite new file mode 100644 index 000000000..3dca8b1fe Binary files /dev/null and b/__tests__/fixtures/local-database-before-3.0.sqlite differ diff --git a/__tests__/initial.migration.test.ts b/__tests__/initial.migration.test.ts index 1c3d8e400..f64285638 100644 --- a/__tests__/initial.migration.test.ts +++ b/__tests__/initial.migration.test.ts @@ -1,4 +1,4 @@ -import { createAgent, TAgent, IDIDManager, IResolver, IKeyManager, IDataStore } from '@veramo/core/src' +import { createAgent, TAgent, IDIDManager, IResolver, IKeyManager, IDataStore } from '../packages/core/src' import { DIDResolverPlugin } from '../packages/did-resolver/src' import { EthrDIDProvider } from '../packages/did-provider-ethr/src' import { WebDIDProvider } from '../packages/did-provider-web/src' @@ -13,13 +13,14 @@ import { KeyStore, DIDStore, migrations, + PrivateKeyStore, } from '../packages/data-store/src' -import { createConnection, Connection } from 'typeorm' import { getDidKeyResolver } from '../packages/did-provider-key/src' import { KeyManager } from '../packages/key-manager/src' import { DIDManager } from '../packages/did-manager/src' import { FakeDidProvider, FakeDidResolver } from './utils/fake-did' +import { createConnection, Connection } from 'typeorm' import { Resolver } from 'did-resolver' import { getResolver as ethrDidResolver } from 'ethr-did-resolver' import { getResolver as webDidResolver } from 'web-did-resolver' @@ -32,7 +33,7 @@ const databaseFile = __dirname + '/migrated1.database.sqlite' const infuraProjectId = '5ffc47f65c4042ce847ef66a3fa70d4c' const secretKey = '29739248cad1bd1a0fc4d9b75cd4d2990de535baf5caadfdf8d8f86664aa830c' -describe('database migration tests', () => { +describe('database initial migration tests', () => { describe('using pre-migration database fixture', () => { type TestingAgentPlugins = IDIDManager & IKeyManager & IDataStore & IDataStoreORM & IResolver & IDIDComm let agent: TAgent @@ -58,9 +59,9 @@ describe('database migration tests', () => { }, plugins: [ new KeyManager({ - store: new KeyStore(dbConnection, new SecretBox(secretKey)), + store: new KeyStore(dbConnection), kms: { - local: new KeyManagementSystem(), + local: new KeyManagementSystem(new PrivateKeyStore(dbConnection, new SecretBox(secretKey))), }, }), new DIDManager({ @@ -102,6 +103,7 @@ describe('database migration tests', () => { }) it('signs using a migrated key', async () => { + expect.assertions(2) // output of agent.keyManagerGet() before migration const key = { kid: '048bb0844ebbcf434048862008991b01cdebb564207f0cea08e5c8d925cec3542bb4c8c1630f38a6b05528ec7460139fe0978bf34a1e4ff32ec210bbaed98dddda', @@ -128,12 +130,16 @@ describe('database migration tests', () => { }) it('reads a credential by hash', async () => { - const cred = await agent.dataStoreGetVerifiableCredential({hash: '133b9636e2fe2b7a77b88ca5d81998773b8bc3ebe0b1f3f80dc419dfa0bb797bea779ba0946d603c3ea8611fee5148395894f327661531929294a61589d4d0e7'}) + const cred = await agent.dataStoreGetVerifiableCredential({ + hash: '133b9636e2fe2b7a77b88ca5d81998773b8bc3ebe0b1f3f80dc419dfa0bb797bea779ba0946d603c3ea8611fee5148395894f327661531929294a61589d4d0e7', + }) expect(cred.credentialSubject.name).toEqual('Alice') }) it('reads a presentation by hash', async () => { - const cred = await agent.dataStoreGetVerifiablePresentation({hash: '4cfe965596a0d343ff2cc02afd32068bced34caa2b1e7e3f253b23e420de106b58a613f06f55d9d9cbbdbe0b0f051a45d44404020b123c58f0ee48bdaeafdc90'}) + const cred = await agent.dataStoreGetVerifiablePresentation({ + hash: '4cfe965596a0d343ff2cc02afd32068bced34caa2b1e7e3f253b23e420de106b58a613f06f55d9d9cbbdbe0b0f051a45d44404020b123c58f0ee48bdaeafdc90', + }) expect(cred?.verifiableCredential?.[0]?.credentialSubject?.name).toEqual('Alice') }) @@ -144,7 +150,14 @@ describe('database migration tests', () => { it('reads existing message with attachments', async () => { const msgs = await agent.dataStoreORMGetMessages({ - where: [{column: 'id', value: ['13065b8bb97cd37410f4f43cfa878f396aa906701e70c7e2bb86c5de1fe1351a41fb05f445cb68b1ba2805858db619ddd26c71e30a0079c200843d52276213d8']}] + where: [ + { + column: 'id', + value: [ + '13065b8bb97cd37410f4f43cfa878f396aa906701e70c7e2bb86c5de1fe1351a41fb05f445cb68b1ba2805858db619ddd26c71e30a0079c200843d52276213d8', + ], + }, + ], }) expect(msgs[0]?.presentations?.length).toEqual(1) expect(msgs[0]?.credentials?.length).toEqual(1) @@ -152,7 +165,10 @@ describe('database migration tests', () => { it('reads a credential by claim', async () => { const creds = await agent.dataStoreORMGetVerifiableCredentialsByClaims({ - where: [{ column: 'type', value: ['name'] }, { column: 'value', value: ['Alice']}] + where: [ + { column: 'type', value: ['name'] }, + { column: 'value', value: ['Alice'] }, + ], }) expect(creds.length).toEqual(1) }) diff --git a/__tests__/keyMigration.test.ts b/__tests__/keyMigration.test.ts new file mode 100644 index 000000000..df4fe7e5d --- /dev/null +++ b/__tests__/keyMigration.test.ts @@ -0,0 +1,141 @@ +import { createAgent, TAgent, IDIDManager, IResolver, IKeyManager, IDataStore } from '../packages/core/src' +import { DIDResolverPlugin } from '../packages/did-resolver/src' +import { EthrDIDProvider } from '../packages/did-provider-ethr/src' +import { WebDIDProvider } from '../packages/did-provider-web/src' +import { KeyDIDProvider } from '../packages/did-provider-key/src' +import { DIDComm, IDIDComm } from '../packages/did-comm/src' +import { KeyManagementSystem, SecretBox } from '../packages/kms-local/src' +import { + Entities, + IDataStoreORM, + DataStore, + DataStoreORM, + KeyStore, + DIDStore, + PrivateKeyStore, + migrations, +} from '../packages/data-store/src' +import { getDidKeyResolver } from '../packages/did-provider-key/src' +import { KeyManager } from '../packages/key-manager/src' +import { DIDManager } from '../packages/did-manager/src' +import { FakeDidProvider, FakeDidResolver } from './utils/fake-did' + +import { createConnection, Connection } from 'typeorm' +import { Resolver } from 'did-resolver' +import { getResolver as ethrDidResolver } from 'ethr-did-resolver' +import { getResolver as webDidResolver } from 'web-did-resolver' +import fs from 'fs' + +jest.setTimeout(30000) + +const databaseBeforeFile = __dirname + '/fixtures/local-database-before-3.0.sqlite' +const databaseFile = __dirname + '/migrated.keys.database.sqlite' +const infuraProjectId = '5ffc47f65c4042ce847ef66a3fa70d4c' +const secretKey = '29739248cad1bd1a0fc4d9b75cd4d2990de535baf5caadfdf8d8f86664aa830c' + +describe('database private-key migration tests', () => { + describe('using pre-migration database fixture', () => { + type TestingAgentPlugins = IDIDManager & IKeyManager & IDataStore & IDataStoreORM & IResolver & IDIDComm + let agent: TAgent + let dbConnection: Promise + + beforeAll(async () => { + fs.copyFileSync(databaseBeforeFile, databaseFile) + + dbConnection = createConnection({ + name: 'key-migration-test', + type: 'sqlite', + database: databaseFile, + synchronize: false, + migrations: migrations, + migrationsRun: true, + logging: false, + entities: Entities, + }) + + agent = createAgent({ + context: { + // authenticatedDid: 'did:example:3456' + }, + plugins: [ + new KeyManager({ + store: new KeyStore(dbConnection), + kms: { + local: new KeyManagementSystem(new PrivateKeyStore(dbConnection, new SecretBox(secretKey))), + }, + }), + new DIDManager({ + store: new DIDStore(dbConnection), + defaultProvider: 'did:ethr:goerli', + providers: { + 'did:ethr:goerli': new EthrDIDProvider({ + defaultKms: 'local', + network: 'goerli', + rpcUrl: 'https://goerli.infura.io/v3/' + infuraProjectId, + }), + 'did:web': new WebDIDProvider({ + defaultKms: 'local', + }), + 'did:key': new KeyDIDProvider({ + defaultKms: 'local', + }), + 'did:fake': new FakeDidProvider(), + }, + }), + new DIDResolverPlugin({ + resolver: new Resolver({ + ...ethrDidResolver({ infuraProjectId }), + ...webDidResolver(), + ...getDidKeyResolver(), + ...new FakeDidResolver(() => agent).getDidFakeResolver(), + }), + }), + new DataStore(dbConnection), + new DataStoreORM(dbConnection), + new DIDComm(), + ], + }) + return true + }) + afterAll(async () => { + await (await dbConnection).close() + fs.unlinkSync(databaseFile) + }) + + it('loads a migrated key', async () => { + // output of agent.keyManagerGet() before migration + const key = { + kid: '04539ffde912c094bc48b64c9ee71b2baece24c710bcad2c7bacced615f60ae53949cdc95379eb50556d11cb0afab0e5a6ca8cb475d413b2f12307cc2d7f5438de', + kms: 'local', + type: 'Secp256k1', + publicKeyHex: + '04539ffde912c094bc48b64c9ee71b2baece24c710bcad2c7bacced615f60ae53949cdc95379eb50556d11cb0afab0e5a6ca8cb475d413b2f12307cc2d7f5438de', + privateKeyHex: 'a5e81a8cd50cf5c31d5b87db3e153e2817f86de350a60edc2335f76d5c3b4e0d', + meta: { + algorithms: ['ES256K', 'ES256K-R', 'eth_signTransaction', 'eth_signTypedData', 'eth_signMessage'], + }, + } + const migratedKey = await agent.keyManagerGet({ kid: key.kid }) + expect(migratedKey.kid).toEqual(key.kid) + expect(migratedKey).not.toHaveProperty('privateKeyHex') + const signedMessage = await agent.keyManagerSign({ + data: 'hello world', + keyRef: migratedKey.kid, + algorithm: 'ES256K', + encoding: 'utf-8', + }) + expect(signedMessage).toEqual( + 'vzDocUViJh7ooOCZ-jBHKZddEsTa4yClHwhIL9SHJwjAv3bC6TZIcUnX36ZqNBWvLbnNAQvdtzqrVf3l0pv3QQ', + ) + }) + + it('unpacks DIDComm message intended for migrated managed key', async () => { + const packed = { + message: + '{"protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiZW5jIjoiWEMyMFAifQ","iv":"mBAgYLce2JpmKtmlNQLG6w9lm6kqf4Ne","ciphertext":"D9_7Xxj51xn3T9yBU-rZmxSTrR82Pi4G7hWCDSxSpRUlmUh2uJoqeCHixSTFeZvFAfw2ryROjrxbpCh5Arg-wqrW3WwKGpVFHXO_r0jHso5lNMO-vGjxOULN","tag":"9Qs-esw1tcnM0jE_Q3LxIQ","recipients":[{"encrypted_key":"kGNaBfhPS2VETu-_iYaUwy13sC1ZVm3i_qYiYkuEleA","header":{"alg":"ECDH-ES+XC20PKW","iv":"1sK1pyOwy_hNY_WsJPGdoFqE8ken51IA","tag":"MplY66h-bHnuSdP1ZGLYyw","epk":{"kty":"OKP","crv":"X25519","x":"UZx8Uf3BJ-m3wm7sBjvCp1UXuHA9v0Qu5KvWfWyBNio"},"kid":"did:key:z6MkiPXoC2uAWPdQpotWxzNMJpaDbfPxaQWcbux5avNwEMfD#z6LSqL9zfeZa53RwpAkjxN7Gizvzv4rjAT7GwhLVYLPXK5dC"}}]}', + } + const msg = await agent.unpackDIDCommMessage(packed) + expect(msg.message.body).toEqual({ hello: 'world' }) + }) + }) +}) diff --git a/__tests__/localAgent.test.ts b/__tests__/localAgent.test.ts index 70d657f2c..17e0d3c52 100644 --- a/__tests__/localAgent.test.ts +++ b/__tests__/localAgent.test.ts @@ -35,6 +35,7 @@ import { DataStore, DataStoreORM, ProfileDiscoveryProvider, + PrivateKeyStore, migrations, } from '../packages/data-store/src' import { createConnection, Connection } from 'typeorm' @@ -91,7 +92,7 @@ const setup = async (options?: IAgentOptions): Promise => { logging: false, entities: Entities, // allow shared tests to override connection options - ...options?.context?.dbConnectionOptions + ...options?.context?.dbConnectionOptions, }) agent = createAgent< @@ -112,9 +113,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(new PrivateKeyStore(dbConnection, new SecretBox(secretKey))), }, }), new DIDManager({ diff --git a/__tests__/localMemoryStoreAgent.test.ts b/__tests__/localMemoryStoreAgent.test.ts index dc9b3a050..8f419db3f 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' @@ -95,7 +95,7 @@ const setup = async (options?: IAgentOptions): Promise => { new KeyManager({ store: new MemoryKeyStore(), kms: { - local: new KeyManagementSystem(), + local: new KeyManagementSystem(new MemoryPrivateKeyStore()), }, }), new DIDManager({ diff --git a/__tests__/restAgent.test.ts b/__tests__/restAgent.test.ts index aa6f9f8e8..26e0b3589 100644 --- a/__tests__/restAgent.test.ts +++ b/__tests__/restAgent.test.ts @@ -35,6 +35,7 @@ import { DataStore, DataStoreORM, ProfileDiscoveryProvider, + PrivateKeyStore, migrations, } from '../packages/data-store/src' import { createConnection, Connection } from 'typeorm' @@ -115,9 +116,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(new PrivateKeyStore(dbConnection, new SecretBox(secretKey))), }, }), new DIDManager({ diff --git a/__tests__/shared/didManager.ts b/__tests__/shared/didManager.ts index 789193036..d87f38676 100644 --- a/__tests__/shared/didManager.ts +++ b/__tests__/shared/didManager.ts @@ -268,13 +268,14 @@ export default (testContext: { }) it('should import identifier', async () => { - const imported: IIdentifier = { - did: 'did:web:example.org', - alias: 'example.org', + expect.assertions(1) + const did = 'did:web:imported.example' + const imported = await agent.didManagerImport({ + did, provider: 'did:web', services: [ { - id: 'did:web:example.org#msg', + id: `${did}#msg`, type: 'Messaging', serviceEndpoint: 'https://example.org/messaging', description: 'Handles incoming messages', @@ -282,12 +283,18 @@ export default (testContext: { ], keys: [ { - kid: '0405debbb55a873648ca545f810e44edd1997688eb8e46b1fdd6842bd83e300b16fd8258a3ca19f5d858a5ab0c9ba8381b0bf00727be1154e5bbd3a4da5f186af6', kms: 'local', + privateKeyHex: 'e63886b5ba367dc2aff9acea6d955ee7c39115f12eaf2aa6b1a2eaa852036668', type: 'Secp256k1', - publicKeyHex: - '0405debbb55a873648ca545f810e44edd1997688eb8e46b1fdd6842bd83e300b16fd8258a3ca19f5d858a5ab0c9ba8381b0bf00727be1154e5bbd3a4da5f186af6', - privateKeyHex: '55e67c7f7b4c25657d3144fa7ca3cf842790906ce24176b02b927b3aebffab50', + }, + ], + }) + expect(imported).toEqual({ + did, + keys: [ + { + kid: '04dd467afb12bdb797303e7f3f0c8cd0ba80d518dc4e339e0e2eb8f2d99a9415cac537854a30d31a854b7af0b4fcb54c3954047390fa9500d3cc2e15a3e09017bb', + kms: 'local', meta: { algorithms: [ 'ES256K', @@ -297,28 +304,21 @@ export default (testContext: { 'eth_signMessage', ], }, + publicKeyHex: + '04dd467afb12bdb797303e7f3f0c8cd0ba80d518dc4e339e0e2eb8f2d99a9415cac537854a30d31a854b7af0b4fcb54c3954047390fa9500d3cc2e15a3e09017bb', + type: 'Secp256k1', }, + ], + provider: 'did:web', + services: [ { - kid: 'b4fdd9cb90730776f2934fd5bfd43d187fff667ddbe81ec40445250fcc6b60df', - kms: 'local', - type: 'Ed25519', - publicKeyHex: 'b4fdd9cb90730776f2934fd5bfd43d187fff667ddbe81ec40445250fcc6b60df', - privateKeyHex: - 'eb1722c5767e9e938e6bef6361baf16d389051b6c1dcaf58adedf5ef9652be67b4fdd9cb90730776f2934fd5bfd43d187fff667ddbe81ec40445250fcc6b60df', - meta: { algorithms: ['Ed25519', 'EdDSA'] }, + description: 'Handles incoming messages', + id: `${did}#msg`, + serviceEndpoint: 'https://example.org/messaging', + type: 'Messaging', }, ], - } - - await agent.didManagerImport(imported) - - const importedIdentifier = await agent.didManagerGet({ - did: imported.did, }) - - expect(importedIdentifier.did).toEqual(imported.did) - expect(importedIdentifier.keys.length).toEqual(imported.keys.length) - expect(importedIdentifier.services).toEqual(imported.services) }) it('should set alias for identifier', async () => { diff --git a/__tests__/shared/keyManager.ts b/__tests__/shared/keyManager.ts index 83759de91..53de3f7cd 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 aea9eaf07..70f68121c 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 80d05ea64..43907925b 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 @@ -176,20 +176,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 @@ -210,12 +197,16 @@ 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: + - $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/package.json b/packages/cli/package.json index 55421453d..1720ad8d3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -63,7 +63,7 @@ "sqlite3": "^5.0.0", "swagger-ui-express": "^4.1.5", "ts-json-schema-generator": "^0.95.0", - "typeorm": "0.2.34", + "typeorm": "0.2.37", "url-parse": "^1.5.1", "web-did-resolver": "2.0.4", "ws": "^8.0.0", diff --git a/packages/cli/src/migrations/SecretBox1588075773000.ts b/packages/cli/src/migrations/SecretBox1588075773000.ts deleted file mode 100644 index 5da40981b..000000000 --- a/packages/cli/src/migrations/SecretBox1588075773000.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' -import { Key } from '@veramo/data-store' -import { SecretBox } from '@veramo/kms-local' - -export class SecretBox1588075773000 implements MigrationInterface { - async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('key') - if (exists && process.env.VERAMO_SECRET_KEY) { - 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) - } - await key.save() - } - } - } - - async down(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('key') - if (exists && process.env.VERAMO_SECRET_KEY) { - 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) - } - await key.save() - } - } - } -} diff --git a/packages/cli/src/migrations/index.ts b/packages/cli/src/migrations/index.ts deleted file mode 100644 index bfb6f1d92..000000000 --- a/packages/cli/src/migrations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SecretBox1588075773000 } from './SecretBox1588075773000' - -export const migrations = [SecretBox1588075773000] diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts index 7e52d99d6..190b73f3c 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 6f243ca48..225eba564 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,90 @@ ], "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." + } + }, + "description": "Represents an object type where a subset of keys are required and everything else is optional." + }, "IKeyManagerSharedSecretArgs": { "type": "object", "properties": { @@ -733,7 +813,7 @@ "$ref": "#/components/schemas/IKeyManagerCreateArgs" }, "returnType": { - "$ref": "#/components/schemas/IKey" + "$ref": "#/components/schemas/ManagedKeyInfo" } }, "keyManagerDecryptJWE": { @@ -787,10 +867,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 +1195,86 @@ ], "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." + } + }, + "description": "Represents an object type where a subset of keys are required and everything else is optional." + }, "IDIDManagerRemoveKeyArgs": { "type": "object", "properties": { @@ -1269,7 +1429,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 446805f3c..8130f22a2 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 b19d2b606..4e6734fb2 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 3a903f8a5..ab47e6135 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/package.json b/packages/data-store/package.json index 4398c5cb0..2a4e64a6e 100644 --- a/packages/data-store/package.json +++ b/packages/data-store/package.json @@ -19,7 +19,7 @@ "@veramo/did-manager": "^2.1.0", "@veramo/key-manager": "^2.1.0", "debug": "^4.1.1", - "typeorm": "0.2.34" + "typeorm": "0.2.37" }, "devDependencies": { "@types/debug": "4.1.7", diff --git a/packages/data-store/src/data-store-orm.ts b/packages/data-store/src/data-store-orm.ts index 241b215c3..e71eb15a7 100644 --- a/packages/data-store/src/data-store-orm.ts +++ b/packages/data-store/src/data-store-orm.ts @@ -37,7 +37,7 @@ import { FindArgs, } from './types' -import { schema } from './' +import { Key, schema } from './' interface IContext { authenticatedDid?: string @@ -130,7 +130,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 +140,7 @@ export class DataStoreORM implements IAgentPlugin { if (identifier.provider === null) { delete identifier.provider } - return identifier + return identifier as IIdentifier }) } @@ -376,8 +376,8 @@ function createWhereObject( TMessageColumns | TClaimsColumns | TCredentialColumns | TPresentationColumns | TIdentifiersColumns >, ): any { + const where: Record = {} if (input?.where) { - const where: Record = {} for (const item of input.where) { if (item.column === 'verifier') { continue @@ -427,8 +427,8 @@ function createWhereObject( where[item.column] = Not(where[item.column]) } } - return where } + return where } function decorateQB( diff --git a/packages/data-store/src/entities/PreMigrationEntities.ts b/packages/data-store/src/entities/PreMigrationEntities.ts new file mode 100644 index 000000000..0b1833da8 --- /dev/null +++ b/packages/data-store/src/entities/PreMigrationEntities.ts @@ -0,0 +1,15 @@ +import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm' + +@Entity('key') +export class PreMigrationKey extends BaseEntity { + @PrimaryColumn() + //@ts-ignore + kid: string + + @Column() + //@ts-ignore + type: KeyType + + @Column({ nullable: true }) + privateKeyHex?: string +} diff --git a/packages/data-store/src/entities/key.ts b/packages/data-store/src/entities/key.ts index 60d87463a..ea794e6bb 100644 --- a/packages/data-store/src/entities/key.ts +++ b/packages/data-store/src/entities/key.ts @@ -22,9 +22,6 @@ export class Key extends BaseEntity { //@ts-ignore publicKeyHex: string - @Column({ nullable: true }) - privateKeyHex?: string - @Column({ type: 'simple-json', nullable: true, 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 000000000..3e2662f0b --- /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 3d67e7a60..dbb2da5bc 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 { Credential } from '../entities/credential' @@ -50,7 +50,7 @@ export class DIDStore extends AbstractDIDStore { kms: k.kms, publicKeyHex: k.publicKeyHex, meta: k.meta, - })), + } as IKey)), } if (identifier.alias) { result.alias = identifier.alias @@ -112,7 +112,6 @@ export class DIDStore extends AbstractDIDStore { const key = new Key() key.kid = argsKey.kid key.publicKeyHex = argsKey.publicKeyHex - key.privateKeyHex = argsKey.privateKeyHex key.kms = argsKey.kms key.meta = argsKey.meta identifier.keys.push(key) diff --git a/packages/data-store/src/identifier/key-store.ts b/packages/data-store/src/identifier/key-store.ts index 64aed60ca..f1d3ff4d9 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,14 @@ 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 + return key as IKey } async delete({ kid }: { kid: string }) { @@ -35,10 +29,6 @@ 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.publicKeyHex = args.publicKeyHex key.type = args.type key.kms = args.kms @@ -47,4 +37,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 000000000..d986c83ce --- /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 8c1cc5f15..4f249fbdf 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,10 @@ 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' +import { PreMigrationKey } from './entities/PreMigrationEntities' +export const Entities = [Key, Identifier, Message, Claim, Credential, Presentation, Service, PrivateKey, PreMigrationKey] +export { KeyType, Key, Identifier, Message, Claim, Credential, Presentation, MetaData, Service, PrivateKey, PreMigrationKey } export { migrations } from './migrations' const schema = require('../plugin.schema.json') export { schema } diff --git a/packages/data-store/src/migrations/createPrivateKeyStorage.ts b/packages/data-store/src/migrations/createPrivateKeyStorage.ts new file mode 100644 index 000000000..e8cae8240 --- /dev/null +++ b/packages/data-store/src/migrations/createPrivateKeyStorage.ts @@ -0,0 +1,83 @@ +import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm' +import { PrivateKey } from '..' +import { PreMigrationKey } from '../entities/PreMigrationEntities' +import Debug from 'debug' +const debug = Debug('veramo:data-store:key-migration') + +/** + * Migration of existing private keys from Veramo 2.x to Veramo 3.x + */ +export class CreatePrivateKeyStorage1629293428674 implements MigrationInterface { + async up(queryRunner: QueryRunner): Promise { + // 1.create new table + debug(`creating new private-key table`) + await queryRunner.createTable( + new Table({ + name: 'private-key', + columns: [ + { + name: 'alias', + type: 'varchar', + isPrimary: true, + }, + { + name: 'type', + type: 'varchar', + }, + { + name: 'privateKeyHex', + type: 'varchar', + }, + ], + }), + true, + ) + // 2. copy key data + const keys: PreMigrationKey[] = await queryRunner.manager.find(PreMigrationKey) + debug(`got ${keys.length} potential keys to migrate`) + const privKeys = keys + .filter((key) => typeof key.privateKeyHex !== 'undefined' && key.privateKeyHex !== null) + .map((key) => ({ + alias: key.kid, + type: key.type, + privateKeyHex: key.privateKeyHex, + })) + debug(`${privKeys.length} keys need to be migrated`) + await queryRunner.manager.createQueryBuilder().insert().into('private-key').values(privKeys).execute() + // 3. drop old column + debug(`dropping privKeyHex column from old key table`) + await queryRunner.dropColumn('key', 'privateKeyHex') + //4. done + debug(`migrated ${privKeys.length} keys to private key storage`) + } + + async down(queryRunner: QueryRunner): Promise { + // 1. add old column back + debug(`adding back privateKeyHex column to key table`) + await queryRunner.addColumn( + 'key', + new TableColumn({ + name: 'privateKeyHex', + type: 'varchar', + isNullable: true, + }), + ) + // 2. copy key data + debug(`checking keys to be rolled back`) + const keys: PrivateKey[] = await queryRunner.manager.find(PrivateKey) + debug(`copying ${keys.length} keys`) + for (const key of keys) { + await queryRunner.manager + .createQueryBuilder() + .update(PreMigrationKey) + .set({ privateKeyHex: key.privateKeyHex }) + .where('kid = :alias', { alias: key.alias }) + .execute() + } + debug(`dropping private-key table`) + // 3. drop the new private key table + await queryRunner.dropTable('private-key') + // 4. done + debug(`rolled back ${keys.length} keys`) + } +} diff --git a/packages/data-store/src/migrations/index.ts b/packages/data-store/src/migrations/index.ts index e08e07ef4..ed3ec01dd 100644 --- a/packages/data-store/src/migrations/index.ts +++ b/packages/data-store/src/migrations/index.ts @@ -1,4 +1,5 @@ import { CreateDatabase1447159020001 } from './1.createDatabase' import { SimplifyRelations1447159020002 } from './2.simplifyRelations' +import { CreatePrivateKeyStorage1629293428674 } from './createPrivateKeyStorage' -export const migrations = [CreateDatabase1447159020001, SimplifyRelations1447159020002] +export const migrations = [CreateDatabase1447159020001, SimplifyRelations1447159020002, CreatePrivateKeyStorage1629293428674] diff --git a/packages/did-manager/src/id-manager.ts b/packages/did-manager/src/id-manager.ts index 7fe5f833a..44a687705 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 bd76bd452..94c2396fd 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 af1b4ffb6..e079d6f1b 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 d27003333..fa7ecb427 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 2b535eb59..a07a2bc99 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 23053278d..a238f9fd7 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 000000000..f036e188f --- /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 f6f0451d0..1b8d90242 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, ManagedPrivateKey } 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 31f3329d0..0417becb3 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 048323224..c3f139fb6 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 2325e24a9..37b6ff076 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(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(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(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(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(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(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 2848dedb5..70be169da 100644 --- a/packages/kms-local/src/key-management-system.ts +++ b/packages/kms-local/src/key-management-system.ts @@ -1,8 +1,19 @@ -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' +import { ManagedPrivateKey } from '@veramo/key-manager' + 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 +22,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(keyStore: AbstractPrivateKeyStore) { + super() + this.keyStore = 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 +88,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) @@ -143,7 +191,7 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { const wallet = new Wallet(privateKeyHex) const signature = await wallet._signTypedData(msgDomain, msgTypes, msg) - //HEX encoded string + // HEX encoded string return signature } @@ -153,7 +201,7 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { private async eth_signMessage(privateKeyHex: string, rawMessageBytes: Uint8Array) { const wallet = new Wallet(privateKeyHex) const signature = await wallet.signMessage(rawMessageBytes) - //HEX encoded string, 0x prefixed + // HEX encoded string, 0x prefixed return signature } @@ -172,7 +220,7 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { } } const signedRawTransaction = await wallet.signTransaction(tx) - //HEX encoded string, 0x prefixed + // HEX encoded string, 0x prefixed return signedRawTransaction } @@ -182,7 +230,7 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { private async signEdDSA(key: string, data: Uint8Array): Promise { const signer = EdDSASigner(key) const signature = await signer(data) - //base64url encoded string + // base64url encoded string return signature as string } @@ -196,9 +244,62 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { ): Promise { const signer = ES256KSigner(privateKeyHex, alg === 'ES256K-R') const signature = await signer(data) - //base64url encoded string + // 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 885694387..4d2d56294 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3695,7 +3695,7 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-highlight@^2.1.10, cli-highlight@^2.1.4: +cli-highlight@^2.1.11, cli-highlight@^2.1.4: version "2.1.11" resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf" integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg== @@ -4359,7 +4359,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== @@ -10112,16 +10112,16 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typeorm@0.2.34: - version "0.2.34" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.34.tgz#637b3cec2de54ee7f423012b813a2022c0aacc8b" - integrity sha512-FZAeEGGdSGq7uTH3FWRQq67JjKu0mgANsSZ04j3kvDYNgy9KwBl/6RFgMVgiSgjf7Rqd7NrhC2KxVT7I80qf7w== +typeorm@0.2.37: + version "0.2.37" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.37.tgz#1a5e59216077640694d27c04c99ed3f968d15dc8" + integrity sha512-7rkW0yCgFC24I5T0f3S/twmLSuccPh1SQmxET/oDWn2sSDVzbyWdnItSdKy27CdJGTlKHYtUVeOcMYw5LRsXVw== dependencies: "@sqltools/formatter" "^1.2.2" app-root-path "^3.0.0" buffer "^6.0.3" chalk "^4.1.0" - cli-highlight "^2.1.10" + cli-highlight "^2.1.11" debug "^4.3.1" dotenv "^8.2.0" glob "^7.1.6" @@ -10132,7 +10132,7 @@ typeorm@0.2.34: tslib "^2.1.0" xml2js "^0.4.23" yargonaut "^1.1.4" - yargs "^16.2.0" + yargs "^17.0.1" zen-observable-ts "^1.0.0" typescript@4.3.5, typescript@~4.3.4, typescript@~4.3.5: @@ -10155,6 +10155,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"