fix(auth): every call with a code after the password takes the address

Whoever guessed codes decided whether the owner was mailed (the re-review's
fourth finding): the address a sign-in sent was optional, so a guesser left
it out and no mail went, and a wait begun without a mail used up the one
mail there was. Every call carrying a code after the password or the phrase
proved (signing in, the phrase's reset, a change of password, a new phrase,
deletion) now takes the account's address and the language of its mails,
and is refused (wrongCredentials, as anything wrong after the password)
before any code is checked when the address isn't the account's; every
wait mails the owner, not only the first. The app sends the address the
person typed, or for changes while signed in the one this device signed in
with, and says so where it doesn't know it (addressUnknown).

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-20 23:08:25 +02:00
commit 2a21672b43
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 795 additions and 347 deletions

View file

@ -278,8 +278,8 @@ class EndpointAccountRecovery extends _isc.EndpointRef {
String? totpCode,
String? recoveryCode,
required String host,
String? address,
String? language,
required String address,
required String language,
}) => caller.callServerEndpoint<_iacc.AuthSuccess>(
'halkyon_base.accountRecovery',
'resetPassword',
@ -455,8 +455,10 @@ class EndpointPassword extends _isc.EndpointRef {
/// Changes the caller's password to [password] (the same keys, wrapped
/// under the new password's key on the device), with the old password's
/// proof for [challenge] ([beginChange]) and a [totpCode] or
/// [recoveryCode]. Every other session of theirs ends, in every service of
/// the instance; this one stays. Refusals as `changePassword`'s.
/// [recoveryCode], and the account's [address] as the device typed it
/// (checked before the code; the owner is mailed in [language] should
/// codes come to wait). Every other session of theirs ends, in every
/// service of the instance; this one stays. Refusals as `changePassword`'s.
_ida.Future<void> change({
required _isc.UuidValue challenge,
required _idt.ByteData clientValue,
@ -465,6 +467,8 @@ class EndpointPassword extends _isc.EndpointRef {
String? recoveryCode,
required _i95l60wq.NewPassword password,
required String host,
required String address,
required String language,
}) => caller.callServerEndpoint<void>(
'halkyon_base.password',
'change',
@ -476,14 +480,17 @@ class EndpointPassword extends _isc.EndpointRef {
'recoveryCode': recoveryCode,
'password': password,
'host': host,
'address': address,
'language': language,
},
);
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
/// public half of its proof key, the old one, with the password's proof
/// for [challenge] ([beginChange]) and a [totpCode] or [recoveryCode], as
/// [change] takes. Refusals as `setRecoveryKeys`'.
/// for [challenge] ([beginChange]) and a [totpCode] or [recoveryCode], with
/// the account's [address] and [language], as [change] takes. Refusals as
/// `setRecoveryKeys`'.
_ida.Future<void> setRecoveryKeys({
required _isc.UuidValue challenge,
required _idt.ByteData clientValue,
@ -493,6 +500,8 @@ class EndpointPassword extends _isc.EndpointRef {
required _i3so7e0p.WrappedKeys recoveryKeys,
required _idt.ByteData recoveryProofKey,
required String host,
required String address,
required String language,
}) => caller.callServerEndpoint<void>(
'halkyon_base.password',
'setRecoveryKeys',
@ -505,6 +514,8 @@ class EndpointPassword extends _isc.EndpointRef {
'recoveryKeys': recoveryKeys,
'recoveryProofKey': recoveryProofKey,
'host': host,
'address': address,
'language': language,
},
);
}
@ -550,8 +561,8 @@ class EndpointPasswordSignIn extends _isc.EndpointRef {
_isc.UuidValue? request,
String? code,
required String host,
String? address,
String? language,
required String address,
required String language,
}) => caller.callServerEndpoint<_ix90myvx.PasswordSignedIn>(
'halkyon_base.passwordSignIn',
'complete',

View file

@ -27,6 +27,8 @@ abstract class DeletionProof
this.totpCode,
this.recoveryCode,
required this.host,
required this.address,
required this.language,
});
factory DeletionProof({
@ -36,6 +38,8 @@ abstract class DeletionProof
String? totpCode,
String? recoveryCode,
required String host,
required String address,
required String language,
}) = _DeletionProofImpl;
factory DeletionProof.fromJson(Map<String, dynamic> jsonSerialization) {
@ -52,6 +56,8 @@ abstract class DeletionProof
totpCode: jsonSerialization['totpCode'] as String?,
recoveryCode: jsonSerialization['recoveryCode'] as String?,
host: jsonSerialization['host'] as String,
address: jsonSerialization['address'] as String,
language: jsonSerialization['language'] as String,
);
}
@ -69,6 +75,13 @@ abstract class DeletionProof
/// checks it is its own before the register binds the proof to it.
String host;
/// The account's address as the device typed it, and the language its
/// mails are worded in: checked before the code, so the owner is mailed
/// should codes come to wait (the re-review's fourth finding).
String address;
String language;
/// Returns a shallow copy of this [DeletionProof]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@ -79,6 +92,8 @@ abstract class DeletionProof
String? totpCode,
String? recoveryCode,
String? host,
String? address,
String? language,
});
@override
Map<String, dynamic> toJson() {
@ -90,6 +105,8 @@ abstract class DeletionProof
if (totpCode != null) 'totpCode': totpCode,
if (recoveryCode != null) 'recoveryCode': recoveryCode,
'host': host,
'address': address,
'language': language,
};
}
@ -103,6 +120,8 @@ abstract class DeletionProof
if (totpCode != null) 'totpCode': totpCode,
if (recoveryCode != null) 'recoveryCode': recoveryCode,
'host': host,
'address': address,
'language': language,
};
}
@ -122,6 +141,8 @@ class _DeletionProofImpl extends DeletionProof {
String? totpCode,
String? recoveryCode,
required String host,
required String address,
required String language,
}) : super._(
challenge: challenge,
clientValue: clientValue,
@ -129,6 +150,8 @@ class _DeletionProofImpl extends DeletionProof {
totpCode: totpCode,
recoveryCode: recoveryCode,
host: host,
address: address,
language: language,
);
/// Returns a shallow copy of this [DeletionProof]
@ -142,6 +165,8 @@ class _DeletionProofImpl extends DeletionProof {
Object? totpCode = _Undefined,
Object? recoveryCode = _Undefined,
String? host,
String? address,
String? language,
}) {
return DeletionProof(
challenge: challenge ?? this.challenge,
@ -150,6 +175,8 @@ class _DeletionProofImpl extends DeletionProof {
totpCode: totpCode is String? ? totpCode : this.totpCode,
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
host: host ?? this.host,
address: address ?? this.address,
language: language ?? this.language,
);
}
}