From 26cf6a5686cac72faa84d0abe6966541f656c748 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 3 Nov 2022 18:00:33 -0230 Subject: [PATCH] Export key derivation options Key derivation options are now exported from the functions `keyFromPassword` and `encryptWithDetail`. This can allow the project using this package to store the key derivation options alongside the vault, allowing for easier migrations to newer derivation options in the future. --- src/index.ts | 94 +++++++++++++++++++++++++++++++++++++--------- test/index.spec.ts | 73 +++++++++++++++++++++++------------ 2 files changed, 126 insertions(+), 41 deletions(-) diff --git a/src/index.ts b/src/index.ts index b425e8a..02c4349 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ type DetailedEncryptionResult = { vault: string; exportedKeyString: string; + keyDerivationOptions: KeyDerivationOptions; }; type EncryptionResult = { @@ -35,7 +36,7 @@ export async function encrypt( key?: CryptoKey, salt: string = generateSalt(), ): Promise { - const cryptoKey = key || (await keyFromPassword(password, salt)); + const cryptoKey = key || (await keyFromPassword({ password, salt })).key; const payload = await encryptWithKey(cryptoKey, dataObj); payload.salt = salt; return JSON.stringify(payload); @@ -55,13 +56,17 @@ export async function encryptWithDetail( dataObj: R, salt = generateSalt(), ): Promise { - const key = await keyFromPassword(password, salt); + const { key, keyDerivationOptions } = await keyFromPassword({ + password, + salt, + }); const exportedKeyString = await exportKey(key); const vault = await encrypt(password, dataObj, key, salt); return { vault, exportedKeyString, + keyDerivationOptions, }; } @@ -117,7 +122,7 @@ export async function decrypt( const payload = JSON.parse(text); const { salt } = payload; - const cryptoKey = key || (await keyFromPassword(password, salt)); + const cryptoKey = key || (await keyFromPassword({ password, salt })).key; const result = await decryptWithKey(cryptoKey, payload); return result; @@ -137,7 +142,11 @@ export async function decryptWithDetail( ): Promise { const payload = JSON.parse(text); const { salt } = payload; - const key = await keyFromPassword(password, salt); + + const { key } = await keyFromPassword({ + password, + salt, + }); const exportedKeyString = await exportKey(key); const vault = await decrypt(password, text, key); @@ -211,42 +220,93 @@ export async function exportKey(key: CryptoKey): Promise { return JSON.stringify(exportedKey); } +type AllowedImportAlgorithms = 'PBKDF2'; +type AllowedDerivationAlgorithms = { + name: 'PBKDF2'; + iterations: 10000; + hash: 'SHA-256'; +}; +type AllowedDerivedKeyAlgorithms = { + name: 'AES-GCM'; + length: 256; +}; + +export type KeyDerivationOptions = { + /** + * The algorithm used to import a key from the password + * (see {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey}). + */ + importAlgorithm?: AllowedImportAlgorithms; + /** + * The algorithm used to derive an encryption/decryption key + * from the imported key (see {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey}). + */ + derivationAlgorithm?: AllowedDerivationAlgorithms; + /** + * The algorithm the derived key will be used for + * (see {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey}). + */ + derivedKeyAlgorithm?: AllowedDerivedKeyAlgorithms; +}; + /** * Generate a CryptoKey from a password and random salt. * - * @param password - The password to use to generate key. - * @param salt - The salt string to use in key derivation. - * @returns A CryptoKey for encryption and decryption. + * @param options - Key derivation options. + * @param options.password - The password to use to generate key. + * @param options.salt - The salt string to use in key derivation. + * @returns The derived key, along with all encryption options used. */ -export async function keyFromPassword( - password: string, - salt: string, -): Promise { +export async function keyFromPassword({ + password, + salt, +}: { + password: string; + salt: string; +}): Promise<{ + keyDerivationOptions: KeyDerivationOptions; + key: CryptoKey; +}> { const passBuffer = Buffer.from(password, STRING_ENCODING); const saltBuffer = Buffer.from(salt, 'base64'); + const importAlgorithm = 'PBKDF2'; + const derivationAlgorithm = { + name: 'PBKDF2' as const, + iterations: 10000 as const, + hash: 'SHA-256' as const, + }; + const derivedKeyAlgorithm = { + name: 'AES-GCM' as const, + length: 256 as const, + }; const key = await global.crypto.subtle.importKey( 'raw', passBuffer, - { name: 'PBKDF2' }, + importAlgorithm, false, ['deriveBits', 'deriveKey'], ); const derivedKey = await global.crypto.subtle.deriveKey( { - name: 'PBKDF2', + ...derivationAlgorithm, salt: saltBuffer, - iterations: 10000, - hash: 'SHA-256', }, key, - { name: DERIVED_KEY_FORMAT, length: 256 }, + derivedKeyAlgorithm, true, ['encrypt', 'decrypt'], ); - return derivedKey; + return { + key: derivedKey, + keyDerivationOptions: { + importAlgorithm, + derivationAlgorithm, + derivedKeyAlgorithm, + }, + }; } /** diff --git a/test/index.spec.ts b/test/index.spec.ts index d86528c..e87d721 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -111,6 +111,31 @@ test('encryptor:encryptWithDetail returns vault', async ({ page }) => { expect(typeof encryptedDetail.vault).toBe('string'); }); +test('encryptor:encryptWithDetail returns expected key derivation options', async ({ + page, +}) => { + const password = 'a sample passw0rd'; + const data = { foo: 'data to encrypt' }; + + const { keyDerivationOptions } = await page.evaluate( + async (args) => + await window.encryptor.encryptWithDetail(args.password, args.data), + { data, password }, + ); + expect(keyDerivationOptions).toMatchObject({ + importAlgorithm: 'PBKDF2', + derivationAlgorithm: { + name: 'PBKDF2', + iterations: 10000, + hash: 'SHA-256', + }, + derivedKeyAlgorithm: { + name: 'AES-GCM', + length: 256, + }, + }); +}); + test('encryptor:encrypt & decrypt with wrong password', async ({ page }) => { const password = 'a sample passw0rd'; const wrongPassword = 'a wrong password'; @@ -216,10 +241,10 @@ test('encryptor:encrypt using key then decrypt', async ({ page }) => { const encryptedData = await page.evaluate( async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); + const { key } = await window.encryptor.keyFromPassword({ + password: args.password, + salt: args.salt, + }); return await window.encryptor.encryptWithKey(key, args.data); }, { data, password, salt }, @@ -248,10 +273,10 @@ test('encryptor:encrypt using key then decrypt using wrong password', async ({ const encryptedData = await page.evaluate( async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); + const { key } = await window.encryptor.keyFromPassword({ + password: args.password, + salt: args.salt, + }); return await window.encryptor.encryptWithKey(key, args.data); }, { data, password, salt }, @@ -288,10 +313,10 @@ test('encryptor:encrypt then decrypt using key', async ({ page }) => { const decryptedData = await page.evaluate( async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); + const { key } = await window.encryptor.keyFromPassword({ + password: args.password, + salt: args.salt, + }); return await window.encryptor.decryptWithKey(key, args.encryptedPayload); }, { encryptedPayload, password, salt }, @@ -319,10 +344,10 @@ test('encryptor:encrypt then decrypt using key derived from wrong password', asy await expect( page.evaluate( async (args) => { - const key = await window.encryptor.keyFromPassword( - args.wrongPassword, - args.salt, - ); + const { key } = await window.encryptor.keyFromPassword({ + password: args.wrongPassword, + salt: args.salt, + }); return await window.encryptor.decryptWithKey( key, args.encryptedPayload, @@ -344,10 +369,10 @@ test('encryptor:decrypt encrypted data using key', async ({ page }) => { const decryptedData = await page.evaluate( async (args) => { - const key = await window.encryptor.keyFromPassword( - args.password, - args.salt, - ); + const { key } = await window.encryptor.keyFromPassword({ + password: args.password, + salt: args.salt, + }); return await window.encryptor.decryptWithKey(key, args.encryptedPayload); }, { encryptedPayload, password, salt }, @@ -369,10 +394,10 @@ test('encryptor:decrypt encrypted data using key derived from wrong password', a await expect( page.evaluate( async (args) => { - const key = await window.encryptor.keyFromPassword( - args.wrongPassword, - args.salt, - ); + const { key } = await window.encryptor.keyFromPassword({ + password: args.wrongPassword, + salt: args.salt, + }); return await window.encryptor.decryptWithKey( key, args.encryptedPayload,