diff --git a/src/encryption.test.ts b/src/encryption.test.ts index a8c330bb..77f65839 100644 --- a/src/encryption.test.ts +++ b/src/encryption.test.ts @@ -1,8 +1,10 @@ import { decrypt, decryptSafely, + decryptWithSharedSecret, encrypt, encryptSafely, + EthEncryptedData, getEncryptionPublicKey, } from './encryption'; @@ -73,6 +75,86 @@ describe('encryption', function () { expect(plaintext).toBe(secretMessage); }); + // shared secret decryption test + it('bob decrypts message that Alice sent to him with his shared secret', function () { + const encryptedDataWithHW: EthEncryptedData = { + version: 'x25519-xsalsa20-poly1305', + nonce: 'QjBaLWLlYeIDUcMjqUpDkIxoaBIck/lh', + ephemPublicKey: '/uwH3xIXDBG8ARritky4dSh9DXFNo1Jw2lSgq+Prdx0=', + ciphertext: 'pT7dEopOHWZgFQZ0cK2ia/9Ewz03xq6db/vU8glwg+deI4WiyP2lTY0s', + }; + const sharedSecret = 'Pplxc07fb6IiXAmtDJ9ebL4KRGXF9qeic0ZmktOdHCk='; + + const result = decryptWithSharedSecret({ + encryptedData: encryptedDataWithHW, + sharedSecret, + }); + expect(result).toBe(secretMessage); + }); + + it('bob decrypts invalid message that Alice sent to him with his shared secret', function () { + const encryptedDataWithHW: EthEncryptedData = { + version: 'x25519-xsalsa20-poly1305', + nonce: 'QjBaLWLlYeIDUcMjqUpDkIxoaBIck/lh', + ephemPublicKey: '/uwH3xIXDBG8ARritky4dSh9DXFNo1Jw2lSgq+Prdx0=', + ciphertext: 'pT7dEopOHWZgFQZ0cK2ia/9Ewz03xq6db/vU8glgg+deI4WiyP2lTY0s', + }; + const sharedSecret = 'Pplxc07fb6IiXAmtDJ9ebL4KRGXF9qeic0ZmktOdHCk='; + + expect(() => + decryptWithSharedSecret({ + encryptedData: encryptedDataWithHW, + sharedSecret, + }), + ).toThrow('Decryption failed.'); + }); + + it('bob decrypts unknown version message that Alice sent to him with his shared secret', function () { + const encryptedDataWithHW: EthEncryptedData = { + version: 'x25519-xsalsa21-poly1305', + nonce: 'QjBaLWLlYeIDUcMjqUpDkIxoaBIck/lh', + ephemPublicKey: '/uwH3xIXDBG8ARritky4dSh9DXFNo1Jw2lSgq+Prdx0=', + ciphertext: 'pT7dEopOHWZgFQZ0cK2ia/9Ewz03xq6db/vU8glgg+deI4WiyP2lTY0s', + }; + const sharedSecret = 'Pplxc07fb6IiXAmtDJ9ebL4KRGXF9qeic0ZmktOdHCk='; + + expect(() => + decryptWithSharedSecret({ + encryptedData: encryptedDataWithHW, + sharedSecret, + }), + ).toThrow('Encryption type/version not supported.'); + }); + + it('bob decrypts null encrypted that Alice sent to him with his shared secret', function () { + const encryptedDataWithHW: EthEncryptedData = null; + const sharedSecret = 'Pplxc07fb6IiXAmtDJ9ebL4KRGXF9qeic0ZmktOdHCk='; + + expect(() => + decryptWithSharedSecret({ + encryptedData: encryptedDataWithHW, + sharedSecret, + }), + ).toThrow('Missing encryptedData parameter'); + }); + + it('bob decrypts message that Alice sent to him with null shared secret', function () { + const encryptedDataWithHW: EthEncryptedData = { + version: 'x25519-xsalsa21-poly1305', + nonce: 'QjBaLWLlYeIDUcMjqUpDkIxoaBIck/lh', + ephemPublicKey: '/uwH3xIXDBG8ARritky4dSh9DXFNo1Jw2lSgq+Prdx0=', + ciphertext: 'pT7dEopOHWZgFQZ0cK2ia/9Ewz03xq6db/vU8glgg+deI4WiyP2lTY0s', + }; + const sharedSecret = null; + + expect(() => + decryptWithSharedSecret({ + encryptedData: encryptedDataWithHW, + sharedSecret, + }), + ).toThrow('Missing sharedSecret parameter'); + }); + // decryption test it('bob decrypts message that Alice sent to him', function () { const result = decrypt({ diff --git a/src/encryption.ts b/src/encryption.ts index ccfea714..fbd3e1e0 100644 --- a/src/encryption.ts +++ b/src/encryption.ts @@ -205,6 +205,78 @@ export function decrypt({ } } +/** + * Decrypt a message using a shared secret rather than a private key. + * + * @param options - The decryption options. + * @param options.encryptedData - The encrypted data. + * @param options.sharedSecret - The shared secret to decrypt with. + * @returns The decrypted message. + */ +export function decryptWithSharedSecret({ + encryptedData, + sharedSecret, +}: { + encryptedData: EthEncryptedData; + sharedSecret: string; +}): string { + if (isNullish(encryptedData)) { + throw new Error('Missing encryptedData parameter'); + } else if (isNullish(sharedSecret)) { + throw new Error('Missing sharedSecret parameter'); + } + + switch (encryptedData.version) { + case 'x25519-xsalsa20-poly1305': { + // assemble decryption parameters + const nonce = naclUtil.decodeBase64(encryptedData.nonce); + const ciphertext = naclUtil.decodeBase64(encryptedData.ciphertext); + const secret = naclUtil.decodeBase64(sharedSecret); + + // Hsalsa20 constant + const sigma = new Uint8Array([ + 101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107, + ]); + const _0 = new Uint8Array(16); + + // Hsalsa20 result + const privateKey = new Uint8Array(32); + + // Calculate private key + // We use any conversion here because lowlevel function of nacl are not typed + (nacl as any).lowlevel.crypto_core_hsalsa20( + privateKey, + _0, + secret, + sigma, + ); + + // decrypt + const decryptedMessage = nacl.secretbox.open( + ciphertext, + nonce, + privateKey, + ); + + // return decrypted msg data + let output; + try { + output = naclUtil.encodeUTF8(decryptedMessage); + } catch (err) { + throw new Error('Decryption failed.'); + } + + if (output) { + return output; + } + throw new Error('Decryption failed.'); + } + + default: + throw new Error('Encryption type/version not supported.'); + } +} + /** * Decrypt a message that has been encrypted using `encryptSafely`. * diff --git a/src/index.test.ts b/src/index.test.ts index 04a11665..73dbade5 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -19,6 +19,7 @@ Array [ "encrypt", "encryptSafely", "decrypt", + "decryptWithSharedSecret", "decryptSafely", "getEncryptionPublicKey", ]