feat(crypto): Argon2id KDF foundation (vendored, not yet adopted)

Phase 1 of CODE_AUDIT §1.2 — additive, no live account uses Argon2id yet.

- Vendor @noble/hashes@2.2.0 argon2id as js/argon2.js (esbuild IIFE exposing
  globalThis.NobleArgon2). Pure-JS, not WASM: CSP is script-src 'self' with no
  wasm-unsafe-eval, so WASM would require weakening it. Verified against the
  RFC 9106 §5.3 test vector. Server needs zero Argon2 (zero-knowledge: it only
  ever SHA256-wraps the client verifier).
- app.js: deriveKeyBytes(pwd, salt, algo, iters, argonParams) branches Argon2id
  vs PBKDF2; deriveKeyAndVerifier refactored around it. New markers
  HASH_ALGO_ARGON2='argon2id-v2' + ARGON2_DEFAULT_PARAMS (OWASP m=19MiB,t=2,p=1,
  ~0.65s/unlock). isDecoupledVerifierAlgo() generalises the decoupled-verifier
  rule to any '-v2' scheme so argon2id-v2 inherits it. AES key is still ALWAYS
  the raw KDF output → entries decryptable, legacy accounts untouched.
- index.html loads js/argon2.js before app.js; added to BuildAssets whitelist;
  test harness loads it into the sandbox first.
- Tests: +5 (RFC 9106 vector via vendored bundle, argon2 branch derives Argon2
  key not PBKDF2, decoupled verifier, AES round-trip under Argon2 key). 40/40.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-05 14:13:36 +01:00
