@noble/hashes
Advanced tools
+13
-13
| import { type KDFInput, type TArg, type TRet } from './utils.ts'; | ||
| /** Argon2 cost, output, and optional secret/personalization inputs. */ | ||
| export type ArgonOpts = { | ||
| /** Time cost measured in iterations. */ | ||
| t: number; | ||
| /** Memory cost in kibibytes. */ | ||
| m: number; | ||
| /** Parallelization parameter. */ | ||
| p: number; | ||
| /** Time cost measured in iterations. Defaults to `3`. */ | ||
| t?: number; | ||
| /** Memory cost in kibibytes. Defaults to `1024 ** 2` (1 GiB). */ | ||
| m?: number; | ||
| /** Parallelization parameter. Defaults to `1`. */ | ||
| p?: number; | ||
| /** Argon2 version number. Defaults to `0x13`. */ | ||
@@ -20,3 +20,3 @@ version?: number; | ||
| asyncTick?: number; | ||
| /** Maximum temporary memory budget in bytes. */ | ||
| /** Maximum temporary memory budget in bytes. Defaults to 1 GiB. */ | ||
| maxmem?: number; | ||
@@ -61,3 +61,3 @@ /** | ||
| */ | ||
| export declare const argon2d: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts: TArg<ArgonOpts>) => TRet<Uint8Array>; | ||
| export declare const argon2d: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts?: TArg<ArgonOpts>) => TRet<Uint8Array>; | ||
| /** | ||
@@ -76,3 +76,3 @@ * Argon2i side-channel-resistant version. | ||
| */ | ||
| export declare const argon2i: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts: TArg<ArgonOpts>) => TRet<Uint8Array>; | ||
| export declare const argon2i: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts?: TArg<ArgonOpts>) => TRet<Uint8Array>; | ||
| /** | ||
@@ -91,3 +91,3 @@ * Argon2id, combining i+d, the most popular version from RFC 9106. | ||
| */ | ||
| export declare const argon2id: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts: TArg<ArgonOpts>) => TRet<Uint8Array>; | ||
| export declare const argon2id: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts?: TArg<ArgonOpts>) => TRet<Uint8Array>; | ||
| /** | ||
@@ -124,3 +124,3 @@ * Argon2d async GPU-resistant version. | ||
| */ | ||
| export declare const argon2dAsync: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts: TArg<ArgonOpts>) => Promise<TRet<Uint8Array>>; | ||
| export declare const argon2dAsync: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts?: TArg<ArgonOpts>) => Promise<TRet<Uint8Array>>; | ||
| /** | ||
@@ -139,3 +139,3 @@ * Argon2i async side-channel-resistant version. | ||
| */ | ||
| export declare const argon2iAsync: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts: TArg<ArgonOpts>) => Promise<TRet<Uint8Array>>; | ||
| export declare const argon2iAsync: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts?: TArg<ArgonOpts>) => Promise<TRet<Uint8Array>>; | ||
| /** | ||
@@ -154,2 +154,2 @@ * Argon2id async, combining i+d, the most popular version from RFC 9106. | ||
| */ | ||
| export declare const argon2idAsync: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts: TArg<ArgonOpts>) => Promise<TRet<Uint8Array>>; | ||
| export declare const argon2idAsync: (password: TArg<KDFInput>, salt: TArg<KDFInput>, opts?: TArg<ArgonOpts>) => Promise<TRet<Uint8Array>>; |
+164
-137
@@ -11,3 +11,2 @@ /** | ||
| */ | ||
| import { rotr32H, rotr32L, rotrBH, rotrBL, rotrSH, rotrSL } from "./_u64.js"; | ||
| import { blake2b } from "./blake2.js"; | ||
@@ -27,27 +26,2 @@ import { anumber, checkOpts, clean, kdfInputToBytes, nextTick, swap32IfBE, swap8IfBE, u32, u8, } from "./utils.js"; | ||
| }; | ||
| // Unsigned `u32 * u32 = { h, l }`, returned as split 64-bit halves. | ||
| function mul(a, b) { | ||
| // Split into 16-bit limbs so each partial product stays exact under `Math.imul`. | ||
| const aL = a & 0xffff; | ||
| const aH = a >>> 16; | ||
| const bL = b & 0xffff; | ||
| const bH = b >>> 16; | ||
| const ll = Math.imul(aL, bL); | ||
| const hl = Math.imul(aH, bL); | ||
| const lh = Math.imul(aL, bH); | ||
| const hh = Math.imul(aH, bH); | ||
| const carry = (ll >>> 16) + (hl & 0xffff) + lh; | ||
| const high = (hh + (hl >>> 16) + (carry >>> 16)) | 0; | ||
| const low = (carry << 16) | (ll & 0xffff); | ||
| return { h: high, l: low }; | ||
| } | ||
| // High 32 bits of unsigned u32 multiply, via the same 16-bit limb split as `mul` below. | ||
| // Kept single-purpose and number-returning so V8 inlines it (object-returning | ||
| // helpers here cost 2.2x of the whole derivation, measured; small helpers | ||
| // returning one number are free — see rotr* usage everywhere). | ||
| function mulHi(a, b) { | ||
| const aL = a & 0xffff, aH = a >>> 16, bL = b & 0xffff, bH = b >>> 16; // prettier-ignore | ||
| const carry = (Math.imul(aL, bL) >>> 16) + (Math.imul(aH, bL) & 0xffff) + Math.imul(aL, bH); | ||
| return (Math.imul(aH, bH) + (Math.imul(aH, bL) >>> 16) + (carry >>> 16)) | 0; | ||
| } | ||
| // Temporary block buffer. | ||
@@ -58,5 +32,7 @@ // 1024-byte block: 256 u32 = 128 interleaved low/high halves = RFC's | ||
| // Quarter-round over 64-bit word indices into `A2_BUF`; each index maps to adjacent low/high u32s. | ||
| // Each BlaMka step `X = X + Y + 2 * trunc(X) * trunc(Y)` (trunc = low 32 bits) is three lines: | ||
| // `Math.imul` is the low product half, `mulHi` the high half, then a split 64-bit add with the | ||
| // doubling folded in. RFC 9106 Figure 19 GB rotates by 32, 24, 16, and 63 bits after each XOR. | ||
| // Each BlaMka step `X = X + Y + 2 * trunc(X) * trunc(Y)` (trunc = low 32 bits) starts | ||
| // with an exact low product from `Math.imul`. The rounded double product is within 1024 of the | ||
| // exact u64 product; subtracting that exact low half and rounding to the nearest multiple of | ||
| // 2^32 therefore recovers the exact high half. RFC 9106 Figure 19 GB then rotates by 32, 24, | ||
| // 16, and 63 bits after each XOR. | ||
| function G(a, b, c, d) { | ||
@@ -70,3 +46,3 @@ let Al = A2_BUF[2 * a], Ah = A2_BUF[2 * a + 1]; // prettier-ignore | ||
| ml = Math.imul(Al, Bl); | ||
| mh = mulHi(Al, Bl); // prettier-ignore | ||
| mh = (((Al >>> 0) * (Bl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Al >>> 0) + (Bl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -77,7 +53,7 @@ Ah = (Ah + Bh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Dl ^ Al; // prettier-ignore | ||
| Dh = rotr32H(xh, xl); | ||
| Dl = rotr32L(xh, xl); // prettier-ignore | ||
| Dh = xl; | ||
| Dl = xh; // prettier-ignore | ||
| // C = blamka(C, D); B = rotr64(B ^ C, 24) | ||
| ml = Math.imul(Cl, Dl); | ||
| mh = mulHi(Cl, Dl); // prettier-ignore | ||
| mh = (((Cl >>> 0) * (Dl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Cl >>> 0) + (Dl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -88,7 +64,7 @@ Ch = (Ch + Dh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Bl ^ Cl; // prettier-ignore | ||
| Bh = rotrSH(xh, xl, 24); | ||
| Bl = rotrSL(xh, xl, 24); // prettier-ignore | ||
| Bh = (xh >>> 24) | (xl << 8); | ||
| Bl = (xh << 8) | (xl >>> 24); // prettier-ignore | ||
| // A = blamka(A, B); D = rotr64(D ^ A, 16) | ||
| ml = Math.imul(Al, Bl); | ||
| mh = mulHi(Al, Bl); // prettier-ignore | ||
| mh = (((Al >>> 0) * (Bl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Al >>> 0) + (Bl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -99,7 +75,7 @@ Ah = (Ah + Bh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Dl ^ Al; // prettier-ignore | ||
| Dh = rotrSH(xh, xl, 16); | ||
| Dl = rotrSL(xh, xl, 16); // prettier-ignore | ||
| Dh = (xh >>> 16) | (xl << 16); | ||
| Dl = (xh << 16) | (xl >>> 16); // prettier-ignore | ||
| // C = blamka(C, D); B = rotr64(B ^ C, 63) | ||
| ml = Math.imul(Cl, Dl); | ||
| mh = mulHi(Cl, Dl); // prettier-ignore | ||
| mh = (((Cl >>> 0) * (Dl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Cl >>> 0) + (Dl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -110,4 +86,4 @@ Ch = (Ch + Dh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Bl ^ Cl; // prettier-ignore | ||
| Bh = rotrBH(xh, xl, 63); | ||
| Bl = rotrBL(xh, xl, 63); // prettier-ignore | ||
| Bh = (xh << 1) | (xl >>> 31); | ||
| Bl = (xh >>> 31) | (xl << 1); // prettier-ignore | ||
| ((A2_BUF[2 * a] = Al), (A2_BUF[2 * a + 1] = Ah)); | ||
@@ -134,4 +110,18 @@ ((A2_BUF[2 * b] = Bl), (A2_BUF[2 * b + 1] = Bh)); | ||
| function block(x, xPos, yPos, outPos, needXor) { | ||
| for (let i = 0; i < 256; i++) | ||
| A2_BUF[i] = x[xPos + i] ^ x[yPos + i]; | ||
| // Stage R = X xor Y in the destination before permuting it. This avoids rereading both source | ||
| // blocks when folding R into the permuted scratch block below. | ||
| if (needXor) { | ||
| for (let i = 0; i < 256; i++) { | ||
| const r = x[xPos + i] ^ x[yPos + i]; | ||
| A2_BUF[i] = r; | ||
| x[outPos + i] ^= r; | ||
| } | ||
| } | ||
| else { | ||
| for (let i = 0; i < 256; i++) { | ||
| const r = x[xPos + i] ^ x[yPos + i]; | ||
| A2_BUF[i] = r; | ||
| x[outPos + i] = r; | ||
| } | ||
| } | ||
| // rows (8 consecutive 16-register groups) | ||
@@ -148,8 +138,4 @@ for (let i = 0; i < 128; i += 16) { | ||
| // RFC 9106 step 6: passes after the first XOR the old destination block into the new G(X, Y). | ||
| if (needXor) | ||
| for (let i = 0; i < 256; i++) | ||
| x[outPos + i] ^= A2_BUF[i] ^ x[xPos + i] ^ x[yPos + i]; | ||
| else | ||
| for (let i = 0; i < 256; i++) | ||
| x[outPos + i] = A2_BUF[i] ^ x[xPos + i] ^ x[yPos + i]; | ||
| for (let i = 0; i < 256; i++) | ||
| x[outPos + i] ^= A2_BUF[i]; | ||
| clean(A2_BUF); | ||
@@ -205,5 +191,8 @@ } | ||
| const startPos = r !== 0 && s !== ARGON2_SYNC_POINTS - 1 ? (s + 1) * segmentLen : 0; | ||
| // RFC 9106 Figure 13: `mul(randL, randL).h` is `floor(J_1^2 / 2^32)`, and the outer high-half | ||
| // multiply computes `floor(|W| * x / 2^32)` without floating-point math. | ||
| const rel = area - 1 - mul(area, mul(randL, randL).h).h; | ||
| // Use the same exact-high recovery as G. `areaHigh` is floor(|W| * J1^2/2^32 / 2^32). | ||
| const randLow = Math.imul(randL, randL); | ||
| const randHigh = (((randL >>> 0) * (randL >>> 0) - (randLow >>> 0)) / 0x100000000 + 0.5) | 0; | ||
| const areaLow = Math.imul(area, randHigh); | ||
| const areaHigh = (((area >>> 0) * (randHigh >>> 0) - (areaLow >>> 0)) / 0x100000000 + 0.5) | 0; | ||
| const rel = area - 1 - areaHigh; | ||
| return (startPos + rel) % laneLen; | ||
@@ -213,2 +202,4 @@ } | ||
| const maxUint32 = Math.pow(2, 32); | ||
| const ARGON2_DEFAULT_MEMORY = 1024 ** 2; // KiB: 1 GiB | ||
| const ARGON2_DEFAULT_MAXMEM = ARGON2_DEFAULT_MEMORY * 1024; | ||
| // Validate safe JS integers in `[0, 2^32 - 1]`. | ||
@@ -218,8 +209,11 @@ function isU32(num) { | ||
| } | ||
| function argon2Opts(opts) { | ||
| function argon2Opts(opts = {}) { | ||
| opts = checkOpts({}, opts); | ||
| const merged = { | ||
| t: 3, | ||
| m: ARGON2_DEFAULT_MEMORY, | ||
| p: 1, | ||
| version: 0x13, | ||
| dkLen: 32, | ||
| maxmem: maxUint32 - 1, | ||
| maxmem: ARGON2_DEFAULT_MAXMEM, | ||
| asyncTick: 10, | ||
@@ -257,80 +251,110 @@ }; | ||
| } | ||
| function argon2Init(password, salt, type, opts) { | ||
| password = kdfInputToBytes(password, 'password'); | ||
| salt = kdfInputToBytes(salt, 'salt'); | ||
| if (!isU32(password.length)) | ||
| throw new Error('"password" must be less of length 1..4Gb'); | ||
| // RFC 9106 §3.1 only requires S <= 2^32-1 bytes and says 16 bytes is RECOMMENDED for password | ||
| // hashing; this library intentionally takes the stricter common >=8-byte salt path. | ||
| if (!isU32(salt.length) || salt.length < 8) | ||
| throw new Error('"salt" must be of length 8..4Gb'); | ||
| if (!Object.values(AT).includes(type)) | ||
| throw new Error('"type" was invalid'); | ||
| let { p, dkLen, m, t, version, key, personalization, maxmem, onProgress, asyncTick } = argon2Opts(opts); | ||
| // Validation | ||
| key = abytesOrZero(key, 'key'); | ||
| personalization = abytesOrZero(personalization, 'personalization'); | ||
| // H_0 = H^(64)(LE32(p) || LE32(T) || LE32(m) || LE32(t) || | ||
| // LE32(v) || LE32(y) || LE32(length(P)) || P || | ||
| // LE32(length(S)) || S || LE32(length(K)) || K || | ||
| // LE32(length(X)) || X) | ||
| const h = blake2b.create(); | ||
| function argon2InitialHash(password, salt, type, opts) { | ||
| const ownedInputs = []; | ||
| const BUF = new Uint32Array(1); | ||
| const BUF8 = u8(BUF); | ||
| for (let item of [p, dkLen, m, t, version, type]) { | ||
| // RFC 9106 H0 encodes these scalars as LE32, so normalize the host word before exposing bytes. | ||
| BUF[0] = swap8IfBE(item); | ||
| h.update(BUF8); | ||
| let h; | ||
| let H0; | ||
| let succeeded = false; | ||
| const rememberOwned = (input, bytes) => { | ||
| if (typeof input === 'string') | ||
| ownedInputs.push(bytes); | ||
| return bytes; | ||
| }; | ||
| try { | ||
| const passwordBytes = rememberOwned(password, kdfInputToBytes(password, 'password')); | ||
| const saltBytes = rememberOwned(salt, kdfInputToBytes(salt, 'salt')); | ||
| if (!isU32(passwordBytes.length)) | ||
| throw new Error('"password" must be less of length 1..4Gb'); | ||
| // RFC 9106 §3.1 only requires S <= 2^32-1 bytes and says 16 bytes is RECOMMENDED for password | ||
| // hashing; this library intentionally takes the stricter common >=8-byte salt path. | ||
| if (!isU32(saltBytes.length) || saltBytes.length < 8) | ||
| throw new Error('"salt" must be of length 8..4Gb'); | ||
| if (!Object.values(AT).includes(type)) | ||
| throw new Error('"type" was invalid'); | ||
| let { p, dkLen, m, t, version, key, personalization, maxmem, onProgress, asyncTick } = argon2Opts(opts); | ||
| // Validation | ||
| const keyInput = key; | ||
| key = rememberOwned(keyInput, abytesOrZero(keyInput, 'key')); | ||
| const personalizationInput = personalization; | ||
| personalization = rememberOwned(personalizationInput, abytesOrZero(personalizationInput, 'personalization')); | ||
| // H_0 = H^(64)(LE32(p) || LE32(T) || LE32(m) || LE32(t) || | ||
| // LE32(v) || LE32(y) || LE32(length(P)) || P || | ||
| // LE32(length(S)) || S || LE32(length(K)) || K || | ||
| // LE32(length(X)) || X) | ||
| h = blake2b.create(); | ||
| for (let item of [p, dkLen, m, t, version, type]) { | ||
| // RFC 9106 H0 encodes these scalars as LE32, so normalize the host word before | ||
| // exposing bytes. | ||
| BUF[0] = swap8IfBE(item); | ||
| h.update(BUF8); | ||
| } | ||
| for (let i of [passwordBytes, saltBytes, key, personalization]) { | ||
| BUF[0] = swap8IfBE(i.length); // BUF is u32 array, this is valid once normalized to LE bytes | ||
| h.update(BUF8).update(i); | ||
| } | ||
| // Reserve two extra LE32 words after the 64-byte `H_0` so Figures 3-4 can append | ||
| // `LE32(0 or 1) || LE32(i)` in place for the lane-starting blocks. | ||
| H0 = new Uint32Array(18); | ||
| h.digestInto(u8(H0)); | ||
| succeeded = true; | ||
| return { H0, p, dkLen, m, t, version, maxmem, onProgress, asyncTick }; | ||
| } | ||
| for (let i of [password, salt, key, personalization]) { | ||
| BUF[0] = swap8IfBE(i.length); // BUF is u32 array, this is valid once normalized to LE bytes | ||
| h.update(BUF8).update(i); | ||
| finally { | ||
| // digestInto() does not destroy BLAKE2 state. It and all string-derived inputs contain secrets. | ||
| if (h) | ||
| h.destroy(); | ||
| clean(BUF, ...ownedInputs); | ||
| if (!succeeded && H0) | ||
| clean(H0); | ||
| } | ||
| // Reserve two extra LE32 words after the 64-byte `H_0` so Figures 3-4 can append | ||
| // `LE32(0 or 1) || LE32(i)` in place for the lane-starting blocks. | ||
| const H0 = new Uint32Array(18); | ||
| const H0_8 = u8(H0); | ||
| h.digestInto(H0_8); | ||
| } | ||
| function argon2Init(password, salt, type, opts) { | ||
| const { H0, p, dkLen, m, t, version, maxmem, onProgress, asyncTick } = argon2InitialHash(password, salt, type, opts); | ||
| // 256 u32 = 1024 (BLOCK_SIZE), fills A2_BUF on processing | ||
| // Params | ||
| const lanes = p; | ||
| // m' = 4 * p * floor (m / 4p) | ||
| const mP = 4 * p * Math.floor(m / (ARGON2_SYNC_POINTS * p)); | ||
| //q = m' / p columns | ||
| const laneLen = Math.floor(mP / p); | ||
| const segmentLen = Math.floor(laneLen / ARGON2_SYNC_POINTS); | ||
| // `maxmem` is documented in bytes; compare against the actual 1024-byte block allocation. | ||
| const memUsed = mP * 1024; | ||
| if (!isU32(maxmem)) | ||
| throw new Error('"maxmem" expected <2**32, got ' + maxmem); | ||
| if (memUsed > maxmem) | ||
| throw new Error('"maxmem" limit was hit: memUsed(mP*1024)=' + memUsed + ', maxmem=' + maxmem); | ||
| const B = new Uint32Array(memUsed / 4); | ||
| // Fill first blocks | ||
| for (let l = 0; l < p; l++) { | ||
| const i = 256 * laneLen * l; | ||
| // B[i][0] = H'^(1024)(H_0 || LE32(0) || LE32(i)) | ||
| H0[17] = swap8IfBE(l); | ||
| H0[16] = swap8IfBE(0); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i); | ||
| // B[i][1] = H'^(1024)(H_0 || LE32(1) || LE32(i)) | ||
| H0[16] = swap8IfBE(1); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i + 256); | ||
| try { | ||
| // Params | ||
| const lanes = p; | ||
| // m' = 4 * p * floor (m / 4p) | ||
| const mP = 4 * p * Math.floor(m / (ARGON2_SYNC_POINTS * p)); | ||
| //q = m' / p columns | ||
| const laneLen = Math.floor(mP / p); | ||
| const segmentLen = Math.floor(laneLen / ARGON2_SYNC_POINTS); | ||
| // `maxmem` is documented in bytes; compare against the actual 1024-byte block allocation. | ||
| const memUsed = mP * 1024; | ||
| if (!isU32(maxmem)) | ||
| throw new Error('"maxmem" expected <2**32, got ' + maxmem); | ||
| if (memUsed > maxmem) | ||
| throw new Error('"maxmem" limit was hit: memUsed(mP*1024)=' + memUsed + ', maxmem=' + maxmem); | ||
| const B = new Uint32Array(memUsed / 4); | ||
| // Fill first blocks | ||
| for (let l = 0; l < p; l++) { | ||
| const i = 256 * laneLen * l; | ||
| // B[i][0] = H'^(1024)(H_0 || LE32(0) || LE32(i)) | ||
| H0[17] = swap8IfBE(l); | ||
| H0[16] = swap8IfBE(0); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i); | ||
| // B[i][1] = H'^(1024)(H_0 || LE32(1) || LE32(i)) | ||
| H0[16] = swap8IfBE(1); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i + 256); | ||
| } | ||
| let perBlock = () => { }; | ||
| if (onProgress) { | ||
| // The first segment of the first pass skips two preinitialized blocks per lane. | ||
| const totalBlock = t * ARGON2_SYNC_POINTS * p * segmentLen - 2 * p; | ||
| // Invoke callback if progress changes from 10.01 to 10.02 | ||
| // Allows to draw smooth progress bar on up to 8K screen | ||
| const callbackPer = Math.max(Math.floor(totalBlock / 10000), 1); | ||
| let blockCnt = 0; | ||
| perBlock = () => { | ||
| blockCnt++; | ||
| if (onProgress && (!(blockCnt % callbackPer) || blockCnt === totalBlock)) | ||
| onProgress(blockCnt / totalBlock); | ||
| }; | ||
| } | ||
| return { type, mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock, asyncTick }; | ||
| } | ||
| let perBlock = () => { }; | ||
| if (onProgress) { | ||
| // The first segment of the first pass skips two preinitialized blocks per lane. | ||
| const totalBlock = t * ARGON2_SYNC_POINTS * p * segmentLen - 2 * p; | ||
| // Invoke callback if progress changes from 10.01 to 10.02 | ||
| // Allows to draw smooth progress bar on up to 8K screen | ||
| const callbackPer = Math.max(Math.floor(totalBlock / 10000), 1); | ||
| let blockCnt = 0; | ||
| perBlock = () => { | ||
| blockCnt++; | ||
| if (onProgress && (!(blockCnt % callbackPer) || blockCnt === totalBlock)) | ||
| onProgress(blockCnt / totalBlock); | ||
| }; | ||
| finally { | ||
| clean(H0); | ||
| } | ||
| clean(BUF, H0); | ||
| return { type, mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock, asyncTick }; | ||
| } | ||
@@ -355,6 +379,5 @@ function argon2Output(B, p, laneLen, dkLen) { | ||
| */ | ||
| function* argon2Blocks(ctx) { | ||
| function* argon2Blocks(ctx, address) { | ||
| const { type, mP, p, t, version, B, laneLen, lanes, segmentLen, perBlock } = ctx; | ||
| // [address, input, zero_block] format so we can pass single U32 to block function | ||
| const address = new Uint32Array(3 * 256); | ||
| address[256 + 6] = mP; | ||
@@ -426,3 +449,3 @@ address[256 + 8] = t; | ||
| const ctx = argon2Init(password, salt, type, opts); | ||
| const blocks = argon2Blocks(ctx); | ||
| const blocks = argon2Blocks(ctx, new Uint32Array(3 * 256)); | ||
| while (!blocks.next().done) { } | ||
@@ -463,3 +486,3 @@ return argon2Output(ctx.B, ctx.p, ctx.laneLen, ctx.dkLen); | ||
| */ | ||
| export const argon2d = (password, salt, opts) => argon2(AT.Argon2d, password, salt, opts); | ||
| export const argon2d = (password, salt, opts = {}) => argon2(AT.Argon2d, password, salt, opts); | ||
| /** | ||
@@ -478,3 +501,3 @@ * Argon2i side-channel-resistant version. | ||
| */ | ||
| export const argon2i = (password, salt, opts) => argon2(AT.Argon2i, password, salt, opts); | ||
| export const argon2i = (password, salt, opts = {}) => argon2(AT.Argon2i, password, salt, opts); | ||
| /** | ||
@@ -493,6 +516,9 @@ * Argon2id, combining i+d, the most popular version from RFC 9106. | ||
| */ | ||
| export const argon2id = (password, salt, opts) => argon2(AT.Argon2id, password, salt, opts); | ||
| export const argon2id = (password, salt, opts = {}) => argon2(AT.Argon2id, password, salt, opts); | ||
| async function argon2Async(type, password, salt, opts) { | ||
| const ctx = argon2Init(password, salt, type, opts); | ||
| const blocks = argon2Blocks(ctx); | ||
| // Keep the generator-local address block reachable so an aborted scheduler yield can wipe it. | ||
| const address = new Uint32Array(3 * 256); | ||
| const blocks = argon2Blocks(ctx, address); | ||
| const abort = () => clean(address, ctx.B); | ||
| let ts = Date.now(); | ||
@@ -505,4 +531,5 @@ while (!blocks.next().done) { | ||
| continue; | ||
| await nextTick(); | ||
| ts += diff; | ||
| await nextTick(abort); | ||
| // Scheduler delay is outside the synchronous work budget. | ||
| ts = Date.now(); | ||
| } | ||
@@ -542,3 +569,3 @@ return argon2Output(ctx.B, ctx.p, ctx.laneLen, ctx.dkLen); | ||
| */ | ||
| export const argon2dAsync = (password, salt, opts) => argon2Async(AT.Argon2d, password, salt, opts); | ||
| export const argon2dAsync = (password, salt, opts = {}) => argon2Async(AT.Argon2d, password, salt, opts); | ||
| /** | ||
@@ -557,3 +584,3 @@ * Argon2i async side-channel-resistant version. | ||
| */ | ||
| export const argon2iAsync = (password, salt, opts) => argon2Async(AT.Argon2i, password, salt, opts); | ||
| export const argon2iAsync = (password, salt, opts = {}) => argon2Async(AT.Argon2i, password, salt, opts); | ||
| /** | ||
@@ -572,2 +599,2 @@ * Argon2id async, combining i+d, the most popular version from RFC 9106. | ||
| */ | ||
| export const argon2idAsync = (password, salt, opts) => argon2Async(AT.Argon2id, password, salt, opts); | ||
| export const argon2idAsync = (password, salt, opts = {}) => argon2Async(AT.Argon2id, password, salt, opts); |
+3
-2
@@ -32,4 +32,2 @@ /** | ||
| const EMPTY_SALT = /* @__PURE__ */ new Uint32Array(8); | ||
| // Base destroy logic only clears salt-derived state; the partial message buffer and length/position | ||
| // bookkeeping remain until the instance or backing buffer is reused. | ||
| class BLAKE1 { | ||
@@ -111,2 +109,5 @@ canXOF = false; | ||
| this.destroyed = true; | ||
| clean(this.buffer); | ||
| this.length = 0; | ||
| this.pos = 0; | ||
| if (this.salt !== EMPTY_SALT) { | ||
@@ -113,0 +114,0 @@ clean(this.salt, this.constants); |
+3
-1
@@ -137,3 +137,5 @@ /** | ||
| // 4 (100) - leafs finished at depth=1 and depth=2 | ||
| for (let last, chunks = this.chunksDone + 1; isLast || !(chunks & 1); chunks >>= 1) { | ||
| // Reading the low bit is safe for parity, but keep division full-width: shifting would | ||
| // truncate the safe-integer chunk counter to 32 bits. | ||
| for (let last, chunks = this.chunksDone + 1; isLast || !(chunks & 1); chunks = Math.floor(chunks / 2)) { | ||
| if (!(last = this.stack.pop())) | ||
@@ -140,0 +142,0 @@ break; |
+11
-7
| { | ||
| "name": "@noble/hashes", | ||
| "version": "2.3.0", | ||
| "version": "2.4.0", | ||
| "description": "Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt", | ||
@@ -11,7 +11,8 @@ "files": [ | ||
| "devDependencies": { | ||
| "@paulmillr/jsbt": "0.6.5", | ||
| "@types/node": "25.3.0", | ||
| "@paulmillr/jsbt": "0.7.1", | ||
| "bismar": "0.1.8", | ||
| "@types/node": "26.2.0", | ||
| "fast-check": "4.2.0", | ||
| "prettier": "3.6.2", | ||
| "typescript": "6.0.2" | ||
| "prettier": "3.9.6", | ||
| "typescript": "6.0.3" | ||
| }, | ||
@@ -21,3 +22,4 @@ "scripts": { | ||
| "benchmark:thirdparty": "cd benchmark/thirdparty; npm ci; JSBT_BENCHMARK_DIMENSIONS='algorithm,buffer,library' node hashes.ts", | ||
| "benchmark:thirdparty-scrypt": "cd benchmark/thirdparty; npm ci; JSBT_BENCHMARK_DIMENSIONS='iters,library' JSBT_BENCHMARK_FILTER='async' node scrypt.ts", | ||
| "benchmark:thirdparty-scrypt": "cd benchmark/thirdparty; npm ci; JSBT_BENCHMARK_DIMENSIONS='iters,library' FILTER='async' node scrypt.ts", | ||
| "benchmark:size": "bismar -bsm", | ||
| "build": "tsc", | ||
@@ -31,3 +33,5 @@ "build:clean": "rm *.{js,d.ts} 2> /dev/null", | ||
| "test:acvp": "node test/slow-acvp.test.ts", | ||
| "test:kdf": "node test/slow-kdf.test.ts" | ||
| "test:kdf": "node test/slow-kdf.test.ts", | ||
| "test:ultra": "node test/slow-ultra.test.ts", | ||
| "test:ultra:scrypt": "node test/slow-extreme-scrypt.test.ts" | ||
| }, | ||
@@ -34,0 +38,0 @@ "exports": { |
+27
-9
@@ -26,10 +26,23 @@ /** | ||
| const p = kdfInputToBytes(_password, 'password'); | ||
| const s = kdfInputToBytes(_salt, 'salt'); | ||
| // DK = PBKDF2(PRF, Password, Salt, c, dkLen); | ||
| const DK = new Uint8Array(dkLen); | ||
| const { iHash, oHash, outputLen } = hmac.create(hash, p); | ||
| // Drive keyed hashes directly; the wrapper is only needed to initialize their HMAC midstates. | ||
| const u = new Uint8Array(outputLen); | ||
| const eng = pbkdf2Engine(iHash, oHash, s, u); | ||
| return { c, dkLen, asyncTick, DK, outputLen, eng }; | ||
| try { | ||
| const s = kdfInputToBytes(_salt, 'salt'); | ||
| try { | ||
| // DK = PBKDF2(PRF, Password, Salt, c, dkLen); | ||
| const DK = new Uint8Array(dkLen); | ||
| const { iHash, oHash, outputLen } = hmac.create(hash, p); | ||
| // Drive keyed hashes directly; the wrapper is only needed to initialize their HMAC midstates. | ||
| const u = new Uint8Array(outputLen); | ||
| const eng = pbkdf2Engine(iHash, oHash, s, u); | ||
| return { c, dkLen, asyncTick, DK, outputLen, eng }; | ||
| } | ||
| finally { | ||
| // Uint8Array inputs belong to the caller; only wipe our UTF-8 conversion. | ||
| if (typeof _salt === 'string') | ||
| clean(s); | ||
| } | ||
| } | ||
| finally { | ||
| if (typeof _password === 'string') | ||
| clean(p); | ||
| } | ||
| } | ||
@@ -144,2 +157,7 @@ // Per-call PRF driver writes U1 into both `u` and `Ti`, then later digests into `u`; | ||
| const { c, dkLen, asyncTick, DK, outputLen, eng } = pbkdf2Init(hash, password, salt, opts); | ||
| // Reuse normal state destruction, then wipe the incomplete output if a host yield aborts. | ||
| const abort = () => { | ||
| eng.output(DK); | ||
| clean(DK); | ||
| }; | ||
| // DK = T1 + T2 + ⋯ + Tdklen/hlen | ||
@@ -157,5 +175,5 @@ for (let ti = 1, pos = 0; pos < dkLen; ti++, pos += outputLen) { | ||
| eng.rounds(2, Ti); // c=2 runs exactly one PRF iteration per callback. | ||
| }); | ||
| }, abort); | ||
| } | ||
| return eng.output(DK); | ||
| } |
+105
-206
@@ -6,3 +6,3 @@ # noble-hashes | ||
| - 🔒 [**Audited**](#security) by an independent security firm | ||
| - 🪶 Minimal: 2.7KB (gzipped) sha256, unused code is excluded from your builds | ||
| - 🪶 Minimal: 2.8KB (gzipped) sha256, unused code is excluded from your builds | ||
| - 🏎 Fast: hand-optimized for caveats of JS engines | ||
@@ -46,30 +46,4 @@ - 🔍 Reliable: chained / ACVP tests ensure correctness | ||
| // import * from '@noble/hashes'; // Error: use sub-imports, to ensure small app size | ||
| import { sha256 as noble_sha256 } from '@noble/hashes/sha2.js'; | ||
| const hash = noble_sha256(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])); | ||
| // Available modules | ||
| import { sha256, sha384, sha512, sha224, sha512_224, sha512_256 } from '@noble/hashes/sha2.js'; | ||
| import { | ||
| sha3_256, sha3_512, | ||
| keccak_256, keccak_512, | ||
| shake128, shake256, | ||
| } from '@noble/hashes/sha3.js'; | ||
| import { | ||
| cshake256, turboshake256, kmac256, tuplehash256, | ||
| kt128, kt256, keccakprg, | ||
| } from '@noble/hashes/sha3-addons.js'; | ||
| import { blake3 } from '@noble/hashes/blake3.js'; | ||
| import { blake2b, blake2s } from '@noble/hashes/blake2.js'; | ||
| import { blake256, blake512 } from '@noble/hashes/blake1.js'; | ||
| import { sha1, md5, ripemd160 } from '@noble/hashes/legacy.js'; | ||
| import { hmac } from '@noble/hashes/hmac.js'; | ||
| import { hkdf } from '@noble/hashes/hkdf.js'; | ||
| import { pbkdf2, pbkdf2Async } from '@noble/hashes/pbkdf2.js'; | ||
| import { scrypt, scryptAsync } from '@noble/hashes/scrypt.js'; | ||
| import { argon2d, argon2i, argon2id } from '@noble/hashes/argon2.js'; | ||
| import { eskdf } from '@noble/hashes/eskdf.js'; | ||
| import * as webcrypto from '@noble/hashes/webcrypto.js'; | ||
| // const { sha256, sha384, sha512, hmac, hkdf, pbkdf2 } = webcrypto; | ||
| import * as utils from '@noble/hashes/utils.js'; | ||
| const { bytesToHex, concatBytes, equalBytes, hexToBytes } = utils; | ||
| import { sha256 } from '@noble/hashes/sha2.js'; | ||
| const hash = sha256(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])); | ||
| ``` | ||
@@ -102,12 +76,5 @@ | ||
| import { sha224, sha256, sha384, sha512, sha512_224, sha512_256 } from '@noble/hashes/sha2.js'; | ||
| const res = sha256(Uint8Array.from([0xbc])); // basic | ||
| for (let hash of [sha256, sha384, sha512, sha224, sha512_224, sha512_256]) { | ||
| const arr = Uint8Array.from([0x10, 0x20, 0x30]); | ||
| const a = hash(arr); | ||
| const b = hash.create().update(arr).digest(); | ||
| } | ||
| const res = sha256(Uint8Array.from([0xbc])); | ||
| ``` | ||
| Check out [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234) and | ||
| [the paper on truncated SHA512/256](https://eprint.iacr.org/2010/548.pdf). | ||
@@ -122,10 +89,3 @@ #### sha3: FIPS, SHAKE, Keccak | ||
| } from '@noble/hashes/sha3.js'; | ||
| for (let hash of [ | ||
| sha3_224, sha3_256, sha3_384, sha3_512, | ||
| keccak_224, keccak_256, keccak_384, keccak_512, | ||
| ]) { | ||
| const arr = Uint8Array.from([0x10, 0x20, 0x30]); | ||
| const a = hash(arr); | ||
| const b = hash.create().update(arr).digest(); | ||
| } | ||
| const s = sha3_256(Uint8Array.from([0x10, 0x20, 0x30])); | ||
| const shka = shake128(Uint8Array.from([0x10]), { dkLen: 512 }); | ||
@@ -135,7 +95,2 @@ const shkb = shake256(Uint8Array.from([0x30]), { dkLen: 512 }); | ||
| Check out [FIPS-202](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf), | ||
| [Website](https://keccak.team/keccak.html). | ||
| Check out [the differences between SHA-3 and Keccak](https://crypto.stackexchange.com/questions/15727/what-are-the-key-differences-between-the-draft-sha-3-standard-and-the-keccak-sub) | ||
| #### sha3-addons: cSHAKE, KMAC, KT128, TurboSHAKE | ||
@@ -151,28 +106,15 @@ | ||
| const data = Uint8Array.from([0x10, 0x20, 0x30]); | ||
| const personalization = new TextEncoder().encode('def'); | ||
| const ec1 = cshake128(data, { personalization }); | ||
| const ec2 = cshake256(data, { personalization }); | ||
| const et1 = turboshake128(data); | ||
| const et2 = turboshake256(data, { D: 0x05 }); | ||
| const ec = cshake128(data, { personalization: new TextEncoder().encode('def') }); | ||
| const et = turboshake256(data, { D: 0x05 }); | ||
| // tuplehash(['ab', 'c']) !== tuplehash(['a', 'bc']) !== tuplehash([data]) | ||
| const et3 = tuplehash256([new TextEncoder().encode('ab'), new TextEncoder().encode('c')]); | ||
| const eu = tuplehash256([new TextEncoder().encode('ab'), new TextEncoder().encode('c')]); | ||
| // Not parallel in JS (similar to blake3 / kt128), added for compat | ||
| const ep1 = parallelhash256(data, { blockLen: 8 }); | ||
| const kk = Uint8Array.from([0xca]); | ||
| const ek10 = kmac128(kk, data); | ||
| const ek11 = kmac256(kk, data); | ||
| const ek12 = kt128(data); // kangarootwelve 128-bit | ||
| const ek13 = kt256(data); // kangarootwelve 256-bit | ||
| // pseudo-random generator, first argument is capacity. XKCP recommends 254 bits capacity for 128-bit security strength. | ||
| const ep = parallelhash256(data, { blockLen: 8 }); | ||
| const ek = kmac256(Uint8Array.from([0xca]), data); | ||
| const ekt = kt128(data); | ||
| const p = keccakprg(254); | ||
| p.addEntropy(Uint8Array.from([1, 2, 3])); | ||
| p.addEntropy(); | ||
| const rand1b = p.randomBytes(32); | ||
| ``` | ||
| - cSHAKE, KMAC, TupleHash, ParallelHash + XOF are available, matching | ||
| [NIST SP 800-185](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf) | ||
| - Reduced-round Keccak KT128 (KangarooTwelve 🦘, K12) and TurboSHAKE are available, matching | ||
| [RFC 9861](https://datatracker.ietf.org/doc/rfc9861/). | ||
| - [KeccakPRG](https://keccak.team/files/CSF-0.1.pdf): pseudo-random generator based on Keccak | ||
| #### blake1, blake2, blake3 | ||
@@ -184,23 +126,12 @@ | ||
| import { blake3 } from '@noble/hashes/blake3.js'; | ||
| const ab = Uint8Array.from([0x01]); | ||
| blake256(ab); | ||
| for (let hash of [blake224, blake256, blake384, blake512, blake2b, blake2s, blake3]) { | ||
| const arr = Uint8Array.from([0x10, 0x20, 0x30]); | ||
| const a = hash(arr); | ||
| const b = hash.create().update(arr).digest(); | ||
| } | ||
| // blake2 advanced usage | ||
| const ab = Uint8Array.from([0x01]); | ||
| const txt = new TextEncoder(); | ||
| blake2s(ab); | ||
| blake2s(ab, { key: new Uint8Array(32) }); | ||
| blake2s(ab, { personalization: txt.encode('pers1234') }); | ||
| blake2s(ab, { salt: txt.encode('salt1234') }); | ||
| blake2b(ab); | ||
| blake2b(ab, { key: new Uint8Array(64) }); | ||
| blake2b(ab, { personalization: txt.encode('pers1234pers1234') }); | ||
| blake2b(ab, { salt: txt.encode('salt1234salt1234') }); | ||
| blake2s(ab, { key: new Uint8Array(32) }); // blake2b keys can be 64 bytes | ||
| blake2s(ab, { personalization: txt.encode('pers1234') }); // 16 bytes for blake2b | ||
| blake2s(ab, { salt: txt.encode('salt1234') }); // 16 bytes for blake2b | ||
| // blake3 advanced usage | ||
| blake3(ab); | ||
| blake3(ab, { dkLen: 256 }); | ||
@@ -211,22 +142,7 @@ blake3(ab, { key: new Uint8Array(32) }); | ||
| - Blake1 is legacy hash, one of SHA3 proposals. It is rarely used anywhere. See [pdf](https://www.aumasson.jp/blake/blake.pdf). | ||
| - Blake2 is popular fast hash. blake2b focuses on 64-bit platforms while blake2s is for 8-bit to 32-bit ones. See [RFC 7693](https://datatracker.ietf.org/doc/html/rfc7693), [Website](https://www.blake2.net) | ||
| - Blake3 is faster, reduced-round blake2. See [Website & specs](https://blake3.io) | ||
| #### legacy: sha1, md5, ripemd160 | ||
| SHA1 (RFC 3174), MD5 (RFC 1321) and RIPEMD160 (ISO/IEC 10118-3) legacy, weak hash functions. | ||
| Don't use them in a new protocol. What "weak" means: | ||
| - Collisions can be made with 2^24 effort in MD5 (seconds on commodity hardware), 2^61 in SHA1 (demonstrated in practice), 2^80 in RIPEMD160. | ||
| - No practical pre-image attacks (only theoretical, 2^123.4) | ||
| - HMAC seems kinda ok: https://datatracker.ietf.org/doc/html/rfc6151 | ||
| ```typescript | ||
| import { md5, ripemd160, sha1 } from '@noble/hashes/legacy.js'; | ||
| for (let hash of [md5, ripemd160, sha1]) { | ||
| const arr = Uint8Array.from([0x10, 0x20, 0x30]); | ||
| const a = hash(arr); | ||
| const b = hash.create().update(arr).digest(); | ||
| } | ||
| const h = sha1(Uint8Array.from([0x10, 0x20, 0x30])); | ||
| ``` | ||
@@ -245,4 +161,2 @@ | ||
| Conforms to [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104). | ||
| #### hkdf | ||
@@ -265,4 +179,2 @@ | ||
| Conforms to [RFC 5869](https://datatracker.ietf.org/doc/html/rfc5869). | ||
| #### pbkdf2 | ||
@@ -281,4 +193,2 @@ | ||
| Conforms to [RFC 8018](https://datatracker.ietf.org/doc/html/rfc8018). | ||
| #### scrypt | ||
@@ -298,9 +208,6 @@ | ||
| }, | ||
| maxmem: 128 * 8 * (2 ** 17 + 1 + 1), // 128 * r * (N + p + 1) | ||
| // maxmem: 128 * 8 * (2 ** 17 + 1 + 1), // 128 * r * (N + p + 1) | ||
| }); | ||
| ``` | ||
| Conforms to [RFC 7914](https://datatracker.ietf.org/doc/html/rfc7914), | ||
| [Website](https://www.tarsnap.com/scrypt.html) | ||
| - `N, r, p` are work factors. It is common to only adjust N, while keeping `r: 8, p: 1`. | ||
@@ -311,18 +218,7 @@ See [the blog post](https://blog.filippo.io/the-scrypt-parameters/). | ||
| - `onProgress` can be used with async version of the function to report progress to a user. | ||
| - `maxmem` prevents DoS and is limited to `1GB + 1KB` (`2**30 + 2**10`), but can be adjusted using formula: `128 * r * (N + p + 1)` | ||
| - `maxmem` prevents DoS and defaults to `1GiB + 2KiB` (`2**30 + 2**11`), enough for `N: 2**20, r: 8, p: 1`. It can be adjusted using formula: `128 * r * (N + p + 1)` | ||
| Time it takes to derive Scrypt key under different values of N (2\*\*N) on Apple M4 (mobile phones can be 1x-4x slower): | ||
| On Apple M4, `N: 2**16` takes 0.1s and 64MB RAM; each increment of N doubles both, | ||
| up to `N: 2**24` at 27s and 16GB. Mobile phones can be 1x-4x slower. | ||
| | N pow | Time | RAM | | ||
| | ----- | ---- | ----- | | ||
| | 16 | 0.1s | 64MB | | ||
| | 17 | 0.2s | 128MB | | ||
| | 18 | 0.4s | 256MB | | ||
| | 19 | 0.8s | 512MB | | ||
| | 20 | 1.5s | 1GB | | ||
| | 21 | 3.1s | 2GB | | ||
| | 22 | 6.2s | 4GB | | ||
| | 23 | 13s | 8GB | | ||
| | 24 | 27s | 16GB | | ||
| > [!NOTE] | ||
@@ -338,7 +234,6 @@ > We support N larger than `2**20` where available, however, | ||
| import { argon2d, argon2i, argon2id } from '@noble/hashes/argon2.js'; | ||
| const arg1 = argon2id('password', 'saltsalt', { t: 2, m: 65536, p: 1, maxmem: 2 ** 32 - 1 }); | ||
| // Defaults to t=3, m=1GiB (specified in KiB), p=1, and a 1GiB maxmem limit. | ||
| const arg1 = argon2id('password', 'saltsalt'); | ||
| ``` | ||
| Argon2 [RFC 9106](https://datatracker.ietf.org/doc/html/rfc9106) implementation. | ||
| > [!WARNING] | ||
@@ -382,11 +277,5 @@ > Argon2 can't be fast in JS, because there is no fast Uint64Array. | ||
| Sometimes people want to use built-in `crypto.subtle` instead of pure JS implementation. | ||
| However, it has terrible API. | ||
| A thin wrapper over built-in `crypto.subtle`, mirroring the noble-hashes API and validating | ||
| inputs, in just 30+ lines of code. Webcrypto methods are always async. | ||
| We simplify access to built-ins with API which mirrors noble-hashes. | ||
| The overhead is minimal - just 30+ lines of code, which verify input correctness. | ||
| > [!NOTE] | ||
| > Webcrypto methods are always async. | ||
| #### utils | ||
@@ -402,2 +291,21 @@ | ||
| ### Specs | ||
| - SHA2: [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234) | ||
| - SHA2-512/256: [pdf](https://eprint.iacr.org/2010/548.pdf) | ||
| - SHA3: [FIPS-202](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf) | ||
| - SHA3-addons: [NIST SP 800-185](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf) | ||
| - SHA3-addons KT128 (KangarooTwelve 🦘, K12) / TurboSHAKE: [RFC 9861](https://datatracker.ietf.org/doc/rfc9861/) | ||
| - BLAKE1: [pdf](https://www.aumasson.jp/blake/blake.pdf) | ||
| - BLAKE2: [RFC 7693](https://datatracker.ietf.org/doc/html/rfc7693) | ||
| - BLAKE3: [site](https://blake3.io) | ||
| - SHA1: [RFC 3174](https://datatracker.ietf.org/doc/html/rfc3174) | ||
| - MD5: [RFC 1321](https://datatracker.ietf.org/doc/html/rfc1321) | ||
| - RIPEMD160 (ISO/IEC 10118-3) | ||
| - HMAC: [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) | ||
| - HKDF: [RFC 5869](https://datatracker.ietf.org/doc/html/rfc5869) | ||
| - PBKDF2: [RFC 8018](https://datatracker.ietf.org/doc/html/rfc8018) | ||
| - Scrypt: [RFC 7914](https://datatracker.ietf.org/doc/html/rfc7914) | ||
| - Argon2: [RFC 9106](https://datatracker.ietf.org/doc/html/rfc9106) | ||
| ## Security | ||
@@ -407,5 +315,2 @@ | ||
| - at version 2.2.0, in Apr 2026, by ourselves (self-audited) | ||
| - Scope: everything | ||
| - [Changes since audit](https://github.com/paulmillr/noble-hashes/compare/2.2.0..main) | ||
| - at version 1.0.0, in Jan 2022, independently, by [Cure53](https://cure53.de) | ||
@@ -416,5 +321,7 @@ - PDFs: [website](https://cure53.de/pentest-report_hashing-libs.pdf), [in-repo](./audit/2022-01-05-cure53-audit-nbl2.pdf) | ||
| We've started regular AI-assisted self-audits in Apr 2026. | ||
| It is tested against official (ACVP / KAT) vectors, cross-library chained hashing, | ||
| sliding-window length sweeps and property-based tests (fast-check), | ||
| and is being fuzzed in [the separate repo](https://github.com/paulmillr/fuzzing). | ||
| and is being fuzzed in CI. | ||
@@ -435,19 +342,11 @@ If you see anything unusual: investigate and report. | ||
| The library shares state buffers between hash | ||
| function calls. The buffers are zeroed-out after each call. However, if an attacker | ||
| can read application memory, you are doomed in any case: | ||
| The library shares state buffers between hash function calls. Library-owned working buffers are | ||
| zeroed after use, including mutable UTF-8 copies created from password-KDF string inputs. | ||
| However, if an attacker can read application memory, you are doomed in any case: | ||
| - At some point, input will be a string and strings are immutable in JS: | ||
| there is no way to overwrite them with zeros. For example: deriving | ||
| key from `scrypt(password, salt)` where password and salt are strings | ||
| - Input from a file will stay in file buffers | ||
| - Input / output will be re-used multiple times in application which means it could stay in memory | ||
| - `await anything()` will always write all internal variables (including numbers) | ||
| to memory. With async functions / Promises there are no guarantees when the code | ||
| chunk would be executed. Which means attacker can have plenty of time to read data from memory | ||
| - There is no way to guarantee anything about zeroing sensitive data without | ||
| complex tests-suite which will dump process memory and verify that there is | ||
| no sensitive data left. For JS it means testing all browsers (incl. mobile), | ||
| which is complex. And of course it will be useless without using the same | ||
| test-suite in the actual application that consumes the library | ||
| - JS strings are immutable and can't be overwritten with zeros — e.g. a password passed | ||
| to `scrypt(password, salt)` as a string stays in memory | ||
| - Inputs & outputs are re-used across the application and stay in file buffers / memory anyway | ||
| - `await anything()` writes all internal variables (including numbers) to memory, with no | ||
| guarantee of when they get overwritten — plenty of time for an attacker to read them | ||
@@ -459,5 +358,3 @@ ### Supply chain security | ||
| - **Rare releasing** is practiced to minimize the need for re-audits by end-users. | ||
| - **Dependencies** are minimized and strictly pinned to reduce supply-chain risk. | ||
| - We use as few dependencies as possible. | ||
| - Version ranges are locked, and changes are checked with npm-diff. | ||
| - **Dependencies** are minimized, strictly pinned, and changes are checked with npm-diff. | ||
| - **Dev dependencies** are excluded from end-user installs; they’re only used for development and build steps. | ||
@@ -515,13 +412,11 @@ | ||
| `npm install && npm run build && npm test` will build the code and run tests. | ||
| There are **additional** slow suites: timing-based DoS tests `npm run test:dos`, | ||
| multi-hour large-input tests `npm run test:slow`, ACVP LDT vectors `npm run test:acvp`, | ||
| and memory-intensive KDF tests `npm run test:ultra`. The 9–17GiB scrypt cases require an | ||
| explicitly provisioned machine and run separately with `npm run test:ultra:scrypt`. | ||
| `test/misc` directory contains unrolled implementations (sha3, argon2) and misc helper scripts. | ||
| - `npm install && npm run build && npm test` will build the code and run tests. | ||
| - `npm run check` / `npm run format` will run linter / fix linter issues. | ||
| - `npm run benchmark` will run benchmarks | ||
| - `npm run bundle` will build single file | ||
| - There are **additional** slow suites: 20-min DoS test `npm run test:dos`, | ||
| multi-hour 4GB-input test `npm run test:slow`, ACVP vectors `npm run test:acvp` | ||
| and KDF vectors `npm run test:kdf`. | ||
| See [our approach to testing](./test/README.md) | ||
| Some hashes are outside of scope of the library: | ||
@@ -551,43 +446,47 @@ - [Pedersen in micro-zk-proofs](https://github.com/paulmillr/micro-zk-proofs/blob/1ed5ce1253583b2e540eef7f3477fb52bf5344ff/src/pedersen.ts) | ||
| # 32B | ||
| sha256 x 2,016,129 ops/sec @ 496ns/op | ||
| sha512 x 740,740 ops/sec @ 1μs/op | ||
| sha3_256 x 287,686 ops/sec @ 3μs/op | ||
| sha3_512 x 288,267 ops/sec @ 3μs/op | ||
| kt128 x 476,190 ops/sec @ 2μs/op | ||
| blake2b x 410,340 ops/sec @ 2μs/op | ||
| blake2s x 942,507 ops/sec @ 1μs/op | ||
| blake3 x 1,006,036 ops/sec @ 994ns/op | ||
| ripemd160 x 1,410,437 ops/sec @ 709ns/op | ||
| md5 x 1,663,893 ops/sec @ 601ns/op | ||
| sha1 x 1,589,825 ops/sec @ 629ns/op | ||
| sha256 438 ns | ||
| sha512 1219 ns | ||
| sha3_256 1853 ns | ||
| sha3_512 1864 ns | ||
| kt128 1380 ns | ||
| kt256 1370 ns | ||
| turboshake128 1191 ns | ||
| blake256 1335 ns | ||
| blake2b 2186 ns | ||
| blake2s 1055 ns | ||
| blake3 981 ns | ||
| ripemd160 563 ns | ||
| md5 449 ns | ||
| sha1 507 ns | ||
| hmac(sha256) 1955 ns | ||
| hmac(sha512) 5126 ns | ||
| kmac256 6653 ns | ||
| blake3(key) 1120 ns | ||
| # 1MB | ||
| sha256 x 331 ops/sec @ 3ms/op | ||
| sha512 x 128 ops/sec @ 7ms/op | ||
| sha3_256 x 39 ops/sec @ 25ms/op | ||
| sha3_512 x 21 ops/sec @ 46ms/op | ||
| kt128 x 91 ops/sec @ 10ms/op | ||
| kt256 x 75 ops/sec @ 13ms/op | ||
| turboshake128 x 93 ops/sec @ 10ms/op | ||
| blake256 x 57 ops/sec @ 17ms/op | ||
| blake2b x 61 ops/sec @ 16ms/op | ||
| blake2s x 78 ops/sec @ 12ms/op | ||
| blake3 x 95 ops/sec @ 10ms/op | ||
| ripemd160 x 177 ops/sec @ 5ms/op | ||
| md5 x 250 ops/sec @ 3ms/op | ||
| sha1 x 416 ops/sec @ 2ms/op | ||
| sha256 x 297 mib/sec | ||
| sha512 x 130 mib/sec | ||
| sha3_256 x 78.1 mib/sec | ||
| sha3_512 x 41.9 mib/sec | ||
| kt128 x 184 mib/sec | ||
| kt256 x 147 mib/sec | ||
| turboshake128 x 186 mib/sec | ||
| blake256 x 56.7 mib/sec | ||
| blake2b x 66.2 mib/sec | ||
| blake2s x 62.9 mib/sec | ||
| blake3 x 90 mib/sec | ||
| ripemd160 x 179 mib/sec | ||
| md5 x 275 mib/sec | ||
| sha1 x 417 mib/sec | ||
| hmac(sha256) x 290 mib/sec | ||
| hmac(sha512) x 129 mib/sec | ||
| kmac256 x 78.4 mib/sec | ||
| blake3(key) x 90.5 mib/sec | ||
| # MAC | ||
| hmac(sha256) x 599,880 ops/sec @ 1μs/op | ||
| hmac(sha512) x 197,122 ops/sec @ 5μs/op | ||
| kmac256 x 87,981 ops/sec @ 11μs/op | ||
| blake3(key) x 796,812 ops/sec @ 1μs/op | ||
| # KDF | ||
| hkdf(sha256) x 259,942 ops/sec @ 3μs/op | ||
| blake3(context) x 424,808 ops/sec @ 2μs/op | ||
| pbkdf2(sha256, c: 2 ** 18) x 5 ops/sec @ 197ms/op | ||
| pbkdf2(sha512, c: 2 ** 18) x 1 ops/sec @ 630ms/op | ||
| scrypt(n: 2 ** 18, r: 8, p: 1) x 2 ops/sec @ 400ms/op | ||
| argon2id(t: 1, m: 256MB) 2881ms | ||
| hkdf(sha256) x 249,100 ops/sec @ 4015 ns/op | ||
| blake3(context) x 480,400 ops/sec @ 2081 ns/op | ||
| pbkdf2(sha256, c: 2 ** 18) x 5 ops/sec @ 199 ms/op | ||
| scrypt(n: 2 ** 19, r: 8, p: 1) x 1 ops/sec @ 751 ms/op | ||
| argon2id(t: 1, m: 128MB) x 3 ops/sec @ 276 ms/op | ||
| ``` | ||
@@ -594,0 +493,0 @@ |
+2
-1
@@ -9,3 +9,4 @@ import { type KDFInput, type TArg, type TRet } from './utils.ts'; | ||
| * - `asyncTick` - (default: 10) max time in ms for which async function can block execution | ||
| * - `maxmem` - (default: `1024 ** 3 + 1024` aka 1GB+1KB). A limit that the app could use for scrypt | ||
| * - `maxmem` - (default: `1024 ** 3 + 2 * 1024` aka 1GiB+2KiB). A limit that the app | ||
| * could use for scrypt | ||
| * - `onProgress` - callback function that would be executed for progress report | ||
@@ -12,0 +13,0 @@ */ |
+7
-4
@@ -97,9 +97,10 @@ /** | ||
| } | ||
| // 128*r*(N+p+1) for N=2**20, r=8, p=1: 1 GiB main table plus 2 KiB workspace. | ||
| const SCRYPT_DEFAULT_MAXMEM = 128 * 8 * (2 ** 20 + 1 + 1); | ||
| // Common prologue and epilogue for sync/async functions | ||
| function scryptInit(password, salt, _opts) { | ||
| // Maxmem - 1GB+1KB by default | ||
| const opts = checkOpts({ | ||
| dkLen: 32, | ||
| asyncTick: 10, | ||
| maxmem: 1024 ** 3 + 1024, | ||
| maxmem: SCRYPT_DEFAULT_MAXMEM, | ||
| }, _opts); | ||
@@ -273,2 +274,4 @@ const { N, r, p, dkLen, asyncTick, maxmem, onProgress } = opts; | ||
| const { N, r, p, dkLen, blockSize32, V, B32, B, tmp, blockMixCb, asyncTick } = scryptInit(password, salt, opts); | ||
| // One failure handler covers both yield boundaries without putting the hot loops in a try block. | ||
| const abort = () => clean(B, V, tmp); | ||
| swap32IfBE(B32); | ||
@@ -283,3 +286,3 @@ for (let pi = 0; pi < p; pi++) { | ||
| blockMixCb(); | ||
| }); | ||
| }, abort); | ||
| BlockMix(V, (N - 1) * blockSize32, B32, Pi, r); // Process last element | ||
@@ -300,3 +303,3 @@ blockMixCb(); | ||
| blockMixCb(); | ||
| }); | ||
| }, abort); | ||
| } | ||
@@ -303,0 +306,0 @@ swap32IfBE(B32); |
+9
-1
@@ -498,2 +498,3 @@ /** | ||
| protected rate: number; | ||
| private entropyAdded; | ||
| constructor(capacity: number); | ||
@@ -504,3 +505,4 @@ protected keccak(): void; | ||
| digestInto(_out: TArg<Uint8Array>): void; | ||
| addEntropy(seed: TArg<Uint8Array>): void; | ||
| addEntropy(seed?: TArg<Uint8Array>): void; | ||
| xofInto(out: TArg<Uint8Array>): TRet<Uint8Array>; | ||
| randomBytes(length: number): TRet<Uint8Array>; | ||
@@ -514,2 +516,5 @@ clean(): void; | ||
| * See {@link https://keccak.team/files/CSF-0.1.pdf}. | ||
| * Fresh instances reject output until `.addEntropy()` has been called. With no | ||
| * argument, `addEntropy()` obtains 32 bytes from the platform CSPRNG; callers | ||
| * may instead supply their own non-empty entropy bytes. | ||
| * @param capacity - sponge capacity in bits. Accepted values are those that | ||
@@ -523,3 +528,6 @@ * keep `rho = 1598 - capacity` byte-aligned; the default `254` is chosen | ||
| * ```ts | ||
| * import { keccakprg } from '@noble/hashes/sha3-addons.js'; | ||
| * | ||
| * const prg = keccakprg(254); | ||
| * prg.addEntropy(); | ||
| * prg.randomBytes(8); | ||
@@ -526,0 +534,0 @@ * ``` |
+33
-2
@@ -13,3 +13,3 @@ /** | ||
| import { Keccak } from "./sha3.js"; | ||
| import { abytes, aexists, anumber, checkOpts, clean, copyBytes, createHasher, kdfInputToBytes, u32, } from "./utils.js"; | ||
| import { abytes, aexists, anumber, checkOpts, clean, copyBytes, createHasher, randomBytes, kdfInputToBytes, u32, } from "./utils.js"; | ||
| // cSHAKE && KMAC (NIST SP800-185) | ||
@@ -868,2 +868,3 @@ const _8n = /* @__PURE__ */ BigInt(8); | ||
| rate; | ||
| entropyAdded = false; | ||
| constructor(capacity) { | ||
@@ -899,4 +900,27 @@ anumber(capacity); | ||
| addEntropy(seed) { | ||
| this.update(seed); | ||
| // Check lifecycle before asking the system RNG for entropy that cannot be used. | ||
| aexists(this); | ||
| if (seed !== undefined) { | ||
| abytes(seed, undefined, 'seed'); | ||
| if (seed.length === 0) | ||
| throw new Error('"seed" must not be empty'); | ||
| } | ||
| const generated = seed === undefined; | ||
| const entropy = generated ? randomBytes() : seed; | ||
| try { | ||
| this.update(entropy); | ||
| this.entropyAdded = true; | ||
| } | ||
| finally { | ||
| // This temporary is library-owned; caller-provided entropy remains caller-owned. | ||
| if (generated) | ||
| clean(entropy); | ||
| } | ||
| } | ||
| xofInto(out) { | ||
| aexists(this, false); | ||
| if (!this.entropyAdded) | ||
| throw new Error('addEntropy() must be called before randomBytes()'); | ||
| return super.xofInto(out); | ||
| } | ||
| randomBytes(length) { | ||
@@ -924,2 +948,3 @@ return this.xof(length); | ||
| to.rate = rate; | ||
| to.entropyAdded = this.entropyAdded; | ||
| return to; | ||
@@ -934,2 +959,5 @@ } | ||
| * See {@link https://keccak.team/files/CSF-0.1.pdf}. | ||
| * Fresh instances reject output until `.addEntropy()` has been called. With no | ||
| * argument, `addEntropy()` obtains 32 bytes from the platform CSPRNG; callers | ||
| * may instead supply their own non-empty entropy bytes. | ||
| * @param capacity - sponge capacity in bits. Accepted values are those that | ||
@@ -943,3 +971,6 @@ * keep `rho = 1598 - capacity` byte-aligned; the default `254` is chosen | ||
| * ```ts | ||
| * import { keccakprg } from '@noble/hashes/sha3-addons.js'; | ||
| * | ||
| * const prg = keccakprg(254); | ||
| * prg.addEntropy(); | ||
| * prg.randomBytes(8); | ||
@@ -946,0 +977,0 @@ * ``` |
+1
-1
@@ -104,3 +104,3 @@ /** | ||
| let processed = false; | ||
| for (let pos = 0; pos < len; ) { | ||
| for (let pos = 0; pos < len;) { | ||
| const take = Math.min(blockLen - this.pos, len - pos); | ||
@@ -107,0 +107,0 @@ // Fast path only when there is no buffered partial block: `take === blockLen` implies |
+181
-141
@@ -11,3 +11,2 @@ /** | ||
| */ | ||
| import { rotr32H, rotr32L, rotrBH, rotrBL, rotrSH, rotrSL } from './_u64.ts'; | ||
| import { blake2b } from './blake2.ts'; | ||
@@ -43,29 +42,2 @@ import { | ||
| // Unsigned `u32 * u32 = { h, l }`, returned as split 64-bit halves. | ||
| function mul(a: number, b: number) { | ||
| // Split into 16-bit limbs so each partial product stays exact under `Math.imul`. | ||
| const aL = a & 0xffff; | ||
| const aH = a >>> 16; | ||
| const bL = b & 0xffff; | ||
| const bH = b >>> 16; | ||
| const ll = Math.imul(aL, bL); | ||
| const hl = Math.imul(aH, bL); | ||
| const lh = Math.imul(aL, bH); | ||
| const hh = Math.imul(aH, bH); | ||
| const carry = (ll >>> 16) + (hl & 0xffff) + lh; | ||
| const high = (hh + (hl >>> 16) + (carry >>> 16)) | 0; | ||
| const low = (carry << 16) | (ll & 0xffff); | ||
| return { h: high, l: low }; | ||
| } | ||
| // High 32 bits of unsigned u32 multiply, via the same 16-bit limb split as `mul` below. | ||
| // Kept single-purpose and number-returning so V8 inlines it (object-returning | ||
| // helpers here cost 2.2x of the whole derivation, measured; small helpers | ||
| // returning one number are free — see rotr* usage everywhere). | ||
| function mulHi(a: number, b: number): number { | ||
| const aL = a & 0xffff, aH = a >>> 16, bL = b & 0xffff, bH = b >>> 16; // prettier-ignore | ||
| const carry = (Math.imul(aL, bL) >>> 16) + (Math.imul(aH, bL) & 0xffff) + Math.imul(aL, bH); | ||
| return (Math.imul(aH, bH) + (Math.imul(aH, bL) >>> 16) + (carry >>> 16)) | 0; | ||
| } | ||
| // Temporary block buffer. | ||
@@ -77,5 +49,7 @@ // 1024-byte block: 256 u32 = 128 interleaved low/high halves = RFC's | ||
| // Quarter-round over 64-bit word indices into `A2_BUF`; each index maps to adjacent low/high u32s. | ||
| // Each BlaMka step `X = X + Y + 2 * trunc(X) * trunc(Y)` (trunc = low 32 bits) is three lines: | ||
| // `Math.imul` is the low product half, `mulHi` the high half, then a split 64-bit add with the | ||
| // doubling folded in. RFC 9106 Figure 19 GB rotates by 32, 24, 16, and 63 bits after each XOR. | ||
| // Each BlaMka step `X = X + Y + 2 * trunc(X) * trunc(Y)` (trunc = low 32 bits) starts | ||
| // with an exact low product from `Math.imul`. The rounded double product is within 1024 of the | ||
| // exact u64 product; subtracting that exact low half and rounding to the nearest multiple of | ||
| // 2^32 therefore recovers the exact high half. RFC 9106 Figure 19 GB then rotates by 32, 24, | ||
| // 16, and 63 bits after each XOR. | ||
| function G(a: number, b: number, c: number, d: number) { | ||
@@ -90,3 +64,3 @@ let Al = A2_BUF[2*a], Ah = A2_BUF[2*a + 1]; // prettier-ignore | ||
| ml = Math.imul(Al, Bl); | ||
| mh = mulHi(Al, Bl); // prettier-ignore | ||
| mh = (((Al >>> 0) * (Bl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Al >>> 0) + (Bl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -97,8 +71,8 @@ Ah = (Ah + Bh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Dl ^ Al; // prettier-ignore | ||
| Dh = rotr32H(xh, xl); | ||
| Dl = rotr32L(xh, xl); // prettier-ignore | ||
| Dh = xl; | ||
| Dl = xh; // prettier-ignore | ||
| // C = blamka(C, D); B = rotr64(B ^ C, 24) | ||
| ml = Math.imul(Cl, Dl); | ||
| mh = mulHi(Cl, Dl); // prettier-ignore | ||
| mh = (((Cl >>> 0) * (Dl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Cl >>> 0) + (Dl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -109,8 +83,8 @@ Ch = (Ch + Dh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Bl ^ Cl; // prettier-ignore | ||
| Bh = rotrSH(xh, xl, 24); | ||
| Bl = rotrSL(xh, xl, 24); // prettier-ignore | ||
| Bh = (xh >>> 24) | (xl << 8); | ||
| Bl = (xh << 8) | (xl >>> 24); // prettier-ignore | ||
| // A = blamka(A, B); D = rotr64(D ^ A, 16) | ||
| ml = Math.imul(Al, Bl); | ||
| mh = mulHi(Al, Bl); // prettier-ignore | ||
| mh = (((Al >>> 0) * (Bl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Al >>> 0) + (Bl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -121,8 +95,8 @@ Ah = (Ah + Bh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Dl ^ Al; // prettier-ignore | ||
| Dh = rotrSH(xh, xl, 16); | ||
| Dl = rotrSL(xh, xl, 16); // prettier-ignore | ||
| Dh = (xh >>> 16) | (xl << 16); | ||
| Dl = (xh << 16) | (xl >>> 16); // prettier-ignore | ||
| // C = blamka(C, D); B = rotr64(B ^ C, 63) | ||
| ml = Math.imul(Cl, Dl); | ||
| mh = mulHi(Cl, Dl); // prettier-ignore | ||
| mh = (((Cl >>> 0) * (Dl >>> 0) - (ml >>> 0)) / 0x100000000 + 0.5) | 0; // prettier-ignore | ||
| rl = (Cl >>> 0) + (Dl >>> 0) + ((ml << 1) >>> 0); | ||
@@ -133,4 +107,4 @@ Ch = (Ch + Dh + ((mh << 1) | (ml >>> 31)) + ((rl / 0x100000000) | 0)) | 0; | ||
| xl = Bl ^ Cl; // prettier-ignore | ||
| Bh = rotrBH(xh, xl, 63); | ||
| Bl = rotrBL(xh, xl, 63); // prettier-ignore | ||
| Bh = (xh << 1) | (xl >>> 31); | ||
| Bl = (xh >>> 31) | (xl << 1); // prettier-ignore | ||
@@ -163,3 +137,17 @@ ((A2_BUF[2 * a] = Al), (A2_BUF[2 * a + 1] = Ah)); | ||
| function block(x: TArg<Uint32Array>, xPos: number, yPos: number, outPos: number, needXor: boolean) { | ||
| for (let i = 0; i < 256; i++) A2_BUF[i] = x[xPos + i] ^ x[yPos + i]; | ||
| // Stage R = X xor Y in the destination before permuting it. This avoids rereading both source | ||
| // blocks when folding R into the permuted scratch block below. | ||
| if (needXor) { | ||
| for (let i = 0; i < 256; i++) { | ||
| const r = x[xPos + i] ^ x[yPos + i]; | ||
| A2_BUF[i] = r; | ||
| x[outPos + i] ^= r; | ||
| } | ||
| } else { | ||
| for (let i = 0; i < 256; i++) { | ||
| const r = x[xPos + i] ^ x[yPos + i]; | ||
| A2_BUF[i] = r; | ||
| x[outPos + i] = r; | ||
| } | ||
| } | ||
| // rows (8 consecutive 16-register groups) | ||
@@ -183,4 +171,3 @@ for (let i = 0; i < 128; i += 16) { | ||
| // RFC 9106 step 6: passes after the first XOR the old destination block into the new G(X, Y). | ||
| if (needXor) for (let i = 0; i < 256; i++) x[outPos + i] ^= A2_BUF[i] ^ x[xPos + i] ^ x[yPos + i]; | ||
| else for (let i = 0; i < 256; i++) x[outPos + i] = A2_BUF[i] ^ x[xPos + i] ^ x[yPos + i]; | ||
| for (let i = 0; i < 256; i++) x[outPos + i] ^= A2_BUF[i]; | ||
| clean(A2_BUF); | ||
@@ -239,5 +226,8 @@ } | ||
| const startPos = r !== 0 && s !== ARGON2_SYNC_POINTS - 1 ? (s + 1) * segmentLen : 0; | ||
| // RFC 9106 Figure 13: `mul(randL, randL).h` is `floor(J_1^2 / 2^32)`, and the outer high-half | ||
| // multiply computes `floor(|W| * x / 2^32)` without floating-point math. | ||
| const rel = area - 1 - mul(area, mul(randL, randL).h).h; | ||
| // Use the same exact-high recovery as G. `areaHigh` is floor(|W| * J1^2/2^32 / 2^32). | ||
| const randLow = Math.imul(randL, randL); | ||
| const randHigh = (((randL >>> 0) * (randL >>> 0) - (randLow >>> 0)) / 0x100000000 + 0.5) | 0; | ||
| const areaLow = Math.imul(area, randHigh); | ||
| const areaHigh = (((area >>> 0) * (randHigh >>> 0) - (areaLow >>> 0)) / 0x100000000 + 0.5) | 0; | ||
| const rel = area - 1 - areaHigh; | ||
| return (startPos + rel) % laneLen; | ||
@@ -248,8 +238,8 @@ } | ||
| export type ArgonOpts = { | ||
| /** Time cost measured in iterations. */ | ||
| t: number; | ||
| /** Memory cost in kibibytes. */ | ||
| m: number; | ||
| /** Parallelization parameter. */ | ||
| p: number; | ||
| /** Time cost measured in iterations. Defaults to `3`. */ | ||
| t?: number; | ||
| /** Memory cost in kibibytes. Defaults to `1024 ** 2` (1 GiB). */ | ||
| m?: number; | ||
| /** Parallelization parameter. Defaults to `1`. */ | ||
| p?: number; | ||
| /** Argon2 version number. Defaults to `0x13`. */ | ||
@@ -265,3 +255,3 @@ version?: number; | ||
| asyncTick?: number; | ||
| /** Maximum temporary memory budget in bytes. */ | ||
| /** Maximum temporary memory budget in bytes. Defaults to 1 GiB. */ | ||
| maxmem?: number; | ||
@@ -277,2 +267,4 @@ /** | ||
| const maxUint32 = Math.pow(2, 32); | ||
| const ARGON2_DEFAULT_MEMORY = 1024 ** 2; // KiB: 1 GiB | ||
| const ARGON2_DEFAULT_MAXMEM = ARGON2_DEFAULT_MEMORY * 1024; | ||
| // Validate safe JS integers in `[0, 2^32 - 1]`. | ||
@@ -283,8 +275,11 @@ function isU32(num: number) { | ||
| function argon2Opts(opts: TArg<ArgonOpts>) { | ||
| function argon2Opts(opts: TArg<ArgonOpts> = {}) { | ||
| opts = checkOpts({}, opts); | ||
| const merged: any = { | ||
| t: 3, | ||
| m: ARGON2_DEFAULT_MEMORY, | ||
| p: 1, | ||
| version: 0x13, | ||
| dkLen: 32, | ||
| maxmem: maxUint32 - 1, | ||
| maxmem: ARGON2_DEFAULT_MAXMEM, | ||
| asyncTick: 10, | ||
@@ -317,3 +312,3 @@ }; | ||
| function argon2Init( | ||
| function argon2InitialHash( | ||
| password: TArg<KDFInput>, | ||
@@ -324,77 +319,116 @@ salt: TArg<KDFInput>, | ||
| ) { | ||
| password = kdfInputToBytes(password, 'password'); | ||
| salt = kdfInputToBytes(salt, 'salt'); | ||
| if (!isU32(password.length)) throw new Error('"password" must be less of length 1..4Gb'); | ||
| // RFC 9106 §3.1 only requires S <= 2^32-1 bytes and says 16 bytes is RECOMMENDED for password | ||
| // hashing; this library intentionally takes the stricter common >=8-byte salt path. | ||
| if (!isU32(salt.length) || salt.length < 8) throw new Error('"salt" must be of length 8..4Gb'); | ||
| if (!Object.values(AT).includes(type)) throw new Error('"type" was invalid'); | ||
| let { p, dkLen, m, t, version, key, personalization, maxmem, onProgress, asyncTick } = | ||
| argon2Opts(opts); | ||
| // Validation | ||
| key = abytesOrZero(key, 'key'); | ||
| personalization = abytesOrZero(personalization, 'personalization'); | ||
| // H_0 = H^(64)(LE32(p) || LE32(T) || LE32(m) || LE32(t) || | ||
| // LE32(v) || LE32(y) || LE32(length(P)) || P || | ||
| // LE32(length(S)) || S || LE32(length(K)) || K || | ||
| // LE32(length(X)) || X) | ||
| const h = blake2b.create(); | ||
| const ownedInputs: Uint8Array[] = []; | ||
| const BUF = new Uint32Array(1); | ||
| const BUF8 = u8(BUF); | ||
| for (let item of [p, dkLen, m, t, version, type]) { | ||
| // RFC 9106 H0 encodes these scalars as LE32, so normalize the host word before exposing bytes. | ||
| BUF[0] = swap8IfBE(item); | ||
| h.update(BUF8); | ||
| let h: ReturnType<typeof blake2b.create> | undefined; | ||
| let H0: Uint32Array | undefined; | ||
| let succeeded = false; | ||
| const rememberOwned = (input: unknown, bytes: TArg<TRet<Uint8Array>>): TRet<Uint8Array> => { | ||
| if (typeof input === 'string') ownedInputs.push(bytes); | ||
| return bytes as TRet<Uint8Array>; | ||
| }; | ||
| try { | ||
| const passwordBytes = rememberOwned(password, kdfInputToBytes(password, 'password')); | ||
| const saltBytes = rememberOwned(salt, kdfInputToBytes(salt, 'salt')); | ||
| if (!isU32(passwordBytes.length)) throw new Error('"password" must be less of length 1..4Gb'); | ||
| // RFC 9106 §3.1 only requires S <= 2^32-1 bytes and says 16 bytes is RECOMMENDED for password | ||
| // hashing; this library intentionally takes the stricter common >=8-byte salt path. | ||
| if (!isU32(saltBytes.length) || saltBytes.length < 8) | ||
| throw new Error('"salt" must be of length 8..4Gb'); | ||
| if (!Object.values(AT).includes(type)) throw new Error('"type" was invalid'); | ||
| let { p, dkLen, m, t, version, key, personalization, maxmem, onProgress, asyncTick } = | ||
| argon2Opts(opts); | ||
| // Validation | ||
| const keyInput = key; | ||
| key = rememberOwned(keyInput, abytesOrZero(keyInput, 'key')); | ||
| const personalizationInput = personalization; | ||
| personalization = rememberOwned( | ||
| personalizationInput, | ||
| abytesOrZero(personalizationInput, 'personalization') | ||
| ); | ||
| // H_0 = H^(64)(LE32(p) || LE32(T) || LE32(m) || LE32(t) || | ||
| // LE32(v) || LE32(y) || LE32(length(P)) || P || | ||
| // LE32(length(S)) || S || LE32(length(K)) || K || | ||
| // LE32(length(X)) || X) | ||
| h = blake2b.create(); | ||
| for (let item of [p, dkLen, m, t, version, type]) { | ||
| // RFC 9106 H0 encodes these scalars as LE32, so normalize the host word before | ||
| // exposing bytes. | ||
| BUF[0] = swap8IfBE(item); | ||
| h.update(BUF8); | ||
| } | ||
| for (let i of [passwordBytes, saltBytes, key, personalization]) { | ||
| BUF[0] = swap8IfBE(i.length); // BUF is u32 array, this is valid once normalized to LE bytes | ||
| h.update(BUF8).update(i); | ||
| } | ||
| // Reserve two extra LE32 words after the 64-byte `H_0` so Figures 3-4 can append | ||
| // `LE32(0 or 1) || LE32(i)` in place for the lane-starting blocks. | ||
| H0 = new Uint32Array(18); | ||
| h.digestInto(u8(H0)); | ||
| succeeded = true; | ||
| return { H0, p, dkLen, m, t, version, maxmem, onProgress, asyncTick }; | ||
| } finally { | ||
| // digestInto() does not destroy BLAKE2 state. It and all string-derived inputs contain secrets. | ||
| if (h) h.destroy(); | ||
| clean(BUF, ...ownedInputs); | ||
| if (!succeeded && H0) clean(H0); | ||
| } | ||
| for (let i of [password, salt, key, personalization]) { | ||
| BUF[0] = swap8IfBE(i.length); // BUF is u32 array, this is valid once normalized to LE bytes | ||
| h.update(BUF8).update(i); | ||
| } | ||
| // Reserve two extra LE32 words after the 64-byte `H_0` so Figures 3-4 can append | ||
| // `LE32(0 or 1) || LE32(i)` in place for the lane-starting blocks. | ||
| const H0 = new Uint32Array(18); | ||
| const H0_8 = u8(H0); | ||
| h.digestInto(H0_8); | ||
| } | ||
| function argon2Init( | ||
| password: TArg<KDFInput>, | ||
| salt: TArg<KDFInput>, | ||
| type: Types, | ||
| opts: TArg<ArgonOpts> | ||
| ) { | ||
| const { H0, p, dkLen, m, t, version, maxmem, onProgress, asyncTick } = argon2InitialHash( | ||
| password, | ||
| salt, | ||
| type, | ||
| opts | ||
| ); | ||
| // 256 u32 = 1024 (BLOCK_SIZE), fills A2_BUF on processing | ||
| // Params | ||
| const lanes = p; | ||
| // m' = 4 * p * floor (m / 4p) | ||
| const mP = 4 * p * Math.floor(m / (ARGON2_SYNC_POINTS * p)); | ||
| //q = m' / p columns | ||
| const laneLen = Math.floor(mP / p); | ||
| const segmentLen = Math.floor(laneLen / ARGON2_SYNC_POINTS); | ||
| // `maxmem` is documented in bytes; compare against the actual 1024-byte block allocation. | ||
| const memUsed = mP * 1024; | ||
| if (!isU32(maxmem)) throw new Error('"maxmem" expected <2**32, got ' + maxmem); | ||
| if (memUsed > maxmem) | ||
| throw new Error('"maxmem" limit was hit: memUsed(mP*1024)=' + memUsed + ', maxmem=' + maxmem); | ||
| const B = new Uint32Array(memUsed / 4); | ||
| // Fill first blocks | ||
| for (let l = 0; l < p; l++) { | ||
| const i = 256 * laneLen * l; | ||
| // B[i][0] = H'^(1024)(H_0 || LE32(0) || LE32(i)) | ||
| H0[17] = swap8IfBE(l); | ||
| H0[16] = swap8IfBE(0); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i); | ||
| // B[i][1] = H'^(1024)(H_0 || LE32(1) || LE32(i)) | ||
| H0[16] = swap8IfBE(1); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i + 256); | ||
| try { | ||
| // Params | ||
| const lanes = p; | ||
| // m' = 4 * p * floor (m / 4p) | ||
| const mP = 4 * p * Math.floor(m / (ARGON2_SYNC_POINTS * p)); | ||
| //q = m' / p columns | ||
| const laneLen = Math.floor(mP / p); | ||
| const segmentLen = Math.floor(laneLen / ARGON2_SYNC_POINTS); | ||
| // `maxmem` is documented in bytes; compare against the actual 1024-byte block allocation. | ||
| const memUsed = mP * 1024; | ||
| if (!isU32(maxmem)) throw new Error('"maxmem" expected <2**32, got ' + maxmem); | ||
| if (memUsed > maxmem) | ||
| throw new Error('"maxmem" limit was hit: memUsed(mP*1024)=' + memUsed + ', maxmem=' + maxmem); | ||
| const B = new Uint32Array(memUsed / 4); | ||
| // Fill first blocks | ||
| for (let l = 0; l < p; l++) { | ||
| const i = 256 * laneLen * l; | ||
| // B[i][0] = H'^(1024)(H_0 || LE32(0) || LE32(i)) | ||
| H0[17] = swap8IfBE(l); | ||
| H0[16] = swap8IfBE(0); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i); | ||
| // B[i][1] = H'^(1024)(H_0 || LE32(1) || LE32(i)) | ||
| H0[16] = swap8IfBE(1); | ||
| B.set(swap32IfBE(u32(Hp(H0, 1024))), i + 256); | ||
| } | ||
| let perBlock = () => {}; | ||
| if (onProgress) { | ||
| // The first segment of the first pass skips two preinitialized blocks per lane. | ||
| const totalBlock = t * ARGON2_SYNC_POINTS * p * segmentLen - 2 * p; | ||
| // Invoke callback if progress changes from 10.01 to 10.02 | ||
| // Allows to draw smooth progress bar on up to 8K screen | ||
| const callbackPer = Math.max(Math.floor(totalBlock / 10000), 1); | ||
| let blockCnt = 0; | ||
| perBlock = () => { | ||
| blockCnt++; | ||
| if (onProgress && (!(blockCnt % callbackPer) || blockCnt === totalBlock)) | ||
| onProgress(blockCnt / totalBlock); | ||
| }; | ||
| } | ||
| return { type, mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock, asyncTick }; | ||
| } finally { | ||
| clean(H0); | ||
| } | ||
| let perBlock = () => {}; | ||
| if (onProgress) { | ||
| // The first segment of the first pass skips two preinitialized blocks per lane. | ||
| const totalBlock = t * ARGON2_SYNC_POINTS * p * segmentLen - 2 * p; | ||
| // Invoke callback if progress changes from 10.01 to 10.02 | ||
| // Allows to draw smooth progress bar on up to 8K screen | ||
| const callbackPer = Math.max(Math.floor(totalBlock / 10000), 1); | ||
| let blockCnt = 0; | ||
| perBlock = () => { | ||
| blockCnt++; | ||
| if (onProgress && (!(blockCnt % callbackPer) || blockCnt === totalBlock)) | ||
| onProgress(blockCnt / totalBlock); | ||
| }; | ||
| } | ||
| clean(BUF, H0); | ||
| return { type, mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock, asyncTick }; | ||
| } | ||
@@ -425,6 +459,8 @@ | ||
| */ | ||
| function* argon2Blocks(ctx: ReturnType<typeof argon2Init>): Generator<void, void> { | ||
| function* argon2Blocks( | ||
| ctx: ReturnType<typeof argon2Init>, | ||
| address: TArg<Uint32Array> | ||
| ): Generator<void, void> { | ||
| const { type, mP, p, t, version, B, laneLen, lanes, segmentLen, perBlock } = ctx; | ||
| // [address, input, zero_block] format so we can pass single U32 to block function | ||
| const address = new Uint32Array(3 * 256); | ||
| address[256 + 6] = mP; | ||
@@ -501,3 +537,3 @@ address[256 + 8] = t; | ||
| const ctx = argon2Init(password, salt, type, opts); | ||
| const blocks = argon2Blocks(ctx); | ||
| const blocks = argon2Blocks(ctx, new Uint32Array(3 * 256)); | ||
| while (!blocks.next().done) {} | ||
@@ -542,3 +578,3 @@ return argon2Output(ctx.B, ctx.p, ctx.laneLen, ctx.dkLen); | ||
| salt: TArg<KDFInput>, | ||
| opts: TArg<ArgonOpts> | ||
| opts: TArg<ArgonOpts> = {} | ||
| ): TRet<Uint8Array> => argon2(AT.Argon2d, password, salt, opts); | ||
@@ -561,3 +597,3 @@ /** | ||
| salt: TArg<KDFInput>, | ||
| opts: TArg<ArgonOpts> | ||
| opts: TArg<ArgonOpts> = {} | ||
| ): TRet<Uint8Array> => argon2(AT.Argon2i, password, salt, opts); | ||
@@ -580,3 +616,3 @@ /** | ||
| salt: TArg<KDFInput>, | ||
| opts: TArg<ArgonOpts> | ||
| opts: TArg<ArgonOpts> = {} | ||
| ): TRet<Uint8Array> => argon2(AT.Argon2id, password, salt, opts); | ||
@@ -591,3 +627,6 @@ | ||
| const ctx = argon2Init(password, salt, type, opts); | ||
| const blocks = argon2Blocks(ctx); | ||
| // Keep the generator-local address block reachable so an aborted scheduler yield can wipe it. | ||
| const address = new Uint32Array(3 * 256); | ||
| const blocks = argon2Blocks(ctx, address); | ||
| const abort = () => clean(address, ctx.B); | ||
| let ts = Date.now(); | ||
@@ -599,4 +638,5 @@ while (!blocks.next().done) { | ||
| if (diff >= 0 && diff < ctx.asyncTick) continue; | ||
| await nextTick(); | ||
| ts += diff; | ||
| await nextTick(abort); | ||
| // Scheduler delay is outside the synchronous work budget. | ||
| ts = Date.now(); | ||
| } | ||
@@ -640,3 +680,3 @@ return argon2Output(ctx.B, ctx.p, ctx.laneLen, ctx.dkLen); | ||
| salt: TArg<KDFInput>, | ||
| opts: TArg<ArgonOpts> | ||
| opts: TArg<ArgonOpts> = {} | ||
| ): Promise<TRet<Uint8Array>> => argon2Async(AT.Argon2d, password, salt, opts); | ||
@@ -659,3 +699,3 @@ /** | ||
| salt: TArg<KDFInput>, | ||
| opts: TArg<ArgonOpts> | ||
| opts: TArg<ArgonOpts> = {} | ||
| ): Promise<TRet<Uint8Array>> => argon2Async(AT.Argon2i, password, salt, opts); | ||
@@ -678,3 +718,3 @@ /** | ||
| salt: TArg<KDFInput>, | ||
| opts: TArg<ArgonOpts> | ||
| opts: TArg<ArgonOpts> = {} | ||
| ): Promise<TRet<Uint8Array>> => argon2Async(AT.Argon2id, password, salt, opts); |
+4
-3
@@ -55,4 +55,2 @@ /** | ||
| // Base destroy logic only clears salt-derived state; the partial message buffer and length/position | ||
| // bookkeeping remain until the instance or backing buffer is reused. | ||
| abstract class BLAKE1<T extends BLAKE1<T>> implements Hash<T> { | ||
@@ -122,3 +120,3 @@ readonly canXOF = false; | ||
| let dataView; | ||
| for (let pos = 0; pos < len; ) { | ||
| for (let pos = 0; pos < len;) { | ||
| const take = Math.min(blockLen - this.pos, len - pos); | ||
@@ -150,2 +148,5 @@ // Fast path only when there is no buffered partial block: `take === blockLen` implies | ||
| this.destroyed = true; | ||
| clean(this.buffer); | ||
| this.length = 0; | ||
| this.pos = 0; | ||
| if (this.salt !== EMPTY_SALT) { | ||
@@ -152,0 +153,0 @@ clean(this.salt, this.constants); |
+1
-1
@@ -174,3 +174,3 @@ /** | ||
| const buf = data.buffer; | ||
| for (let pos = 0; pos < len; ) { | ||
| for (let pos = 0; pos < len;) { | ||
| // If buffer is full and we still have input (don't process last block, same as blake2s) | ||
@@ -177,0 +177,0 @@ if (this.pos === blockLen) { |
+8
-2
@@ -170,3 +170,9 @@ /** | ||
| // 4 (100) - leafs finished at depth=1 and depth=2 | ||
| for (let last, chunks = this.chunksDone + 1; isLast || !(chunks & 1); chunks >>= 1) { | ||
| // Reading the low bit is safe for parity, but keep division full-width: shifting would | ||
| // truncate the safe-integer chunk counter to 32 bits. | ||
| for ( | ||
| let last, chunks = this.chunksDone + 1; | ||
| isLast || !(chunks & 1); | ||
| chunks = Math.floor(chunks / 2) | ||
| ) { | ||
| if (!(last = this.stack.pop())) break; | ||
@@ -286,3 +292,3 @@ this.buffer32.set(last, 0); | ||
| const { blockLen, bufferOut } = this; | ||
| for (let pos = 0, len = out.length; pos < len; ) { | ||
| for (let pos = 0, len = out.length; pos < len;) { | ||
| if (this.posOut >= blockLen) this.b2CompressOut(); | ||
@@ -289,0 +295,0 @@ const take = Math.min(blockLen - this.posOut, len - pos); |
+31
-12
@@ -51,10 +51,19 @@ /** | ||
| const p = kdfInputToBytes(_password, 'password'); | ||
| const s = kdfInputToBytes(_salt, 'salt'); | ||
| // DK = PBKDF2(PRF, Password, Salt, c, dkLen); | ||
| const DK = new Uint8Array(dkLen); | ||
| const { iHash, oHash, outputLen } = hmac.create(hash, p); | ||
| // Drive keyed hashes directly; the wrapper is only needed to initialize their HMAC midstates. | ||
| const u = new Uint8Array(outputLen); | ||
| const eng = pbkdf2Engine(iHash, oHash, s, u); | ||
| return { c, dkLen, asyncTick, DK, outputLen, eng }; | ||
| try { | ||
| const s = kdfInputToBytes(_salt, 'salt'); | ||
| try { | ||
| // DK = PBKDF2(PRF, Password, Salt, c, dkLen); | ||
| const DK = new Uint8Array(dkLen); | ||
| const { iHash, oHash, outputLen } = hmac.create(hash, p); | ||
| // Drive keyed hashes directly; the wrapper is only needed to initialize their HMAC midstates. | ||
| const u = new Uint8Array(outputLen); | ||
| const eng = pbkdf2Engine(iHash, oHash, s, u); | ||
| return { c, dkLen, asyncTick, DK, outputLen, eng }; | ||
| } finally { | ||
| // Uint8Array inputs belong to the caller; only wipe our UTF-8 conversion. | ||
| if (typeof _salt === 'string') clean(s); | ||
| } | ||
| } finally { | ||
| if (typeof _password === 'string') clean(p); | ||
| } | ||
| } | ||
@@ -186,2 +195,7 @@ | ||
| const { c, dkLen, asyncTick, DK, outputLen, eng } = pbkdf2Init(hash, password, salt, opts); | ||
| // Reuse normal state destruction, then wipe the incomplete output if a host yield aborts. | ||
| const abort = () => { | ||
| eng.output(DK); | ||
| clean(DK); | ||
| }; | ||
| // DK = T1 + T2 + ⋯ + Tdklen/hlen | ||
@@ -196,8 +210,13 @@ for (let ti = 1, pos = 0; pos < dkLen; ti++, pos += outputLen) { | ||
| eng.u1(ti, Ti); | ||
| await asyncLoop(c - 1, asyncTick, () => { | ||
| // Uc = PRF(Password, Uc−1) | ||
| eng.rounds(2, Ti); // c=2 runs exactly one PRF iteration per callback. | ||
| }); | ||
| await asyncLoop( | ||
| c - 1, | ||
| asyncTick, | ||
| () => { | ||
| // Uc = PRF(Password, Uc−1) | ||
| eng.rounds(2, Ti); // c=2 runs exactly one PRF iteration per callback. | ||
| }, | ||
| abort | ||
| ); | ||
| } | ||
| return eng.output(DK); | ||
| } |
+35
-20
@@ -99,2 +99,5 @@ /** | ||
| // 128*r*(N+p+1) for N=2**20, r=8, p=1: 1 GiB main table plus 2 KiB workspace. | ||
| const SCRYPT_DEFAULT_MAXMEM = 128 * 8 * (2 ** 20 + 1 + 1); | ||
| /** | ||
@@ -107,3 +110,4 @@ * Scrypt options: | ||
| * - `asyncTick` - (default: 10) max time in ms for which async function can block execution | ||
| * - `maxmem` - (default: `1024 ** 3 + 1024` aka 1GB+1KB). A limit that the app could use for scrypt | ||
| * - `maxmem` - (default: `1024 ** 3 + 2 * 1024` aka 1GiB+2KiB). A limit that the app | ||
| * could use for scrypt | ||
| * - `onProgress` - callback function that would be executed for progress report | ||
@@ -135,3 +139,2 @@ */ | ||
| function scryptInit(password: TArg<KDFInput>, salt: TArg<KDFInput>, _opts?: ScryptOpts) { | ||
| // Maxmem - 1GB+1KB by default | ||
| const opts = checkOpts( | ||
@@ -141,3 +144,3 @@ { | ||
| asyncTick: 10, | ||
| maxmem: 1024 ** 3 + 1024, | ||
| maxmem: SCRYPT_DEFAULT_MAXMEM, | ||
| }, | ||
@@ -337,2 +340,4 @@ _opts | ||
| ); | ||
| // One failure handler covers both yield boundaries without putting the hot loops in a try block. | ||
| const abort = () => clean(B, V, tmp); | ||
| swap32IfBE(B32); | ||
@@ -343,21 +348,31 @@ for (let pi = 0; pi < p; pi++) { | ||
| let pos = 0; | ||
| await asyncLoop(N - 1, asyncTick, () => { | ||
| BlockMix(V, pos, V, (pos += blockSize32), r); // V[i] = BlockMix(V[i-1]); | ||
| blockMixCb(); | ||
| }); | ||
| await asyncLoop( | ||
| N - 1, | ||
| asyncTick, | ||
| () => { | ||
| BlockMix(V, pos, V, (pos += blockSize32), r); // V[i] = BlockMix(V[i-1]); | ||
| blockMixCb(); | ||
| }, | ||
| abort | ||
| ); | ||
| BlockMix(V, (N - 1) * blockSize32, B32, Pi, r); // Process last element | ||
| blockMixCb(); | ||
| await asyncLoop(N, asyncTick, () => { | ||
| // First u32 of the last 64-byte block (u32 is LE) | ||
| // RFC 7914 Integerify(X) uses the whole last 64-byte block, but mod N | ||
| // only depends on the low word here because N is a power of two and | ||
| // this implementation caps N at 2^32. | ||
| // & (N - 1) is % N as N is a power of 2, N & (N - 1) = 0 is checked | ||
| // above; >>> 0 for unsigned, input fits in u32. | ||
| const j = (B32[Pi + blockSize32 - 16] & (N - 1)) >>> 0; // j = Integrify(X) % iterations | ||
| // tmp = B ^ V[j] | ||
| for (let k = 0; k < blockSize32; k++) tmp[k] = B32[Pi + k] ^ V[j * blockSize32 + k]; | ||
| BlockMix(tmp, 0, B32, Pi, r); // B = BlockMix(B ^ V[j]) | ||
| blockMixCb(); | ||
| }); | ||
| await asyncLoop( | ||
| N, | ||
| asyncTick, | ||
| () => { | ||
| // First u32 of the last 64-byte block (u32 is LE) | ||
| // RFC 7914 Integerify(X) uses the whole last 64-byte block, but mod N | ||
| // only depends on the low word here because N is a power of two and | ||
| // this implementation caps N at 2^32. | ||
| // & (N - 1) is % N as N is a power of 2, N & (N - 1) = 0 is checked | ||
| // above; >>> 0 for unsigned, input fits in u32. | ||
| const j = (B32[Pi + blockSize32 - 16] & (N - 1)) >>> 0; // j = Integrify(X) % iterations | ||
| // tmp = B ^ V[j] | ||
| for (let k = 0; k < blockSize32; k++) tmp[k] = B32[Pi + k] ^ V[j * blockSize32 + k]; | ||
| BlockMix(tmp, 0, B32, Pi, r); // B = BlockMix(B ^ V[j]) | ||
| blockMixCb(); | ||
| }, | ||
| abort | ||
| ); | ||
| } | ||
@@ -364,0 +379,0 @@ swap32IfBE(B32); |
+32
-4
@@ -23,2 +23,3 @@ /** | ||
| createHasher, | ||
| randomBytes, | ||
| type Hash, | ||
@@ -526,3 +527,3 @@ type HashXOF, | ||
| const { chunkLen, leafCons } = this; | ||
| for (let pos = 0, len = data.length; pos < len; ) { | ||
| for (let pos = 0, len = data.length; pos < len;) { | ||
| if (this.chunkPos == chunkLen || !this.leafHash) { | ||
@@ -838,3 +839,3 @@ if (this.leafHash) this.flushLeaf(); | ||
| const { chunkLen, blockLen, leafLen, rounds } = this; | ||
| for (let pos = 0, len = data.length; pos < len; ) { | ||
| for (let pos = 0, len = data.length; pos < len;) { | ||
| if (this.chunkPos == chunkLen) { | ||
@@ -1058,2 +1059,3 @@ if (this.leafHash) this.flushLeaf(); | ||
| protected rate: number; | ||
| private entropyAdded = false; | ||
| constructor(capacity: number) { | ||
@@ -1087,5 +1089,24 @@ anumber(capacity); | ||
| } | ||
| addEntropy(seed: TArg<Uint8Array>): void { | ||
| this.update(seed); | ||
| addEntropy(seed?: TArg<Uint8Array>): void { | ||
| // Check lifecycle before asking the system RNG for entropy that cannot be used. | ||
| aexists(this); | ||
| if (seed !== undefined) { | ||
| abytes(seed, undefined, 'seed'); | ||
| if (seed.length === 0) throw new Error('"seed" must not be empty'); | ||
| } | ||
| const generated = seed === undefined; | ||
| const entropy = generated ? randomBytes() : seed; | ||
| try { | ||
| this.update(entropy); | ||
| this.entropyAdded = true; | ||
| } finally { | ||
| // This temporary is library-owned; caller-provided entropy remains caller-owned. | ||
| if (generated) clean(entropy); | ||
| } | ||
| } | ||
| xofInto(out: TArg<Uint8Array>): TRet<Uint8Array> { | ||
| aexists(this, false); | ||
| if (!this.entropyAdded) throw new Error('addEntropy() must be called before randomBytes()'); | ||
| return super.xofInto(out); | ||
| } | ||
| randomBytes(length: number): TRet<Uint8Array> { | ||
@@ -1111,2 +1132,3 @@ return this.xof(length); | ||
| to.rate = rate; | ||
| to.entropyAdded = this.entropyAdded; | ||
| return to; | ||
@@ -1122,2 +1144,5 @@ } | ||
| * See {@link https://keccak.team/files/CSF-0.1.pdf}. | ||
| * Fresh instances reject output until `.addEntropy()` has been called. With no | ||
| * argument, `addEntropy()` obtains 32 bytes from the platform CSPRNG; callers | ||
| * may instead supply their own non-empty entropy bytes. | ||
| * @param capacity - sponge capacity in bits. Accepted values are those that | ||
@@ -1131,3 +1156,6 @@ * keep `rho = 1598 - capacity` byte-aligned; the default `254` is chosen | ||
| * ```ts | ||
| * import { keccakprg } from '@noble/hashes/sha3-addons.js'; | ||
| * | ||
| * const prg = keccakprg(254); | ||
| * prg.addEntropy(); | ||
| * prg.randomBytes(8); | ||
@@ -1134,0 +1162,0 @@ * ``` |
+2
-2
@@ -235,3 +235,3 @@ /** | ||
| const data32 = canUseU32 && len >= blockLen ? u32(data) : undefined; | ||
| for (let pos = 0; pos < len; ) { | ||
| for (let pos = 0; pos < len;) { | ||
| if (data32 !== undefined && this.pos === 0 && pos % 4 === 0 && len - pos >= blockLen) { | ||
@@ -273,3 +273,3 @@ for (let i = 0, o = pos / 4; i < blockLen32; i++) state32[i] ^= data32[o + i]; | ||
| const { blockLen } = this; | ||
| for (let pos = 0, len = out.length; pos < len; ) { | ||
| for (let pos = 0, len = out.length; pos < len;) { | ||
| if (this.posOut >= blockLen) this.keccak(); | ||
@@ -276,0 +276,0 @@ const take = Math.min(blockLen - this.posOut, len - pos); |
+49
-16
@@ -260,2 +260,12 @@ /** | ||
| const aopts = (value: Record<string, any>, label: string) => { | ||
| aobject(value, label); | ||
| const proto = Object.getPrototypeOf(value); | ||
| if (proto !== Object.prototype && proto !== null) | ||
| throw new TypeError(`"${label}" expected plain object`); | ||
| // Object.assign() treats an own "__proto__" source key as a write to the target's legacy | ||
| // prototype setter. Reject it before merging so inherited option values cannot be injected. | ||
| if (Object.hasOwn(value, '__proto__')) throw new TypeError(`"${label}.__proto__" is not allowed`); | ||
| }; | ||
| /** | ||
@@ -560,5 +570,8 @@ * Asserts a hash instance has not been destroyed or finished. | ||
| /** | ||
| * There is no setImmediate in browser and setTimeout is slow. | ||
| * This yields to the Promise/microtask scheduler queue, not to timers or the | ||
| * full macrotask event loop. | ||
| * Yields to the host task scheduler so timers, I/O, and rendering can make progress. | ||
| * Uses the Web Scheduling API when available or `setTimeout` as a cross-platform fallback. | ||
| * Host-task yields are much slower than microtasks (roughly 1ms with the timer fallback), so | ||
| * async loops should use `asyncTick >= 10` to amortize scheduling overhead to about 10% while | ||
| * still allowing other event-loop work to proceed. | ||
| * @param onReject - optional cleanup invoked only if the host yield fails | ||
| * @example | ||
@@ -570,10 +583,19 @@ * Yield to the next scheduler tick. | ||
| */ | ||
| export const nextTick = async (): Promise<void> => {}; | ||
| export function nextTick(onReject?: () => void): Promise<void> { | ||
| const host = globalThis as any; | ||
| if (typeof host.scheduler?.yield === 'function') { | ||
| const promise: Promise<void> = host.scheduler.yield(); | ||
| // Keep the original scheduler rejection; this handler exists only for cleanup. | ||
| if (onReject) promise.catch(onReject); | ||
| return promise; | ||
| } | ||
| return new Promise((resolve) => host.setTimeout(resolve, 0)); | ||
| } | ||
| /** | ||
| * Returns control to the Promise/microtask scheduler every `tick` | ||
| * milliseconds to avoid blocking long loops. | ||
| * Returns control to the host event loop every `tick` milliseconds to avoid blocking long loops. | ||
| * @param iters - number of loop iterations to run | ||
| * @param tick - maximum time slice in milliseconds | ||
| * @param cb - callback executed on each iteration | ||
| * @param onReject - optional cleanup invoked only if a host yield fails | ||
| * @throws On wrong argument types. {@link TypeError} | ||
@@ -590,3 +612,4 @@ * @throws On wrong argument ranges or values. {@link RangeError} | ||
| tick: number, | ||
| cb: (i: number) => void | ||
| cb: (i: number) => void, | ||
| onReject?: () => void | ||
| ): Promise<void> { | ||
@@ -603,5 +626,5 @@ anumber(iters, 'iters'); | ||
| if (diff >= 0 && diff < tick) continue; | ||
| await nextTick(); | ||
| await nextTick(onReject); | ||
| // Track only synchronous work time; scheduler delay after yielding is outside our budget. | ||
| ts += diff; | ||
| ts = Date.now(); | ||
| } | ||
@@ -628,3 +651,10 @@ } | ||
| if (typeof str !== 'string') throw new TypeError('string expected'); | ||
| return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809 | ||
| const encoded = new TextEncoder().encode(str); | ||
| try { | ||
| // Copy into the current realm for Firefox extension contexts. Callers that own the returned | ||
| // buffer can then wipe it independently of TextEncoder's temporary result. | ||
| return new Uint8Array(encoded) as TRet<Uint8Array>; // https://bugzil.la/1681809 | ||
| } finally { | ||
| clean(encoded); | ||
| } | ||
| } | ||
@@ -735,3 +765,3 @@ | ||
| * @param title - label included in thrown override errors | ||
| * @returns Merged option object. The merge mutates `defaults` in place. | ||
| * @returns Fresh merged option object with a null prototype. | ||
| * @throws On wrong argument types. {@link TypeError} | ||
@@ -749,5 +779,7 @@ * @example | ||
| ): T1 & T2 { | ||
| aobject(defaults as Record<string, any>, 'defaults'); | ||
| if (opts !== undefined) aobject(opts as Record<string, any>, title); | ||
| const merged = Object.assign(defaults, opts); | ||
| aopts(defaults as Record<string, any>, 'defaults'); | ||
| if (opts !== undefined) aopts(opts as Record<string, any>, title); | ||
| // Callers read optional fields directly, so omitted values must not fall through to ambient | ||
| // Object.prototype pollution (for example a forged `dkLen` changing SHAKE's default output). | ||
| const merged = Object.assign(Object.create(null), defaults, opts); | ||
| return merged as T1 & T2; | ||
@@ -805,5 +837,6 @@ } | ||
| * Mixes fresh entropy into the current generator state. | ||
| * @param seed - Entropy bytes to absorb. | ||
| * @param seed - Non-empty entropy bytes to absorb. When omitted, the implementation uses its | ||
| * system RNG. | ||
| */ | ||
| addEntropy(seed: TArg<Uint8Array>): void; | ||
| addEntropy(seed?: TArg<Uint8Array>): void; | ||
| /** | ||
@@ -810,0 +843,0 @@ * Produces a requested number of pseudorandom bytes. |
+75
-34
@@ -7,2 +7,4 @@ import { type Pbkdf2Opt } from './pbkdf2.ts'; | ||
| checkOpts, | ||
| clean, | ||
| copyBytes, | ||
| kdfInputToBytes, | ||
@@ -57,5 +59,7 @@ type CHash, | ||
| function ahashWeb(hash: TArg<WebHash>) { | ||
| function ahashWeb(hash: TArg<WebHash>): string { | ||
| ahash(hash as unknown as TArg<CHash>); | ||
| if (typeof hash.webCryptoName !== 'string') throw new Error('non-web hash'); | ||
| const name = hash.webCryptoName; | ||
| if (typeof name !== 'string') throw new Error('non-web hash'); | ||
| return name; | ||
| } | ||
@@ -135,15 +139,22 @@ | ||
| abytes(message, undefined, 'message'); | ||
| ahashWeb(hash); | ||
| // WebCrypto keys can't be zeroized | ||
| // prettier-ignore | ||
| const wkey = await crypto.importKey( | ||
| 'raw', | ||
| key as BufferSource, | ||
| { name: 'HMAC', hash: hash.webCryptoName }, | ||
| false, | ||
| ['sign'] | ||
| ); | ||
| return new Uint8Array( | ||
| await crypto.sign('HMAC', wkey, message as BufferSource) | ||
| ) as TRet<Uint8Array>; | ||
| const hashName = ahashWeb(hash); | ||
| // importKey() snapshots key synchronously, but message is not passed to sign() until after | ||
| // importKey() resolves. Keep the wrapper's inputs stable across that await. | ||
| const _message = copyBytes(message); | ||
| try { | ||
| // WebCrypto keys can't be zeroized | ||
| // prettier-ignore | ||
| const wkey = await crypto.importKey( | ||
| 'raw', | ||
| key as BufferSource, | ||
| { name: 'HMAC', hash: hashName }, | ||
| false, | ||
| ['sign'] | ||
| ); | ||
| return new Uint8Array( | ||
| await crypto.sign('HMAC', wkey, _message as BufferSource) | ||
| ) as TRet<Uint8Array>; | ||
| } finally { | ||
| clean(_message); | ||
| } | ||
| }; | ||
@@ -166,4 +177,3 @@ hmac_.create = (_hash: TArg<WebHash>, _key: TArg<Uint8Array>) => { | ||
| * @returns Promise resolving to derived key bytes. | ||
| * The RFC `L <= 255 * HashLen` bound is currently enforced only by backend | ||
| * `deriveBits()` rejection, not by an explicit library-side guard. | ||
| * The RFC `L <= 255 * HashLen` bound is enforced before calling WebCrypto. | ||
| * @throws If the current runtime does not provide `crypto.subtle`. {@link Error} | ||
@@ -189,15 +199,24 @@ * @example | ||
| const crypto = _subtle(); | ||
| ahashWeb(hash); | ||
| const hashName = ahashWeb(hash); | ||
| const hashOutputLen = hash.outputLen; | ||
| abytes(ikm, undefined, 'ikm'); | ||
| anumber(length, 'length'); | ||
| if (length > 255 * hashOutputLen) throw new Error('Length must be <= 255*HashLen'); | ||
| if (salt !== undefined) abytes(salt, undefined, 'salt'); | ||
| if (info !== undefined) abytes(info, undefined, 'info'); | ||
| const wkey = await crypto.importKey('raw', ikm as BufferSource, 'HKDF', false, ['deriveBits']); | ||
| const opts = { | ||
| name: 'HKDF', | ||
| hash: hash.webCryptoName, | ||
| salt: salt === undefined ? new Uint8Array(0) : salt, | ||
| info: info === undefined ? new Uint8Array(0) : info, | ||
| }; | ||
| return new Uint8Array(await crypto.deriveBits(opts, wkey, 8 * length)) as TRet<Uint8Array>; | ||
| // salt and info reach deriveBits() only after importKey() resolves, so snapshot them now. | ||
| const _salt = salt === undefined ? new Uint8Array(0) : copyBytes(salt); | ||
| const _info = info === undefined ? new Uint8Array(0) : copyBytes(info); | ||
| try { | ||
| const wkey = await crypto.importKey('raw', ikm as BufferSource, 'HKDF', false, ['deriveBits']); | ||
| const opts = { name: 'HKDF', hash: hashName, salt: _salt, info: _info }; | ||
| const out = new Uint8Array(await crypto.deriveBits(opts, wkey, 8 * length)); | ||
| if (out.length !== length) { | ||
| clean(out); | ||
| throw new Error('WebCrypto returned an invalid derived key length'); | ||
| } | ||
| return out as TRet<Uint8Array>; | ||
| } finally { | ||
| clean(_salt, _info); | ||
| } | ||
| } | ||
@@ -217,3 +236,3 @@ | ||
| * `deriveBits()` rejection (for example `c = 0`), not a dedicated | ||
| * library-side guard. | ||
| * library-side guard. Values above the signed 32-bit backend range are rejected locally. | ||
| * @throws If the current runtime does not provide `crypto.subtle`. {@link Error} | ||
@@ -234,3 +253,3 @@ * @example | ||
| const crypto = _subtle(); | ||
| ahashWeb(hash); | ||
| const hashName = ahashWeb(hash); | ||
| const _opts = checkOpts({ dkLen: 32 }, opts); | ||
@@ -240,11 +259,33 @@ const { c, dkLen } = _opts; | ||
| anumber(dkLen, 'dkLen'); | ||
| // Node's native WebCrypto PBKDF2 binding accepts only a signed 32-bit iteration count and aborts | ||
| // the process on larger values instead of returning a rejected promise. | ||
| if (c > 0x7fffffff) throw new Error('"c" exceeds WebCrypto backend limit'); | ||
| // RFC 8018 §5.2 defines dkLen as a positive integer. | ||
| if (dkLen < 1) throw new Error('"dkLen" must be >= 1'); | ||
| // SubtleCrypto.deriveBits() accepts an unsigned-long bit count. Byte lengths at or above | ||
| // 2^29 would wrap after multiplication by eight instead of requesting the intended length. | ||
| if (dkLen >= 2 ** 29) throw new Error('derived key too long'); | ||
| const _password = kdfInputToBytes(password, 'password'); | ||
| const _salt = kdfInputToBytes(salt, 'salt'); | ||
| const key = await crypto.importKey('raw', _password as BufferSource, 'PBKDF2', false, [ | ||
| 'deriveBits', | ||
| ]); | ||
| const deriveOpts = { name: 'PBKDF2', salt: _salt, iterations: c, hash: hash.webCryptoName }; | ||
| return new Uint8Array(await crypto.deriveBits(deriveOpts, key, 8 * dkLen)) as TRet<Uint8Array>; | ||
| try { | ||
| const saltBytes = kdfInputToBytes(salt, 'salt'); | ||
| // String conversion already returns an owned array. Caller-owned byte salts need a snapshot | ||
| // because deriveBits() does not receive them until after importKey() resolves. | ||
| const _salt = typeof salt === 'string' ? saltBytes : copyBytes(saltBytes); | ||
| try { | ||
| const key = await crypto.importKey('raw', _password as BufferSource, 'PBKDF2', false, [ | ||
| 'deriveBits', | ||
| ]); | ||
| const deriveOpts = { name: 'PBKDF2', salt: _salt, iterations: c, hash: hashName }; | ||
| const out = new Uint8Array(await crypto.deriveBits(deriveOpts, key, 8 * dkLen)); | ||
| if (out.length !== dkLen) { | ||
| clean(out); | ||
| throw new Error('WebCrypto returned an invalid derived key length'); | ||
| } | ||
| return out as TRet<Uint8Array>; | ||
| } finally { | ||
| clean(_salt); | ||
| } | ||
| } finally { | ||
| if (typeof password === 'string') clean(_password); | ||
| } | ||
| } |
+14
-10
@@ -305,5 +305,8 @@ /** | ||
| /** | ||
| * There is no setImmediate in browser and setTimeout is slow. | ||
| * This yields to the Promise/microtask scheduler queue, not to timers or the | ||
| * full macrotask event loop. | ||
| * Yields to the host task scheduler so timers, I/O, and rendering can make progress. | ||
| * Uses the Web Scheduling API when available or `setTimeout` as a cross-platform fallback. | ||
| * Host-task yields are much slower than microtasks (roughly 1ms with the timer fallback), so | ||
| * async loops should use `asyncTick >= 10` to amortize scheduling overhead to about 10% while | ||
| * still allowing other event-loop work to proceed. | ||
| * @param onReject - optional cleanup invoked only if the host yield fails | ||
| * @example | ||
@@ -315,9 +318,9 @@ * Yield to the next scheduler tick. | ||
| */ | ||
| export declare const nextTick: () => Promise<void>; | ||
| export declare function nextTick(onReject?: () => void): Promise<void>; | ||
| /** | ||
| * Returns control to the Promise/microtask scheduler every `tick` | ||
| * milliseconds to avoid blocking long loops. | ||
| * Returns control to the host event loop every `tick` milliseconds to avoid blocking long loops. | ||
| * @param iters - number of loop iterations to run | ||
| * @param tick - maximum time slice in milliseconds | ||
| * @param cb - callback executed on each iteration | ||
| * @param onReject - optional cleanup invoked only if a host yield fails | ||
| * @throws On wrong argument types. {@link TypeError} | ||
@@ -331,3 +334,3 @@ * @throws On wrong argument ranges or values. {@link RangeError} | ||
| */ | ||
| export declare function asyncLoop(iters: number, tick: number, cb: (i: number) => void): Promise<void>; | ||
| export declare function asyncLoop(iters: number, tick: number, cb: (i: number) => void, onReject?: () => void): Promise<void>; | ||
| /** | ||
@@ -397,3 +400,3 @@ * Converts string to bytes using UTF8 encoding. | ||
| * @param title - label included in thrown override errors | ||
| * @returns Merged option object. The merge mutates `defaults` in place. | ||
| * @returns Fresh merged option object with a null prototype. | ||
| * @throws On wrong argument types. {@link TypeError} | ||
@@ -455,5 +458,6 @@ * @example | ||
| * Mixes fresh entropy into the current generator state. | ||
| * @param seed - Entropy bytes to absorb. | ||
| * @param seed - Non-empty entropy bytes to absorb. When omitted, the implementation uses its | ||
| * system RNG. | ||
| */ | ||
| addEntropy(seed: TArg<Uint8Array>): void; | ||
| addEntropy(seed?: TArg<Uint8Array>): void; | ||
| /** | ||
@@ -460,0 +464,0 @@ * Produces a requested number of pseudorandom bytes. |
+47
-14
@@ -137,2 +137,12 @@ /** | ||
| }; | ||
| const aopts = (value, label) => { | ||
| aobject(value, label); | ||
| const proto = Object.getPrototypeOf(value); | ||
| if (proto !== Object.prototype && proto !== null) | ||
| throw new TypeError(`"${label}" expected plain object`); | ||
| // Object.assign() treats an own "__proto__" source key as a write to the target's legacy | ||
| // prototype setter. Reject it before merging so inherited option values cannot be injected. | ||
| if (Object.hasOwn(value, '__proto__')) | ||
| throw new TypeError(`"${label}.__proto__" is not allowed`); | ||
| }; | ||
| /** | ||
@@ -411,5 +421,8 @@ * Asserts a hash instance has not been destroyed or finished. | ||
| /** | ||
| * There is no setImmediate in browser and setTimeout is slow. | ||
| * This yields to the Promise/microtask scheduler queue, not to timers or the | ||
| * full macrotask event loop. | ||
| * Yields to the host task scheduler so timers, I/O, and rendering can make progress. | ||
| * Uses the Web Scheduling API when available or `setTimeout` as a cross-platform fallback. | ||
| * Host-task yields are much slower than microtasks (roughly 1ms with the timer fallback), so | ||
| * async loops should use `asyncTick >= 10` to amortize scheduling overhead to about 10% while | ||
| * still allowing other event-loop work to proceed. | ||
| * @param onReject - optional cleanup invoked only if the host yield fails | ||
| * @example | ||
@@ -421,9 +434,19 @@ * Yield to the next scheduler tick. | ||
| */ | ||
| export const nextTick = async () => { }; | ||
| export function nextTick(onReject) { | ||
| const host = globalThis; | ||
| if (typeof host.scheduler?.yield === 'function') { | ||
| const promise = host.scheduler.yield(); | ||
| // Keep the original scheduler rejection; this handler exists only for cleanup. | ||
| if (onReject) | ||
| promise.catch(onReject); | ||
| return promise; | ||
| } | ||
| return new Promise((resolve) => host.setTimeout(resolve, 0)); | ||
| } | ||
| /** | ||
| * Returns control to the Promise/microtask scheduler every `tick` | ||
| * milliseconds to avoid blocking long loops. | ||
| * Returns control to the host event loop every `tick` milliseconds to avoid blocking long loops. | ||
| * @param iters - number of loop iterations to run | ||
| * @param tick - maximum time slice in milliseconds | ||
| * @param cb - callback executed on each iteration | ||
| * @param onReject - optional cleanup invoked only if a host yield fails | ||
| * @throws On wrong argument types. {@link TypeError} | ||
@@ -437,3 +460,3 @@ * @throws On wrong argument ranges or values. {@link RangeError} | ||
| */ | ||
| export async function asyncLoop(iters, tick, cb) { | ||
| export async function asyncLoop(iters, tick, cb, onReject) { | ||
| anumber(iters, 'iters'); | ||
@@ -451,5 +474,5 @@ anumber(tick, 'tick'); | ||
| continue; | ||
| await nextTick(); | ||
| await nextTick(onReject); | ||
| // Track only synchronous work time; scheduler delay after yielding is outside our budget. | ||
| ts += diff; | ||
| ts = Date.now(); | ||
| } | ||
@@ -473,3 +496,11 @@ } | ||
| throw new TypeError('string expected'); | ||
| return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809 | ||
| const encoded = new TextEncoder().encode(str); | ||
| try { | ||
| // Copy into the current realm for Firefox extension contexts. Callers that own the returned | ||
| // buffer can then wipe it independently of TextEncoder's temporary result. | ||
| return new Uint8Array(encoded); // https://bugzil.la/1681809 | ||
| } | ||
| finally { | ||
| clean(encoded); | ||
| } | ||
| } | ||
@@ -565,3 +596,3 @@ /** | ||
| * @param title - label included in thrown override errors | ||
| * @returns Merged option object. The merge mutates `defaults` in place. | ||
| * @returns Fresh merged option object with a null prototype. | ||
| * @throws On wrong argument types. {@link TypeError} | ||
@@ -575,6 +606,8 @@ * @example | ||
| export function checkOpts(defaults, opts, title = 'opts') { | ||
| aobject(defaults, 'defaults'); | ||
| aopts(defaults, 'defaults'); | ||
| if (opts !== undefined) | ||
| aobject(opts, title); | ||
| const merged = Object.assign(defaults, opts); | ||
| aopts(opts, title); | ||
| // Callers read optional fields directly, so omitted values must not fall through to ambient | ||
| // Object.prototype pollution (for example a forged `dkLen` changing SHAKE's default output). | ||
| const merged = Object.assign(Object.create(null), defaults, opts); | ||
| return merged; | ||
@@ -581,0 +614,0 @@ } |
+2
-3
@@ -85,4 +85,3 @@ import { type Pbkdf2Opt } from './pbkdf2.ts'; | ||
| * @returns Promise resolving to derived key bytes. | ||
| * The RFC `L <= 255 * HashLen` bound is currently enforced only by backend | ||
| * `deriveBits()` rejection, not by an explicit library-side guard. | ||
| * The RFC `L <= 255 * HashLen` bound is enforced before calling WebCrypto. | ||
| * @throws If the current runtime does not provide `crypto.subtle`. {@link Error} | ||
@@ -113,3 +112,3 @@ * @example | ||
| * `deriveBits()` rejection (for example `c = 0`), not a dedicated | ||
| * library-side guard. | ||
| * library-side guard. Values above the signed 32-bit backend range are rejected locally. | ||
| * @throws If the current runtime does not provide `crypto.subtle`. {@link Error} | ||
@@ -116,0 +115,0 @@ * @example |
+73
-26
| import {} from "./pbkdf2.js"; | ||
| import { abytes, ahash, anumber, checkOpts, kdfInputToBytes, } from "./utils.js"; | ||
| import { abytes, ahash, anumber, checkOpts, clean, copyBytes, kdfInputToBytes, } from "./utils.js"; | ||
| function _subtle() { | ||
@@ -30,4 +30,6 @@ const cr = typeof globalThis === 'object' ? globalThis.crypto : null; | ||
| ahash(hash); | ||
| if (typeof hash.webCryptoName !== 'string') | ||
| const name = hash.webCryptoName; | ||
| if (typeof name !== 'string') | ||
| throw new Error('non-web hash'); | ||
| return name; | ||
| } | ||
@@ -91,7 +93,15 @@ /** WebCrypto SHA1 (RFC 3174) legacy hash function. It was cryptographically broken. */ | ||
| abytes(message, undefined, 'message'); | ||
| ahashWeb(hash); | ||
| // WebCrypto keys can't be zeroized | ||
| // prettier-ignore | ||
| const wkey = await crypto.importKey('raw', key, { name: 'HMAC', hash: hash.webCryptoName }, false, ['sign']); | ||
| return new Uint8Array(await crypto.sign('HMAC', wkey, message)); | ||
| const hashName = ahashWeb(hash); | ||
| // importKey() snapshots key synchronously, but message is not passed to sign() until after | ||
| // importKey() resolves. Keep the wrapper's inputs stable across that await. | ||
| const _message = copyBytes(message); | ||
| try { | ||
| // WebCrypto keys can't be zeroized | ||
| // prettier-ignore | ||
| const wkey = await crypto.importKey('raw', key, { name: 'HMAC', hash: hashName }, false, ['sign']); | ||
| return new Uint8Array(await crypto.sign('HMAC', wkey, _message)); | ||
| } | ||
| finally { | ||
| clean(_message); | ||
| } | ||
| }; | ||
@@ -113,4 +123,3 @@ hmac_.create = (_hash, _key) => { | ||
| * @returns Promise resolving to derived key bytes. | ||
| * The RFC `L <= 255 * HashLen` bound is currently enforced only by backend | ||
| * `deriveBits()` rejection, not by an explicit library-side guard. | ||
| * The RFC `L <= 255 * HashLen` bound is enforced before calling WebCrypto. | ||
| * @throws If the current runtime does not provide `crypto.subtle`. {@link Error} | ||
@@ -130,5 +139,8 @@ * @example | ||
| const crypto = _subtle(); | ||
| ahashWeb(hash); | ||
| const hashName = ahashWeb(hash); | ||
| const hashOutputLen = hash.outputLen; | ||
| abytes(ikm, undefined, 'ikm'); | ||
| anumber(length, 'length'); | ||
| if (length > 255 * hashOutputLen) | ||
| throw new Error('Length must be <= 255*HashLen'); | ||
| if (salt !== undefined) | ||
@@ -138,10 +150,18 @@ abytes(salt, undefined, 'salt'); | ||
| abytes(info, undefined, 'info'); | ||
| const wkey = await crypto.importKey('raw', ikm, 'HKDF', false, ['deriveBits']); | ||
| const opts = { | ||
| name: 'HKDF', | ||
| hash: hash.webCryptoName, | ||
| salt: salt === undefined ? new Uint8Array(0) : salt, | ||
| info: info === undefined ? new Uint8Array(0) : info, | ||
| }; | ||
| return new Uint8Array(await crypto.deriveBits(opts, wkey, 8 * length)); | ||
| // salt and info reach deriveBits() only after importKey() resolves, so snapshot them now. | ||
| const _salt = salt === undefined ? new Uint8Array(0) : copyBytes(salt); | ||
| const _info = info === undefined ? new Uint8Array(0) : copyBytes(info); | ||
| try { | ||
| const wkey = await crypto.importKey('raw', ikm, 'HKDF', false, ['deriveBits']); | ||
| const opts = { name: 'HKDF', hash: hashName, salt: _salt, info: _info }; | ||
| const out = new Uint8Array(await crypto.deriveBits(opts, wkey, 8 * length)); | ||
| if (out.length !== length) { | ||
| clean(out); | ||
| throw new Error('WebCrypto returned an invalid derived key length'); | ||
| } | ||
| return out; | ||
| } | ||
| finally { | ||
| clean(_salt, _info); | ||
| } | ||
| } | ||
@@ -160,3 +180,3 @@ /** | ||
| * `deriveBits()` rejection (for example `c = 0`), not a dedicated | ||
| * library-side guard. | ||
| * library-side guard. Values above the signed 32-bit backend range are rejected locally. | ||
| * @throws If the current runtime does not provide `crypto.subtle`. {@link Error} | ||
@@ -172,3 +192,3 @@ * @example | ||
| const crypto = _subtle(); | ||
| ahashWeb(hash); | ||
| const hashName = ahashWeb(hash); | ||
| const _opts = checkOpts({ dkLen: 32 }, opts); | ||
@@ -178,12 +198,39 @@ const { c, dkLen } = _opts; | ||
| anumber(dkLen, 'dkLen'); | ||
| // Node's native WebCrypto PBKDF2 binding accepts only a signed 32-bit iteration count and aborts | ||
| // the process on larger values instead of returning a rejected promise. | ||
| if (c > 0x7fffffff) | ||
| throw new Error('"c" exceeds WebCrypto backend limit'); | ||
| // RFC 8018 §5.2 defines dkLen as a positive integer. | ||
| if (dkLen < 1) | ||
| throw new Error('"dkLen" must be >= 1'); | ||
| // SubtleCrypto.deriveBits() accepts an unsigned-long bit count. Byte lengths at or above | ||
| // 2^29 would wrap after multiplication by eight instead of requesting the intended length. | ||
| if (dkLen >= 2 ** 29) | ||
| throw new Error('derived key too long'); | ||
| const _password = kdfInputToBytes(password, 'password'); | ||
| const _salt = kdfInputToBytes(salt, 'salt'); | ||
| const key = await crypto.importKey('raw', _password, 'PBKDF2', false, [ | ||
| 'deriveBits', | ||
| ]); | ||
| const deriveOpts = { name: 'PBKDF2', salt: _salt, iterations: c, hash: hash.webCryptoName }; | ||
| return new Uint8Array(await crypto.deriveBits(deriveOpts, key, 8 * dkLen)); | ||
| try { | ||
| const saltBytes = kdfInputToBytes(salt, 'salt'); | ||
| // String conversion already returns an owned array. Caller-owned byte salts need a snapshot | ||
| // because deriveBits() does not receive them until after importKey() resolves. | ||
| const _salt = typeof salt === 'string' ? saltBytes : copyBytes(saltBytes); | ||
| try { | ||
| const key = await crypto.importKey('raw', _password, 'PBKDF2', false, [ | ||
| 'deriveBits', | ||
| ]); | ||
| const deriveOpts = { name: 'PBKDF2', salt: _salt, iterations: c, hash: hashName }; | ||
| const out = new Uint8Array(await crypto.deriveBits(deriveOpts, key, 8 * dkLen)); | ||
| if (out.length !== dkLen) { | ||
| clean(out); | ||
| throw new Error('WebCrypto returned an invalid derived key length'); | ||
| } | ||
| return out; | ||
| } | ||
| finally { | ||
| clean(_salt); | ||
| } | ||
| } | ||
| finally { | ||
| if (typeof password === 'string') | ||
| clean(_password); | ||
| } | ||
| } |
691646
1.6%16864
2.16%6
20%479
-17.41%