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

@ -20,6 +20,22 @@ import 'package:halkyon_base_server/src/generated/auth/sign_in_ways.dart'
as _ik4ah9vo;
import 'package:halkyon_base_server/src/generated/instance/instance_basics.dart'
as _ihusd8tw;
import 'package:halkyon_base_server/src/generated/password/authenticator.dart'
as _ip34xal6;
import 'package:halkyon_base_server/src/generated/password/authenticator_enrolment.dart'
as _id229ey1;
import 'package:halkyon_base_server/src/generated/password/key_proof_challenge.dart'
as _iaysae70;
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
as _ijye8mcv;
import 'package:halkyon_base_server/src/generated/password/password_challenge.dart'
as _isz0atlx;
import 'package:halkyon_base_server/src/generated/password/password_setup.dart'
as _ikhyee47;
import 'package:halkyon_base_server/src/generated/password/password_signed_in.dart'
as _ikpyb5fh;
import 'package:halkyon_base_server/src/generated/password/reset_pending.dart'
as _isakxto6;
import 'package:halkyon_base_server/src/generated/profile/profile.dart'
as _it4tom37;
import 'package:halkyon_base_server/src/generated/profile/profile_refusal.dart'
@ -175,6 +191,14 @@ class TestEndpoints {
late final _InstanceEndpoint instance;
late final _AccountRecoveryEndpoint accountRecovery;
late final _AuthenticatorEndpoint authenticator;
late final _PasswordEndpoint password;
late final _PasswordSignInEndpoint passwordSignIn;
late final _ProfileEndpoint profile;
late final _ProfilesEndpoint profiles;
@ -215,6 +239,22 @@ class _InternalTestEndpoints extends TestEndpoints
endpoints,
serializationManager,
);
accountRecovery = _AccountRecoveryEndpoint(
endpoints,
serializationManager,
);
authenticator = _AuthenticatorEndpoint(
endpoints,
serializationManager,
);
password = _PasswordEndpoint(
endpoints,
serializationManager,
);
passwordSignIn = _PasswordSignInEndpoint(
endpoints,
serializationManager,
);
profile = _ProfileEndpoint(
endpoints,
serializationManager,
@ -597,6 +637,559 @@ class _InstanceEndpoint {
}
}
class _AccountRecoveryEndpoint {
_AccountRecoveryEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<_is.UuidValue> sendCode(
_ist.TestSessionBuilder sessionBuilder, {
required String address,
required String language,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'accountRecovery',
method: 'sendCode',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'accountRecovery',
methodName: 'sendCode',
parameters: _ist.testObjectToJson({
'address': address,
'language': language,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_is.UuidValue>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_isakxto6.ResetPending> resetAuthenticator(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue challenge,
required _idt.ByteData clientValue,
required _idt.ByteData clientProof,
required String address,
required String language,
_is.UuidValue? request,
String? code,
_is.UuidValue? keyChallenge,
_idt.ByteData? signature,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'accountRecovery',
method: 'resetAuthenticator',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'accountRecovery',
methodName: 'resetAuthenticator',
parameters: _ist.testObjectToJson({
'challenge': challenge,
'clientValue': clientValue,
'clientProof': clientProof,
'address': address,
'language': language,
'request': request,
'code': code,
'keyChallenge': keyChallenge,
'signature': signature,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_isakxto6.ResetPending>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_iaysae70.KeyProofChallenge> beginKeyProof(
_ist.TestSessionBuilder sessionBuilder, {
required String address,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'accountRecovery',
method: 'beginKeyProof',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'accountRecovery',
methodName: 'beginKeyProof',
parameters: _ist.testObjectToJson({'address': address}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_iaysae70.KeyProofChallenge>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_iacs.AuthSuccess> resetPassword(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue challenge,
required _idt.ByteData signature,
required _ijye8mcv.NewPassword password,
String? totpCode,
String? recoveryCode,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'accountRecovery',
method: 'resetPassword',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'accountRecovery',
methodName: 'resetPassword',
parameters: _ist.testObjectToJson({
'challenge': challenge,
'signature': signature,
'password': password,
'totpCode': totpCode,
'recoveryCode': recoveryCode,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_iacs.AuthSuccess>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_isakxto6.ResetPending> startAccountReset(
_ist.TestSessionBuilder sessionBuilder, {
required String address,
required String language,
required _is.UuidValue request,
required String code,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'accountRecovery',
method: 'startAccountReset',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'accountRecovery',
methodName: 'startAccountReset',
parameters: _ist.testObjectToJson({
'address': address,
'language': language,
'request': request,
'code': code,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_isakxto6.ResetPending>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_iacs.AuthSuccess> completeAccountReset(
_ist.TestSessionBuilder sessionBuilder, {
required String address,
required _is.UuidValue request,
required String code,
required _ikhyee47.PasswordSetup setup,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'accountRecovery',
method: 'completeAccountReset',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'accountRecovery',
methodName: 'completeAccountReset',
parameters: _ist.testObjectToJson({
'address': address,
'request': request,
'code': code,
'setup': setup,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_iacs.AuthSuccess>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _AuthenticatorEndpoint {
_AuthenticatorEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<_id229ey1.AuthenticatorEnrolment> enrol(
_ist.TestSessionBuilder sessionBuilder, {
required String name,
required String label,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'authenticator',
method: 'enrol',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'authenticator',
methodName: 'enrol',
parameters: _ist.testObjectToJson({
'name': name,
'label': label,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_id229ey1.AuthenticatorEnrolment>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<List<String>> confirm(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue authenticator,
required String code,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'authenticator',
method: 'confirm',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'authenticator',
methodName: 'confirm',
parameters: _ist.testObjectToJson({
'authenticator': authenticator,
'code': code,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<String>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<List<_ip34xal6.Authenticator>> list(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'authenticator',
method: 'list',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'authenticator',
methodName: 'list',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_ip34xal6.Authenticator>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> remove(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue authenticator,
String? code,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'authenticator',
method: 'remove',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'authenticator',
methodName: 'remove',
parameters: _ist.testObjectToJson({
'authenticator': authenticator,
'code': code,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<List<String>> newRecoveryCodes(
_ist.TestSessionBuilder sessionBuilder, {
required String code,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'authenticator',
method: 'newRecoveryCodes',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'authenticator',
methodName: 'newRecoveryCodes',
parameters: _ist.testObjectToJson({'code': code}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<String>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _PasswordEndpoint {
_PasswordEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<void> set(
_ist.TestSessionBuilder sessionBuilder, {
required _ikhyee47.PasswordSetup setup,
_is.UuidValue? request,
String? code,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'password',
method: 'set',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'password',
methodName: 'set',
parameters: _ist.testObjectToJson({
'setup': setup,
'request': request,
'code': code,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _PasswordSignInEndpoint {
_PasswordSignInEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<_isz0atlx.PasswordChallenge> begin(
_ist.TestSessionBuilder sessionBuilder, {
required String address,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'passwordSignIn',
method: 'begin',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'passwordSignIn',
methodName: 'begin',
parameters: _ist.testObjectToJson({'address': address}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_isz0atlx.PasswordChallenge>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_ikpyb5fh.PasswordSignedIn> complete(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue challenge,
required _idt.ByteData clientValue,
required _idt.ByteData clientProof,
String? totpCode,
String? recoveryCode,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'passwordSignIn',
method: 'complete',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'passwordSignIn',
methodName: 'complete',
parameters: _ist.testObjectToJson({
'challenge': challenge,
'clientValue': clientValue,
'clientProof': clientProof,
'totpCode': totpCode,
'recoveryCode': recoveryCode,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_ikpyb5fh.PasswordSignedIn>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _ProfileEndpoint {
_ProfileEndpoint(
this._endpointDispatch,