parent d9397881dc
commit 2bd0fcfbf8
6 changed files with 1071 additions and 14 deletions
+1
View File
@@ -51,6 +51,7 @@ Log "Web root: $WebRoot"
# Use exact paths (not wildcards) to avoid embedding *-legacy.* backups. # Use exact paths (not wildcards) to avoid embedding *-legacy.* backups.
$patterns = @( $patterns = @(
'index.html', 'index.html',
'js\argon2.js',
'js\app.js', 'js\app.js',
'css\style.css' 'css\style.css'
) )
+1
View File
@@ -1192,6 +1192,7 @@
</div> </div>
</div> </div>
<script src="js/argon2.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </body>
</html> </html>
+42 -13
View File
@@ -714,46 +714,75 @@ function bytesToHex(arr) {
return hex; return hex;
} }
// Decoupled-verifier scheme marker + domain separator. When the account's // Decoupled-verifier scheme markers + domain separator. When the account's
// hash_algo is HASH_ALGO_V2, the verifier sent to the server is a one-way // hash_algo ends in '-v2', the verifier sent to the server is a one-way
// SHA-256 of the key hex (domain-separated), NOT the key hex itself — so // SHA-256 of the key hex (domain-separated), NOT the key hex itself — so
// intercepting the /login body no longer hands over the AES vault key. // intercepting the /login body no longer hands over the AES vault key.
// The AES key (cryptoKey) is ALWAYS the raw PBKDF2 output regardless, so // The AES key (cryptoKey) is ALWAYS the raw KDF output regardless of algo, so
// entries stay decryptable and legacy accounts are unaffected. // entries stay decryptable and legacy accounts are unaffected.
const HASH_ALGO_V2 = 'pbkdf2-sha256-v2'; const HASH_ALGO_V2 = 'pbkdf2-sha256-v2'; // PBKDF2 KDF + decoupled verifier
const HASH_ALGO_ARGON2 = 'argon2id-v2'; // Argon2id KDF + decoupled verifier
const AUTH_VERIFIER_DOMAIN = 'pmserver/auth-verifier/v2'; const AUTH_VERIFIER_DOMAIN = 'pmserver/auth-verifier/v2';
// OWASP-recommended Argon2id baseline (m = 19 MiB, t = 2, p = 1). Stored
// per-account (like kdfIterations for PBKDF2) so it's tunable later without
// breaking existing accounts. dkLen is fixed at 32 (AES-256 key).
const ARGON2_DEFAULT_PARAMS = { m: 19456, t: 2, p: 1 };
// True for any scheme whose transmitted verifier is decoupled from the key
// (all '-v2' markers: pbkdf2-sha256-v2, argon2id-v2). endsWith keeps it
// future-proof for any later '-v2' KDF.
function isDecoupledVerifierAlgo(algo) {
return typeof algo === 'string' && algo.endsWith('-v2');
}
async function sha256Hex(str) { async function sha256Hex(str) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)); const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
return bytesToHex(new Uint8Array(buf)); return bytesToHex(new Uint8Array(buf));
} }
// Map the raw PBKDF2 key hex → the verifier to transmit, per account algo. // Map the raw KDF key hex → the verifier to transmit, per account algo.
// v2 → domain-separated SHA-256 (decoupled from the key). Anything else → // Decoupled ('-v2') → domain-separated SHA-256. Anything else → the key hex
// the key hex verbatim (legacy behaviour, unchanged for existing accounts). // verbatim (legacy behaviour, unchanged for existing pre-v2 accounts).
async function verifierFromKeyHex(keyHex, algo) { async function verifierFromKeyHex(keyHex, algo) {
if (algo === HASH_ALGO_V2) return await sha256Hex(keyHex + AUTH_VERIFIER_DOMAIN); if (isDecoupledVerifierAlgo(algo)) return await sha256Hex(keyHex + AUTH_VERIFIER_DOMAIN);
return keyHex; return keyHex;
} }
async function deriveKeyAndVerifier(pwd, saltHex, iterations, algo) { // Derive the 32 raw key bytes from the master password, per account KDF.
iterations = iterations || 100000; // Argon2id (memory-hard) for argon2id-* accounts, else PBKDF2-SHA256. Both
// feed the salt HEX STRING's UTF-8 bytes as the salt (historical quirk kept
// identical across KDFs so a given pw+salt maps to one deterministic key).
async function deriveKeyBytes(pwd, saltHex, algo, iterations, argonParams) {
const enc = new TextEncoder(); const enc = new TextEncoder();
if (algo === HASH_ALGO_ARGON2) {
if (typeof NobleArgon2 === 'undefined' || !NobleArgon2 || !NobleArgon2.argon2id)
throw new Error('Argon2 library not loaded (js/argon2.js missing?)');
const p = argonParams || ARGON2_DEFAULT_PARAMS;
return NobleArgon2.argon2id(enc.encode(pwd), enc.encode(saltHex),
{ t: p.t, m: p.m, p: p.p, dkLen: 32, version: 0x13 });
}
// PBKDF2-SHA256 (default / legacy).
iterations = iterations || 100000;
const km = await crypto.subtle.importKey( const km = await crypto.subtle.importKey(
'raw', enc.encode(pwd), 'PBKDF2', false, ['deriveBits']); 'raw', enc.encode(pwd), 'PBKDF2', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits( const bits = await crypto.subtle.deriveBits(
{ name: 'PBKDF2', salt: enc.encode(saltHex), { name: 'PBKDF2', salt: enc.encode(saltHex),
iterations: iterations, hash: 'SHA-256' }, iterations: iterations, hash: 'SHA-256' },
km, 256); // 256 bits = 32 bytes — matches PBKDF2_SHA256_Hex output km, 256); // 256 bits = 32 bytes — matches PBKDF2_SHA256_Hex output
const keyBytes = new Uint8Array(bits); return new Uint8Array(bits);
}
async function deriveKeyAndVerifier(pwd, saltHex, iterations, algo, argonParams) {
const keyBytes = await deriveKeyBytes(pwd, saltHex, algo, iterations, argonParams);
const cryptoKey = await crypto.subtle.importKey( const cryptoKey = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']); 'raw', keyBytes, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']);
const verifier = await verifierFromKeyHex(bytesToHex(keyBytes), algo); const verifier = await verifierFromKeyHex(bytesToHex(keyBytes), algo);
return { cryptoKey, verifier }; return { cryptoKey, verifier };
} }
async function computeVerifier(pwd, saltHex, iterations, algo) { async function computeVerifier(pwd, saltHex, iterations, algo, argonParams) {
const r = await deriveKeyAndVerifier(pwd, saltHex, iterations, algo); const r = await deriveKeyAndVerifier(pwd, saltHex, iterations, algo, argonParams);
return r.verifier; return r.verifier;
} }
+957
View File
@@ -0,0 +1,957 @@
/* @noble/hashes argon2id v2.2.0 — vendored bundle (esbuild IIFE). Verified against RFC 9106 §5.3. Do not edit by hand; re-bundle from @noble/hashes@2.2.0. */
(() => {
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// node_modules/@noble/hashes/_u64.js
var U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
var _32n = /* @__PURE__ */ BigInt(32);
function fromBig(n, le = false) {
if (le)
return { h: Number(n & U32_MASK64), l: Number(n >> _32n & U32_MASK64) };
return { h: Number(n >> _32n & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 };
}
var rotrSH = (h, l, s) => h >>> s | l << 32 - s;
var rotrSL = (h, l, s) => h << 32 - s | l >>> s;
var rotrBH = (h, l, s) => h << 64 - s | l >>> s - 32;
var rotrBL = (h, l, s) => h >>> s - 32 | l << 64 - s;
var rotr32H = (_h, l) => l;
var rotr32L = (h, _l) => h;
function add(Ah, Al, Bh, Bl) {
const l = (Al >>> 0) + (Bl >>> 0);
return { h: Ah + Bh + (l / 2 ** 32 | 0) | 0, l: l | 0 };
}
var add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0);
var add3H = (low, Ah, Bh, Ch) => Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0;
// node_modules/@noble/hashes/utils.js
function isBytes(a) {
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
}
function anumber(n, title = "") {
if (typeof n !== "number") {
const prefix = title && `"${title}" `;
throw new TypeError(`${prefix}expected number, got ${typeof n}`);
}
if (!Number.isSafeInteger(n) || n < 0) {
const prefix = title && `"${title}" `;
throw new RangeError(`${prefix}expected integer >= 0, got ${n}`);
}
}
function abytes(value, length, title = "") {
const bytes = isBytes(value);
const len = value?.length;
const needsLen = length !== void 0;
if (!bytes || needsLen && len !== length) {
const prefix = title && `"${title}" `;
const ofLen = needsLen ? ` of length ${length}` : "";
const got = bytes ? `length=${len}` : `type=${typeof value}`;
const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
if (!bytes)
throw new TypeError(message);
throw new RangeError(message);
}
return value;
}
function aexists(instance, checkFinished = true) {
if (instance.destroyed)
throw new Error("Hash instance has been destroyed");
if (checkFinished && instance.finished)
throw new Error("Hash#digest() has already been called");
}
function aoutput(out, instance) {
abytes(out, void 0, "digestInto() output");
const min = instance.outputLen;
if (out.length < min) {
throw new RangeError('"digestInto() output" expected to be of length >=' + min);
}
}
function u8(arr) {
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
}
function u32(arr) {
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
}
function clean(...arrays) {
for (let i = 0; i < arrays.length; i++) {
arrays[i].fill(0);
}
}
var isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68)();
function byteSwap(word) {
return word << 24 & 4278190080 | word << 8 & 16711680 | word >>> 8 & 65280 | word >>> 24 & 255;
}
var swap8IfBE = isLE ? (n) => n : (n) => byteSwap(n) >>> 0;
function byteSwap32(arr) {
for (let i = 0; i < arr.length; i++) {
arr[i] = byteSwap(arr[i]);
}
return arr;
}
var swap32IfBE = isLE ? (u) => u : byteSwap32;
function utf8ToBytes(str) {
if (typeof str !== "string")
throw new TypeError("string expected");
return new Uint8Array(new TextEncoder().encode(str));
}
function kdfInputToBytes(data, errorTitle = "") {
if (typeof data === "string")
return utf8ToBytes(data);
return abytes(data, void 0, errorTitle);
}
function createHasher(hashCons, info = {}) {
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
const tmp = hashCons(void 0);
hashC.outputLen = tmp.outputLen;
hashC.blockLen = tmp.blockLen;
hashC.canXOF = tmp.canXOF;
hashC.create = (opts) => hashCons(opts);
Object.assign(hashC, info);
return Object.freeze(hashC);
}
// node_modules/@noble/hashes/_blake.js
var BSIGMA = /* @__PURE__ */ Uint8Array.from([
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
14,
10,
4,
8,
9,
15,
13,
6,
1,
12,
0,
2,
11,
7,
5,
3,
11,
8,
12,
0,
5,
2,
15,
13,
10,
14,
3,
6,
7,
1,
9,
4,
7,
9,
3,
1,
13,
12,
11,
14,
2,
6,
5,
10,
4,
0,
15,
8,
9,
0,
5,
7,
2,
4,
10,
15,
14,
1,
11,
12,
6,
8,
3,
13,
2,
12,
6,
10,
0,
11,
8,
3,
4,
13,
7,
5,
15,
14,
1,
9,
12,
5,
1,
15,
14,
13,
4,
10,
0,
7,
6,
3,
9,
2,
8,
11,
13,
11,
7,
14,
12,
1,
3,
9,
5,
0,
15,
4,
8,
6,
2,
10,
6,
15,
14,
9,
11,
3,
0,
8,
12,
2,
13,
7,
1,
4,
10,
5,
10,
2,
8,
4,
7,
6,
1,
5,
15,
11,
9,
14,
3,
12,
13,
0,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
14,
10,
4,
8,
9,
15,
13,
6,
1,
12,
0,
2,
11,
7,
5,
3,
// Blake1, unused in others
11,
8,
12,
0,
5,
2,
15,
13,
10,
14,
3,
6,
7,
1,
9,
4,
7,
9,
3,
1,
13,
12,
11,
14,
2,
6,
5,
10,
4,
0,
15,
8,
9,
0,
5,
7,
2,
4,
10,
15,
14,
1,
11,
12,
6,
8,
3,
13,
2,
12,
6,
10,
0,
11,
8,
3,
4,
13,
7,
5,
15,
14,
1,
9
]);
// node_modules/@noble/hashes/blake2.js
var B2B_IV = /* @__PURE__ */ Uint32Array.from([
4089235720,
1779033703,
2227873595,
3144134277,
4271175723,
1013904242,
1595750129,
2773480762,
2917565137,
1359893119,
725511199,
2600822924,
4215389547,
528734635,
327033209,
1541459225
]);
var BBUF = /* @__PURE__ */ new Uint32Array(32);
function G1b(a, b, c, d, msg, x) {
const Xl = msg[x], Xh = msg[x + 1];
let Al = BBUF[2 * a], Ah = BBUF[2 * a + 1];
let Bl = BBUF[2 * b], Bh = BBUF[2 * b + 1];
let Cl = BBUF[2 * c], Ch = BBUF[2 * c + 1];
let Dl = BBUF[2 * d], Dh = BBUF[2 * d + 1];
let ll = add3L(Al, Bl, Xl);
Ah = add3H(ll, Ah, Bh, Xh);
Al = ll | 0;
({ Dh, Dl } = { Dh: Dh ^ Ah, Dl: Dl ^ Al });
({ Dh, Dl } = { Dh: rotr32H(Dh, Dl), Dl: rotr32L(Dh, Dl) });
({ h: Ch, l: Cl } = add(Ch, Cl, Dh, Dl));
({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl });
({ Bh, Bl } = { Bh: rotrSH(Bh, Bl, 24), Bl: rotrSL(Bh, Bl, 24) });
BBUF[2 * a] = Al, BBUF[2 * a + 1] = Ah;
BBUF[2 * b] = Bl, BBUF[2 * b + 1] = Bh;
BBUF[2 * c] = Cl, BBUF[2 * c + 1] = Ch;
BBUF[2 * d] = Dl, BBUF[2 * d + 1] = Dh;
}
function G2b(a, b, c, d, msg, x) {
const Xl = msg[x], Xh = msg[x + 1];
let Al = BBUF[2 * a], Ah = BBUF[2 * a + 1];
let Bl = BBUF[2 * b], Bh = BBUF[2 * b + 1];
let Cl = BBUF[2 * c], Ch = BBUF[2 * c + 1];
let Dl = BBUF[2 * d], Dh = BBUF[2 * d + 1];
let ll = add3L(Al, Bl, Xl);
Ah = add3H(ll, Ah, Bh, Xh);
Al = ll | 0;
({ Dh, Dl } = { Dh: Dh ^ Ah, Dl: Dl ^ Al });
({ Dh, Dl } = { Dh: rotrSH(Dh, Dl, 16), Dl: rotrSL(Dh, Dl, 16) });
({ h: Ch, l: Cl } = add(Ch, Cl, Dh, Dl));
({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl });
({ Bh, Bl } = { Bh: rotrBH(Bh, Bl, 63), Bl: rotrBL(Bh, Bl, 63) });
BBUF[2 * a] = Al, BBUF[2 * a + 1] = Ah;
BBUF[2 * b] = Bl, BBUF[2 * b + 1] = Bh;
BBUF[2 * c] = Cl, BBUF[2 * c + 1] = Ch;
BBUF[2 * d] = Dl, BBUF[2 * d + 1] = Dh;
}
function checkBlake2Opts(outputLen, opts = {}, keyLen, saltLen, persLen) {
anumber(keyLen);
if (outputLen <= 0 || outputLen > keyLen)
throw new Error("outputLen bigger than keyLen");
const { key, salt, personalization } = opts;
if (key !== void 0 && (key.length < 1 || key.length > keyLen))
throw new Error('"key" expected to be undefined or of length=1..' + keyLen);
if (salt !== void 0)
abytes(salt, saltLen, "salt");
if (personalization !== void 0)
abytes(personalization, persLen, "personalization");
}
var _BLAKE2 = class {
constructor(blockLen, outputLen) {
__publicField(this, "buffer");
__publicField(this, "buffer32");
__publicField(this, "finished", false);
__publicField(this, "destroyed", false);
__publicField(this, "length", 0);
__publicField(this, "pos", 0);
__publicField(this, "blockLen");
__publicField(this, "outputLen");
__publicField(this, "canXOF", false);
anumber(blockLen);
anumber(outputLen);
this.blockLen = blockLen;
this.outputLen = outputLen;
this.buffer = new Uint8Array(blockLen);
this.buffer32 = u32(this.buffer);
}
update(data) {
aexists(this);
abytes(data);
const { blockLen, buffer, buffer32 } = this;
const len = data.length;
const offset = data.byteOffset;
const buf = data.buffer;
for (let pos = 0; pos < len; ) {
if (this.pos === blockLen) {
swap32IfBE(buffer32);
this.compress(buffer32, 0, false);
swap32IfBE(buffer32);
this.pos = 0;
}
const take = Math.min(blockLen - this.pos, len - pos);
const dataOffset = offset + pos;
if (take === blockLen && !(dataOffset % 4) && pos + take < len) {
const data32 = new Uint32Array(buf, dataOffset, Math.floor((len - pos) / 4));
swap32IfBE(data32);
for (let pos32 = 0; pos + blockLen < len; pos32 += buffer32.length, pos += blockLen) {
this.length += blockLen;
this.compress(data32, pos32, false);
}
swap32IfBE(data32);
continue;
}
buffer.set(data.subarray(pos, pos + take), this.pos);
this.pos += take;
this.length += take;
pos += take;
}
return this;
}
digestInto(out) {
aexists(this);
aoutput(out, this);
const { pos, buffer32 } = this;
this.finished = true;
clean(this.buffer.subarray(pos));
swap32IfBE(buffer32);
this.compress(buffer32, 0, true);
swap32IfBE(buffer32);
if (out.byteOffset & 3)
throw new RangeError('"digestInto() output" expected 4-byte aligned byteOffset, got ' + out.byteOffset);
const state = this.get();
const out32 = u32(out);
const full = Math.floor(this.outputLen / 4);
for (let i = 0; i < full; i++)
out32[i] = swap8IfBE(state[i]);
const tail = this.outputLen % 4;
if (!tail)
return;
const off = full * 4;
const word = state[full];
for (let i = 0; i < tail; i++)
out[off + i] = word >>> 8 * i;
}
digest() {
const { buffer, outputLen } = this;
this.digestInto(buffer);
const res = buffer.slice(0, outputLen);
this.destroy();
return res;
}
_cloneInto(to) {
const { buffer, length, finished, destroyed, outputLen, pos } = this;
to || (to = new this.constructor({ dkLen: outputLen }));
to.set(...this.get());
to.buffer.set(buffer);
to.destroyed = destroyed;
to.finished = finished;
to.length = length;
to.pos = pos;
to.outputLen = outputLen;
return to;
}
clone() {
return this._cloneInto();
}
};
var _BLAKE2b = class extends _BLAKE2 {
constructor(opts = {}) {
const olen = opts.dkLen === void 0 ? 64 : opts.dkLen;
super(128, olen);
// Same IV words as SHA-512 / BLAKE2b, encoded as LE u32 low/high halves.
__publicField(this, "v0l", B2B_IV[0] | 0);
__publicField(this, "v0h", B2B_IV[1] | 0);
__publicField(this, "v1l", B2B_IV[2] | 0);
__publicField(this, "v1h", B2B_IV[3] | 0);
__publicField(this, "v2l", B2B_IV[4] | 0);
__publicField(this, "v2h", B2B_IV[5] | 0);
__publicField(this, "v3l", B2B_IV[6] | 0);
__publicField(this, "v3h", B2B_IV[7] | 0);
__publicField(this, "v4l", B2B_IV[8] | 0);
__publicField(this, "v4h", B2B_IV[9] | 0);
__publicField(this, "v5l", B2B_IV[10] | 0);
__publicField(this, "v5h", B2B_IV[11] | 0);
__publicField(this, "v6l", B2B_IV[12] | 0);
__publicField(this, "v6h", B2B_IV[13] | 0);
__publicField(this, "v7l", B2B_IV[14] | 0);
__publicField(this, "v7h", B2B_IV[15] | 0);
checkBlake2Opts(olen, opts, 64, 16, 16);
let { key, personalization, salt } = opts;
let keyLength = 0;
if (key !== void 0) {
abytes(key, void 0, "key");
keyLength = key.length;
}
this.v0l ^= this.outputLen | keyLength << 8 | 1 << 16 | 1 << 24;
if (salt !== void 0) {
abytes(salt, void 0, "salt");
const slt = u32(salt);
this.v4l ^= swap8IfBE(slt[0]);
this.v4h ^= swap8IfBE(slt[1]);
this.v5l ^= swap8IfBE(slt[2]);
this.v5h ^= swap8IfBE(slt[3]);
}
if (personalization !== void 0) {
abytes(personalization, void 0, "personalization");
const pers = u32(personalization);
this.v6l ^= swap8IfBE(pers[0]);
this.v6h ^= swap8IfBE(pers[1]);
this.v7l ^= swap8IfBE(pers[2]);
this.v7h ^= swap8IfBE(pers[3]);
}
if (key !== void 0) {
const tmp = new Uint8Array(this.blockLen);
tmp.set(key);
this.update(tmp);
}
}
// prettier-ignore
get() {
let { v0l, v0h, v1l, v1h, v2l, v2h, v3l, v3h, v4l, v4h, v5l, v5h, v6l, v6h, v7l, v7h } = this;
return [v0l, v0h, v1l, v1h, v2l, v2h, v3l, v3h, v4l, v4h, v5l, v5h, v6l, v6h, v7l, v7h];
}
// prettier-ignore
set(v0l, v0h, v1l, v1h, v2l, v2h, v3l, v3h, v4l, v4h, v5l, v5h, v6l, v6h, v7l, v7h) {
this.v0l = v0l | 0;
this.v0h = v0h | 0;
this.v1l = v1l | 0;
this.v1h = v1h | 0;
this.v2l = v2l | 0;
this.v2h = v2h | 0;
this.v3l = v3l | 0;
this.v3h = v3h | 0;
this.v4l = v4l | 0;
this.v4h = v4h | 0;
this.v5l = v5l | 0;
this.v5h = v5h | 0;
this.v6l = v6l | 0;
this.v6h = v6h | 0;
this.v7l = v7l | 0;
this.v7h = v7h | 0;
}
compress(msg, offset, isLast) {
this.get().forEach((v, i) => BBUF[i] = v);
BBUF.set(B2B_IV, 16);
let { h, l } = fromBig(BigInt(this.length));
BBUF[24] = B2B_IV[8] ^ l;
BBUF[25] = B2B_IV[9] ^ h;
if (isLast) {
BBUF[28] = ~BBUF[28];
BBUF[29] = ~BBUF[29];
}
let j = 0;
const s = BSIGMA;
for (let i = 0; i < 12; i++) {
G1b(0, 4, 8, 12, msg, offset + 2 * s[j++]);
G2b(0, 4, 8, 12, msg, offset + 2 * s[j++]);
G1b(1, 5, 9, 13, msg, offset + 2 * s[j++]);
G2b(1, 5, 9, 13, msg, offset + 2 * s[j++]);
G1b(2, 6, 10, 14, msg, offset + 2 * s[j++]);
G2b(2, 6, 10, 14, msg, offset + 2 * s[j++]);
G1b(3, 7, 11, 15, msg, offset + 2 * s[j++]);
G2b(3, 7, 11, 15, msg, offset + 2 * s[j++]);
G1b(0, 5, 10, 15, msg, offset + 2 * s[j++]);
G2b(0, 5, 10, 15, msg, offset + 2 * s[j++]);
G1b(1, 6, 11, 12, msg, offset + 2 * s[j++]);
G2b(1, 6, 11, 12, msg, offset + 2 * s[j++]);
G1b(2, 7, 8, 13, msg, offset + 2 * s[j++]);
G2b(2, 7, 8, 13, msg, offset + 2 * s[j++]);
G1b(3, 4, 9, 14, msg, offset + 2 * s[j++]);
G2b(3, 4, 9, 14, msg, offset + 2 * s[j++]);
}
this.v0l ^= BBUF[0] ^ BBUF[16];
this.v0h ^= BBUF[1] ^ BBUF[17];
this.v1l ^= BBUF[2] ^ BBUF[18];
this.v1h ^= BBUF[3] ^ BBUF[19];
this.v2l ^= BBUF[4] ^ BBUF[20];
this.v2h ^= BBUF[5] ^ BBUF[21];
this.v3l ^= BBUF[6] ^ BBUF[22];
this.v3h ^= BBUF[7] ^ BBUF[23];
this.v4l ^= BBUF[8] ^ BBUF[24];
this.v4h ^= BBUF[9] ^ BBUF[25];
this.v5l ^= BBUF[10] ^ BBUF[26];
this.v5h ^= BBUF[11] ^ BBUF[27];
this.v6l ^= BBUF[12] ^ BBUF[28];
this.v6h ^= BBUF[13] ^ BBUF[29];
this.v7l ^= BBUF[14] ^ BBUF[30];
this.v7h ^= BBUF[15] ^ BBUF[31];
clean(BBUF);
}
destroy() {
this.destroyed = true;
clean(this.buffer32);
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
};
var blake2b = /* @__PURE__ */ createHasher((opts) => new _BLAKE2b(opts));
// node_modules/@noble/hashes/argon2.js
var AT = { Argond2d: 0, Argon2i: 1, Argon2id: 2 };
var ARGON2_SYNC_POINTS = 4;
var abytesOrZero = (buf, errorTitle = "") => {
if (buf === void 0)
return Uint8Array.of();
return kdfInputToBytes(buf, errorTitle);
};
function mul(a, b) {
const aL = a & 65535;
const aH = a >>> 16;
const bL = b & 65535;
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 & 65535) + lh;
const high = hh + (hl >>> 16) + (carry >>> 16) | 0;
const low = carry << 16 | ll & 65535;
return { h: high, l: low };
}
function mul2(a, b) {
const { h, l } = mul(a, b);
return { h: (h << 1 | l >>> 31) & 4294967295, l: l << 1 & 4294967295 };
}
function blamka(Ah, Al, Bh, Bl) {
const { h: Ch, l: Cl } = mul2(Al, Bl);
const Rll = add3L(Al, Bl, Cl);
return { h: add3H(Rll, Ah, Bh, Ch), l: Rll | 0 };
}
var A2_BUF = new Uint32Array(256);
function G(a, b, c, d) {
let Al = A2_BUF[2 * a], Ah = A2_BUF[2 * a + 1];
let Bl = A2_BUF[2 * b], Bh = A2_BUF[2 * b + 1];
let Cl = A2_BUF[2 * c], Ch = A2_BUF[2 * c + 1];
let Dl = A2_BUF[2 * d], Dh = A2_BUF[2 * d + 1];
({ h: Ah, l: Al } = blamka(Ah, Al, Bh, Bl));
({ Dh, Dl } = { Dh: Dh ^ Ah, Dl: Dl ^ Al });
({ Dh, Dl } = { Dh: rotr32H(Dh, Dl), Dl: rotr32L(Dh, Dl) });
({ h: Ch, l: Cl } = blamka(Ch, Cl, Dh, Dl));
({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl });
({ Bh, Bl } = { Bh: rotrSH(Bh, Bl, 24), Bl: rotrSL(Bh, Bl, 24) });
({ h: Ah, l: Al } = blamka(Ah, Al, Bh, Bl));
({ Dh, Dl } = { Dh: Dh ^ Ah, Dl: Dl ^ Al });
({ Dh, Dl } = { Dh: rotrSH(Dh, Dl, 16), Dl: rotrSL(Dh, Dl, 16) });
({ h: Ch, l: Cl } = blamka(Ch, Cl, Dh, Dl));
({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl });
({ Bh, Bl } = { Bh: rotrBH(Bh, Bl, 63), Bl: rotrBL(Bh, Bl, 63) });
A2_BUF[2 * a] = Al, A2_BUF[2 * a + 1] = Ah;
A2_BUF[2 * b] = Bl, A2_BUF[2 * b + 1] = Bh;
A2_BUF[2 * c] = Cl, A2_BUF[2 * c + 1] = Ch;
A2_BUF[2 * d] = Dl, A2_BUF[2 * d + 1] = Dh;
}
function P(v00, v01, v02, v03, v04, v05, v06, v07, v08, v09, v10, v11, v12, v13, v14, v15) {
G(v00, v04, v08, v12);
G(v01, v05, v09, v13);
G(v02, v06, v10, v14);
G(v03, v07, v11, v15);
G(v00, v05, v10, v15);
G(v01, v06, v11, v12);
G(v02, v07, v08, v13);
G(v03, v04, v09, v14);
}
function block(x, xPos, yPos, outPos, needXor) {
for (let i = 0; i < 256; i++)
A2_BUF[i] = x[xPos + i] ^ x[yPos + i];
for (let i = 0; i < 128; i += 16) {
P(i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, i + 8, i + 9, i + 10, i + 11, i + 12, i + 13, i + 14, i + 15);
}
for (let i = 0; i < 16; i += 2) {
P(i, i + 1, i + 16, i + 17, i + 32, i + 33, i + 48, i + 49, i + 64, i + 65, i + 80, i + 81, i + 96, i + 97, i + 112, i + 113);
}
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];
clean(A2_BUF);
}
function Hp(A, dkLen) {
const A8 = u8(A);
const T = new Uint32Array(1);
const T8 = u8(T);
T[0] = swap8IfBE(dkLen);
if (dkLen <= 64)
return blake2b.create({ dkLen }).update(T8).update(A8).digest();
const out = new Uint8Array(dkLen);
let V = blake2b.create({}).update(T8).update(A8).digest();
let pos = 0;
out.set(V.subarray(0, 32));
pos += 32;
for (; dkLen - pos > 64; pos += 32) {
const Vh = blake2b.create({}).update(V);
Vh.digestInto(V);
Vh.destroy();
out.set(V.subarray(0, 32), pos);
}
out.set(blake2b(V, { dkLen: dkLen - pos }), pos);
clean(V, T);
return out;
}
function indexAlpha(r, s, laneLen, segmentLen, index, randL, sameLane = false) {
let area;
if (r === 0) {
if (s === 0)
area = index - 1;
else if (sameLane)
area = s * segmentLen + index - 1;
else
area = s * segmentLen + (index == 0 ? -1 : 0);
} else if (sameLane)
area = laneLen - segmentLen + index - 1;
else
area = laneLen - segmentLen + (index == 0 ? -1 : 0);
const startPos = r !== 0 && s !== ARGON2_SYNC_POINTS - 1 ? (s + 1) * segmentLen : 0;
const rel = area - 1 - mul(area, mul(randL, randL).h).h;
return (startPos + rel) % laneLen;
}
var maxUint32 = Math.pow(2, 32);
function isU32(num) {
return Number.isSafeInteger(num) && num >= 0 && num < maxUint32;
}
function argon2Opts(opts) {
const merged = {
version: 19,
dkLen: 32,
maxmem: maxUint32 - 1,
asyncTick: 10
};
for (let [k, v] of Object.entries(opts))
if (v !== void 0)
merged[k] = v;
const { dkLen, p, m, t, version, onProgress, asyncTick } = merged;
if (!isU32(dkLen) || dkLen < 4)
throw new Error('"dkLen" must be 4..');
if (!isU32(p) || p < 1 || p >= Math.pow(2, 24))
throw new Error('"p" must be 1..2^24');
if (!isU32(m))
throw new Error('"m" must be 0..2^32');
if (!isU32(t) || t < 1)
throw new Error('"t" (iterations) must be 1..2^32');
if (onProgress !== void 0 && typeof onProgress !== "function")
throw new Error('"progressCb" must be a function');
anumber(asyncTick, "asyncTick");
if (!isU32(m) || m < 8 * p)
throw new Error('"m" (memory) must be at least 8*p bytes');
if (version !== 16 && version !== 19)
throw new Error('"version" must be 0x10 or 0x13, got ' + version);
return merged;
}
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');
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);
key = abytesOrZero(key, "key");
personalization = abytesOrZero(personalization, "personalization");
const h = blake2b.create();
const BUF = new Uint32Array(1);
const BUF8 = u8(BUF);
for (let item of [p, dkLen, m, t, version, type]) {
BUF[0] = swap8IfBE(item);
h.update(BUF8);
}
for (let i of [password, salt, key, personalization]) {
BUF[0] = swap8IfBE(i.length);
h.update(BUF8).update(i);
}
const H0 = new Uint32Array(18);
const H0_8 = u8(H0);
h.digestInto(H0_8);
const lanes = p;
const mP = 4 * p * Math.floor(m / (ARGON2_SYNC_POINTS * p));
const laneLen = Math.floor(mP / p);
const segmentLen = Math.floor(laneLen / ARGON2_SYNC_POINTS);
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);
for (let l = 0; l < p; l++) {
const i = 256 * laneLen * l;
H0[17] = swap8IfBE(l);
H0[16] = swap8IfBE(0);
B.set(swap32IfBE(u32(Hp(H0, 1024))), i);
H0[16] = swap8IfBE(1);
B.set(swap32IfBE(u32(Hp(H0, 1024))), i + 256);
}
let perBlock = () => {
};
if (onProgress) {
const totalBlock = t * ARGON2_SYNC_POINTS * p * segmentLen - 2 * p;
const callbackPer = Math.max(Math.floor(totalBlock / 1e4), 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 };
}
function argon2Output(B, p, laneLen, dkLen) {
const B_final = new Uint32Array(256);
for (let l = 0; l < p; l++)
for (let j = 0; j < 256; j++)
B_final[j] ^= B[256 * (laneLen * l + laneLen - 1) + j];
const res = Hp(swap32IfBE(B_final), dkLen);
clean(B, B_final);
return res;
}
function processBlock(B, address, l, r, s, index, laneLen, segmentLen, lanes, offset, prev, dataIndependent, needXor) {
if (offset % laneLen)
prev = offset - 1;
let randL, randH;
if (dataIndependent) {
let i128 = index % 128;
if (i128 === 0) {
address[256 + 12]++;
block(address, 256, 2 * 256, 0, false);
block(address, 0, 2 * 256, 0, false);
}
randL = address[2 * i128];
randH = address[2 * i128 + 1];
} else {
const T = 256 * prev;
randL = B[T];
randH = B[T + 1];
}
const refLane = r === 0 && s === 0 ? l : randH % lanes;
const refPos = indexAlpha(r, s, laneLen, segmentLen, index, randL, refLane == l);
const refBlock = laneLen * refLane + refPos;
block(B, 256 * prev, 256 * refBlock, offset * 256, needXor);
}
function argon2(type, password, salt, opts) {
const { mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock } = argon2Init(password, salt, type, opts);
const address = new Uint32Array(3 * 256);
address[256 + 6] = mP;
address[256 + 8] = t;
address[256 + 10] = type;
for (let r = 0; r < t; r++) {
const needXor = r !== 0 && version === 19;
address[256 + 0] = r;
for (let s = 0; s < ARGON2_SYNC_POINTS; s++) {
address[256 + 4] = s;
const dataIndependent = type == AT.Argon2i || type == AT.Argon2id && r === 0 && s < 2;
for (let l = 0; l < p; l++) {
address[256 + 2] = l;
address[256 + 12] = 0;
let startPos = 0;
if (r === 0 && s === 0) {
startPos = 2;
if (dataIndependent) {
address[256 + 12]++;
block(address, 256, 2 * 256, 0, false);
block(address, 0, 2 * 256, 0, false);
}
}
let offset = l * laneLen + s * segmentLen + startPos;
let prev = offset % laneLen ? offset - 1 : offset + laneLen - 1;
for (let index = startPos; index < segmentLen; index++, offset++, prev++) {
perBlock();
processBlock(B, address, l, r, s, index, laneLen, segmentLen, lanes, offset, prev, dataIndependent, needXor);
}
}
}
}
clean(address);
return argon2Output(B, p, laneLen, dkLen);
}
var argon2id = (password, salt, opts) => argon2(AT.Argon2id, password, salt, opts);
// entry.js
globalThis.NobleArgon2 = { argon2id };
})();
+61
View File
@@ -69,6 +69,67 @@ test('deriveKeyAndVerifier: v2 verifier is decoupled SHA-256(keyHex + domain)',
assert.notEqual(verifier, keyHex, 'v2 verifier must not leak the AES key'); assert.notEqual(verifier, keyHex, 'v2 verifier must not leak the AES key');
}); });
// ---- Argon2id KDF (js/argon2.js — vendored @noble/hashes) -------------------
test('vendored argon2id matches the RFC 9106 §5.3 test vector', () => {
// Independent cross-check that the bundled library is correct (not just
// self-consistent). Same vector used to validate the vendored bundle.
const out = T.NobleArgon2.argon2id(
new Uint8Array(32).fill(1), new Uint8Array(16).fill(2),
{ t: 3, m: 32, p: 4, dkLen: 32,
key: new Uint8Array(8).fill(3),
personalization: new Uint8Array(12).fill(4), version: 0x13 });
assert.equal(T.bytesToHex(out),
'0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659');
});
test('isDecoupledVerifierAlgo: all -v2 markers decouple, legacy does not', () => {
assert.equal(T.isDecoupledVerifierAlgo(T.HASH_ALGO_V2), true);
assert.equal(T.isDecoupledVerifierAlgo(T.HASH_ALGO_ARGON2), true);
assert.equal(T.isDecoupledVerifierAlgo('pbkdf2-sha256'), false);
assert.equal(T.isDecoupledVerifierAlgo('pbkdf2'), false);
assert.equal(T.isDecoupledVerifierAlgo(''), false);
});
test('deriveKeyAndVerifier: argon2id branch derives an Argon2 key, NOT PBKDF2', async () => {
const pwd = 'correct horse battery staple';
const saltHex = 'a1b2c3d4e5f6a1b2';
// The raw key bytes must equal argon2id(pwd, saltHex-utf8, params) — cross
// checked against the vendored lib directly (small params for test speed).
const params = { m: 256, t: 1, p: 1 };
const expectKey = T.bytesToHex(T.NobleArgon2.argon2id(
new TextEncoder().encode(pwd), new TextEncoder().encode(saltHex),
{ t: params.t, m: params.m, p: params.p, dkLen: 32, version: 0x13 }));
const { cryptoKey, verifier } = await T.deriveKeyAndVerifier(
pwd, saltHex, 0, T.HASH_ALGO_ARGON2, params);
const raw = await ctx.crypto.subtle.exportKey('raw', cryptoKey);
assert.equal(T.bytesToHex(new Uint8Array(raw)), expectKey, 'AES key must be the Argon2id output');
// Differs from what PBKDF2 would give for the same pw/salt (different KDF).
assert.notEqual(T.bytesToHex(new Uint8Array(raw)), refKeyHex(pwd, saltHex, 100000));
// argon2id-v2 is a '-v2' scheme → verifier is decoupled SHA-256, not the key.
assert.equal(verifier, refSha256Hex(expectKey + T.AUTH_VERIFIER_DOMAIN));
assert.notEqual(verifier, expectKey, 'argon2id-v2 verifier must not leak the key');
});
test('deriveKeyAndVerifier: argon2id uses ARGON2_DEFAULT_PARAMS when none passed', async () => {
// OWASP baseline is the default (m=19456, t=2, p=1). Just assert the
// default object is what we expect; deriving at 19 MiB is left to one
// explicit round-trip below to keep the suite fast.
assert.deepEqual({ ...T.ARGON2_DEFAULT_PARAMS }, { m: 19456, t: 2, p: 1 });
});
test('encrypt/decrypt round-trips under an Argon2id-derived key', async () => {
const { cryptoKey } = await T.deriveKeyAndVerifier(
'master', 'saltsaltsalt', 0, T.HASH_ALGO_ARGON2, { m: 512, t: 1, p: 1 });
T.state.cryptoKey = cryptoKey;
const { encrypted, iv } = await T.encryptPwd('argon-secret 🔐');
assert.equal(await T.decryptPwd(encrypted, iv), 'argon-secret 🔐');
});
test('verifierFromKeyHex: pure mapping matches deriveKeyAndVerifier', async () => { test('verifierFromKeyHex: pure mapping matches deriveKeyAndVerifier', async () => {
const keyHex = 'ab'.repeat(32); const keyHex = 'ab'.repeat(32);
assert.equal(await T.verifierFromKeyHex(keyHex, 'anything-legacy'), keyHex); assert.equal(await T.verifierFromKeyHex(keyHex, 'anything-legacy'), keyHex);
+9 -1
View File
@@ -113,6 +113,12 @@ function loadApp(overrides = {}) {
vm.createContext(sandbox); vm.createContext(sandbox);
// Load the vendored Argon2 bundle first (index.html loads it before
// app.js). It assigns globalThis.NobleArgon2 — needed by the argon2id
// KDF branch in deriveKeyBytes.
const ARGON2_JS = path.join(__dirname, '..', 'argon2.js');
vm.runInContext(fs.readFileSync(ARGON2_JS, 'utf8'), sandbox, { filename: 'argon2.js' });
let src = fs.readFileSync(APP_JS, 'utf8'); let src = fs.readFileSync(APP_JS, 'utf8');
// Export epilogue — surface the lexical (const) symbols we test, plus a // Export epilogue — surface the lexical (const) symbols we test, plus a
@@ -124,8 +130,10 @@ function loadApp(overrides = {}) {
// crypto // crypto
bytesToHex, hexToBytes: (typeof hexToBytes !== 'undefined' ? hexToBytes : undefined), bytesToHex, hexToBytes: (typeof hexToBytes !== 'undefined' ? hexToBytes : undefined),
verifierFromKeyHex, deriveKeyAndVerifier, computeVerifier, verifierFromKeyHex, deriveKeyAndVerifier, computeVerifier,
deriveKeyBytes, isDecoupledVerifierAlgo,
encryptPwd, decryptPwd, sha256Hex, encryptPwd, decryptPwd, sha256Hex,
HASH_ALGO_V2, AUTH_VERIFIER_DOMAIN, HASH_ALGO_V2, HASH_ALGO_ARGON2, AUTH_VERIFIER_DOMAIN, ARGON2_DEFAULT_PARAMS,
NobleArgon2: (typeof NobleArgon2 !== 'undefined' ? NobleArgon2 : undefined),
// csv // csv
parseCSV, findColumn, parseEntriesFromCSV, parseCSV, findColumn, parseEntriesFromCSV,
// strength // strength