Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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.
  • Loading branch information
Gudahtt committed Nov 4, 2022
commit 26cf6a5686cac72faa84d0abe6966541f656c748
94 changes: 77 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type DetailedEncryptionResult = {
vault: string;
exportedKeyString: string;
keyDerivationOptions: KeyDerivationOptions;
};

type EncryptionResult = {
Expand Down Expand Up @@ -35,7 +36,7 @@ export async function encrypt<R>(
key?: CryptoKey,
salt: string = generateSalt(),
): Promise<string> {
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);
Expand All @@ -55,13 +56,17 @@ export async function encryptWithDetail<R>(
dataObj: R,
salt = generateSalt(),
): Promise<DetailedEncryptionResult> {
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,
};
}

Expand Down Expand Up @@ -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;
Expand All @@ -137,7 +142,11 @@ export async function decryptWithDetail(
): Promise<DetailedDecryptResult> {
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);

Expand Down Expand Up @@ -211,42 +220,93 @@ export async function exportKey(key: CryptoKey): Promise<string> {
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<CryptoKey> {
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,
},
};
}

/**
Expand Down
73 changes: 49 additions & 24 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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,
Expand All @@ -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 },
Expand All @@ -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,
Expand Down