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:
Tizian 「ティツィアーン」 2026-10-21 10:33:25 +02:00
commit a9459bb875
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -194,15 +194,15 @@ abstract interface class AccountRegister {
required String label,
});
/// Confirms [person]'s [authenticator], with [existingCode] from one
/// confirmed before where the account has one; the first one's recovery
/// codes. Refusals as `confirmAuthenticator`'s.
/// Confirms [person]'s [authenticator], with the owner's [proof] where
/// the account has one confirmed already; the first one's recovery codes.
/// Refusals as `confirmAuthenticator`'s.
Future<List<String>> confirmAuthenticator(
Session session, {
required UuidValue person,
required UuidValue authenticator,
required String code,
String? existingCode,
OwnerProof? proof,
});
/// [person]'s authenticators, the oldest first.
@ -217,14 +217,15 @@ abstract interface class AccountRegister {
Session session, {
required UuidValue person,
required UuidValue authenticator,
String? code,
OwnerProof? proof,
});
/// New recovery codes for [person]. Refusals as `newRecoveryCodes`'.
/// New recovery codes for [person], with the owner's [proof]. Refusals as
/// `newRecoveryCodes`'.
Future<List<String>> newRecoveryCodes(
Session session, {
required UuidValue person,
required String code,
required OwnerProof proof,
});
/// Asks for the reset of a lost authenticator. Refusals as
@ -299,34 +300,20 @@ abstract interface class 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,
});
/// Replaces the wrapping of [person]'s keys under their recovery phrase
/// and the phrase's proof key, with the password's proof and a code.
/// Refusals as `setRecoveryKeys`'.
/// and the phrase's proof key, with the owner's [proof]. Refusals as
/// `setRecoveryKeys`'.
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,
});
/// Deletes the account [person] with everything the register's server has