From 21e626c282d1d3f4df8e66a4a61e3feeee03c203 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Mon, 4 Sep 2023 16:06:54 +0000 Subject: [PATCH] AES: refactor webcrypto, export overridable cryptoSubtleUtils --- src/webcrypto/aes.ts | 56 +++++++++++++++++------------------------- src/webcrypto/ff1.ts | 20 ++++----------- src/webcrypto/siv.ts | 21 ++++------------ src/webcrypto/utils.ts | 31 ++++++++++++++++++++--- 4 files changed, 60 insertions(+), 68 deletions(-) diff --git a/src/webcrypto/aes.ts b/src/webcrypto/aes.ts index cfbf13f..29274c3 100644 --- a/src/webcrypto/aes.ts +++ b/src/webcrypto/aes.ts @@ -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 @@ -14,61 +14,51 @@ export type Cipher = ( decrypt(ciphertext: Uint8Array): Promise; }; -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); diff --git a/src/webcrypto/ff1.ts b/src/webcrypto/ff1.ts index 432578b..952a74a 100644 --- a/src/webcrypto/ff1.ts +++ b/src/webcrypto/ff1.ts @@ -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); @@ -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 { - 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); @@ -81,7 +71,7 @@ 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). @@ -89,7 +79,7 @@ async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: nu 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); diff --git a/src/webcrypto/siv.ts b/src/webcrypto/siv.ts index fe37ca9..1f2beb7 100644 --- a/src/webcrypto/siv.ts +++ b/src/webcrypto/siv.ts @@ -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 { - 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 @@ -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]; @@ -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); } } @@ -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, diff --git a/src/webcrypto/utils.ts b/src/webcrypto/utils.ts index c006024..fa924fd 100644 --- a/src/webcrypto/utils.ts +++ b/src/webcrypto/utils.ts @@ -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 { + 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); + }, +};