From 393c316e27fb31b3c7fa63aae039b8fc6ae963ce Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 10 Jun 2021 12:18:58 +0200 Subject: [PATCH] feat(key-manager): add method to compute a shared secret (#555) * feat(key-manager): add method to compute a shared secret fixes #541 --- __tests__/shared/keyManager.ts | 29 +++-- packages/core/plugin.schema.json | 41 +++++++ packages/core/src/types/IKeyManager.ts | 31 +++++- .../key-manager/src/__tests__/default.test.ts | 2 +- .../src/abstract-key-management-system.ts | 2 + packages/key-manager/src/key-manager.ts | 20 +++- .../kms-local/src/__tests__/kms-local.test.ts | 100 ++++++++++++++++++ .../kms-local/src/key-management-system.ts | 24 ++++- 8 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 packages/kms-local/src/__tests__/kms-local.test.ts diff --git a/__tests__/shared/keyManager.ts b/__tests__/shared/keyManager.ts index f4b408871..1a912499c 100644 --- a/__tests__/shared/keyManager.ts +++ b/__tests__/shared/keyManager.ts @@ -200,37 +200,48 @@ export default (testContext: { expect(typeof rawTx).toEqual('string') }) - // it.todo('Should Encrypt/Decrypt') it('Should Encrypt/Decrypt', async () => { const message = 'foo bar' - const senderKey = await agent.keyManagerCreate({ kms: 'local', type: 'Ed25519', }) - const recipientKey = await agent.keyManagerCreate({ kms: 'local', type: 'Ed25519', }) - const encrypted = await agent.keyManagerEncryptJWE({ kid: senderKey.kid, to: recipientKey, data: message, }) - expect(typeof encrypted).toEqual('string') - const decrypted = await agent.keyManagerDecryptJWE({ kid: recipientKey.kid, data: encrypted, }) - expect(decrypted).toEqual(message) }) - describe('using Secp256k1 testvectors', () => { + it('Should compute sharedSecret', async () => { + const kmsArray = await agent.keyManagerGetKeyManagementSystems() + const kms = kmsArray[0] || 'local' + await agent.keyManagerImport({ + type: 'X25519', + kid: 'senderKey1', + publicKeyHex: 'c4f35b52cc5309e70a1954b00e757d2b134f3cde9beb8179312b5ce4198a1379', + privateKeyHex: 'c796444e0ee9ec8e1c57ae1334a5900c287426fa5177aa093ed9199573e34aca', + kms, + }) + const receiverKey = { + type: 'X25519', + publicKeyHex: 'c1d9ca35bd2c86ad0d61f682c30b24c73045a96773d82ff3b21ebadf85c39244', + } + const secret = await agent.keyManagerSharedSecret({ secretKeyRef: 'senderKey1', publicKey: receiverKey }) + expect(secret).toEqual('ee94c7fcf5298291029a3c3d59a8a05367a1806f36668a1f67f5ea8149097476') + }) + + describe('using Secp256k1 test vectors', () => { const importedKey = { kid: '04155ee0cbefeecd80de63a62b4ed8f0f97ac22a58f76a265903b9acab79bf018c7037e2bd897812170c92a4c978d6a10481491a37299d74c4bd412a111a4ac875', kms: 'local', @@ -304,7 +315,7 @@ export default (testContext: { }) }) - describe('using Ed25519 testvectors', () => { + describe('using Ed25519 test vectors', () => { const importedKey = { kid: 'ea75250531f6834328ac210618253288e4c54632962a9708ca82e4a399f79000', kms: 'local', diff --git a/packages/core/plugin.schema.json b/packages/core/plugin.schema.json index 13e8a86e7..7d91907ce 100644 --- a/packages/core/plugin.schema.json +++ b/packages/core/plugin.schema.json @@ -523,6 +523,38 @@ ], "description": "Input arguments for {@link IKeyManager.keyManagerGet | keyManagerGet }" }, + "IKeyManagerSharedSecretArgs": { + "type": "object", + "properties": { + "secretKeyRef": { + "type": "string", + "description": "The secret key handle (`kid`) as returned by {@link IKeyManager.keyManagerCreate | keyManagerCreate }" + }, + "publicKey": { + "type": "object", + "properties": { + "publicKeyHex": { + "type": "string", + "description": "Public key" + }, + "type": { + "$ref": "#/components/schemas/TKeyType", + "description": "Key type" + } + }, + "required": [ + "publicKeyHex", + "type" + ], + "description": "The public key of the other party. The `type` of key MUST be compatible with the type referenced by `secretKeyRef`" + } + }, + "required": [ + "secretKeyRef", + "publicKey" + ], + "description": "Input arguments for {@link IKeyManager.keyManagerSharedSecret | keyManagerSharedSecret }" + }, "IKeyManagerSignArgs": { "type": "object", "properties": { @@ -706,6 +738,15 @@ "type": "boolean" } }, + "keyManagerSharedSecret": { + "description": "Compute a shared secret with the public key of another party.", + "arguments": { + "$ref": "#/components/schemas/IKeyManagerSharedSecretArgs" + }, + "returnType": { + "type": "string" + } + }, "keyManagerSign": { "description": "Generates a signature according to the algorithm specified.", "arguments": { diff --git a/packages/core/src/types/IKeyManager.ts b/packages/core/src/types/IKeyManager.ts index c6ce78a53..cd5896680 100644 --- a/packages/core/src/types/IKeyManager.ts +++ b/packages/core/src/types/IKeyManager.ts @@ -94,7 +94,7 @@ export interface IKeyManagerSignArgs { /** * The algorithm to use for signing. * This must be one of the algorithms supported by the KMS for this key type. - * + * * The algorithm used here should match one of the names listed in `IKey.meta.algorithms` */ algorithm?: string @@ -112,6 +112,24 @@ export interface IKeyManagerSignArgs { [x: string]: any } +/** + * Input arguments for {@link IKeyManager.keyManagerSharedSecret | keyManagerSharedSecret} + * @public + */ +export interface IKeyManagerSharedSecretArgs { + /** + * The secret key handle (`kid`) + * as returned by {@link IKeyManager.keyManagerCreate | keyManagerCreate} + */ + secretKeyRef: string, + + /** + * The public key of the other party. + * The `type` of key MUST be compatible with the type referenced by `secretKeyRef` + */ + publicKey: Pick +} + /** * Input arguments for {@link IKeyManager.keyManagerSignJWT | keyManagerSignJWT} * @public @@ -181,6 +199,17 @@ export interface IKeyManager extends IPluginMethodMap { */ keyManagerSign(args: IKeyManagerSignArgs): Promise + /** + * Compute a shared secret with the public key of another party. + * + * This computes the raw shared secret (the result of a Diffie-Hellman computation) + * To use this for symmetric encryption you MUST apply a KDF on the result. + * + * @param args {@link IKeyManagerSharedKeyArgs} + * @returns a `Promise` that resolves to a hex encoded shared secret + */ + keyManagerSharedSecret(args: IKeyManagerSharedSecretArgs): Promise + /** * Encrypts data * @beta diff --git a/packages/key-manager/src/__tests__/default.test.ts b/packages/key-manager/src/__tests__/default.test.ts index f029aade6..459e455f4 100644 --- a/packages/key-manager/src/__tests__/default.test.ts +++ b/packages/key-manager/src/__tests__/default.test.ts @@ -1,4 +1,4 @@ -describe('did-manager', () => { +describe('key-manager', () => { const a = 100 it('should run a dummy test', () => { expect(a).toEqual(100) diff --git a/packages/key-manager/src/abstract-key-management-system.ts b/packages/key-manager/src/abstract-key-management-system.ts index 46022149f..d801c838c 100644 --- a/packages/key-manager/src/abstract-key-management-system.ts +++ b/packages/key-manager/src/abstract-key-management-system.ts @@ -34,4 +34,6 @@ export abstract class AbstractKeyManagementSystem { } abstract sign(args: { key: IKey; algorithm?: string; data: Uint8Array; [x: string]: any }): Promise + + abstract sharedSecret(args: { myKey: IKey; theirKey: Pick }): Promise } diff --git a/packages/key-manager/src/key-manager.ts b/packages/key-manager/src/key-manager.ts index 0b3e2e95a..a9e787c0c 100644 --- a/packages/key-manager/src/key-manager.ts +++ b/packages/key-manager/src/key-manager.ts @@ -13,6 +13,7 @@ import { IKeyManagerSignEthTXArgs, schema, IKeyManagerSignArgs, + IKeyManagerSharedSecretArgs, } from '@veramo/core' import * as u8a from 'uint8arrays' @@ -46,6 +47,7 @@ export class KeyManager implements IAgentPlugin { keyManagerSignJWT: this.keyManagerSignJWT.bind(this), keyManagerSignEthTX: this.keyManagerSignEthTX.bind(this), keyManagerSign: this.keyManagerSign.bind(this), + keyManagerSharedSecret: this.keyManagerSharedSecret.bind(this), } } @@ -66,7 +68,7 @@ export class KeyManager implements IAgentPlugin { const partialKey = await kms.createKey({ type: args.type, meta: args.meta }) const key: IKey = { ...partialKey, kms: args.kms } if (args.meta || key.meta) { - key.meta = {...args.meta, ...key.meta} + key.meta = { ...args.meta, ...key.meta } } await this.store.import(key) if (key.privateKeyHex) { @@ -140,4 +142,20 @@ export class KeyManager implements IAgentPlugin { const kms = this.getKms(key.kms) return kms.signEthTX({ key, transaction }) } + + /** {@inheritDoc @veramo/core#IKeyManager.keyManagerSharedKey} */ + async keyManagerSharedSecret(args: IKeyManagerSharedSecretArgs): Promise { + const { secretKeyRef, publicKey } = args + const myKey = await this.store.get({ kid: secretKeyRef }) + const theirKey = publicKey + if ( + myKey.type === theirKey.type || + (['Ed25519', 'X25519'].includes(myKey.type) && ['Ed25519', 'X25519'].includes(theirKey.type)) + ) { + } else { + throw new Error('invalid_argument: the key types have to match to be able to compute a shared secret') + } + const kms = this.getKms(myKey.kms) + return kms.sharedSecret({ myKey, theirKey }) + } } diff --git a/packages/kms-local/src/__tests__/kms-local.test.ts b/packages/kms-local/src/__tests__/kms-local.test.ts new file mode 100644 index 000000000..2325e24a9 --- /dev/null +++ b/packages/kms-local/src/__tests__/kms-local.test.ts @@ -0,0 +1,100 @@ +import { KeyManagementSystem } from '../key-management-system' +import { IKey, TKeyType } from '@veramo/core' + +describe('@veramo/kms-local', () => { + it('should compute a shared secret Ed+Ed', async () => { + const kms = new KeyManagementSystem() + const myKey = { + type: 'Ed25519', + privateKeyHex: + 'ed3991fa33d4df22c88b78249e4d73c509c640a873a66808ad5dce774334ce94ee5072bc20355b4cd5499e04ee70853591bffa1874b1b5467dedd648d5b89ecb', + } as IKey + const theirKey = { + type: 'Ed25519', + publicKeyHex: 'e1d1dc2afe59bb054c44ba23ba07561d15ba83f9d1c42568ac11351fbdfd87c6', + } + + const secret = await kms.sharedSecret({ myKey, theirKey }) + expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') + }) + + it('should compute a shared secret Ed+X', async () => { + const kms = new KeyManagementSystem() + const myKey = { + type: 'Ed25519', + privateKeyHex: + 'ed3991fa33d4df22c88b78249e4d73c509c640a873a66808ad5dce774334ce94ee5072bc20355b4cd5499e04ee70853591bffa1874b1b5467dedd648d5b89ecb', + } as IKey + const theirKey = { + type: 'X25519', + publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', + } + + const secret = await kms.sharedSecret({ myKey, theirKey }) + expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') + }) + + it('should compute a shared secret X+Ed', async () => { + const kms = new KeyManagementSystem() + const myKey = { + type: 'X25519', + privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', + } as IKey + + const theirKey = { + type: 'Ed25519', + publicKeyHex: 'e1d1dc2afe59bb054c44ba23ba07561d15ba83f9d1c42568ac11351fbdfd87c6', + } + + const secret = await kms.sharedSecret({ myKey, theirKey }) + expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') + }) + + it('should compute a shared secret X+X', async () => { + const kms = new KeyManagementSystem() + const myKey = { + type: 'X25519', + privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', + } as IKey + + const theirKey = { + type: 'X25519', + publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', + } + + const secret = await kms.sharedSecret({ myKey, theirKey }) + expect(secret).toEqual('2f1d171ad32fdbd10d1b06600d70223f7298809d4a3690fa03d6b4688c7b116a') + }) + + it('should throw on invalid myKey type', async () => { + expect.assertions(1) + const kms = new KeyManagementSystem() + const myKey = { + type: 'Secp256k1', + privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', + } as IKey + + const theirKey = { + type: 'X25519', + publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', + } + + expect(kms.sharedSecret({ myKey, theirKey })).rejects.toThrow('not_supported') + }) + + it('should throw on invalid theirKey type', async () => { + expect.assertions(1) + const kms = new KeyManagementSystem() + const myKey = { + type: 'X25519', + privateKeyHex: '704380837434dde8a41bebcb75494578bf243fa19cd59e120a1de84e0815c84d', + } as IKey + + const theirKey = { + type: 'Secp256k1', + publicKeyHex: '09c99ad2fdb13247d97f4343d05cc20930db0808697e89f8f3d111a40cb6ee35', + } + + expect(kms.sharedSecret({ myKey, 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 8994212ae..38fd91f69 100644 --- a/packages/kms-local/src/key-management-system.ts +++ b/packages/kms-local/src/key-management-system.ts @@ -10,16 +10,16 @@ import { JWE, } from 'did-jwt' import { generateKeyPair, convertPublicKeyToX25519, convertSecretKeyToX25519 } from '@stablelib/ed25519' -import { generateKeyPair as generateEncryptionKeypair } from '@stablelib/x25519' +import { generateKeyPair as generateEncryptionKeypair, sharedKey } from '@stablelib/x25519' import { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' import { TransactionRequest } from '@ethersproject/abstract-provider' import { toUtf8Bytes, toUtf8String } from '@ethersproject/strings' import { parse } from '@ethersproject/transactions' import { Wallet } from '@ethersproject/wallet' import { SigningKey } from '@ethersproject/signing-key' -import { randomBytes } from "@ethersproject/random"; +import { randomBytes } from '@ethersproject/random' import Debug from 'debug' -import { arrayify } from '@ethersproject/bytes' +import { arrayify, hexlify } from '@ethersproject/bytes' const debug = Debug('veramo:kms:local') export class KeyManagementSystem extends AbstractKeyManagementSystem { @@ -145,6 +145,24 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { throw Error(`not_supported: Cannot sign ${algorithm} using key of type ${key.type}`) } + async sharedSecret(args: { myKey: IKey; theirKey: Pick }): Promise { + const { myKey, theirKey } = args + let myKeyBytes = arrayify('0x' + myKey.privateKeyHex) + if (myKey.type === 'Ed25519') { + myKeyBytes = convertSecretKeyToX25519(myKeyBytes) + } else if (myKey.type !== 'X25519') { + throw new Error(`not_supported: can't compute shared secret for type=${myKey.type}`) + } + let theirKeyBytes = arrayify('0x' + theirKey.publicKeyHex) + if (theirKey.type === 'Ed25519') { + theirKeyBytes = convertPublicKeyToX25519(theirKeyBytes) + } else if (theirKey.type !== 'X25519') { + throw new Error(`not_supported: can't compute shared secret for type=${theirKey.type}`) + } + const shared = sharedKey(myKeyBytes, theirKeyBytes) + return hexlify(shared).substring(2) + } + /** * @returns a `0x` prefixed hex string representing the signed EIP712 data */