901 lines
36 KiB
JavaScript
901 lines
36 KiB
JavaScript
/**
|
||
* [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)
|
||
* a.k.a. Advanced Encryption Standard
|
||
* is a variant of Rijndael block cipher, standardized by NIST in 2001.
|
||
* We provide the fastest available pure JS implementation.
|
||
*
|
||
* Data is split into 128-bit blocks. Encrypted in 10/12/14 rounds (128/192/256 bits). In every round:
|
||
* 1. **S-box**, table substitution
|
||
* 2. **Shift rows**, cyclic shift left of all rows of data array
|
||
* 3. **Mix columns**, multiplying every column by fixed polynomial
|
||
* 4. **Add round key**, round_key xor i-th column of array
|
||
*
|
||
* Check out [FIPS-197](https://csrc.nist.gov/files/pubs/fips/197/final/docs/fips-197.pdf)
|
||
* and [original proposal](https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf)
|
||
* @module
|
||
*/
|
||
import { abytes } from './_assert.js';
|
||
import { ghash, polyval } from './_polyval.js';
|
||
import { clean, complexOverlapBytes, concatBytes, copyBytes, createView, equalBytes, getOutput, isAligned32, overlapBytes, setBigUint64, u32, u8, wrapCipher, } from './utils.js';
|
||
const BLOCK_SIZE = 16;
|
||
const BLOCK_SIZE32 = 4;
|
||
const EMPTY_BLOCK = /* @__PURE__ */ new Uint8Array(BLOCK_SIZE);
|
||
const POLY = 0x11b; // 1 + x + x**3 + x**4 + x**8
|
||
// TODO: remove multiplication, binary ops only
|
||
function mul2(n) {
|
||
return (n << 1) ^ (POLY & -(n >> 7));
|
||
}
|
||
function mul(a, b) {
|
||
let res = 0;
|
||
for (; b > 0; b >>= 1) {
|
||
// Montgomery ladder
|
||
res ^= a & -(b & 1); // if (b&1) res ^=a (but const-time).
|
||
a = mul2(a); // a = 2*a
|
||
}
|
||
return res;
|
||
}
|
||
// AES S-box is generated using finite field inversion,
|
||
// an affine transform, and xor of a constant 0x63.
|
||
const sbox = /* @__PURE__ */ (() => {
|
||
const t = new Uint8Array(256);
|
||
for (let i = 0, x = 1; i < 256; i++, x ^= mul2(x))
|
||
t[i] = x;
|
||
const box = new Uint8Array(256);
|
||
box[0] = 0x63; // first elm
|
||
for (let i = 0; i < 255; i++) {
|
||
let x = t[255 - i];
|
||
x |= x << 8;
|
||
box[t[i]] = (x ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63) & 0xff;
|
||
}
|
||
clean(t);
|
||
return box;
|
||
})();
|
||
// Inverted S-box
|
||
const invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j));
|
||
// Rotate u32 by 8
|
||
const rotr32_8 = (n) => (n << 24) | (n >>> 8);
|
||
const rotl32_8 = (n) => (n << 8) | (n >>> 24);
|
||
// The byte swap operation for uint32 (LE<->BE)
|
||
const byteSwap = (word) => ((word << 24) & 0xff000000) |
|
||
((word << 8) & 0xff0000) |
|
||
((word >>> 8) & 0xff00) |
|
||
((word >>> 24) & 0xff);
|
||
// T-table is optimization suggested in 5.2 of original proposal (missed from FIPS-197). Changes:
|
||
// - LE instead of BE
|
||
// - bigger tables: T0 and T1 are merged into T01 table and T2 & T3 into T23;
|
||
// so index is u16, instead of u8. This speeds up things, unexpectedly
|
||
function genTtable(sbox, fn) {
|
||
if (sbox.length !== 256)
|
||
throw new Error('Wrong sbox length');
|
||
const T0 = new Uint32Array(256).map((_, j) => fn(sbox[j]));
|
||
const T1 = T0.map(rotl32_8);
|
||
const T2 = T1.map(rotl32_8);
|
||
const T3 = T2.map(rotl32_8);
|
||
const T01 = new Uint32Array(256 * 256);
|
||
const T23 = new Uint32Array(256 * 256);
|
||
const sbox2 = new Uint16Array(256 * 256);
|
||
for (let i = 0; i < 256; i++) {
|
||
for (let j = 0; j < 256; j++) {
|
||
const idx = i * 256 + j;
|
||
T01[idx] = T0[i] ^ T1[j];
|
||
T23[idx] = T2[i] ^ T3[j];
|
||
sbox2[idx] = (sbox[i] << 8) | sbox[j];
|
||
}
|
||
}
|
||
return { sbox, sbox2, T0, T1, T2, T3, T01, T23 };
|
||
}
|
||
const tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => (mul(s, 3) << 24) | (s << 16) | (s << 8) | mul(s, 2));
|
||
const tableDecoding = /* @__PURE__ */ genTtable(invSbox, (s) => (mul(s, 11) << 24) | (mul(s, 13) << 16) | (mul(s, 9) << 8) | mul(s, 14));
|
||
const xPowers = /* @__PURE__ */ (() => {
|
||
const p = new Uint8Array(16);
|
||
for (let i = 0, x = 1; i < 16; i++, x = mul2(x))
|
||
p[i] = x;
|
||
return p;
|
||
})();
|
||
/** Key expansion used in CTR. */
|
||
function expandKeyLE(key) {
|
||
abytes(key);
|
||
const len = key.length;
|
||
if (![16, 24, 32].includes(len))
|
||
throw new Error('aes: invalid key size, should be 16, 24 or 32, got ' + len);
|
||
const { sbox2 } = tableEncoding;
|
||
const toClean = [];
|
||
if (!isAligned32(key))
|
||
toClean.push((key = copyBytes(key)));
|
||
const k32 = u32(key);
|
||
const Nk = k32.length;
|
||
const subByte = (n) => applySbox(sbox2, n, n, n, n);
|
||
const xk = new Uint32Array(len + 28); // expanded key
|
||
xk.set(k32);
|
||
// 4.3.1 Key expansion
|
||
for (let i = Nk; i < xk.length; i++) {
|
||
let t = xk[i - 1];
|
||
if (i % Nk === 0)
|
||
t = subByte(rotr32_8(t)) ^ xPowers[i / Nk - 1];
|
||
else if (Nk > 6 && i % Nk === 4)
|
||
t = subByte(t);
|
||
xk[i] = xk[i - Nk] ^ t;
|
||
}
|
||
clean(...toClean);
|
||
return xk;
|
||
}
|
||
function expandKeyDecLE(key) {
|
||
const encKey = expandKeyLE(key);
|
||
const xk = encKey.slice();
|
||
const Nk = encKey.length;
|
||
const { sbox2 } = tableEncoding;
|
||
const { T0, T1, T2, T3 } = tableDecoding;
|
||
// Inverse key by chunks of 4 (rounds)
|
||
for (let i = 0; i < Nk; i += 4) {
|
||
for (let j = 0; j < 4; j++)
|
||
xk[i + j] = encKey[Nk - i - 4 + j];
|
||
}
|
||
clean(encKey);
|
||
// apply InvMixColumn except first & last round
|
||
for (let i = 4; i < Nk - 4; i++) {
|
||
const x = xk[i];
|
||
const w = applySbox(sbox2, x, x, x, x);
|
||
xk[i] = T0[w & 0xff] ^ T1[(w >>> 8) & 0xff] ^ T2[(w >>> 16) & 0xff] ^ T3[w >>> 24];
|
||
}
|
||
return xk;
|
||
}
|
||
// Apply tables
|
||
function apply0123(T01, T23, s0, s1, s2, s3) {
|
||
return (T01[((s0 << 8) & 0xff00) | ((s1 >>> 8) & 0xff)] ^
|
||
T23[((s2 >>> 8) & 0xff00) | ((s3 >>> 24) & 0xff)]);
|
||
}
|
||
function applySbox(sbox2, s0, s1, s2, s3) {
|
||
return (sbox2[(s0 & 0xff) | (s1 & 0xff00)] |
|
||
(sbox2[((s2 >>> 16) & 0xff) | ((s3 >>> 16) & 0xff00)] << 16));
|
||
}
|
||
function encrypt(xk, s0, s1, s2, s3) {
|
||
const { sbox2, T01, T23 } = tableEncoding;
|
||
let k = 0;
|
||
(s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]);
|
||
const rounds = xk.length / 4 - 2;
|
||
for (let i = 0; i < rounds; i++) {
|
||
const t0 = xk[k++] ^ apply0123(T01, T23, s0, s1, s2, s3);
|
||
const t1 = xk[k++] ^ apply0123(T01, T23, s1, s2, s3, s0);
|
||
const t2 = xk[k++] ^ apply0123(T01, T23, s2, s3, s0, s1);
|
||
const t3 = xk[k++] ^ apply0123(T01, T23, s3, s0, s1, s2);
|
||
(s0 = t0), (s1 = t1), (s2 = t2), (s3 = t3);
|
||
}
|
||
// last round (without mixcolumns, so using SBOX2 table)
|
||
const t0 = xk[k++] ^ applySbox(sbox2, s0, s1, s2, s3);
|
||
const t1 = xk[k++] ^ applySbox(sbox2, s1, s2, s3, s0);
|
||
const t2 = xk[k++] ^ applySbox(sbox2, s2, s3, s0, s1);
|
||
const t3 = xk[k++] ^ applySbox(sbox2, s3, s0, s1, s2);
|
||
return { s0: t0, s1: t1, s2: t2, s3: t3 };
|
||
}
|
||
// Can't be merged with encrypt: arg positions for apply0123 / applySbox are different
|
||
function decrypt(xk, s0, s1, s2, s3) {
|
||
const { sbox2, T01, T23 } = tableDecoding;
|
||
let k = 0;
|
||
(s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]);
|
||
const rounds = xk.length / 4 - 2;
|
||
for (let i = 0; i < rounds; i++) {
|
||
const t0 = xk[k++] ^ apply0123(T01, T23, s0, s3, s2, s1);
|
||
const t1 = xk[k++] ^ apply0123(T01, T23, s1, s0, s3, s2);
|
||
const t2 = xk[k++] ^ apply0123(T01, T23, s2, s1, s0, s3);
|
||
const t3 = xk[k++] ^ apply0123(T01, T23, s3, s2, s1, s0);
|
||
(s0 = t0), (s1 = t1), (s2 = t2), (s3 = t3);
|
||
}
|
||
// Last round
|
||
const t0 = xk[k++] ^ applySbox(sbox2, s0, s3, s2, s1);
|
||
const t1 = xk[k++] ^ applySbox(sbox2, s1, s0, s3, s2);
|
||
const t2 = xk[k++] ^ applySbox(sbox2, s2, s1, s0, s3);
|
||
const t3 = xk[k++] ^ applySbox(sbox2, s3, s2, s1, s0);
|
||
return { s0: t0, s1: t1, s2: t2, s3: t3 };
|
||
}
|
||
// TODO: investigate merging with ctr32
|
||
function ctrCounter(xk, nonce, src, dst) {
|
||
abytes(nonce, BLOCK_SIZE);
|
||
abytes(src);
|
||
const srcLen = src.length;
|
||
dst = getOutput(srcLen, dst);
|
||
complexOverlapBytes(src, dst);
|
||
const ctr = nonce;
|
||
const c32 = u32(ctr);
|
||
// Fill block (empty, ctr=0)
|
||
let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
|
||
const src32 = u32(src);
|
||
const dst32 = u32(dst);
|
||
// process blocks
|
||
for (let i = 0; i + 4 <= src32.length; i += 4) {
|
||
dst32[i + 0] = src32[i + 0] ^ s0;
|
||
dst32[i + 1] = src32[i + 1] ^ s1;
|
||
dst32[i + 2] = src32[i + 2] ^ s2;
|
||
dst32[i + 3] = src32[i + 3] ^ s3;
|
||
// Full 128 bit counter with wrap around
|
||
let carry = 1;
|
||
for (let i = ctr.length - 1; i >= 0; i--) {
|
||
carry = (carry + (ctr[i] & 0xff)) | 0;
|
||
ctr[i] = carry & 0xff;
|
||
carry >>>= 8;
|
||
}
|
||
({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
|
||
}
|
||
// leftovers (less than block)
|
||
// It's possible to handle > u32 fast, but is it worth it?
|
||
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||
if (start < srcLen) {
|
||
const b32 = new Uint32Array([s0, s1, s2, s3]);
|
||
const buf = u8(b32);
|
||
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||
dst[i] = src[i] ^ buf[pos];
|
||
clean(b32);
|
||
}
|
||
return dst;
|
||
}
|
||
// AES CTR with overflowing 32 bit counter
|
||
// It's possible to do 32le significantly simpler (and probably faster) by using u32.
|
||
// But, we need both, and perf bottleneck is in ghash anyway.
|
||
function ctr32(xk, isLE, nonce, src, dst) {
|
||
abytes(nonce, BLOCK_SIZE);
|
||
abytes(src);
|
||
dst = getOutput(src.length, dst);
|
||
const ctr = nonce; // write new value to nonce, so it can be re-used
|
||
const c32 = u32(ctr);
|
||
const view = createView(ctr);
|
||
const src32 = u32(src);
|
||
const dst32 = u32(dst);
|
||
const ctrPos = isLE ? 0 : 12;
|
||
const srcLen = src.length;
|
||
// Fill block (empty, ctr=0)
|
||
let ctrNum = view.getUint32(ctrPos, isLE); // read current counter value
|
||
let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
|
||
// process blocks
|
||
for (let i = 0; i + 4 <= src32.length; i += 4) {
|
||
dst32[i + 0] = src32[i + 0] ^ s0;
|
||
dst32[i + 1] = src32[i + 1] ^ s1;
|
||
dst32[i + 2] = src32[i + 2] ^ s2;
|
||
dst32[i + 3] = src32[i + 3] ^ s3;
|
||
ctrNum = (ctrNum + 1) >>> 0; // u32 wrap
|
||
view.setUint32(ctrPos, ctrNum, isLE);
|
||
({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
|
||
}
|
||
// leftovers (less than a block)
|
||
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||
if (start < srcLen) {
|
||
const b32 = new Uint32Array([s0, s1, s2, s3]);
|
||
const buf = u8(b32);
|
||
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||
dst[i] = src[i] ^ buf[pos];
|
||
clean(b32);
|
||
}
|
||
return dst;
|
||
}
|
||
/**
|
||
* CTR: counter mode. Creates stream cipher.
|
||
* Requires good IV. Parallelizable. OK, but no MAC.
|
||
*/
|
||
export const ctr = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 16 }, function aesctr(key, nonce) {
|
||
function processCtr(buf, dst) {
|
||
abytes(buf);
|
||
if (dst !== undefined) {
|
||
abytes(dst);
|
||
if (!isAligned32(dst))
|
||
throw new Error('unaligned destination');
|
||
}
|
||
const xk = expandKeyLE(key);
|
||
const n = copyBytes(nonce); // align + avoid changing
|
||
const toClean = [xk, n];
|
||
if (!isAligned32(buf))
|
||
toClean.push((buf = copyBytes(buf)));
|
||
const out = ctrCounter(xk, n, buf, dst);
|
||
clean(...toClean);
|
||
return out;
|
||
}
|
||
return {
|
||
encrypt: (plaintext, dst) => processCtr(plaintext, dst),
|
||
decrypt: (ciphertext, dst) => processCtr(ciphertext, dst),
|
||
};
|
||
});
|
||
function validateBlockDecrypt(data) {
|
||
abytes(data);
|
||
if (data.length % BLOCK_SIZE !== 0) {
|
||
throw new Error('aes-(cbc/ecb).decrypt ciphertext should consist of blocks with size ' + BLOCK_SIZE);
|
||
}
|
||
}
|
||
function validateBlockEncrypt(plaintext, pcks5, dst) {
|
||
abytes(plaintext);
|
||
let outLen = plaintext.length;
|
||
const remaining = outLen % BLOCK_SIZE;
|
||
if (!pcks5 && remaining !== 0)
|
||
throw new Error('aec/(cbc-ecb): unpadded plaintext with disabled padding');
|
||
if (!isAligned32(plaintext))
|
||
plaintext = copyBytes(plaintext);
|
||
const b = u32(plaintext);
|
||
if (pcks5) {
|
||
let left = BLOCK_SIZE - remaining;
|
||
if (!left)
|
||
left = BLOCK_SIZE; // if no bytes left, create empty padding block
|
||
outLen = outLen + left;
|
||
}
|
||
dst = getOutput(outLen, dst);
|
||
complexOverlapBytes(plaintext, dst);
|
||
const o = u32(dst);
|
||
return { b, o, out: dst };
|
||
}
|
||
function validatePCKS(data, pcks5) {
|
||
if (!pcks5)
|
||
return data;
|
||
const len = data.length;
|
||
if (!len)
|
||
throw new Error('aes/pcks5: empty ciphertext not allowed');
|
||
const lastByte = data[len - 1];
|
||
if (lastByte <= 0 || lastByte > 16)
|
||
throw new Error('aes/pcks5: wrong padding');
|
||
const out = data.subarray(0, -lastByte);
|
||
for (let i = 0; i < lastByte; i++)
|
||
if (data[len - i - 1] !== lastByte)
|
||
throw new Error('aes/pcks5: wrong padding');
|
||
return out;
|
||
}
|
||
function padPCKS(left) {
|
||
const tmp = new Uint8Array(16);
|
||
const tmp32 = u32(tmp);
|
||
tmp.set(left);
|
||
const paddingByte = BLOCK_SIZE - left.length;
|
||
for (let i = BLOCK_SIZE - paddingByte; i < BLOCK_SIZE; i++)
|
||
tmp[i] = paddingByte;
|
||
return tmp32;
|
||
}
|
||
/**
|
||
* ECB: Electronic CodeBook. Simple deterministic replacement.
|
||
* Dangerous: always map x to y. See [AES Penguin](https://words.filippo.io/the-ecb-penguin/).
|
||
*/
|
||
export const ecb = /* @__PURE__ */ wrapCipher({ blockSize: 16 }, function aesecb(key, opts = {}) {
|
||
const pcks5 = !opts.disablePadding;
|
||
return {
|
||
encrypt(plaintext, dst) {
|
||
const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst);
|
||
const xk = expandKeyLE(key);
|
||
let i = 0;
|
||
for (; i + 4 <= b.length;) {
|
||
const { s0, s1, s2, s3 } = encrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]);
|
||
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||
}
|
||
if (pcks5) {
|
||
const tmp32 = padPCKS(plaintext.subarray(i * 4));
|
||
const { s0, s1, s2, s3 } = encrypt(xk, tmp32[0], tmp32[1], tmp32[2], tmp32[3]);
|
||
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||
}
|
||
clean(xk);
|
||
return _out;
|
||
},
|
||
decrypt(ciphertext, dst) {
|
||
validateBlockDecrypt(ciphertext);
|
||
const xk = expandKeyDecLE(key);
|
||
dst = getOutput(ciphertext.length, dst);
|
||
const toClean = [xk];
|
||
if (!isAligned32(ciphertext))
|
||
toClean.push((ciphertext = copyBytes(ciphertext)));
|
||
complexOverlapBytes(ciphertext, dst);
|
||
const b = u32(ciphertext);
|
||
const o = u32(dst);
|
||
for (let i = 0; i + 4 <= b.length;) {
|
||
const { s0, s1, s2, s3 } = decrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]);
|
||
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||
}
|
||
clean(...toClean);
|
||
return validatePCKS(dst, pcks5);
|
||
},
|
||
};
|
||
});
|
||
/**
|
||
* CBC: Cipher-Block-Chaining. Key is previous round’s block.
|
||
* Fragile: needs proper padding. Unauthenticated: needs MAC.
|
||
*/
|
||
export const cbc = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 16 }, function aescbc(key, iv, opts = {}) {
|
||
const pcks5 = !opts.disablePadding;
|
||
return {
|
||
encrypt(plaintext, dst) {
|
||
const xk = expandKeyLE(key);
|
||
const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst);
|
||
let _iv = iv;
|
||
const toClean = [xk];
|
||
if (!isAligned32(_iv))
|
||
toClean.push((_iv = copyBytes(_iv)));
|
||
const n32 = u32(_iv);
|
||
// prettier-ignore
|
||
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||
let i = 0;
|
||
for (; i + 4 <= b.length;) {
|
||
(s0 ^= b[i + 0]), (s1 ^= b[i + 1]), (s2 ^= b[i + 2]), (s3 ^= b[i + 3]);
|
||
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||
}
|
||
if (pcks5) {
|
||
const tmp32 = padPCKS(plaintext.subarray(i * 4));
|
||
(s0 ^= tmp32[0]), (s1 ^= tmp32[1]), (s2 ^= tmp32[2]), (s3 ^= tmp32[3]);
|
||
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||
}
|
||
clean(...toClean);
|
||
return _out;
|
||
},
|
||
decrypt(ciphertext, dst) {
|
||
validateBlockDecrypt(ciphertext);
|
||
const xk = expandKeyDecLE(key);
|
||
let _iv = iv;
|
||
const toClean = [xk];
|
||
if (!isAligned32(_iv))
|
||
toClean.push((_iv = copyBytes(_iv)));
|
||
const n32 = u32(_iv);
|
||
dst = getOutput(ciphertext.length, dst);
|
||
if (!isAligned32(ciphertext))
|
||
toClean.push((ciphertext = copyBytes(ciphertext)));
|
||
complexOverlapBytes(ciphertext, dst);
|
||
const b = u32(ciphertext);
|
||
const o = u32(dst);
|
||
// prettier-ignore
|
||
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||
for (let i = 0; i + 4 <= b.length;) {
|
||
// prettier-ignore
|
||
const ps0 = s0, ps1 = s1, ps2 = s2, ps3 = s3;
|
||
(s0 = b[i + 0]), (s1 = b[i + 1]), (s2 = b[i + 2]), (s3 = b[i + 3]);
|
||
const { s0: o0, s1: o1, s2: o2, s3: o3 } = decrypt(xk, s0, s1, s2, s3);
|
||
(o[i++] = o0 ^ ps0), (o[i++] = o1 ^ ps1), (o[i++] = o2 ^ ps2), (o[i++] = o3 ^ ps3);
|
||
}
|
||
clean(...toClean);
|
||
return validatePCKS(dst, pcks5);
|
||
},
|
||
};
|
||
});
|
||
/**
|
||
* CFB: Cipher Feedback Mode. The input for the block cipher is the previous cipher output.
|
||
* Unauthenticated: needs MAC.
|
||
*/
|
||
export const cfb = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 16 }, function aescfb(key, iv) {
|
||
function processCfb(src, isEncrypt, dst) {
|
||
abytes(src);
|
||
const srcLen = src.length;
|
||
dst = getOutput(srcLen, dst);
|
||
if (overlapBytes(src, dst))
|
||
throw new Error('overlapping src and dst not supported.');
|
||
const xk = expandKeyLE(key);
|
||
let _iv = iv;
|
||
const toClean = [xk];
|
||
if (!isAligned32(_iv))
|
||
toClean.push((_iv = copyBytes(_iv)));
|
||
if (!isAligned32(src))
|
||
toClean.push((src = copyBytes(src)));
|
||
const src32 = u32(src);
|
||
const dst32 = u32(dst);
|
||
const next32 = isEncrypt ? dst32 : src32;
|
||
const n32 = u32(_iv);
|
||
// prettier-ignore
|
||
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||
for (let i = 0; i + 4 <= src32.length;) {
|
||
const { s0: e0, s1: e1, s2: e2, s3: e3 } = encrypt(xk, s0, s1, s2, s3);
|
||
dst32[i + 0] = src32[i + 0] ^ e0;
|
||
dst32[i + 1] = src32[i + 1] ^ e1;
|
||
dst32[i + 2] = src32[i + 2] ^ e2;
|
||
dst32[i + 3] = src32[i + 3] ^ e3;
|
||
(s0 = next32[i++]), (s1 = next32[i++]), (s2 = next32[i++]), (s3 = next32[i++]);
|
||
}
|
||
// leftovers (less than block)
|
||
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||
if (start < srcLen) {
|
||
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||
const buf = u8(new Uint32Array([s0, s1, s2, s3]));
|
||
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||
dst[i] = src[i] ^ buf[pos];
|
||
clean(buf);
|
||
}
|
||
clean(...toClean);
|
||
return dst;
|
||
}
|
||
return {
|
||
encrypt: (plaintext, dst) => processCfb(plaintext, true, dst),
|
||
decrypt: (ciphertext, dst) => processCfb(ciphertext, false, dst),
|
||
};
|
||
});
|
||
// TODO: merge with chacha, however gcm has bitLen while chacha has byteLen
|
||
function computeTag(fn, isLE, key, data, AAD) {
|
||
const aadLength = AAD == null ? 0 : AAD.length;
|
||
const h = fn.create(key, data.length + aadLength);
|
||
if (AAD)
|
||
h.update(AAD);
|
||
h.update(data);
|
||
const num = new Uint8Array(16);
|
||
const view = createView(num);
|
||
if (AAD)
|
||
setBigUint64(view, 0, BigInt(aadLength * 8), isLE);
|
||
setBigUint64(view, 8, BigInt(data.length * 8), isLE);
|
||
h.update(num);
|
||
const res = h.digest();
|
||
clean(num);
|
||
return res;
|
||
}
|
||
/**
|
||
* GCM: Galois/Counter Mode.
|
||
* Modern, parallel version of CTR, with MAC.
|
||
* Be careful: MACs can be forged.
|
||
* Unsafe to use random nonces under the same key, due to collision chance.
|
||
* As for nonce size, prefer 12-byte, instead of 8-byte.
|
||
*/
|
||
export const gcm = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16, varSizeNonce: true }, function aesgcm(key, nonce, AAD) {
|
||
// NIST 800-38d doesn't enforce minimum nonce length.
|
||
// We enforce 8 bytes for compat with openssl.
|
||
// 12 bytes are recommended. More than 12 bytes would be converted into 12.
|
||
if (nonce.length < 8)
|
||
throw new Error('aes/gcm: invalid nonce length');
|
||
const tagLength = 16;
|
||
function _computeTag(authKey, tagMask, data) {
|
||
const tag = computeTag(ghash, false, authKey, data, AAD);
|
||
for (let i = 0; i < tagMask.length; i++)
|
||
tag[i] ^= tagMask[i];
|
||
return tag;
|
||
}
|
||
function deriveKeys() {
|
||
const xk = expandKeyLE(key);
|
||
const authKey = EMPTY_BLOCK.slice();
|
||
const counter = EMPTY_BLOCK.slice();
|
||
ctr32(xk, false, counter, counter, authKey);
|
||
// NIST 800-38d, page 15: different behavior for 96-bit and non-96-bit nonces
|
||
if (nonce.length === 12) {
|
||
counter.set(nonce);
|
||
}
|
||
else {
|
||
const nonceLen = EMPTY_BLOCK.slice();
|
||
const view = createView(nonceLen);
|
||
setBigUint64(view, 8, BigInt(nonce.length * 8), false);
|
||
// ghash(nonce || u64be(0) || u64be(nonceLen*8))
|
||
const g = ghash.create(authKey).update(nonce).update(nonceLen);
|
||
g.digestInto(counter); // digestInto doesn't trigger '.destroy'
|
||
g.destroy();
|
||
}
|
||
const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK);
|
||
return { xk, authKey, counter, tagMask };
|
||
}
|
||
return {
|
||
encrypt(plaintext) {
|
||
const { xk, authKey, counter, tagMask } = deriveKeys();
|
||
const out = new Uint8Array(plaintext.length + tagLength);
|
||
const toClean = [xk, authKey, counter, tagMask];
|
||
if (!isAligned32(plaintext))
|
||
toClean.push((plaintext = copyBytes(plaintext)));
|
||
ctr32(xk, false, counter, plaintext, out.subarray(0, plaintext.length));
|
||
const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength));
|
||
toClean.push(tag);
|
||
out.set(tag, plaintext.length);
|
||
clean(...toClean);
|
||
return out;
|
||
},
|
||
decrypt(ciphertext) {
|
||
const { xk, authKey, counter, tagMask } = deriveKeys();
|
||
const toClean = [xk, authKey, tagMask, counter];
|
||
if (!isAligned32(ciphertext))
|
||
toClean.push((ciphertext = copyBytes(ciphertext)));
|
||
const data = ciphertext.subarray(0, -tagLength);
|
||
const passedTag = ciphertext.subarray(-tagLength);
|
||
const tag = _computeTag(authKey, tagMask, data);
|
||
toClean.push(tag);
|
||
if (!equalBytes(tag, passedTag))
|
||
throw new Error('aes/gcm: invalid ghash tag');
|
||
const out = ctr32(xk, false, counter, data);
|
||
clean(...toClean);
|
||
return out;
|
||
},
|
||
};
|
||
});
|
||
const limit = (name, min, max) => (value) => {
|
||
if (!Number.isSafeInteger(value) || min > value || value > max) {
|
||
const minmax = '[' + min + '..' + max + ']';
|
||
throw new Error('' + name + ': expected value in range ' + minmax + ', got ' + value);
|
||
}
|
||
};
|
||
/**
|
||
* AES-GCM-SIV: classic AES-GCM with nonce-misuse resistance.
|
||
* Guarantees that, when a nonce is repeated, the only security loss is that identical
|
||
* plaintexts will produce identical ciphertexts.
|
||
* RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452
|
||
*/
|
||
export const siv = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16, varSizeNonce: true }, function aessiv(key, nonce, AAD) {
|
||
const tagLength = 16;
|
||
// From RFC 8452: Section 6
|
||
const AAD_LIMIT = limit('AAD', 0, 2 ** 36);
|
||
const PLAIN_LIMIT = limit('plaintext', 0, 2 ** 36);
|
||
const NONCE_LIMIT = limit('nonce', 12, 12);
|
||
const CIPHER_LIMIT = limit('ciphertext', 16, 2 ** 36 + 16);
|
||
abytes(key, 16, 24, 32);
|
||
NONCE_LIMIT(nonce.length);
|
||
if (AAD !== undefined)
|
||
AAD_LIMIT(AAD.length);
|
||
function deriveKeys() {
|
||
const xk = expandKeyLE(key);
|
||
const encKey = new Uint8Array(key.length);
|
||
const authKey = new Uint8Array(16);
|
||
const toClean = [xk, encKey];
|
||
let _nonce = nonce;
|
||
if (!isAligned32(_nonce))
|
||
toClean.push((_nonce = copyBytes(_nonce)));
|
||
const n32 = u32(_nonce);
|
||
// prettier-ignore
|
||
let s0 = 0, s1 = n32[0], s2 = n32[1], s3 = n32[2];
|
||
let counter = 0;
|
||
for (const derivedKey of [authKey, encKey].map(u32)) {
|
||
const d32 = u32(derivedKey);
|
||
for (let i = 0; i < d32.length; i += 2) {
|
||
// aes(u32le(0) || nonce)[:8] || aes(u32le(1) || nonce)[:8] ...
|
||
const { s0: o0, s1: o1 } = encrypt(xk, s0, s1, s2, s3);
|
||
d32[i + 0] = o0;
|
||
d32[i + 1] = o1;
|
||
s0 = ++counter; // increment counter inside state
|
||
}
|
||
}
|
||
const res = { authKey, encKey: expandKeyLE(encKey) };
|
||
// Cleanup
|
||
clean(...toClean);
|
||
return res;
|
||
}
|
||
function _computeTag(encKey, authKey, data) {
|
||
const tag = computeTag(polyval, true, authKey, data, AAD);
|
||
// Compute the expected tag by XORing S_s and the nonce, clearing the
|
||
// most significant bit of the last byte and encrypting with the
|
||
// message-encryption key.
|
||
for (let i = 0; i < 12; i++)
|
||
tag[i] ^= nonce[i];
|
||
tag[15] &= 0x7f; // Clear the highest bit
|
||
// encrypt tag as block
|
||
const t32 = u32(tag);
|
||
// prettier-ignore
|
||
let s0 = t32[0], s1 = t32[1], s2 = t32[2], s3 = t32[3];
|
||
({ s0, s1, s2, s3 } = encrypt(encKey, s0, s1, s2, s3));
|
||
(t32[0] = s0), (t32[1] = s1), (t32[2] = s2), (t32[3] = s3);
|
||
return tag;
|
||
}
|
||
// actual decrypt/encrypt of message.
|
||
function processSiv(encKey, tag, input) {
|
||
let block = copyBytes(tag);
|
||
block[15] |= 0x80; // Force highest bit
|
||
const res = ctr32(encKey, true, block, input);
|
||
// Cleanup
|
||
clean(block);
|
||
return res;
|
||
}
|
||
return {
|
||
encrypt(plaintext) {
|
||
PLAIN_LIMIT(plaintext.length);
|
||
const { encKey, authKey } = deriveKeys();
|
||
const tag = _computeTag(encKey, authKey, plaintext);
|
||
const toClean = [encKey, authKey, tag];
|
||
if (!isAligned32(plaintext))
|
||
toClean.push((plaintext = copyBytes(plaintext)));
|
||
const out = new Uint8Array(plaintext.length + tagLength);
|
||
out.set(tag, plaintext.length);
|
||
out.set(processSiv(encKey, tag, plaintext));
|
||
// Cleanup
|
||
clean(...toClean);
|
||
return out;
|
||
},
|
||
decrypt(ciphertext) {
|
||
CIPHER_LIMIT(ciphertext.length);
|
||
const tag = ciphertext.subarray(-tagLength);
|
||
const { encKey, authKey } = deriveKeys();
|
||
const toClean = [encKey, authKey];
|
||
if (!isAligned32(ciphertext))
|
||
toClean.push((ciphertext = copyBytes(ciphertext)));
|
||
const plaintext = processSiv(encKey, tag, ciphertext.subarray(0, -tagLength));
|
||
const expectedTag = _computeTag(encKey, authKey, plaintext);
|
||
toClean.push(expectedTag);
|
||
if (!equalBytes(tag, expectedTag)) {
|
||
clean(...toClean);
|
||
throw new Error('invalid polyval tag');
|
||
}
|
||
// Cleanup
|
||
clean(...toClean);
|
||
return plaintext;
|
||
},
|
||
};
|
||
});
|
||
function isBytes32(a) {
|
||
return (a instanceof Uint32Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint32Array'));
|
||
}
|
||
function encryptBlock(xk, block) {
|
||
abytes(block, 16);
|
||
if (!isBytes32(xk))
|
||
throw new Error('_encryptBlock accepts result of expandKeyLE');
|
||
const b32 = u32(block);
|
||
let { s0, s1, s2, s3 } = encrypt(xk, b32[0], b32[1], b32[2], b32[3]);
|
||
(b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3);
|
||
return block;
|
||
}
|
||
function decryptBlock(xk, block) {
|
||
abytes(block, 16);
|
||
if (!isBytes32(xk))
|
||
throw new Error('_decryptBlock accepts result of expandKeyLE');
|
||
const b32 = u32(block);
|
||
let { s0, s1, s2, s3 } = decrypt(xk, b32[0], b32[1], b32[2], b32[3]);
|
||
(b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3);
|
||
return block;
|
||
}
|
||
/**
|
||
* AES-W (base for AESKW/AESKWP).
|
||
* Specs: [SP800-38F](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf),
|
||
* [RFC 3394](https://datatracker.ietf.org/doc/rfc3394/),
|
||
* [RFC 5649](https://datatracker.ietf.org/doc/rfc5649/).
|
||
*/
|
||
const AESW = {
|
||
/*
|
||
High-level pseudocode:
|
||
```
|
||
A: u64 = IV
|
||
out = []
|
||
for (let i=0, ctr = 0; i<6; i++) {
|
||
for (const chunk of chunks(plaintext, 8)) {
|
||
A ^= swapEndianess(ctr++)
|
||
[A, res] = chunks(encrypt(A || chunk), 8);
|
||
out ||= res
|
||
}
|
||
}
|
||
out = A || out
|
||
```
|
||
Decrypt is the same, but reversed.
|
||
*/
|
||
encrypt(kek, out) {
|
||
// Size is limited to 4GB, otherwise ctr will overflow and we'll need to switch to bigints.
|
||
// If you need it larger, open an issue.
|
||
if (out.length >= 2 ** 32)
|
||
throw new Error('plaintext should be less than 4gb');
|
||
const xk = expandKeyLE(kek);
|
||
if (out.length === 16)
|
||
encryptBlock(xk, out);
|
||
else {
|
||
const o32 = u32(out);
|
||
// prettier-ignore
|
||
let a0 = o32[0], a1 = o32[1]; // A
|
||
for (let j = 0, ctr = 1; j < 6; j++) {
|
||
for (let pos = 2; pos < o32.length; pos += 2, ctr++) {
|
||
const { s0, s1, s2, s3 } = encrypt(xk, a0, a1, o32[pos], o32[pos + 1]);
|
||
// A = MSB(64, B) ^ t where t = (n*j)+i
|
||
(a0 = s0), (a1 = s1 ^ byteSwap(ctr)), (o32[pos] = s2), (o32[pos + 1] = s3);
|
||
}
|
||
}
|
||
(o32[0] = a0), (o32[1] = a1); // out = A || out
|
||
}
|
||
xk.fill(0);
|
||
},
|
||
decrypt(kek, out) {
|
||
if (out.length - 8 >= 2 ** 32)
|
||
throw new Error('ciphertext should be less than 4gb');
|
||
const xk = expandKeyDecLE(kek);
|
||
const chunks = out.length / 8 - 1; // first chunk is IV
|
||
if (chunks === 1)
|
||
decryptBlock(xk, out);
|
||
else {
|
||
const o32 = u32(out);
|
||
// prettier-ignore
|
||
let a0 = o32[0], a1 = o32[1]; // A
|
||
for (let j = 0, ctr = chunks * 6; j < 6; j++) {
|
||
for (let pos = chunks * 2; pos >= 1; pos -= 2, ctr--) {
|
||
a1 ^= byteSwap(ctr);
|
||
const { s0, s1, s2, s3 } = decrypt(xk, a0, a1, o32[pos], o32[pos + 1]);
|
||
(a0 = s0), (a1 = s1), (o32[pos] = s2), (o32[pos + 1] = s3);
|
||
}
|
||
}
|
||
(o32[0] = a0), (o32[1] = a1);
|
||
}
|
||
xk.fill(0);
|
||
},
|
||
};
|
||
const AESKW_IV = /* @__PURE__ */ new Uint8Array(8).fill(0xa6); // A6A6A6A6A6A6A6A6
|
||
/**
|
||
* AES-KW (key-wrap). Injects static IV into plaintext, adds counter, encrypts 6 times.
|
||
* Reduces block size from 16 to 8 bytes.
|
||
* For padded version, use aeskwp.
|
||
* [RFC 3394](https://datatracker.ietf.org/doc/rfc3394/),
|
||
* [NIST.SP.800-38F](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf).
|
||
*/
|
||
export const aeskw = /* @__PURE__ */ wrapCipher({ blockSize: 8 }, (kek) => ({
|
||
encrypt(plaintext) {
|
||
if (!plaintext.length || plaintext.length % 8 !== 0)
|
||
throw new Error('invalid plaintext length');
|
||
if (plaintext.length === 8)
|
||
throw new Error('8-byte keys not allowed in AESKW, use AESKWP instead');
|
||
const out = concatBytes(AESKW_IV, plaintext);
|
||
AESW.encrypt(kek, out);
|
||
return out;
|
||
},
|
||
decrypt(ciphertext) {
|
||
// ciphertext must be at least 24 bytes and a multiple of 8 bytes
|
||
// 24 because should have at least two block (1 iv + 2).
|
||
// Replace with 16 to enable '8-byte keys'
|
||
if (ciphertext.length % 8 !== 0 || ciphertext.length < 3 * 8)
|
||
throw new Error('invalid ciphertext length');
|
||
const out = copyBytes(ciphertext);
|
||
AESW.decrypt(kek, out);
|
||
if (!equalBytes(out.subarray(0, 8), AESKW_IV))
|
||
throw new Error('integrity check failed');
|
||
out.subarray(0, 8).fill(0); // ciphertext.subarray(0, 8) === IV, but we clean it anyway
|
||
return out.subarray(8);
|
||
},
|
||
}));
|
||
/*
|
||
We don't support 8-byte keys. The rabbit hole:
|
||
|
||
- Wycheproof says: "NIST SP 800-38F does not define the wrapping of 8 byte keys.
|
||
RFC 3394 Section 2 on the other hand specifies that 8 byte keys are wrapped
|
||
by directly encrypting one block with AES."
|
||
- https://github.com/C2SP/wycheproof/blob/master/doc/key_wrap.md
|
||
- "RFC 3394 specifies in Section 2, that the input for the key wrap
|
||
algorithm must be at least two blocks and otherwise the constant
|
||
field and key are simply encrypted with ECB as a single block"
|
||
- What RFC 3394 actually says (in Section 2):
|
||
- "Before being wrapped, the key data is parsed into n blocks of 64 bits.
|
||
The only restriction the key wrap algorithm places on n is that n be
|
||
at least two"
|
||
- "For key data with length less than or equal to 64 bits, the constant
|
||
field used in this specification and the key data form a single
|
||
128-bit codebook input making this key wrap unnecessary."
|
||
- Which means "assert(n >= 2)" and "use something else for 8 byte keys"
|
||
- NIST SP800-38F actually prohibits 8-byte in "5.3.1 Mandatory Limits".
|
||
It states that plaintext for KW should be "2 to 2^54 -1 semiblocks".
|
||
- So, where does "directly encrypt single block with AES" come from?
|
||
- Not RFC 3394. Pseudocode of key wrap in 2.2 explicitly uses
|
||
loop of 6 for any code path
|
||
- There is a weird W3C spec:
|
||
https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html#kw-aes128
|
||
- This spec is outdated, as admitted by Wycheproof authors
|
||
- There is RFC 5649 for padded key wrap, which is padding construction on
|
||
top of AESKW. In '4.1.2' it says: "If the padded plaintext contains exactly
|
||
eight octets, then prepend the AIV as defined in Section 3 above to P[1] and
|
||
encrypt the resulting 128-bit block using AES in ECB mode [Modes] with key
|
||
K (the KEK). In this case, the output is two 64-bit blocks C[0] and C[1]:"
|
||
- Browser subtle crypto is actually crashes on wrapping keys less than 16 bytes:
|
||
`Error: error:1C8000E6:Provider routines::invalid input length] { opensslErrorStack: [ 'error:030000BD:digital envelope routines::update error' ]`
|
||
|
||
In the end, seems like a bug in Wycheproof.
|
||
The 8-byte check can be easily disabled inside of AES_W.
|
||
*/
|
||
const AESKWP_IV = 0xa65959a6; // single u32le value
|
||
/**
|
||
* AES-KW, but with padding and allows random keys.
|
||
* Second u32 of IV is used as counter for length.
|
||
* [RFC 5649](https://www.rfc-editor.org/rfc/rfc5649)
|
||
*/
|
||
export const aeskwp = /* @__PURE__ */ wrapCipher({ blockSize: 8 }, (kek) => ({
|
||
encrypt(plaintext) {
|
||
if (!plaintext.length)
|
||
throw new Error('invalid plaintext length');
|
||
const padded = Math.ceil(plaintext.length / 8) * 8;
|
||
const out = new Uint8Array(8 + padded);
|
||
out.set(plaintext, 8);
|
||
const out32 = u32(out);
|
||
out32[0] = AESKWP_IV;
|
||
out32[1] = byteSwap(plaintext.length);
|
||
AESW.encrypt(kek, out);
|
||
return out;
|
||
},
|
||
decrypt(ciphertext) {
|
||
// 16 because should have at least one block
|
||
if (ciphertext.length < 16)
|
||
throw new Error('invalid ciphertext length');
|
||
const out = copyBytes(ciphertext);
|
||
const o32 = u32(out);
|
||
AESW.decrypt(kek, out);
|
||
const len = byteSwap(o32[1]) >>> 0;
|
||
const padded = Math.ceil(len / 8) * 8;
|
||
if (o32[0] !== AESKWP_IV || out.length - 8 !== padded)
|
||
throw new Error('integrity check failed');
|
||
for (let i = len; i < padded; i++)
|
||
if (out[8 + i] !== 0)
|
||
throw new Error('integrity check failed');
|
||
out.subarray(0, 8).fill(0); // ciphertext.subarray(0, 8) === IV, but we clean it anyway
|
||
return out.subarray(8, 8 + len);
|
||
},
|
||
}));
|
||
/** Unsafe low-level internal methods. May change at any time. */
|
||
export const unsafe = {
|
||
expandKeyLE,
|
||
expandKeyDecLE,
|
||
encrypt,
|
||
decrypt,
|
||
encryptBlock,
|
||
decryptBlock,
|
||
ctrCounter,
|
||
ctr32,
|
||
};
|
||
//# sourceMappingURL=aes.js.map
|