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:
+4
-2
@@ -74,8 +74,10 @@ décplé). Détails dans le CLAUDE.md « Auth-hash schemes ».
|
|||||||
**Validé runtime** : un compte ayant tourné sa master pw affiche
|
**Validé runtime** : un compte ayant tourné sa master pw affiche
|
||||||
`hash_algo=argon2id-v2` (m=19456, t=2, p=1) et se reconnecte/déchiffre.
|
`hash_algo=argon2id-v2` (m=19456, t=2, p=1) et se reconnecte/déchiffre.
|
||||||
|
|
||||||
**Reste (mineur)** : dériver via `argon2idAsync` pour ne pas geler l'UI
|
✅ **Async (2026-07-05)** : `deriveKeyBytes` utilise `argon2idAsync` (bundle
|
||||||
~0.65 s (actuellement sync).
|
re-vendé pour l'exposer) — cède la main à l'event loop pour que le spinner
|
||||||
|
s'anime au lieu de figer ~0.65 s. Parité sync/async testée sur le vecteur
|
||||||
|
RFC 9106. **Plus rien d'ouvert sur §1.2.**
|
||||||
|
|
||||||
### 1.3 🟡 Métadonnées en clair
|
### 1.3 🟡 Métadonnées en clair
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -96,10 +96,13 @@ async function verifierFromKeyHex(keyHex, algo) {
|
|||||||
async function deriveKeyBytes(pwd, saltHex, algo, iterations, argonParams) {
|
async function deriveKeyBytes(pwd, saltHex, algo, iterations, argonParams) {
|
||||||
const enc = new TextEncoder();
|
const enc = new TextEncoder();
|
||||||
if (algo === HASH_ALGO_ARGON2) {
|
if (algo === HASH_ALGO_ARGON2) {
|
||||||
if (typeof NobleArgon2 === 'undefined' || !NobleArgon2 || !NobleArgon2.argon2id)
|
if (typeof NobleArgon2 === 'undefined' || !NobleArgon2 || !NobleArgon2.argon2idAsync)
|
||||||
throw new Error('Argon2 library not loaded (js/argon2.js missing?)');
|
throw new Error('Argon2 library not loaded (js/argon2.js missing?)');
|
||||||
const p = argonParams || ARGON2_DEFAULT_PARAMS;
|
const p = argonParams || ARGON2_DEFAULT_PARAMS;
|
||||||
return NobleArgon2.argon2id(enc.encode(pwd), enc.encode(saltHex),
|
// Async variant yields to the event loop periodically so the unlock
|
||||||
|
// spinner keeps animating instead of freezing ~0.65 s. Same result as
|
||||||
|
// the sync argon2id (both RFC-9106-verified).
|
||||||
|
return await NobleArgon2.argon2idAsync(enc.encode(pwd), enc.encode(saltHex),
|
||||||
{ t: p.t, m: p.m, p: p.p, dkLen: 32, version: 0x13 });
|
{ t: p.t, m: p.m, p: p.p, dkLen: 32, version: 0x13 });
|
||||||
}
|
}
|
||||||
// PBKDF2-SHA256 (default / legacy).
|
// PBKDF2-SHA256 (default / legacy).
|
||||||
|
|||||||
+47
-2
@@ -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 __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 __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;
|
return arr;
|
||||||
}
|
}
|
||||||
var swap32IfBE = isLE ? (u) => u : byteSwap32;
|
var swap32IfBE = isLE ? (u) => u : byteSwap32;
|
||||||
|
var nextTick = async () => {
|
||||||
|
};
|
||||||
function utf8ToBytes(str) {
|
function utf8ToBytes(str) {
|
||||||
if (typeof str !== "string")
|
if (typeof str !== "string")
|
||||||
throw new TypeError("string expected");
|
throw new TypeError("string expected");
|
||||||
@@ -951,7 +953,50 @@
|
|||||||
return argon2Output(B, p, laneLen, dkLen);
|
return argon2Output(B, p, laneLen, dkLen);
|
||||||
}
|
}
|
||||||
var argon2id = (password, salt, opts) => argon2(AT.Argon2id, password, salt, opts);
|
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
|
// entry.js
|
||||||
globalThis.NobleArgon2 = { argon2id };
|
globalThis.NobleArgon2 = { argon2id, argon2idAsync };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -83,6 +83,20 @@ test('vendored argon2id matches the RFC 9106 §5.3 test vector', () => {
|
|||||||
'0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659');
|
'0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('argon2idAsync matches argon2id (deriveKeyBytes uses the async variant)', async () => {
|
||||||
|
// deriveKeyBytes now calls argon2idAsync (yields to the event loop so the
|
||||||
|
// unlock spinner animates). It must produce the exact same key as the sync
|
||||||
|
// path — assert parity on the RFC vector.
|
||||||
|
const args = [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 }];
|
||||||
|
const sync = T.NobleArgon2.argon2id(...args);
|
||||||
|
const async_ = await T.NobleArgon2.argon2idAsync(...args);
|
||||||
|
assert.equal(T.bytesToHex(async_), T.bytesToHex(sync));
|
||||||
|
assert.equal(T.bytesToHex(async_),
|
||||||
|
'0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659');
|
||||||
|
});
|
||||||
|
|
||||||
test('isDecoupledVerifierAlgo: all -v2 markers decouple, legacy does not', () => {
|
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_V2), true);
|
||||||
assert.equal(T.isDecoupledVerifierAlgo(T.HASH_ALGO_ARGON2), true);
|
assert.equal(T.isDecoupledVerifierAlgo(T.HASH_ALGO_ARGON2), true);
|
||||||
|
|||||||
Reference in New Issue
Block a user