Skip to content

Commit

Permalink
AES: refactor webcrypto, export overridable cryptoSubtleUtils
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Sep 4, 2023
1 parent 48f982a commit 21e626c
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 68 deletions.
56 changes: 23 additions & 33 deletions src/webcrypto/aes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ensureBytes } from '../utils.js';
import { getWebcryptoSubtle } from './utils.js';
import { cryptoSubtleUtils } from './utils.js';

/**
* AAD is only effective on AES-256-GCM or AES-128-GCM. Otherwise it'll be ignored
Expand All @@ -14,61 +14,51 @@ export type Cipher = (
decrypt(ciphertext: Uint8Array): Promise<Uint8Array>;
};

type Algo = 'AES-CTR' | 'AES-GCM' | 'AES-CBC';
enum AesBlockMode {
CBC = 'AES-CBC',
CTR = 'AES-CTR',
GCM = 'AES-GCM',
}

type BitLength = 128 | 256;

function getCryptParams(
algo: Algo,
algo: AesBlockMode,
nonce: Uint8Array,
AAD?: Uint8Array
): AesCbcParams | AesCtrParams | AesGcmParams {
const params = { name: algo };
if (algo === 'AES-CTR') {
return { ...params, counter: nonce, length: 64 } as AesCtrParams;
} else if (algo === 'AES-GCM') {
return { ...params, iv: nonce, additionalData: AAD } as AesGcmParams;
} else if (algo === 'AES-CBC') {
return { ...params, iv: nonce } as AesCbcParams;
} else {
throw new Error('unknown aes cipher');
}
if (algo === AesBlockMode.CBC) return { name: AesBlockMode.CBC, iv: nonce };
if (algo === AesBlockMode.CTR) return { name: AesBlockMode.CTR, counter: nonce, length: 64 };
if (algo === AesBlockMode.GCM) return { name: AesBlockMode.GCM, iv: nonce, additionalData: AAD };
throw new Error('unknown aes cipher');
}

function generate(algo: Algo, length: BitLength): Cipher {
function generate(algo: AesBlockMode, length: BitLength): Cipher {
const keyLength = length / 8;
const keyParams = { name: algo, length };

return (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => {
ensureBytes(key, keyLength);
const cryptParams = getCryptParams(algo, nonce, AAD);

return {
keyLength,

async encrypt(plaintext: Uint8Array) {
encrypt(plaintext: Uint8Array) {
ensureBytes(plaintext);
const cr = getWebcryptoSubtle();
const iKey = await cr.importKey('raw', key, keyParams, true, ['encrypt']);
const cipher = await cr.encrypt(cryptParams, iKey, plaintext);
return new Uint8Array(cipher);
return cryptoSubtleUtils.aesEncrypt(key, keyParams, cryptParams, plaintext);
},

async decrypt(ciphertext: Uint8Array) {
decrypt(ciphertext: Uint8Array) {
ensureBytes(ciphertext);
const cr = getWebcryptoSubtle();
const iKey = await cr.importKey('raw', key, keyParams, true, ['decrypt']);
const plaintext = await cr.decrypt(cryptParams, iKey, ciphertext);
return new Uint8Array(plaintext);
return cryptoSubtleUtils.aesDecrypt(key, keyParams, cryptParams, ciphertext);
},
};
};
}

export const aes_128_ctr = generate('AES-CTR', 128);
export const aes_256_ctr = generate('AES-CTR', 256);
export const aes_128_ctr = generate(AesBlockMode.CTR, 128);
export const aes_256_ctr = generate(AesBlockMode.CTR, 256);

export const aes_128_cbc = generate('AES-CBC', 128);
export const aes_256_cbc = generate('AES-CBC', 256);
export const aes_128_cbc = generate(AesBlockMode.CBC, 128);
export const aes_256_cbc = generate(AesBlockMode.CBC, 256);

export const aes_128_gcm = generate('AES-GCM', 128);
export const aes_256_gcm = generate('AES-GCM', 256);
export const aes_128_gcm = generate(AesBlockMode.GCM, 128);
export const aes_256_gcm = generate(AesBlockMode.GCM, 256);
20 changes: 5 additions & 15 deletions src/webcrypto/ff1.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { AsyncCipher } from '../utils.js';
import { getWebcryptoSubtle } from './utils.js';
import { cryptoSubtleUtils } from './utils.js';

// Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G.
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf

const BLOCK_LEN = 16;

// Utils
function toBytesBE(num: bigint, length?: number): Uint8Array {
let hex = num.toString(16);
Expand All @@ -30,18 +32,6 @@ function mod(a: any, b: any): number | bigint {
const result = a % b;
return result >= 0 ? result : b + result;
}
// AES stuff
const BLOCK_LEN = 16;
const IV = new Uint8Array(BLOCK_LEN);
export async function encryptBlock(msg: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
if (key.length !== 16 && key.length !== 32) throw new Error('Invalid key length');
const cr = getWebcryptoSubtle();
const mode = { name: `AES-CBC`, length: key.length * 8 };
const wKey = await cr.importKey('raw', key, mode, true, ['encrypt']);
const cipher = await cr.encrypt({ name: `aes-cbc`, iv: IV, counter: IV, length: 64 }, wKey, msg);
return new Uint8Array(cipher).subarray(0, 16);
}

function NUMradix(radix: number, data: number[]): bigint {
let res = 0n;
for (let i of data) res = res * BigInt(radix) + BigInt(i);
Expand Down Expand Up @@ -81,15 +71,15 @@ async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: nu
let r = new Uint8Array(16);
for (let j = 0; j < PQ.length / BLOCK_LEN; j++) {
for (let i = 0; i < BLOCK_LEN; i++) r[i] ^= PQ[j * BLOCK_LEN + i];
r.set(await encryptBlock(r, key));
r.set(await cryptoSubtleUtils.aesEncryptBlock(r, key));
}
// Let S be the first d bytes of the following string of ⎡d/16⎤ blocks:
// R || CIPHK(R ⊕[1]16) || CIPHK(R ⊕[2]16) ...CIPHK(R ⊕[⎡d / 16⎤ – 1]16).
let s = Array.from(r);
for (let j = 1; s.length < d; j++) {
const block = toBytesBE(BigInt(j), 16);
for (let k = 0; k < BLOCK_LEN; k++) block[k] ^= r[k];
s.push(...Array.from(await encryptBlock(block, key)));
s.push(...Array.from(await cryptoSubtleUtils.aesEncryptBlock(block, key)));
}
let y = fromBytesBE(Uint8Array.from(s.slice(0, d)));
s.fill(0);
Expand Down
21 changes: 5 additions & 16 deletions src/webcrypto/siv.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { AsyncCipher, createView, setBigUint64 } from '../utils.js';
import { polyval } from '../_polyval.js';
import { getWebcryptoSubtle } from './utils.js';
import { cryptoSubtleUtils } from './utils.js';

/**
* AES-GCM-SIV: classic AES-GCM with nonce-misuse resistance.
* RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452
*/

