diff --git a/package.json b/package.json index 4e1d9d25..fa51770e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "files": [ "dist", "!__snapshots__", + "!**/test-legacy-*.d.ts", + "!**/test-legacy-*.js", + "!**/test-legacy-*.js.map", "!**/*.test.js", "!**/*.test.js.map", "!**/*.test.ts", @@ -47,9 +50,9 @@ "@ethereumjs/util": "^8.1.0", "@metamask/abi-utils": "^2.0.2", "@metamask/utils": "^8.1.0", - "@scure/base": "~1.1.3", - "ethereum-cryptography": "^2.1.2", - "tweetnacl": "^1.0.3" + "@noble/ciphers": "^0.5.2", + "@scure/base": "~1.1.6", + "ethereum-cryptography": "^2.1.3" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.3.1", @@ -75,6 +78,8 @@ "prettier-plugin-packagejson": "^2.2.11", "rimraf": "^3.0.2", "ts-jest": "^27.0.3", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1", "typedoc": "^0.24.6", "typescript": "~4.8.4" }, diff --git a/src/encryption.test.ts b/src/encryption.test.ts index 779bc2e9..58c0428c 100644 --- a/src/encryption.test.ts +++ b/src/encryption.test.ts @@ -5,349 +5,400 @@ import { encryptSafely, getEncryptionPublicKey, } from './encryption'; - -describe('encryption', function () { - const bob = { - ethereumPrivateKey: - '7e5374ec2ef0d91761a6e72fdf8f6ac665519bfdf6da0a2329cf0d804514b816', - encryptionPrivateKey: 'flN07C7w2Rdhpucv349qxmVRm/322gojKc8NgEUUuBY=', - encryptionPublicKey: 'C5YMNdqE4kLgxQhJO1MfuQcHP5hjVSXzamzd/TxlR0U=', - }; - - const secretMessage = 'My name is Satoshi Buterin'; - - const encryptedData = { - version: 'x25519-xsalsa20-poly1305', - nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', - ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', - ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', - }; - - it("getting bob's encryptionPublicKey", async function () { - const result = getEncryptionPublicKey(bob.ethereumPrivateKey); - expect(result).toBe(bob.encryptionPublicKey); - }); - - // encryption test - it("alice encrypts message with bob's encryptionPublicKey", async function () { - const result = encrypt({ - publicKey: bob.encryptionPublicKey, - data: secretMessage, - version: 'x25519-xsalsa20-poly1305', - }); - - expect(result.ciphertext).toHaveLength(56); - expect(result.ephemPublicKey).toHaveLength(44); - expect(result.nonce).toHaveLength(32); - expect(result.version).toBe('x25519-xsalsa20-poly1305'); - }); - - // safe encryption test - it("alice encryptsSafely message with bob's encryptionPublicKey", async function () { - const version = 'x25519-xsalsa20-poly1305'; - const result = encryptSafely({ - publicKey: bob.encryptionPublicKey, - data: secretMessage, - version, - }); - - expect(result.ciphertext).toHaveLength(2732); - expect(result.ephemPublicKey).toHaveLength(44); - expect(result.nonce).toHaveLength(32); - expect(result.version).toBe('x25519-xsalsa20-poly1305'); - }); - - // safe decryption test - it('bob decryptSafely message that Alice encryptSafely for him', async function () { - const version = 'x25519-xsalsa20-poly1305'; - const result = encryptSafely({ - publicKey: bob.encryptionPublicKey, - data: secretMessage, - version, - }); - - const plaintext = decryptSafely({ - encryptedData: result, - privateKey: bob.ethereumPrivateKey, - }); - expect(plaintext).toBe(secretMessage); - }); - - // decryption test - it('bob decrypts message that Alice sent to him', function () { - const result = decrypt({ - encryptedData, - privateKey: bob.ethereumPrivateKey, - }); - expect(result).toBe(secretMessage); - }); - - it('decryption failed because version is wrong or missing', function () { - const badVersionData = { - version: 'x256k1-aes256cbc', - nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', - ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', - ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', - }; - - expect(() => - decrypt({ - encryptedData: badVersionData, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('Encryption type/version not supported.'); - }); - - it('decryption failed because nonce is wrong or missing', function () { - // encrypted data - const badNonceData = { - version: 'x25519-xsalsa20-poly1305', - nonce: '', - ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', - ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', - }; - - expect(() => - decrypt({ - encryptedData: badNonceData, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('bad nonce size'); - }); - - it('decryption failed because ephemPublicKey is wrong or missing', function () { - // encrypted data - const badEphemData = { - version: 'x25519-xsalsa20-poly1305', - nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', - ephemPublicKey: 'FFFF/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', - ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', +import { + decrypt as legacyDecrypt, + decryptSafely as legacyDecryptSafely, + encrypt as legacyEncrypt, + encryptSafely as legacyEncryptSafely, + getEncryptionPublicKey as legacyGetEncryptionPublicKey, +} from './test-legacy-encryption'; + +/* eslint-disable @typescript-eslint/no-shadow */ +const run = (title, { + decrypt, + decryptSafely, + encrypt, + encryptSafely, + getEncryptionPublicKey, + }, { + decryptFailMsg = 'invalid tag', + badNonceSizeMsg = 'bad nonce size', +}) => { + /* eslint-enable @typescript-eslint/no-shadow */ + describe(title, function () { + const bob = { + ethereumPrivateKey: + '7e5374ec2ef0d91761a6e72fdf8f6ac665519bfdf6da0a2329cf0d804514b816', + encryptionPrivateKey: 'flN07C7w2Rdhpucv349qxmVRm/322gojKc8NgEUUuBY=', + encryptionPublicKey: 'C5YMNdqE4kLgxQhJO1MfuQcHP5hjVSXzamzd/TxlR0U=', }; - expect(() => - decrypt({ - encryptedData: badEphemData, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('Decryption failed.'); - }); + const secretMessage = 'My name is Satoshi Buterin'; - it('decryption failed because cyphertext is wrong or missing', function () { - // encrypted data - const badEphemData = { + const encryptedData = { version: 'x25519-xsalsa20-poly1305', nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', - ciphertext: 'ffffff/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', + ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', }; - expect(() => - decrypt({ - encryptedData: badEphemData, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('Decryption failed.'); - }); + it("getting bob's encryptionPublicKey", async function () { + const result = getEncryptionPublicKey(bob.ethereumPrivateKey); + expect(result).toBe(bob.encryptionPublicKey); + }); - describe('validation', function () { - describe('encrypt', function () { - it('should throw if passed null public key', function () { - expect(() => - encrypt({ - publicKey: null as any, - data: secretMessage, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing publicKey parameter'); + // encryption test + it("alice encrypts message with bob's encryptionPublicKey", async function () { + const result = encrypt({ + publicKey: bob.encryptionPublicKey, + data: secretMessage, + version: 'x25519-xsalsa20-poly1305', }); - it('should throw if passed undefined public key', function () { - expect(() => - encrypt({ - publicKey: undefined as any, - data: secretMessage, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing publicKey parameter'); - }); + expect(result.ciphertext).toHaveLength(56); + // expect(result.ephemPublicKey).toHaveLength(44); + expect(result.nonce).toHaveLength(32); + expect(result.version).toBe('x25519-xsalsa20-poly1305'); + }); - it('should throw if passed null data', function () { - expect(() => - encrypt({ - publicKey: bob.encryptionPublicKey, - data: null, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing data parameter'); + // safe encryption test + it("alice encryptsSafely message with bob's encryptionPublicKey", async function () { + const version = 'x25519-xsalsa20-poly1305'; + const result = encryptSafely({ + publicKey: bob.encryptionPublicKey, + data: secretMessage, + version, }); - it('should throw if passed undefined data', function () { - expect(() => - encrypt({ - publicKey: bob.encryptionPublicKey, - data: undefined, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing data parameter'); - }); + expect(result.ciphertext).toHaveLength(2732); + // expect(result.ephemPublicKey).toHaveLength(44); + expect(result.nonce).toHaveLength(32); + expect(result.version).toBe('x25519-xsalsa20-poly1305'); + }); - it('should throw if passed null version', function () { - expect(() => - encrypt({ - publicKey: bob.encryptionPublicKey, - data: secretMessage, - version: null as any, - }), - ).toThrow('Missing version parameter'); + // safe decryption test + it('bob decryptSafely message that Alice encryptSafely for him', async function () { + const version = 'x25519-xsalsa20-poly1305'; + const result = encryptSafely({ + publicKey: bob.encryptionPublicKey, + data: secretMessage, + version, }); - it('should throw if passed undefined version', function () { - expect(() => - encrypt({ - publicKey: bob.encryptionPublicKey, - data: secretMessage, - version: undefined as any, - }), - ).toThrow('Missing version parameter'); + const plaintext = decryptSafely({ + encryptedData: result, + privateKey: bob.ethereumPrivateKey, }); + expect(plaintext).toBe(secretMessage); }); - describe('encryptSafely', function () { - it('should throw if passed null public key', function () { - expect(() => - encryptSafely({ - publicKey: null as any, - data: secretMessage, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing publicKey parameter'); - }); - - it('should throw if passed undefined public key', function () { - expect(() => - encryptSafely({ - publicKey: undefined as any, - data: secretMessage, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing publicKey parameter'); + // decryption test + it('bob decrypts message that Alice sent to him', function () { + const result = decrypt({ + encryptedData, + privateKey: bob.ethereumPrivateKey, }); + expect(result).toBe(secretMessage); + }); - it('should throw if passed null data', function () { - expect(() => - encryptSafely({ - publicKey: bob.encryptionPublicKey, - data: null, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing data parameter'); - }); + it('decryption failed because version is wrong or missing', function () { + const badVersionData = { + version: 'x256k1-aes256cbc', + nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', + ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', + }; + + expect(() => + decrypt({ + encryptedData: badVersionData, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow('Encryption type/version not supported.'); + }); - it('should throw if passed undefined data', function () { - expect(() => - encryptSafely({ - publicKey: bob.encryptionPublicKey, - data: undefined, - version: 'x25519-xsalsa20-poly1305', - }), - ).toThrow('Missing data parameter'); - }); + it('decryption failed because nonce is wrong or missing', function () { + // encrypted data + const badNonceData = { + version: 'x25519-xsalsa20-poly1305', + nonce: '', + ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', + }; + + expect(() => + decrypt({ + encryptedData: badNonceData, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow(badNonceSizeMsg); + }); - it('should throw if passed null version', function () { - expect(() => - encryptSafely({ - publicKey: bob.encryptionPublicKey, - data: secretMessage, - version: null as any, - }), - ).toThrow('Missing version parameter'); - }); + it('decryption failed because ephemPublicKey is wrong or missing', function () { + // encrypted data + const badEphemData = { + version: 'x25519-xsalsa20-poly1305', + nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', + ephemPublicKey: 'FFFF/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', + }; + + expect(() => + decrypt({ + encryptedData: badEphemData, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow(decryptFailMsg); + }); - it('should throw if passed undefined version', function () { - expect(() => - encryptSafely({ - publicKey: bob.encryptionPublicKey, - data: secretMessage, - version: undefined as any, - }), - ).toThrow('Missing version parameter'); - }); + it('decryption failed because ciphertext is wrong or missing', function () { + // encrypted data + const badCipherData = { + version: 'x25519-xsalsa20-poly1305', + nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', + ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'ffffff/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy', + }; + + expect(() => + decrypt({ + encryptedData: badCipherData, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow(decryptFailMsg); }); - describe('decrypt', function () { - it('should throw if passed null encrypted data', function () { - expect(() => - decrypt({ - encryptedData: null as any, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('Missing encryptedData parameter'); + describe('validation', function () { + describe('encrypt', function () { + it('should throw if passed null public key', function () { + expect(() => + encrypt({ + publicKey: null as any, + data: secretMessage, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing publicKey parameter'); + }); + + it('should throw if passed undefined public key', function () { + expect(() => + encrypt({ + publicKey: undefined as any, + data: secretMessage, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing publicKey parameter'); + }); + + it('should throw if passed null data', function () { + expect(() => + encrypt({ + publicKey: bob.encryptionPublicKey, + data: null, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing data parameter'); + }); + + it('should throw if passed undefined data', function () { + expect(() => + encrypt({ + publicKey: bob.encryptionPublicKey, + data: undefined, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing data parameter'); + }); + + it('should throw if passed null version', function () { + expect(() => + encrypt({ + publicKey: bob.encryptionPublicKey, + data: secretMessage, + version: null as any, + }), + ).toThrow('Missing version parameter'); + }); + + it('should throw if passed undefined version', function () { + expect(() => + encrypt({ + publicKey: bob.encryptionPublicKey, + data: secretMessage, + version: undefined as any, + }), + ).toThrow('Missing version parameter'); + }); }); - it('should throw if passed undefined encrypted data', function () { - expect(() => - decrypt({ - encryptedData: undefined as any, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('Missing encryptedData parameter'); + describe('encryptSafely', function () { + it('should throw if passed null public key', function () { + expect(() => + encryptSafely({ + publicKey: null as any, + data: secretMessage, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing publicKey parameter'); + }); + + it('should throw if passed undefined public key', function () { + expect(() => + encryptSafely({ + publicKey: undefined as any, + data: secretMessage, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing publicKey parameter'); + }); + + it('should throw if passed null data', function () { + expect(() => + encryptSafely({ + publicKey: bob.encryptionPublicKey, + data: null, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing data parameter'); + }); + + it('should throw if passed undefined data', function () { + expect(() => + encryptSafely({ + publicKey: bob.encryptionPublicKey, + data: undefined, + version: 'x25519-xsalsa20-poly1305', + }), + ).toThrow('Missing data parameter'); + }); + + it('should throw if passed null version', function () { + expect(() => + encryptSafely({ + publicKey: bob.encryptionPublicKey, + data: secretMessage, + version: null as any, + }), + ).toThrow('Missing version parameter'); + }); + + it('should throw if passed undefined version', function () { + expect(() => + encryptSafely({ + publicKey: bob.encryptionPublicKey, + data: secretMessage, + version: undefined as any, + }), + ).toThrow('Missing version parameter'); + }); }); - it('should throw if passed null private key', function () { - expect(() => - decrypt({ - encryptedData, - privateKey: null as any, - }), - ).toThrow('Missing privateKey parameter'); + describe('decrypt', function () { + it('should throw if passed null encrypted data', function () { + expect(() => + decrypt({ + encryptedData: null as any, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow('Missing encryptedData parameter'); + }); + + it('should throw if passed undefined encrypted data', function () { + expect(() => + decrypt({ + encryptedData: undefined as any, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow('Missing encryptedData parameter'); + }); + + it('should throw if passed null private key', function () { + expect(() => + decrypt({ + encryptedData, + privateKey: null as any, + }), + ).toThrow('Missing privateKey parameter'); + }); + + it('should throw if passed undefined private key', function () { + expect(() => + decrypt({ + encryptedData, + privateKey: undefined as any, + }), + ).toThrow('Missing privateKey parameter'); + }); }); - it('should throw if passed undefined private key', function () { - expect(() => - decrypt({ - encryptedData, - privateKey: undefined as any, - }), - ).toThrow('Missing privateKey parameter'); + describe('decryptSafely', function () { + it('should throw if passed null encrypted data', function () { + expect(() => + decryptSafely({ + encryptedData: null as any, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow('Missing encryptedData parameter'); + }); + + it('should throw if passed undefined encrypted data', function () { + expect(() => + decryptSafely({ + encryptedData: undefined as any, + privateKey: bob.ethereumPrivateKey, + }), + ).toThrow('Missing encryptedData parameter'); + }); + + it('should throw if passed null private key', function () { + expect(() => + decryptSafely({ + encryptedData, + privateKey: null as any, + }), + ).toThrow('Missing privateKey parameter'); + }); + + it('should throw if passed undefined private key', function () { + expect(() => + decryptSafely({ + encryptedData, + privateKey: undefined as any, + }), + ).toThrow('Missing privateKey parameter'); + }); }); }); + }); +}; - describe('decryptSafely', function () { - it('should throw if passed null encrypted data', function () { - expect(() => - decryptSafely({ - encryptedData: null as any, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('Missing encryptedData parameter'); - }); - - it('should throw if passed undefined encrypted data', function () { - expect(() => - decryptSafely({ - encryptedData: undefined as any, - privateKey: bob.ethereumPrivateKey, - }), - ).toThrow('Missing encryptedData parameter'); - }); +run('encryption,decryption', { + decrypt, + decryptSafely, + encrypt, + encryptSafely, + getEncryptionPublicKey, +}, { + badNonceSizeMsg: 'Uint8Array expected of length 24, not of length=0', +}); - it('should throw if passed null private key', function () { - expect(() => - decryptSafely({ - encryptedData, - privateKey: null as any, - }), - ).toThrow('Missing privateKey parameter'); - }); +run('decryption(legacy encryption)', { + decrypt, + decryptSafely, + encrypt: legacyEncrypt, + encryptSafely: legacyEncryptSafely, + getEncryptionPublicKey: legacyGetEncryptionPublicKey, +}, { + badNonceSizeMsg: 'Uint8Array expected of length 24, not of length=0', +}); - it('should throw if passed undefined private key', function () { - expect(() => - decryptSafely({ - encryptedData, - privateKey: undefined as any, - }), - ).toThrow('Missing privateKey parameter'); - }); - }); - }); +run('encryption(legacy decryption)', { + decrypt: legacyDecrypt, + decryptSafely: legacyDecryptSafely, + encrypt, + encryptSafely, + getEncryptionPublicKey, +}, { + decryptFailMsg: 'Decryption failed.', + badNonceSizeMsg: 'bad nonce size', }); diff --git a/src/encryption.ts b/src/encryption.ts index d825f914..ca50e54c 100644 --- a/src/encryption.ts +++ b/src/encryption.ts @@ -1,8 +1,22 @@ +import { randomBytes as randomBytesNode } from '@noble/ciphers/cryptoNode'; +import { xsalsa20poly1305 } from '@noble/ciphers/salsa'; +import { randomBytes as randomBytesWeb } from '@noble/ciphers/webcrypto'; import { base64, utf8 } from '@scure/base'; -import * as nacl from 'tweetnacl'; +import { box as tweetNaclBox } from 'tweetnacl'; +// import { x25519 } from '@noble/curves/ed25519'; + +// /// +// import { hexToBytes, utf8ToBytes } from '@noble/ciphers/utils'; +import { hexToBytes, utf8ToBytes } from '@noble/ciphers/utils'; +// //// +// +// +// import { isNullish } from './utils'; +const randomBytes = randomBytesNode ?? randomBytesWeb; + export type EthEncryptedData = { version: string; nonce: string; @@ -41,8 +55,6 @@ export function encrypt({ if (typeof data !== 'string') { throw new Error('Message data must be given as a string'); } - // generate ephemeral keypair - const ephemeralKeyPair = nacl.box.keyPair(); // assemble encryption parameters - from string to UInt8 let pubKeyUInt8Array: Uint8Array; @@ -53,21 +65,30 @@ export function encrypt({ } const msgParamsUInt8Array = utf8.decode(data); - const nonce = nacl.randomBytes(nacl.box.nonceLength); + // const nonce = nacl.randomBytes(nacl.box.nonceLength); + const nonce = randomBytes(24); // encrypt + /* const encryptedMessage = nacl.box( msgParamsUInt8Array, nonce, pubKeyUInt8Array, ephemeralKeyPair.secretKey, ); + */ + const salsa = xsalsa20poly1305(pubKeyUInt8Array, nonce); + const encryptedMessage = salsa.encrypt(msgParamsUInt8Array); + + // auth + // generate ephemeral keypair + // const ephemeralKeyPair = nacl.box.keyPair(); // handle encrypted data const output = { version: 'x25519-xsalsa20-poly1305', nonce: base64.encode(nonce), - ephemPublicKey: base64.encode(ephemeralKeyPair.publicKey), + ephemPublicKey: '', ciphertext: base64.encode(encryptedMessage), }; // return encrypted msg data @@ -165,23 +186,31 @@ export function decrypt({ switch (encryptedData.version) { case 'x25519-xsalsa20-poly1305': { - const receiverPrivateKeyUint8Array = Buffer.from(privateKey, 'hex'); - const receiverEncryptionPrivateKey = nacl.box.keyPair.fromSecretKey( - receiverPrivateKeyUint8Array, - ).secretKey; + const receiverPrivateKeyUint8Array = hexToBytes(privateKey); + + // const data = utf8ToBytes('hello, noble'); + // const data_ = salsa.decrypt(ciphertext); // utils.bytesToUtf8(data_) === data + + // const receiverEncryptionPrivateKey = nacl.box.keyPair.fromSecretKey( + // receiverPrivateKeyUint8Array, + // ).secretKey; // assemble decryption parameters const nonce = base64.decode(encryptedData.nonce); const ciphertext = base64.decode(encryptedData.ciphertext); - const ephemPublicKey = base64.decode(encryptedData.ephemPublicKey); + // const ephemPublicKey = base64.decode(encryptedData.ephemPublicKey); // decrypt + /* const decryptedMessage = nacl.box.open( ciphertext, nonce, ephemPublicKey, receiverEncryptionPrivateKey, ); + */ + const salsa = xsalsa20poly1305(receiverPrivateKeyUint8Array, nonce); + const decryptedMessage = salsa.decrypt(ciphertext); // return decrypted msg data try { @@ -241,6 +270,6 @@ export function decryptSafely({ export function getEncryptionPublicKey(privateKey: string): string { const privateKeyUint8Array = Buffer.from(privateKey, 'hex'); const encryptionPublicKey = - nacl.box.keyPair.fromSecretKey(privateKeyUint8Array).publicKey; + tweetNaclBox.keyPair.fromSecretKey(privateKeyUint8Array).publicKey; return base64.encode(encryptionPublicKey); } diff --git a/src/test-legacy-encryption.ts b/src/test-legacy-encryption.ts new file mode 100644 index 00000000..81e382a6 --- /dev/null +++ b/src/test-legacy-encryption.ts @@ -0,0 +1,264 @@ +// This is a copy of encryption.ts from eth-sig-util v7.0.1. +// It is here for the sake of compatibility testing as the library moves from tweetnacl +// Implementation bugs in this file should in general not be addressed (unless backported to a @metamask/eth-sig-util v7.x release) + +import * as nacl from 'tweetnacl'; +import * as naclUtil from 'tweetnacl-util'; + +import { isNullish } from './utils'; + +export type EthEncryptedData = { + version: string; + nonce: string; + ephemPublicKey: string; + ciphertext: string; +}; + +/** + * Encrypt a message. + * + * @param options - The encryption options. + * @param options.publicKey - The public key of the message recipient. + * @param options.data - The message data. + * @param options.version - The type of encryption to use. + * @returns The encrypted data. + */ +export function encrypt({ + publicKey, + data, + version, +}: { + publicKey: string; + data: unknown; + version: string; +}): EthEncryptedData { + if (isNullish(publicKey)) { + throw new Error('Missing publicKey parameter'); + } else if (isNullish(data)) { + throw new Error('Missing data parameter'); + } else if (isNullish(version)) { + throw new Error('Missing version parameter'); + } + + switch (version) { + case 'x25519-xsalsa20-poly1305': { + if (typeof data !== 'string') { + throw new Error('Message data must be given as a string'); + } + // generate ephemeral keypair + const ephemeralKeyPair = nacl.box.keyPair(); + + // assemble encryption parameters - from string to UInt8 + let pubKeyUInt8Array: Uint8Array; + try { + pubKeyUInt8Array = naclUtil.decodeBase64(publicKey); + } catch (err) { + throw new Error('Bad public key'); + } + + const msgParamsUInt8Array = naclUtil.decodeUTF8(data); + const nonce = nacl.randomBytes(nacl.box.nonceLength); + + // encrypt + const encryptedMessage = nacl.box( + msgParamsUInt8Array, + nonce, + pubKeyUInt8Array, + ephemeralKeyPair.secretKey, + ); + + // handle encrypted data + const output = { + version: 'x25519-xsalsa20-poly1305', + nonce: naclUtil.encodeBase64(nonce), + ephemPublicKey: naclUtil.encodeBase64(ephemeralKeyPair.publicKey), + ciphertext: naclUtil.encodeBase64(encryptedMessage), + }; + // return encrypted msg data + return output; + } + + default: + throw new Error('Encryption type/version not supported'); + } +} + +/** + * Encrypt a message in a way that obscures the message length. + * + * The message is padded to a multiple of 2048 before being encrypted so that the length of the + * resulting encrypted message can't be used to guess the exact length of the original message. + * + * @param options - The encryption options. + * @param options.publicKey - The public key of the message recipient. + * @param options.data - The message data. + * @param options.version - The type of encryption to use. + * @returns The encrypted data. + */ +export function encryptSafely({ + publicKey, + data, + version, +}: { + publicKey: string; + data: unknown; + version: string; +}): EthEncryptedData { + if (isNullish(publicKey)) { + throw new Error('Missing publicKey parameter'); + } else if (isNullish(data)) { + throw new Error('Missing data parameter'); + } else if (isNullish(version)) { + throw new Error('Missing version parameter'); + } + + const DEFAULT_PADDING_LENGTH = 2 ** 11; + const NACL_EXTRA_BYTES = 16; + + if (typeof data === 'object' && data && 'toJSON' in data) { + // remove toJSON attack vector + // TODO, check all possible children + throw new Error( + 'Cannot encrypt with toJSON property. Please remove toJSON property', + ); + } + + // add padding + const dataWithPadding = { + data, + padding: '', + }; + + // calculate padding + const dataLength = Buffer.byteLength( + JSON.stringify(dataWithPadding), + 'utf-8', + ); + const modVal = dataLength % DEFAULT_PADDING_LENGTH; + let padLength = 0; + // Only pad if necessary + if (modVal > 0) { + padLength = DEFAULT_PADDING_LENGTH - modVal - NACL_EXTRA_BYTES; // nacl extra bytes + } + dataWithPadding.padding = '0'.repeat(padLength); + + const paddedMessage = JSON.stringify(dataWithPadding); + return encrypt({ publicKey, data: paddedMessage, version }); +} + +/** + * Decrypt a message. + * + * @param options - The decryption options. + * @param options.encryptedData - The encrypted data. + * @param options.privateKey - The private key to decrypt with. + * @returns The decrypted message. + */ +export function decrypt({ + encryptedData, + privateKey, +}: { + encryptedData: EthEncryptedData; + privateKey: string; +}): string { + if (isNullish(encryptedData)) { + throw new Error('Missing encryptedData parameter'); + } else if (isNullish(privateKey)) { + throw new Error('Missing privateKey parameter'); + } + + switch (encryptedData.version) { + case 'x25519-xsalsa20-poly1305': { + // string to buffer to UInt8Array + const receiverPrivateKeyUint8Array = naclDecodeHex(privateKey); + const receiverEncryptionPrivateKey = nacl.box.keyPair.fromSecretKey( + receiverPrivateKeyUint8Array, + ).secretKey; + + // assemble decryption parameters + const nonce = naclUtil.decodeBase64(encryptedData.nonce); + const ciphertext = naclUtil.decodeBase64(encryptedData.ciphertext); + const ephemPublicKey = naclUtil.decodeBase64( + encryptedData.ephemPublicKey, + ); + + // decrypt + const decryptedMessage = nacl.box.open( + ciphertext, + nonce, + ephemPublicKey, + receiverEncryptionPrivateKey, + ); + + // return decrypted msg data + try { + if (!decryptedMessage) { + throw new Error(); + } + const output = naclUtil.encodeUTF8(decryptedMessage); + // TODO: This is probably extraneous but was kept to minimize changes during refactor + if (!output) { + throw new Error(); + } + return output; + } catch (err) { + if (err && typeof err.message === 'string' && err.message.length) { + throw new Error(`Decryption failed: ${err.message as string}`); + } + throw new Error(`Decryption failed.`); + } + } + + default: + throw new Error('Encryption type/version not supported.'); + } +} + +/** + * Decrypt a message that has been encrypted using `encryptSafely`. + * + * @param options - The decryption options. + * @param options.encryptedData - The encrypted data. + * @param options.privateKey - The private key to decrypt with. + * @returns The decrypted message. + */ +export function decryptSafely({ + encryptedData, + privateKey, +}: { + encryptedData: EthEncryptedData; + privateKey: string; +}): string { + if (isNullish(encryptedData)) { + throw new Error('Missing encryptedData parameter'); + } else if (isNullish(privateKey)) { + throw new Error('Missing privateKey parameter'); + } + + const dataWithPadding = JSON.parse(decrypt({ encryptedData, privateKey })); + return dataWithPadding.data; +} + +/** + * Get the encryption public key for the given key. + * + * @param privateKey - The private key to generate the encryption public key with. + * @returns The encryption public key. + */ +export function getEncryptionPublicKey(privateKey: string): string { + const privateKeyUint8Array = naclDecodeHex(privateKey); + const encryptionPublicKey = + nacl.box.keyPair.fromSecretKey(privateKeyUint8Array).publicKey; + return naclUtil.encodeBase64(encryptionPublicKey); +} + +/** + * Convert a hex string to the UInt8Array format used by nacl. + * + * @param msgHex - The string to convert. + * @returns The converted string. + */ +function naclDecodeHex(msgHex: string): Uint8Array { + const msgBase64 = Buffer.from(msgHex, 'hex').toString('base64'); + return naclUtil.decodeBase64(msgBase64); +} diff --git a/yarn.lock b/yarn.lock index db82f7eb..3a66fc63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -903,7 +903,8 @@ __metadata: "@metamask/eslint-config-nodejs": ^11.1.0 "@metamask/eslint-config-typescript": ^11.1.0 "@metamask/utils": ^8.1.0 - "@scure/base": ~1.1.3 + "@noble/ciphers": ^0.5.2 + "@scure/base": ~1.1.6 "@types/jest": ^27.0.6 "@types/node": ^16.18.50 "@typescript-eslint/eslint-plugin": ^5.59.1 @@ -916,13 +917,14 @@ __metadata: eslint-plugin-jsdoc: ^39.6.2 eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.2.1 - ethereum-cryptography: ^2.1.2 + ethereum-cryptography: ^2.1.3 jest: ^27.0.6 prettier: ^2.3.2 prettier-plugin-packagejson: ^2.2.11 rimraf: ^3.0.2 ts-jest: ^27.0.3 tweetnacl: ^1.0.3 + tweetnacl-util: ^0.15.1 typedoc: ^0.24.6 typescript: ~4.8.4 languageName: unknown @@ -945,26 +947,26 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0": - version: 1.1.0 - resolution: "@noble/curves@npm:1.1.0" - dependencies: - "@noble/hashes": 1.3.1 - checksum: 2658cdd3f84f71079b4e3516c47559d22cf4b55c23ac8ee9d2b1f8e5b72916d9689e59820e0f9d9cb4a46a8423af5b56dc6bb7782405c88be06a015180508db5 +"@noble/ciphers@npm:^0.5.2": + version: 0.5.2 + resolution: "@noble/ciphers@npm:0.5.2" + checksum: b3b32c8cb9ef4450358116edf97900d20465eaf76c3103cbb72851da0b6b5815cb2a65b24f5a1d1abd8658064cb29d75b17512dbe97bc546273a63fc3306a599 languageName: node linkType: hard -"@noble/hashes@npm:1.3.1": - version: 1.3.1 - resolution: "@noble/hashes@npm:1.3.1" - checksum: 7fdefc0f7a0c1ec27acc6ff88841793e3f93ec4ce6b8a6a12bfc0dd70ae6b7c4c82fe305fdfeda1735d5ad4a9eebe761e6693b3d355689c559e91242f4bc95b1 +"@noble/curves@npm:1.3.0, @noble/curves@npm:~1.3.0": + version: 1.3.0 + resolution: "@noble/curves@npm:1.3.0" + dependencies: + "@noble/hashes": 1.3.3 + checksum: b65342ee66c4a440eee2978524412eabba9a9efdd16d6370e15218c6a7d80bddf35e66bb57ed52c0dfd32cb9a717b439ab3a72db618f1a0066dfebe3fd12a421 languageName: node linkType: hard -"@noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": - version: 1.3.2 - resolution: "@noble/hashes@npm:1.3.2" - checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 +"@noble/hashes@npm:1.3.3, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.2": + version: 1.3.3 + resolution: "@noble/hashes@npm:1.3.3" + checksum: 8a6496d1c0c64797339bc694ad06cdfaa0f9e56cd0c3f68ae3666cfb153a791a55deb0af9c653c7ed2db64d537aa3e3054629740d2f2338bb1dcb7ab60cd205b languageName: node linkType: hard @@ -1044,38 +1046,31 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.3": +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.4, @scure/base@npm:~1.1.6": version: 1.1.6 resolution: "@scure/base@npm:1.1.6" checksum: d6deaae91deba99e87939af9e55d80edba302674983f32bba57f942e22b1726a83c62dc50d8f4370a5d5d35a212dda167fb169f4b0d0c297488d8604608fc3d3 languageName: node linkType: hard -"@scure/base@npm:~1.1.0": - version: 1.1.1 - resolution: "@scure/base@npm:1.1.1" - checksum: b4fc810b492693e7e8d0107313ac74c3646970c198bbe26d7332820886fa4f09441991023ec9aa3a2a51246b74409ab5ebae2e8ef148bbc253da79ac49130309 - languageName: node - linkType: hard - -"@scure/bip32@npm:1.3.1": - version: 1.3.1 - resolution: "@scure/bip32@npm:1.3.1" +"@scure/bip32@npm:1.3.3": + version: 1.3.3 + resolution: "@scure/bip32@npm:1.3.3" dependencies: - "@noble/curves": ~1.1.0 - "@noble/hashes": ~1.3.1 - "@scure/base": ~1.1.0 - checksum: 394d65f77a40651eba21a5096da0f4233c3b50d422864751d373fcf142eeedb94a1149f9ab1dbb078086dab2d0bc27e2b1afec8321bf22d4403c7df2fea5bfe2 + "@noble/curves": ~1.3.0 + "@noble/hashes": ~1.3.2 + "@scure/base": ~1.1.4 + checksum: f939ca733972622fcc1e61d4fdf170a0ad294b24ddb7ed7cdd4c467e1ef283b970154cb101cf5f1a7b64cf5337e917ad31135911dfc36b1d76625320167df2fa languageName: node linkType: hard -"@scure/bip39@npm:1.2.1": - version: 1.2.1 - resolution: "@scure/bip39@npm:1.2.1" +"@scure/bip39@npm:1.2.2": + version: 1.2.2 + resolution: "@scure/bip39@npm:1.2.2" dependencies: - "@noble/hashes": ~1.3.0 - "@scure/base": ~1.1.0 - checksum: c5bd6f1328fdbeae2dcdd891825b1610225310e5e62a4942714db51066866e4f7bef242c7b06a1b9dcc8043a4a13412cf5c5df76d3b10aa9e36b82e9b6e3eeaa + "@noble/hashes": ~1.3.2 + "@scure/base": ~1.1.4 + checksum: cb99505e6d2deef8e55e81df8c563ce8dbfdf1595596dc912bceadcf366c91b05a98130e928ecb090df74efdb20150b64acc4be55bc42768cab4d39a2833d234 languageName: node linkType: hard @@ -2703,15 +2698,15 @@ __metadata: languageName: node linkType: hard -"ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.2": - version: 2.1.2 - resolution: "ethereum-cryptography@npm:2.1.2" +"ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.3": + version: 2.1.3 + resolution: "ethereum-cryptography@npm:2.1.3" dependencies: - "@noble/curves": 1.1.0 - "@noble/hashes": 1.3.1 - "@scure/bip32": 1.3.1 - "@scure/bip39": 1.2.1 - checksum: 2e8f7b8cc90232ae838ab6a8167708e8362621404d26e79b5d9e762c7b53d699f7520aff358d9254de658fcd54d2d0af168ff909943259ed27dc4cef2736410c + "@noble/curves": 1.3.0 + "@noble/hashes": 1.3.3 + "@scure/bip32": 1.3.3 + "@scure/bip39": 1.2.2 + checksum: 7f9c14f868a588641179cace3eb86c332c4743290865db699870710253cabc4dc74bd4bce5e7bc6db667482e032e94d6f79521219eb6be5dc422059d279a27b7 languageName: node linkType: hard @@ -5808,6 +5803,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl-util@npm:^0.15.1": + version: 0.15.1 + resolution: "tweetnacl-util@npm:0.15.1" + checksum: ae6aa8a52cdd21a95103a4cc10657d6a2040b36c7a6da7b9d3ab811c6750a2d5db77e8c36969e75fdee11f511aa2b91c552496c6e8e989b6e490e54aca2864fc + languageName: node + linkType: hard + "tweetnacl@npm:^1.0.3": version: 1.0.3 resolution: "tweetnacl@npm:1.0.3"