fix(auth): every proof counts toward the address as it is checked
The ceiling of 30 proofs an hour per address was looked at only when a sign-in began and counted only wrong passwords (the re-review's second finding): sign-ins begun ahead from many networks got 60 wrong passwords checked in an hour, and a right password, not counted, could be told from a wrong one by whether the next sign-in still began. Every proof for an address, of a password or of the recovery phrase, is now recorded before it is checked, whatever its outcome, and refused past 30 an hour (tooManyAttempts); the check at begin stays as an early refusal. A change's proof is no longer counted toward the address (the fifth finding: strangers' wrong passwords held a signed-in owner out of changing the password, a new phrase and deletion). Its challenge has a purpose of its own (change), counted per account as it begins (10 an hour), taken by no sign-in, and a change takes no sign-in's challenge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ae41937e0b
commit
24d46a8503
2 changed files with 246 additions and 37 deletions
|
|
@ -290,6 +290,89 @@ void main() {
|
|||
expect(members.sessionsTold, isEmpty);
|
||||
});
|
||||
|
||||
test("is held back by no stranger's wrong passwords for the "
|
||||
"owner's address", () async {
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await atNow(() async {
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
// From many networks, as a stranger would.
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
privateValue: BigInt.from(4242),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
});
|
||||
if (i % 10 == 9) {
|
||||
// Past a network's ten an hour: the next hour, from scratch.
|
||||
now = now.add(const Duration(minutes: 1));
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.source.equals('begins_per_address_and_network'),
|
||||
);
|
||||
}
|
||||
}
|
||||
await change();
|
||||
});
|
||||
|
||||
test("takes a change's challenge alone, and a sign-in takes none of "
|
||||
'those', () async {
|
||||
final signInChallenge = await atNow(
|
||||
() => endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
atNow(() async {
|
||||
final answer = device.answer(signInChallenge);
|
||||
await endpoints.password.change(
|
||||
onPhone(),
|
||||
challenge: signInChallenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
password: device.newPassword(),
|
||||
host: testHost,
|
||||
);
|
||||
}),
|
||||
refused(PasswordRefusal.challengeExpired),
|
||||
);
|
||||
final changeChallenge = await atNow(
|
||||
() => endpoints.password.beginChange(onPhone()),
|
||||
);
|
||||
await expectLater(
|
||||
atNow(() async {
|
||||
final answer = device.answer(changeChallenge);
|
||||
await endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: changeChallenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
}),
|
||||
refused(PasswordRefusal.challengeExpired),
|
||||
);
|
||||
});
|
||||
|
||||
test('is begun only by someone with a password', () async {
|
||||
final ben = await newAnonymousPerson(session);
|
||||
final bensLaptop = await signInNewDevice(session, ben);
|
||||
|
|
|
|||
|
|
@ -2017,6 +2017,98 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('counts every proof as it is checked: of sign-ins begun ahead '
|
||||
'from many networks, at most 30 an hour are checked', () async {
|
||||
await annaComplete();
|
||||
final challenges = [
|
||||
for (var network = 0; network < 6; network++)
|
||||
for (var i = 0; i < 10; i++)
|
||||
await atNow(
|
||||
() => beginPasswordSignIn(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: '203.0.113.$network',
|
||||
),
|
||||
),
|
||||
];
|
||||
final reasons = <PasswordRefusal>[];
|
||||
for (final challenge in challenges) {
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
privateValue: BigInt.from(4242),
|
||||
);
|
||||
try {
|
||||
await atNow(
|
||||
() => endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
);
|
||||
} on PasswordRefusedException catch (refusal) {
|
||||
reasons.add(refusal.reason);
|
||||
}
|
||||
}
|
||||
expect(
|
||||
reasons.where((r) => r == PasswordRefusal.wrongCredentials),
|
||||
hasLength(30),
|
||||
);
|
||||
expect(
|
||||
reasons.where((r) => r == PasswordRefusal.tooManyAttempts),
|
||||
hasLength(30),
|
||||
);
|
||||
});
|
||||
|
||||
test('counts a right password as a wrong one: the ceiling tells '
|
||||
'neither', () async {
|
||||
await annaComplete();
|
||||
Future<void> proveFrom(String network, {required bool right}) =>
|
||||
atNow(() async {
|
||||
final challenge = await beginPasswordSignIn(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: network,
|
||||
);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
privateValue: right ? null : BigInt.from(4242),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
});
|
||||
for (var i = 0; i < 29; i++) {
|
||||
await proveFrom('203.0.113.${i ~/ 10}', right: false);
|
||||
}
|
||||
// The right password with a wrong code: counted all the same.
|
||||
await proveFrom('198.51.100.1', right: true);
|
||||
await expectLater(
|
||||
atNow(
|
||||
() => beginPasswordSignIn(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
network: '198.51.100.2',
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.tooManyAttempts),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'counts at most 5 proofs begun for an address from a network '
|
||||
'an hour; another network begins its own, and failed proofs '
|
||||
|
|
|
|||
Loading…
Reference in a new issue