fix(auth): only a session made since dismisses the owner's warning

The full reading's L1: the warning that ten wrong proofs ended a session is
there for the owner to learn of it, and any other session of the account
(another app on the same stolen phone) dismissed it. Now only a session
made after the warning dismisses it (signed in again, or the sign-in that
first shows it); an older one is refused (PasswordRefusal.signedInBefore,
its own text in English and German), and a new password clears it as
before. A member tells the register when its session asking was made.

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-21 13:32:33 +02:00
commit 003cb4821a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 165 additions and 24 deletions

View file

@ -459,6 +459,8 @@ void main() {
test('which her sign-in ways and her next sign-in tell, with when, '
'until she dismisses it', () async {
expect(await warningOn(onLaptop()), isNull);
// Her tablet, signed in before (at the tests' time).
final tablet = await atNow(() => signInNewDevice(session, anna));
final ended = await endPhone();
expect(await warningOn(onLaptop()), ended);
@ -467,7 +469,34 @@ void main() {
// Told, not dismissed: it stays until she says so.
expect(await warningOn(onLaptop()), ended);
await endpoints.password.dismissSessionEndedWarning(onLaptop());
// Her tablet's session is older than the warning: it dismisses
// nothing (the full reading's L1: another session of a stolen
// device would dismiss it). Signed in again, it does.
await expectLater(
endpoints.password.dismissSessionEndedWarning(
sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
anna.uuid,
{},
authId: tablet.uuid,
),
),
),
refused(PasswordRefusal.signedInBefore),
);
expect(await warningOn(onLaptop()), ended);
final newLaptop = await atNow(
() => signInNewDevice(session, anna),
);
await endpoints.password.dismissSessionEndedWarning(
sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
anna.uuid,
{},
authId: newLaptop.uuid,
),
),
);
expect(await warningOn(onLaptop()), isNull);
nextStep();
expect((await signIn()).sessionEndedForProofsAt, isNull);

View file

@ -48,6 +48,9 @@ final class _Register implements AccountRegister {
/// member's made just now.
final freshGitHubSignIns = <UuidValue>{};
/// When the session dismissing a warning was made, by person.
final dismissedMadeAt = <UuidValue, DateTime?>{};
/// Those [setPassword] was told are anonymous on the member.
final anonymousHere_ = <UuidValue>{};
@ -226,10 +229,14 @@ final class _Register implements AccountRegister {
}
@override
Future<void> dismissSessionEndedWarning(Session session, UuidValue person) =>
_answer('dismissSessionEndedWarning', () {
dismissed.add(person);
});
Future<void> dismissSessionEndedWarning(
Session session,
UuidValue person, {
required DateTime? sessionMadeAt,
}) => _answer('dismissSessionEndedWarning', () {
dismissed.add(person);
dismissedMadeAt[person] = sessionMadeAt;
});
@override
Future<bool> passwordSignInOffered(Session session) =>
@ -1337,6 +1344,8 @@ void main() {
"never another's", () async {
await endpoints.password.dismissSessionEndedWarning(asBen());
expect(register.dismissed, {ben});
// With when Ben's session here was made (the full reading's L1).
expect(register.dismissedMadeAt[ben], isNotNull);
await expectLater(
endpoints.password.dismissSessionEndedWarning(
sessionBuilder.copyWith(

View file

@ -613,7 +613,21 @@ void main() {
expect(answer.verifiesServer(proof.serverProof), isTrue);
expect(proof.passwordKeys.toJson(), device.passwordKeys.toJson());
expect(proof.sessionEndedForProofsAt, warned);
await remote.dismissSessionEndedWarning(session, annaOnTodo);
// An older session of the member's dismisses nothing (the full
// reading's L1); one made since does.
await expectLater(
remote.dismissSessionEndedWarning(
session,
annaOnTodo,
sessionMadeAt: warned,
),
refused(PasswordRefusal.signedInBefore),
);
await remote.dismissSessionEndedWarning(
session,
annaOnTodo,
sessionMadeAt: warned.add(const Duration(seconds: 1)),
);
expect(
(await remote.signInWaysOf(
session,