@bicycle-codes/simple-aes
Version:
An easy way to use symmetric keys in browsers or node
159 lines • 6.19 kB
JavaScript
import { webcrypto } from 'one-webcrypto';
import { fromString, toString } from 'uint8arrays';
import { CONTENT_ENCODING, KEY_ENCODING, DEFAULT_SYMM_ALG } from './CONSTANTS.js';
import { normalizeToBuf, normalizeUtf16ToBuf, base64ToArrBuf } from './util.js';
import { SymmAlg } from './types.js';
/**
* Export a given AES key, returning a string encoded as `base64url`.
*
* @param {CryptoKey} key The key to export.
* @returns {Promise<string>} The key, encoded as `base64url`
*/
async function exportKey(key) {
const buffer = await webcrypto.subtle.exportKey('raw', key);
const arr = new Uint8Array(buffer);
const str = toString(arr, KEY_ENCODING);
return str;
}
export var SymmKeyLength;
(function (SymmKeyLength) {
SymmKeyLength[SymmKeyLength["B128"] = 128] = "B128";
SymmKeyLength[SymmKeyLength["B192"] = 192] = "B192";
SymmKeyLength[SymmKeyLength["B256"] = 256] = "B256";
})(SymmKeyLength || (SymmKeyLength = {}));
export const DEFAULT_SYMM_LEN = SymmKeyLength.B256;
/**
* Take a message object, create a new AES key, and encrypt the message with the
* key. Return encrypted message and the key, encoded as `base64url`.
*
* @param msg The message to encrypt.
* @returns {Promise<[
* { content:string },
* { key:string }
* ]>} The encrypted message and key.
*/
export async function encryptMessage(msg, opts = { length: DEFAULT_SYMM_LEN }) {
const newKey = await createKey({ length: opts.length });
const encryptedContent = await aesEncrypt(msg.content, newKey, SymmAlg.AES_GCM);
const encryptedString = toString(encryptedContent, CONTENT_ENCODING);
const keyAsString = await exportKey(newKey);
return [{ content: encryptedString }, { key: keyAsString }];
}
export async function aesEncrypt(_data, cryptoKey, alg, iv) {
const data = typeof _data === 'string' ?
fromString(_data) :
_data;
const encrypted = (iv ?
await webcrypto.subtle.encrypt({ name: alg, iv }, cryptoKey, data) :
await encryptBytes(data, cryptoKey, { alg }));
return new Uint8Array(encrypted);
}
/**
* @TODO -- can pass in key size
*/
function createKey(opts) {
return webcrypto.subtle.generateKey({
name: opts?.alg || DEFAULT_SYMM_ALG,
length: opts?.length || DEFAULT_SYMM_LEN,
}, true, ['encrypt', 'decrypt']);
}
const DEFAULT_CTR_LEN = 64;
export async function encryptBytes(msg, key, opts) {
const data = normalizeUtf16ToBuf(msg);
const importedKey = (typeof key === 'string' ?
await importKey(key, opts) :
key);
const alg = opts?.alg || DEFAULT_SYMM_ALG;
const iv = opts?.iv || randomBuf(12);
const cipherBuf = await webcrypto.subtle.encrypt({
name: alg,
// AES-CTR uses a counter,
// AES-GCM/AES-CBC use an initialization vector
iv: alg === SymmAlg.AES_CTR ? undefined : iv,
counter: alg === SymmAlg.AES_CTR ? new Uint8Array(iv) : undefined,
length: alg === SymmAlg.AES_CTR ? DEFAULT_CTR_LEN : undefined,
}, importedKey, data);
return joinBufs(iv, cipherBuf);
}
function joinBufs(fst, snd) {
const view1 = new Uint8Array(fst);
const view2 = new Uint8Array(snd);
const joined = new Uint8Array(view1.length + view2.length);
joined.set(view1);
joined.set(view2, view1.length);
return joined.buffer;
}
function randomBuf(length, { max } = { max: 255 }) {
if (max < 1 || max > 255) {
throw new Error('Max must be less than 256 and greater than 0');
}
const arr = new Uint8Array(length);
if (max === 255) {
webcrypto.getRandomValues(arr);
return arr.buffer;
}
let index = 0;
const interval = max + 1;
const divisibleMax = Math.floor(256 / interval) * interval;
const tmp = new Uint8Array(1);
while (index < arr.length) {
webcrypto.getRandomValues(tmp);
if (tmp[0] < divisibleMax) {
arr[index] = tmp[0] % interval;
index++;
}
}
return arr.buffer;
}
/**
* Take a `base64url` encoded key, return a CryptoKey.
*
* @param base64key Key encoded as a string.
* @param opts Algorithm, length, IV (don't need to use this)
* @returns {Promise<CryptoKey>} The CryptoKey
*/
function importKey(base64key, opts) {
const buf = base64ToArrBuf('base64url', base64key);
return webcrypto.subtle.importKey('raw', buf, {
name: opts?.alg || DEFAULT_SYMM_ALG,
length: opts?.length || DEFAULT_SYMM_LEN,
}, true, ['encrypt', 'decrypt']);
}
/**
* Take a message and a `base64url` encoded string as a key.
* Return the decrypted message object.
*
* @param msg The message object
* @param keyString The `base64url` encoded key
* @returns {Promise<{ content:string }>} The decrypted message object.
*/
export async function decryptMessage(msg, keyString) {
const key = await importKey(keyString);
const msgBuf = fromString(msg.content, CONTENT_ENCODING);
const decryptedMsg = await aesDecrypt(msgBuf, key, SymmAlg.AES_GCM);
return { content: toString(decryptedMsg) };
}
export async function aesDecrypt(encrypted, cryptoKey, alg, iv) {
const decrypted = iv ?
await webcrypto.subtle.decrypt({ name: alg, iv }, cryptoKey, encrypted) : await decryptBytes(encrypted, cryptoKey, { alg });
return new Uint8Array(decrypted);
}
async function decryptBytes(msg, key, opts) {
const cipherText = normalizeBase64ToBuf(msg, 'base64pad');
const importedKey = typeof key === 'string' ? await importKey(key, opts) : key;
const alg = opts?.alg || DEFAULT_SYMM_ALG;
const iv = cipherText.slice(0, 12);
const cipherBytes = cipherText.slice(12);
const msgBuff = await webcrypto.subtle.decrypt({
name: alg,
// AES-CTR uses a counter, AES-GCM/AES-CBC use an initialization vector
iv: alg === SymmAlg.AES_CTR ? undefined : iv,
counter: alg === SymmAlg.AES_CTR ? new Uint8Array(iv) : undefined,
length: alg === SymmAlg.AES_CTR ? DEFAULT_CTR_LEN : undefined,
}, importedKey, cipherBytes);
return msgBuff;
}
function normalizeBase64ToBuf(msg, encoding) {
return normalizeToBuf(msg, base64ToArrBuf.bind(null, encoding));
}
//# sourceMappingURL=index.js.map