fix(recovery): accept -v2 / argon2id verifiers in setup

/recovery-key/setup verified the master password with a local copy of the
verifier check that only knew pbkdf2 (legacy) and pbkdf2-sha256. Any account
on a decoupled scheme — pbkdf2-sha256-v2 or argon2id-v2, the current default
after a master-pw rotation — fell through both branches and was rejected as
"invalid password" even when correct, so recovery-key setup was impossible.

Match the canonical VerifierToStoredHash (PM.Handler.Auth): legacy stores the
verifier raw, every other algo is SHA256-wrapped identically. One branch, plus
a keep-in-sync note so the copy doesn't drift again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-25 00:00:40 +01:00
parent 9b7d04ee46
commit 665d8ea4e6
@@ -82,11 +82,15 @@ begin
if AVerifier <> '' then if AVerifier <> '' then
begin begin
// ZK: client computed PBKDF2 hex locally. Server only does the wrap. // ZK: client computed the verifier locally. Server only wraps it — legacy
// pbkdf2 stores it raw, EVERY other algo (pbkdf2-sha256, pbkdf2-sha256-v2,
// argon2id-v2) is SHA256-wrapped identically. Mirrors VerifierToStoredHash
// in PM.Handler.Auth — keep in sync. The old `else if =CURRENT` silently
// rejected -v2 / argon2id accounts (the current default) as wrong-password.
if Length(AVerifier) <> 64 then Exit; if Length(AVerifier) <> 64 then Exit;
if SameText(LAlgo, HASH_ALGO_LEGACY) then if SameText(LAlgo, HASH_ALGO_LEGACY) then
Result := ConstantTimeEquals(AVerifier, LStoredHash) Result := ConstantTimeEquals(AVerifier, LStoredHash)
else if SameText(LAlgo, HASH_ALGO_CURRENT) then else
Result := ConstantTimeEquals(SHA256Hex(AVerifier), LStoredHash); Result := ConstantTimeEquals(SHA256Hex(AVerifier), LStoredHash);
Exit; Exit;
end; end;