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

@ -148,9 +148,136 @@ abstract interface class AccountRegister {
/// for none.
Future<UuidValue?> accountWithGitHub(Session session, String gitHubUserId);
/// How [person] signs in: with GitHub, an email address, both or neither.
/// How [person] signs in: with GitHub, an email address, a password and
/// whether an authenticator is confirmed.
Future<SignInWays> signInWaysOf(Session session, UuidValue person);
/// Whether people sign in with a password and TOTP (ADR 0017): the
/// register holds the keys it needs.
Future<bool> passwordSignInOffered(Session session);
/// Gives [person] the password [setup] describes, with the address [code]
/// proves for [request] unless they have one. Refusals as `setPassword`'s.
Future<void> setPassword(
Session session, {
required UuidValue person,
required PasswordSetup setup,
UuidValue? request,
String? code,
});
/// A password sign-in's first step for [address], the sign-ins begun from
/// [network] counted. Refusals as `beginPasswordSignIn`'s.
Future<PasswordChallenge> beginPasswordSignIn(
Session session, {
required String address,
required String network,
});
/// A password sign-in's second step: the account it proved, for the
/// caller's session. Refusals as `completePasswordSignIn`'s.
Future<PasswordProof> completePasswordSignIn(
Session session, {
required UuidValue challenge,
required ByteData clientValue,
required ByteData clientProof,
String? totpCode,
String? recoveryCode,
});
/// A new authenticator of [person]. Refusals as `enrolAuthenticator`'s.
Future<AuthenticatorEnrolment> enrolAuthenticator(
Session session, {
required UuidValue person,
required String name,
required String label,
});
/// Confirms [person]'s [authenticator]; 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,
});
/// [person]'s authenticators, the oldest first.
Future<List<Authenticator>> authenticatorsOf(
Session session,
UuidValue person,
);
/// Removes [person]'s [authenticator]. Refusals as
/// `removeAuthenticator`'s.
Future<void> removeAuthenticator(
Session session, {
required UuidValue person,
required UuidValue authenticator,
String? code,
});
/// New recovery codes for [person]. Refusals as `newRecoveryCodes`'.
Future<List<String>> newRecoveryCodes(
Session session, {
required UuidValue person,
required String code,
});
/// Asks for the reset of a lost authenticator. Refusals as
/// `resetAuthenticator`'s.
Future<ResetPending> resetAuthenticator(
Session session, {
required UuidValue challenge,
required ByteData clientValue,
required ByteData clientProof,
required String address,
required String language,
UuidValue? request,
String? code,
UuidValue? keyChallenge,
ByteData? signature,
});
/// The first step of proving the recovery phrase for [address], the
/// proofs begun from [network] counted. Refusals as `beginKeyProof`'s.
Future<KeyProofChallenge> beginKeyProof(
Session session, {
required String address,
required String network,
});
/// Sets a new password with the recovery phrase's proof; the account, for
/// the caller's session. Refusals as `resetPasswordWithPhrase`'s.
Future<UuidValue> resetPasswordWithPhrase(
Session session, {
required UuidValue challenge,
required ByteData signature,
required NewPassword password,
String? totpCode,
String? recoveryCode,
});
/// Asks for the reset of a whole account. Refusals as
/// `startAccountReset`'s.
Future<ResetPending> startAccountReset(
Session session, {
required String address,
required String language,
required UuidValue request,
required String code,
});
/// Carries out the reset of a whole account; the account, for the
/// caller's session. Refusals as `completeAccountReset`'s.
Future<UuidValue> completeAccountReset(
Session session, {
required String address,
required UuidValue request,
required String code,
required PasswordSetup setup,
});
/// Deletes the account [person] with everything the register's server has
/// of them, and has every member of the instance delete theirs.
Future<void> deleteAccount(Session session, UuidValue person);