// AES stuff (same as ff1)
const BLOCK_LEN = 16;
const IV = new Uint8Array(BLOCK_LEN);
async function encryptBlock(msg: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
if (key.length !== 16 && key.length !== 32) throw new Error('Invalid key length');
const mode = { name: `AES-CBC`, length: key.length * 8 };
const cr = getWebcryptoSubtle();
const wKey = await cr.importKey('raw', key, mode, true, ['encrypt']);
const cipher = await cr.encrypt({ name: `aes-cbc`, iv: IV, counter: IV, length: 64 }, wKey, msg);
return new Uint8Array(cipher).subarray(0, 16);
}

// Kinda constant-time equality
function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
// Should not happen
Expand Down Expand Up @@ -48,7 +37,7 @@ async function ctr(key: Uint8Array, tag: Uint8Array, input: Uint8Array) {
let view = createView(block);
let output = new Uint8Array(input.length);
for (let pos = 0; pos < input.length; ) {
const encryptedBlock = await encryptBlock(block, key);
const encryptedBlock = await cryptoSubtleUtils.aesEncryptBlock(block, key);
view.setUint32(0, view.getUint32(0, true) + 1, true);
const take = Math.min(input.length, encryptedBlock.length);
for (let j = 0; j < take; j++, pos++) output[pos] = encryptedBlock[j] ^ input[pos];
Expand All @@ -70,7 +59,7 @@ export async function deriveKeys(key: Uint8Array, nonce: Uint8Array) {
for (const derivedKey of [authKey, encKey]) {
for (let i = 0; i < derivedKey.length; i += 8) {
view.setUint32(0, counter++, true);
const block = await encryptBlock(deriveBlock, key);
const block = await cryptoSubtleUtils.aesEncryptBlock(deriveBlock, key);
derivedKey.set(block.subarray(0, 8), i);
}
}
Expand Down Expand Up @@ -99,7 +88,7 @@ export async function aes_256_gcm_siv(
for (let i = 0; i < 12; i++) tag[i] ^= nonce[i];
// Clear the highest bit
tag[15] &= 0x7f;
return await encryptBlock(tag, encKey);
return await cryptoSubtleUtils.aesEncryptBlock(tag, encKey);
};
return {
// computeTag,
Expand Down
31 changes: 27 additions & 4 deletions src/webcrypto/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,32 @@ export function randomBytes(bytesLength = 32): Uint8Array {
throw new Error('crypto.getRandomValues must be defined');
}

export function getWebcryptoSubtle() {
if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) {
return crypto.subtle;
}
function getWebcryptoSubtle() {
if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) return crypto.subtle;
throw new Error('crypto.subtle must be defined');
}

// Overridable
const BLOCK_LEN = 16;
const IV_BUF = new Uint8Array(BLOCK_LEN);
export const cryptoSubtleUtils = {
async aesEncrypt(key: Uint8Array, keyParams: any, cryptParams: any, plaintext: Uint8Array) {
const cr = getWebcryptoSubtle();
const iKey = await cr.importKey('raw', key, keyParams, true, ['encrypt']);
const ciphertext = await cr.encrypt(cryptParams, iKey, plaintext);
return new Uint8Array(ciphertext);
},
async aesDecrypt(key: Uint8Array, keyParams: any, cryptParams: any, ciphertext: Uint8Array) {
const cr = getWebcryptoSubtle();
const iKey = await cr.importKey('raw', key, keyParams, true, ['decrypt']);
const plaintext = await cr.decrypt(cryptParams, iKey, ciphertext);
return new Uint8Array(plaintext);
},
async aesEncryptBlock(msg: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
if (key.length !== 16 && key.length !== 32) throw new Error('invalid key length');
const keyParams = { name: 'AES-CBC', length: key.length * 8 };
const cryptParams = { name: 'aes-cbc', iv: IV_BUF, counter: IV_BUF, length: 64 };
const ciphertext = await cryptoSubtleUtils.aesEncrypt(key, keyParams, cryptParams, msg);
return ciphertext.subarray(0, 16);
},
};

0 comments on commit 21e626c

Please sign in to comment.