perf(crypto): derive Argon2id via argon2idAsync (unfreeze unlock UI)

deriveKeyBytes now calls NobleArgon2.argon2idAsync instead of the sync
argon2id, so it yields to the event loop periodically and the busy/unlock
spinner keeps animating instead of freezing ~0.65 s during login, register,
and master-pw rotation. Same result (both RFC-9106-verified); all callers
already await deriveKeyBytes so no call-site changes.

- Re-vendored js/argon2.js to export argon2idAsync alongside argon2id
  (re-bundled from @noble/hashes@2.2.0; both variants pass the RFC 9106 §5.3
  vector). 27KB → 29KB.
- Added a sync/async parity test. 63/63 green.
- Closes the last open item of CODE_AUDIT §1.2.

NOTE: argon2.js grew — run BuildAssets to re-embed it before the next
Delphi build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-08 19:41:59 +01:00
parent 9e424efaf4
commit dd86b2bd23
4 changed files with 70 additions and 6 deletions
+47 -2
View File
@@ -1,4 +1,4 @@
/* @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. */
/* @noble/hashes argon2id (sync+async) 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;
@@ -90,6 +90,8 @@
return arr;
}
var swap32IfBE = isLE ? (u) => u : byteSwap32;
var nextTick = async () => {
};
function utf8ToBytes(str) {
if (typeof str !== "string")
throw new TypeError("string expected");
@@ -951,7 +953,50 @@
return argon2Output(B, p, laneLen, dkLen);
}
var argon2id = (password, salt, opts) => argon2(AT.Argon2id, password, salt, opts);
async function argon2Async(type, password, salt, opts) {
const { mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock, asyncTick } = argon2Init(password, salt, type, opts);
const address = new Uint32Array(3 * 256);
address[256 + 6] = mP;
address[256 + 8] = t;
address[256 + 10] = type;
let ts = Date.now();
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);
const diff = Date.now() - ts;
if (!(diff >= 0 && diff < asyncTick)) {
await nextTick();
ts += diff;
}
}
}
}
}
clean(address);
return argon2Output(B, p, laneLen, dkLen);
}
var argon2idAsync = (password, salt, opts) => argon2Async(AT.Argon2id, password, salt, opts);
// entry.js
globalThis.NobleArgon2 = { argon2id };
globalThis.NobleArgon2 = { argon2id, argon2idAsync };
})();