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
21 changed files with 2711 additions and 248 deletions
|
|
@ -101,6 +101,7 @@ export 'src/simplelogin/data/simplelogin_repository.dart';
|
|||
export 'src/simplelogin/model/simplelogin_arrival.dart';
|
||||
export 'src/simplelogin/model/simplelogin_failed.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_sheet.dart';
|
||||
export 'src/simplelogin/widget/simplelogin_standing_scope.dart';
|
||||
|
|
|
|||
|
|
@ -266,6 +266,32 @@
|
|||
"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.",
|
||||
"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.",
|
||||
"simpleLoginDenied": "Die Anmeldung bei SimpleLogin wurde abgelehnt, also wurde nichts geteilt.",
|
||||
"simpleLoginNoBrowser": "Es gibt keinen Browser, der die Seite von SimpleLogin öffnen kann.",
|
||||
|
|
|
|||
|
|
@ -600,6 +600,67 @@
|
|||
"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.",
|
||||
"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.",
|
||||
"simpleLoginDenied": "SimpleLogin's sign-in was declined, so nothing was shared.",
|
||||
"simpleLoginNoBrowser": "There's no browser to open SimpleLogin's page in.",
|
||||
|
|
|
|||
|
|
@ -1699,6 +1699,162 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'The recovery phrase isn\'t right.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1002,6 +1002,105 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
String get simpleLoginForgotDoesNotFit =>
|
||||
'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
|
||||
String get simpleLoginUnavailable =>
|
||||
'Diese Instanz bietet die Anmeldung mit SimpleLogin nicht an.';
|
||||
|
|
|
|||
|
|
@ -983,6 +983,103 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get simpleLoginUnavailable =>
|
||||
'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
|
||||
/// 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({
|
||||
/// The salt and costs they were derived with.
|
||||
required final Uint8List salt,
|
||||
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]).
|
||||
required final Uint8List _srpSecret,
|
||||
|
||||
|
|
@ -204,14 +210,15 @@ abstract interface class AccountCrypto() {
|
|||
Future<NewKeysMaterial> newKeys(String password, {UnlockedAccountKeys? keys});
|
||||
|
||||
/// The proof of [password] for [challenge] on [instanceUrl], signed by
|
||||
/// the password's proof key for unlocking the keys
|
||||
/// (`KeyProofPurpose.unlockKeys`), with what it unlocked for the
|
||||
/// the password's proof key for [purpose] (unlocking the keys, or calling
|
||||
/// 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
|
||||
/// the unlock once the keys are open.
|
||||
/// the unlock once done with it.
|
||||
Future<KeysPasswordProof> proveKeysPassword(
|
||||
String password,
|
||||
protocol.KeysChallenge challenge, {
|
||||
required String instanceUrl,
|
||||
srp.KeyProofPurpose purpose = srp.KeyProofPurpose.unlockKeys,
|
||||
});
|
||||
|
||||
/// A new password for the keys of an account that signs in with
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
return PasswordUnlock(
|
||||
salt: salt,
|
||||
costs: costs,
|
||||
// Copied by proofPublicKey already.
|
||||
proofKey: unlocked.proofPublicKey,
|
||||
srpSecret: unlocked.srpSecret.extractBytes(),
|
||||
keyEncryptionKey: unlocked.keyEncryptionKey.extractBytes(),
|
||||
);
|
||||
|
|
@ -286,6 +288,7 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
String password,
|
||||
protocol.KeysChallenge challenge, {
|
||||
required String instanceUrl,
|
||||
srp.KeyProofPurpose purpose = srp.KeyProofPurpose.unlockKeys,
|
||||
}) => Isolate.run(
|
||||
() => _guarded(() async {
|
||||
final sodium = await SodiumSumoInit.init();
|
||||
|
|
@ -306,7 +309,7 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
challenge.challenge.toBytes(),
|
||||
Uint8List.sublistView(challenge.nonce),
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: srp.KeyProofPurpose.unlockKeys,
|
||||
purpose: purpose,
|
||||
),
|
||||
secretKey: pair.secretKey,
|
||||
),
|
||||
|
|
@ -323,6 +326,7 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
unlock: PasswordUnlock(
|
||||
salt: salt,
|
||||
costs: challenge.costs,
|
||||
proofKey: _own(pair.publicKey),
|
||||
srpSecret: unlocked.srpSecret.extractBytes(),
|
||||
keyEncryptionKey: unlocked.keyEncryptionKey.extractBytes(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -244,79 +244,30 @@ class _SessionEndedWarningState() extends State<_SessionEndedWarning> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
return BlocBuilder<SecurityStepCubit<bool>, SecurityStepState<bool>>(
|
||||
bloc: _step,
|
||||
builder: (context, state) => Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
builder: (context, state) => AlertBox(
|
||||
title: l10n.passwordWarningTitle,
|
||||
lines: [
|
||||
l10n.passwordWarningBody(passwordMoment(context, widget._ended)),
|
||||
],
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(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),
|
||||
return AlertBox(
|
||||
title: l10n.passwordResetsTitle,
|
||||
lines: [
|
||||
if (authenticatorAt case final at?)
|
||||
l10n.passwordResetsAuthenticator(passwordMoment(context, at)),
|
||||
if (accountAt case final at?)
|
||||
l10n.passwordResetsAccount(passwordMoment(context, at)),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
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.
|
||||
class const SheetProblem(final String text, {super.key})
|
||||
extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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.
|
||||
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.
|
||||
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.
|
||||
mend,
|
||||
|
||||
/// Signed in, or SimpleLogin added to the account.
|
||||
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,
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +81,17 @@ final class const SimpleLoginState({
|
|||
|
||||
/// Whether the sign-in is someone new's, whose account is yet to be.
|
||||
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 List<int> checked = const [],
|
||||
}) with Equatable {
|
||||
|
|
@ -76,6 +105,10 @@ final class const SimpleLoginState({
|
|||
String? handle,
|
||||
String? signedInAs,
|
||||
bool? newcomer,
|
||||
bool? passwordSignsIn,
|
||||
DateTime? resetAt,
|
||||
bool clearResetAt = false,
|
||||
bool? resetting,
|
||||
String? phrase,
|
||||
List<int>? checked,
|
||||
}) => SimpleLoginState(
|
||||
|
|
@ -87,6 +120,9 @@ final class const SimpleLoginState({
|
|||
handle: handle ?? this.handle,
|
||||
signedInAs: signedInAs ?? this.signedInAs,
|
||||
newcomer: newcomer ?? this.newcomer,
|
||||
passwordSignsIn: passwordSignsIn ?? this.passwordSignsIn,
|
||||
resetAt: clearResetAt ? null : resetAt ?? this.resetAt,
|
||||
resetting: resetting ?? this.resetting,
|
||||
phrase: phrase ?? this.phrase,
|
||||
checked: checked ?? this.checked,
|
||||
);
|
||||
|
|
@ -105,6 +141,9 @@ final class const SimpleLoginState({
|
|||
handle,
|
||||
signedInAs,
|
||||
newcomer,
|
||||
passwordSignsIn,
|
||||
resetAt,
|
||||
resetting,
|
||||
phrase,
|
||||
checked,
|
||||
];
|
||||
|
|
@ -115,11 +154,13 @@ final class const SimpleLoginState({
|
|||
/// twice, the recovery phrase shown once with three of its words typed
|
||||
/// 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
|
||||
/// recovery phrase and a new password), or, for one without keys, the steps
|
||||
/// making them. With [_link], SimpleLogin is added to the
|
||||
/// account of the device's person instead, who keeps everything. Nothing is
|
||||
/// the device's until the last step: no account exists whose phrase was
|
||||
/// never written down.
|
||||
/// recovery phrase and a new password; both lost, a reset to new keys 72
|
||||
/// hours later, D31), or, for one without keys, the steps making them; for
|
||||
/// one whose password signs in too, that password and a code first, once
|
||||
/// (D34). With [_link], SimpleLogin is added to the account of the device's
|
||||
/// 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(
|
||||
final SimpleLoginRepository _simpleLogin, {
|
||||
final bool _link = false,
|
||||
|
|
@ -156,34 +197,55 @@ class SimpleLoginCubit(
|
|||
if (isClosed) {
|
||||
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) {
|
||||
null => state.copyWith(step: SimpleLoginStep.closed, newcomer: false),
|
||||
SimpleLoginNewcomer(:final suggestedHandle) => state.copyWith(
|
||||
null => arrived.copyWith(step: SimpleLoginStep.closed),
|
||||
SimpleLoginNewcomer(:final suggestedHandle) => arrived.copyWith(
|
||||
step: SimpleLoginStep.newcomer,
|
||||
newcomer: true,
|
||||
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.
|
||||
SimpleLoginAccount(hasKeys: true) when _link => state.copyWith(
|
||||
SimpleLoginAccount(hasKeys: true) when _link => arrived.copyWith(
|
||||
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,
|
||||
newcomer: false,
|
||||
signedInAs: handle,
|
||||
),
|
||||
SimpleLoginAccount(handle: null) => state.copyWith(
|
||||
SimpleLoginAccount(handle: null) => arrived.copyWith(
|
||||
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.
|
||||
Future<void> cancelled() => _simpleLogin.cancel();
|
||||
|
||||
|
|
@ -296,6 +358,8 @@ class SimpleLoginCubit(
|
|||
handle: state.handle,
|
||||
made: made,
|
||||
),
|
||||
final SimpleLoginAccount account when state.resetting =>
|
||||
await _simpleLogin.completeReset(account, made: made),
|
||||
final SimpleLoginAccount account => await _simpleLogin.setFirstKeys(
|
||||
account,
|
||||
made: made,
|
||||
|
|
@ -326,6 +390,56 @@ class SimpleLoginCubit(
|
|||
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
|
||||
/// phrase opens wrapped under it, and the account signed in.
|
||||
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/base/base_calls.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/password/data/password_gateway.dart'
|
||||
show OwnerAnswer;
|
||||
import 'package:halkyon_app/src/password/data/serverpod_password_gateway.dart'
|
||||
show passwordProblemOf;
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
|
|
@ -35,8 +39,16 @@ class ServerpodSimpleLoginGateway(
|
|||
_failing(() async => (await _base.instance.basics()).simpleLogin);
|
||||
|
||||
@override
|
||||
Future<bool> hasSimpleLogin() =>
|
||||
_failing(() async => (await _base.account.signInWays()).simpleLogin);
|
||||
Future<({bool simpleLogin, DateTime? keysResetAt})> standing() =>
|
||||
_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
|
||||
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) =>
|
||||
SessionGrant(session.authUserId.uuid, session);
|
||||
|
||||
|
|
@ -191,6 +256,10 @@ class ServerpodSimpleLoginGateway(
|
|||
},
|
||||
hasKeys: completed.hasKeys,
|
||||
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/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_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart' show OidcStart;
|
||||
|
|
@ -18,8 +22,10 @@ abstract interface class SimpleLoginGateway() {
|
|||
/// Whether the instance offers SimpleLogin.
|
||||
Future<bool> offered();
|
||||
|
||||
/// Whether the device's person's account signs in with SimpleLogin.
|
||||
Future<bool> hasSimpleLogin();
|
||||
/// Whether the device's person's account signs in with SimpleLogin, and
|
||||
/// 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:
|
||||
/// the attempt, SimpleLogin's page and the state. With [link], as the
|
||||
|
|
@ -107,4 +113,37 @@ abstract interface class SimpleLoginGateway() {
|
|||
String? handle,
|
||||
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:developer';
|
||||
|
||||
import 'package:flutter/foundation.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_standing.dart';
|
||||
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';
|
||||
|
||||
/// 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
|
||||
/// 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 {
|
||||
if (_auth.status is! SignedIn || !await _gateway.offered()) {
|
||||
return const SimpleLoginUnavailable();
|
||||
if (_auth.status case SignedIn(:final authUserId)
|
||||
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
|
||||
|
|
@ -283,6 +303,87 @@ class SimpleLoginRepository(
|
|||
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
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// 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
|
||||
/// 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;
|
||||
|
||||
/// 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.
|
||||
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 {
|
||||
@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 [];
|
||||
}
|
||||
|
||||
/// The person's account signs in with SimpleLogin.
|
||||
final class const SimpleLoginLinked() extends SimpleLoginStanding {
|
||||
/// The person's account signs in with SimpleLogin; [resetAt] where a reset
|
||||
/// 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
|
||||
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
|
||||
|
|
|
|||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_app/src/app/app_scope.dart';
|
|||
import 'package:halkyon_app/src/instance/instance_scope.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/keys_reset.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_standing_scope.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -12,9 +13,10 @@ import 'package:material_ui/material_ui.dart';
|
|||
/// The SimpleLogin part of an app's account section (ADR 0020, D21), which
|
||||
/// each app places above `AccountSecuritySection`: for someone without
|
||||
/// SimpleLogin a heading and Add SimpleLogin; once added, one more way in,
|
||||
/// a row without heading lined up with the others. Nothing where the
|
||||
/// instance offers no SimpleLogin or nobody is signed in. [gapAbove] is
|
||||
/// the space above its heading, none above the row.
|
||||
/// a row without heading lined up with the others, and a reset of the keys
|
||||
/// waiting below it (D35). Nothing where the instance offers no SimpleLogin
|
||||
/// or nobody is signed in. [gapAbove] is the space above its heading, none
|
||||
/// above the row.
|
||||
class const SimpleLoginSection({
|
||||
final double gapAbove = 0,
|
||||
super.key,
|
||||
|
|
@ -35,14 +37,20 @@ class const _SimpleLoginSection({required final double gapAbove})
|
|||
return switch (context.watch<SimpleLoginStandingCubit>().state) {
|
||||
SimpleLoginChecking() ||
|
||||
SimpleLoginUnavailable() => const SizedBox.shrink(),
|
||||
SimpleLoginLinked() => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(l10n.simpleLoginSectionTitle),
|
||||
subtitle: Text(l10n.simpleLoginSignsYouIn),
|
||||
trailing: Icon(
|
||||
LucideIcons.circleCheck,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
final SimpleLoginLinked linked => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(l10n.simpleLoginSectionTitle),
|
||||
subtitle: Text(l10n.simpleLoginSignsYouIn),
|
||||
trailing: Icon(
|
||||
LucideIcons.circleCheck,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (linked.resetAt != null) KeysResetNotice(linked),
|
||||
],
|
||||
),
|
||||
SimpleLoginMissing() => Padding(
|
||||
padding: EdgeInsets.only(top: gapAbove),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/handle/bloc/handle_check_cubit.dart';
|
||||
import 'package:halkyon_app/src/handle/widget/handle_field.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.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_cubit.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_texts.dart';
|
||||
|
|
@ -53,6 +55,7 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
final _password = TextEditingController();
|
||||
final _again = TextEditingController();
|
||||
final _phrase = TextEditingController();
|
||||
final _code = TextEditingController();
|
||||
final _words = [for (var i = 0; i < 3; i++) TextEditingController()];
|
||||
|
||||
/// What the instance says of the handle typed, asked as it is typed (ADR
|
||||
|
|
@ -78,6 +81,7 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
_password.dispose();
|
||||
_again.dispose();
|
||||
_phrase.dispose();
|
||||
_code.dispose();
|
||||
for (final word in _words) {
|
||||
word.dispose();
|
||||
}
|
||||
|
|
@ -104,11 +108,18 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
_cubit.phraseChecked([for (final word in _words) word.text]);
|
||||
case SimpleLoginStep.unlock:
|
||||
_cubit.unlockEntered(_password.text);
|
||||
case SimpleLoginStep.proofKey:
|
||||
_cubit.proofKeyEntered(_password.text, _code.text);
|
||||
case SimpleLoginStep.forgot:
|
||||
_cubit.forgotEntered(_phrase.text, _password.text, _again.text);
|
||||
case SimpleLoginStep.resetAsk:
|
||||
_cubit.resetAsked();
|
||||
case SimpleLoginStep.resetAsked:
|
||||
_cubit.finished();
|
||||
case SimpleLoginStep.mend:
|
||||
_cubit.mendEntered(_handle.text, _phrase.text);
|
||||
case SimpleLoginStep.newcomer ||
|
||||
SimpleLoginStep.resetDue ||
|
||||
SimpleLoginStep.done ||
|
||||
SimpleLoginStep.closed:
|
||||
break;
|
||||
|
|
@ -127,6 +138,9 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
case SimpleLoginStep.password || SimpleLoginStep.unlock:
|
||||
_password.clear();
|
||||
_again.clear();
|
||||
case SimpleLoginStep.proofKey:
|
||||
_password.clear();
|
||||
_code.clear();
|
||||
case SimpleLoginStep.forgot:
|
||||
_phrase.clear();
|
||||
_password.clear();
|
||||
|
|
@ -141,7 +155,11 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
_phrase.clear();
|
||||
case SimpleLoginStep.handle:
|
||||
_handleChecked().typed(_handle.text);
|
||||
case SimpleLoginStep.browser || SimpleLoginStep.phrase:
|
||||
case SimpleLoginStep.browser ||
|
||||
SimpleLoginStep.phrase ||
|
||||
SimpleLoginStep.resetAsk ||
|
||||
SimpleLoginStep.resetAsked ||
|
||||
SimpleLoginStep.resetDue:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -167,6 +185,11 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
return SheetScaffold(
|
||||
title: switch (state.step) {
|
||||
SimpleLoginStep.newcomer => l10n.simpleLoginNewcomerTitle,
|
||||
// A device that holds the keys keeps them (D33).
|
||||
SimpleLoginStep.password when state.resetting && widget.link =>
|
||||
l10n.simpleLoginForgotTitle,
|
||||
SimpleLoginStep.password when state.resetting =>
|
||||
l10n.simpleLoginResetTitle,
|
||||
SimpleLoginStep.handle || SimpleLoginStep.password
|
||||
when state.newcomer =>
|
||||
l10n.passwordCreateTitle,
|
||||
|
|
@ -178,7 +201,11 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
SimpleLoginStep.phrase => l10n.passwordPhraseTitle,
|
||||
SimpleLoginStep.phraseCheck => l10n.passwordPhraseCheckTitle,
|
||||
SimpleLoginStep.unlock => l10n.simpleLoginUnlockTitle,
|
||||
SimpleLoginStep.proofKey => l10n.simpleLoginProofKeyTitle,
|
||||
SimpleLoginStep.forgot => l10n.simpleLoginForgotTitle,
|
||||
SimpleLoginStep.resetAsk => l10n.simpleLoginResetTitle,
|
||||
SimpleLoginStep.resetAsked => l10n.simpleLoginResetAskedTitle,
|
||||
SimpleLoginStep.resetDue => l10n.simpleLoginResetDueTitle,
|
||||
SimpleLoginStep.mend => l10n.simpleLoginMendTitle,
|
||||
_ when widget.link => l10n.simpleLoginAddTitle,
|
||||
_ => l10n.simpleLoginTitle,
|
||||
|
|
@ -200,6 +227,7 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
child: Text(HalkyonUiLocalizations.of(context).cancel),
|
||||
),
|
||||
SimpleLoginStep.newcomer ||
|
||||
SimpleLoginStep.resetDue ||
|
||||
SimpleLoginStep.done ||
|
||||
SimpleLoginStep.closed => null,
|
||||
_ => FilledButton(
|
||||
|
|
@ -211,8 +239,17 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
SimpleLoginStep.phrase => l10n.passwordPhraseWrittenDown,
|
||||
SimpleLoginStep.phraseCheck when state.newcomer =>
|
||||
l10n.simpleLoginCreate,
|
||||
SimpleLoginStep.phraseCheck when state.resetting =>
|
||||
l10n.simpleLoginResetFinish,
|
||||
SimpleLoginStep.unlock => l10n.simpleLoginOpen,
|
||||
SimpleLoginStep.proofKey when widget.link =>
|
||||
l10n.passwordContinue,
|
||||
SimpleLoginStep.proofKey => l10n.simpleLoginOpen,
|
||||
SimpleLoginStep.forgot => l10n.passwordSignInAction,
|
||||
SimpleLoginStep.resetAsk => l10n.simpleLoginResetAsk,
|
||||
SimpleLoginStep.resetAsked => HalkyonUiLocalizations.of(
|
||||
context,
|
||||
).close,
|
||||
SimpleLoginStep.mend => l10n.simpleLoginMend,
|
||||
_ => l10n.passwordContinue,
|
||||
}),
|
||||
|
|
@ -275,7 +312,11 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SheetIntro(l10n.simpleLoginPasswordIntro),
|
||||
SheetIntro(
|
||||
state.resetting && widget.link
|
||||
? l10n.simpleLoginResetKeepIntro
|
||||
: l10n.simpleLoginPasswordIntro,
|
||||
),
|
||||
PasswordField(
|
||||
controller: _password,
|
||||
label: l10n.passwordNewPassword,
|
||||
|
|
@ -332,6 +373,15 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
final handle? => l10n.simpleLoginUnlockIntroAs(handle),
|
||||
null => l10n.simpleLoginUnlockIntro,
|
||||
}),
|
||||
if (state.resetAt case final at? when !_due(at)) ...[
|
||||
AlertBox(
|
||||
title: l10n.passwordResetsTitle,
|
||||
lines: [
|
||||
l10n.simpleLoginResetWaiting(passwordMoment(context, at)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
PasswordField(
|
||||
controller: _password,
|
||||
label: l10n.passwordPassword,
|
||||
|
|
@ -341,12 +391,48 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
onSubmitted: (_) => _submit(state),
|
||||
errorText: problem,
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton(
|
||||
onPressed: enabled ? _cubit.forgotChosen : null,
|
||||
child: Text(l10n.passwordForgot),
|
||||
// One whose password signs in too resets it as ADR 0017 has it,
|
||||
// by mail, where it signs in with it (D34).
|
||||
if (!state.passwordSignsIn)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton(
|
||||
onPressed: enabled ? _cubit.forgotChosen : null,
|
||||
child: Text(l10n.passwordForgot),
|
||||
),
|
||||
),
|
||||
if (state.resetAt case final at? when _due(at))
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton(
|
||||
onPressed: enabled ? _cubit.resetDueShownAgain : null,
|
||||
child: Text(l10n.passwordBack),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SimpleLoginStep.proofKey => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SheetIntro(switch (state.signedInAs) {
|
||||
final handle? when !widget.link => l10n.simpleLoginProofKeyIntroAs(
|
||||
handle,
|
||||
),
|
||||
_ => l10n.simpleLoginProofKeyIntro,
|
||||
}),
|
||||
PasswordField(
|
||||
controller: _password,
|
||||
label: l10n.passwordPassword,
|
||||
enabled: enabled,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CodeField(
|
||||
controller: _code,
|
||||
kind: CodeKind.totp,
|
||||
enabled: enabled,
|
||||
errorText: problem,
|
||||
onSubmitted: (_) => _submit(state),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -360,7 +446,13 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
autofocus: true,
|
||||
errorText: _phraseProblem(state.problem) ? problem : null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton(
|
||||
onPressed: enabled ? _cubit.phraseLost : null,
|
||||
child: Text(l10n.simpleLoginPhraseLost),
|
||||
),
|
||||
),
|
||||
PasswordField(
|
||||
controller: _password,
|
||||
label: l10n.passwordNewPassword,
|
||||
|
|
@ -391,6 +483,44 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
),
|
||||
],
|
||||
),
|
||||
SimpleLoginStep.resetAsk => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SheetIntro(l10n.simpleLoginResetIntro(widget.instance)),
|
||||
SheetIntro(l10n.simpleLoginResetIntroDevices),
|
||||
if (problem case final problem?) SheetProblem(problem),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton(
|
||||
onPressed: enabled ? _cubit.forgotChosen : null,
|
||||
child: Text(l10n.passwordBack),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SimpleLoginStep.resetAsked => SheetIntro(
|
||||
l10n.simpleLoginResetAskedIntro(
|
||||
passwordMoment(context, state.resetAt!),
|
||||
),
|
||||
),
|
||||
SimpleLoginStep.resetDue => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SheetIntro(l10n.simpleLoginResetDueIntro),
|
||||
_Choice(
|
||||
icon: LucideIcons.keyRound,
|
||||
title: l10n.simpleLoginResetNewKeys,
|
||||
hint: l10n.simpleLoginResetNewKeysHint,
|
||||
onTap: _cubit.newKeysChosen,
|
||||
),
|
||||
_Choice(
|
||||
icon: LucideIcons.lockOpen,
|
||||
title: l10n.simpleLoginResetHavePassword,
|
||||
hint: l10n.simpleLoginResetHavePasswordHint,
|
||||
onTap: _cubit.passwordChosen,
|
||||
),
|
||||
],
|
||||
),
|
||||
SimpleLoginStep.mend => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
|
@ -415,6 +545,9 @@ class _SimpleLoginState() extends State<_SimpleLogin> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether a reset taking effect [at] is due.
|
||||
bool _due(DateTime at) => !at.isAfter(clock.now());
|
||||
|
||||
/// Whether [problem] is the recovery phrase's, shown at its field.
|
||||
bool _phraseProblem(Exception? problem) => switch (problem) {
|
||||
PasswordFailed(
|
||||
|
|
|
|||
|
|
@ -72,17 +72,35 @@ class FakeAccountCrypto() implements AccountCrypto {
|
|||
static ByteData passwordKeyOf(String password) =>
|
||||
ByteData.sublistView(utf8.encode('password key of $password'));
|
||||
|
||||
/// Whether [proof] is signed by the password's proof key whose public
|
||||
/// half is [passwordKey].
|
||||
static bool provesPassword(protocol.KeysProof proof, ByteData? passwordKey) {
|
||||
/// The signature of [password]'s proof key for [purpose], as the fake
|
||||
/// makes it.
|
||||
static ByteData passwordSignatureOf(
|
||||
String password, {
|
||||
srp.KeyProofPurpose purpose = srp.KeyProofPurpose.unlockKeys,
|
||||
}) => ByteData.sublistView(
|
||||
utf8.encode('signed by password $password for ${purpose.name}'),
|
||||
);
|
||||
|
||||
/// Whether [proof] is signed for [purpose] by the password's proof key
|
||||
/// whose public half is [passwordKey].
|
||||
static bool provesPassword(
|
||||
protocol.KeysProof proof,
|
||||
ByteData? passwordKey, {
|
||||
srp.KeyProofPurpose purpose = srp.KeyProofPurpose.unlockKeys,
|
||||
}) {
|
||||
if (passwordKey == null) {
|
||||
return false;
|
||||
}
|
||||
final signed = utf8.decode(Uint8List.sublistView(proof.signature));
|
||||
final key = utf8.decode(Uint8List.sublistView(passwordKey));
|
||||
return signed.startsWith('signed by password ') &&
|
||||
key ==
|
||||
'password key of ${signed.substring('signed by password '.length)}';
|
||||
if (!key.startsWith('password key of ')) {
|
||||
return false;
|
||||
}
|
||||
final expected = passwordSignatureOf(
|
||||
key.substring('password key of '.length),
|
||||
purpose: purpose,
|
||||
);
|
||||
return utf8.decode(Uint8List.sublistView(proof.signature)) ==
|
||||
utf8.decode(Uint8List.sublistView(expected));
|
||||
}
|
||||
|
||||
static String _words(String phrase) =>
|
||||
|
|
@ -167,6 +185,7 @@ class FakeAccountCrypto() implements AccountCrypto {
|
|||
final unlock = PasswordUnlock(
|
||||
salt: salt,
|
||||
costs: costs,
|
||||
proofKey: Uint8List.sublistView(passwordKeyOf(password)),
|
||||
srpSecret: Uint8List.fromList(utf8.encode(password)),
|
||||
keyEncryptionKey: Uint8List.fromList(utf8.encode(password)),
|
||||
);
|
||||
|
|
@ -293,6 +312,7 @@ class FakeAccountCrypto() implements AccountCrypto {
|
|||
String password,
|
||||
protocol.KeysChallenge challenge, {
|
||||
required String instanceUrl,
|
||||
srp.KeyProofPurpose purpose = srp.KeyProofPurpose.unlockKeys,
|
||||
}) async {
|
||||
final unlock = await this.unlock(
|
||||
password,
|
||||
|
|
@ -302,9 +322,7 @@ class FakeAccountCrypto() implements AccountCrypto {
|
|||
return (
|
||||
proof: protocol.KeysProof(
|
||||
challenge: challenge.challenge,
|
||||
signature: ByteData.sublistView(
|
||||
utf8.encode('signed by password $password'),
|
||||
),
|
||||
signature: passwordSignatureOf(password, purpose: purpose),
|
||||
instanceUrl: instanceUrl,
|
||||
),
|
||||
unlock: unlock,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue