feat(auth): a password with TOTP on the register, and resets

The register's side of signing in with an address, a password that never
leaves the device (SRP-6a) and a code from an authenticator (ADR 0017),
with members forwarding every step through AccountRegister:

- password.set: salt, Argon2id's costs, SRP's verifier, the public keys
  and the private keys wrapped twice, all made on the device; with the
  address a mailed code proves, an anonymous person becomes identified
  under their ID. It signs in once an authenticator is confirmed.
- passwordSignIn.begin and complete: SRP's two steps with a TOTP or
  recovery code. An address nobody has is answered from a decoy key as
  an account would be; every wrong credential is refused alike.
- authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound
  to their row), each code accepted once, ten recovery codes with the
  first, the last one kept.
- accountRecovery: a lost authenticator reset 72 hours after the
  password and a mailed code or the recovery phrase ask, a lost password
  set anew with the phrase's signature, or the account reset with new
  keys 72 hours after a mailed code asks; a mail at each start, a
  sign-in calling it off.
- An account with a password no longer signs in by a mailed code or
  GitHub; no new GitHub account is linked where passwords are offered,
  and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go.

Five tables, added by the module's migration; deletePerson deletes
their rows.

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 13:32:12 +02:00
commit a3a8ed3890
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -11,6 +11,8 @@
// ignore_for_file: dead_code, unnecessary_type_check
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_base_client/src/protocol/password/authenticator.dart'
as _ivfmflch;
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
as _iacc;
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
@ -23,6 +25,20 @@ import 'auth/github_sign_in_refusal.dart' as _ir1gd9p5;
import 'auth/github_sign_in_refused_exception.dart' as _i0xfcu5n;
import 'auth/sign_in_ways.dart' as _ie6tbh85;
import 'instance/instance_basics.dart' as _i8jixqx6;
import 'password/account_reset_kind.dart' as _i91ka1it;
import 'password/authenticator.dart' as _icpzuvvi;
import 'password/authenticator_enrolment.dart' as _iv7tra7v;
import 'password/key_proof_challenge.dart' as _iehtx95q;
import 'password/new_password.dart' as _itvvd6m6;
import 'password/password_challenge.dart' as _iuxisac7;
import 'password/password_costs.dart' as _izp492e5;
import 'password/password_refusal.dart' as _ibfrwdhm;
import 'password/password_refused_exception.dart' as _i4froq5y;
import 'password/password_setup.dart' as _igpggy1y;
import 'password/password_signed_in.dart' as _ipk7gvzy;
import 'password/reset_pending.dart' as _i3y6qzi3;
import 'password/sign_in_challenge_purpose.dart' as _izijxzng;
import 'password/wrapped_keys.dart' as _i6v10ugm;
import 'profile/profile.dart' as _ia5j8fch;
import 'profile/profile_not_found_exception.dart' as _iim3hfcf;
import 'profile/profile_refusal.dart' as _i6j6efd1;
@ -34,6 +50,20 @@ export 'auth/github_sign_in_refusal.dart';
export 'auth/github_sign_in_refused_exception.dart';
export 'auth/sign_in_ways.dart';
export 'instance/instance_basics.dart';
export 'password/account_reset_kind.dart';
export 'password/authenticator.dart';
export 'password/authenticator_enrolment.dart';
export 'password/key_proof_challenge.dart';
export 'password/new_password.dart';
export 'password/password_challenge.dart';
export 'password/password_costs.dart';
export 'password/password_refusal.dart';
export 'password/password_refused_exception.dart';
export 'password/password_setup.dart';
export 'password/password_signed_in.dart';
export 'password/reset_pending.dart';
export 'password/sign_in_challenge_purpose.dart';
export 'password/wrapped_keys.dart';
export 'profile/profile.dart';
export 'profile/profile_not_found_exception.dart';
export 'profile/profile_refusal.dart';
@ -106,6 +136,48 @@ class Protocol extends _isc.SerializationManager {
if (t == _i8jixqx6.InstanceBasics) {
return _i8jixqx6.InstanceBasics.fromJson(data) as T;
}
if (t == _i91ka1it.AccountResetKind) {
return _i91ka1it.AccountResetKind.fromJson(data) as T;
}
if (t == _icpzuvvi.Authenticator) {
return _icpzuvvi.Authenticator.fromJson(data) as T;
}
if (t == _iv7tra7v.AuthenticatorEnrolment) {
return _iv7tra7v.AuthenticatorEnrolment.fromJson(data) as T;
}
if (t == _iehtx95q.KeyProofChallenge) {
return _iehtx95q.KeyProofChallenge.fromJson(data) as T;
}
if (t == _itvvd6m6.NewPassword) {
return _itvvd6m6.NewPassword.fromJson(data) as T;
}
if (t == _iuxisac7.PasswordChallenge) {
return _iuxisac7.PasswordChallenge.fromJson(data) as T;
}
if (t == _izp492e5.PasswordCosts) {
return _izp492e5.PasswordCosts.fromJson(data) as T;
}
if (t == _ibfrwdhm.PasswordRefusal) {
return _ibfrwdhm.PasswordRefusal.fromJson(data) as T;
}
if (t == _i4froq5y.PasswordRefusedException) {
return _i4froq5y.PasswordRefusedException.fromJson(data) as T;
}
if (t == _igpggy1y.PasswordSetup) {
return _igpggy1y.PasswordSetup.fromJson(data) as T;
}
if (t == _ipk7gvzy.PasswordSignedIn) {
return _ipk7gvzy.PasswordSignedIn.fromJson(data) as T;
}
if (t == _i3y6qzi3.ResetPending) {
return _i3y6qzi3.ResetPending.fromJson(data) as T;
}
if (t == _izijxzng.SignInChallengePurpose) {
return _izijxzng.SignInChallengePurpose.fromJson(data) as T;
}
if (t == _i6v10ugm.WrappedKeys) {
return _i6v10ugm.WrappedKeys.fromJson(data) as T;
}
if (t == _ia5j8fch.Profile) {
return _ia5j8fch.Profile.fromJson(data) as T;
}
@ -151,6 +223,65 @@ class Protocol extends _isc.SerializationManager {
return (data != null ? _i8jixqx6.InstanceBasics.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_i91ka1it.AccountResetKind?>()) {
return (data != null ? _i91ka1it.AccountResetKind.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_icpzuvvi.Authenticator?>()) {
return (data != null ? _icpzuvvi.Authenticator.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_iv7tra7v.AuthenticatorEnrolment?>()) {
return (data != null
? _iv7tra7v.AuthenticatorEnrolment.fromJson(data)
: null)
as T;
}
if (t == _isc.getType<_iehtx95q.KeyProofChallenge?>()) {
return (data != null ? _iehtx95q.KeyProofChallenge.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_itvvd6m6.NewPassword?>()) {
return (data != null ? _itvvd6m6.NewPassword.fromJson(data) : null) as T;
}
if (t == _isc.getType<_iuxisac7.PasswordChallenge?>()) {
return (data != null ? _iuxisac7.PasswordChallenge.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_izp492e5.PasswordCosts?>()) {
return (data != null ? _izp492e5.PasswordCosts.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ibfrwdhm.PasswordRefusal?>()) {
return (data != null ? _ibfrwdhm.PasswordRefusal.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_i4froq5y.PasswordRefusedException?>()) {
return (data != null
? _i4froq5y.PasswordRefusedException.fromJson(data)
: null)
as T;
}
if (t == _isc.getType<_igpggy1y.PasswordSetup?>()) {
return (data != null ? _igpggy1y.PasswordSetup.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ipk7gvzy.PasswordSignedIn?>()) {
return (data != null ? _ipk7gvzy.PasswordSignedIn.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_i3y6qzi3.ResetPending?>()) {
return (data != null ? _i3y6qzi3.ResetPending.fromJson(data) : null) as T;
}
if (t == _isc.getType<_izijxzng.SignInChallengePurpose?>()) {
return (data != null
? _izijxzng.SignInChallengePurpose.fromJson(data)
: null)
as T;
}
if (t == _isc.getType<_i6v10ugm.WrappedKeys?>()) {
return (data != null ? _i6v10ugm.WrappedKeys.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ia5j8fch.Profile?>()) {
return (data != null ? _ia5j8fch.Profile.fromJson(data) : null) as T;
}
@ -170,6 +301,9 @@ class Protocol extends _isc.SerializationManager {
: null)
as T;
}
if (t == Set<String>) {
return (data as List).map((e) => deserialize<String>(e)).toSet() as T;
}
if (t == List<String>) {
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
}
@ -182,6 +316,12 @@ class Protocol extends _isc.SerializationManager {
if (t == List<String>) {
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
}
if (t == List<_ivfmflch.Authenticator>) {
return (data as List)
.map((e) => deserialize<_ivfmflch.Authenticator>(e))
.toList()
as T;
}
try {
return _iaic.Protocol().deserialize<T>(data, t);
} on _isc.DeserializationTypeNotFoundException catch (_) {}
@ -200,6 +340,20 @@ class Protocol extends _isc.SerializationManager {
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
_ie6tbh85.SignInWays => 'SignInWays',
_i8jixqx6.InstanceBasics => 'InstanceBasics',
_i91ka1it.AccountResetKind => 'AccountResetKind',
_icpzuvvi.Authenticator => 'Authenticator',
_iv7tra7v.AuthenticatorEnrolment => 'AuthenticatorEnrolment',
_iehtx95q.KeyProofChallenge => 'KeyProofChallenge',
_itvvd6m6.NewPassword => 'NewPassword',
_iuxisac7.PasswordChallenge => 'PasswordChallenge',
_izp492e5.PasswordCosts => 'PasswordCosts',
_ibfrwdhm.PasswordRefusal => 'PasswordRefusal',
_i4froq5y.PasswordRefusedException => 'PasswordRefusedException',
_igpggy1y.PasswordSetup => 'PasswordSetup',
_ipk7gvzy.PasswordSignedIn => 'PasswordSignedIn',
_i3y6qzi3.ResetPending => 'ResetPending',
_izijxzng.SignInChallengePurpose => 'SignInChallengePurpose',
_i6v10ugm.WrappedKeys => 'WrappedKeys',
_ia5j8fch.Profile => 'Profile',
_iim3hfcf.ProfileNotFoundException => 'ProfileNotFoundException',
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
@ -235,6 +389,34 @@ class Protocol extends _isc.SerializationManager {
return 'SignInWays';
case _i8jixqx6.InstanceBasics():
return 'InstanceBasics';
case _i91ka1it.AccountResetKind():
return 'AccountResetKind';
case _icpzuvvi.Authenticator():
return 'Authenticator';
case _iv7tra7v.AuthenticatorEnrolment():
return 'AuthenticatorEnrolment';
case _iehtx95q.KeyProofChallenge():
return 'KeyProofChallenge';
case _itvvd6m6.NewPassword():
return 'NewPassword';
case _iuxisac7.PasswordChallenge():
return 'PasswordChallenge';
case _izp492e5.PasswordCosts():
return 'PasswordCosts';
case _ibfrwdhm.PasswordRefusal():
return 'PasswordRefusal';
case _i4froq5y.PasswordRefusedException():
return 'PasswordRefusedException';
case _igpggy1y.PasswordSetup():
return 'PasswordSetup';
case _ipk7gvzy.PasswordSignedIn():
return 'PasswordSignedIn';
case _i3y6qzi3.ResetPending():
return 'ResetPending';
case _izijxzng.SignInChallengePurpose():
return 'SignInChallengePurpose';
case _i6v10ugm.WrappedKeys():
return 'WrappedKeys';
case _ia5j8fch.Profile():
return 'Profile';
case _iim3hfcf.ProfileNotFoundException():
@ -274,6 +456,48 @@ class Protocol extends _isc.SerializationManager {
if (dataClassName == 'InstanceBasics') {
return deserialize<_i8jixqx6.InstanceBasics>(data['data']);
}
if (dataClassName == 'AccountResetKind') {
return deserialize<_i91ka1it.AccountResetKind>(data['data']);
}
if (dataClassName == 'Authenticator') {
return deserialize<_icpzuvvi.Authenticator>(data['data']);
}
if (dataClassName == 'AuthenticatorEnrolment') {
return deserialize<_iv7tra7v.AuthenticatorEnrolment>(data['data']);
}
if (dataClassName == 'KeyProofChallenge') {
return deserialize<_iehtx95q.KeyProofChallenge>(data['data']);
}
if (dataClassName == 'NewPassword') {
return deserialize<_itvvd6m6.NewPassword>(data['data']);
}
if (dataClassName == 'PasswordChallenge') {
return deserialize<_iuxisac7.PasswordChallenge>(data['data']);
}
if (dataClassName == 'PasswordCosts') {
return deserialize<_izp492e5.PasswordCosts>(data['data']);
}
if (dataClassName == 'PasswordRefusal') {
return deserialize<_ibfrwdhm.PasswordRefusal>(data['data']);
}
if (dataClassName == 'PasswordRefusedException') {
return deserialize<_i4froq5y.PasswordRefusedException>(data['data']);
}
if (dataClassName == 'PasswordSetup') {
return deserialize<_igpggy1y.PasswordSetup>(data['data']);
}
if (dataClassName == 'PasswordSignedIn') {
return deserialize<_ipk7gvzy.PasswordSignedIn>(data['data']);
}
if (dataClassName == 'ResetPending') {
return deserialize<_i3y6qzi3.ResetPending>(data['data']);
}
if (dataClassName == 'SignInChallengePurpose') {
return deserialize<_izijxzng.SignInChallengePurpose>(data['data']);
}
if (dataClassName == 'WrappedKeys') {
return deserialize<_i6v10ugm.WrappedKeys>(data['data']);
}
if (dataClassName == 'Profile') {
return deserialize<_ia5j8fch.Profile>(data['data']);
}