Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide exported key with encryption #29

Merged
merged 4 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
128 changes: 115 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,70 @@
interface DetailedEncryptionResult {
vault: string;
exportedKeyString: string;
}

interface EncryptionResult {
data: string;
iv: string;
salt?: string;
}

interface DetailedDecryptResult {
exportedKeyString: string;
vault: unknown;
salt: string;
}

const EXPORT_FORMAT = 'jwk';
const DERIVED_KEY_FORMAT = 'AES-GCM';
const STRING_ENCODING = 'utf-8';

/**
* Encrypts a data object that can be any serializable value using
* a provided password.
*
* @param {string} password - password to use for encryption
* @param {R} dataObj - data to encrypt
* @param {CryptoKey} key - a CryptoKey instance
* @param {string} salt - salt used to encrypt
* @returns {Promise<string>} cypher text
*/
export async function encrypt<R>(
password: string,
dataObj: R,
key?: CryptoKey,
salt: string = generateSalt(),
): Promise<string> {
const salt = generateSalt();

const passwordDerivedKey = await keyFromPassword(password, salt);
const payload = await encryptWithKey(passwordDerivedKey, dataObj);
const cryptoKey = key || (await keyFromPassword(password, salt));
const payload = await encryptWithKey(cryptoKey, dataObj);
payload.salt = salt;
return JSON.stringify(payload);
}

/**
* Encrypts a data object that can be any serializable value using
* a provided password.
*
* @param {string} password - password to use for encryption
* @param {R} dataObj - data to encrypt
* @param {R} salt - salt used to encrypt
* @returns {Promise<DetailedEncryptionResult>} object with vault and exportedKeyString
*/
export async function encryptWithDetail<R>(
password: string,
dataObj: R,
salt = generateSalt(),
): Promise<DetailedEncryptionResult> {
const key = await keyFromPassword(password, salt);
const exportedKeyString = await exportKey(key);
const vault = await encrypt(password, dataObj, key, salt);

return {
vault,
exportedKeyString,
};
}

/**
* Encrypts the provided serializable javascript object using the
* provided CryptoKey and returns an object containing the cypher text and
Expand All @@ -37,12 +78,12 @@ export async function encryptWithKey<R>(
dataObj: R,
): Promise<EncryptionResult> {
const data = JSON.stringify(dataObj);
const dataBuffer = Buffer.from(data, 'utf-8');
const dataBuffer = Buffer.from(data, STRING_ENCODING);
const vector = global.crypto.getRandomValues(new Uint8Array(16));

const buf = await global.crypto.subtle.encrypt(
{
name: 'AES-GCM',
name: DERIVED_KEY_FORMAT,
iv: vector,
},
key,
Expand All @@ -63,12 +104,45 @@ export async function encryptWithKey<R>(
* the resulting value
* @param {string} password - password to decrypt with
* @param {string} text - cypher text to decrypt
* @param {CryptoKey} key - a key to use for decrypting
* @returns {object}
*/
export async function decrypt(
password: string,
text: string,
key?: CryptoKey,
): Promise<unknown> {
const payload = JSON.parse(text);
const { salt } = payload;

const cryptoKey = key || (await keyFromPassword(password, salt));

const result = await decryptWithKey(cryptoKey, payload);
return result;
}

/**
* Given a password and a cypher text, decrypts the text and returns
* the resulting value, keyString, and salt
* @param {string} password - password to decrypt with
* @param {string} text - cypher text to decrypt
* @returns {object}
*/
export async function decrypt<R>(password: string, text: string): Promise<R> {
export async function decryptWithDetail(
password: string,
text: string,
): Promise<DetailedDecryptResult> {
const payload = JSON.parse(text);
const { salt } = payload;
const key = await keyFromPassword(password, salt);
return await decryptWithKey(key, payload);
const exportedKeyString = await exportKey(key);
const vault = await decrypt(password, text, key);

return {
exportedKeyString,
vault,
salt,
};
}

/**
Expand All @@ -87,13 +161,13 @@ export async function decryptWithKey<R>(
let decryptedObj;
try {
const result = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: vector },
{ name: DERIVED_KEY_FORMAT, iv: vector },
key,
encryptedData,
);

const decryptedData = new Uint8Array(result);
const decryptedStr = Buffer.from(decryptedData).toString('utf-8');
const decryptedStr = Buffer.from(decryptedData).toString(STRING_ENCODING);
decryptedObj = JSON.parse(decryptedStr);
} catch (e) {
throw new Error('Incorrect password');
Expand All @@ -102,6 +176,34 @@ export async function decryptWithKey<R>(
return decryptedObj;
}

/**
* Receives an exported CryptoKey string and creates a key
* @param {string} keyString - keyString to import
* @returns {CryptoKey}
*/
export async function importKey(keyString: string): Promise<CryptoKey> {
const key = await window.crypto.subtle.importKey(
EXPORT_FORMAT,
JSON.parse(keyString),
DERIVED_KEY_FORMAT,
true,
['encrypt', 'decrypt'],
);

return key;
}

/**
* Receives an exported CryptoKey string, creates a key,
* and decrypts cipher text with the reconstructed key
* @param {CryptoKey} key - key to export
* @returns {string}
*/
export async function exportKey(key: CryptoKey): Promise<string> {
const exportedKey = await window.crypto.subtle.exportKey(EXPORT_FORMAT, key);
return JSON.stringify(exportedKey);
}

/**
* Generate a CryptoKey from a password and random salt
* @param {string} password - The password to use to generate key
Expand All @@ -111,7 +213,7 @@ export async function keyFromPassword(
password: string,
salt: string,
): Promise<CryptoKey> {
const passBuffer = Buffer.from(password, 'utf-8');
const passBuffer = Buffer.from(password, STRING_ENCODING);
const saltBuffer = Buffer.from(salt, 'base64');

const key = await global.crypto.subtle.importKey(
Expand All @@ -130,8 +232,8 @@ export async function keyFromPassword(
hash: 'SHA-256',
},
key,
{ name: 'AES-GCM', length: 256 },
false,
{ name: DERIVED_KEY_FORMAT, length: 256 },
true,
['encrypt', 'decrypt'],
);

Expand Down
Loading