From 103fec6854338e8c45db4e4525d08c6d49ad873d Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Thu, 12 Oct 2023 14:22:38 +0200 Subject: [PATCH] feat: introduce encryption result version --- src/index.ts | 98 +++- test/index.spec.ts | 1239 +++++++++++++++++++++++++++++++------------- 2 files changed, 962 insertions(+), 375 deletions(-) diff --git a/src/index.ts b/src/index.ts index 45fd310..cf6caf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,16 @@ export type DetailedEncryptionResult = { exportedKeyString: string; }; +export enum EncryptionResultVersion { + V1 = '1.0', + V2 = '2.0', +} + export type EncryptionResult = { data: string; iv: string; salt?: string; + version?: EncryptionResultVersion; }; export type DetailedDecryptResult = { @@ -18,6 +24,10 @@ export type DetailedDecryptResult = { const EXPORT_FORMAT = 'jwk'; const DERIVED_KEY_FORMAT = 'AES-GCM'; const STRING_ENCODING = 'utf-8'; +const KEY_DERIVATION_ITERATIONS = { + [EncryptionResultVersion.V1]: 10000, + [EncryptionResultVersion.V2]: 600000, +}; /** * Encrypts a data object that can be any serializable value using @@ -27,6 +37,7 @@ const STRING_ENCODING = 'utf-8'; * @param dataObj - The data to encrypt. * @param key - The CryptoKey to encrypt with. * @param salt - The salt to use to encrypt. + * @param version - The encryption version to use. * @returns The encrypted vault. */ export async function encrypt( @@ -34,9 +45,12 @@ export async function encrypt( dataObj: R, key?: CryptoKey, salt: string = generateSalt(), + version = EncryptionResultVersion.V2, ): Promise { - const cryptoKey = key || (await keyFromPassword(password, salt)); - const payload = await encryptWithKey(cryptoKey, dataObj); + const cryptoKey = + key || + (await keyFromPassword(password, salt, KEY_DERIVATION_ITERATIONS[version])); + const payload = await encryptWithKey(cryptoKey, dataObj, version); payload.salt = salt; return JSON.stringify(payload); } @@ -48,16 +62,23 @@ export async function encrypt( * @param password - A password to use for encryption. * @param dataObj - The data to encrypt. * @param salt - The salt used to encrypt. + * @param version - The encryption version to use. * @returns The vault and exported key string. */ export async function encryptWithDetail( password: string, dataObj: R, salt = generateSalt(), + version = EncryptionResultVersion.V2, ): Promise { - const key = await keyFromPassword(password, salt, true); + const key = await keyFromPassword( + password, + salt, + KEY_DERIVATION_ITERATIONS[version], + true, + ); const exportedKeyString = await exportKey(key); - const vault = await encrypt(password, dataObj, key, salt); + const vault = await encrypt(password, dataObj, key, salt, version); return { vault, @@ -72,11 +93,13 @@ export async function encryptWithDetail( * * @param key - The CryptoKey to encrypt with. * @param dataObj - A serializable JavaScript object to encrypt. + * @param version - The encryption version to use. * @returns The encrypted data. */ export async function encryptWithKey( key: CryptoKey, dataObj: R, + version = EncryptionResultVersion.V2, ): Promise { const data = JSON.stringify(dataObj); const dataBuffer = Buffer.from(data, STRING_ENCODING); @@ -95,6 +118,7 @@ export async function encryptWithKey( const vectorStr = Buffer.from(vector).toString('base64'); const vaultStr = Buffer.from(buffer).toString('base64'); return { + version, data: vaultStr, iv: vectorStr, }; @@ -115,9 +139,35 @@ export async function decrypt( key?: CryptoKey, ): Promise { const payload = JSON.parse(text); - const { salt } = payload; + const { salt, version } = payload; - const cryptoKey = key || (await keyFromPassword(password, salt)); + if (key) { + return decryptWithKey(key, payload); + } + + let cryptoKey: CryptoKey; + switch (version) { + case EncryptionResultVersion.V1: + cryptoKey = await keyFromPassword( + password, + salt, + KEY_DERIVATION_ITERATIONS[EncryptionResultVersion.V1], + ); + break; + case EncryptionResultVersion.V2: + cryptoKey = await keyFromPassword( + password, + salt, + KEY_DERIVATION_ITERATIONS[EncryptionResultVersion.V2], + ); + break; + default: + cryptoKey = await keyFromPassword( + password, + salt, + KEY_DERIVATION_ITERATIONS[EncryptionResultVersion.V1], + ); + } const result = await decryptWithKey(cryptoKey, payload); return result; @@ -136,8 +186,36 @@ export async function decryptWithDetail( text: string, ): Promise { const payload = JSON.parse(text); - const { salt } = payload; - const key = await keyFromPassword(password, salt, true); + const { salt, version } = payload; + let key: CryptoKey; + switch (version) { + case EncryptionResultVersion.V1: + key = await keyFromPassword( + password, + salt, + KEY_DERIVATION_ITERATIONS[EncryptionResultVersion.V1], + true, + ); + break; + case EncryptionResultVersion.V2: + key = await keyFromPassword( + password, + salt, + KEY_DERIVATION_ITERATIONS[EncryptionResultVersion.V2], + true, + ); + break; + default: + key = await keyFromPassword( + password, + salt, + // This ensures compatibility with data encrypted with + // older versions, which may not even have a `version` property, + // and for those '1.0' should be inferred. + KEY_DERIVATION_ITERATIONS[EncryptionResultVersion.V1], + true, + ); + } const exportedKeyString = await exportKey(key); const vault = await decrypt(password, text, key); @@ -216,12 +294,14 @@ export async function exportKey(key: CryptoKey): Promise { * * @param password - The password to use to generate key. * @param salt - The salt string to use in key derivation. + * @param iterations - The number of iterations used to derive the key. * @param exportable - Should the derived key be exportable. * @returns A CryptoKey for encryption and decryption. */ export async function keyFromPassword( password: string, salt: string, + iterations = KEY_DERIVATION_ITERATIONS[EncryptionResultVersion.V2], exportable = false, ): Promise { const passBuffer = Buffer.from(password, STRING_ENCODING); @@ -239,7 +319,7 @@ export async function keyFromPassword( { name: 'PBKDF2', salt: saltBuffer, - iterations: 10000, + iterations, hash: 'SHA-256', }, key, diff --git a/test/index.spec.ts b/test/index.spec.ts index db105c7..018157d 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -140,191 +140,651 @@ test('encryptor:encrypt & decrypt with wrong password', async ({ page }) => { * `a sample passw0rd`. This should be left unmodified, as it's used to test * that decrypting older encrypted data continues to work. */ -const sampleEncryptedData = { +const sampleEncryptedDataV1 = { data: 'bfCvija6QfwqARmHsKT7ZR0GHi8yjz7iVEZodRVx3xI2yzFHwq7+B/U=', iv: 'N9s46G5sp37A7wtf3vo/LA==', salt: '+uzzUKmbAdwkjw8rILhJvZE9dOfz2ecF5Gtf7yNkyyE=', }; -test('encryptor:decrypt encrypted data', async ({ page }) => { - const password = 'a sample passw0rd'; - const expectedData = { foo: 'data to encrypt' }; +const sampleEncryptedDataV2 = { + version: '2.0', + data: '/7Pqz+WFluKFlmPhG3fKEHalzZqEvwvtFZibXVK3133fhtp72MJnEmY=', + iv: '3DkwjtOhrfjmmhueQL/hOA==', + salt: '0u9YlQFIaC8l8nUnq+K0M8/cMy4xKYEeHE6XCSGuTtQ=', +}; - const decryptedData = await page.evaluate( - async (args) => - await window.encryptor.decrypt( - args.password, - JSON.stringify(args.sampleEncryptedData), +test.describe('with version 1.0', () => { + test('encryptor:decrypt encrypted data', async ({ page }) => { + const password = 'a sample passw0rd'; + const expectedData = { foo: 'data to encrypt' }; + + const decryptedData = await page.evaluate( + async (args) => + await window.encryptor.decrypt( + args.password, + JSON.stringify(args.sampleEncryptedDataV1), + ), + { sampleEncryptedDataV1, password }, + ); + + expect(decryptedData).toStrictEqual(expectedData); + }); + + test('encryptor:decrypt encrypted data using wrong password', async ({ + page, + }) => { + const wrongPassword = 'a wrong password'; + + await expect( + page.evaluate( + async (args) => + await window.encryptor.decrypt( + args.wrongPassword, + JSON.stringify(args.sampleEncryptedDataV1), + ), + { sampleEncryptedDataV1, wrongPassword }, ), - { sampleEncryptedData, password }, - ); + ).rejects.toThrow('Incorrect password'); + }); - expect(decryptedData).toStrictEqual(expectedData); -}); + test('encryptor:decryptWithDetail returns same vault as decrypt', async ({ + page, + }) => { + const password = 'a sample passw0rd'; -test('encryptor:decrypt encrypted data using wrong password', async ({ - page, -}) => { - const wrongPassword = 'a wrong password'; + const decryptResult = await page.evaluate( + async (args) => { + return await window.encryptor.decrypt( + args.password, + JSON.stringify(args.sampleEncryptedDataV1), + ); + }, + { password, sampleEncryptedDataV1 }, + ); - await expect( - page.evaluate( + const decryptWithDetailResult = await page.evaluate( + async (args) => { + return await window.encryptor.decryptWithDetail( + args.password, + JSON.stringify(args.sampleEncryptedDataV1), + ); + }, + { password, sampleEncryptedDataV1 }, + ); + + expect(JSON.stringify(decryptResult)).toStrictEqual( + JSON.stringify(decryptWithDetailResult.vault), + ); + expect(Object.keys(decryptWithDetailResult).length).toBe(3); + expect(typeof decryptWithDetailResult.exportedKeyString).toStrictEqual( + 'string', + ); + }); + + test('encryptor:encrypt using key then decrypt', async ({ page }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); + + const encryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + ); + return await window.encryptor.encryptWithKey(key, args.data); + }, + { data, password, salt }, + ); + expect(Object.keys(encryptedData).sort()).toStrictEqual([ + 'data', + 'iv', + 'version', + ]); + + const encryptedString = JSON.stringify( + Object.assign({}, encryptedData, { salt }), + ); + const decryptedData = await page.evaluate( async (args) => - await window.encryptor.decrypt( - args.wrongPassword, - JSON.stringify(args.sampleEncryptedData), - ), - { sampleEncryptedData, wrongPassword }, - ), - ).rejects.toThrow('Incorrect password'); -}); + await window.encryptor.decrypt(args.password, args.encryptedString), + { encryptedString, password }, + ); -test('encryptor:decryptWithDetail returns same vault as decrypt', async ({ - page, -}) => { - const password = 'a sample passw0rd'; + expect(decryptedData).toStrictEqual(data); + }); - const decryptResult = await page.evaluate( - async (args) => { - return await window.encryptor.decrypt( - args.password, - JSON.stringify(args.sampleEncryptedData), - ); - }, - { password, sampleEncryptedData }, - ); + test('encryptor:encrypt using key then decrypt using wrong password', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const wrongPassword = 'a wrong password'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); - const decryptWithDetailResult = await page.evaluate( - async (args) => { - return await window.encryptor.decryptWithDetail( - args.password, - JSON.stringify(args.sampleEncryptedData), - ); - }, - { password, sampleEncryptedData }, - ); + const encryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + ); + return await window.encryptor.encryptWithKey(key, args.data); + }, + { data, password, salt }, + ); + expect(Object.keys(encryptedData).sort()).toStrictEqual([ + 'data', + 'iv', + 'version', + ]); + + const encryptedString = JSON.stringify( + Object.assign({}, encryptedData, { salt }), + ); + await expect( + page.evaluate( + async (args) => + await window.encryptor.decrypt( + args.wrongPassword, + args.encryptedString, + ), + { encryptedString, wrongPassword }, + ), + ).rejects.toThrow('Incorrect password'); + }); - expect(JSON.stringify(decryptResult)).toStrictEqual( - JSON.stringify(decryptWithDetailResult.vault), - ); - expect(Object.keys(decryptWithDetailResult).length).toBe(3); - expect(typeof decryptWithDetailResult.exportedKeyString).toStrictEqual( - 'string', - ); -}); + test('encryptor:encrypt then decrypt using key', async ({ page }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + + const encryptedString = await page.evaluate( + async (args) => await window.encryptor.encrypt(args.password, args.data), + { data, password }, + ); + expect(typeof encryptedString).toBe('string'); + const encryptedData = JSON.parse(encryptedString); + const { salt } = encryptedData; + const encryptedPayload = { + data: encryptedData.data, + iv: encryptedData.iv, + version: encryptedData.version, + }; + + const decryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + ); + return await window.encryptor.decryptWithKey( + key, + args.encryptedPayload, + ); + }, + { encryptedPayload, password, salt }, + ); -test('encryptor:encrypt using key then decrypt', async ({ page }) => { - const password = 'a sample passw0rd'; - const data = { foo: 'data to encrypt' }; - const salt = await page.evaluate(() => window.encryptor.generateSalt()); + expect(decryptedData).toStrictEqual(data); + }); - const encryptedData = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); - return await window.encryptor.encryptWithKey(key, args.data); - }, - { data, password, salt }, - ); - expect(Object.keys(encryptedData).sort()).toStrictEqual(['data', 'iv']); + test('encryptor:encrypt then decrypt using key derived from wrong password', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const wrongPassword = 'a wrong password'; + const data = { foo: 'data to encrypt' }; + + const encryptedString = await page.evaluate( + async (args) => await window.encryptor.encrypt(args.password, args.data), + { data, password }, + ); + expect(typeof encryptedString).toBe('string'); + const encryptedData = JSON.parse(encryptedString); + const { salt } = encryptedData; + const encryptedPayload = { + data: encryptedData.data, + iv: encryptedData.iv, + version: encryptedData.version, + }; + + await expect( + page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.wrongPassword, + args.salt, + ); + return await window.encryptor.decryptWithKey( + key, + args.encryptedPayload, + ); + }, + { encryptedPayload, salt, wrongPassword }, + ), + ).rejects.toThrow('Incorrect password'); + }); - const encryptedString = JSON.stringify( - Object.assign({}, encryptedData, { salt }), - ); - const decryptedData = await page.evaluate( - async (args) => - await window.encryptor.decrypt(args.password, args.encryptedString), - { encryptedString, password }, - ); + test('encryptor:decrypt encrypted data using key', async ({ page }) => { + const password = 'a sample passw0rd'; + const expectedData = { foo: 'data to encrypt' }; + const encryptedPayload = { + data: sampleEncryptedDataV1.data, + iv: sampleEncryptedDataV1.iv, + }; + const { salt } = sampleEncryptedDataV1; - expect(decryptedData).toStrictEqual(data); -}); + const decryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + 10000, + ); + return await window.encryptor.decryptWithKey( + key, + args.encryptedPayload, + ); + }, + { encryptedPayload, password, salt }, + ); -test('encryptor:encrypt using key then decrypt using wrong password', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - const wrongPassword = 'a wrong password'; - const data = { foo: 'data to encrypt' }; - const salt = await page.evaluate(() => window.encryptor.generateSalt()); + expect(decryptedData).toStrictEqual(expectedData); + }); - const encryptedData = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); - return await window.encryptor.encryptWithKey(key, args.data); - }, - { data, password, salt }, - ); - expect(Object.keys(encryptedData).sort()).toStrictEqual(['data', 'iv']); + test('encryptor:decrypt encrypted data using key derived from wrong password', async ({ + page, + }) => { + const wrongPassword = 'a wrong password'; + const encryptedPayload = { + data: sampleEncryptedDataV1.data, + iv: sampleEncryptedDataV1.iv, + }; + const { salt } = sampleEncryptedDataV1; + + await expect( + page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.wrongPassword, + args.salt, + ); + return await window.encryptor.decryptWithKey( + key, + args.encryptedPayload, + ); + }, + { encryptedPayload, salt, wrongPassword }, + ), + ).rejects.toThrow('Incorrect password'); + }); - const encryptedString = JSON.stringify( - Object.assign({}, encryptedData, { salt }), - ); - await expect( - page.evaluate( + test('encryptor:importKey generates valid CryptoKey', async ({ page }) => { + const isKey = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.SAMPLE_EXPORTED_KEY); + return key instanceof CryptoKey; + }, + { SAMPLE_EXPORTED_KEY }, + ); + expect(isKey).toBe(true); + }); + + test('encryptor:exportKey generates valid CryptoKey string', async ({ + page, + }) => { + const keyString = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.SAMPLE_EXPORTED_KEY); + return await window.encryptor.exportKey(key); + }, + { SAMPLE_EXPORTED_KEY }, + ); + expect(keyString).toStrictEqual(SAMPLE_EXPORTED_KEY); + }); + + test('encryptor:encryptWithDetail and decryptWithDetail provide same data', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + + const { vault } = await page.evaluate( + async (args) => + await window.encryptor.encryptWithDetail(args.password, args.data), + { data, password }, + ); + + const decryptedDetail = await page.evaluate( + async (args) => + await window.encryptor.decryptWithDetail(args.password, args.data), + { data: vault, password }, + ); + + expect(JSON.stringify(decryptedDetail.vault)).toStrictEqual( + JSON.stringify(data), + ); + }); + + test('encryptor:decryptWithKey provide same data when using exported key from encryptWithDetail', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + + const { vault, exportedKeyString } = await page.evaluate( + async (args) => + await window.encryptor.encryptWithDetail(args.password, args.data), + { data, password }, + ); + + // Use the exported key and vault to properly decrypt the data + const decryptWithKeyResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.keyString); + return await window.encryptor.decryptWithKey( + key, + JSON.parse(args.data), + ); + }, + { data: vault, keyString: exportedKeyString }, + ); + + expect(JSON.stringify(decryptWithKeyResult)).toStrictEqual( + JSON.stringify(data), + ); + }); + + test('encryptor:decryptWithDetail works with password after encryption with key', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const startingData = { foo: 'data to encrypt' }; + + // Get an exported key to use + const { salt, exportedKeyString } = await page.evaluate( + async (args) => { + const usedSalt = window.encryptor.generateSalt(); + const { exportedKeyString: newKeyString } = + await window.encryptor.encryptWithDetail( + args.password, + args.data, + usedSalt, + ); + + return { + salt: usedSalt, + exportedKeyString: newKeyString, + }; + }, + { data: startingData, password }, + ); + + // Update the data, encrypt using key + const newData = { ...startingData, bar: 'more data' }; + const encryptWithKeyResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.keyString); + return await window.encryptor.encryptWithKey(key, args.data); + }, + { data: newData, keyString: exportedKeyString }, + ); + + // Mock the encrypted object + const decryptable = { + ...encryptWithKeyResult, + salt, + }; + + // Prove that a vault created with key can be decrypted with password + const decryptedResult = await page.evaluate( + async (args) => + await window.encryptor.decryptWithDetail(args.password, args.data), + { password, data: JSON.stringify(decryptable) }, + ); + + expect(JSON.stringify(decryptedResult.vault)).toStrictEqual( + JSON.stringify(newData), + ); + }); + + test('encryptor:encryptWithKey works with decryptWithKey', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const startingData = { foo: 'data to encrypt' }; + + // Get an exported key to use + const exportedKeyString = await page.evaluate( + async (args) => { + const { exportedKeyString: newKeyString } = + await window.encryptor.encryptWithDetail(args.password, args.data); + + return newKeyString; + }, + { data: startingData, password }, + ); + + // Update the data, encrypt using key + const newData = { ...startingData, bar: 'more data' }; + const encryptWithKeyResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.keyString); + const result = await window.encryptor.encryptWithKey(key, args.data); + + return { + encryptWithKeyResult: result, + exportedKeyString: await window.encryptor.exportKey(key), + }; + }, + { data: newData, keyString: exportedKeyString }, + ); + + // Prove that a vault created with key can be decrypted with password + const decryptedResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.exportedKeyString); + return await window.encryptor.decryptWithKey(key, args.data); + }, + { + exportedKeyString: encryptWithKeyResult.exportedKeyString, + data: encryptWithKeyResult.encryptWithKeyResult, + }, + ); + + expect(JSON.stringify(decryptedResult)).toStrictEqual( + JSON.stringify(newData), + ); + }); + + test('encryptor:keyFromPassword cannot be exported by default', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); + + const exportResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + ); + + try { + const result = await window.encryptor.exportKey(key); + return result; + } catch (e) { + return 'error'; + } + }, + { data, password, salt }, + ); + + expect(exportResult).toStrictEqual('error'); + }); +}); + +test.describe('with version 2.0', () => { + test('encryptor:decrypt encrypted data', async ({ page }) => { + const password = 'a sample passw0rd'; + const expectedData = { foo: 'data to encrypt' }; + + const decryptedData = await page.evaluate( async (args) => await window.encryptor.decrypt( - args.wrongPassword, - args.encryptedString, + args.password, + JSON.stringify(args.sampleEncryptedDataV2), ), - { encryptedString, wrongPassword }, - ), - ).rejects.toThrow('Incorrect password'); -}); + { sampleEncryptedDataV2, password }, + ); -test('encryptor:encrypt then decrypt using key', async ({ page }) => { - const password = 'a sample passw0rd'; - const data = { foo: 'data to encrypt' }; + expect(decryptedData).toStrictEqual(expectedData); + }); - const encryptedString = await page.evaluate( - async (args) => await window.encryptor.encrypt(args.password, args.data), - { data, password }, - ); - expect(typeof encryptedString).toBe('string'); - const encryptedData = JSON.parse(encryptedString); - const { salt } = encryptedData; - const encryptedPayload = { data: encryptedData.data, iv: encryptedData.iv }; - - const decryptedData = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); - return await window.encryptor.decryptWithKey(key, args.encryptedPayload); - }, - { encryptedPayload, password, salt }, - ); + test('encryptor:decrypt encrypted data using wrong password', async ({ + page, + }) => { + const wrongPassword = 'a wrong password'; + + await expect( + page.evaluate( + async (args) => + await window.encryptor.decrypt( + args.wrongPassword, + JSON.stringify(args.sampleEncryptedDataV2), + ), + { sampleEncryptedDataV2, wrongPassword }, + ), + ).rejects.toThrow('Incorrect password'); + }); - expect(decryptedData).toStrictEqual(data); -}); + test('encryptor:decryptWithDetail returns same vault as decrypt', async ({ + page, + }) => { + const password = 'a sample passw0rd'; -test('encryptor:encrypt then decrypt using key derived from wrong password', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - const wrongPassword = 'a wrong password'; - const data = { foo: 'data to encrypt' }; + const decryptResult = await page.evaluate( + async (args) => { + return await window.encryptor.decrypt( + args.password, + JSON.stringify(args.sampleEncryptedDataV2), + ); + }, + { password, sampleEncryptedDataV2 }, + ); - const encryptedString = await page.evaluate( - async (args) => await window.encryptor.encrypt(args.password, args.data), - { data, password }, - ); - expect(typeof encryptedString).toBe('string'); - const encryptedData = JSON.parse(encryptedString); - const { salt } = encryptedData; - const encryptedPayload = { data: encryptedData.data, iv: encryptedData.iv }; + const decryptWithDetailResult = await page.evaluate( + async (args) => { + return await window.encryptor.decryptWithDetail( + args.password, + JSON.stringify(args.sampleEncryptedDataV2), + ); + }, + { password, sampleEncryptedDataV2 }, + ); + + expect(JSON.stringify(decryptResult)).toStrictEqual( + JSON.stringify(decryptWithDetailResult.vault), + ); + expect(Object.keys(decryptWithDetailResult).length).toBe(3); + expect(typeof decryptWithDetailResult.exportedKeyString).toStrictEqual( + 'string', + ); + }); - await expect( - page.evaluate( + test('encryptor:encrypt using key then decrypt', async ({ page }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); + + const encryptedData = await page.evaluate( async (args) => { const key = await window.encryptor.keyFromPassword( - args.wrongPassword, + args.password, + args.salt, + ); + return await window.encryptor.encryptWithKey(key, args.data); + }, + { data, password, salt }, + ); + expect(Object.keys(encryptedData).sort()).toStrictEqual([ + 'data', + 'iv', + 'version', + ]); + + const encryptedString = JSON.stringify( + Object.assign({}, encryptedData, { salt }), + ); + const decryptedData = await page.evaluate( + async (args) => + await window.encryptor.decrypt(args.password, args.encryptedString), + { encryptedString, password }, + ); + + expect(decryptedData).toStrictEqual(data); + }); + + test('encryptor:encrypt using key then decrypt using wrong password', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const wrongPassword = 'a wrong password'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); + + const encryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + ); + return await window.encryptor.encryptWithKey(key, args.data); + }, + { data, password, salt }, + ); + expect(Object.keys(encryptedData).sort()).toStrictEqual([ + 'data', + 'iv', + 'version', + ]); + + const encryptedString = JSON.stringify( + Object.assign({}, encryptedData, { salt }), + ); + await expect( + page.evaluate( + async (args) => + await window.encryptor.decrypt( + args.wrongPassword, + args.encryptedString, + ), + { encryptedString, wrongPassword }, + ), + ).rejects.toThrow('Incorrect password'); + }); + + test('encryptor:encrypt then decrypt using key', async ({ page }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + + const encryptedString = await page.evaluate( + async (args) => await window.encryptor.encrypt(args.password, args.data), + { data, password }, + ); + expect(typeof encryptedString).toBe('string'); + const encryptedData = JSON.parse(encryptedString); + const { salt } = encryptedData; + const encryptedPayload = { + data: encryptedData.data, + iv: encryptedData.iv, + version: encryptedData.version, + }; + + const decryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, args.salt, ); return await window.encryptor.decryptWithKey( @@ -332,49 +792,62 @@ test('encryptor:encrypt then decrypt using key derived from wrong password', asy args.encryptedPayload, ); }, - { encryptedPayload, salt, wrongPassword }, - ), - ).rejects.toThrow('Incorrect password'); -}); + { encryptedPayload, password, salt }, + ); -test('encryptor:decrypt encrypted data using key', async ({ page }) => { - const password = 'a sample passw0rd'; - const expectedData = { foo: 'data to encrypt' }; - const encryptedPayload = { - data: sampleEncryptedData.data, - iv: sampleEncryptedData.iv, - }; - const { salt } = sampleEncryptedData; - - const decryptedData = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); - return await window.encryptor.decryptWithKey(key, args.encryptedPayload); - }, - { encryptedPayload, password, salt }, - ); + expect(decryptedData).toStrictEqual(data); + }); - expect(decryptedData).toStrictEqual(expectedData); -}); + test('encryptor:encrypt then decrypt using key derived from wrong password', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const wrongPassword = 'a wrong password'; + const data = { foo: 'data to encrypt' }; + + const encryptedString = await page.evaluate( + async (args) => await window.encryptor.encrypt(args.password, args.data), + { data, password }, + ); + expect(typeof encryptedString).toBe('string'); + const encryptedData = JSON.parse(encryptedString); + const { salt } = encryptedData; + const encryptedPayload = { + data: encryptedData.data, + iv: encryptedData.iv, + version: encryptedData.version, + }; + + await expect( + page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.wrongPassword, + args.salt, + ); + return await window.encryptor.decryptWithKey( + key, + args.encryptedPayload, + ); + }, + { encryptedPayload, salt, wrongPassword }, + ), + ).rejects.toThrow('Incorrect password'); + }); -test('encryptor:decrypt encrypted data using key derived from wrong password', async ({ - page, -}) => { - const wrongPassword = 'a wrong password'; - const encryptedPayload = { - data: sampleEncryptedData.data, - iv: sampleEncryptedData.iv, - }; - const { salt } = sampleEncryptedData; + test('encryptor:decrypt encrypted data using key', async ({ page }) => { + const password = 'a sample passw0rd'; + const expectedData = { foo: 'data to encrypt' }; + const encryptedPayload = { + data: sampleEncryptedDataV2.data, + iv: sampleEncryptedDataV2.iv, + }; + const { salt } = sampleEncryptedDataV2; - await expect( - page.evaluate( + const decryptedData = await page.evaluate( async (args) => { const key = await window.encryptor.keyFromPassword( - args.wrongPassword, + args.password, args.salt, ); return await window.encryptor.decryptWithKey( @@ -382,207 +855,241 @@ test('encryptor:decrypt encrypted data using key derived from wrong password', a args.encryptedPayload, ); }, - { encryptedPayload, salt, wrongPassword }, - ), - ).rejects.toThrow('Incorrect password'); -}); + { encryptedPayload, password, salt }, + ); -test('encryptor:importKey generates valid CryptoKey', async ({ page }) => { - const isKey = await page.evaluate( - async (args) => { - const key = await window.encryptor.importKey(args.SAMPLE_EXPORTED_KEY); - return key instanceof CryptoKey; - }, - { SAMPLE_EXPORTED_KEY }, - ); - expect(isKey).toBe(true); -}); + expect(decryptedData).toStrictEqual(expectedData); + }); -test('encryptor:exportKey generates valid CryptoKey string', async ({ - page, -}) => { - const keyString = await page.evaluate( - async (args) => { - const key = await window.encryptor.importKey(args.SAMPLE_EXPORTED_KEY); - return await window.encryptor.exportKey(key); - }, - { SAMPLE_EXPORTED_KEY }, - ); - expect(keyString).toStrictEqual(SAMPLE_EXPORTED_KEY); -}); + test('encryptor:decrypt encrypted data using key derived from wrong password', async ({ + page, + }) => { + const wrongPassword = 'a wrong password'; + const encryptedPayload = { + data: sampleEncryptedDataV2.data, + iv: sampleEncryptedDataV2.iv, + }; + const { salt } = sampleEncryptedDataV2; + + await expect( + page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.wrongPassword, + args.salt, + ); + return await window.encryptor.decryptWithKey( + key, + args.encryptedPayload, + ); + }, + { encryptedPayload, salt, wrongPassword }, + ), + ).rejects.toThrow('Incorrect password'); + }); -test('encryptor:encryptWithDetail and decryptWithDetail provide same data', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - const data = { foo: 'data to encrypt' }; + test('encryptor:importKey generates valid CryptoKey', async ({ page }) => { + const isKey = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.SAMPLE_EXPORTED_KEY); + return key instanceof CryptoKey; + }, + { SAMPLE_EXPORTED_KEY }, + ); + expect(isKey).toBe(true); + }); - const { vault } = await page.evaluate( - async (args) => - await window.encryptor.encryptWithDetail(args.password, args.data), - { data, password }, - ); + test('encryptor:exportKey generates valid CryptoKey string', async ({ + page, + }) => { + const keyString = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.SAMPLE_EXPORTED_KEY); + return await window.encryptor.exportKey(key); + }, + { SAMPLE_EXPORTED_KEY }, + ); + expect(keyString).toStrictEqual(SAMPLE_EXPORTED_KEY); + }); - const decryptedDetail = await page.evaluate( - async (args) => - await window.encryptor.decryptWithDetail(args.password, args.data), - { data: vault, password }, - ); + test('encryptor:encryptWithDetail and decryptWithDetail provide same data', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; - expect(JSON.stringify(decryptedDetail.vault)).toStrictEqual( - JSON.stringify(data), - ); -}); + const { vault } = await page.evaluate( + async (args) => + await window.encryptor.encryptWithDetail(args.password, args.data), + { data, password }, + ); -test('encryptor:decryptWithKey provide same data when using exported key from encryptWithDetail', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - const data = { foo: 'data to encrypt' }; + const decryptedDetail = await page.evaluate( + async (args) => + await window.encryptor.decryptWithDetail(args.password, args.data), + { data: vault, password }, + ); - const { vault, exportedKeyString } = await page.evaluate( - async (args) => - await window.encryptor.encryptWithDetail(args.password, args.data), - { data, password }, - ); + expect(JSON.stringify(decryptedDetail.vault)).toStrictEqual( + JSON.stringify(data), + ); + }); - // Use the exported key and vault to properly decrypt the data - const decryptWithKeyResult = await page.evaluate( - async (args) => { - const key = await window.encryptor.importKey(args.keyString); - return await window.encryptor.decryptWithKey(key, JSON.parse(args.data)); - }, - { data: vault, keyString: exportedKeyString }, - ); + test('encryptor:decryptWithKey provide same data when using exported key from encryptWithDetail', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; - expect(JSON.stringify(decryptWithKeyResult)).toStrictEqual( - JSON.stringify(data), - ); -}); + const { vault, exportedKeyString } = await page.evaluate( + async (args) => + await window.encryptor.encryptWithDetail(args.password, args.data), + { data, password }, + ); -test('encryptor:decryptWithDetail works with password after encryption with key', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - const startingData = { foo: 'data to encrypt' }; - - // Get an exported key to use - const { salt, exportedKeyString } = await page.evaluate( - async (args) => { - const usedSalt = window.encryptor.generateSalt(); - const { exportedKeyString: newKeyString } = - await window.encryptor.encryptWithDetail( - args.password, - args.data, - usedSalt, + // Use the exported key and vault to properly decrypt the data + const decryptWithKeyResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.keyString); + return await window.encryptor.decryptWithKey( + key, + JSON.parse(args.data), ); + }, + { data: vault, keyString: exportedKeyString }, + ); - return { - salt: usedSalt, - exportedKeyString: newKeyString, - }; - }, - { data: startingData, password }, - ); + expect(JSON.stringify(decryptWithKeyResult)).toStrictEqual( + JSON.stringify(data), + ); + }); - // Update the data, encrypt using key - const newData = { ...startingData, bar: 'more data' }; - const encryptWithKeyResult = await page.evaluate( - async (args) => { - const key = await window.encryptor.importKey(args.keyString); - return await window.encryptor.encryptWithKey(key, args.data); - }, - { data: newData, keyString: exportedKeyString }, - ); + test('encryptor:decryptWithDetail works with password after encryption with key', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const startingData = { foo: 'data to encrypt' }; - // Mock the encrypted object - const decryptable = { - ...encryptWithKeyResult, - salt, - }; + // Get an exported key to use + const { salt, exportedKeyString } = await page.evaluate( + async (args) => { + const usedSalt = window.encryptor.generateSalt(); + const { exportedKeyString: newKeyString } = + await window.encryptor.encryptWithDetail( + args.password, + args.data, + usedSalt, + ); + + return { + salt: usedSalt, + exportedKeyString: newKeyString, + }; + }, + { data: startingData, password }, + ); - // Prove that a vault created with key can be decrypted with password - const decryptedResult = await page.evaluate( - async (args) => - await window.encryptor.decryptWithDetail(args.password, args.data), - { password, data: JSON.stringify(decryptable) }, - ); + // Update the data, encrypt using key + const newData = { ...startingData, bar: 'more data' }; + const encryptWithKeyResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.keyString); + return await window.encryptor.encryptWithKey(key, args.data); + }, + { data: newData, keyString: exportedKeyString }, + ); - expect(JSON.stringify(decryptedResult.vault)).toStrictEqual( - JSON.stringify(newData), - ); -}); + // Mock the encrypted object + const decryptable = { + ...encryptWithKeyResult, + salt, + }; -test('encryptor:encryptWithKey works with decryptWithKey', async ({ page }) => { - const password = 'a sample passw0rd'; - const startingData = { foo: 'data to encrypt' }; + // Prove that a vault created with key can be decrypted with password + const decryptedResult = await page.evaluate( + async (args) => + await window.encryptor.decryptWithDetail(args.password, args.data), + { password, data: JSON.stringify(decryptable) }, + ); - // Get an exported key to use - const exportedKeyString = await page.evaluate( - async (args) => { - const { exportedKeyString: newKeyString } = - await window.encryptor.encryptWithDetail(args.password, args.data); + expect(JSON.stringify(decryptedResult.vault)).toStrictEqual( + JSON.stringify(newData), + ); + }); - return newKeyString; - }, - { data: startingData, password }, - ); + test('encryptor:encryptWithKey works with decryptWithKey', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const startingData = { foo: 'data to encrypt' }; - // Update the data, encrypt using key - const newData = { ...startingData, bar: 'more data' }; - const encryptWithKeyResult = await page.evaluate( - async (args) => { - const key = await window.encryptor.importKey(args.keyString); - const result = await window.encryptor.encryptWithKey(key, args.data); - - return { - encryptWithKeyResult: result, - exportedKeyString: await window.encryptor.exportKey(key), - }; - }, - { data: newData, keyString: exportedKeyString }, - ); + // Get an exported key to use + const exportedKeyString = await page.evaluate( + async (args) => { + const { exportedKeyString: newKeyString } = + await window.encryptor.encryptWithDetail(args.password, args.data); - // Prove that a vault created with key can be decrypted with password - const decryptedResult = await page.evaluate( - async (args) => { - const key = await window.encryptor.importKey(args.exportedKeyString); - return await window.encryptor.decryptWithKey(key, args.data); - }, - { - exportedKeyString: encryptWithKeyResult.exportedKeyString, - data: encryptWithKeyResult.encryptWithKeyResult, - }, - ); + return newKeyString; + }, + { data: startingData, password }, + ); - expect(JSON.stringify(decryptedResult)).toStrictEqual( - JSON.stringify(newData), - ); -}); + // Update the data, encrypt using key + const newData = { ...startingData, bar: 'more data' }; + const encryptWithKeyResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.keyString); + const result = await window.encryptor.encryptWithKey(key, args.data); -test('encryptor:keyFromPassword cannot be exported by default', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - const data = { foo: 'data to encrypt' }; - const salt = await page.evaluate(() => window.encryptor.generateSalt()); + return { + encryptWithKeyResult: result, + exportedKeyString: await window.encryptor.exportKey(key), + }; + }, + { data: newData, keyString: exportedKeyString }, + ); - const exportResult = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); - - try { - const result = await window.encryptor.exportKey(key); - return result; - } catch (e) { - return 'error'; - } - }, - { data, password, salt }, - ); + // Prove that a vault created with key can be decrypted with password + const decryptedResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.importKey(args.exportedKeyString); + return await window.encryptor.decryptWithKey(key, args.data); + }, + { + exportedKeyString: encryptWithKeyResult.exportedKeyString, + data: encryptWithKeyResult.encryptWithKeyResult, + }, + ); + + expect(JSON.stringify(decryptedResult)).toStrictEqual( + JSON.stringify(newData), + ); + }); - expect(exportResult).toStrictEqual('error'); + test('encryptor:keyFromPassword cannot be exported by default', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); + + const exportResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + ); + + try { + const result = await window.encryptor.exportKey(key); + return result; + } catch (e) { + return 'error'; + } + }, + { data, password, salt }, + ); + + expect(exportResult).toStrictEqual('error'); + }); });