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:
parent
200a2df32f
commit
a3a8ed3890
1 changed files with 22557 additions and 60 deletions
|
|
@ -131,6 +131,49 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group("a reset's notice", () {
|
||||
({String subject, String body}) notice(
|
||||
AccountResetKind kind,
|
||||
String language,
|
||||
) => resetMail(
|
||||
kind: kind,
|
||||
takesEffectAt: DateTime.utc(2026, 10, 23, 14, 5, 59),
|
||||
language: language,
|
||||
app: 'Halkyon Learn',
|
||||
instance: 'learn.halkyon.eu',
|
||||
);
|
||||
|
||||
test('says what goes when, in UTC, on which instance, and that signing '
|
||||
'in calls it off, in German for German and English else', () {
|
||||
for (final kind in AccountResetKind.values) {
|
||||
for (final language in ['en', 'de', 'fr']) {
|
||||
final mail = notice(kind, language);
|
||||
expect(mail.body, contains('2026-10-23 14:05 UTC'));
|
||||
expect(mail.body, contains('learn.halkyon.eu'));
|
||||
expect(mail.subject, contains('Halkyon Learn'));
|
||||
expect(
|
||||
mail.body,
|
||||
language == 'de'
|
||||
? contains('bricht es ab')
|
||||
: contains('calls it off'),
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(
|
||||
notice(AccountResetKind.account, 'en').body,
|
||||
contains('can no longer be read'),
|
||||
);
|
||||
});
|
||||
|
||||
test('holds no link and no code: it helps nobody but the owner', () {
|
||||
for (final kind in AccountResetKind.values) {
|
||||
final mail = notice(kind, 'en');
|
||||
expect(mail.body, isNot(contains('http')));
|
||||
expect(mail.body, isNot(matches(RegExp('[0-9A-Z]{4}-[0-9A-Z]{4}'))));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('codes go', () {
|
||||
final smtp = SmtpCodeMailer(
|
||||
server: SmtpServer('localhost'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue