fix(recovery): echo Argon2 params in the redeem response

The redeem client already reads `state.argon2Params = r.argon2 || null`, but
the server never sent `argon2`, so it always fell back to null → the default
params. Correct today (every account uses ARGON2_DEFAULT_PARAMS), but a latent
wrong-key derivation the day per-account params diverge: same class as the
setup verifier bug — an Argon2 account derived under the wrong params.

Echo m/t/p from the users row like /login/challenge does; omitted for
non-Argon2 accounts (m=0). No client change — it already consumes the field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-25 00:19:16 +01:00
parent 665d8ea4e6
commit e7dca84f40
@@ -279,6 +279,8 @@ var
LUser, LCode, LCodeHash, LIP, LStoredHash, LKdfSalt, LWrappedKey, LWrappedIv,
LSalt, LToken, LCSRF, LAlgo: string;
LUserId, LKdfIters, LCurrentUses, LNewUses: Integer;
LArgM, LArgT, LArgP: Integer;
LArg: TJSONObject;
LQ: TFDQuery;
begin
LIP := GetClientIP(ARequest);
@@ -314,6 +316,7 @@ begin
// Join to users to look up by username + verify the code in one shot.
LQ.SQL.Text :=
'SELECT u.id, u.salt, u.kdf_iterations, u.hash_algo, ' +
' u.argon2_m, u.argon2_t, u.argon2_p, ' +
' rk.code_hash, rk.kdf_salt, rk.wrapped_key, rk.wrapped_iv, rk.remaining_uses ' +
'FROM users u ' +
'LEFT JOIN recovery_keys rk ON rk.user_id = u.id ' +
@@ -331,6 +334,9 @@ begin
LSalt := LQ.FieldByName('salt').AsString;
LKdfIters := LQ.FieldByName('kdf_iterations').AsInteger;
LAlgo := LQ.FieldByName('hash_algo').AsString;
LArgM := LQ.FieldByName('argon2_m').AsInteger;
LArgT := LQ.FieldByName('argon2_t').AsInteger;
LArgP := LQ.FieldByName('argon2_p').AsInteger;
LStoredHash := LQ.FieldByName('code_hash').AsString;
LKdfSalt := LQ.FieldByName('kdf_salt').AsString;
LWrappedKey := LQ.FieldByName('wrapped_key').AsString;
@@ -407,6 +413,18 @@ begin
LObj.AddPair('salt', LSalt);
LObj.AddPair('kdfIterations', TJSONNumber.Create(LKdfIters));
LObj.AddPair('hashAlgo', LAlgo);
// Echo the Argon2 params (as /login/challenge does) so the client derives
// under the account's real params, not the default fallback. Harmless today
// (all accounts use the default) but avoids a silent wrong-key derivation if
// per-account params are ever tuned. Omitted for non-Argon2 accounts (m=0).
if LArgM > 0 then
begin
LArg := TJSONObject.Create;
LArg.AddPair('m', TJSONNumber.Create(LArgM));
LArg.AddPair('t', TJSONNumber.Create(LArgT));
LArg.AddPair('p', TJSONNumber.Create(LArgP));
LObj.AddPair('argon2', LArg);
end;
LObj.AddPair('wrappedKey', LWrappedKey);
LObj.AddPair('wrappedIv', LWrappedIv);
LObj.AddPair('kdfSalt', LKdfSalt);