From c88eabab30d88759a0324a9a40b76e3476b0ff9f Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Tue, 24 Oct 2023 13:13:16 +0200 Subject: [PATCH 01/12] refactor: increase `PBKDF2` iterations --- src/index.ts | 4 +++- test/index.spec.ts | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 45fd310..eeadedd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -217,12 +217,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 exportable - Should the derived key be exportable. + * @param iterations - The number of iterations to use in key derivation. * @returns A CryptoKey for encryption and decryption. */ export async function keyFromPassword( password: string, salt: string, exportable = false, + iterations = 900_000, ): Promise { const passBuffer = Buffer.from(password, STRING_ENCODING); const saltBuffer = Buffer.from(salt, 'base64'); @@ -239,7 +241,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..a074bf9 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -137,15 +137,53 @@ test('encryptor:encrypt & decrypt with wrong password', async ({ page }) => { /** * This is the encrypted object `{ foo: 'data to encrypt' }`, which was * encrypted using v2.0.3 of this library with the password - * `a sample passw0rd`. This should be left unmodified, as it's used to test - * that decrypting older encrypted data continues to work. + * `a sample passw0rd` and 10000 iterations. This should be left unmodified, + * as it's used to test that decrypting older encrypted data continues to work. */ -const sampleEncryptedData = { +const oldSampleEncryptedData = { data: 'bfCvija6QfwqARmHsKT7ZR0GHi8yjz7iVEZodRVx3xI2yzFHwq7+B/U=', iv: 'N9s46G5sp37A7wtf3vo/LA==', salt: '+uzzUKmbAdwkjw8rILhJvZE9dOfz2ecF5Gtf7yNkyyE=', }; +/** + * This is the encrypted object `{ foo: 'data to encrypt' }`, which was + * encrypted using v5.0.0 of this library with the password + * `a sample passw0rd` and 900.000 iterations. This should be left unmodified, + * as it's used to test that decrypting older encrypted data continues to work. + */ +const sampleEncryptedData = { + data: 'WQbagUPb+XLvSR+U7sV9jzyS+5UZfVjBiWpmJjPOlJT93dJo9kltpls=', + iv: '7NsJ8mmL1DgC5LlsIyaIXA==', + salt: 'sysHvNRoWykN/JVUSpBwXhmp0llTMQabfY7zucEfAJg=', +}; + +test('encryptor:decrypt encrypted data with key derived with different number of iterations', async ({ + page, +}) => { + const password = 'a sample passw0rd'; + const expectedData = { foo: 'data to encrypt' }; + + const decryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + 'a sample passw0rd', + args.oldSampleEncryptedData.salt, + true, + 10_000, + ); + return await window.encryptor.decrypt( + args.password, + JSON.stringify(args.oldSampleEncryptedData), + key, + ); + }, + { oldSampleEncryptedData, password }, + ); + + expect(decryptedData).toStrictEqual(expectedData); +}); + test('encryptor:decrypt encrypted data', async ({ page }) => { const password = 'a sample passw0rd'; const expectedData = { foo: 'data to encrypt' }; From 068d4397f553fea6134debc67cbdc009b2ce0487 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Wed, 25 Oct 2023 12:48:23 +0200 Subject: [PATCH 02/12] refactor: add key derivation info to encryption key and result --- src/index.ts | 124 +++++-- test/index.spec.ts | 893 +++++++++++++++++++++++---------------------- 2 files changed, 562 insertions(+), 455 deletions(-) diff --git a/src/index.ts b/src/index.ts index eeadedd..331dcdc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,27 @@ export type DetailedEncryptionResult = { exportedKeyString: string; }; +export type PBKDF2Params = { + iterations?: number; + exportable?: boolean; +}; + +export type KeyDerivationOptions = { + algorithm: 'PBKDF2'; + params: PBKDF2Params; +}; + +export type EncryptionKey = { + key: CryptoKey; + derivationFunction: KeyDerivationOptions; +}; + export type EncryptionResult = { data: string; iv: string; salt?: string; + // old encryption results will not have this + keyMetadata?: KeyDerivationOptions; }; export type DetailedDecryptResult = { @@ -32,7 +49,7 @@ const STRING_ENCODING = 'utf-8'; export async function encrypt( password: string, dataObj: R, - key?: CryptoKey, + key?: EncryptionKey | CryptoKey, salt: string = generateSalt(), ): Promise { const cryptoKey = key || (await keyFromPassword(password, salt)); @@ -55,7 +72,12 @@ export async function encryptWithDetail( dataObj: R, salt = generateSalt(), ): Promise { - const key = await keyFromPassword(password, salt, true); + const key = await keyFromPassword(password, salt, { + algorithm: 'PBKDF2', + params: { + exportable: true, + }, + }); const exportedKeyString = await exportKey(key); const vault = await encrypt(password, dataObj, key, salt); @@ -70,17 +92,19 @@ export async function encryptWithDetail( * provided CryptoKey and returns an object containing the cypher text and * the initialization vector used. * - * @param key - The CryptoKey to encrypt with. + * @param encryptionKey - The CryptoKey to encrypt with. * @param dataObj - A serializable JavaScript object to encrypt. * @returns The encrypted data. */ export async function encryptWithKey( - key: CryptoKey, + encryptionKey: EncryptionKey | CryptoKey, dataObj: R, ): Promise { const data = JSON.stringify(dataObj); const dataBuffer = Buffer.from(data, STRING_ENCODING); const vector = global.crypto.getRandomValues(new Uint8Array(16)); + const key = + encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key; const buf = await global.crypto.subtle.encrypt( { @@ -94,10 +118,16 @@ export async function encryptWithKey( const buffer = new Uint8Array(buf); const vectorStr = Buffer.from(vector).toString('base64'); const vaultStr = Buffer.from(buffer).toString('base64'); - return { + const encryptionResult: EncryptionResult = { data: vaultStr, iv: vectorStr, }; + + if (!(encryptionKey instanceof CryptoKey)) { + encryptionResult.keyMetadata = encryptionKey.derivationFunction; + } + + return encryptionResult; } /** @@ -106,18 +136,38 @@ export async function encryptWithKey( * * @param password - The password to decrypt with. * @param text - The cypher text to decrypt. - * @param key - The key to decrypt with. + * @param encryptionKey - The key to decrypt with. * @returns The decrypted data. */ export async function decrypt( password: string, text: string, - key?: CryptoKey, + encryptionKey?: EncryptionKey | CryptoKey, ): Promise { const payload = JSON.parse(text); - const { salt } = payload; + const { salt, keyMetadata } = payload; + let cryptoKey: CryptoKey; - const cryptoKey = key || (await keyFromPassword(password, salt)); + if (encryptionKey) { + cryptoKey = + encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key; + } else { + cryptoKey = ( + await keyFromPassword( + password, + salt, + keyMetadata || { + algorithm: 'PBKDF2', + params: { + exportable: true, + // we assume that this is an old vault, + // so we use the old iteration count + iterations: 10_000, + }, + }, + ) + ).key; + } const result = await decryptWithKey(cryptoKey, payload); return result; @@ -136,8 +186,20 @@ export async function decryptWithDetail( text: string, ): Promise { const payload = JSON.parse(text); - const { salt } = payload; - const key = await keyFromPassword(password, salt, true); + const { salt, keyMetadata } = payload; + const key = await keyFromPassword( + password, + salt, + keyMetadata || { + algorithm: 'PBKDF2', + params: { + exportable: true, + // we assume that this is an old vault, + // so we use the old iteration count + iterations: 10_000, + }, + }, + ); const exportedKeyString = await exportKey(key); const vault = await decrypt(password, text, key); @@ -152,16 +214,18 @@ export async function decryptWithDetail( * Given a CryptoKey and an EncryptionResult object containing the initialization * vector (iv) and data to decrypt, return the resulting decrypted value. * - * @param key - The CryptoKey to decrypt with. + * @param encryptionKey - The CryptoKey to decrypt with. * @param payload - The payload to decrypt, returned from an encryption method. * @returns The decrypted data. */ export async function decryptWithKey( - key: CryptoKey, + encryptionKey: EncryptionKey | CryptoKey, payload: EncryptionResult, ): Promise { const encryptedData = Buffer.from(payload.data, 'base64'); const vector = Buffer.from(payload.iv, 'base64'); + const key = + encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key; let decryptedObj; try { @@ -203,11 +267,16 @@ export async function importKey(keyString: string): Promise { * Receives an exported CryptoKey string, creates a key, * and decrypts cipher text with the reconstructed key. * - * @param key - The CryptoKey to export. + * @param encryptionKey - The CryptoKey to export. * @returns A key string. */ -export async function exportKey(key: CryptoKey): Promise { - const exportedKey = await window.crypto.subtle.exportKey(EXPORT_FORMAT, key); +export async function exportKey( + encryptionKey: CryptoKey | EncryptionKey, +): Promise { + const exportedKey = await window.crypto.subtle.exportKey( + EXPORT_FORMAT, + encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key, + ); return JSON.stringify(exportedKey); } @@ -216,16 +285,20 @@ 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 exportable - Should the derived key be exportable. - * @param iterations - The number of iterations to use in key derivation. + * @param opts - The options to use for key derivation. * @returns A CryptoKey for encryption and decryption. */ export async function keyFromPassword( password: string, salt: string, - exportable = false, - iterations = 900_000, -): Promise { + opts: KeyDerivationOptions = { + algorithm: 'PBKDF2', + params: { + iterations: 900_000, + exportable: false, + }, + }, +): Promise { const passBuffer = Buffer.from(password, STRING_ENCODING); const saltBuffer = Buffer.from(salt, 'base64'); @@ -241,16 +314,19 @@ export async function keyFromPassword( { name: 'PBKDF2', salt: saltBuffer, - iterations, + iterations: opts.params.iterations || 900_000, hash: 'SHA-256', }, key, { name: DERIVED_KEY_FORMAT, length: 256 }, - exportable, + opts.params.exportable || false, ['encrypt', 'decrypt'], ); - return derivedKey; + return { + key: derivedKey, + derivationFunction: opts, + }; } /** diff --git a/test/index.spec.ts b/test/index.spec.ts index a074bf9..335d04d 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -140,7 +140,7 @@ test('encryptor:encrypt & decrypt with wrong password', async ({ page }) => { * `a sample passw0rd` and 10000 iterations. This should be left unmodified, * as it's used to test that decrypting older encrypted data continues to work. */ -const oldSampleEncryptedData = { +const oldSampleEncryptedData: Encryptor.EncryptionResult = { data: 'bfCvija6QfwqARmHsKT7ZR0GHi8yjz7iVEZodRVx3xI2yzFHwq7+B/U=', iv: 'N9s46G5sp37A7wtf3vo/LA==', salt: '+uzzUKmbAdwkjw8rILhJvZE9dOfz2ecF5Gtf7yNkyyE=', @@ -152,475 +152,506 @@ const oldSampleEncryptedData = { * `a sample passw0rd` and 900.000 iterations. This should be left unmodified, * as it's used to test that decrypting older encrypted data continues to work. */ -const sampleEncryptedData = { +const sampleEncryptedData: Encryptor.EncryptionResult = { data: 'WQbagUPb+XLvSR+U7sV9jzyS+5UZfVjBiWpmJjPOlJT93dJo9kltpls=', iv: '7NsJ8mmL1DgC5LlsIyaIXA==', salt: 'sysHvNRoWykN/JVUSpBwXhmp0llTMQabfY7zucEfAJg=', + keyMetadata: { + algorithm: 'PBKDF2', + params: { + exportable: true, + iterations: 900000, + }, + }, }; -test('encryptor:decrypt encrypted data with key derived with different number of iterations', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - const expectedData = { foo: 'data to encrypt' }; - - const decryptedData = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - 'a sample passw0rd', - args.oldSampleEncryptedData.salt, - true, - 10_000, +[sampleEncryptedData, oldSampleEncryptedData].forEach((testEncryptedData) => { + test.describe(`${ + testEncryptedData === oldSampleEncryptedData ? 'without' : 'with' + } key derivation function metadata`, () => { + 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.testEncryptedData), + ), + { testEncryptedData, password }, ); - return await window.encryptor.decrypt( - args.password, - JSON.stringify(args.oldSampleEncryptedData), - key, - ); - }, - { oldSampleEncryptedData, password }, - ); - - expect(decryptedData).toStrictEqual(expectedData); -}); - -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.sampleEncryptedData), - ), - { sampleEncryptedData, 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.sampleEncryptedData), + 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.testEncryptedData), + ), + { testEncryptedData, wrongPassword }, ), - { sampleEncryptedData, wrongPassword }, - ), - ).rejects.toThrow('Incorrect password'); -}); - -test('encryptor:decryptWithDetail returns same vault as decrypt', async ({ - page, -}) => { - const password = 'a sample passw0rd'; - - const decryptResult = await page.evaluate( - async (args) => { - return await window.encryptor.decrypt( - args.password, - JSON.stringify(args.sampleEncryptedData), + ).rejects.toThrow('Incorrect password'); + }); + + test('encryptor:decryptWithDetail returns same vault as decrypt', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + + const decryptResult = await page.evaluate( + async (args) => { + return await window.encryptor.decrypt( + args.password, + JSON.stringify(args.testEncryptedData), + ); + }, + { password, testEncryptedData }, ); - }, - { password, sampleEncryptedData }, - ); - const decryptWithDetailResult = await page.evaluate( - async (args) => { - return await window.encryptor.decryptWithDetail( - args.password, - JSON.stringify(args.sampleEncryptedData), + const decryptWithDetailResult = await page.evaluate( + async (args) => { + return await window.encryptor.decryptWithDetail( + args.password, + JSON.stringify(args.testEncryptedData), + ); + }, + { password, testEncryptedData }, ); - }, - { password, sampleEncryptedData }, - ); - 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, + expect(JSON.stringify(decryptResult)).toStrictEqual( + JSON.stringify(decryptWithDetailResult.vault), ); - return await window.encryptor.encryptWithKey(key, args.data); - }, - { data, password, salt }, - ); - expect(Object.keys(encryptedData).sort()).toStrictEqual(['data', 'iv']); - - 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, + 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', + 'keyMetadata', + ]); + + 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 }, ); - return await window.encryptor.encryptWithKey(key, args.data); - }, - { data, password, salt }, - ); - expect(Object.keys(encryptedData).sort()).toStrictEqual(['data', 'iv']); - const encryptedString = JSON.stringify( - Object.assign({}, encryptedData, { salt }), - ); - await expect( - page.evaluate( - async (args) => - await window.encryptor.decrypt( - args.wrongPassword, - args.encryptedString, + 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', + 'keyMetadata', + ]); + + const encryptedString = JSON.stringify( + Object.assign({}, encryptedData, { salt }), + ); + await expect( + page.evaluate( + async (args) => + await window.encryptor.decrypt( + args.wrongPassword, + args.encryptedString, + ), + { encryptedString, wrongPassword }, ), - { encryptedString, wrongPassword }, - ), - ).rejects.toThrow('Incorrect password'); -}); + ).rejects.toThrow('Incorrect password'); + }); -test('encryptor:encrypt then decrypt using key', async ({ page }) => { - const password = 'a sample passw0rd'; - const data = { foo: 'data to encrypt' }; + 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 }; - - const decryptedData = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, + const encryptedString = await page.evaluate( + async (args) => + await window.encryptor.encrypt(args.password, args.data), + { data, password }, ); - return await window.encryptor.decryptWithKey(key, args.encryptedPayload); - }, - { encryptedPayload, password, salt }, - ); - - expect(decryptedData).toStrictEqual(data); -}); - -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 }; - - 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'); -}); + expect(typeof encryptedString).toBe('string'); + const encryptedData = JSON.parse(encryptedString); + const { salt } = encryptedData; + const encryptedPayload = { + data: encryptedData.data, + iv: encryptedData.iv, + }; -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, + 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 }, ); - return await window.encryptor.decryptWithKey(key, args.encryptedPayload); - }, - { encryptedPayload, password, salt }, - ); - - expect(decryptedData).toStrictEqual(expectedData); -}); - -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; - - 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: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(data); + }); -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: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' }; -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, + 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, }; - }, - { 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, - }; + 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', async ({ page }) => { + const password = 'a sample passw0rd'; + const expectedData = { foo: 'data to encrypt' }; + const { salt } = testEncryptedData; + + const decryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt as string, + args.testEncryptedData.keyMetadata || { + algorithm: 'PBKDF2', + params: { + exportable: true, + // we assume that this is an old vault, + // so we use the old iteration count + iterations: 10_000, + }, + }, + ); + return await window.encryptor.decryptWithKey( + key, + args.testEncryptedData, + ); + }, + { testEncryptedData, password, 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(decryptedData).toStrictEqual(expectedData); + }); + + test('encryptor:decrypt encrypted data using key derived from wrong password', async ({ + page, + }) => { + const wrongPassword = 'a wrong password'; + + await expect( + page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.wrongPassword, + args.salt as string, + args.encryptedPayload.keyMetadata || { + algorithm: 'PBKDF2', + params: { + exportable: true, + // we assume that this is an old vault, + // so we use the old iteration count + iterations: 10_000, + }, + }, + ); + return await window.encryptor.decryptWithKey( + key, + args.encryptedPayload, + ); + }, + { + encryptedPayload: testEncryptedData, + salt: testEncryptedData.salt, + wrongPassword, + }, + ), + ).rejects.toThrow('Incorrect password'); + }); + + 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 }, + ); - expect(JSON.stringify(decryptedResult.vault)).toStrictEqual( - JSON.stringify(newData), - ); -}); + const decryptedDetail = await page.evaluate( + async (args) => + await window.encryptor.decryptWithDetail(args.password, args.data), + { data: vault, password }, + ); -test('encryptor:encryptWithKey works with decryptWithKey', async ({ page }) => { - const password = 'a sample passw0rd'; - const startingData = { foo: 'data to encrypt' }; + 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 }, + ); - // Get an exported key to use - const exportedKeyString = await page.evaluate( - async (args) => { - const { exportedKeyString: newKeyString } = - await window.encryptor.encryptWithDetail(args.password, args.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 }, + ); - return newKeyString; - }, - { data: startingData, password }, - ); + 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); - const result = await window.encryptor.encryptWithKey(key, args.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 }, + ); - return { - encryptWithKeyResult: result, - exportedKeyString: await window.encryptor.exportKey(key), + // Mock the encrypted object + const decryptable = { + ...encryptWithKeyResult, + salt, }; - }, - { 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, - }, - ); + // 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)).toStrictEqual( - JSON.stringify(newData), - ); -}); + 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 }, + ); -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()); + // 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 }, + ); - const exportResult = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.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, + }, ); - try { - const result = await window.encryptor.exportKey(key); - return result; - } catch (e) { - return 'error'; - } - }, - { data, password, salt }, - ); + 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'); + expect(exportResult).toStrictEqual('error'); + }); + }); }); From 2d84b2370a564c4147cfe4bafd6ef6dba42170c3 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Wed, 25 Oct 2023 14:08:51 +0200 Subject: [PATCH 03/12] fix: add derivation function metadata to exported key --- src/index.ts | 58 +++++++++++++++++++++++++++++++++------------- test/index.spec.ts | 6 ++--- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index 331dcdc..92f70bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,12 @@ export type KeyDerivationOptions = { export type EncryptionKey = { key: CryptoKey; - derivationFunction: KeyDerivationOptions; + derivationFunction?: KeyDerivationOptions; +}; + +export type ExportedEncryptionKey = { + key: JsonWebKey; + derivationFunction?: KeyDerivationOptions; }; export type EncryptionResult = { @@ -123,7 +128,10 @@ export async function encryptWithKey( iv: vectorStr, }; - if (!(encryptionKey instanceof CryptoKey)) { + if ( + !(encryptionKey instanceof CryptoKey) && + encryptionKey.derivationFunction + ) { encryptionResult.keyMetadata = encryptionKey.derivationFunction; } @@ -249,18 +257,26 @@ export async function decryptWithKey( * Receives an exported CryptoKey string and creates a key. * * @param keyString - The key string to import. - * @returns A CryptoKey. + * @returns An EncryptionKey. */ -export async function importKey(keyString: string): Promise { - const key = await window.crypto.subtle.importKey( - EXPORT_FORMAT, - JSON.parse(keyString), - DERIVED_KEY_FORMAT, - true, - ['encrypt', 'decrypt'], - ); +export async function importKey(keyString: string): Promise { + const exportedEncryptionKey = JSON.parse(keyString); + + const encryptionKey: EncryptionKey = { + key: await window.crypto.subtle.importKey( + EXPORT_FORMAT, + exportedEncryptionKey.key, + DERIVED_KEY_FORMAT, + true, + ['encrypt', 'decrypt'], + ), + }; + + if (exportedEncryptionKey.derivationFunction) { + encryptionKey.derivationFunction = exportedEncryptionKey.derivationFunction; + } - return key; + return encryptionKey; } /** @@ -273,10 +289,20 @@ export async function importKey(keyString: string): Promise { export async function exportKey( encryptionKey: CryptoKey | EncryptionKey, ): Promise { - const exportedKey = await window.crypto.subtle.exportKey( - EXPORT_FORMAT, - encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key, - ); + const exportedKey: ExportedEncryptionKey = { + key: await window.crypto.subtle.exportKey( + EXPORT_FORMAT, + encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key, + ), + }; + + if ( + !(encryptionKey instanceof CryptoKey) && + encryptionKey.derivationFunction + ) { + exportedKey.derivationFunction = encryptionKey.derivationFunction; + } + return JSON.stringify(exportedKey); } diff --git a/test/index.spec.ts b/test/index.spec.ts index 335d04d..b62e6d7 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -14,7 +14,7 @@ declare global { const testPagePath = path.resolve(__dirname, 'index.html'); const SAMPLE_EXPORTED_KEY = - '{"alg":"A256GCM","ext":true,"k":"leW0IR00ACQp3SoWuITXQComCte7lwKLR9ztPlGkFeM","key_ops":["encrypt","decrypt"],"kty":"oct"}'; + '{"key":{"alg":"A256GCM","ext":true,"k":"leW0IR00ACQp3SoWuITXQComCte7lwKLR9ztPlGkFeM","key_ops":["encrypt","decrypt"],"kty":"oct"}}'; test.beforeEach(async ({ page }) => { await page.goto(`file://${testPagePath}`); @@ -448,10 +448,10 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { test('encryptor:importKey generates valid CryptoKey', async ({ page }) => { const isKey = await page.evaluate( async (args) => { - const key = await window.encryptor.importKey( + const encryptionKey = await window.encryptor.importKey( args.SAMPLE_EXPORTED_KEY, ); - return key instanceof CryptoKey; + return encryptionKey.key instanceof CryptoKey; }, { SAMPLE_EXPORTED_KEY }, ); From 1daf678b8397a1dbdca27e0a16e7cc949f57bcdd Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Thu, 2 Nov 2023 15:41:49 +0100 Subject: [PATCH 04/12] refactor: changes made non-breaking --- src/index.ts | 215 ++++++++++++++++++++++++++++----------------- test/index.spec.ts | 61 ++++++++----- 2 files changed, 173 insertions(+), 103 deletions(-) diff --git a/src/index.ts b/src/index.ts index 92f70bc..301d0fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,7 @@ export type DetailedEncryptionResult = { }; export type PBKDF2Params = { - iterations?: number; - exportable?: boolean; + iterations: number; }; export type KeyDerivationOptions = { @@ -15,12 +14,12 @@ export type KeyDerivationOptions = { export type EncryptionKey = { key: CryptoKey; - derivationFunction?: KeyDerivationOptions; + derivationOptions: KeyDerivationOptions; }; export type ExportedEncryptionKey = { key: JsonWebKey; - derivationFunction?: KeyDerivationOptions; + derivationOptions: KeyDerivationOptions; }; export type EncryptionResult = { @@ -40,6 +39,18 @@ export type DetailedDecryptResult = { const EXPORT_FORMAT = 'jwk'; const DERIVED_KEY_FORMAT = 'AES-GCM'; const STRING_ENCODING = 'utf-8'; +const OLD_DERIVATION_PARAMS: KeyDerivationOptions = { + algorithm: 'PBKDF2', + params: { + iterations: 10_000, + }, +}; +const DEFAULT_DERIVATION_PARAMS: KeyDerivationOptions = { + algorithm: 'PBKDF2', + params: { + iterations: 900_000, + }, +}; /** * Encrypts a data object that can be any serializable value using @@ -57,7 +68,9 @@ export async function encrypt( key?: EncryptionKey | CryptoKey, salt: string = generateSalt(), ): Promise { - const cryptoKey = key || (await keyFromPassword(password, salt)); + const cryptoKey = + key || + (await keyFromPassword(password, salt, false, DEFAULT_DERIVATION_PARAMS)); const payload = await encryptWithKey(cryptoKey, dataObj); payload.salt = salt; return JSON.stringify(payload); @@ -77,12 +90,12 @@ export async function encryptWithDetail( dataObj: R, salt = generateSalt(), ): Promise { - const key = await keyFromPassword(password, salt, { - algorithm: 'PBKDF2', - params: { - exportable: true, - }, - }); + const key = await keyFromPassword( + password, + salt, + true, + DEFAULT_DERIVATION_PARAMS, + ); const exportedKeyString = await exportKey(key); const vault = await encrypt(password, dataObj, key, salt); @@ -130,9 +143,9 @@ export async function encryptWithKey( if ( !(encryptionKey instanceof CryptoKey) && - encryptionKey.derivationFunction + encryptionKey.derivationOptions ) { - encryptionResult.keyMetadata = encryptionKey.derivationFunction; + encryptionResult.keyMetadata = encryptionKey.derivationOptions; } return encryptionResult; @@ -160,21 +173,7 @@ export async function decrypt( cryptoKey = encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key; } else { - cryptoKey = ( - await keyFromPassword( - password, - salt, - keyMetadata || { - algorithm: 'PBKDF2', - params: { - exportable: true, - // we assume that this is an old vault, - // so we use the old iteration count - iterations: 10_000, - }, - }, - ) - ).key; + cryptoKey = (await keyFromPassword(password, salt, false, keyMetadata)).key; } const result = await decryptWithKey(cryptoKey, payload); @@ -195,19 +194,7 @@ export async function decryptWithDetail( ): Promise { const payload = JSON.parse(text); const { salt, keyMetadata } = payload; - const key = await keyFromPassword( - password, - salt, - keyMetadata || { - algorithm: 'PBKDF2', - params: { - exportable: true, - // we assume that this is an old vault, - // so we use the old iteration count - iterations: 10_000, - }, - }, - ); + const key = await keyFromPassword(password, salt, true, keyMetadata); const exportedKeyString = await exportKey(key); const vault = await decrypt(password, text, key); @@ -259,24 +246,31 @@ export async function decryptWithKey( * @param keyString - The key string to import. * @returns An EncryptionKey. */ -export async function importKey(keyString: string): Promise { +export async function importKey( + keyString: string, +): Promise { const exportedEncryptionKey = JSON.parse(keyString); - const encryptionKey: EncryptionKey = { - key: await window.crypto.subtle.importKey( - EXPORT_FORMAT, - exportedEncryptionKey.key, - DERIVED_KEY_FORMAT, - true, - ['encrypt', 'decrypt'], - ), - }; - - if (exportedEncryptionKey.derivationFunction) { - encryptionKey.derivationFunction = exportedEncryptionKey.derivationFunction; + if (isExportedEncryptionKey(exportedEncryptionKey)) { + return { + key: await window.crypto.subtle.importKey( + EXPORT_FORMAT, + exportedEncryptionKey.key, + DERIVED_KEY_FORMAT, + true, + ['encrypt', 'decrypt'], + ), + derivationOptions: exportedEncryptionKey.derivationOptions, + }; } - return encryptionKey; + return await window.crypto.subtle.importKey( + EXPORT_FORMAT, + exportedEncryptionKey, + DERIVED_KEY_FORMAT, + true, + ['encrypt', 'decrypt'], + ); } /** @@ -289,21 +283,19 @@ export async function importKey(keyString: string): Promise { export async function exportKey( encryptionKey: CryptoKey | EncryptionKey, ): Promise { - const exportedKey: ExportedEncryptionKey = { - key: await window.crypto.subtle.exportKey( - EXPORT_FORMAT, - encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key, - ), - }; - - if ( - !(encryptionKey instanceof CryptoKey) && - encryptionKey.derivationFunction - ) { - exportedKey.derivationFunction = encryptionKey.derivationFunction; + if (isEncryptionKey(encryptionKey)) { + return JSON.stringify({ + key: await window.crypto.subtle.exportKey( + EXPORT_FORMAT, + encryptionKey.key, + ), + derivationOptions: encryptionKey.derivationOptions, + }); } - return JSON.stringify(exportedKey); + return JSON.stringify( + await window.crypto.subtle.exportKey(EXPORT_FORMAT, encryptionKey), + ); } /** @@ -311,20 +303,48 @@ export async function exportKey( * * @param password - The password to use to generate key. * @param salt - The salt string to use in key derivation. + * @param exportable - Whether or not the key should be exportable. + * @returns A CryptoKey for encryption and decryption. + */ +export async function keyFromPassword( + password: string, + salt: string, + exportable?: boolean, +): Promise; +/** + * Generate a CryptoKey from a password and random salt, specifying + * key derivation options. + * + * @param password - The password to use to generate key. + * @param salt - The salt string to use in key derivation. + * @param exportable - Whether or not the key should be exportable. * @param opts - The options to use for key derivation. * @returns A CryptoKey for encryption and decryption. */ export async function keyFromPassword( password: string, salt: string, - opts: KeyDerivationOptions = { - algorithm: 'PBKDF2', - params: { - iterations: 900_000, - exportable: false, - }, - }, -): Promise { + exportable?: boolean, + opts?: KeyDerivationOptions, +): Promise; +/** + * Generate a CryptoKey from a password and random salt, optionally + * specifying key derivation options. + * + * When no derivation options are specified, the default options are used. + * + * @param password - The password to use to generate key. + * @param salt - The salt string to use in key derivation. + * @param exportable - Whether or not the key should be exportable. + * @param opts - The options to use for key derivation. + * @returns A CryptoKey for encryption and decryption. + */ +export async function keyFromPassword( + password: string, + salt: string, + exportable = false, + opts: KeyDerivationOptions = OLD_DERIVATION_PARAMS, +): Promise { const passBuffer = Buffer.from(password, STRING_ENCODING); const saltBuffer = Buffer.from(salt, 'base64'); @@ -340,19 +360,21 @@ export async function keyFromPassword( { name: 'PBKDF2', salt: saltBuffer, - iterations: opts.params.iterations || 900_000, + iterations: opts.params.iterations, hash: 'SHA-256', }, key, { name: DERIVED_KEY_FORMAT, length: 256 }, - opts.params.exportable || false, + exportable, ['encrypt', 'decrypt'], ); - return { - key: derivedKey, - derivationFunction: opts, - }; + return opts + ? { + key: derivedKey, + derivationOptions: opts, + } + : derivedKey; } /** @@ -419,3 +441,32 @@ export function generateSalt(byteCount = 32): string { ); return b64encoded; } + +/** + * Checks if the provided key is an `EncryptionKey`. + * + * @param key - The key to check. + * @returns Whether or not the key is an `EncryptionKey`. + */ +export function isEncryptionKey( + key: CryptoKey | EncryptionKey, +): key is EncryptionKey { + return ( + 'key' in key && + 'derivationOptions' in key && + 'algorithm' in key.derivationOptions && + 'params' in key.derivationOptions + ); +} + +/** + * Checks if the provided key is an `ExportedEncryptionKey`. + * + * @param key - The key to check. + * @returns Whether or not the key is an `EncryptionKey`. + */ +export function isExportedEncryptionKey( + key: JsonWebKey | ExportedEncryptionKey, +): key is ExportedEncryptionKey { + return 'key' in key && 'derivationOptions' in key; +} diff --git a/test/index.spec.ts b/test/index.spec.ts index b62e6d7..88586b5 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -14,7 +14,7 @@ declare global { const testPagePath = path.resolve(__dirname, 'index.html'); const SAMPLE_EXPORTED_KEY = - '{"key":{"alg":"A256GCM","ext":true,"k":"leW0IR00ACQp3SoWuITXQComCte7lwKLR9ztPlGkFeM","key_ops":["encrypt","decrypt"],"kty":"oct"}}'; + '{"alg":"A256GCM","ext":true,"k":"leW0IR00ACQp3SoWuITXQComCte7lwKLR9ztPlGkFeM","key_ops":["encrypt","decrypt"],"kty":"oct"}'; test.beforeEach(async ({ page }) => { await page.goto(`file://${testPagePath}`); @@ -159,12 +159,42 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { keyMetadata: { algorithm: 'PBKDF2', params: { - exportable: true, iterations: 900000, }, }, }; +test('encryptor:decrypt old encrypted data and re-encrypt with password', 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.encryptedData), + ), + { encryptedData: oldSampleEncryptedData, password }, + ); + const encryptedData: Encryptor.EncryptionResult = JSON.parse( + await page.evaluate( + async (args) => await window.encryptor.encrypt(args.password, args.data), + { data: decryptedData, password }, + ), + ); + + expect(decryptedData).toStrictEqual(expectedData); + expect(encryptedData).toHaveProperty('keyMetadata'); + expect(encryptedData.keyMetadata).toStrictEqual({ + algorithm: 'PBKDF2', + params: { + iterations: 900000, + }, + }); +}); + [sampleEncryptedData, oldSampleEncryptedData].forEach((testEncryptedData) => { test.describe(`${ testEncryptedData === oldSampleEncryptedData ? 'without' : 'with' @@ -323,6 +353,7 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { const encryptedPayload = { data: encryptedData.data, iv: encryptedData.iv, + keyMetadata: encryptedData.keyMetadata, }; const decryptedData = await page.evaluate( @@ -330,6 +361,8 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { const key = await window.encryptor.keyFromPassword( args.password, args.salt, + false, + args.encryptedPayload.keyMetadata, ); return await window.encryptor.decryptWithKey( key, @@ -389,15 +422,8 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { const key = await window.encryptor.keyFromPassword( args.password, args.salt as string, - args.testEncryptedData.keyMetadata || { - algorithm: 'PBKDF2', - params: { - exportable: true, - // we assume that this is an old vault, - // so we use the old iteration count - iterations: 10_000, - }, - }, + false, + args.testEncryptedData.keyMetadata, ); return await window.encryptor.decryptWithKey( key, @@ -421,15 +447,8 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { const key = await window.encryptor.keyFromPassword( args.wrongPassword, args.salt as string, - args.encryptedPayload.keyMetadata || { - algorithm: 'PBKDF2', - params: { - exportable: true, - // we assume that this is an old vault, - // so we use the old iteration count - iterations: 10_000, - }, - }, + false, + args.encryptedPayload.keyMetadata, ); return await window.encryptor.decryptWithKey( key, @@ -451,7 +470,7 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { const encryptionKey = await window.encryptor.importKey( args.SAMPLE_EXPORTED_KEY, ); - return encryptionKey.key instanceof CryptoKey; + return encryptionKey instanceof CryptoKey; }, { SAMPLE_EXPORTED_KEY }, ); From 5d0e65cec8a0e31dbcc45d81f2c5dea0c49643f0 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Fri, 3 Nov 2023 15:46:09 +0100 Subject: [PATCH 05/12] refactor: apply @mcmire suggestions --- src/index.ts | 65 ++-- test/index.spec.ts | 774 +++++++++++++++++++++++++-------------------- yarn.lock | 27 +- 3 files changed, 488 insertions(+), 378 deletions(-) diff --git a/src/index.ts b/src/index.ts index 301d0fa..b4d0ca0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -319,7 +319,7 @@ export async function keyFromPassword( * @param salt - The salt string to use in key derivation. * @param exportable - Whether or not the key should be exportable. * @param opts - The options to use for key derivation. - * @returns A CryptoKey for encryption and decryption. + * @returns An EncryptionKey for encryption and decryption. */ export async function keyFromPassword( password: string, @@ -327,18 +327,8 @@ export async function keyFromPassword( exportable?: boolean, opts?: KeyDerivationOptions, ): Promise; -/** - * Generate a CryptoKey from a password and random salt, optionally - * specifying key derivation options. - * - * When no derivation options are specified, the default options are used. - * - * @param password - The password to use to generate key. - * @param salt - The salt string to use in key derivation. - * @param exportable - Whether or not the key should be exportable. - * @param opts - The options to use for key derivation. - * @returns A CryptoKey for encryption and decryption. - */ +// The overloads are already documented. +// eslint-disable-next-line jsdoc/require-jsdoc export async function keyFromPassword( password: string, salt: string, @@ -445,28 +435,53 @@ export function generateSalt(byteCount = 32): string { /** * Checks if the provided key is an `EncryptionKey`. * - * @param key - The key to check. + * @param encryptionKey - The object to check. * @returns Whether or not the key is an `EncryptionKey`. */ export function isEncryptionKey( - key: CryptoKey | EncryptionKey, -): key is EncryptionKey { + encryptionKey: unknown, +): encryptionKey is EncryptionKey { return ( - 'key' in key && - 'derivationOptions' in key && - 'algorithm' in key.derivationOptions && - 'params' in key.derivationOptions + encryptionKey !== null && + typeof encryptionKey === 'object' && + 'key' in encryptionKey && + 'derivationOptions' in encryptionKey && + encryptionKey.key instanceof CryptoKey && + isKeyDerivationOptions(encryptionKey.derivationOptions) + ); +} + +/** + * Checks if the provided object is a `KeyDerivationOptions`. + * + * @param derivationOptions - The object to check. + * @returns Whether or not the object is a `KeyDerivationOptions`. + */ +export function isKeyDerivationOptions( + derivationOptions: unknown, +): derivationOptions is KeyDerivationOptions { + return ( + derivationOptions !== null && + typeof derivationOptions === 'object' && + 'algorithm' in derivationOptions && + 'params' in derivationOptions ); } /** * Checks if the provided key is an `ExportedEncryptionKey`. * - * @param key - The key to check. - * @returns Whether or not the key is an `EncryptionKey`. + * @param exportedKey - The object to check. + * @returns Whether or not the object is an `ExportedEncryptionKey`. */ export function isExportedEncryptionKey( - key: JsonWebKey | ExportedEncryptionKey, -): key is ExportedEncryptionKey { - return 'key' in key && 'derivationOptions' in key; + exportedKey: unknown, +): exportedKey is ExportedEncryptionKey { + return ( + exportedKey !== null && + typeof exportedKey === 'object' && + 'key' in exportedKey && + 'derivationOptions' in exportedKey && + isKeyDerivationOptions(exportedKey.derivationOptions) + ); } diff --git a/test/index.spec.ts b/test/index.spec.ts index 88586b5..83db61d 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -13,8 +13,10 @@ declare global { const testPagePath = path.resolve(__dirname, 'index.html'); -const SAMPLE_EXPORTED_KEY = +const OLD_SAMPLE_EXPORTED_KEY = '{"alg":"A256GCM","ext":true,"k":"leW0IR00ACQp3SoWuITXQComCte7lwKLR9ztPlGkFeM","key_ops":["encrypt","decrypt"],"kty":"oct"}'; +const SAMPLE_EXPORTED_KEY = + '{"key":{"alg":"A256GCM","ext":true,"k":"leW0IR00ACQp3SoWuITXQComCte7lwKLR9ztPlGkFeM","key_ops":["encrypt","decrypt"],"kty":"oct"},"derivationOptions":{"algorithm":"PBKDF2","params":{"iterations":10000}}}'; test.beforeEach(async ({ page }) => { await page.goto(`file://${testPagePath}`); @@ -164,37 +166,6 @@ const sampleEncryptedData: Encryptor.EncryptionResult = { }, }; -test('encryptor:decrypt old encrypted data and re-encrypt with password', 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.encryptedData), - ), - { encryptedData: oldSampleEncryptedData, password }, - ); - const encryptedData: Encryptor.EncryptionResult = JSON.parse( - await page.evaluate( - async (args) => await window.encryptor.encrypt(args.password, args.data), - { data: decryptedData, password }, - ), - ); - - expect(decryptedData).toStrictEqual(expectedData); - expect(encryptedData).toHaveProperty('keyMetadata'); - expect(encryptedData.keyMetadata).toStrictEqual({ - algorithm: 'PBKDF2', - params: { - iterations: 900000, - }, - }); -}); - [sampleEncryptedData, oldSampleEncryptedData].forEach((testEncryptedData) => { test.describe(`${ testEncryptedData === oldSampleEncryptedData ? 'without' : 'with' @@ -266,152 +237,6 @@ test('encryptor:decrypt old encrypted data and re-encrypt with password', async ); }); - 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', - 'keyMetadata', - ]); - - 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', - 'keyMetadata', - ]); - - 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, - keyMetadata: encryptedData.keyMetadata, - }; - - const decryptedData = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - false, - args.encryptedPayload.keyMetadata, - ); - return await window.encryptor.decryptWithKey( - key, - args.encryptedPayload, - ); - }, - { encryptedPayload, password, salt }, - ); - - expect(decryptedData).toStrictEqual(data); - }); - - 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, - }; - - 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', async ({ page }) => { const password = 'a sample passw0rd'; const expectedData = { foo: 'data to encrypt' }; @@ -463,214 +288,467 @@ test('encryptor:decrypt old encrypted data and re-encrypt with password', async ), ).rejects.toThrow('Incorrect password'); }); + }); +}); - test('encryptor:importKey generates valid CryptoKey', async ({ page }) => { - const isKey = await page.evaluate( - async (args) => { - const encryptionKey = await window.encryptor.importKey( - args.SAMPLE_EXPORTED_KEY, - ); - return encryptionKey instanceof CryptoKey; - }, - { SAMPLE_EXPORTED_KEY }, +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, ); - expect(isKey).toBe(true); - }); + return await window.encryptor.encryptWithKey(key, args.data); + }, + { data, password, salt }, + ); + expect(Object.keys(encryptedData).sort()).toStrictEqual([ + 'data', + 'iv', + 'keyMetadata', + ]); + + 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', + 'keyMetadata', + ]); + + 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, + keyMetadata: encryptedData.keyMetadata, + }; + const decryptedData = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, + false, + args.encryptedPayload.keyMetadata, + ); + return await window.encryptor.decryptWithKey(key, args.encryptedPayload); + }, + { encryptedPayload, password, salt }, + ); + + expect(decryptedData).toStrictEqual(data); +}); + +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, + }; + + 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:importKey generates valid CryptoKey using old key export format', async ({ + page, +}) => { + const isKey = await page.evaluate( + async (args) => { + const encryptionKey = await window.encryptor.importKey( + args.OLD_SAMPLE_EXPORTED_KEY, + ); + return encryptionKey instanceof CryptoKey; + }, + { OLD_SAMPLE_EXPORTED_KEY }, + ); + expect(isKey).toBe(true); +}); + +test('encryptor:importKey generates valid EncryptionKey using new key export format', async ({ + page, +}) => { + const isKey = await page.evaluate( + async (args) => { + const encryptionKey = await window.encryptor.importKey( + args.SAMPLE_EXPORTED_KEY, + ); + return ( + !(encryptionKey instanceof CryptoKey) && + encryptionKey.key instanceof CryptoKey && + encryptionKey.derivationOptions.algorithm === 'PBKDF2' && + encryptionKey.derivationOptions.params.iterations === 10000 + ); + }, + { SAMPLE_EXPORTED_KEY }, + ); + expect(isKey).toBe(true); +}); + +[OLD_SAMPLE_EXPORTED_KEY, SAMPLE_EXPORTED_KEY].forEach((testKey) => { + test.describe(`with the ${ + testKey === OLD_SAMPLE_EXPORTED_KEY ? 'old' : 'new' + } exported key format`, () => { 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, - ); + const key = await window.encryptor.importKey(args.testKey); return await window.encryptor.exportKey(key); }, - { SAMPLE_EXPORTED_KEY }, + { testKey }, ); - expect(keyString).toStrictEqual(SAMPLE_EXPORTED_KEY); + expect(keyString).toStrictEqual(testKey); }); + }); +}); - test('encryptor:encryptWithDetail and decryptWithDetail provide same data', async ({ - page, - }) => { - const password = 'a sample passw0rd'; - const data = { foo: 'data to encrypt' }; +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 { 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 }, - ); + 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), - ); - }); + 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' }; +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 }, - ); + 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 }, - ); + // 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), - ); - }); + 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' }; +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 }, + ); - // 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, - ); + // 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 }, + ); - return { - salt: usedSalt, - exportedKeyString: newKeyString, - }; - }, - { data: startingData, password }, - ); + // Mock the encrypted object + const decryptable = { + ...encryptWithKeyResult, + salt, + }; - // 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 }, - ); + // 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) }, + ); - // Mock the encrypted object - const decryptable = { - ...encryptWithKeyResult, - salt, - }; + expect(JSON.stringify(decryptedResult.vault)).toStrictEqual( + JSON.stringify(newData), + ); +}); - // 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) }, - ); +test('encryptor:encryptWithKey works with decryptWithKey', async ({ page }) => { + const password = 'a sample passw0rd'; + const startingData = { foo: 'data to encrypt' }; - expect(JSON.stringify(decryptedResult.vault)).toStrictEqual( - JSON.stringify(newData), - ); - }); + // Get an exported key to use + const exportedKeyString = await page.evaluate( + async (args) => { + const { exportedKeyString: newKeyString } = + await window.encryptor.encryptWithDetail(args.password, args.data); - test('encryptor:encryptWithKey works with decryptWithKey', async ({ - page, - }) => { - const password = 'a sample passw0rd'; - const startingData = { foo: 'data to encrypt' }; + return newKeyString; + }, + { data: startingData, password }, + ); - // Get an exported key to use - const exportedKeyString = await page.evaluate( - async (args) => { - const { exportedKeyString: newKeyString } = - await window.encryptor.encryptWithDetail(args.password, args.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); + const result = await window.encryptor.encryptWithKey(key, args.data); - return newKeyString; - }, - { data: startingData, password }, - ); + return { + encryptWithKeyResult: result, + exportedKeyString: await window.encryptor.exportKey(key), + }; + }, + { data: newData, keyString: exportedKeyString }, + ); - // 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); + // 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 { - encryptWithKeyResult: result, - exportedKeyString: await window.encryptor.exportKey(key), - }; - }, - { data: newData, keyString: exportedKeyString }, - ); + expect(JSON.stringify(decryptedResult)).toStrictEqual( + JSON.stringify(newData), + ); +}); - // 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, - }, - ); +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()); - expect(JSON.stringify(decryptedResult)).toStrictEqual( - JSON.stringify(newData), + const exportResult = await page.evaluate( + async (args) => { + const key = await window.encryptor.keyFromPassword( + args.password, + args.salt, ); - }); - 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()); + try { + const result = await window.encryptor.exportKey(key); + return result; + } catch (e) { + return 'error'; + } + }, + { data, password, salt }, + ); - const exportResult = await page.evaluate( - async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); + expect(exportResult).toStrictEqual('error'); +}); - try { - const result = await window.encryptor.exportKey(key); - return result; - } catch (e) { - return 'error'; - } - }, - { data, password, salt }, - ); +test('encryptor:decrypt old encrypted data and re-encrypt with password', async ({ + page, +}) => { + const password = 'a sample passw0rd'; + const expectedData = { foo: 'data to encrypt' }; - expect(exportResult).toStrictEqual('error'); - }); + const decryptedData = await page.evaluate( + async (args) => + await window.encryptor.decrypt( + args.password, + JSON.stringify(args.encryptedData), + ), + { encryptedData: oldSampleEncryptedData, password }, + ); + const encryptedData: Encryptor.EncryptionResult = JSON.parse( + await page.evaluate( + async (args) => await window.encryptor.encrypt(args.password, args.data), + { data: decryptedData, password }, + ), + ); + + expect(decryptedData).toStrictEqual(expectedData); + expect(encryptedData).toHaveProperty('keyMetadata'); + expect(encryptedData.keyMetadata).toStrictEqual({ + algorithm: 'PBKDF2', + params: { + iterations: 900000, + }, + }); +}); + +test.describe('encryptor:isEncryptionKey', async () => { + test('should return false with CryptoKey', async ({ page }) => { + const isEncryptionKey = await page.evaluate( + async (args) => { + const encryptionKey = await window.encryptor.importKey( + args.OLD_SAMPLE_EXPORTED_KEY, + ); + return window.encryptor.isEncryptionKey(encryptionKey); + }, + { OLD_SAMPLE_EXPORTED_KEY }, + ); + + expect(isEncryptionKey).toBe(false); + }); + + test('should return true with valid EncryptionKey', async ({ page }) => { + const isEncryptionKey = await page.evaluate( + async (args) => { + const encryptionKey = await window.encryptor.importKey( + args.SAMPLE_EXPORTED_KEY, + ); + return window.encryptor.isEncryptionKey(encryptionKey); + }, + { SAMPLE_EXPORTED_KEY }, + ); + + expect(isEncryptionKey).toBe(true); + }); +}); + +test.describe('encryptor:isExportedEncryptionKey', async () => { + test('should return false with JsonWebKey', async ({ page }) => { + const isEncryptionKey = await page.evaluate( + async (args) => { + return window.encryptor.isExportedEncryptionKey( + JSON.parse(args.OLD_SAMPLE_EXPORTED_KEY), + ); + }, + { OLD_SAMPLE_EXPORTED_KEY }, + ); + + expect(isEncryptionKey).toBe(false); + }); + + test('should return true with valid ExportedEncryptionKey', async ({ + page, + }) => { + const isEncryptionKey = await page.evaluate( + async (args) => { + return window.encryptor.isExportedEncryptionKey( + JSON.parse(args.SAMPLE_EXPORTED_KEY), + ); + }, + { SAMPLE_EXPORTED_KEY }, + ); + + expect(isEncryptionKey).toBe(true); }); }); diff --git a/yarn.lock b/yarn.lock index 7602e27..394c049 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6 +1,3 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - __metadata: version: 6 cacheKey: 8 @@ -4527,13 +4524,33 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^4.8.4": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + languageName: node + linkType: hard + "typescript@npm:~4.8.4": version: 4.8.4 resolution: "typescript@npm:4.8.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + languageName: node + linkType: hard + +"typescript@patch:typescript@^4.8.4#~builtin": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=701156" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 languageName: node linkType: hard @@ -4543,7 +4560,7 @@ __metadata: bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 301459fc3eb3b1a38fe91bf96d98eb55da88a9cb17b4ef80b4d105d620f4d547ba776cc27b44cc2ef58b66eda23fe0a74142feb5e79a6fb99f54fc018a696afa + checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 languageName: node linkType: hard From c0822a317a8edbd7dc9797b5bf5bbfe11ae460d3 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Mon, 6 Nov 2023 11:34:23 +0100 Subject: [PATCH 06/12] refactor: add key derivation opts to encrypt methods --- src/index.ts | 48 ++++++++++++++++---------------- test/index.spec.ts | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index b4d0ca0..14cbdeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,6 +60,7 @@ const DEFAULT_DERIVATION_PARAMS: KeyDerivationOptions = { * @param dataObj - The data to encrypt. * @param key - The CryptoKey to encrypt with. * @param salt - The salt to use to encrypt. + * @param keyDerivationOptions - The options to use for key derivation. * @returns The encrypted vault. */ export async function encrypt( @@ -67,10 +68,10 @@ export async function encrypt( dataObj: R, key?: EncryptionKey | CryptoKey, salt: string = generateSalt(), + keyDerivationOptions = DEFAULT_DERIVATION_PARAMS, ): Promise { const cryptoKey = - key || - (await keyFromPassword(password, salt, false, DEFAULT_DERIVATION_PARAMS)); + key || (await keyFromPassword(password, salt, false, keyDerivationOptions)); const payload = await encryptWithKey(cryptoKey, dataObj); payload.salt = salt; return JSON.stringify(payload); @@ -83,19 +84,16 @@ 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 keyDerivationOptions - The options to use for key derivation. * @returns The vault and exported key string. */ export async function encryptWithDetail( password: string, dataObj: R, salt = generateSalt(), + keyDerivationOptions = DEFAULT_DERIVATION_PARAMS, ): Promise { - const key = await keyFromPassword( - password, - salt, - true, - DEFAULT_DERIVATION_PARAMS, - ); + const key = await keyFromPassword(password, salt, true, keyDerivationOptions); const exportedKeyString = await exportKey(key); const vault = await encrypt(password, dataObj, key, salt); @@ -121,8 +119,7 @@ export async function encryptWithKey( const data = JSON.stringify(dataObj); const dataBuffer = Buffer.from(data, STRING_ENCODING); const vector = global.crypto.getRandomValues(new Uint8Array(16)); - const key = - encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key; + const key = unwrapKey(encryptionKey); const buf = await global.crypto.subtle.encrypt( { @@ -141,10 +138,7 @@ export async function encryptWithKey( iv: vectorStr, }; - if ( - !(encryptionKey instanceof CryptoKey) && - encryptionKey.derivationOptions - ) { + if (isEncryptionKey(encryptionKey)) { encryptionResult.keyMetadata = encryptionKey.derivationOptions; } @@ -167,14 +161,10 @@ export async function decrypt( ): Promise { const payload = JSON.parse(text); const { salt, keyMetadata } = payload; - let cryptoKey: CryptoKey; - - if (encryptionKey) { - cryptoKey = - encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key; - } else { - cryptoKey = (await keyFromPassword(password, salt, false, keyMetadata)).key; - } + const cryptoKey = unwrapKey( + encryptionKey || + (await keyFromPassword(password, salt, false, keyMetadata)), + ); const result = await decryptWithKey(cryptoKey, payload); return result; @@ -219,8 +209,7 @@ export async function decryptWithKey( ): Promise { const encryptedData = Buffer.from(payload.data, 'base64'); const vector = Buffer.from(payload.iv, 'base64'); - const key = - encryptionKey instanceof CryptoKey ? encryptionKey : encryptionKey.key; + const key = unwrapKey(encryptionKey); let decryptedObj; try { @@ -485,3 +474,14 @@ export function isExportedEncryptionKey( isKeyDerivationOptions(exportedKey.derivationOptions) ); } + +/** + * Returns the `CryptoKey` from the provided encryption key. + * If the provided key is a `CryptoKey`, it is returned as is. + * + * @param encryptionKey - The key to unwrap. + * @returns The `CryptoKey` from the provided encryption key. + */ +export function unwrapKey(encryptionKey: EncryptionKey | CryptoKey): CryptoKey { + return isEncryptionKey(encryptionKey) ? encryptionKey.key : encryptionKey; +} diff --git a/test/index.spec.ts b/test/index.spec.ts index 83db61d..816e3f6 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -752,3 +752,71 @@ test.describe('encryptor:isExportedEncryptionKey', async () => { expect(isEncryptionKey).toBe(true); }); }); + +test('encryptor:encrypt with arbitrary key derivation options then decrypt', async ({ + page, +}) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); + + const encryptedString = await page.evaluate( + async (args) => + await window.encryptor.encrypt( + args.password, + args.data, + undefined, + args.salt, + { + algorithm: 'PBKDF2', + params: { + iterations: 100_000, + }, + }, + ), + { data, password, salt }, + ); + + const decryptedObj = await page.evaluate( + async (args) => + await window.encryptor.decrypt(args.password, args.encryptedString), + { encryptedString, password }, + ); + + expect(decryptedObj).toStrictEqual(data); +}); + +test('encryptor:encryptWithDetail with arbitrary key derivation options then decrypt', async ({ + page, +}) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + const salt = await page.evaluate(() => window.encryptor.generateSalt()); + + const { vault: encryptedString } = await page.evaluate( + async (args) => + await window.encryptor.encryptWithDetail( + args.password, + args.data, + args.salt, + { + algorithm: 'PBKDF2', + params: { + iterations: 100_000, + }, + }, + ), + { data, password, salt }, + ); + + const { vault: decryptedObj } = await page.evaluate( + async (args) => + await window.encryptor.decryptWithDetail( + args.password, + args.encryptedString, + ), + { encryptedString, password }, + ); + + expect(decryptedObj).toStrictEqual(data); +}); From 76373a584ac39a5e0b8d45ba591acdacf59bfcdf Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Tue, 7 Nov 2023 13:52:26 +0100 Subject: [PATCH 07/12] add `updateVault` helper function --- src/index.ts | 38 +++++++++++++++++++ test/index.spec.ts | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/src/index.ts b/src/index.ts index 14cbdeb..c189492 100644 --- a/src/index.ts +++ b/src/index.ts @@ -485,3 +485,41 @@ export function isExportedEncryptionKey( export function unwrapKey(encryptionKey: EncryptionKey | CryptoKey): CryptoKey { return isEncryptionKey(encryptionKey) ? encryptionKey.key : encryptionKey; } + +/** + * Checks if the provided vault is an updated encryption format. + * + * @param vault - The vault to check. + * @returns Whether or not the vault is an updated encryption format. + */ +export function isVaultUpdated(vault: string): boolean { + const { keyMetadata } = JSON.parse(vault); + return ( + isKeyDerivationOptions(keyMetadata) && + keyMetadata.algorithm === DEFAULT_DERIVATION_PARAMS.algorithm && + keyMetadata.params.iterations === + DEFAULT_DERIVATION_PARAMS.params.iterations + ); +} + +/** + * Updates the provided vault, re-encrypting + * data with a safer algorithm if one is available. + * + * If the provided vault is already using the latest available encryption method, + * it is returned as is. + * + * @param vault - The vault to update. + * @param password - The password to use for encryption. + * @returns A promise resolving to the updated vault. + */ +export async function updateVault( + vault: string, + password: string, +): Promise { + if (isVaultUpdated(vault)) { + return vault; + } + + return encrypt(password, await decrypt(password, vault)); +} diff --git a/test/index.spec.ts b/test/index.spec.ts index 816e3f6..be77219 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -820,3 +820,96 @@ test('encryptor:encryptWithDetail with arbitrary key derivation options then dec expect(decryptedObj).toStrictEqual(data); }); + +test.describe('encryptor:isVaultUpdated', async () => { + test('should return false with old value format', async ({ page }) => { + const isVaultUpdated = await page.evaluate( + async (args) => + window.encryptor.isVaultUpdated(JSON.stringify(args.vault)), + { vault: oldSampleEncryptedData }, + ); + + expect(isVaultUpdated).toBe(false); + }); + + test('should return true with new value format', async ({ page }) => { + const isVaultUpdated = await page.evaluate( + async (args) => + window.encryptor.isVaultUpdated(JSON.stringify(args.vault)), + { vault: sampleEncryptedData }, + ); + + expect(isVaultUpdated).toBe(true); + }); +}); + +test.describe('encryptor:updateVault', async () => { + test.describe('with old vault format', async () => { + test('should return a vault encrypted with a key derived with new key derivation options', async ({ + page, + }) => { + const updatedVault = await page.evaluate( + async (args) => { + const vault = await window.encryptor.updateVault( + args.vault, + args.password, + ); + return JSON.parse(vault); + }, + { + vault: JSON.stringify(oldSampleEncryptedData), + password: 'a sample passw0rd', + }, + ); + + expect(updatedVault).toHaveProperty('keyMetadata'); + expect(updatedVault.keyMetadata).toStrictEqual( + sampleEncryptedData.keyMetadata, + ); + }); + + test('should return a vault that can be decrypted with the same password', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const updatedVault = await page.evaluate( + async (args) => window.encryptor.updateVault(args.vault, args.password), + { + vault: JSON.stringify(oldSampleEncryptedData), + password, + }, + ); + + const decryptedObj = await page.evaluate( + async (args) => + await window.encryptor.decrypt(args.password, args.encryptedString), + { + encryptedString: updatedVault, + password, + }, + ); + + expect(decryptedObj).toStrictEqual({ foo: 'data to encrypt' }); + }); + }); + + test.describe('with new vault format', async () => { + test('should return the same vault', async ({ page }) => { + const updatedVault = await page.evaluate( + async (args) => { + const vault = await window.encryptor.updateVault( + args.vault, + args.password, + ); + return JSON.parse(vault); + }, + { + vault: JSON.stringify(sampleEncryptedData), + password: 'a sample passw0rd', + }, + ); + + expect(updatedVault).toStrictEqual(sampleEncryptedData); + }); + }); +}); From e1fe87eaae92987e6851f198ac8f3130dbce2328 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Wed, 8 Nov 2023 13:29:17 +0100 Subject: [PATCH 08/12] refactor: rollback typescript and fix typeguards --- package.json | 3 + src/index.ts | 23 +++---- yarn.lock | 191 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 199 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 4983937..7a7e3f3 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,9 @@ "prepack": "./scripts/prepack.sh", "test": "yarn buildTest && playwright test" }, + "dependencies": { + "@metamask/utils": "^8.2.0" + }, "devDependencies": { "@lavamoat/allow-scripts": "^2.0.3", "@metamask/auto-changelog": "^2.6.1", diff --git a/src/index.ts b/src/index.ts index c189492..166f09c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +import { hasProperty, isPlainObject } from '@metamask/utils'; + export type DetailedEncryptionResult = { vault: string; exportedKeyString: string; @@ -431,10 +433,9 @@ export function isEncryptionKey( encryptionKey: unknown, ): encryptionKey is EncryptionKey { return ( - encryptionKey !== null && - typeof encryptionKey === 'object' && - 'key' in encryptionKey && - 'derivationOptions' in encryptionKey && + isPlainObject(encryptionKey) && + hasProperty(encryptionKey, 'key') && + hasProperty(encryptionKey, 'derivationOptions') && encryptionKey.key instanceof CryptoKey && isKeyDerivationOptions(encryptionKey.derivationOptions) ); @@ -450,10 +451,9 @@ export function isKeyDerivationOptions( derivationOptions: unknown, ): derivationOptions is KeyDerivationOptions { return ( - derivationOptions !== null && - typeof derivationOptions === 'object' && - 'algorithm' in derivationOptions && - 'params' in derivationOptions + isPlainObject(derivationOptions) && + hasProperty(derivationOptions, 'algorithm') && + hasProperty(derivationOptions, 'params') ); } @@ -467,10 +467,9 @@ export function isExportedEncryptionKey( exportedKey: unknown, ): exportedKey is ExportedEncryptionKey { return ( - exportedKey !== null && - typeof exportedKey === 'object' && - 'key' in exportedKey && - 'derivationOptions' in exportedKey && + isPlainObject(exportedKey) && + hasProperty(exportedKey, 'key') && + hasProperty(exportedKey, 'derivationOptions') && isKeyDerivationOptions(exportedKey.derivationOptions) ); } diff --git a/yarn.lock b/yarn.lock index 394c049..c20824a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,48 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/common@npm:^3.2.0": + version: 3.2.0 + resolution: "@ethereumjs/common@npm:3.2.0" + dependencies: + "@ethereumjs/util": ^8.1.0 + crc-32: ^1.2.0 + checksum: cb9cc11f5c868cb577ba611cebf55046e509218bbb89b47ccce010776dafe8256d70f8f43fab238aec74cf71f62601cd5842bc03a83261200802de365732a14b + languageName: node + linkType: hard + +"@ethereumjs/rlp@npm:^4.0.1": + version: 4.0.1 + resolution: "@ethereumjs/rlp@npm:4.0.1" + bin: + rlp: bin/rlp + checksum: 30db19c78faa2b6ff27275ab767646929207bb207f903f09eb3e4c273ce2738b45f3c82169ddacd67468b4f063d8d96035f2bf36f02b6b7e4d928eefe2e3ecbc + languageName: node + linkType: hard + +"@ethereumjs/tx@npm:^4.2.0": + version: 4.2.0 + resolution: "@ethereumjs/tx@npm:4.2.0" + dependencies: + "@ethereumjs/common": ^3.2.0 + "@ethereumjs/rlp": ^4.0.1 + "@ethereumjs/util": ^8.1.0 + ethereum-cryptography: ^2.0.0 + checksum: 87a3f5f2452cfbf6712f8847525a80c213210ed453c211c793c5df801fe35ecef28bae17fadd222fcbdd94277478a47e52d2b916a90a6b30cda21f1e0cdaee42 + languageName: node + linkType: hard + +"@ethereumjs/util@npm:^8.1.0": + version: 8.1.0 + resolution: "@ethereumjs/util@npm:8.1.0" + dependencies: + "@ethereumjs/rlp": ^4.0.1 + ethereum-cryptography: ^2.0.0 + micro-ftch: ^0.3.1 + checksum: 9ae5dee8f12b0faf81cd83f06a41560e79b0ba96a48262771d897a510ecae605eb6d84f687da001ab8ccffd50f612ae50f988ef76e6312c752897f462f3ac08d + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -109,6 +151,7 @@ __metadata: "@metamask/eslint-config": ^10.0.0 "@metamask/eslint-config-nodejs": ^10.0.0 "@metamask/eslint-config-typescript": ^10.0.0 + "@metamask/utils": ^8.2.0 "@playwright/test": ^1.27.1 "@types/node": ^14.14.5 "@typescript-eslint/eslint-plugin": ^5.41.0 @@ -166,6 +209,45 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^8.2.0": + version: 8.2.0 + resolution: "@metamask/utils@npm:8.2.0" + dependencies: + "@ethereumjs/tx": ^4.2.0 + "@noble/hashes": ^1.3.1 + "@scure/base": ^1.1.3 + "@types/debug": ^4.1.7 + debug: ^4.3.4 + pony-cause: ^2.1.10 + semver: ^7.5.4 + superstruct: ^1.0.3 + checksum: 1c70c0f9c375bfa3836c15d48990dbea1c3cadfd3dd69b4867667116c09c3bdeef70a0c7027f1cdea88a9913cb846dc94812ece91be7ec32e65a62e00281b04c + 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 + languageName: node + linkType: hard + +"@noble/hashes@npm:1.3.1": + version: 1.3.1 + resolution: "@noble/hashes@npm:1.3.1" + checksum: 7fdefc0f7a0c1ec27acc6ff88841793e3f93ec4ce6b8a6a12bfc0dd70ae6b7c4c82fe305fdfeda1735d5ad4a9eebe761e6693b3d355689c559e91242f4bc95b1 + 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 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -253,6 +335,34 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0": + version: 1.1.3 + resolution: "@scure/base@npm:1.1.3" + checksum: 1606ab8a4db898cb3a1ada16c15437c3bce4e25854fadc8eb03ae93cbbbac1ed90655af4b0be3da37e12056fef11c0374499f69b9e658c9e5b7b3e06353c630c + languageName: node + linkType: hard + +"@scure/bip32@npm:1.3.1": + version: 1.3.1 + resolution: "@scure/bip32@npm:1.3.1" + dependencies: + "@noble/curves": ~1.1.0 + "@noble/hashes": ~1.3.1 + "@scure/base": ~1.1.0 + checksum: 394d65f77a40651eba21a5096da0f4233c3b50d422864751d373fcf142eeedb94a1149f9ab1dbb078086dab2d0bc27e2b1afec8321bf22d4403c7df2fea5bfe2 + languageName: node + linkType: hard + +"@scure/bip39@npm:1.2.1": + version: 1.2.1 + resolution: "@scure/bip39@npm:1.2.1" + dependencies: + "@noble/hashes": ~1.3.0 + "@scure/base": ~1.1.0 + checksum: c5bd6f1328fdbeae2dcdd891825b1610225310e5e62a4942714db51066866e4f7bef242c7b06a1b9dcc8043a4a13412cf5c5df76d3b10aa9e36b82e9b6e3eeaa + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -260,6 +370,15 @@ __metadata: languageName: node linkType: hard +"@types/debug@npm:^4.1.7": + version: 4.1.11 + resolution: "@types/debug@npm:4.1.11" + dependencies: + "@types/ms": "*" + checksum: 6f666691f4706c4c1bbb3023026bc3acff7c98f181cbd4bd12bfc56caa9b4cd084e996358ab7b7748e7d9a59a00cdd15f0c6d4b60ff5a085eac98fa71f04784e + languageName: node + linkType: hard + "@types/glob@npm:^7.1.1": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -291,6 +410,13 @@ __metadata: languageName: node linkType: hard +"@types/ms@npm:*": + version: 0.7.34 + resolution: "@types/ms@npm:0.7.34" + checksum: f38d36e7b6edecd9badc9cf50474159e9da5fa6965a75186cceaf883278611b9df6669dc3a3cc122b7938d317b68a9e3d573d316fcb35d1be47ec9e468c6bd8a + languageName: node + linkType: hard + "@types/node@npm:*": version: 18.11.9 resolution: "@types/node@npm:18.11.9" @@ -1193,6 +1319,15 @@ __metadata: languageName: node linkType: hard +"crc-32@npm:^1.2.0": + version: 1.2.2 + resolution: "crc-32@npm:1.2.2" + bin: + crc32: bin/crc32.njs + checksum: ad2d0ad0cbd465b75dcaeeff0600f8195b686816ab5f3ba4c6e052a07f728c3e70df2e3ca9fd3d4484dc4ba70586e161ca5a2334ec8bf5a41bf022a6103ff243 + languageName: node + linkType: hard + "create-ecdh@npm:^4.0.0": version: 4.0.4 resolution: "create-ecdh@npm:4.0.4" @@ -1860,6 +1995,18 @@ __metadata: languageName: node linkType: hard +"ethereum-cryptography@npm:^2.0.0": + version: 2.1.2 + resolution: "ethereum-cryptography@npm:2.1.2" + dependencies: + "@noble/curves": 1.1.0 + "@noble/hashes": 1.3.1 + "@scure/bip32": 1.3.1 + "@scure/bip39": 1.2.1 + checksum: 2e8f7b8cc90232ae838ab6a8167708e8362621404d26e79b5d9e762c7b53d699f7520aff358d9254de658fcd54d2d0af168ff909943259ed27dc4cef2736410c + languageName: node + linkType: hard + "events@npm:^3.0.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -3027,6 +3174,13 @@ __metadata: languageName: node linkType: hard +"micro-ftch@npm:^0.3.1": + version: 0.3.1 + resolution: "micro-ftch@npm:0.3.1" + checksum: 0e496547253a36e98a83fb00c628c53c3fb540fa5aaeaf718438873785afd193244988c09d219bb1802984ff227d04938d9571ef90fe82b48bd282262586aaff + languageName: node + linkType: hard + "micromatch@npm:^4.0.4": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -3624,6 +3778,13 @@ __metadata: languageName: node linkType: hard +"pony-cause@npm:^2.1.10": + version: 2.1.10 + resolution: "pony-cause@npm:2.1.10" + checksum: 8b61378f213e61056312dc274a1c79980154e9d864f6ad86e0c8b91a50d3ce900d430995ee24147c9f3caa440dfe7d51c274b488d7f033b65b206522536d7217 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -4010,6 +4171,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.5.4": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -4357,6 +4529,13 @@ __metadata: languageName: node linkType: hard +"superstruct@npm:^1.0.3": + version: 1.0.3 + resolution: "superstruct@npm:1.0.3" + checksum: 761790bb111e6e21ddd608299c252f3be35df543263a7ebbc004e840d01fcf8046794c274bcb351bdf3eae4600f79d317d085cdbb19ca05803a4361840cc9bb1 + languageName: node + linkType: hard + "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -4525,12 +4704,12 @@ __metadata: linkType: hard "typescript@npm:^4.8.4": - version: 4.9.5 - resolution: "typescript@npm:4.9.5" + version: 4.8.4 + resolution: "typescript@npm:4.8.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 languageName: node linkType: hard @@ -4545,12 +4724,12 @@ __metadata: linkType: hard "typescript@patch:typescript@^4.8.4#~builtin": - version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=701156" + version: 4.8.4 + resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=701156" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 + checksum: 301459fc3eb3b1a38fe91bf96d98eb55da88a9cb17b4ef80b4d105d620f4d547ba776cc27b44cc2ef58b66eda23fe0a74142feb5e79a6fb99f54fc018a696afa languageName: node linkType: hard From 8675d3462dfa78fa951f747fad7e80db6dcec74b Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Wed, 8 Nov 2023 18:10:37 +0100 Subject: [PATCH 09/12] fix lock file --- yarn.lock | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/yarn.lock b/yarn.lock index c20824a..6d6b120 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -4703,33 +4706,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.8.4": - version: 4.8.4 - resolution: "typescript@npm:4.8.4" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 - languageName: node - linkType: hard - "typescript@npm:~4.8.4": version: 4.8.4 resolution: "typescript@npm:4.8.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db - languageName: node - linkType: hard - -"typescript@patch:typescript@^4.8.4#~builtin": - version: 4.8.4 - resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=701156" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 301459fc3eb3b1a38fe91bf96d98eb55da88a9cb17b4ef80b4d105d620f4d547ba776cc27b44cc2ef58b66eda23fe0a74142feb5e79a6fb99f54fc018a696afa + checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 languageName: node linkType: hard @@ -4739,7 +4722,7 @@ __metadata: bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 + checksum: 301459fc3eb3b1a38fe91bf96d98eb55da88a9cb17b4ef80b4d105d620f4d547ba776cc27b44cc2ef58b66eda23fe0a74142feb5e79a6fb99f54fc018a696afa languageName: node linkType: hard From 60ec49af96b2093cc57fd0f046c73d5e75a6fb11 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Thu, 9 Nov 2023 10:12:55 +0100 Subject: [PATCH 10/12] refactor: make not useful functions private --- src/index.ts | 54 +++++++++++++++--------------- test/index.spec.ts | 82 ---------------------------------------------- 2 files changed, 27 insertions(+), 109 deletions(-) diff --git a/src/index.ts b/src/index.ts index 166f09c..48dfa4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -423,13 +423,35 @@ export function generateSalt(byteCount = 32): string { return b64encoded; } +/** + * Updates the provided vault, re-encrypting + * data with a safer algorithm if one is available. + * + * If the provided vault is already using the latest available encryption method, + * it is returned as is. + * + * @param vault - The vault to update. + * @param password - The password to use for encryption. + * @returns A promise resolving to the updated vault. + */ +export async function updateVault( + vault: string, + password: string, +): Promise { + if (isVaultUpdated(vault)) { + return vault; + } + + return encrypt(password, await decrypt(password, vault)); +} + /** * Checks if the provided key is an `EncryptionKey`. * * @param encryptionKey - The object to check. * @returns Whether or not the key is an `EncryptionKey`. */ -export function isEncryptionKey( +function isEncryptionKey( encryptionKey: unknown, ): encryptionKey is EncryptionKey { return ( @@ -447,7 +469,7 @@ export function isEncryptionKey( * @param derivationOptions - The object to check. * @returns Whether or not the object is a `KeyDerivationOptions`. */ -export function isKeyDerivationOptions( +function isKeyDerivationOptions( derivationOptions: unknown, ): derivationOptions is KeyDerivationOptions { return ( @@ -463,7 +485,7 @@ export function isKeyDerivationOptions( * @param exportedKey - The object to check. * @returns Whether or not the object is an `ExportedEncryptionKey`. */ -export function isExportedEncryptionKey( +function isExportedEncryptionKey( exportedKey: unknown, ): exportedKey is ExportedEncryptionKey { return ( @@ -481,7 +503,7 @@ export function isExportedEncryptionKey( * @param encryptionKey - The key to unwrap. * @returns The `CryptoKey` from the provided encryption key. */ -export function unwrapKey(encryptionKey: EncryptionKey | CryptoKey): CryptoKey { +function unwrapKey(encryptionKey: EncryptionKey | CryptoKey): CryptoKey { return isEncryptionKey(encryptionKey) ? encryptionKey.key : encryptionKey; } @@ -491,7 +513,7 @@ export function unwrapKey(encryptionKey: EncryptionKey | CryptoKey): CryptoKey { * @param vault - The vault to check. * @returns Whether or not the vault is an updated encryption format. */ -export function isVaultUpdated(vault: string): boolean { +function isVaultUpdated(vault: string): boolean { const { keyMetadata } = JSON.parse(vault); return ( isKeyDerivationOptions(keyMetadata) && @@ -500,25 +522,3 @@ export function isVaultUpdated(vault: string): boolean { DEFAULT_DERIVATION_PARAMS.params.iterations ); } - -/** - * Updates the provided vault, re-encrypting - * data with a safer algorithm if one is available. - * - * If the provided vault is already using the latest available encryption method, - * it is returned as is. - * - * @param vault - The vault to update. - * @param password - The password to use for encryption. - * @returns A promise resolving to the updated vault. - */ -export async function updateVault( - vault: string, - password: string, -): Promise { - if (isVaultUpdated(vault)) { - return vault; - } - - return encrypt(password, await decrypt(password, vault)); -} diff --git a/test/index.spec.ts b/test/index.spec.ts index be77219..2ab500a 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -693,66 +693,6 @@ test('encryptor:decrypt old encrypted data and re-encrypt with password', async }); }); -test.describe('encryptor:isEncryptionKey', async () => { - test('should return false with CryptoKey', async ({ page }) => { - const isEncryptionKey = await page.evaluate( - async (args) => { - const encryptionKey = await window.encryptor.importKey( - args.OLD_SAMPLE_EXPORTED_KEY, - ); - return window.encryptor.isEncryptionKey(encryptionKey); - }, - { OLD_SAMPLE_EXPORTED_KEY }, - ); - - expect(isEncryptionKey).toBe(false); - }); - - test('should return true with valid EncryptionKey', async ({ page }) => { - const isEncryptionKey = await page.evaluate( - async (args) => { - const encryptionKey = await window.encryptor.importKey( - args.SAMPLE_EXPORTED_KEY, - ); - return window.encryptor.isEncryptionKey(encryptionKey); - }, - { SAMPLE_EXPORTED_KEY }, - ); - - expect(isEncryptionKey).toBe(true); - }); -}); - -test.describe('encryptor:isExportedEncryptionKey', async () => { - test('should return false with JsonWebKey', async ({ page }) => { - const isEncryptionKey = await page.evaluate( - async (args) => { - return window.encryptor.isExportedEncryptionKey( - JSON.parse(args.OLD_SAMPLE_EXPORTED_KEY), - ); - }, - { OLD_SAMPLE_EXPORTED_KEY }, - ); - - expect(isEncryptionKey).toBe(false); - }); - - test('should return true with valid ExportedEncryptionKey', async ({ - page, - }) => { - const isEncryptionKey = await page.evaluate( - async (args) => { - return window.encryptor.isExportedEncryptionKey( - JSON.parse(args.SAMPLE_EXPORTED_KEY), - ); - }, - { SAMPLE_EXPORTED_KEY }, - ); - - expect(isEncryptionKey).toBe(true); - }); -}); - test('encryptor:encrypt with arbitrary key derivation options then decrypt', async ({ page, }) => { @@ -821,28 +761,6 @@ test('encryptor:encryptWithDetail with arbitrary key derivation options then dec expect(decryptedObj).toStrictEqual(data); }); -test.describe('encryptor:isVaultUpdated', async () => { - test('should return false with old value format', async ({ page }) => { - const isVaultUpdated = await page.evaluate( - async (args) => - window.encryptor.isVaultUpdated(JSON.stringify(args.vault)), - { vault: oldSampleEncryptedData }, - ); - - expect(isVaultUpdated).toBe(false); - }); - - test('should return true with new value format', async ({ page }) => { - const isVaultUpdated = await page.evaluate( - async (args) => - window.encryptor.isVaultUpdated(JSON.stringify(args.vault)), - { vault: sampleEncryptedData }, - ); - - expect(isVaultUpdated).toBe(true); - }); -}); - test.describe('encryptor:updateVault', async () => { test.describe('with old vault format', async () => { test('should return a vault encrypted with a key derived with new key derivation options', async ({ From ce50096e896e5e99eccc470c476696a5e4c1aa47 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Thu, 9 Nov 2023 11:15:02 +0100 Subject: [PATCH 11/12] feat: add `updateVaultWithDetail` --- src/index.ts | 25 ++++++++++++++ test/index.spec.ts | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/index.ts b/src/index.ts index 48dfa4c..d5427f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -445,6 +445,31 @@ export async function updateVault( return encrypt(password, await decrypt(password, vault)); } +/** + * Updates the provided vault and exported key, re-encrypting + * data with a safer algorithm if one is available. + * + * If the provided vault is already using the latest available encryption method, + * it is returned as is. + * + * @param encryptionResult - The encrypted data to update. + * @param password - The password to use for encryption. + * @returns A promise resolving to the updated encrypted data and exported key. + */ +export async function updateVaultWithDetail( + encryptionResult: DetailedEncryptionResult, + password: string, +): Promise { + if (isVaultUpdated(encryptionResult.vault)) { + return encryptionResult; + } + + return encryptWithDetail( + password, + await decrypt(password, encryptionResult.vault), + ); +} + /** * Checks if the provided key is an `EncryptionKey`. * diff --git a/test/index.spec.ts b/test/index.spec.ts index 2ab500a..d7484c6 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -831,3 +831,87 @@ test.describe('encryptor:updateVault', async () => { }); }); }); + +test.describe('encryptor:updateVaultWithDetail', async () => { + test.describe('with old vault format', async () => { + test('should return a vault encrypted with a key derived with new key derivation options', async ({ + page, + }) => { + const detailedVault: Encryptor.DetailedEncryptionResult = { + vault: JSON.stringify(oldSampleEncryptedData), + exportedKeyString: OLD_SAMPLE_EXPORTED_KEY, + }; + + const updatedVault = await page.evaluate( + async (args) => + window.encryptor.updateVaultWithDetail( + args.detailedVault, + args.password, + ), + { + detailedVault, + password: 'a sample passw0rd', + }, + ); + const vault = JSON.parse(updatedVault.vault); + + expect(vault).toHaveProperty('keyMetadata'); + expect(vault.keyMetadata).toStrictEqual(sampleEncryptedData.keyMetadata); + }); + + test('should return a vault that can be decrypted with the same password', async ({ + page, + }) => { + const password = 'a sample passw0rd'; + const detailedVault: Encryptor.DetailedEncryptionResult = { + vault: JSON.stringify(oldSampleEncryptedData), + exportedKeyString: OLD_SAMPLE_EXPORTED_KEY, + }; + const updatedVault = await page.evaluate( + async (args) => + window.encryptor.updateVaultWithDetail( + args.detailedVault, + args.password, + ), + { + detailedVault, + password, + }, + ); + + const decryptedObj = await page.evaluate( + async (args) => + await window.encryptor.decrypt(args.password, args.encryptedString), + { + encryptedString: updatedVault.vault, + password, + }, + ); + + expect(decryptedObj).toStrictEqual({ foo: 'data to encrypt' }); + }); + }); + + test.describe('with new vault format', async () => { + test('should return the same vault', async ({ page }) => { + const detailedVault: Encryptor.DetailedEncryptionResult = { + vault: JSON.stringify(sampleEncryptedData), + exportedKeyString: SAMPLE_EXPORTED_KEY, + }; + + const updatedVault = await page.evaluate( + async (args) => + window.encryptor.updateVaultWithDetail( + args.detailedVault, + args.password, + ), + { + detailedVault, + password: 'a sample passw0rd', + }, + ); + + expect(JSON.parse(updatedVault.vault)).toStrictEqual(sampleEncryptedData); + }); + }); +}); From 131974999a2a2af50f047c16689e46f48f28d085 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Thu, 9 Nov 2023 11:26:05 +0100 Subject: [PATCH 12/12] docs: fix jsdoc for importKey and exportKey --- src/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index d5427f0..5f38cc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -234,8 +234,11 @@ export async function decryptWithKey( /** * Receives an exported CryptoKey string and creates a key. * + * This function supports both JsonWebKey's and exported EncryptionKey's. + * It will return a CryptoKey for the former, and an EncryptionKey for the latter. + * * @param keyString - The key string to import. - * @returns An EncryptionKey. + * @returns An EncryptionKey or a CryptoKey. */ export async function importKey( keyString: string, @@ -265,10 +268,10 @@ export async function importKey( } /** - * Receives an exported CryptoKey string, creates a key, - * and decrypts cipher text with the reconstructed key. + * Exports a key string from a CryptoKey or from an + * EncryptionKey instance. * - * @param encryptionKey - The CryptoKey to export. + * @param encryptionKey - The CryptoKey or EncryptionKey to export. * @returns A key string. */ export async function exportKey(