feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35)
SimpleLogin's sheet, its password and its recovery phrase both lost: under the lost-password step's phrase field, Lost your recovery phrase too? says what stays and what is lost and asks for new keys (`keys.startReset`) with the sign-in of a moment ago, which the device then drops. The next sign-in says when while it waits, and once due offers new keys, the sign-up's own password and phrase steps (`keys.completeReset`), or the password, which calls it off (D31). Every app's home shows `KeysResetBanner` where "too old" shows, and the account section's SimpleLogin row the same words (`KeysResetNotice`), each with Call it off, the password's proof alone (`keys.callOffResets`, D32), and, once due on a device that holds the keys, Finish here, which wraps the same keys anew (D33, D35). Both show only a reset of an account that signs in with SimpleLogin alone; ADR 0017's resets show in its own section, which now shares `AlertBox` with them. An account whose password signs in has no Forgot your password? there, and is asked its password and a code once while its keys lack the proof key, at a sign-in or on adding SimpleLogin (`password.setProofKey`, D34). `PasswordUnlock` carries the proof key, which its forgetting leaves, and the keys' password proves for a purpose given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
fe2ef02c92
commit
1b79214764
31 changed files with 2711 additions and 248 deletions
|
|
@ -153,6 +153,7 @@ class _HomeState() extends State<_Home> {
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
AppMismatchBanner(onMore: () => context.push(RoutePaths.settings)),
|
AppMismatchBanner(onMore: () => context.push(RoutePaths.settings)),
|
||||||
|
const KeysResetBanner(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: loaded == null || today == null
|
child: loaded == null || today == null
|
||||||
? const Center(child: AppLoadingIndicator())
|
? const Center(child: AppLoadingIndicator())
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/account/account_sheet.dart';
|
import 'package:halkyon_todo_flutter/src/account/account_sheet.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
|
@ -5,7 +6,8 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// A signed-in person's home: nothing yet, as Todo keeps nothing until its
|
/// A signed-in person's home: nothing yet, as Todo keeps nothing until its
|
||||||
/// first model, and the way to the account.
|
/// first model, and the way to the account; a reset of the person's keys
|
||||||
|
/// waiting above it (`KeysResetBanner`).
|
||||||
class const HomeScreen({super.key}) extends StatelessWidget {
|
class const HomeScreen({super.key}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -21,12 +23,19 @@ class const HomeScreen({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Column(
|
||||||
child: EmptyListMessage(
|
children: [
|
||||||
icon: LucideIcons.listChecks,
|
const KeysResetBanner(),
|
||||||
title: l10n.homeEmptyTitle,
|
Expanded(
|
||||||
hint: l10n.homeEmptyHint,
|
child: Center(
|
||||||
),
|
child: EmptyListMessage(
|
||||||
|
icon: LucideIcons.listChecks,
|
||||||
|
title: l10n.homeEmptyTitle,
|
||||||
|
hint: l10n.homeEmptyHint,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# ADR 0020: Signing in with SimpleLogin only
|
# ADR 0020: Signing in with SimpleLogin only
|
||||||
|
|
||||||
- **Status:** decided 23 Oct 2026, by the developer (D1–D20 below; D21–D34 on 24
|
- **Status:** decided 23 Oct 2026, by the developer (D1–D20 below; D21–D35 on 24
|
||||||
Oct), the *Proposal* with it (D15), as D18 to D20 change it and D24 drops the
|
Oct), the *Proposal* with it (D15), as D18 to D20 change it and D24 drops the
|
||||||
cut-off's machinery (D8's code, D10, D22, 10's `ANONYMOUS_SESSIONS_END`, 11, step 5).
|
cut-off's machinery (D8's code, D10, D22, 10's `ANONYMOUS_SESSIONS_END`, 11, step 5).
|
||||||
- **Built (23 Oct 2026), step 1:** the forwarding page (3),
|
- **Built (23 Oct 2026), step 1:** the forwarding page (3),
|
||||||
|
|
@ -125,6 +125,20 @@
|
||||||
says whether the password signs in too (`passwordSignsIn`), whether the keys have the
|
says whether the password signs in too (`passwordSignsIn`), whether the keys have the
|
||||||
proof key (`hasProofKey`) and when a reset takes effect (`accountResetAt`). No
|
proof key (`hasProofKey`) and when a reset takes effect (`accountResetAt`). No
|
||||||
migration: the reset is ADR 0017's `account_reset` row of kind `account`.
|
migration: the reset is ADR 0017's `account_reset` row of kind `account`.
|
||||||
|
Then the app's half (D31–D35): under the lost-password step's phrase field, Lost your
|
||||||
|
recovery phrase too? explains what stays and what is lost and asks for new keys with
|
||||||
|
the sign-in of a moment ago, which the device then drops; the next sign-in says when
|
||||||
|
while it waits, and once due offers new keys (the sign-up's password and phrase steps)
|
||||||
|
or the password. An account whose password signs in has no Forgot your password? there
|
||||||
|
and is asked its password and a code once, while its keys lack the proof key (D34),
|
||||||
|
with two Argon2id runs (the proof, then the unlock), as it happens once. Every app's
|
||||||
|
home shows `KeysResetBanner` where "too old" shows, and the account section's
|
||||||
|
SimpleLogin row the same words (`KeysResetNotice`), each with Call it off (the
|
||||||
|
password, D32) and, once due on a device that holds the keys, Finish here (D33); both
|
||||||
|
show only a reset of an account that signs in with SimpleLogin alone, as ADR 0017's
|
||||||
|
resets show in its own section. The security section's boxes and this one share
|
||||||
|
`AlertBox`. Left for later: an account whose password signs in, on an instance that
|
||||||
|
offers SimpleLogin alone, has no password sign-in to reset a lost password from.
|
||||||
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
|
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
|
||||||
register), ADR 0017 (a password, TOTP and the keys).
|
register), ADR 0017 (a password, TOTP and the keys).
|
||||||
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
|
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
|
||||||
|
|
@ -346,6 +360,11 @@ against app.simplelogin.io on 23 Oct:
|
||||||
its keys through `keys.unlock` but changes nothing through `keys`: its changes and
|
its keys through `keys.unlock` but changes nothing through `keys`: its changes and
|
||||||
resets stay ADR 0017's, which keep SRP's verifier in step. `simpleLogin.complete` says
|
resets stay ADR 0017's, which keep SRP's verifier in step. `simpleLogin.complete` says
|
||||||
whether the password signs in and has the proof key.
|
whether the password signs in and has the proof key.
|
||||||
|
- **D35 The banner on each app's home and in the account section** (24 Oct,
|
||||||
|
recommended, over the account section alone, as ADR 0017's resets show, which the
|
||||||
|
owner sees only on opening it, and over the home alone): at the top of the home,
|
||||||
|
where an app too old for its instance shows, and under the SimpleLogin row, each with
|
||||||
|
Call it off.
|
||||||
- **Also (23 Oct):** the developer's own account moves by adding SimpleLogin to it while
|
- **Also (23 Oct):** the developer's own account moves by adding SimpleLogin to it while
|
||||||
signed in with GitHub; the instance's SimpleLogin app goes up for approval once
|
signed in with GitHub; the instance's SimpleLogin app goes up for approval once
|
||||||
halkyon.app/learn exists, before learn.halkyon.eu switches.
|
halkyon.app/learn exists, before learn.halkyon.eu switches.
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ export 'src/simplelogin/data/simplelogin_repository.dart';
|
||||||
export 'src/simplelogin/model/simplelogin_arrival.dart';
|
export 'src/simplelogin/model/simplelogin_arrival.dart';
|
||||||
export 'src/simplelogin/model/simplelogin_failed.dart';
|
export 'src/simplelogin/model/simplelogin_failed.dart';
|
||||||
export 'src/simplelogin/model/simplelogin_standing.dart';
|
export 'src/simplelogin/model/simplelogin_standing.dart';
|
||||||
|
export 'src/simplelogin/widget/keys_reset.dart';
|
||||||
export 'src/simplelogin/widget/simplelogin_section.dart';
|
export 'src/simplelogin/widget/simplelogin_section.dart';
|
||||||
export 'src/simplelogin/widget/simplelogin_sheet.dart';
|
export 'src/simplelogin/widget/simplelogin_sheet.dart';
|
||||||
export 'src/simplelogin/widget/simplelogin_standing_scope.dart';
|
export 'src/simplelogin/widget/simplelogin_standing_scope.dart';
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,32 @@
|
||||||
"simpleLoginForgotTitle": "Ein neues Passwort",
|
"simpleLoginForgotTitle": "Ein neues Passwort",
|
||||||
"simpleLoginForgotIntro": "Deine Wiederherstellungsphrase öffnet deine Schlüssel, die dann dein neues Passwort schützt. Alles bleibt; deine anderen Geräte werden abgemeldet.",
|
"simpleLoginForgotIntro": "Deine Wiederherstellungsphrase öffnet deine Schlüssel, die dann dein neues Passwort schützt. Alles bleibt; deine anderen Geräte werden abgemeldet.",
|
||||||
"simpleLoginForgotDoesNotFit": "Die Wiederherstellungsphrase stimmt nicht.",
|
"simpleLoginForgotDoesNotFit": "Die Wiederherstellungsphrase stimmt nicht.",
|
||||||
|
"simpleLoginResetWaiting": "Neue Schlüssel wurden angefordert: Ab dem {date} kann eine Anmeldung mit SimpleLogin sie einrichten. Öffnest du deine Schlüssel mit deinem Passwort, ist es abgesagt.",
|
||||||
|
"simpleLoginPhraseLost": "Auch die Wiederherstellungsphrase verloren?",
|
||||||
|
"simpleLoginResetTitle": "Neue Schlüssel",
|
||||||
|
"simpleLoginResetIntro": "Ohne dein Passwort oder deine Wiederherstellungsphrase kann niemand deine Schlüssel öffnen, auch {instance} nicht. Neue lassen sich in 72 Stunden einrichten, wenn du dich wieder mit SimpleLogin anmeldest. Dein Handle, dein Profil und was du veröffentlicht hast, bleiben; was mit deinen alten Schlüsseln verschlüsselt ist, geht verloren, außer du schließt auf einem Gerät ab, das noch angemeldet ist und sie behält.",
|
||||||
|
"simpleLoginResetIntroDevices": "Bis dahin zeigen deine angemeldeten Geräte das Zurücksetzen, und dein Passwort sagt es dort ab, falls jemand anderes es angefordert hat.",
|
||||||
|
"simpleLoginResetAsk": "Neue Schlüssel anfordern",
|
||||||
|
"simpleLoginResetAskedTitle": "Neue Schlüssel angefordert",
|
||||||
|
"simpleLoginResetAskedIntro": "Melde dich ab dem {date} wieder mit SimpleLogin an, um sie einzurichten. Findest du vorher dein Passwort oder deine Wiederherstellungsphrase, sagt das Öffnen deiner Schlüssel damit das Zurücksetzen ab.",
|
||||||
|
"simpleLoginResetDueTitle": "Deine neuen Schlüssel",
|
||||||
|
"simpleLoginResetDueIntro": "Die angeforderten neuen Schlüssel lassen sich jetzt einrichten. Oder öffne deine alten Schlüssel mit deinem Passwort, was das Zurücksetzen absagt.",
|
||||||
|
"simpleLoginResetNewKeys": "Neue Schlüssel einrichten",
|
||||||
|
"simpleLoginResetNewKeysHint": "Ein neues Passwort und eine neue Wiederherstellungsphrase. Was mit deinen alten Schlüsseln verschlüsselt ist, geht verloren, außer du schließt auf einem Gerät ab, das noch angemeldet ist.",
|
||||||
|
"simpleLoginResetHavePassword": "Ich habe mein Passwort",
|
||||||
|
"simpleLoginResetHavePasswordHint": "Deine Schlüssel öffnen sich und bleiben.",
|
||||||
|
"simpleLoginResetKeepIntro": "Wähle ein neues Passwort: Deine Schlüssel bleiben, unter ihm und einer neuen Wiederherstellungsphrase, sodass nichts verloren geht. Deine anderen Geräte werden abgemeldet.",
|
||||||
|
"simpleLoginResetFinish": "Schlüssel einrichten",
|
||||||
|
"simpleLoginProofKeyTitle": "Dein Passwort und ein Code",
|
||||||
|
"simpleLoginProofKeyIntroAs": "Mit SimpleLogin als @{handle} angemeldet. Du meldest dich bei deinem Konto auch mit einem Passwort an: Gib es und einen Code aus deiner Authenticator-App einmal ein, dann öffnet dein Passwort deine Schlüssel künftig nach SimpleLogin.",
|
||||||
|
"simpleLoginProofKeyIntro": "Du meldest dich bei deinem Konto auch mit einem Passwort an: Gib es und einen Code aus deiner Authenticator-App einmal ein, dann öffnet dein Passwort deine Schlüssel künftig nach SimpleLogin.",
|
||||||
|
"simpleLoginResetBanner": "Mit deinem SimpleLogin-Konto wurden neue Schlüssel angefordert: Ab dem {date} kann sie einrichten, wer es hat. Warst du das nicht, sag es mit deinem Passwort ab.",
|
||||||
|
"simpleLoginResetBannerDue": "Mit deinem SimpleLogin-Konto wurden neue Schlüssel angefordert, und wer es hat, kann sie jetzt einrichten. Warst du das nicht, sag es mit deinem Passwort ab.",
|
||||||
|
"simpleLoginResetBannerDueHere": "Mit deinem SimpleLogin-Konto wurden neue Schlüssel angefordert, die sich jetzt einrichten lassen. Warst du das, schließ hier ab: Deine Schlüssel bleiben. Wenn nicht, sag es mit deinem Passwort ab.",
|
||||||
|
"simpleLoginResetFinishHere": "Hier abschließen",
|
||||||
|
"simpleLoginCallOffIntro": "Dein Passwort sagt das Zurücksetzen deiner Schlüssel ab.",
|
||||||
|
"simpleLoginResetCalledOff": "Zurücksetzen abgesagt.",
|
||||||
|
"simpleLoginResetFinished": "Neues Passwort und neue Wiederherstellungsphrase gesetzt.",
|
||||||
"simpleLoginUnavailable": "Diese Instanz bietet die Anmeldung mit SimpleLogin nicht an.",
|
"simpleLoginUnavailable": "Diese Instanz bietet die Anmeldung mit SimpleLogin nicht an.",
|
||||||
"simpleLoginDenied": "Die Anmeldung bei SimpleLogin wurde abgelehnt, also wurde nichts geteilt.",
|
"simpleLoginDenied": "Die Anmeldung bei SimpleLogin wurde abgelehnt, also wurde nichts geteilt.",
|
||||||
"simpleLoginNoBrowser": "Es gibt keinen Browser, der die Seite von SimpleLogin öffnen kann.",
|
"simpleLoginNoBrowser": "Es gibt keinen Browser, der die Seite von SimpleLogin öffnen kann.",
|
||||||
|
|
|
||||||
|
|
@ -600,6 +600,67 @@
|
||||||
"simpleLoginForgotTitle": "A new password",
|
"simpleLoginForgotTitle": "A new password",
|
||||||
"simpleLoginForgotIntro": "Your recovery phrase opens your keys, which your new password then protects. Everything stays; your other devices are signed out.",
|
"simpleLoginForgotIntro": "Your recovery phrase opens your keys, which your new password then protects. Everything stays; your other devices are signed out.",
|
||||||
"simpleLoginForgotDoesNotFit": "The recovery phrase isn't right.",
|
"simpleLoginForgotDoesNotFit": "The recovery phrase isn't right.",
|
||||||
|
"simpleLoginResetWaiting": "New keys were asked for: from {date}, a SimpleLogin sign-in can set them up. Your password opening your keys calls it off.",
|
||||||
|
"@simpleLoginResetWaiting": {
|
||||||
|
"placeholders": {
|
||||||
|
"date": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simpleLoginPhraseLost": "Lost your recovery phrase too?",
|
||||||
|
"simpleLoginResetTitle": "New keys",
|
||||||
|
"simpleLoginResetIntro": "Without your password or your recovery phrase, nobody can open your keys, not even {instance}. New ones can be set up 72 hours from now, when you sign in with SimpleLogin again. Your handle, your profile and what you published stay; what was encrypted under your old keys is lost, unless you finish on a device still signed in, which keeps them.",
|
||||||
|
"@simpleLoginResetIntro": {
|
||||||
|
"placeholders": {
|
||||||
|
"instance": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simpleLoginResetIntroDevices": "Meanwhile your signed-in devices show the reset, and your password calls it off there, should someone else have asked for it.",
|
||||||
|
"simpleLoginResetAsk": "Ask for new keys",
|
||||||
|
"simpleLoginResetAskedTitle": "New keys asked for",
|
||||||
|
"simpleLoginResetAskedIntro": "From {date}, sign in with SimpleLogin again to set them up. Should you find your password or your recovery phrase before then, opening your keys with it calls the reset off.",
|
||||||
|
"@simpleLoginResetAskedIntro": {
|
||||||
|
"placeholders": {
|
||||||
|
"date": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simpleLoginResetDueTitle": "Your new keys",
|
||||||
|
"simpleLoginResetDueIntro": "The new keys you asked for can be set up now. Or open your old keys with your password, which calls the reset off.",
|
||||||
|
"simpleLoginResetNewKeys": "Set up new keys",
|
||||||
|
"simpleLoginResetNewKeysHint": "A new password and recovery phrase. What was encrypted under your old keys is lost, unless you finish on a device still signed in.",
|
||||||
|
"simpleLoginResetHavePassword": "I have my password",
|
||||||
|
"simpleLoginResetHavePasswordHint": "Your keys open, and stay.",
|
||||||
|
"simpleLoginResetKeepIntro": "Choose a new password: your keys stay, under it and a new recovery phrase, so nothing is lost. Your other devices are signed out.",
|
||||||
|
"simpleLoginResetFinish": "Set up the keys",
|
||||||
|
"simpleLoginProofKeyTitle": "Your password and a code",
|
||||||
|
"simpleLoginProofKeyIntroAs": "Signed in with SimpleLogin as @{handle}. Your account also signs in with a password: type it and a code from your authenticator once, and your password opens your keys after SimpleLogin from then on.",
|
||||||
|
"@simpleLoginProofKeyIntroAs": {
|
||||||
|
"placeholders": {
|
||||||
|
"handle": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simpleLoginProofKeyIntro": "Your account also signs in with a password: type it and a code from your authenticator once, and your password opens your keys after SimpleLogin from then on.",
|
||||||
|
"simpleLoginResetBanner": "New keys were asked for with your SimpleLogin account: from {date}, whoever has it can set them up. Unless that was you, call it off with your password.",
|
||||||
|
"@simpleLoginResetBanner": {
|
||||||
|
"placeholders": {
|
||||||
|
"date": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simpleLoginResetBannerDue": "New keys were asked for with your SimpleLogin account, and whoever has it can set them up now. Unless that was you, call it off with your password.",
|
||||||
|
"simpleLoginResetBannerDueHere": "New keys were asked for with your SimpleLogin account and can be set up now. If that was you, finish here: your keys stay. If not, call it off with your password.",
|
||||||
|
"simpleLoginResetFinishHere": "Finish here",
|
||||||
|
"simpleLoginCallOffIntro": "Your password calls off the reset of your keys.",
|
||||||
|
"simpleLoginResetCalledOff": "Reset called off.",
|
||||||
|
"simpleLoginResetFinished": "New password and recovery phrase set.",
|
||||||
"simpleLoginUnavailable": "This instance doesn't offer sign-in with SimpleLogin.",
|
"simpleLoginUnavailable": "This instance doesn't offer sign-in with SimpleLogin.",
|
||||||
"simpleLoginDenied": "SimpleLogin's sign-in was declined, so nothing was shared.",
|
"simpleLoginDenied": "SimpleLogin's sign-in was declined, so nothing was shared.",
|
||||||
"simpleLoginNoBrowser": "There's no browser to open SimpleLogin's page in.",
|
"simpleLoginNoBrowser": "There's no browser to open SimpleLogin's page in.",
|
||||||
|
|
|
||||||
|
|
@ -1699,6 +1699,162 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// **'The recovery phrase isn\'t right.'**
|
/// **'The recovery phrase isn\'t right.'**
|
||||||
String get simpleLoginForgotDoesNotFit;
|
String get simpleLoginForgotDoesNotFit;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetWaiting.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New keys were asked for: from {date}, a SimpleLogin sign-in can set them up. Your password opening your keys calls it off.'**
|
||||||
|
String simpleLoginResetWaiting(String date);
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginPhraseLost.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Lost your recovery phrase too?'**
|
||||||
|
String get simpleLoginPhraseLost;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New keys'**
|
||||||
|
String get simpleLoginResetTitle;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Without your password or your recovery phrase, nobody can open your keys, not even {instance}. New ones can be set up 72 hours from now, when you sign in with SimpleLogin again. Your handle, your profile and what you published stay; what was encrypted under your old keys is lost, unless you finish on a device still signed in, which keeps them.'**
|
||||||
|
String simpleLoginResetIntro(String instance);
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetIntroDevices.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Meanwhile your signed-in devices show the reset, and your password calls it off there, should someone else have asked for it.'**
|
||||||
|
String get simpleLoginResetIntroDevices;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetAsk.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask for new keys'**
|
||||||
|
String get simpleLoginResetAsk;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetAskedTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New keys asked for'**
|
||||||
|
String get simpleLoginResetAskedTitle;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetAskedIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'From {date}, sign in with SimpleLogin again to set them up. Should you find your password or your recovery phrase before then, opening your keys with it calls the reset off.'**
|
||||||
|
String simpleLoginResetAskedIntro(String date);
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetDueTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your new keys'**
|
||||||
|
String get simpleLoginResetDueTitle;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetDueIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The new keys you asked for can be set up now. Or open your old keys with your password, which calls the reset off.'**
|
||||||
|
String get simpleLoginResetDueIntro;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetNewKeys.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Set up new keys'**
|
||||||
|
String get simpleLoginResetNewKeys;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetNewKeysHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A new password and recovery phrase. What was encrypted under your old keys is lost, unless you finish on a device still signed in.'**
|
||||||
|
String get simpleLoginResetNewKeysHint;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetHavePassword.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'I have my password'**
|
||||||
|
String get simpleLoginResetHavePassword;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetHavePasswordHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your keys open, and stay.'**
|
||||||
|
String get simpleLoginResetHavePasswordHint;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetKeepIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose a new password: your keys stay, under it and a new recovery phrase, so nothing is lost. Your other devices are signed out.'**
|
||||||
|
String get simpleLoginResetKeepIntro;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetFinish.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Set up the keys'**
|
||||||
|
String get simpleLoginResetFinish;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginProofKeyTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your password and a code'**
|
||||||
|
String get simpleLoginProofKeyTitle;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginProofKeyIntroAs.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Signed in with SimpleLogin as @{handle}. Your account also signs in with a password: type it and a code from your authenticator once, and your password opens your keys after SimpleLogin from then on.'**
|
||||||
|
String simpleLoginProofKeyIntroAs(String handle);
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginProofKeyIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your account also signs in with a password: type it and a code from your authenticator once, and your password opens your keys after SimpleLogin from then on.'**
|
||||||
|
String get simpleLoginProofKeyIntro;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetBanner.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New keys were asked for with your SimpleLogin account: from {date}, whoever has it can set them up. Unless that was you, call it off with your password.'**
|
||||||
|
String simpleLoginResetBanner(String date);
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetBannerDue.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New keys were asked for with your SimpleLogin account, and whoever has it can set them up now. Unless that was you, call it off with your password.'**
|
||||||
|
String get simpleLoginResetBannerDue;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetBannerDueHere.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New keys were asked for with your SimpleLogin account and can be set up now. If that was you, finish here: your keys stay. If not, call it off with your password.'**
|
||||||
|
String get simpleLoginResetBannerDueHere;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetFinishHere.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Finish here'**
|
||||||
|
String get simpleLoginResetFinishHere;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginCallOffIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your password calls off the reset of your keys.'**
|
||||||
|
String get simpleLoginCallOffIntro;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetCalledOff.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reset called off.'**
|
||||||
|
String get simpleLoginResetCalledOff;
|
||||||
|
|
||||||
|
/// No description provided for @simpleLoginResetFinished.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New password and recovery phrase set.'**
|
||||||
|
String get simpleLoginResetFinished;
|
||||||
|
|
||||||
/// No description provided for @simpleLoginUnavailable.
|
/// No description provided for @simpleLoginUnavailable.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -1002,6 +1002,105 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
String get simpleLoginForgotDoesNotFit =>
|
String get simpleLoginForgotDoesNotFit =>
|
||||||
'Die Wiederherstellungsphrase stimmt nicht.';
|
'Die Wiederherstellungsphrase stimmt nicht.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetWaiting(String date) {
|
||||||
|
return 'Neue Schlüssel wurden angefordert: Ab dem $date kann eine Anmeldung mit SimpleLogin sie einrichten. Öffnest du deine Schlüssel mit deinem Passwort, ist es abgesagt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginPhraseLost =>
|
||||||
|
'Auch die Wiederherstellungsphrase verloren?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetTitle => 'Neue Schlüssel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetIntro(String instance) {
|
||||||
|
return 'Ohne dein Passwort oder deine Wiederherstellungsphrase kann niemand deine Schlüssel öffnen, auch $instance nicht. Neue lassen sich in 72 Stunden einrichten, wenn du dich wieder mit SimpleLogin anmeldest. Dein Handle, dein Profil und was du veröffentlicht hast, bleiben; was mit deinen alten Schlüsseln verschlüsselt ist, geht verloren, außer du schließt auf einem Gerät ab, das noch angemeldet ist und sie behält.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetIntroDevices =>
|
||||||
|
'Bis dahin zeigen deine angemeldeten Geräte das Zurücksetzen, und dein Passwort sagt es dort ab, falls jemand anderes es angefordert hat.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetAsk => 'Neue Schlüssel anfordern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetAskedTitle => 'Neue Schlüssel angefordert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetAskedIntro(String date) {
|
||||||
|
return 'Melde dich ab dem $date wieder mit SimpleLogin an, um sie einzurichten. Findest du vorher dein Passwort oder deine Wiederherstellungsphrase, sagt das Öffnen deiner Schlüssel damit das Zurücksetzen ab.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetDueTitle => 'Deine neuen Schlüssel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetDueIntro =>
|
||||||
|
'Die angeforderten neuen Schlüssel lassen sich jetzt einrichten. Oder öffne deine alten Schlüssel mit deinem Passwort, was das Zurücksetzen absagt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetNewKeys => 'Neue Schlüssel einrichten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetNewKeysHint =>
|
||||||
|
'Ein neues Passwort und eine neue Wiederherstellungsphrase. Was mit deinen alten Schlüsseln verschlüsselt ist, geht verloren, außer du schließt auf einem Gerät ab, das noch angemeldet ist.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetHavePassword => 'Ich habe mein Passwort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetHavePasswordHint =>
|
||||||
|
'Deine Schlüssel öffnen sich und bleiben.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetKeepIntro =>
|
||||||
|
'Wähle ein neues Passwort: Deine Schlüssel bleiben, unter ihm und einer neuen Wiederherstellungsphrase, sodass nichts verloren geht. Deine anderen Geräte werden abgemeldet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetFinish => 'Schlüssel einrichten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginProofKeyTitle => 'Dein Passwort und ein Code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginProofKeyIntroAs(String handle) {
|
||||||
|
return 'Mit SimpleLogin als @$handle angemeldet. Du meldest dich bei deinem Konto auch mit einem Passwort an: Gib es und einen Code aus deiner Authenticator-App einmal ein, dann öffnet dein Passwort deine Schlüssel künftig nach SimpleLogin.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginProofKeyIntro =>
|
||||||
|
'Du meldest dich bei deinem Konto auch mit einem Passwort an: Gib es und einen Code aus deiner Authenticator-App einmal ein, dann öffnet dein Passwort deine Schlüssel künftig nach SimpleLogin.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetBanner(String date) {
|
||||||
|
return 'Mit deinem SimpleLogin-Konto wurden neue Schlüssel angefordert: Ab dem $date kann sie einrichten, wer es hat. Warst du das nicht, sag es mit deinem Passwort ab.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetBannerDue =>
|
||||||
|
'Mit deinem SimpleLogin-Konto wurden neue Schlüssel angefordert, und wer es hat, kann sie jetzt einrichten. Warst du das nicht, sag es mit deinem Passwort ab.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetBannerDueHere =>
|
||||||
|
'Mit deinem SimpleLogin-Konto wurden neue Schlüssel angefordert, die sich jetzt einrichten lassen. Warst du das, schließ hier ab: Deine Schlüssel bleiben. Wenn nicht, sag es mit deinem Passwort ab.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetFinishHere => 'Hier abschließen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginCallOffIntro =>
|
||||||
|
'Dein Passwort sagt das Zurücksetzen deiner Schlüssel ab.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetCalledOff => 'Zurücksetzen abgesagt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetFinished =>
|
||||||
|
'Neues Passwort und neue Wiederherstellungsphrase gesetzt.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get simpleLoginUnavailable =>
|
String get simpleLoginUnavailable =>
|
||||||
'Diese Instanz bietet die Anmeldung mit SimpleLogin nicht an.';
|
'Diese Instanz bietet die Anmeldung mit SimpleLogin nicht an.';
|
||||||
|
|
|
||||||
|
|
@ -983,6 +983,103 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get simpleLoginForgotDoesNotFit => 'The recovery phrase isn\'t right.';
|
String get simpleLoginForgotDoesNotFit => 'The recovery phrase isn\'t right.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetWaiting(String date) {
|
||||||
|
return 'New keys were asked for: from $date, a SimpleLogin sign-in can set them up. Your password opening your keys calls it off.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginPhraseLost => 'Lost your recovery phrase too?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetTitle => 'New keys';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetIntro(String instance) {
|
||||||
|
return 'Without your password or your recovery phrase, nobody can open your keys, not even $instance. New ones can be set up 72 hours from now, when you sign in with SimpleLogin again. Your handle, your profile and what you published stay; what was encrypted under your old keys is lost, unless you finish on a device still signed in, which keeps them.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetIntroDevices =>
|
||||||
|
'Meanwhile your signed-in devices show the reset, and your password calls it off there, should someone else have asked for it.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetAsk => 'Ask for new keys';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetAskedTitle => 'New keys asked for';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetAskedIntro(String date) {
|
||||||
|
return 'From $date, sign in with SimpleLogin again to set them up. Should you find your password or your recovery phrase before then, opening your keys with it calls the reset off.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetDueTitle => 'Your new keys';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetDueIntro =>
|
||||||
|
'The new keys you asked for can be set up now. Or open your old keys with your password, which calls the reset off.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetNewKeys => 'Set up new keys';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetNewKeysHint =>
|
||||||
|
'A new password and recovery phrase. What was encrypted under your old keys is lost, unless you finish on a device still signed in.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetHavePassword => 'I have my password';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetHavePasswordHint => 'Your keys open, and stay.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetKeepIntro =>
|
||||||
|
'Choose a new password: your keys stay, under it and a new recovery phrase, so nothing is lost. Your other devices are signed out.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetFinish => 'Set up the keys';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginProofKeyTitle => 'Your password and a code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginProofKeyIntroAs(String handle) {
|
||||||
|
return 'Signed in with SimpleLogin as @$handle. Your account also signs in with a password: type it and a code from your authenticator once, and your password opens your keys after SimpleLogin from then on.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginProofKeyIntro =>
|
||||||
|
'Your account also signs in with a password: type it and a code from your authenticator once, and your password opens your keys after SimpleLogin from then on.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String simpleLoginResetBanner(String date) {
|
||||||
|
return 'New keys were asked for with your SimpleLogin account: from $date, whoever has it can set them up. Unless that was you, call it off with your password.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetBannerDue =>
|
||||||
|
'New keys were asked for with your SimpleLogin account, and whoever has it can set them up now. Unless that was you, call it off with your password.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetBannerDueHere =>
|
||||||
|
'New keys were asked for with your SimpleLogin account and can be set up now. If that was you, finish here: your keys stay. If not, call it off with your password.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetFinishHere => 'Finish here';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginCallOffIntro =>
|
||||||
|
'Your password calls off the reset of your keys.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetCalledOff => 'Reset called off.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get simpleLoginResetFinished =>
|
||||||
|
'New password and recovery phrase set.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get simpleLoginUnavailable =>
|
String get simpleLoginUnavailable =>
|
||||||
'This instance doesn\'t offer sign-in with SimpleLogin.';
|
'This instance doesn\'t offer sign-in with SimpleLogin.';
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,18 @@ import 'package:halkyon_base_client/srp.dart' as srp;
|
||||||
|
|
||||||
/// What a password unlocks on the device (ADR 0017), for one account's salt
|
/// What a password unlocks on the device (ADR 0017), for one account's salt
|
||||||
/// and costs: SRP's private value and the key that wraps the account's
|
/// and costs: SRP's private value and the key that wraps the account's
|
||||||
/// private keys, kept only while a sign-in, reset or change needs them.
|
/// private keys, kept only while a sign-in, reset or change needs them, and
|
||||||
|
/// the public half of its proof key.
|
||||||
final class PasswordUnlock({
|
final class PasswordUnlock({
|
||||||
/// The salt and costs they were derived with.
|
/// The salt and costs they were derived with.
|
||||||
required final Uint8List salt,
|
required final Uint8List salt,
|
||||||
required final protocol.PasswordCosts costs,
|
required final protocol.PasswordCosts costs,
|
||||||
|
|
||||||
|
/// The public half of the password's proof key, which the keys of an
|
||||||
|
/// account signing in with SimpleLogin take in place of SRP's verifier
|
||||||
|
/// (ADR 0020, D19, D34): public, so [forget] leaves it.
|
||||||
|
required final Uint8List proofKey,
|
||||||
|
|
||||||
/// SRP's x, 32 bytes, big-endian ([srpSecret]).
|
/// SRP's x, 32 bytes, big-endian ([srpSecret]).
|
||||||
required final Uint8List _srpSecret,
|
required final Uint8List _srpSecret,
|
||||||
|
|
||||||
|
|
@ -204,14 +210,15 @@ abstract interface class AccountCrypto() {
|
||||||
Future<NewKeysMaterial> newKeys(String password, {UnlockedAccountKeys? keys});
|
Future<NewKeysMaterial> newKeys(String password, {UnlockedAccountKeys? keys});
|
||||||
|
|
||||||
/// The proof of [password] for [challenge] on [instanceUrl], signed by
|
/// The proof of [password] for [challenge] on [instanceUrl], signed by
|
||||||
/// the password's proof key for unlocking the keys
|
/// the password's proof key for [purpose] (unlocking the keys, or calling
|
||||||
/// (`KeyProofPurpose.unlockKeys`), with what it unlocked for the
|
/// off a reset of them, ADR 0020, D32), with what it unlocked for the
|
||||||
/// challenge's salt and costs: one Argon2id for both. The caller forgets
|
/// challenge's salt and costs: one Argon2id for both. The caller forgets
|
||||||
/// the unlock once the keys are open.
|
/// the unlock once done with it.
|
||||||
Future<KeysPasswordProof> proveKeysPassword(
|
Future<KeysPasswordProof> proveKeysPassword(
|
||||||
String password,
|
String password,
|
||||||
protocol.KeysChallenge challenge, {
|
protocol.KeysChallenge challenge, {
|
||||||
required String instanceUrl,
|
required String instanceUrl,
|
||||||
|
srp.KeyProofPurpose purpose = srp.KeyProofPurpose.unlockKeys,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// A new password for the keys of an account that signs in with
|
/// A new password for the keys of an account that signs in with
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
||||||
return PasswordUnlock(
|
return PasswordUnlock(
|
||||||
salt: salt,
|
salt: salt,
|
||||||
costs: costs,
|
costs: costs,
|
||||||
|
// Copied by proofPublicKey already.
|
||||||
|
proofKey: unlocked.proofPublicKey,
|
||||||
srpSecret: unlocked.srpSecret.extractBytes(),
|
srpSecret: unlocked.srpSecret.extractBytes(),
|
||||||
keyEncryptionKey: unlocked.keyEncryptionKey.extractBytes(),
|
keyEncryptionKey: unlocked.keyEncryptionKey.extractBytes(),
|
||||||
);
|
);
|
||||||
|
|
@ -286,6 +288,7 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
||||||
String password,
|
String password,
|
||||||
protocol.KeysChallenge challenge, {
|
protocol.KeysChallenge challenge, {
|
||||||
required String instanceUrl,
|
required String instanceUrl,
|
||||||
|
srp.KeyProofPurpose purpose = srp.KeyProofPurpose.unlockKeys,
|
||||||
}) => Isolate.run(
|
}) => Isolate.run(
|
||||||
() => _guarded(() async {
|
() => _guarded(() async {
|
||||||
final sodium = await SodiumSumoInit.init();
|
final sodium = await SodiumSumoInit.init();
|
||||||
|
|
@ -306,7 +309,7 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
||||||
challenge.challenge.toBytes(),
|
challenge.challenge.toBytes(),
|
||||||
Uint8List.sublistView(challenge.nonce),
|
Uint8List.sublistView(challenge.nonce),
|
||||||
instanceUrl: instanceUrl,
|
instanceUrl: instanceUrl,
|
||||||
purpose: srp.KeyProofPurpose.unlockKeys,
|
purpose: purpose,
|
||||||
),
|
),
|
||||||
secretKey: pair.secretKey,
|
secretKey: pair.secretKey,
|
||||||
),
|
),
|
||||||
|
|
@ -323,6 +326,7 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
||||||
unlock: PasswordUnlock(
|
unlock: PasswordUnlock(
|
||||||
salt: salt,
|
salt: salt,
|
||||||
costs: challenge.costs,
|
costs: challenge.costs,
|
||||||
|
proofKey: _own(pair.publicKey),
|
||||||
srpSecret: unlocked.srpSecret.extractBytes(),
|
srpSecret: unlocked.srpSecret.extractBytes(),
|
||||||
keyEncryptionKey: unlocked.keyEncryptionKey.extractBytes(),
|
keyEncryptionKey: unlocked.keyEncryptionKey.extractBytes(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -244,79 +244,30 @@ class _SessionEndedWarningState() extends State<_SessionEndedWarning> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = HalkyonAppLocalizations.of(context);
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colors = theme.colorScheme;
|
|
||||||
return BlocBuilder<SecurityStepCubit<bool>, SecurityStepState<bool>>(
|
return BlocBuilder<SecurityStepCubit<bool>, SecurityStepState<bool>>(
|
||||||
bloc: _step,
|
bloc: _step,
|
||||||
builder: (context, state) => Padding(
|
builder: (context, state) => AlertBox(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
title: l10n.passwordWarningTitle,
|
||||||
child: DecoratedBox(
|
lines: [
|
||||||
decoration: BoxDecoration(
|
l10n.passwordWarningBody(passwordMoment(context, widget._ended)),
|
||||||
color: colors.errorContainer,
|
],
|
||||||
borderRadius: BorderRadius.circular(12),
|
problem: switch (state.problem) {
|
||||||
|
final failed? => passwordProblemText(context, failed),
|
||||||
|
null => null,
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: state.busy
|
||||||
|
? null
|
||||||
|
: () => _step.run(() async {
|
||||||
|
await AppScope.of(
|
||||||
|
context,
|
||||||
|
).passwords.dismissSessionEndedWarning();
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
child: Text(l10n.passwordWarningDismiss),
|
||||||
),
|
),
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 8, 4),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
LucideIcons.shieldAlert,
|
|
||||||
color: colors.onErrorContainer,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n.passwordWarningTitle,
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
color: colors.onErrorContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: Text(
|
|
||||||
l10n.passwordWarningBody(
|
|
||||||
passwordMoment(context, widget._ended),
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: colors.onErrorContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (state.problem case final failed?)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8, right: 8),
|
|
||||||
child: Text(
|
|
||||||
passwordProblemText(context, failed),
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: colors.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: state.busy
|
|
||||||
? null
|
|
||||||
: () => _step.run(() async {
|
|
||||||
await AppScope.of(
|
|
||||||
context,
|
|
||||||
).passwords.dismissSessionEndedWarning();
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
child: Text(l10n.passwordWarningDismiss),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -629,66 +580,20 @@ class const _ResetsWaiting({
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = HalkyonAppLocalizations.of(context);
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
return AlertBox(
|
||||||
final colors = theme.colorScheme;
|
title: l10n.passwordResetsTitle,
|
||||||
final body = theme.textTheme.bodyMedium?.copyWith(
|
lines: [
|
||||||
color: colors.onErrorContainer,
|
if (authenticatorAt case final at?)
|
||||||
);
|
l10n.passwordResetsAuthenticator(passwordMoment(context, at)),
|
||||||
return Padding(
|
if (accountAt case final at?)
|
||||||
padding: const EdgeInsets.only(top: 8),
|
l10n.passwordResetsAccount(passwordMoment(context, at)),
|
||||||
child: DecoratedBox(
|
],
|
||||||
decoration: BoxDecoration(
|
actions: [
|
||||||
color: colors.errorContainer,
|
TextButton(
|
||||||
borderRadius: BorderRadius.circular(12),
|
onPressed: onCallOff,
|
||||||
|
child: Text(l10n.passwordResetsCallOff),
|
||||||
),
|
),
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 8, 4),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(LucideIcons.shieldAlert, color: colors.onErrorContainer),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n.passwordResetsTitle,
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
color: colors.onErrorContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (authenticatorAt case final at?)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8, right: 8),
|
|
||||||
child: Text(
|
|
||||||
l10n.passwordResetsAuthenticator(
|
|
||||||
passwordMoment(context, at),
|
|
||||||
),
|
|
||||||
style: body,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (accountAt case final at?)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8, right: 8),
|
|
||||||
child: Text(
|
|
||||||
l10n.passwordResetsAccount(passwordMoment(context, at)),
|
|
||||||
style: body,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: onCallOff,
|
|
||||||
child: Text(l10n.passwordResetsCallOff),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,80 @@ class const SheetIntro(final String text, {super.key}) extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A warning in the error container's colours, as an account's section and
|
||||||
|
/// a sheet show one: the shield and [title], [lines] below it, a [problem]
|
||||||
|
/// in the error colour, and [actions] at the end.
|
||||||
|
class const AlertBox({
|
||||||
|
required final String title,
|
||||||
|
required final List<String> lines,
|
||||||
|
final String? problem,
|
||||||
|
final List<Widget> actions = const [],
|
||||||
|
super.key,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
|
final body = theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onErrorContainer,
|
||||||
|
);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.errorContainer,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 12, 8, actions.isEmpty ? 12 : 4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.shieldAlert, color: colors.onErrorContainer),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: colors.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
for (final line in lines)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, right: 8),
|
||||||
|
child: Text(line, style: body),
|
||||||
|
),
|
||||||
|
if (problem case final problem?)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, right: 8),
|
||||||
|
child: Text(
|
||||||
|
problem,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colors.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (actions.isNotEmpty)
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: OverflowBar(
|
||||||
|
alignment: MainAxisAlignment.end,
|
||||||
|
children: actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A refusal shown in a sheet, in the error colour.
|
/// A refusal shown in a sheet, in the error colour.
|
||||||
class const SheetProblem(final String text, {super.key})
|
class const SheetProblem(final String text, {super.key})
|
||||||
extends StatelessWidget {
|
extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_app/src/handle/model/handle_problem.dart';
|
import 'package:halkyon_app/src/handle/model/handle_problem.dart';
|
||||||
|
|
@ -31,16 +32,33 @@ enum SimpleLoginStep() {
|
||||||
/// The password that opens the account's keys.
|
/// The password that opens the account's keys.
|
||||||
unlock,
|
unlock,
|
||||||
|
|
||||||
|
/// The password, which signs in too, and a code, which give the keys the
|
||||||
|
/// proof key that password makes (D34): asked once, and again after a
|
||||||
|
/// new password of ADR 0017's.
|
||||||
|
proofKey,
|
||||||
|
|
||||||
/// The recovery phrase and a new password, twice, for a password lost.
|
/// The recovery phrase and a new password, twice, for a password lost.
|
||||||
forgot,
|
forgot,
|
||||||
|
|
||||||
|
/// What a reset of the keys means, the password and the phrase lost
|
||||||
|
/// (D31), before it is asked for.
|
||||||
|
resetAsk,
|
||||||
|
|
||||||
|
/// The reset asked for, and when it takes effect.
|
||||||
|
resetAsked,
|
||||||
|
|
||||||
|
/// The reset due: new keys, or the old ones opened with the password,
|
||||||
|
/// which calls it off.
|
||||||
|
resetDue,
|
||||||
|
|
||||||
/// The handle and the recovery phrase, which link the account again.
|
/// The handle and the recovery phrase, which link the account again.
|
||||||
mend,
|
mend,
|
||||||
|
|
||||||
/// Signed in, or SimpleLogin added to the account.
|
/// Signed in, or SimpleLogin added to the account.
|
||||||
done,
|
done,
|
||||||
|
|
||||||
/// The browser closed before SimpleLogin came back: nothing happened.
|
/// The browser closed before SimpleLogin came back, or a reset of the
|
||||||
|
/// keys was asked for: nobody is signed in.
|
||||||
closed,
|
closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +81,17 @@ final class const SimpleLoginState({
|
||||||
|
|
||||||
/// Whether the sign-in is someone new's, whose account is yet to be.
|
/// Whether the sign-in is someone new's, whose account is yet to be.
|
||||||
final bool newcomer = false,
|
final bool newcomer = false,
|
||||||
|
|
||||||
|
/// Whether the account's password signs in too: its resets are ADR
|
||||||
|
/// 0017's, by mail (D34).
|
||||||
|
final bool passwordSignsIn = false,
|
||||||
|
|
||||||
|
/// When a reset of the account's keys takes effect (D31).
|
||||||
|
final DateTime? resetAt,
|
||||||
|
|
||||||
|
/// Whether the password and the phrase being made are for the keys a
|
||||||
|
/// reset gives (D31, D33).
|
||||||
|
final bool resetting = false,
|
||||||
final String phrase = '',
|
final String phrase = '',
|
||||||
final List<int> checked = const [],
|
final List<int> checked = const [],
|
||||||
}) with Equatable {
|
}) with Equatable {
|
||||||
|
|
@ -76,6 +105,10 @@ final class const SimpleLoginState({
|
||||||
String? handle,
|
String? handle,
|
||||||
String? signedInAs,
|
String? signedInAs,
|
||||||
bool? newcomer,
|
bool? newcomer,
|
||||||
|
bool? passwordSignsIn,
|
||||||
|
DateTime? resetAt,
|
||||||
|
bool clearResetAt = false,
|
||||||
|
bool? resetting,
|
||||||
String? phrase,
|
String? phrase,
|
||||||
List<int>? checked,
|
List<int>? checked,
|
||||||
}) => SimpleLoginState(
|
}) => SimpleLoginState(
|
||||||
|
|
@ -87,6 +120,9 @@ final class const SimpleLoginState({
|
||||||
handle: handle ?? this.handle,
|
handle: handle ?? this.handle,
|
||||||
signedInAs: signedInAs ?? this.signedInAs,
|
signedInAs: signedInAs ?? this.signedInAs,
|
||||||
newcomer: newcomer ?? this.newcomer,
|
newcomer: newcomer ?? this.newcomer,
|
||||||
|
passwordSignsIn: passwordSignsIn ?? this.passwordSignsIn,
|
||||||
|
resetAt: clearResetAt ? null : resetAt ?? this.resetAt,
|
||||||
|
resetting: resetting ?? this.resetting,
|
||||||
phrase: phrase ?? this.phrase,
|
phrase: phrase ?? this.phrase,
|
||||||
checked: checked ?? this.checked,
|
checked: checked ?? this.checked,
|
||||||
);
|
);
|
||||||
|
|
@ -105,6 +141,9 @@ final class const SimpleLoginState({
|
||||||
handle,
|
handle,
|
||||||
signedInAs,
|
signedInAs,
|
||||||
newcomer,
|
newcomer,
|
||||||
|
passwordSignsIn,
|
||||||
|
resetAt,
|
||||||
|
resetting,
|
||||||
phrase,
|
phrase,
|
||||||
checked,
|
checked,
|
||||||
];
|
];
|
||||||
|
|
@ -115,11 +154,13 @@ final class const SimpleLoginState({
|
||||||
/// twice, the recovery phrase shown once with three of its words typed
|
/// twice, the recovery phrase shown once with three of its words typed
|
||||||
/// back) or theirs linked again with its handle and recovery phrase (D7);
|
/// back) or theirs linked again with its handle and recovery phrase (D7);
|
||||||
/// for an account, its password, which opens its keys (or, for one lost, the
|
/// for an account, its password, which opens its keys (or, for one lost, the
|
||||||
/// recovery phrase and a new password), or, for one without keys, the steps
|
/// recovery phrase and a new password; both lost, a reset to new keys 72
|
||||||
/// making them. With [_link], SimpleLogin is added to the
|
/// hours later, D31), or, for one without keys, the steps making them; for
|
||||||
/// account of the device's person instead, who keeps everything. Nothing is
|
/// one whose password signs in too, that password and a code first, once
|
||||||
/// the device's until the last step: no account exists whose phrase was
|
/// (D34). With [_link], SimpleLogin is added to the account of the device's
|
||||||
/// never written down.
|
/// person instead, who keeps everything, and finishes a reset due there
|
||||||
|
/// with the keys the device holds (D33). Nothing is the device's until the
|
||||||
|
/// last step: no account exists whose phrase was never written down.
|
||||||
class SimpleLoginCubit(
|
class SimpleLoginCubit(
|
||||||
final SimpleLoginRepository _simpleLogin, {
|
final SimpleLoginRepository _simpleLogin, {
|
||||||
final bool _link = false,
|
final bool _link = false,
|
||||||
|
|
@ -156,34 +197,55 @@ class SimpleLoginCubit(
|
||||||
if (isClosed) {
|
if (isClosed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final arrived = state.copyWith(
|
||||||
|
newcomer: arrival is SimpleLoginNewcomer,
|
||||||
|
passwordSignsIn: switch (arrival) {
|
||||||
|
SimpleLoginAccount(:final passwordSignsIn) => passwordSignsIn,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
resetAt: switch (arrival) {
|
||||||
|
SimpleLoginAccount(:final resetAt) => resetAt,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
clearResetAt: switch (arrival) {
|
||||||
|
SimpleLoginAccount(resetAt: _?) => false,
|
||||||
|
_ => true,
|
||||||
|
},
|
||||||
|
resetting: false,
|
||||||
|
);
|
||||||
emit(switch (arrival) {
|
emit(switch (arrival) {
|
||||||
null => state.copyWith(step: SimpleLoginStep.closed, newcomer: false),
|
null => arrived.copyWith(step: SimpleLoginStep.closed),
|
||||||
SimpleLoginNewcomer(:final suggestedHandle) => state.copyWith(
|
SimpleLoginNewcomer(:final suggestedHandle) => arrived.copyWith(
|
||||||
step: SimpleLoginStep.newcomer,
|
step: SimpleLoginStep.newcomer,
|
||||||
newcomer: true,
|
|
||||||
handle: suggestedHandle ?? '',
|
handle: suggestedHandle ?? '',
|
||||||
),
|
),
|
||||||
|
SimpleLoginAccount(hasKeys: true, proofKeyMissing: true, :final handle) =>
|
||||||
|
arrived.copyWith(step: SimpleLoginStep.proofKey, signedInAs: handle),
|
||||||
|
// The device holds the keys, which stay under the new password.
|
||||||
|
SimpleLoginAccount(hasKeys: true, :final resetAt?)
|
||||||
|
when _link && _due(resetAt) =>
|
||||||
|
arrived.copyWith(step: SimpleLoginStep.password, resetting: true),
|
||||||
// The device has the keys of its person already.
|
// The device has the keys of its person already.
|
||||||
SimpleLoginAccount(hasKeys: true) when _link => state.copyWith(
|
SimpleLoginAccount(hasKeys: true) when _link => arrived.copyWith(
|
||||||
step: SimpleLoginStep.done,
|
step: SimpleLoginStep.done,
|
||||||
newcomer: false,
|
|
||||||
),
|
),
|
||||||
SimpleLoginAccount(hasKeys: true, :final handle) => state.copyWith(
|
SimpleLoginAccount(hasKeys: true, :final resetAt?, :final handle)
|
||||||
|
when _due(resetAt) =>
|
||||||
|
arrived.copyWith(step: SimpleLoginStep.resetDue, signedInAs: handle),
|
||||||
|
SimpleLoginAccount(hasKeys: true, :final handle) => arrived.copyWith(
|
||||||
step: SimpleLoginStep.unlock,
|
step: SimpleLoginStep.unlock,
|
||||||
newcomer: false,
|
|
||||||
signedInAs: handle,
|
signedInAs: handle,
|
||||||
),
|
),
|
||||||
SimpleLoginAccount(handle: null) => state.copyWith(
|
SimpleLoginAccount(handle: null) => arrived.copyWith(
|
||||||
step: SimpleLoginStep.handle,
|
step: SimpleLoginStep.handle,
|
||||||
newcomer: false,
|
|
||||||
),
|
|
||||||
SimpleLoginAccount() => state.copyWith(
|
|
||||||
step: SimpleLoginStep.password,
|
|
||||||
newcomer: false,
|
|
||||||
),
|
),
|
||||||
|
SimpleLoginAccount() => arrived.copyWith(step: SimpleLoginStep.password),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Whether a reset taking effect [at] is due.
|
||||||
|
static bool _due(DateTime at) => !at.isAfter(clock.now());
|
||||||
|
|
||||||
/// Stops waiting for the browser: the sign-in ends as closed.
|
/// Stops waiting for the browser: the sign-in ends as closed.
|
||||||
Future<void> cancelled() => _simpleLogin.cancel();
|
Future<void> cancelled() => _simpleLogin.cancel();
|
||||||
|
|
||||||
|
|
@ -296,6 +358,8 @@ class SimpleLoginCubit(
|
||||||
handle: state.handle,
|
handle: state.handle,
|
||||||
made: made,
|
made: made,
|
||||||
),
|
),
|
||||||
|
final SimpleLoginAccount account when state.resetting =>
|
||||||
|
await _simpleLogin.completeReset(account, made: made),
|
||||||
final SimpleLoginAccount account => await _simpleLogin.setFirstKeys(
|
final SimpleLoginAccount account => await _simpleLogin.setFirstKeys(
|
||||||
account,
|
account,
|
||||||
made: made,
|
made: made,
|
||||||
|
|
@ -326,6 +390,56 @@ class SimpleLoginCubit(
|
||||||
state.copyWith(step: SimpleLoginStep.unlock, clearProblem: true),
|
state.copyWith(step: SimpleLoginStep.unlock, clearProblem: true),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// The password, which signs in too, and the authenticator's [code]: the
|
||||||
|
/// keys get the proof key the password makes (D34), then open with it;
|
||||||
|
/// on the device's person's account, which holds them, SimpleLogin is
|
||||||
|
/// added.
|
||||||
|
Future<void> proofKeyEntered(String password, String code) => _run(() async {
|
||||||
|
final account = _arrival! as SimpleLoginAccount;
|
||||||
|
await _simpleLogin.addProofKey(account, password: password, code: code);
|
||||||
|
if (_link) {
|
||||||
|
emit(state.copyWith(step: SimpleLoginStep.done));
|
||||||
|
} else {
|
||||||
|
await _enter(await _simpleLogin.unlock(account, password));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The recovery phrase is lost too: what a reset of the keys means.
|
||||||
|
void phraseLost() => emit(
|
||||||
|
state.copyWith(step: SimpleLoginStep.resetAsk, clearProblem: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Asks for the reset of the keys (D31): when it takes effect, shown;
|
||||||
|
/// nobody is signed in.
|
||||||
|
Future<void> resetAsked() => _run(() async {
|
||||||
|
final at = await _simpleLogin.startReset(_arrival! as SimpleLoginAccount);
|
||||||
|
emit(state.copyWith(step: SimpleLoginStep.resetAsked, resetAt: at));
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Done with the sheet once the reset was asked for.
|
||||||
|
void finished() => emit(state.copyWith(step: SimpleLoginStep.closed));
|
||||||
|
|
||||||
|
/// The reset due: new keys, made with a new password and phrase.
|
||||||
|
void newKeysChosen() => emit(
|
||||||
|
state.copyWith(
|
||||||
|
step: SimpleLoginStep.password,
|
||||||
|
resetting: true,
|
||||||
|
passwordsDiffer: false,
|
||||||
|
clearProblem: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The reset due, the password found: it opens the keys, calling the
|
||||||
|
/// reset off.
|
||||||
|
void passwordChosen() => emit(
|
||||||
|
state.copyWith(step: SimpleLoginStep.unlock, clearProblem: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Back to the choice a reset due offers.
|
||||||
|
void resetDueShownAgain() => emit(
|
||||||
|
state.copyWith(step: SimpleLoginStep.resetDue, clearProblem: true),
|
||||||
|
);
|
||||||
|
|
||||||
/// The recovery phrase and the new password, typed twice: the keys the
|
/// The recovery phrase and the new password, typed twice: the keys the
|
||||||
/// phrase opens wrapped under it, and the account signed in.
|
/// phrase opens wrapped under it, and the account signed in.
|
||||||
Future<void> forgotEntered(
|
Future<void> forgotEntered(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||||
import 'package:halkyon_app/src/base/base_calls.dart';
|
import 'package:halkyon_app/src/base/base_calls.dart';
|
||||||
import 'package:halkyon_app/src/handle/data/handle_refusals.dart';
|
import 'package:halkyon_app/src/handle/data/handle_refusals.dart';
|
||||||
import 'package:halkyon_app/src/handle/model/handle_problem.dart';
|
import 'package:halkyon_app/src/handle/model/handle_problem.dart';
|
||||||
|
import 'package:halkyon_app/src/password/data/password_gateway.dart'
|
||||||
|
show OwnerAnswer;
|
||||||
import 'package:halkyon_app/src/password/data/serverpod_password_gateway.dart'
|
import 'package:halkyon_app/src/password/data/serverpod_password_gateway.dart'
|
||||||
show passwordProblemOf;
|
show passwordProblemOf;
|
||||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||||
|
|
@ -35,8 +39,16 @@ class ServerpodSimpleLoginGateway(
|
||||||
_failing(() async => (await _base.instance.basics()).simpleLogin);
|
_failing(() async => (await _base.instance.basics()).simpleLogin);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> hasSimpleLogin() =>
|
Future<({bool simpleLogin, DateTime? keysResetAt})> standing() =>
|
||||||
_failing(() async => (await _base.account.signInWays()).simpleLogin);
|
_failing(() async {
|
||||||
|
final ways = await _base.account.signInWays();
|
||||||
|
return (
|
||||||
|
simpleLogin: ways.simpleLogin,
|
||||||
|
// One whose password signs in resets as ADR 0017 has it, which
|
||||||
|
// its account section shows.
|
||||||
|
keysResetAt: ways.keys && !ways.password ? ways.accountResetAt : null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OidcStart> begin(
|
Future<OidcStart> begin(
|
||||||
|
|
@ -171,6 +183,59 @@ class ServerpodSimpleLoginGateway(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DateTime> startReset(
|
||||||
|
protocol.UuidValue attempt, {
|
||||||
|
SessionGrant? as,
|
||||||
|
}) => _failing(
|
||||||
|
() async => (await _calls.asGrant(
|
||||||
|
as,
|
||||||
|
(base) => base.keys.startReset(attempt: attempt),
|
||||||
|
)).takesEffectAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> completeReset(
|
||||||
|
protocol.UuidValue attempt,
|
||||||
|
protocol.KeysSetup setup, {
|
||||||
|
SessionGrant? as,
|
||||||
|
}) => _failing(
|
||||||
|
() => _calls.asGrant(
|
||||||
|
as,
|
||||||
|
(base) => base.keys.completeReset(attempt: attempt, setup: setup),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> callOffResets(protocol.KeysProof proof) =>
|
||||||
|
_failing(() => _base.keys.callOffResets(proof: proof));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<protocol.PasswordChallenge> beginOwnerProof({SessionGrant? as}) =>
|
||||||
|
_failing(() => _calls.asGrant(as, (base) => base.password.beginChange()));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setProofKey(
|
||||||
|
OwnerAnswer owner,
|
||||||
|
ByteData proofKey, {
|
||||||
|
SessionGrant? as,
|
||||||
|
}) => _failing(
|
||||||
|
() => _calls.asGrant(
|
||||||
|
as,
|
||||||
|
(base) => base.password.setProofKey(
|
||||||
|
proof: protocol.OwnerProof(
|
||||||
|
challenge: owner.challenge.challenge,
|
||||||
|
clientValue: owner.answer.clientValue,
|
||||||
|
clientProof: owner.answer.clientProof,
|
||||||
|
instanceUrl: instanceUrl,
|
||||||
|
totpCode: owner.totpCode,
|
||||||
|
recoveryCode: owner.recoveryCode,
|
||||||
|
),
|
||||||
|
proofKey: proofKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
static SessionGrant _grantOf(AuthSuccess session) =>
|
static SessionGrant _grantOf(AuthSuccess session) =>
|
||||||
SessionGrant(session.authUserId.uuid, session);
|
SessionGrant(session.authUserId.uuid, session);
|
||||||
|
|
||||||
|
|
@ -191,6 +256,10 @@ class ServerpodSimpleLoginGateway(
|
||||||
},
|
},
|
||||||
hasKeys: completed.hasKeys,
|
hasKeys: completed.hasKeys,
|
||||||
handle: completed.handle,
|
handle: completed.handle,
|
||||||
|
passwordSignsIn: completed.passwordSignsIn,
|
||||||
|
proofKeyMissing: completed.passwordSignsIn && !completed.hasProofKey,
|
||||||
|
// One whose password signs in resets as ADR 0017 has it, by mail.
|
||||||
|
resetAt: completed.passwordSignsIn ? null : completed.accountResetAt,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||||
import 'package:halkyon_app/src/handle/model/handle_problem.dart';
|
import 'package:halkyon_app/src/handle/model/handle_problem.dart';
|
||||||
|
import 'package:halkyon_app/src/password/data/password_gateway.dart'
|
||||||
|
show OwnerAnswer;
|
||||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_arrival.dart';
|
import 'package:halkyon_app/src/simplelogin/model/simplelogin_arrival.dart';
|
||||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||||
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart' show OidcStart;
|
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart' show OidcStart;
|
||||||
|
|
@ -18,8 +22,10 @@ abstract interface class SimpleLoginGateway() {
|
||||||
/// Whether the instance offers SimpleLogin.
|
/// Whether the instance offers SimpleLogin.
|
||||||
Future<bool> offered();
|
Future<bool> offered();
|
||||||
|
|
||||||
/// Whether the device's person's account signs in with SimpleLogin.
|
/// Whether the device's person's account signs in with SimpleLogin, and
|
||||||
Future<bool> hasSimpleLogin();
|
/// when a reset of its keys takes effect (D31): one asked for an account
|
||||||
|
/// whose password opens its keys alone, null for none.
|
||||||
|
Future<({bool simpleLogin, DateTime? keysResetAt})> standing();
|
||||||
|
|
||||||
/// Begins a sign-in that comes [back] by the loopback or the app's scheme:
|
/// Begins a sign-in that comes [back] by the loopback or the app's scheme:
|
||||||
/// the attempt, SimpleLogin's page and the state. With [link], as the
|
/// the attempt, SimpleLogin's page and the state. With [link], as the
|
||||||
|
|
@ -107,4 +113,37 @@ abstract interface class SimpleLoginGateway() {
|
||||||
String? handle,
|
String? handle,
|
||||||
SessionGrant? as,
|
SessionGrant? as,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Asks for a reset of the account's keys, its password and its recovery
|
||||||
|
/// phrase lost (D31), with its SimpleLogin sign-in under [attempt] of the
|
||||||
|
/// last ten minutes, which it takes: when the reset takes effect, kept
|
||||||
|
/// where one was asked for already.
|
||||||
|
Future<DateTime> startReset(protocol.UuidValue attempt, {SessionGrant? as});
|
||||||
|
|
||||||
|
/// Carries out the reset of the account's keys, due now, with its
|
||||||
|
/// SimpleLogin sign-in under [attempt] of the last ten minutes: the keys
|
||||||
|
/// [setup] describes replace its password, keys and phrase, and every
|
||||||
|
/// other session of the account ends.
|
||||||
|
Future<void> completeReset(
|
||||||
|
protocol.UuidValue attempt,
|
||||||
|
protocol.KeysSetup setup, {
|
||||||
|
SessionGrant? as,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Calls off a reset of the device's person's keys with their password's
|
||||||
|
/// [proof] for it (D32), for a challenge of [beginUnlock].
|
||||||
|
Future<void> callOffResets(protocol.KeysProof proof);
|
||||||
|
|
||||||
|
/// The first step of the owner's proof for an account whose password
|
||||||
|
/// signs in (ADR 0017): SRP's challenge, with the salt and costs.
|
||||||
|
Future<protocol.PasswordChallenge> beginOwnerProof({SessionGrant? as});
|
||||||
|
|
||||||
|
/// Gives the keys of an account whose password signs in the proof key
|
||||||
|
/// that password makes ([proofKey], its public half), with the owner's
|
||||||
|
/// proof [owner] binding it (D34): its keys then open after SimpleLogin.
|
||||||
|
Future<void> setProofKey(
|
||||||
|
OwnerAnswer owner,
|
||||||
|
ByteData proofKey, {
|
||||||
|
SessionGrant? as,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
||||||
|
|
@ -13,6 +14,7 @@ import 'package:halkyon_app/src/simplelogin/model/simplelogin_arrival.dart';
|
||||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_failed.dart';
|
import 'package:halkyon_app/src/simplelogin/model/simplelogin_failed.dart';
|
||||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
|
import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
|
||||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||||
|
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||||
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
|
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
|
||||||
|
|
||||||
/// Where SimpleLogin's forwarding page sends the browser back to, on the
|
/// Where SimpleLogin's forwarding page sends the browser back to, on the
|
||||||
|
|
@ -89,14 +91,32 @@ class SimpleLoginRepository(
|
||||||
|
|
||||||
/// Where the device's person stands with SimpleLogin (D21): nothing to
|
/// Where the device's person stands with SimpleLogin (D21): nothing to
|
||||||
/// show where the instance offers none or nobody is signed in; otherwise
|
/// show where the instance offers none or nobody is signed in; otherwise
|
||||||
/// whether their account has it.
|
/// whether their account has it, and a reset of its keys waiting (D35),
|
||||||
|
/// with whether the device holds them (D33).
|
||||||
Future<SimpleLoginStanding> standing() async {
|
Future<SimpleLoginStanding> standing() async {
|
||||||
if (_auth.status is! SignedIn || !await _gateway.offered()) {
|
if (_auth.status case SignedIn(:final authUserId)
|
||||||
return const SimpleLoginUnavailable();
|
when await _gateway.offered()) {
|
||||||
|
final (:simpleLogin, :keysResetAt) = await _gateway.standing();
|
||||||
|
if (!simpleLogin) {
|
||||||
|
return const SimpleLoginMissing();
|
||||||
|
}
|
||||||
|
return SimpleLoginLinked(
|
||||||
|
resetAt: keysResetAt,
|
||||||
|
keysHere: keysResetAt != null && await _keysKept(authUserId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SimpleLoginUnavailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether [person]'s keys are in the keyring; not where it can't be read
|
||||||
|
/// (locked, its service gone), which says nothing else of the account.
|
||||||
|
Future<bool> _keysKept(String person) async {
|
||||||
|
try {
|
||||||
|
return await _keys.read(person) != null;
|
||||||
|
} on Object catch (error) {
|
||||||
|
log('The keyring could not be read: ${error.runtimeType}', name: 'Keys');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return await _gateway.hasSimpleLogin()
|
|
||||||
? const SimpleLoginLinked()
|
|
||||||
: const SimpleLoginMissing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signs in with SimpleLogin in the browser: someone new, or an account
|
/// Signs in with SimpleLogin in the browser: someone new, or an account
|
||||||
|
|
@ -283,6 +303,87 @@ class SimpleLoginRepository(
|
||||||
return PendingSimpleLogin(grant: account.grant, keys: keys);
|
return PendingSimpleLogin(grant: account.grant, keys: keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Asks for a reset of [account]'s keys, its password and its recovery
|
||||||
|
/// phrase lost (D31), with the sign-in it arrived with, which this takes:
|
||||||
|
/// when the reset takes effect, kept where one was asked for already.
|
||||||
|
/// Nothing is the device's: the account has no keys to open until then.
|
||||||
|
Future<DateTime> startReset(SimpleLoginAccount account) =>
|
||||||
|
_gateway.startReset(account.attempt, as: account.grant);
|
||||||
|
|
||||||
|
/// Carries out the reset of [account]'s keys, due now: the keys [made]
|
||||||
|
/// ([makeKeys]: those the device holds where SimpleLogin signed in its
|
||||||
|
/// person, D33, new ones otherwise) replace its password, keys and
|
||||||
|
/// phrase. Every other session of the account ends.
|
||||||
|
Future<PendingSimpleLogin> completeReset(
|
||||||
|
SimpleLoginAccount account, {
|
||||||
|
required NewKeysMaterial made,
|
||||||
|
}) async {
|
||||||
|
await _gateway.completeReset(
|
||||||
|
account.attempt,
|
||||||
|
made.setup,
|
||||||
|
as: account.grant,
|
||||||
|
);
|
||||||
|
return PendingSimpleLogin(grant: account.grant, keys: made.keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls off the reset of the device's person's keys waiting, with their
|
||||||
|
/// [password]'s proof made for it (D32): whoever asked for it has their
|
||||||
|
/// SimpleLogin account, not their password.
|
||||||
|
Future<void> callOffReset(String password) async {
|
||||||
|
final challenge = await _gateway.beginUnlock();
|
||||||
|
final proven = await _crypto.proveKeysPassword(
|
||||||
|
password,
|
||||||
|
challenge,
|
||||||
|
instanceUrl: _gateway.instanceUrl,
|
||||||
|
purpose: srp.KeyProofPurpose.callOffResets,
|
||||||
|
);
|
||||||
|
// Nothing is opened with it.
|
||||||
|
proven.unlock.forget();
|
||||||
|
await _gateway.callOffResets(proven.proof);
|
||||||
|
_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives [account]'s keys, whose password signs in (ADR 0017), the proof
|
||||||
|
/// key that [password] makes, with the owner's proof: the password and
|
||||||
|
/// the authenticator's [code] (D34). Its password then opens its keys
|
||||||
|
/// after SimpleLogin ([unlock]).
|
||||||
|
Future<void> addProofKey(
|
||||||
|
SimpleLoginAccount account, {
|
||||||
|
required String password,
|
||||||
|
required String code,
|
||||||
|
}) async {
|
||||||
|
final challenge = await _gateway.beginOwnerProof(as: account.grant);
|
||||||
|
final unlock = await _crypto.unlock(
|
||||||
|
password,
|
||||||
|
salt: Uint8List.sublistView(challenge.salt),
|
||||||
|
costs: challenge.costs,
|
||||||
|
);
|
||||||
|
final ByteData proofKey;
|
||||||
|
final SrpAnswer answer;
|
||||||
|
try {
|
||||||
|
proofKey = ByteData.sublistView(Uint8List.fromList(unlock.proofKey));
|
||||||
|
answer = await _crypto.answer(
|
||||||
|
unlock,
|
||||||
|
challenge,
|
||||||
|
instanceUrl: _gateway.instanceUrl,
|
||||||
|
purpose: srp.PasswordProofPurpose.proofKey,
|
||||||
|
digest: srp.proofKeyDigest(Uint8List.sublistView(proofKey)),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
unlock.forget();
|
||||||
|
}
|
||||||
|
await _gateway.setProofKey(
|
||||||
|
(
|
||||||
|
challenge: challenge,
|
||||||
|
answer: answer,
|
||||||
|
totpCode: code.replaceAll(' ', ''),
|
||||||
|
recoveryCode: null,
|
||||||
|
),
|
||||||
|
proofKey,
|
||||||
|
as: account.grant,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Links the SimpleLogin account of someone new here ([newcomer]) to the
|
/// Links the SimpleLogin account of someone new here ([newcomer]) to the
|
||||||
/// account with [handle] in place of a lost one (D7), by its recovery
|
/// account with [handle] in place of a lost one (D7), by its recovery
|
||||||
/// [phrase], which opens the keys on the device first: a phrase that
|
/// [phrase], which opens the keys on the device first: a phrase that
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import 'package:halkyon_base_client/halkyon_base_client.dart' show UuidValue;
|
||||||
/// Where a SimpleLogin sign-in arrived once the browser came back (ADR
|
/// Where a SimpleLogin sign-in arrived once the browser came back (ADR
|
||||||
/// 0020, 5): someone new here, or an account. Its attempt's ID stands for
|
/// 0020, 5): someone new here, or an account. Its attempt's ID stands for
|
||||||
/// the sign-in a while longer: half an hour for someone new's sign-up, ten
|
/// the sign-in a while longer: half an hour for someone new's sign-up, ten
|
||||||
/// minutes for an account's first keys or a lost password (D16, D20).
|
/// minutes for an account's first keys, a lost password or a reset of its
|
||||||
|
/// keys (D16, D20, D31).
|
||||||
sealed class const SimpleLoginArrival() with Equatable;
|
sealed class const SimpleLoginArrival() with Equatable;
|
||||||
|
|
||||||
/// Someone new here, whose SimpleLogin identity waits under [attempt] for
|
/// Someone new here, whose SimpleLogin identity waits under [attempt] for
|
||||||
|
|
@ -33,7 +34,28 @@ final class const SimpleLoginAccount({
|
||||||
|
|
||||||
/// Its handle; null for one without, which chooses one with its keys.
|
/// Its handle; null for one without, which chooses one with its keys.
|
||||||
required final String? handle,
|
required final String? handle,
|
||||||
|
|
||||||
|
/// Whether its password signs in too (ADR 0017's): its resets are ADR
|
||||||
|
/// 0017's then, the mailed one for a password lost (D34).
|
||||||
|
final bool passwordSignsIn = false,
|
||||||
|
|
||||||
|
/// Whether its password, which signs in, has yet to get the keys' proof
|
||||||
|
/// key, which the password and a code set once (D34): until then, its
|
||||||
|
/// keys open on no device signed in with SimpleLogin.
|
||||||
|
final bool proofKeyMissing = false,
|
||||||
|
|
||||||
|
/// When a reset of its keys takes effect, asked with the password and
|
||||||
|
/// the phrase lost (D31): due once it is past. Null for none.
|
||||||
|
final DateTime? resetAt,
|
||||||
}) extends SimpleLoginArrival {
|
}) extends SimpleLoginArrival {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [attempt, grant, hasKeys, handle];
|
List<Object?> get props => [
|
||||||
|
attempt,
|
||||||
|
grant,
|
||||||
|
hasKeys,
|
||||||
|
handle,
|
||||||
|
passwordSignsIn,
|
||||||
|
proofKeyMissing,
|
||||||
|
resetAt,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,19 @@ final class const SimpleLoginUnavailable() extends SimpleLoginStanding {
|
||||||
List<Object?> get props => const [];
|
List<Object?> get props => const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The person's account signs in with SimpleLogin.
|
/// The person's account signs in with SimpleLogin; [resetAt] where a reset
|
||||||
final class const SimpleLoginLinked() extends SimpleLoginStanding {
|
/// of its keys waits (D31), which the device shows (D35).
|
||||||
|
final class const SimpleLoginLinked({
|
||||||
|
/// When the reset of the keys takes effect, due once it is past; null for
|
||||||
|
/// none.
|
||||||
|
final DateTime? resetAt,
|
||||||
|
|
||||||
|
/// Whether the device holds the keys, which a reset carried out here
|
||||||
|
/// keeps (D33).
|
||||||
|
final bool keysHere = false,
|
||||||
|
}) extends SimpleLoginStanding {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => const [];
|
List<Object?> get props => [resetAt, keysHere];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The person's account doesn't sign in with SimpleLogin yet: they can add
|
/// The person's account doesn't sign in with SimpleLogin yet: they can add
|
||||||
|
|
|
||||||
186
packages/halkyon_app/lib/src/simplelogin/widget/keys_reset.dart
Normal file
186
packages/halkyon_app/lib/src/simplelogin/widget/keys_reset.dart
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
|
||||||
|
import 'package:halkyon_app/src/app/app_scope.dart';
|
||||||
|
import 'package:halkyon_app/src/password/bloc/account_security_cubit.dart';
|
||||||
|
import 'package:halkyon_app/src/password/widget/password_texts.dart';
|
||||||
|
import 'package:halkyon_app/src/password/widget/password_widgets.dart';
|
||||||
|
import 'package:halkyon_app/src/simplelogin/bloc/simplelogin_standing_cubit.dart';
|
||||||
|
import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
|
||||||
|
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_section.dart';
|
||||||
|
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart';
|
||||||
|
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_standing_scope.dart';
|
||||||
|
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_texts.dart';
|
||||||
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// A banner at the top of an app's home while a reset of the device's
|
||||||
|
/// person's keys waits (ADR 0020, D35), where `AppMismatchBanner` shows: no
|
||||||
|
/// mail reaches an account that signs in with SimpleLogin, so this is how
|
||||||
|
/// its owner learns of a reset someone who took over their SimpleLogin
|
||||||
|
/// account asked for, and calls it off with the password (D32). Once due,
|
||||||
|
/// a device that holds the keys finishes it, keeping them (D33). Nothing
|
||||||
|
/// otherwise; the account section says the same ([KeysResetNotice]).
|
||||||
|
class const KeysResetBanner({super.key}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) =>
|
||||||
|
SimpleLoginStandingScope.around(context, const _KeysResetBanner());
|
||||||
|
}
|
||||||
|
|
||||||
|
class const _KeysResetBanner() extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final standing = context.watch<SimpleLoginStandingCubit>().state;
|
||||||
|
if (standing case SimpleLoginLinked(resetAt: _?)) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return MaterialBanner(
|
||||||
|
backgroundColor: colors.errorContainer,
|
||||||
|
leading: Icon(LucideIcons.shieldAlert, color: colors.onErrorContainer),
|
||||||
|
content: Text(
|
||||||
|
_keysResetText(context, standing),
|
||||||
|
style: TextStyle(color: colors.onErrorContainer),
|
||||||
|
),
|
||||||
|
// The text whole across the banner, alike with one action or two.
|
||||||
|
forceActionsBelow: true,
|
||||||
|
actions: _keysResetActions(context, standing),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reset of the keys waiting, in the SimpleLogin part of the account
|
||||||
|
/// section (D35), as [KeysResetBanner] has it.
|
||||||
|
class const KeysResetNotice(final SimpleLoginLinked _linked, {super.key})
|
||||||
|
extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => AlertBox(
|
||||||
|
title: HalkyonAppLocalizations.of(context).passwordResetsTitle,
|
||||||
|
lines: [_keysResetText(context, _linked)],
|
||||||
|
actions: _keysResetActions(context, _linked),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the reset [linked] has waiting is due.
|
||||||
|
bool _due(SimpleLoginLinked linked) => !linked.resetAt!.isAfter(clock.now());
|
||||||
|
|
||||||
|
String _keysResetText(BuildContext context, SimpleLoginLinked linked) {
|
||||||
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
|
return switch (_due(linked)) {
|
||||||
|
false => l10n.simpleLoginResetBanner(
|
||||||
|
passwordMoment(context, linked.resetAt!),
|
||||||
|
),
|
||||||
|
true when linked.keysHere => l10n.simpleLoginResetBannerDueHere,
|
||||||
|
true => l10n.simpleLoginResetBannerDue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _keysResetActions(BuildContext context, SimpleLoginLinked linked) {
|
||||||
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
|
return [
|
||||||
|
if (_due(linked) && linked.keysHere)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _finishHere(context),
|
||||||
|
child: Text(l10n.simpleLoginResetFinishHere),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _callOff(context),
|
||||||
|
child: Text(l10n.passwordResetsCallOff),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes the reset due on this device, which holds the keys (D33): a
|
||||||
|
/// SimpleLogin sign-in, then a new password and phrase for the same keys.
|
||||||
|
Future<void> _finishHere(BuildContext context) async {
|
||||||
|
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||||
|
final finished = HalkyonAppLocalizations.of(context).simpleLoginResetFinished;
|
||||||
|
if (await showSimpleLoginSheet(
|
||||||
|
context,
|
||||||
|
instance: simpleLoginInstanceName(context),
|
||||||
|
link: true,
|
||||||
|
)) {
|
||||||
|
messenger?.showSnackBar(SnackBar(content: Text(finished)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _callOff(BuildContext context) async {
|
||||||
|
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||||
|
final calledOff = HalkyonAppLocalizations.of(
|
||||||
|
context,
|
||||||
|
).simpleLoginResetCalledOff;
|
||||||
|
if (await showAdaptiveSheet<bool>(
|
||||||
|
context,
|
||||||
|
swipeable: false,
|
||||||
|
builder: (context) => const _CallOff(),
|
||||||
|
) ??
|
||||||
|
false) {
|
||||||
|
messenger?.showSnackBar(SnackBar(content: Text(calledOff)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls off the reset of the keys with the password alone (D32): whoever
|
||||||
|
/// asked for it has the SimpleLogin account, not the password.
|
||||||
|
class const _CallOff() extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<_CallOff> createState() => _CallOffState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CallOffState() extends State<_CallOff> {
|
||||||
|
final _password = TextEditingController();
|
||||||
|
late final _step = SecurityStepCubit<bool>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_password.dispose();
|
||||||
|
_step.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
final simpleLogin = AppScope.of(context).simpleLogin;
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
if (await _step.run(() async {
|
||||||
|
await simpleLogin.callOffReset(_password.text);
|
||||||
|
return true;
|
||||||
|
})) {
|
||||||
|
navigator.pop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
|
return BlocBuilder<SecurityStepCubit<bool>, SecurityStepState<bool>>(
|
||||||
|
bloc: _step,
|
||||||
|
builder: (context, state) => SheetScaffold(
|
||||||
|
title: l10n.passwordCallOffTitle,
|
||||||
|
busy: state.busy,
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SheetIntro(l10n.simpleLoginCallOffIntro),
|
||||||
|
PasswordField(
|
||||||
|
controller: _password,
|
||||||
|
label: l10n.passwordPassword,
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onSubmitted: (_) => _submit(),
|
||||||
|
errorText: switch (state.problem) {
|
||||||
|
final failed? => simpleLoginFailureText(context, failed),
|
||||||
|
null => null,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
footer: FilledButton(
|
||||||
|
onPressed: state.busy ? null : _submit,
|
||||||
|
child: state.busy
|
||||||
|
? const ButtonLoadingIndicator()
|
||||||
|
: Text(l10n.passwordResetsCallOff),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue