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

@ -30,6 +30,8 @@ import 'package:halkyon_base_server/src/generated/password/key_proof_challenge.d
as _iaysae70;
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
as _ijye8mcv;
import 'package:halkyon_base_server/src/generated/password/owner_proof.dart'
as _ifhod73s;
import 'package:halkyon_base_server/src/generated/password/password_challenge.dart'
as _isz0atlx;
import 'package:halkyon_base_server/src/generated/password/password_setup.dart'
@ -830,7 +832,7 @@ class _AuthenticatorEndpoint {
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue authenticator,
required String code,
String? existingCode,
_ifhod73s.OwnerProof? proof,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
@ -846,7 +848,7 @@ class _AuthenticatorEndpoint {
parameters: _ist.testObjectToJson({
'authenticator': authenticator,
'code': code,
'existingCode': existingCode,
'proof': proof,
}),
serializationManager: _serializationManager,
);
@ -896,7 +898,7 @@ class _AuthenticatorEndpoint {
_ida.Future<void> remove(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue authenticator,
String? code,
_ifhod73s.OwnerProof? proof,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
@ -911,7 +913,7 @@ class _AuthenticatorEndpoint {
methodName: 'remove',
parameters: _ist.testObjectToJson({
'authenticator': authenticator,
'code': code,
'proof': proof,
}),
serializationManager: _serializationManager,
);
@ -930,7 +932,7 @@ class _AuthenticatorEndpoint {
_ida.Future<List<String>> newRecoveryCodes(
_ist.TestSessionBuilder sessionBuilder, {
required String code,
required _ifhod73s.OwnerProof proof,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
@ -943,7 +945,7 @@ class _AuthenticatorEndpoint {
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'authenticator',
methodName: 'newRecoveryCodes',
parameters: _ist.testObjectToJson({'code': code}),
parameters: _ist.testObjectToJson({'proof': proof}),
serializationManager: _serializationManager,
);
var _localReturnValue =
@ -1039,15 +1041,8 @@ class _PasswordEndpoint {
_ida.Future<void> change(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue challenge,
required _idt.ByteData clientValue,
required _idt.ByteData clientProof,
String? totpCode,
String? recoveryCode,
required _ifhod73s.OwnerProof proof,
required _ijye8mcv.NewPassword password,
required String host,
required String address,
required String language,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
@ -1061,15 +1056,8 @@ class _PasswordEndpoint {
endpointPath: 'password',
methodName: 'change',
parameters: _ist.testObjectToJson({
'challenge': challenge,
'clientValue': clientValue,
'clientProof': clientProof,
'totpCode': totpCode,
'recoveryCode': recoveryCode,
'proof': proof,
'password': password,
'host': host,
'address': address,
'language': language,
}),
serializationManager: _serializationManager,
);
@ -1088,16 +1076,9 @@ class _PasswordEndpoint {
_ida.Future<void> setRecoveryKeys(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue challenge,
required _idt.ByteData clientValue,
required _idt.ByteData clientProof,
String? totpCode,
String? recoveryCode,
required _ifhod73s.OwnerProof proof,
required _iji0wmu8.WrappedKeys recoveryKeys,
required _idt.ByteData recoveryProofKey,
required String host,
required String address,
required String language,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
@ -1111,16 +1092,9 @@ class _PasswordEndpoint {
endpointPath: 'password',
methodName: 'setRecoveryKeys',
parameters: _ist.testObjectToJson({
'challenge': challenge,
'clientValue': clientValue,
'clientProof': clientProof,
'totpCode': totpCode,
'recoveryCode': recoveryCode,
'proof': proof,
'recoveryKeys': recoveryKeys,
'recoveryProofKey': recoveryProofKey,
'host': host,
'address': address,
'language': language,
}),
serializationManager: _serializationManager,
);