fix(auth): a code is counted before it is checked
Codes sent at once were all checked before the tenth refused began a wait (the re-review's first finding: 30 codes from three networks, all checked), so whoever knew the password could open sign-ins ahead and guess far more than ten before codes waited. Every attempt at a code for an account is now counted before its code is checked, one at a time per account (a transaction holding an advisory lock on the account's key), and the tenth in a row begins the wait as it is counted: of codes sent at once at most ten are checked, the rest refused (codesWait). A code accepted clears it all, that wait too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1908e4f362
commit
ae41937e0b
1 changed files with 158 additions and 74 deletions
|
|
@ -844,6 +844,56 @@ void main() {
|
|||
expect(mailbox.codesWait, hasLength(2));
|
||||
});
|
||||
|
||||
test('sent at once are counted before they are checked: ten '
|
||||
'checked, the rest wait', () async {
|
||||
await annaComplete();
|
||||
final challenges = [
|
||||
for (var network = 0; network < 3; network++)
|
||||
for (var i = 0; i < 10; i++)
|
||||
await atNow(
|
||||
() => beginPasswordSignIn(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: '203.0.113.$network',
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final reasons = await Future.wait([
|
||||
for (final (i, challenge) in challenges.indexed)
|
||||
atNow(() async {
|
||||
final answer = device.answer(challenge);
|
||||
try {
|
||||
await endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '${100000 + i}',
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
return null;
|
||||
} on PasswordRefusedException catch (refusal) {
|
||||
return refusal.reason;
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
reasons.where((r) => r == PasswordRefusal.wrongCredentials),
|
||||
hasLength(10),
|
||||
);
|
||||
expect(
|
||||
reasons.where((r) => r == PasswordRefusal.codesWait),
|
||||
hasLength(20),
|
||||
);
|
||||
final row = await SecondFactorFailures.db.findFirstRow(session);
|
||||
expect(row?.failures, 10);
|
||||
expect(row?.waits, 1);
|
||||
});
|
||||
|
||||
test('wait a day at the most', () {
|
||||
expect(codeWait(0), const Duration(hours: 1));
|
||||
expect(codeWait(1), const Duration(hours: 2));
|
||||
|
|
|
|||
Loading…
Reference in a new issue