From 7eddfc25c9818c0bb3fc36ad96aa0ab08f09b990 Mon Sep 17 00:00:00 2001 From: Song Zhou Date: Sun, 1 Aug 2021 21:18:58 -0700 Subject: [PATCH] Add in ability to encrypt/decrypt msg with keyring (#1070) * feat: able to encrypt & decrypt message in keyring - ed25519 & sr25519 types * chore: make eslint happy * test: add in one more test on cross-usage of ed25519 & sr25519 * make lint happy without eslint-disable * assert away secp256k1 for encrypt/decrypt * Update packages/keyring/src/pair/index.ts * Update packages/keyring/src/pair/index.ts Co-authored-by: Jaco --- package.json | 1 + packages/keyring/src/pair/index.spec.ts | 110 +++++++++++++++++++- packages/keyring/src/pair/index.ts | 21 +++- packages/keyring/src/pair/nobody.ts | 6 ++ packages/keyring/src/types.ts | 2 + packages/util-crypto/package.json | 1 + packages/util-crypto/src/nacl/convertKey.ts | 12 +++ packages/util-crypto/src/nacl/index.ts | 1 + yarn.lock | 34 ++++-- 9 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 packages/util-crypto/src/nacl/convertKey.ts diff --git a/package.json b/package.json index e7e0085b5..28f60627e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@babel/core": "^7.14.8", "@polkadot/dev": "^0.62.57", "@polkadot/ts": "^0.4.4", + "@types/ed2curve": "^0.2.2", "@types/jest": "^26.0.24" }, "version": "7.0.3" diff --git a/packages/keyring/src/pair/index.spec.ts b/packages/keyring/src/pair/index.spec.ts index 43dc28eba..1776ed125 100644 --- a/packages/keyring/src/pair/index.spec.ts +++ b/packages/keyring/src/pair/index.spec.ts @@ -18,6 +18,10 @@ describe('pair', (): void => { }); const SIGNATURE = new Uint8Array([80, 191, 198, 147, 225, 207, 75, 88, 126, 39, 129, 109, 191, 38, 72, 181, 75, 254, 81, 143, 244, 79, 237, 38, 236, 141, 28, 252, 134, 26, 169, 234, 79, 33, 153, 158, 151, 34, 175, 188, 235, 20, 35, 135, 83, 120, 139, 211, 233, 130, 1, 208, 201, 215, 73, 80, 56, 98, 185, 196, 11, 8, 193, 14]); + const ENCRYPTED = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 59, 53, 85, 227, 40, 107, 48, 234, 198, 238, 157, 238, 224, 235, 179, 75, 153, 241, 142, 254]); + + // the last byte is changed from 254 -> 253 + const ENCRYPTED_CHANGED = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 59, 53, 85, 227, 40, 107, 48, 234, 198, 238, 157, 238, 224, 235, 179, 75, 153, 241, 142, 253]); it('has a publicKey', (): void => { expect( @@ -53,7 +57,7 @@ describe('pair', (): void => { it('fails a correctly signed message (signer changed)', (): void => { expect( keyring.alice.verify( - new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]), + new Uint8Array([0x61, 0x62, 0x63, 0x64]), SIGNATURE, keyring.bob.publicKey ) @@ -94,6 +98,93 @@ describe('pair', (): void => { ).toBe(false); }); + it('allows encrypting', (): void => { + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + + expect( + keyring.alice.encryptMessage( + message, + keyring.bob.publicKey, + new Uint8Array(24) + ) + ).toEqual(ENCRYPTED); + }); + + it('validates a correctly encrypted message', (): void => { + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + + expect( + keyring.bob.decryptMessage( + ENCRYPTED, + keyring.alice.publicKey + ) + ).toEqual(message); + }); + + it('fails a correctly encrypted message (message changed)', (): void => { + expect( + keyring.bob.decryptMessage( + ENCRYPTED_CHANGED, + keyring.alice.publicKey + ) + ).toEqual(null); + }); + + it('fails a correctly encrypted message (sender changed)', (): void => { + expect( + keyring.bob.decryptMessage( + ENCRYPTED, + keyring.charlie.publicKey + ) + ).toEqual(null); + }); + + it('fails a correctly encrypted message (receiver changed)', (): void => { + expect( + keyring.charlie.decryptMessage( + ENCRYPTED, + keyring.alice.publicKey + ) + ).toEqual(null); + }); + + it('validates encryption between ed25519 & sr25519 key pairs', (): void => { + const PASS = 'testing'; + const encodedA = keyring.alice.encodePkcs8(PASS); + const encodedB = keyring.bob.encodePkcs8(PASS); + + const pairA = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey }); + const pairB = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.bob.publicKey }); + + pairA.decodePkcs8(PASS, encodedA); + pairB.decodePkcs8(PASS, encodedB); + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + + // alice-sr25519 -> bob-ed25519 + const encrypted1 = pairA.encryptMessage(message, keyring.bob.publicKey); + + // alice-sr25519 -> bob-sr25519 + const encrypted2 = pairA.encryptMessage(message, pairB.publicKey); + + // alice-ed25519 -> bob-sr25519 + const encrypted3 = keyring.alice.encryptMessage(message, pairB.publicKey); + + // decrypt: Bob-ed25519 - use alice-ed25519 pubkey + expect( + keyring.bob.decryptMessage(encrypted3, keyring.alice.publicKey) + ).toEqual(message); + + // decrypt: Bob-ed25519 - use alice-sr25519 pubkey + expect( + keyring.bob.decryptMessage(encrypted1, pairA.publicKey) + ).toEqual(message); + + // decrypt: Bob-sr25519 - use alice-sr25519 pubkey + expect( + pairB.decryptMessage(encrypted2, pairA.publicKey) + ).toEqual(message); + }); + it('allows setting/getting of meta', (): void => { keyring.bob.setMeta({ foo: 'bar', something: 'else' }); @@ -183,5 +274,22 @@ describe('pair', (): void => { }); expect(json.address).toEqual(u8aToHex(PUBLICDERIVED)); }); + + it('denies access to encryptMessage/decryptMessage API', (): void => { + const PASS = 'testing'; + const encoded = keyring.alice.encodePkcs8(PASS); + + const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: keyring.alice.publicKey }); + + pair.decodePkcs8(PASS, encoded); + + expect( + () => pair.encryptMessage( + new Uint8Array(4), // null message + keyring.bob.publicKey, + new Uint8Array(24) + ) + ).toThrow('Secp256k1 not supported yet'); + }); }); }); diff --git a/packages/keyring/src/pair/index.ts b/packages/keyring/src/pair/index.ts index 695b12bb3..185369047 100644 --- a/packages/keyring/src/pair/index.ts +++ b/packages/keyring/src/pair/index.ts @@ -7,7 +7,7 @@ import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta, SignOptions } fro import type { PairInfo } from './types'; import { assert, u8aConcat, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util'; -import { blake2AsU8a, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclSign, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVrfSign, schnorrkelVrfVerify, secp256k1Compress, secp256k1Expand, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify } from '@polkadot/util-crypto'; +import { blake2AsU8a, convertPublicKeyToCurve25519, convertSecretKeyToCurve25519, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclOpen, naclSeal, naclSign, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVrfSign, schnorrkelVrfVerify, secp256k1Compress, secp256k1Expand, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify } from '@polkadot/util-crypto'; import { decodePair } from './decode'; import { encodePair } from './encode'; @@ -145,6 +145,17 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P }, // eslint-disable-next-line sort-keys decodePkcs8, + decryptMessage: (encryptedMessageWithNonce: string | Uint8Array, senderPublicKey: string | Uint8Array): Uint8Array | null => { + assert(!isLocked(secretKey), 'Cannot encrypt with a locked key pair'); + assert(!['ecdsa', 'ethereum'].includes(type), 'Secp256k1 not supported yet'); + + return naclOpen( + u8aToU8a(encryptedMessageWithNonce.slice(24, encryptedMessageWithNonce.length)), + u8aToU8a(encryptedMessageWithNonce.slice(0, 24)), + convertPublicKeyToCurve25519(u8aToU8a(senderPublicKey)), + convertSecretKeyToCurve25519(secretKey) + ); + }, derive: (suri: string, meta?: KeyringPair$Meta): KeyringPair => { assert(type !== 'ethereum', 'Unable to derive on this keypair'); assert(!isLocked(secretKey), 'Cannot derive on a locked keypair'); @@ -157,6 +168,14 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P encodePkcs8: (passphrase?: string): Uint8Array => { return recode(passphrase); }, + encryptMessage: (message: string | Uint8Array, recipientPublicKey: string | Uint8Array, _nonce?: Uint8Array): Uint8Array => { + assert(!isLocked(secretKey), 'Cannot encrypt with a locked key pair'); + assert(!['ecdsa', 'ethereum'].includes(type), 'Secp256k1 not supported yet'); + + const { nonce, sealed } = naclSeal(u8aToU8a(message), convertSecretKeyToCurve25519(secretKey), convertPublicKeyToCurve25519(u8aToU8a(recipientPublicKey)), _nonce); + + return u8aConcat(nonce, sealed); + }, lock: (): void => { secretKey = new Uint8Array(); }, diff --git a/packages/keyring/src/pair/nobody.ts b/packages/keyring/src/pair/nobody.ts index 90d4e068b..b6c29c362 100644 --- a/packages/keyring/src/pair/nobody.ts +++ b/packages/keyring/src/pair/nobody.ts @@ -30,11 +30,17 @@ export function nobody (): KeyringPair { decodePkcs8: (passphrase?: string, encoded?: Uint8Array): void => undefined, // eslint-disable-next-line @typescript-eslint/no-unused-vars + decryptMessage: (encryptedMessageWithNonce: string | Uint8Array, senderPublicKey: string | Uint8Array): Uint8Array | null => + null, + // eslint-disable-next-line @typescript-eslint/no-unused-vars derive: (suri: string, meta?: KeyringPair$Meta): KeyringPair => pair, // eslint-disable-next-line @typescript-eslint/no-unused-vars encodePkcs8: (passphrase?: string): Uint8Array => new Uint8Array(0), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + encryptMessage: (message: string | Uint8Array, recipientPublicKey: string | Uint8Array, _nonce?: Uint8Array): Uint8Array => + new Uint8Array(), isLocked: true, lock: (): void => { // no locking, it is always locked diff --git a/packages/keyring/src/types.ts b/packages/keyring/src/types.ts index 30c09fac4..e9a4df608 100644 --- a/packages/keyring/src/types.ts +++ b/packages/keyring/src/types.ts @@ -37,6 +37,8 @@ export interface KeyringPair { sign (message: string | Uint8Array, options?: SignOptions): Uint8Array; toJson (passphrase?: string): KeyringPair$Json; unlock (passphrase?: string): void; + encryptMessage (message: string | Uint8Array, recipientPublicKey: string | Uint8Array, nonce?: Uint8Array): Uint8Array; + decryptMessage (encryptedMessageWithNonce: string | Uint8Array, senderPublicKey: string | Uint8Array): Uint8Array | null; verify (message: string | Uint8Array, signature: Uint8Array, signerPublic: string | Uint8Array): boolean; vrfSign (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array; vrfVerify (message: string | Uint8Array, vrfResult: Uint8Array, signerPublic: Uint8Array | string, context?: string | Uint8Array, extra?: string | Uint8Array): boolean; diff --git a/packages/util-crypto/package.json b/packages/util-crypto/package.json index bcc0cf7d9..eed5ae925 100644 --- a/packages/util-crypto/package.json +++ b/packages/util-crypto/package.json @@ -31,6 +31,7 @@ "blakejs": "^1.1.1", "bn.js": "^4.11.9", "create-hash": "^1.2.0", + "ed2curve": "^0.3.0", "elliptic": "^6.5.4", "hash.js": "^1.1.7", "js-sha3": "^0.8.0", diff --git a/packages/util-crypto/src/nacl/convertKey.ts b/packages/util-crypto/src/nacl/convertKey.ts new file mode 100644 index 000000000..2cdf48022 --- /dev/null +++ b/packages/util-crypto/src/nacl/convertKey.ts @@ -0,0 +1,12 @@ +// Copyright 2017-2021 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import ed2curve from 'ed2curve'; + +export function convertSecretKeyToCurve25519 (secretKey: Uint8Array): Uint8Array { + return ed2curve.convertSecretKey(secretKey); +} + +export function convertPublicKeyToCurve25519 (publicKey: Uint8Array): Uint8Array { + return ed2curve.convertPublicKey(publicKey) as Uint8Array; +} diff --git a/packages/util-crypto/src/nacl/index.ts b/packages/util-crypto/src/nacl/index.ts index e816842f4..cd85bde02 100644 --- a/packages/util-crypto/src/nacl/index.ts +++ b/packages/util-crypto/src/nacl/index.ts @@ -15,3 +15,4 @@ export { naclVerify } from './verify'; export { naclBoxKeypairFromSecret } from './box/fromSecret'; export { naclOpen } from './open'; export { naclSeal } from './seal'; +export { convertSecretKeyToCurve25519, convertPublicKeyToCurve25519 } from './convertKey'; diff --git a/yarn.lock b/yarn.lock index af5cb081b..199f9f4ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2122,6 +2122,7 @@ __metadata: blakejs: ^1.1.1 bn.js: ^4.11.9 create-hash: ^1.2.0 + ed2curve: ^0.3.0 elliptic: ^6.5.4 hash.js: ^1.1.7 js-sha3: ^0.8.0 @@ -2445,6 +2446,15 @@ __metadata: languageName: node linkType: hard +"@types/ed2curve@npm:^0.2.2": + version: 0.2.2 + resolution: "@types/ed2curve@npm:0.2.2" + dependencies: + tweetnacl: ^1.0.0 + checksum: f1ab16d46c5eed50d51539effef76c87806bb53ab45ecff78c19a632e63825fd08d604984b08778f8201c79be0bd43120565dad38cc5d45ba65a6c7de5bf4ace + languageName: node + linkType: hard + "@types/elliptic@npm:^6.4.13": version: 6.4.13 resolution: "@types/elliptic@npm:6.4.13" @@ -4735,6 +4745,15 @@ __metadata: languageName: node linkType: hard +"ed2curve@npm:^0.3.0": + version: 0.3.0 + resolution: "ed2curve@npm:0.3.0" + dependencies: + tweetnacl: 1.x.x + checksum: 6dfbe2310aa5a47372c9dd2fd920be140c8d52aea5793d716a3e3865d2ceaeaf639a7653e5492dfe3b4910eaf65c09a1d5132580afe2fdca18a75ebb428a52f2 + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.3.723": version: 1.3.727 resolution: "electron-to-chromium@npm:1.3.727" @@ -9867,6 +9886,7 @@ resolve@^2.0.0-next.3: "@babel/core": ^7.14.8 "@polkadot/dev": ^0.62.57 "@polkadot/ts": ^0.4.4 + "@types/ed2curve": ^0.2.2 "@types/jest": ^26.0.24 languageName: unknown linkType: soft @@ -10904,6 +10924,13 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"tweetnacl@npm:1.x.x, tweetnacl@npm:^1.0.0, tweetnacl@npm:^1.0.3": + version: 1.0.3 + resolution: "tweetnacl@npm:1.0.3" + checksum: e4a57cac188f0c53f24c7a33279e223618a2bfb5fea426231991652a13247bea06b081fd745d71291fcae0f4428d29beba1b984b1f1ce6f66b06a6d1ab90645c + languageName: node + linkType: hard + "tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": version: 0.14.5 resolution: "tweetnacl@npm:0.14.5" @@ -10911,13 +10938,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"tweetnacl@npm:^1.0.3": - version: 1.0.3 - resolution: "tweetnacl@npm:1.0.3" - checksum: e4a57cac188f0c53f24c7a33279e223618a2bfb5fea426231991652a13247bea06b081fd745d71291fcae0f4428d29beba1b984b1f1ce6f66b06a6d1ab90645c - languageName: node - linkType: hard - "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0"