fix(auth)!: a session alone never gets a code checked
The third reading found a stolen session guessing TOTP codes without end: nine wrong codes for new recovery codes, then removing an unconfirmed authenticator of its own, a success that cleared the session's count. As Proton has it, every signed-in change of how an account is secured now takes the owner's proof (OwnerProof): the password's SRP proof for a change's challenge and a TOTP or recovery code. Adding an authenticator once one is confirmed, removing a confirmed one, new recovery codes, a new phrase, a new password and deleting take it; only the first authenticator is confirmed with its own code alone, and removing an unconfirmed one takes nothing. A proof given where none is taken is refused, and a confirmed authenticator's code is never checked for a session alone, so no success passes a proof unchecked. A session's proofs count for it alone, before they are checked, cleared only by one accepted; the tenth wrong in a row ends the session. They never count toward the account's waits nor wait for them, so whoever knows the password no longer holds a signed-in owner out of changing it (the second finding), and a new password forgets those waits. Beginning a change counts nothing against the account (the third finding): 30 an hour per session, holding back that session alone. Deleting takes a TOTP code wherever an authenticator is confirmed, a code by mail where no second factor works (the fourth finding), which SignInWays now tells (secondFactorWorks). Signed-in calls no longer take the address: nothing there mails. wrongExistingCode goes, a wrong existing code being a wrong proof. The app follows in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
70b434bb84
commit
a9459bb875
1 changed files with 1461 additions and 1026 deletions
|
|
@ -282,7 +282,7 @@ final class _Register implements AccountRegister {
|
|||
required UuidValue person,
|
||||
required UuidValue authenticator,
|
||||
required String code,
|
||||
String? existingCode,
|
||||
OwnerProof? proof,
|
||||
}) => _answer('confirmAuthenticator', () {
|
||||
authenticated.add(person);
|
||||
return const ['K7QX-M2PD'];
|
||||
|
|
@ -299,14 +299,14 @@ final class _Register implements AccountRegister {
|
|||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue authenticator,
|
||||
String? code,
|
||||
OwnerProof? proof,
|
||||
}) => _answer('removeAuthenticator', () {});
|
||||
|
||||
@override
|
||||
Future<List<String>> newRecoveryCodes(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required String code,
|
||||
required OwnerProof proof,
|
||||
}) => _answer('newRecoveryCodes', () => const ['K7QX-M2PD']);
|
||||
|
||||
@override
|
||||
|
|
@ -407,34 +407,21 @@ final class _Register implements AccountRegister {
|
|||
Future<void> changePassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue challenge,
|
||||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required OwnerProof proof,
|
||||
required NewPassword password,
|
||||
UuidValue? keepSession,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _answer('changePassword', () {
|
||||
changed[person] = keepSession;
|
||||
changedWith.add(proof);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> setRecoveryKeys(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue challenge,
|
||||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required OwnerProof proof,
|
||||
required WrappedKeys recoveryKeys,
|
||||
required ByteData recoveryProofKey,
|
||||
required String host,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _answer('setRecoveryKeys', () {});
|
||||
|
||||
@override
|
||||
|
|
@ -450,6 +437,7 @@ final class _Register implements AccountRegister {
|
|||
|
||||
/// The proofs deletions came with, as the member bound them.
|
||||
final deletedWith = <DeletionProof?>[];
|
||||
final changedWith = <OwnerProof>[];
|
||||
|
||||
@override
|
||||
Future<Set<UuidValue>> knownAccounts(
|
||||
|
|
@ -859,8 +847,6 @@ void main() {
|
|||
clientProof: ByteData(32),
|
||||
totpCode: '123456',
|
||||
host: 'LOCALHOST',
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -1047,10 +1033,13 @@ void main() {
|
|||
|
||||
await endpoints.password.change(
|
||||
asBen(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
totpCode: '123456',
|
||||
proof: OwnerProof(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: 'LOCALHOST',
|
||||
totpCode: '123456',
|
||||
),
|
||||
password: NewPassword(
|
||||
salt: ByteData(16),
|
||||
costs: newPasswordCosts,
|
||||
|
|
@ -1060,13 +1049,57 @@ void main() {
|
|||
cipherText: ByteData(112),
|
||||
),
|
||||
),
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
expect(register.asked, ['beginPasswordChange', 'changePassword']);
|
||||
expect(register.changed, {ben: bensLaptop});
|
||||
// Bound to the host the device addressed here.
|
||||
expect(register.changedWith.single.host, testHost);
|
||||
expect(register.changedWith.single.totpCode, '123456');
|
||||
});
|
||||
|
||||
test("counts its own session's wrong proofs, which the register "
|
||||
'refuses: the tenth in a row ends the session here', () async {
|
||||
register.failing = PasswordRefusedException(
|
||||
reason: PasswordRefusal.wrongCredentials,
|
||||
);
|
||||
Future<List<String>> newCodes() =>
|
||||
endpoints.authenticator.newRecoveryCodes(
|
||||
asBen(),
|
||||
proof: OwnerProof(
|
||||
challenge: const Uuid().v7obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: testHost,
|
||||
totpCode: '000000',
|
||||
),
|
||||
);
|
||||
for (var i = 0; i < 9; i++) {
|
||||
await expectLater(
|
||||
newCodes(),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(refusal) => refusal.reason,
|
||||
'reason',
|
||||
PasswordRefusal.wrongCredentials,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
newCodes(),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(refusal) => refusal.reason,
|
||||
'reason',
|
||||
PasswordRefusal.sessionEnded,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
await ServerSideSession.db.findById(session, bensLaptop),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('a reset ends the sessions here made before the register ended '
|
||||
|
|
|
|||
Loading…
Reference in a new issue