feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1daa1b649a
commit
a4819c3d39
44 changed files with 9456 additions and 35 deletions
|
|
@ -43,12 +43,14 @@ export 'src/auth/model/auth_status.dart';
|
|||
export 'src/auth/model/email_sign_in.dart';
|
||||
export 'src/auth/model/github_link_outcome.dart';
|
||||
export 'src/auth/model/github_sign_in.dart';
|
||||
export 'src/auth/model/session_grant.dart';
|
||||
export 'src/auth/model/sign_in_ways.dart';
|
||||
export 'src/auth/widget/account_tiles.dart';
|
||||
export 'src/auth/widget/email_sign_in_sheet.dart';
|
||||
export 'src/auth/widget/session_gate.dart';
|
||||
export 'src/auth/widget/start_screen.dart';
|
||||
export 'src/base/base_client.dart';
|
||||
export 'src/files/file_saver.dart';
|
||||
export 'src/instance/cldr_license.dart';
|
||||
export 'src/instance/country_names.dart';
|
||||
export 'src/instance/european_union.dart';
|
||||
|
|
@ -67,6 +69,17 @@ export 'src/instance/widget/instance_location.dart';
|
|||
export 'src/instance/widget/instance_location_words.dart';
|
||||
export 'src/instance/widget/instance_step.dart';
|
||||
export 'src/instance/widget/instance_tile.dart';
|
||||
export 'src/password/data/account_crypto.dart';
|
||||
export 'src/password/data/account_key_store.dart';
|
||||
export 'src/password/data/password_gateway.dart';
|
||||
export 'src/password/data/password_repository.dart';
|
||||
export 'src/password/data/serverpod_password_gateway.dart';
|
||||
export 'src/password/data/sodium_account_crypto.dart';
|
||||
export 'src/password/model/account_authenticator.dart';
|
||||
export 'src/password/model/account_security.dart';
|
||||
export 'src/password/model/password_failed.dart';
|
||||
export 'src/password/model/phrase_check.dart';
|
||||
export 'src/password/model/unlocked_account_keys.dart';
|
||||
export 'src/person/person_folders.dart';
|
||||
export 'src/sync/app_compatibility.dart';
|
||||
export 'src/sync/bloc/sync_status_bloc.dart';
|
||||
|
|
|
|||
|
|
@ -86,5 +86,149 @@
|
|||
"syncPending": "{count, plural, =1{1 Änderung wartet auf die Synchronisierung} other{{count} Änderungen warten auf die Synchronisierung}}",
|
||||
"syncTroubleLabel": "Synchronisierung benötigt Aufmerksamkeit",
|
||||
"syncMismatchBanner": "Diese App kann nicht mehr mit {server} synchronisieren. Aktualisiere sie, um weiter zu synchronisieren.",
|
||||
"syncMismatchMore": "Mehr"
|
||||
"syncMismatchMore": "Mehr",
|
||||
"startSignIn": "Anmelden",
|
||||
"startCreateAccount": "Konto erstellen",
|
||||
"startGitHubEnds": "Die Anmeldung mit GitHub endet am {date}.",
|
||||
"passwordAddress": "E-Mail-Adresse",
|
||||
"passwordPassword": "Passwort",
|
||||
"passwordNewPassword": "Neues Passwort",
|
||||
"passwordCurrent": "Aktuelles Passwort",
|
||||
"passwordRepeat": "Neues Passwort wiederholen",
|
||||
"passwordsDiffer": "Die beiden Passwörter stimmen nicht überein.",
|
||||
"passwordShow": "Passwort anzeigen",
|
||||
"passwordHide": "Passwort verbergen",
|
||||
"passwordContinue": "Weiter",
|
||||
"passwordBack": "Zurück",
|
||||
"passwordDone": "Fertig",
|
||||
"passwordCopy": "Kopieren",
|
||||
"passwordCopied": "Kopiert.",
|
||||
"passwordSave": "Als Datei speichern",
|
||||
"passwordQrCode": "QR-Code für deine Authenticator-App",
|
||||
"passwordMailCode": "Code aus der E-Mail",
|
||||
"passwordMailCodeSent": "Wir haben dir einen Code an {address} geschickt. Er gilt 15 Minuten.",
|
||||
"passwordResendCode": "Neuen Code senden",
|
||||
"passwordTotpCode": "Code aus deiner Authenticator-App",
|
||||
"passwordRecoveryCode": "Wiederherstellungscode",
|
||||
"passwordUseRecoveryCode": "Stattdessen einen Wiederherstellungscode verwenden",
|
||||
"passwordUseTotpCode": "Stattdessen deine Authenticator-App verwenden",
|
||||
"passwordUnlocking": "Dein Passwort wird auf diesem Gerät entsperrt …",
|
||||
"passwordMakingKeys": "Deine Schlüssel werden auf diesem Gerät erstellt …",
|
||||
"passwordSignInTitle": "Anmelden",
|
||||
"passwordSignInAction": "Anmelden",
|
||||
"passwordSignInIntro": "Mit der E-Mail-Adresse und dem Passwort deines Kontos auf {instance}.",
|
||||
"passwordSecondFactorIntro": "Gib den Code ein, den deine Authenticator-App für {instance} zeigt.",
|
||||
"passwordRecoveryCodeIntro": "Gib einen deiner Wiederherstellungscodes ein. Jeder meldet einmal an.",
|
||||
"passwordForgot": "Passwort vergessen?",
|
||||
"passwordOtherPassword": "Adresse oder Passwort ändern",
|
||||
"passwordLostAuthenticator": "Authenticator verloren?",
|
||||
"passwordLostAuthenticatorTitle": "Authenticator verloren",
|
||||
"passwordLostAuthenticatorIntro": "Mit einem Wiederherstellungscode meldest du dich wie gewohnt an. Ohne einen kannst du deine Authenticators zurücksetzen lassen: Das wirkt 72 Stunden später, und eine Anmeldung mit einem Code stoppt es bis dahin.",
|
||||
"passwordResetWithMail": "Mit einem Code per E-Mail zurücksetzen",
|
||||
"passwordResetWithMailHint": "Wir schicken einen Code an {address}.",
|
||||
"passwordResetWithPhrase": "Mit deiner Wiederherstellungsphrase zurücksetzen",
|
||||
"passwordResetWithPhraseHint": "Die 24 Wörter, die du dir notiert hast.",
|
||||
"passwordSignInWithoutCode": "Ohne Code anmelden",
|
||||
"passwordSignInWithoutCodeHint": "Sobald ein Zurücksetzen, um das du gebeten hast, wirksam ist.",
|
||||
"passwordResetAction": "Zurücksetzen",
|
||||
"passwordAuthenticatorResetAt": "Deine Authenticators werden am {date} zurückgesetzt. Wir haben dir eine E-Mail geschickt: Warst du es nicht, melde dich vorher mit einem Code an, um es zu stoppen. Danach meldest du dich nur mit deinem Passwort an und richtest einen neuen Authenticator ein.",
|
||||
"passwordUnfinishedIntro": "Dein Konto wurde nie fertig eingerichtet. Wir haben dir einen Code an {address} geschickt: Damit meldest du dich an und richtest einen Authenticator ein.",
|
||||
"passwordPhraseField": "Wiederherstellungsphrase",
|
||||
"passwordPhraseFieldHint": "Die 24 Wörter, durch Leerzeichen getrennt",
|
||||
"passwordPhraseFieldIntro": "Gib deine Wiederherstellungsphrase ein: Sie beweist, dass das Konto deines ist.",
|
||||
"passwordAuthenticatorTitle": "Authenticator einrichten",
|
||||
"passwordAuthenticatorIntro": "Jede Anmeldung braucht einen Code aus einer Authenticator-App wie Aegis, 2FAS oder Proton Authenticator. Scanne diesen Code damit oder tippe den Schlüssel ein.",
|
||||
"passwordAuthenticatorKey": "Schlüssel",
|
||||
"passwordAuthenticatorOpen": "In deiner Authenticator-App öffnen",
|
||||
"passwordAuthenticatorNoApp": "Keine App auf diesem Gerät öffnet ihn. Scanne den Code oder tippe den Schlüssel ein.",
|
||||
"passwordAuthenticatorCodeIntro": "Gib dann den Code ein, den sie zeigt.",
|
||||
"passwordExistingCode": "Code von einem bisherigen Authenticator",
|
||||
"passwordRecoveryCodesTitle": "Wiederherstellungscodes",
|
||||
"passwordRecoveryCodesIntro": "Jeder meldet dich einmal anstelle eines Codes aus deinem Authenticator an, falls du ihn verlierst. Bewahre sie auf, wo nur du sie findest: Sie werden nur jetzt angezeigt.",
|
||||
"passwordCodesKept": "Ich habe sie aufbewahrt",
|
||||
"passwordPhraseTitle": "Deine Wiederherstellungsphrase",
|
||||
"passwordPhraseIntro": "Diese 24 Wörter öffnen die Schlüssel deines Kontos, falls du dein Passwort vergisst: Ohne sie ist Verschlüsseltes verloren. Schreib sie der Reihe nach auf und bewahre sie auf, wo nur du sie findest. Sie werden nur jetzt angezeigt.",
|
||||
"passwordPhraseWrittenDown": "Ich habe sie aufgeschrieben",
|
||||
"passwordPhraseCheckTitle": "Wiederherstellungsphrase prüfen",
|
||||
"passwordPhraseCheckIntro": "Tippe die gefragten Wörter aus deiner Notiz ein.",
|
||||
"passwordPhraseWord": "Wort {number}",
|
||||
"passwordPhraseWrong": "Das sind nicht die Wörter. Sieh noch einmal in deine Notiz.",
|
||||
"passwordPhraseShowAgain": "Phrase noch einmal zeigen",
|
||||
"passwordCreateTitle": "Konto erstellen",
|
||||
"passwordCreateIntro": "Auf {instance}, mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App für den Code, den jede Anmeldung braucht.",
|
||||
"passwordSetUpTitle": "Passwort einrichten",
|
||||
"passwordSetUpIntro": "Melde dich mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App an, auf diesem Gerät und anderen. Alles, was du hast, bleibt.",
|
||||
"passwordChooseIntro": "Wähle ein Passwort. Es verlässt dieses Gerät nie: Die Instanz kann es nicht lesen und nicht zurücksetzen, ohne dass verloren geht, was es schützt.",
|
||||
"passwordFinishResetTitle": "Zurücksetzen abschließen",
|
||||
"passwordFinishResetIntro": "72 Stunden nachdem du um das Zurücksetzen deines Kontos gebeten hast, schließt du es hier mit einem neuen Code per E-Mail ab.",
|
||||
"passwordFinishResetHavePhrase": "Ich habe meine Wiederherstellungsphrase",
|
||||
"passwordFinishResetKeepsKeys": "Dein Konto behält seine Schlüssel: Nichts Verschlüsseltes geht verloren.",
|
||||
"passwordFinishResetNewKeys": "Ohne deine Wiederherstellungsphrase bekommt dein Konto neue Schlüssel: Was mit den alten verschlüsselt war, ist verloren.",
|
||||
"passwordRecoveryTitle": "Zurück in dein Konto",
|
||||
"passwordRecoveryWithPhrase": "Ich habe meine Wiederherstellungsphrase",
|
||||
"passwordRecoveryWithPhraseHint": "Setze sofort ein neues Passwort. Alles bleibt.",
|
||||
"passwordRecoveryWithPhraseIntro": "Deine Wiederherstellungsphrase öffnet die Schlüssel deines Kontos, die dann dein neues Passwort schützt. Ein Code aus deinem Authenticator oder ein Wiederherstellungscode bestätigt es.",
|
||||
"passwordRecoveryNoCodeHint": "Auch deinen Authenticator verloren? Setze dein Konto 72 Stunden später mit deiner Wiederherstellungsphrase zurück: Sie behält alles.",
|
||||
"passwordRecoveryWithoutPhrase": "Mein Konto zurücksetzen",
|
||||
"passwordRecoveryWithoutPhraseHint": "Wirkt 72 Stunden später. Ohne deine Wiederherstellungsphrase ist Verschlüsseltes verloren.",
|
||||
"passwordRecoveryStartIntro": "Wir schicken einen Code an deine Adresse. Dein Konto kann dann 72 Stunden später zurückgesetzt werden; eine Anmeldung mit deinem Passwort stoppt es bis dahin.",
|
||||
"passwordRecoveryFinish": "Zurücksetzen abschließen",
|
||||
"passwordRecoveryFinishHint": "72 Stunden nachdem du darum gebeten hast.",
|
||||
"passwordResetStarted": "Dein Konto kann ab dem {date} zurückgesetzt werden. Wir haben dir eine E-Mail geschickt: Warst du es nicht, melde dich vorher mit deinem Passwort an, um es zu stoppen.",
|
||||
"passwordSectionTitle": "Passwort und Authenticator",
|
||||
"passwordSectionFailed": "Wie dein Konto sich anmeldet, konnte nicht gelesen werden.",
|
||||
"passwordSectionGitHubEnds": "Die Anmeldung mit GitHub endet am {date}. Richte bis dahin ein Passwort ein, um dein Konto zu behalten.",
|
||||
"passwordSectionSetUp": "Passwort einrichten",
|
||||
"passwordSectionSetUpHint": "Melde dich mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App an.",
|
||||
"passwordSectionUnfinished": "Fertig einrichten: Authenticator hinzufügen",
|
||||
"passwordSectionUnfinishedHint": "Dein Passwort meldet dich an, sobald ein Authenticator eingerichtet ist.",
|
||||
"passwordSectionChange": "Passwort ändern",
|
||||
"passwordSectionKeysMissing": "Melde dich auf diesem Gerät mit deinem Passwort an, um das zu ändern.",
|
||||
"passwordSectionNotConfirmed": "Noch nicht eingerichtet",
|
||||
"passwordSectionRemove": "{name} entfernen?",
|
||||
"passwordSectionRemoveIntro": "Gib zur Bestätigung einen Code aus einem deiner Authenticators ein.",
|
||||
"passwordSectionRemoveAction": "Entfernen",
|
||||
"passwordSectionAddAuthenticator": "Authenticator hinzufügen",
|
||||
"passwordSectionRecoveryCodes": "Neue Wiederherstellungscodes",
|
||||
"passwordSectionRecoveryCodesHint": "Die alten gelten dann nicht mehr.",
|
||||
"passwordSectionNewPhrase": "Neue Wiederherstellungsphrase",
|
||||
"passwordSectionPhraseHint": "Deine alte Phrase gilt dann nicht mehr.",
|
||||
"passwordChangeTitle": "Passwort ändern",
|
||||
"passwordChangeIntro": "Deine anderen Geräte werden abgemeldet. Deine Schlüssel bleiben: Nichts geht verloren.",
|
||||
"passwordChangeAction": "Passwort ändern",
|
||||
"passwordChanged": "Passwort geändert. Deine anderen Geräte sind abgemeldet.",
|
||||
"passwordNewCodesIntro": "Gib einen Code aus deiner Authenticator-App ein. Deine alten Wiederherstellungscodes gelten dann nicht mehr.",
|
||||
"passwordNewPhraseIntro": "Deine neue Wiederherstellungsphrase. Schreib die 24 Wörter der Reihe nach auf: Sobald du sie bestätigst, öffnet deine alte Phrase nichts mehr.",
|
||||
"passwordPhraseKept": "Neue Wiederherstellungsphrase gesetzt. Die alte gilt nicht mehr.",
|
||||
"passwordWrongCredentials": "Die Adresse, das Passwort oder der Code stimmt nicht.",
|
||||
"passwordTooManyAttempts": "Zu viele Versuche. Versuch es in einer Stunde wieder.",
|
||||
"passwordSetupIncomplete": "Dein Konto wurde nie fertig eingerichtet: Richte einen Authenticator ein.",
|
||||
"passwordNotDue": "Das wirkt am {date}.",
|
||||
"passwordNotDueYet": "Das wirkt noch nicht.",
|
||||
"passwordNoReset": "Für diese Adresse wartet kein Zurücksetzen.",
|
||||
"passwordExists": "Das Konto dieser Adresse hat schon ein Passwort. Melde dich damit an oder komm zurück in dein Konto, falls du es vergessen hast.",
|
||||
"passwordAddressTaken": "Diese Adresse gehört zu einem anderen Konto.",
|
||||
"passwordNoAddress": "Dein Konto hat noch keine E-Mail-Adresse.",
|
||||
"passwordNoPassword": "Dieses Konto hat kein Passwort.",
|
||||
"passwordWrongCode": "Dieser Code stimmt nicht. Gib den ein, den deine App gerade zeigt.",
|
||||
"passwordWrongExistingCode": "Der Code von deinem anderen Authenticator stimmt nicht.",
|
||||
"passwordLastAuthenticator": "Dein letzter Authenticator kann nicht weg: Jede Anmeldung braucht einen.",
|
||||
"passwordTooManyAuthenticators": "Dein Konto hat so viele Authenticators, wie es darf. Entferne zuerst einen.",
|
||||
"passwordUnknownAuthenticator": "Dieser Authenticator ist weg.",
|
||||
"passwordInvalidSetup": "Die Instanz hat nicht angenommen, was dieses Gerät erstellt hat. Aktualisiere die App oder versuch es noch einmal.",
|
||||
"passwordChallengeExpired": "Das hat zu lange gedauert. Versuch es noch einmal.",
|
||||
"passwordUnavailable": "Diese Instanz bietet keine Anmeldung mit Passwort.",
|
||||
"passwordNotAnAddress": "Das ist keine E-Mail-Adresse.",
|
||||
"passwordTooManyCodes": "Zu viele Codes angefordert. Versuch es in einer Stunde wieder.",
|
||||
"passwordWrongMailCode": "Dieser Code stimmt nicht.",
|
||||
"passwordTooManyMailTries": "Zu viele falsche Codes. Fordere einen neuen an.",
|
||||
"passwordMailCodeExpired": "Dieser Code gilt nicht mehr. Fordere einen neuen an.",
|
||||
"passwordMailFailed": "Die Instanz konnte die E-Mail nicht senden. Versuch es später noch einmal.",
|
||||
"passwordOtherAddressLinked": "Dein Konto hat eine andere E-Mail-Adresse.",
|
||||
"passwordNotAPhrase": "Das ist keine Wiederherstellungsphrase: Prüf jedes Wort und ihre Anzahl.",
|
||||
"passwordPhraseDoesNotFit": "Die Adresse oder die Wiederherstellungsphrase stimmt nicht.",
|
||||
"passwordUnusable": "Dieses Passwort geht nicht: Es ist leer oder enthält Zeichen, die kein Text sind.",
|
||||
"passwordInstanceUnproven": "Die Instanz konnte nicht beweisen, dass sie dein Passwort kennt, daher vertraut dieses Gerät ihr nicht. Versuch es noch einmal; passiert es wieder, ist die Instanz nicht die, die sie zu sein behauptet.",
|
||||
"passwordKeysMissing": "Die Schlüssel deines Kontos sind nicht auf diesem Gerät. Melde dich hier ab und mit deinem Passwort wieder an.",
|
||||
"passwordFailed": "Die Instanz war nicht erreichbar. Prüf die Verbindung und versuch es noch einmal."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,5 +242,240 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"syncMismatchMore": "More"
|
||||
"syncMismatchMore": "More",
|
||||
"startSignIn": "Sign in",
|
||||
"startCreateAccount": "Create an account",
|
||||
"startGitHubEnds": "Sign-in with GitHub ends on {date}.",
|
||||
"@startGitHubEnds": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordAddress": "Email address",
|
||||
"passwordPassword": "Password",
|
||||
"passwordNewPassword": "New password",
|
||||
"passwordCurrent": "Current password",
|
||||
"passwordRepeat": "New password again",
|
||||
"passwordsDiffer": "The two passwords differ.",
|
||||
"passwordShow": "Show password",
|
||||
"passwordHide": "Hide password",
|
||||
"passwordContinue": "Continue",
|
||||
"passwordBack": "Back",
|
||||
"passwordDone": "Done",
|
||||
"passwordCopy": "Copy",
|
||||
"passwordCopied": "Copied.",
|
||||
"passwordSave": "Save as a file",
|
||||
"passwordQrCode": "QR code for your authenticator app",
|
||||
"passwordMailCode": "Code from the email",
|
||||
"passwordMailCodeSent": "We emailed a code to {address}. It's valid for 15 minutes.",
|
||||
"@passwordMailCodeSent": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordResendCode": "Send a new code",
|
||||
"passwordTotpCode": "Code from your authenticator app",
|
||||
"passwordRecoveryCode": "Recovery code",
|
||||
"passwordUseRecoveryCode": "Use a recovery code instead",
|
||||
"passwordUseTotpCode": "Use your authenticator app instead",
|
||||
"passwordUnlocking": "Unlocking your password on this device…",
|
||||
"passwordMakingKeys": "Making your keys on this device…",
|
||||
"passwordSignInTitle": "Sign in",
|
||||
"passwordSignInAction": "Sign in",
|
||||
"passwordSignInIntro": "With the email address and password of your account on {instance}.",
|
||||
"@passwordSignInIntro": {
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordSecondFactorIntro": "Enter the code your authenticator app shows for {instance}.",
|
||||
"@passwordSecondFactorIntro": {
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordRecoveryCodeIntro": "Enter one of the recovery codes you kept. Each signs in once.",
|
||||
"passwordForgot": "Forgot your password?",
|
||||
"passwordOtherPassword": "Change the address or password",
|
||||
"passwordLostAuthenticator": "Lost your authenticator?",
|
||||
"passwordLostAuthenticatorTitle": "Lost authenticator",
|
||||
"passwordLostAuthenticatorIntro": "With a recovery code you sign in as usual. Without one, ask to reset your authenticators: it takes effect 72 hours later, and signing in with a code before then stops it.",
|
||||
"passwordResetWithMail": "Reset with a code by email",
|
||||
"passwordResetWithMailHint": "We email a code to {address}.",
|
||||
"@passwordResetWithMailHint": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordResetWithPhrase": "Reset with your recovery phrase",
|
||||
"passwordResetWithPhraseHint": "The 24 words you wrote down.",
|
||||
"passwordSignInWithoutCode": "Sign in without a code",
|
||||
"passwordSignInWithoutCodeHint": "Once a reset you asked for has taken effect.",
|
||||
"passwordResetAction": "Reset",
|
||||
"passwordAuthenticatorResetAt": "Your authenticators are reset on {date}. We emailed you: if it wasn't you, sign in with a code before then to stop it. After that, sign in with your password alone and set up a new authenticator.",
|
||||
"@passwordAuthenticatorResetAt": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordUnfinishedIntro": "Your account was never finished. We emailed a code to {address}: with it, you sign in and set up an authenticator.",
|
||||
"@passwordUnfinishedIntro": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordPhraseField": "Recovery phrase",
|
||||
"passwordPhraseFieldHint": "The 24 words, with spaces between them",
|
||||
"passwordPhraseFieldIntro": "Enter your recovery phrase: it proves the account is yours.",
|
||||
"passwordAuthenticatorTitle": "Set up an authenticator",
|
||||
"passwordAuthenticatorIntro": "Every sign-in takes a code from an authenticator app, such as Aegis, 2FAS or Proton Authenticator. Scan this code with it, or type in the key.",
|
||||
"passwordAuthenticatorKey": "Key",
|
||||
"passwordAuthenticatorOpen": "Open in your authenticator app",
|
||||
"passwordAuthenticatorNoApp": "No app on this device opens it. Scan the code or type in the key.",
|
||||
"passwordAuthenticatorCodeIntro": "Then enter the code it shows.",
|
||||
"passwordExistingCode": "Code from an authenticator you added before",
|
||||
"passwordRecoveryCodesTitle": "Recovery codes",
|
||||
"passwordRecoveryCodesIntro": "Each signs in once in place of a code from your authenticator, should you lose it. Keep them where only you find them: they're shown only now.",
|
||||
"passwordCodesKept": "I've kept them",
|
||||
"passwordPhraseTitle": "Your recovery phrase",
|
||||
"passwordPhraseIntro": "These 24 words open your account's keys should you forget your password: without them, what's encrypted is lost. Write them down in order, and keep them where only you find them. They're shown only now.",
|
||||
"passwordPhraseWrittenDown": "I've written it down",
|
||||
"passwordPhraseCheckTitle": "Check your recovery phrase",
|
||||
"passwordPhraseCheckIntro": "Type the words asked for, from what you wrote down.",
|
||||
"passwordPhraseWord": "Word {number}",
|
||||
"@passwordPhraseWord": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordPhraseWrong": "These aren't the words. Look again at what you wrote down.",
|
||||
"passwordPhraseShowAgain": "Show the phrase again",
|
||||
"passwordCreateTitle": "Create an account",
|
||||
"passwordCreateIntro": "On {instance}, with your email address, a password, and an authenticator app for the code every sign-in takes.",
|
||||
"@passwordCreateIntro": {
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordSetUpTitle": "Set up a password",
|
||||
"passwordSetUpIntro": "Sign in with your email address, a password and an authenticator app, on this device and others. Everything you have stays.",
|
||||
"passwordChooseIntro": "Choose a password. It never leaves this device: the instance can't read it, and can't reset it without what it protects being lost.",
|
||||
"passwordFinishResetTitle": "Finish the reset",
|
||||
"passwordFinishResetIntro": "72 hours after you asked to reset your account, finish it here with a new code by email.",
|
||||
"passwordFinishResetHavePhrase": "I have my recovery phrase",
|
||||
"passwordFinishResetKeepsKeys": "Your account keeps its keys: nothing encrypted is lost.",
|
||||
"passwordFinishResetNewKeys": "Without your recovery phrase, your account gets new keys: what was encrypted with the old ones is lost.",
|
||||
"passwordRecoveryTitle": "Get back into your account",
|
||||
"passwordRecoveryWithPhrase": "I have my recovery phrase",
|
||||
"passwordRecoveryWithPhraseHint": "Set a new password at once. Everything stays.",
|
||||
"passwordRecoveryWithPhraseIntro": "Your recovery phrase opens your account's keys, which your new password then protects. A code from your authenticator, or a recovery code, confirms it.",
|
||||
"passwordRecoveryNoCodeHint": "Lost your authenticator as well? Reset your account 72 hours later with your recovery phrase: it keeps everything.",
|
||||
"passwordRecoveryWithoutPhrase": "Reset my account",
|
||||
"passwordRecoveryWithoutPhraseHint": "Takes effect 72 hours later. Without your recovery phrase, what's encrypted is lost.",
|
||||
"passwordRecoveryStartIntro": "We email a code to your address. Your account can then be reset 72 hours later; signing in with your password before then stops it.",
|
||||
"passwordRecoveryFinish": "Finish a reset",
|
||||
"passwordRecoveryFinishHint": "72 hours after you asked for it.",
|
||||
"passwordResetStarted": "Your account can be reset from {date}. We emailed you: if it wasn't you, sign in with your password before then to stop it.",
|
||||
"@passwordResetStarted": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordSectionTitle": "Password and authenticator",
|
||||
"passwordSectionFailed": "Couldn't read how your account signs in.",
|
||||
"passwordSectionGitHubEnds": "Sign-in with GitHub ends on {date}. Set up a password before then to keep your account.",
|
||||
"@passwordSectionGitHubEnds": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordSectionSetUp": "Set up a password",
|
||||
"passwordSectionSetUpHint": "Sign in with your email address, a password and an authenticator app.",
|
||||
"passwordSectionUnfinished": "Finish setting up: add an authenticator",
|
||||
"passwordSectionUnfinishedHint": "Your password signs in once an authenticator is set up.",
|
||||
"passwordSectionChange": "Change password",
|
||||
"passwordSectionKeysMissing": "Sign in on this device with your password to change it.",
|
||||
"passwordSectionNotConfirmed": "Not set up yet",
|
||||
"passwordSectionRemove": "Remove {name}?",
|
||||
"@passwordSectionRemove": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordSectionRemoveIntro": "Enter a code from one of your authenticators to confirm.",
|
||||
"passwordSectionRemoveAction": "Remove",
|
||||
"passwordSectionAddAuthenticator": "Add an authenticator",
|
||||
"passwordSectionRecoveryCodes": "New recovery codes",
|
||||
"passwordSectionRecoveryCodesHint": "The old ones stop working.",
|
||||
"passwordSectionNewPhrase": "New recovery phrase",
|
||||
"passwordSectionPhraseHint": "Your old phrase stops working.",
|
||||
"passwordChangeTitle": "Change password",
|
||||
"passwordChangeIntro": "Your other devices are signed out. Your keys stay: nothing is lost.",
|
||||
"passwordChangeAction": "Change password",
|
||||
"passwordChanged": "Password changed. Your other devices are signed out.",
|
||||
"passwordNewCodesIntro": "Enter a code from your authenticator app. Your old recovery codes stop working.",
|
||||
"passwordNewPhraseIntro": "Your new recovery phrase. Write the 24 words down in order: once you confirm it, your old phrase opens nothing any more.",
|
||||
"passwordPhraseKept": "New recovery phrase set. The old one no longer works.",
|
||||
"passwordWrongCredentials": "The address, the password or the code isn't right.",
|
||||
"passwordTooManyAttempts": "Too many attempts. Try again in an hour.",
|
||||
"passwordSetupIncomplete": "Your account was never finished: set up an authenticator.",
|
||||
"passwordNotDue": "This takes effect on {date}.",
|
||||
"@passwordNotDue": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordNotDueYet": "This hasn't taken effect yet.",
|
||||
"passwordNoReset": "No reset waits for this address.",
|
||||
"passwordExists": "This address's account has a password already. Sign in with it, or get back in if you've forgotten it.",
|
||||
"passwordAddressTaken": "This address belongs to another account.",
|
||||
"passwordNoAddress": "Your account has no email address yet.",
|
||||
"passwordNoPassword": "This account has no password.",
|
||||
"passwordWrongCode": "This code isn't right. Enter the one your app shows now.",
|
||||
"passwordWrongExistingCode": "The code from your other authenticator isn't right.",
|
||||
"passwordLastAuthenticator": "Your last authenticator can't go: every sign-in needs one.",
|
||||
"passwordTooManyAuthenticators": "Your account has as many authenticators as it may. Remove one first.",
|
||||
"passwordUnknownAuthenticator": "This authenticator is gone.",
|
||||
"passwordInvalidSetup": "The instance didn't accept what this device made. Update the app, or try again.",
|
||||
"passwordChallengeExpired": "That took too long. Try again.",
|
||||
"passwordUnavailable": "This instance offers no password sign-in.",
|
||||
"passwordNotAnAddress": "This isn't an email address.",
|
||||
"passwordTooManyCodes": "Too many codes asked for. Try again in an hour.",
|
||||
"passwordWrongMailCode": "This code isn't right.",
|
||||
"passwordTooManyMailTries": "Too many wrong codes. Ask for a new one.",
|
||||
"passwordMailCodeExpired": "This code is no longer valid. Ask for a new one.",
|
||||
"passwordMailFailed": "The instance couldn't send the email. Try again later.",
|
||||
"passwordOtherAddressLinked": "Your account has another email address.",
|
||||
"passwordNotAPhrase": "This isn't a recovery phrase: check every word and their number.",
|
||||
"passwordPhraseDoesNotFit": "The address or the recovery phrase isn't right.",
|
||||
"passwordUnusable": "This password can't be used: it's empty, or holds characters that aren't text.",
|
||||
"passwordInstanceUnproven": "The instance couldn't prove it knows your password, so this device trusts nothing it says. Try again; if it happens again, the instance isn't the one it claims to be.",
|
||||
"passwordKeysMissing": "Your account's keys aren't on this device. Sign out and in again with your password here.",
|
||||
"passwordFailed": "Couldn't reach the instance. Check the connection and try again."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -624,6 +624,870 @@ abstract class HalkyonAppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'More'**
|
||||
String get syncMismatchMore;
|
||||
|
||||
/// No description provided for @startSignIn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in'**
|
||||
String get startSignIn;
|
||||
|
||||
/// No description provided for @startCreateAccount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create an account'**
|
||||
String get startCreateAccount;
|
||||
|
||||
/// No description provided for @startGitHubEnds.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign-in with GitHub ends on {date}.'**
|
||||
String startGitHubEnds(String date);
|
||||
|
||||
/// No description provided for @passwordAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Email address'**
|
||||
String get passwordAddress;
|
||||
|
||||
/// No description provided for @passwordPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password'**
|
||||
String get passwordPassword;
|
||||
|
||||
/// No description provided for @passwordNewPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New password'**
|
||||
String get passwordNewPassword;
|
||||
|
||||
/// No description provided for @passwordCurrent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Current password'**
|
||||
String get passwordCurrent;
|
||||
|
||||
/// No description provided for @passwordRepeat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New password again'**
|
||||
String get passwordRepeat;
|
||||
|
||||
/// No description provided for @passwordsDiffer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The two passwords differ.'**
|
||||
String get passwordsDiffer;
|
||||
|
||||
/// No description provided for @passwordShow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show password'**
|
||||
String get passwordShow;
|
||||
|
||||
/// No description provided for @passwordHide.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hide password'**
|
||||
String get passwordHide;
|
||||
|
||||
/// No description provided for @passwordContinue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Continue'**
|
||||
String get passwordContinue;
|
||||
|
||||
/// No description provided for @passwordBack.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Back'**
|
||||
String get passwordBack;
|
||||
|
||||
/// No description provided for @passwordDone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Done'**
|
||||
String get passwordDone;
|
||||
|
||||
/// No description provided for @passwordCopy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy'**
|
||||
String get passwordCopy;
|
||||
|
||||
/// No description provided for @passwordCopied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copied.'**
|
||||
String get passwordCopied;
|
||||
|
||||
/// No description provided for @passwordSave.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save as a file'**
|
||||
String get passwordSave;
|
||||
|
||||
/// No description provided for @passwordQrCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'QR code for your authenticator app'**
|
||||
String get passwordQrCode;
|
||||
|
||||
/// No description provided for @passwordMailCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Code from the email'**
|
||||
String get passwordMailCode;
|
||||
|
||||
/// No description provided for @passwordMailCodeSent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'We emailed a code to {address}. It\'s valid for 15 minutes.'**
|
||||
String passwordMailCodeSent(String address);
|
||||
|
||||
/// No description provided for @passwordResendCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send a new code'**
|
||||
String get passwordResendCode;
|
||||
|
||||
/// No description provided for @passwordTotpCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Code from your authenticator app'**
|
||||
String get passwordTotpCode;
|
||||
|
||||
/// No description provided for @passwordRecoveryCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recovery code'**
|
||||
String get passwordRecoveryCode;
|
||||
|
||||
/// No description provided for @passwordUseRecoveryCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use a recovery code instead'**
|
||||
String get passwordUseRecoveryCode;
|
||||
|
||||
/// No description provided for @passwordUseTotpCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use your authenticator app instead'**
|
||||
String get passwordUseTotpCode;
|
||||
|
||||
/// No description provided for @passwordUnlocking.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unlocking your password on this device…'**
|
||||
String get passwordUnlocking;
|
||||
|
||||
/// No description provided for @passwordMakingKeys.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Making your keys on this device…'**
|
||||
String get passwordMakingKeys;
|
||||
|
||||
/// No description provided for @passwordSignInTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in'**
|
||||
String get passwordSignInTitle;
|
||||
|
||||
/// No description provided for @passwordSignInAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in'**
|
||||
String get passwordSignInAction;
|
||||
|
||||
/// No description provided for @passwordSignInIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'With the email address and password of your account on {instance}.'**
|
||||
String passwordSignInIntro(String instance);
|
||||
|
||||
/// No description provided for @passwordSecondFactorIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter the code your authenticator app shows for {instance}.'**
|
||||
String passwordSecondFactorIntro(String instance);
|
||||
|
||||
/// No description provided for @passwordRecoveryCodeIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter one of the recovery codes you kept. Each signs in once.'**
|
||||
String get passwordRecoveryCodeIntro;
|
||||
|
||||
/// No description provided for @passwordForgot.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Forgot your password?'**
|
||||
String get passwordForgot;
|
||||
|
||||
/// No description provided for @passwordOtherPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change the address or password'**
|
||||
String get passwordOtherPassword;
|
||||
|
||||
/// No description provided for @passwordLostAuthenticator.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Lost your authenticator?'**
|
||||
String get passwordLostAuthenticator;
|
||||
|
||||
/// No description provided for @passwordLostAuthenticatorTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Lost authenticator'**
|
||||
String get passwordLostAuthenticatorTitle;
|
||||
|
||||
/// No description provided for @passwordLostAuthenticatorIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'With a recovery code you sign in as usual. Without one, ask to reset your authenticators: it takes effect 72 hours later, and signing in with a code before then stops it.'**
|
||||
String get passwordLostAuthenticatorIntro;
|
||||
|
||||
/// No description provided for @passwordResetWithMail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset with a code by email'**
|
||||
String get passwordResetWithMail;
|
||||
|
||||
/// No description provided for @passwordResetWithMailHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'We email a code to {address}.'**
|
||||
String passwordResetWithMailHint(String address);
|
||||
|
||||
/// No description provided for @passwordResetWithPhrase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset with your recovery phrase'**
|
||||
String get passwordResetWithPhrase;
|
||||
|
||||
/// No description provided for @passwordResetWithPhraseHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The 24 words you wrote down.'**
|
||||
String get passwordResetWithPhraseHint;
|
||||
|
||||
/// No description provided for @passwordSignInWithoutCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in without a code'**
|
||||
String get passwordSignInWithoutCode;
|
||||
|
||||
/// No description provided for @passwordSignInWithoutCodeHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Once a reset you asked for has taken effect.'**
|
||||
String get passwordSignInWithoutCodeHint;
|
||||
|
||||
/// No description provided for @passwordResetAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset'**
|
||||
String get passwordResetAction;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorResetAt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your authenticators are reset on {date}. We emailed you: if it wasn\'t you, sign in with a code before then to stop it. After that, sign in with your password alone and set up a new authenticator.'**
|
||||
String passwordAuthenticatorResetAt(String date);
|
||||
|
||||
/// No description provided for @passwordUnfinishedIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account was never finished. We emailed a code to {address}: with it, you sign in and set up an authenticator.'**
|
||||
String passwordUnfinishedIntro(String address);
|
||||
|
||||
/// No description provided for @passwordPhraseField.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recovery phrase'**
|
||||
String get passwordPhraseField;
|
||||
|
||||
/// No description provided for @passwordPhraseFieldHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The 24 words, with spaces between them'**
|
||||
String get passwordPhraseFieldHint;
|
||||
|
||||
/// No description provided for @passwordPhraseFieldIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter your recovery phrase: it proves the account is yours.'**
|
||||
String get passwordPhraseFieldIntro;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set up an authenticator'**
|
||||
String get passwordAuthenticatorTitle;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Every sign-in takes a code from an authenticator app, such as Aegis, 2FAS or Proton Authenticator. Scan this code with it, or type in the key.'**
|
||||
String get passwordAuthenticatorIntro;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Key'**
|
||||
String get passwordAuthenticatorKey;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorOpen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open in your authenticator app'**
|
||||
String get passwordAuthenticatorOpen;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorNoApp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No app on this device opens it. Scan the code or type in the key.'**
|
||||
String get passwordAuthenticatorNoApp;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorCodeIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Then enter the code it shows.'**
|
||||
String get passwordAuthenticatorCodeIntro;
|
||||
|
||||
/// No description provided for @passwordExistingCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Code from an authenticator you added before'**
|
||||
String get passwordExistingCode;
|
||||
|
||||
/// No description provided for @passwordRecoveryCodesTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recovery codes'**
|
||||
String get passwordRecoveryCodesTitle;
|
||||
|
||||
/// No description provided for @passwordRecoveryCodesIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Each signs in once in place of a code from your authenticator, should you lose it. Keep them where only you find them: they\'re shown only now.'**
|
||||
String get passwordRecoveryCodesIntro;
|
||||
|
||||
/// No description provided for @passwordCodesKept.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'I\'ve kept them'**
|
||||
String get passwordCodesKept;
|
||||
|
||||
/// No description provided for @passwordPhraseTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your recovery phrase'**
|
||||
String get passwordPhraseTitle;
|
||||
|
||||
/// No description provided for @passwordPhraseIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'These 24 words open your account\'s keys should you forget your password: without them, what\'s encrypted is lost. Write them down in order, and keep them where only you find them. They\'re shown only now.'**
|
||||
String get passwordPhraseIntro;
|
||||
|
||||
/// No description provided for @passwordPhraseWrittenDown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'I\'ve written it down'**
|
||||
String get passwordPhraseWrittenDown;
|
||||
|
||||
/// No description provided for @passwordPhraseCheckTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Check your recovery phrase'**
|
||||
String get passwordPhraseCheckTitle;
|
||||
|
||||
/// No description provided for @passwordPhraseCheckIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Type the words asked for, from what you wrote down.'**
|
||||
String get passwordPhraseCheckIntro;
|
||||
|
||||
/// No description provided for @passwordPhraseWord.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Word {number}'**
|
||||
String passwordPhraseWord(int number);
|
||||
|
||||
/// No description provided for @passwordPhraseWrong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'These aren\'t the words. Look again at what you wrote down.'**
|
||||
String get passwordPhraseWrong;
|
||||
|
||||
/// No description provided for @passwordPhraseShowAgain.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show the phrase again'**
|
||||
String get passwordPhraseShowAgain;
|
||||
|
||||
/// No description provided for @passwordCreateTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create an account'**
|
||||
String get passwordCreateTitle;
|
||||
|
||||
/// No description provided for @passwordCreateIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'On {instance}, with your email address, a password, and an authenticator app for the code every sign-in takes.'**
|
||||
String passwordCreateIntro(String instance);
|
||||
|
||||
/// No description provided for @passwordSetUpTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set up a password'**
|
||||
String get passwordSetUpTitle;
|
||||
|
||||
/// No description provided for @passwordSetUpIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in with your email address, a password and an authenticator app, on this device and others. Everything you have stays.'**
|
||||
String get passwordSetUpIntro;
|
||||
|
||||
/// No description provided for @passwordChooseIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose a password. It never leaves this device: the instance can\'t read it, and can\'t reset it without what it protects being lost.'**
|
||||
String get passwordChooseIntro;
|
||||
|
||||
/// No description provided for @passwordFinishResetTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finish the reset'**
|
||||
String get passwordFinishResetTitle;
|
||||
|
||||
/// No description provided for @passwordFinishResetIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'72 hours after you asked to reset your account, finish it here with a new code by email.'**
|
||||
String get passwordFinishResetIntro;
|
||||
|
||||
/// No description provided for @passwordFinishResetHavePhrase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'I have my recovery phrase'**
|
||||
String get passwordFinishResetHavePhrase;
|
||||
|
||||
/// No description provided for @passwordFinishResetKeepsKeys.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account keeps its keys: nothing encrypted is lost.'**
|
||||
String get passwordFinishResetKeepsKeys;
|
||||
|
||||
/// No description provided for @passwordFinishResetNewKeys.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Without your recovery phrase, your account gets new keys: what was encrypted with the old ones is lost.'**
|
||||
String get passwordFinishResetNewKeys;
|
||||
|
||||
/// No description provided for @passwordRecoveryTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Get back into your account'**
|
||||
String get passwordRecoveryTitle;
|
||||
|
||||
/// No description provided for @passwordRecoveryWithPhrase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'I have my recovery phrase'**
|
||||
String get passwordRecoveryWithPhrase;
|
||||
|
||||
/// No description provided for @passwordRecoveryWithPhraseHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set a new password at once. Everything stays.'**
|
||||
String get passwordRecoveryWithPhraseHint;
|
||||
|
||||
/// No description provided for @passwordRecoveryWithPhraseIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your recovery phrase opens your account\'s keys, which your new password then protects. A code from your authenticator, or a recovery code, confirms it.'**
|
||||
String get passwordRecoveryWithPhraseIntro;
|
||||
|
||||
/// No description provided for @passwordRecoveryNoCodeHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Lost your authenticator as well? Reset your account 72 hours later with your recovery phrase: it keeps everything.'**
|
||||
String get passwordRecoveryNoCodeHint;
|
||||
|
||||
/// No description provided for @passwordRecoveryWithoutPhrase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset my account'**
|
||||
String get passwordRecoveryWithoutPhrase;
|
||||
|
||||
/// No description provided for @passwordRecoveryWithoutPhraseHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Takes effect 72 hours later. Without your recovery phrase, what\'s encrypted is lost.'**
|
||||
String get passwordRecoveryWithoutPhraseHint;
|
||||
|
||||
/// No description provided for @passwordRecoveryStartIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'We email a code to your address. Your account can then be reset 72 hours later; signing in with your password before then stops it.'**
|
||||
String get passwordRecoveryStartIntro;
|
||||
|
||||
/// No description provided for @passwordRecoveryFinish.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finish a reset'**
|
||||
String get passwordRecoveryFinish;
|
||||
|
||||
/// No description provided for @passwordRecoveryFinishHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'72 hours after you asked for it.'**
|
||||
String get passwordRecoveryFinishHint;
|
||||
|
||||
/// No description provided for @passwordResetStarted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account can be reset from {date}. We emailed you: if it wasn\'t you, sign in with your password before then to stop it.'**
|
||||
String passwordResetStarted(String date);
|
||||
|
||||
/// No description provided for @passwordSectionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password and authenticator'**
|
||||
String get passwordSectionTitle;
|
||||
|
||||
/// No description provided for @passwordSectionFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t read how your account signs in.'**
|
||||
String get passwordSectionFailed;
|
||||
|
||||
/// No description provided for @passwordSectionGitHubEnds.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign-in with GitHub ends on {date}. Set up a password before then to keep your account.'**
|
||||
String passwordSectionGitHubEnds(String date);
|
||||
|
||||
/// No description provided for @passwordSectionSetUp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set up a password'**
|
||||
String get passwordSectionSetUp;
|
||||
|
||||
/// No description provided for @passwordSectionSetUpHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in with your email address, a password and an authenticator app.'**
|
||||
String get passwordSectionSetUpHint;
|
||||
|
||||
/// No description provided for @passwordSectionUnfinished.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finish setting up: add an authenticator'**
|
||||
String get passwordSectionUnfinished;
|
||||
|
||||
/// No description provided for @passwordSectionUnfinishedHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your password signs in once an authenticator is set up.'**
|
||||
String get passwordSectionUnfinishedHint;
|
||||
|
||||
/// No description provided for @passwordSectionChange.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change password'**
|
||||
String get passwordSectionChange;
|
||||
|
||||
/// No description provided for @passwordSectionKeysMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in on this device with your password to change it.'**
|
||||
String get passwordSectionKeysMissing;
|
||||
|
||||
/// No description provided for @passwordSectionNotConfirmed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not set up yet'**
|
||||
String get passwordSectionNotConfirmed;
|
||||
|
||||
/// No description provided for @passwordSectionRemove.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove {name}?'**
|
||||
String passwordSectionRemove(String name);
|
||||
|
||||
/// No description provided for @passwordSectionRemoveIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter a code from one of your authenticators to confirm.'**
|
||||
String get passwordSectionRemoveIntro;
|
||||
|
||||
/// No description provided for @passwordSectionRemoveAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove'**
|
||||
String get passwordSectionRemoveAction;
|
||||
|
||||
/// No description provided for @passwordSectionAddAuthenticator.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add an authenticator'**
|
||||
String get passwordSectionAddAuthenticator;
|
||||
|
||||
/// No description provided for @passwordSectionRecoveryCodes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New recovery codes'**
|
||||
String get passwordSectionRecoveryCodes;
|
||||
|
||||
/// No description provided for @passwordSectionRecoveryCodesHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The old ones stop working.'**
|
||||
String get passwordSectionRecoveryCodesHint;
|
||||
|
||||
/// No description provided for @passwordSectionNewPhrase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New recovery phrase'**
|
||||
String get passwordSectionNewPhrase;
|
||||
|
||||
/// No description provided for @passwordSectionPhraseHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your old phrase stops working.'**
|
||||
String get passwordSectionPhraseHint;
|
||||
|
||||
/// No description provided for @passwordChangeTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change password'**
|
||||
String get passwordChangeTitle;
|
||||
|
||||
/// No description provided for @passwordChangeIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your other devices are signed out. Your keys stay: nothing is lost.'**
|
||||
String get passwordChangeIntro;
|
||||
|
||||
/// No description provided for @passwordChangeAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change password'**
|
||||
String get passwordChangeAction;
|
||||
|
||||
/// No description provided for @passwordChanged.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password changed. Your other devices are signed out.'**
|
||||
String get passwordChanged;
|
||||
|
||||
/// No description provided for @passwordNewCodesIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter a code from your authenticator app. Your old recovery codes stop working.'**
|
||||
String get passwordNewCodesIntro;
|
||||
|
||||
/// No description provided for @passwordNewPhraseIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your new recovery phrase. Write the 24 words down in order: once you confirm it, your old phrase opens nothing any more.'**
|
||||
String get passwordNewPhraseIntro;
|
||||
|
||||
/// No description provided for @passwordPhraseKept.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New recovery phrase set. The old one no longer works.'**
|
||||
String get passwordPhraseKept;
|
||||
|
||||
/// No description provided for @passwordWrongCredentials.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The address, the password or the code isn\'t right.'**
|
||||
String get passwordWrongCredentials;
|
||||
|
||||
/// No description provided for @passwordTooManyAttempts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Too many attempts. Try again in an hour.'**
|
||||
String get passwordTooManyAttempts;
|
||||
|
||||
/// No description provided for @passwordSetupIncomplete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account was never finished: set up an authenticator.'**
|
||||
String get passwordSetupIncomplete;
|
||||
|
||||
/// No description provided for @passwordNotDue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This takes effect on {date}.'**
|
||||
String passwordNotDue(String date);
|
||||
|
||||
/// No description provided for @passwordNotDueYet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This hasn\'t taken effect yet.'**
|
||||
String get passwordNotDueYet;
|
||||
|
||||
/// No description provided for @passwordNoReset.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No reset waits for this address.'**
|
||||
String get passwordNoReset;
|
||||
|
||||
/// No description provided for @passwordExists.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This address\'s account has a password already. Sign in with it, or get back in if you\'ve forgotten it.'**
|
||||
String get passwordExists;
|
||||
|
||||
/// No description provided for @passwordAddressTaken.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This address belongs to another account.'**
|
||||
String get passwordAddressTaken;
|
||||
|
||||
/// No description provided for @passwordNoAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account has no email address yet.'**
|
||||
String get passwordNoAddress;
|
||||
|
||||
/// No description provided for @passwordNoPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This account has no password.'**
|
||||
String get passwordNoPassword;
|
||||
|
||||
/// No description provided for @passwordWrongCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This code isn\'t right. Enter the one your app shows now.'**
|
||||
String get passwordWrongCode;
|
||||
|
||||
/// No description provided for @passwordWrongExistingCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The code from your other authenticator isn\'t right.'**
|
||||
String get passwordWrongExistingCode;
|
||||
|
||||
/// No description provided for @passwordLastAuthenticator.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your last authenticator can\'t go: every sign-in needs one.'**
|
||||
String get passwordLastAuthenticator;
|
||||
|
||||
/// No description provided for @passwordTooManyAuthenticators.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account has as many authenticators as it may. Remove one first.'**
|
||||
String get passwordTooManyAuthenticators;
|
||||
|
||||
/// No description provided for @passwordUnknownAuthenticator.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This authenticator is gone.'**
|
||||
String get passwordUnknownAuthenticator;
|
||||
|
||||
/// No description provided for @passwordInvalidSetup.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The instance didn\'t accept what this device made. Update the app, or try again.'**
|
||||
String get passwordInvalidSetup;
|
||||
|
||||
/// No description provided for @passwordChallengeExpired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'That took too long. Try again.'**
|
||||
String get passwordChallengeExpired;
|
||||
|
||||
/// No description provided for @passwordUnavailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This instance offers no password sign-in.'**
|
||||
String get passwordUnavailable;
|
||||
|
||||
/// No description provided for @passwordNotAnAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This isn\'t an email address.'**
|
||||
String get passwordNotAnAddress;
|
||||
|
||||
/// No description provided for @passwordTooManyCodes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Too many codes asked for. Try again in an hour.'**
|
||||
String get passwordTooManyCodes;
|
||||
|
||||
/// No description provided for @passwordWrongMailCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This code isn\'t right.'**
|
||||
String get passwordWrongMailCode;
|
||||
|
||||
/// No description provided for @passwordTooManyMailTries.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Too many wrong codes. Ask for a new one.'**
|
||||
String get passwordTooManyMailTries;
|
||||
|
||||
/// No description provided for @passwordMailCodeExpired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This code is no longer valid. Ask for a new one.'**
|
||||
String get passwordMailCodeExpired;
|
||||
|
||||
/// No description provided for @passwordMailFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The instance couldn\'t send the email. Try again later.'**
|
||||
String get passwordMailFailed;
|
||||
|
||||
/// No description provided for @passwordOtherAddressLinked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account has another email address.'**
|
||||
String get passwordOtherAddressLinked;
|
||||
|
||||
/// No description provided for @passwordNotAPhrase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This isn\'t a recovery phrase: check every word and their number.'**
|
||||
String get passwordNotAPhrase;
|
||||
|
||||
/// No description provided for @passwordPhraseDoesNotFit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The address or the recovery phrase isn\'t right.'**
|
||||
String get passwordPhraseDoesNotFit;
|
||||
|
||||
/// No description provided for @passwordUnusable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This password can\'t be used: it\'s empty, or holds characters that aren\'t text.'**
|
||||
String get passwordUnusable;
|
||||
|
||||
/// No description provided for @passwordInstanceUnproven.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The instance couldn\'t prove it knows your password, so this device trusts nothing it says. Try again; if it happens again, the instance isn\'t the one it claims to be.'**
|
||||
String get passwordInstanceUnproven;
|
||||
|
||||
/// No description provided for @passwordKeysMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account\'s keys aren\'t on this device. Sign out and in again with your password here.'**
|
||||
String get passwordKeysMissing;
|
||||
|
||||
/// No description provided for @passwordFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t reach the instance. Check the connection and try again.'**
|
||||
String get passwordFailed;
|
||||
}
|
||||
|
||||
class _HalkyonAppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -334,4 +334,529 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get syncMismatchMore => 'Mehr';
|
||||
|
||||
@override
|
||||
String get startSignIn => 'Anmelden';
|
||||
|
||||
@override
|
||||
String get startCreateAccount => 'Konto erstellen';
|
||||
|
||||
@override
|
||||
String startGitHubEnds(String date) {
|
||||
return 'Die Anmeldung mit GitHub endet am $date.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordAddress => 'E-Mail-Adresse';
|
||||
|
||||
@override
|
||||
String get passwordPassword => 'Passwort';
|
||||
|
||||
@override
|
||||
String get passwordNewPassword => 'Neues Passwort';
|
||||
|
||||
@override
|
||||
String get passwordCurrent => 'Aktuelles Passwort';
|
||||
|
||||
@override
|
||||
String get passwordRepeat => 'Neues Passwort wiederholen';
|
||||
|
||||
@override
|
||||
String get passwordsDiffer => 'Die beiden Passwörter stimmen nicht überein.';
|
||||
|
||||
@override
|
||||
String get passwordShow => 'Passwort anzeigen';
|
||||
|
||||
@override
|
||||
String get passwordHide => 'Passwort verbergen';
|
||||
|
||||
@override
|
||||
String get passwordContinue => 'Weiter';
|
||||
|
||||
@override
|
||||
String get passwordBack => 'Zurück';
|
||||
|
||||
@override
|
||||
String get passwordDone => 'Fertig';
|
||||
|
||||
@override
|
||||
String get passwordCopy => 'Kopieren';
|
||||
|
||||
@override
|
||||
String get passwordCopied => 'Kopiert.';
|
||||
|
||||
@override
|
||||
String get passwordSave => 'Als Datei speichern';
|
||||
|
||||
@override
|
||||
String get passwordQrCode => 'QR-Code für deine Authenticator-App';
|
||||
|
||||
@override
|
||||
String get passwordMailCode => 'Code aus der E-Mail';
|
||||
|
||||
@override
|
||||
String passwordMailCodeSent(String address) {
|
||||
return 'Wir haben dir einen Code an $address geschickt. Er gilt 15 Minuten.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordResendCode => 'Neuen Code senden';
|
||||
|
||||
@override
|
||||
String get passwordTotpCode => 'Code aus deiner Authenticator-App';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCode => 'Wiederherstellungscode';
|
||||
|
||||
@override
|
||||
String get passwordUseRecoveryCode =>
|
||||
'Stattdessen einen Wiederherstellungscode verwenden';
|
||||
|
||||
@override
|
||||
String get passwordUseTotpCode =>
|
||||
'Stattdessen deine Authenticator-App verwenden';
|
||||
|
||||
@override
|
||||
String get passwordUnlocking =>
|
||||
'Dein Passwort wird auf diesem Gerät entsperrt …';
|
||||
|
||||
@override
|
||||
String get passwordMakingKeys =>
|
||||
'Deine Schlüssel werden auf diesem Gerät erstellt …';
|
||||
|
||||
@override
|
||||
String get passwordSignInTitle => 'Anmelden';
|
||||
|
||||
@override
|
||||
String get passwordSignInAction => 'Anmelden';
|
||||
|
||||
@override
|
||||
String passwordSignInIntro(String instance) {
|
||||
return 'Mit der E-Mail-Adresse und dem Passwort deines Kontos auf $instance.';
|
||||
}
|
||||
|
||||
@override
|
||||
String passwordSecondFactorIntro(String instance) {
|
||||
return 'Gib den Code ein, den deine Authenticator-App für $instance zeigt.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCodeIntro =>
|
||||
'Gib einen deiner Wiederherstellungscodes ein. Jeder meldet einmal an.';
|
||||
|
||||
@override
|
||||
String get passwordForgot => 'Passwort vergessen?';
|
||||
|
||||
@override
|
||||
String get passwordOtherPassword => 'Adresse oder Passwort ändern';
|
||||
|
||||
@override
|
||||
String get passwordLostAuthenticator => 'Authenticator verloren?';
|
||||
|
||||
@override
|
||||
String get passwordLostAuthenticatorTitle => 'Authenticator verloren';
|
||||
|
||||
@override
|
||||
String get passwordLostAuthenticatorIntro =>
|
||||
'Mit einem Wiederherstellungscode meldest du dich wie gewohnt an. Ohne einen kannst du deine Authenticators zurücksetzen lassen: Das wirkt 72 Stunden später, und eine Anmeldung mit einem Code stoppt es bis dahin.';
|
||||
|
||||
@override
|
||||
String get passwordResetWithMail => 'Mit einem Code per E-Mail zurücksetzen';
|
||||
|
||||
@override
|
||||
String passwordResetWithMailHint(String address) {
|
||||
return 'Wir schicken einen Code an $address.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordResetWithPhrase =>
|
||||
'Mit deiner Wiederherstellungsphrase zurücksetzen';
|
||||
|
||||
@override
|
||||
String get passwordResetWithPhraseHint =>
|
||||
'Die 24 Wörter, die du dir notiert hast.';
|
||||
|
||||
@override
|
||||
String get passwordSignInWithoutCode => 'Ohne Code anmelden';
|
||||
|
||||
@override
|
||||
String get passwordSignInWithoutCodeHint =>
|
||||
'Sobald ein Zurücksetzen, um das du gebeten hast, wirksam ist.';
|
||||
|
||||
@override
|
||||
String get passwordResetAction => 'Zurücksetzen';
|
||||
|
||||
@override
|
||||
String passwordAuthenticatorResetAt(String date) {
|
||||
return 'Deine Authenticators werden am $date zurückgesetzt. Wir haben dir eine E-Mail geschickt: Warst du es nicht, melde dich vorher mit einem Code an, um es zu stoppen. Danach meldest du dich nur mit deinem Passwort an und richtest einen neuen Authenticator ein.';
|
||||
}
|
||||
|
||||
@override
|
||||
String passwordUnfinishedIntro(String address) {
|
||||
return 'Dein Konto wurde nie fertig eingerichtet. Wir haben dir einen Code an $address geschickt: Damit meldest du dich an und richtest einen Authenticator ein.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordPhraseField => 'Wiederherstellungsphrase';
|
||||
|
||||
@override
|
||||
String get passwordPhraseFieldHint =>
|
||||
'Die 24 Wörter, durch Leerzeichen getrennt';
|
||||
|
||||
@override
|
||||
String get passwordPhraseFieldIntro =>
|
||||
'Gib deine Wiederherstellungsphrase ein: Sie beweist, dass das Konto deines ist.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorTitle => 'Authenticator einrichten';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorIntro =>
|
||||
'Jede Anmeldung braucht einen Code aus einer Authenticator-App wie Aegis, 2FAS oder Proton Authenticator. Scanne diesen Code damit oder tippe den Schlüssel ein.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorKey => 'Schlüssel';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorOpen => 'In deiner Authenticator-App öffnen';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorNoApp =>
|
||||
'Keine App auf diesem Gerät öffnet ihn. Scanne den Code oder tippe den Schlüssel ein.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorCodeIntro =>
|
||||
'Gib dann den Code ein, den sie zeigt.';
|
||||
|
||||
@override
|
||||
String get passwordExistingCode => 'Code von einem bisherigen Authenticator';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCodesTitle => 'Wiederherstellungscodes';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCodesIntro =>
|
||||
'Jeder meldet dich einmal anstelle eines Codes aus deinem Authenticator an, falls du ihn verlierst. Bewahre sie auf, wo nur du sie findest: Sie werden nur jetzt angezeigt.';
|
||||
|
||||
@override
|
||||
String get passwordCodesKept => 'Ich habe sie aufbewahrt';
|
||||
|
||||
@override
|
||||
String get passwordPhraseTitle => 'Deine Wiederherstellungsphrase';
|
||||
|
||||
@override
|
||||
String get passwordPhraseIntro =>
|
||||
'Diese 24 Wörter öffnen die Schlüssel deines Kontos, falls du dein Passwort vergisst: Ohne sie ist Verschlüsseltes verloren. Schreib sie der Reihe nach auf und bewahre sie auf, wo nur du sie findest. Sie werden nur jetzt angezeigt.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseWrittenDown => 'Ich habe sie aufgeschrieben';
|
||||
|
||||
@override
|
||||
String get passwordPhraseCheckTitle => 'Wiederherstellungsphrase prüfen';
|
||||
|
||||
@override
|
||||
String get passwordPhraseCheckIntro =>
|
||||
'Tippe die gefragten Wörter aus deiner Notiz ein.';
|
||||
|
||||
@override
|
||||
String passwordPhraseWord(int number) {
|
||||
return 'Wort $number';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordPhraseWrong =>
|
||||
'Das sind nicht die Wörter. Sieh noch einmal in deine Notiz.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseShowAgain => 'Phrase noch einmal zeigen';
|
||||
|
||||
@override
|
||||
String get passwordCreateTitle => 'Konto erstellen';
|
||||
|
||||
@override
|
||||
String passwordCreateIntro(String instance) {
|
||||
return 'Auf $instance, mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App für den Code, den jede Anmeldung braucht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSetUpTitle => 'Passwort einrichten';
|
||||
|
||||
@override
|
||||
String get passwordSetUpIntro =>
|
||||
'Melde dich mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App an, auf diesem Gerät und anderen. Alles, was du hast, bleibt.';
|
||||
|
||||
@override
|
||||
String get passwordChooseIntro =>
|
||||
'Wähle ein Passwort. Es verlässt dieses Gerät nie: Die Instanz kann es nicht lesen und nicht zurücksetzen, ohne dass verloren geht, was es schützt.';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetTitle => 'Zurücksetzen abschließen';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetIntro =>
|
||||
'72 Stunden nachdem du um das Zurücksetzen deines Kontos gebeten hast, schließt du es hier mit einem neuen Code per E-Mail ab.';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetHavePhrase =>
|
||||
'Ich habe meine Wiederherstellungsphrase';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetKeepsKeys =>
|
||||
'Dein Konto behält seine Schlüssel: Nichts Verschlüsseltes geht verloren.';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetNewKeys =>
|
||||
'Ohne deine Wiederherstellungsphrase bekommt dein Konto neue Schlüssel: Was mit den alten verschlüsselt war, ist verloren.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryTitle => 'Zurück in dein Konto';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithPhrase =>
|
||||
'Ich habe meine Wiederherstellungsphrase';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithPhraseHint =>
|
||||
'Setze sofort ein neues Passwort. Alles bleibt.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithPhraseIntro =>
|
||||
'Deine Wiederherstellungsphrase öffnet die Schlüssel deines Kontos, die dann dein neues Passwort schützt. Ein Code aus deinem Authenticator oder ein Wiederherstellungscode bestätigt es.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryNoCodeHint =>
|
||||
'Auch deinen Authenticator verloren? Setze dein Konto 72 Stunden später mit deiner Wiederherstellungsphrase zurück: Sie behält alles.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithoutPhrase => 'Mein Konto zurücksetzen';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithoutPhraseHint =>
|
||||
'Wirkt 72 Stunden später. Ohne deine Wiederherstellungsphrase ist Verschlüsseltes verloren.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryStartIntro =>
|
||||
'Wir schicken einen Code an deine Adresse. Dein Konto kann dann 72 Stunden später zurückgesetzt werden; eine Anmeldung mit deinem Passwort stoppt es bis dahin.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryFinish => 'Zurücksetzen abschließen';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryFinishHint =>
|
||||
'72 Stunden nachdem du darum gebeten hast.';
|
||||
|
||||
@override
|
||||
String passwordResetStarted(String date) {
|
||||
return 'Dein Konto kann ab dem $date zurückgesetzt werden. Wir haben dir eine E-Mail geschickt: Warst du es nicht, melde dich vorher mit deinem Passwort an, um es zu stoppen.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSectionTitle => 'Passwort und Authenticator';
|
||||
|
||||
@override
|
||||
String get passwordSectionFailed =>
|
||||
'Wie dein Konto sich anmeldet, konnte nicht gelesen werden.';
|
||||
|
||||
@override
|
||||
String passwordSectionGitHubEnds(String date) {
|
||||
return 'Die Anmeldung mit GitHub endet am $date. Richte bis dahin ein Passwort ein, um dein Konto zu behalten.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSectionSetUp => 'Passwort einrichten';
|
||||
|
||||
@override
|
||||
String get passwordSectionSetUpHint =>
|
||||
'Melde dich mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App an.';
|
||||
|
||||
@override
|
||||
String get passwordSectionUnfinished =>
|
||||
'Fertig einrichten: Authenticator hinzufügen';
|
||||
|
||||
@override
|
||||
String get passwordSectionUnfinishedHint =>
|
||||
'Dein Passwort meldet dich an, sobald ein Authenticator eingerichtet ist.';
|
||||
|
||||
@override
|
||||
String get passwordSectionChange => 'Passwort ändern';
|
||||
|
||||
@override
|
||||
String get passwordSectionKeysMissing =>
|
||||
'Melde dich auf diesem Gerät mit deinem Passwort an, um das zu ändern.';
|
||||
|
||||
@override
|
||||
String get passwordSectionNotConfirmed => 'Noch nicht eingerichtet';
|
||||
|
||||
@override
|
||||
String passwordSectionRemove(String name) {
|
||||
return '$name entfernen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSectionRemoveIntro =>
|
||||
'Gib zur Bestätigung einen Code aus einem deiner Authenticators ein.';
|
||||
|
||||
@override
|
||||
String get passwordSectionRemoveAction => 'Entfernen';
|
||||
|
||||
@override
|
||||
String get passwordSectionAddAuthenticator => 'Authenticator hinzufügen';
|
||||
|
||||
@override
|
||||
String get passwordSectionRecoveryCodes => 'Neue Wiederherstellungscodes';
|
||||
|
||||
@override
|
||||
String get passwordSectionRecoveryCodesHint =>
|
||||
'Die alten gelten dann nicht mehr.';
|
||||
|
||||
@override
|
||||
String get passwordSectionNewPhrase => 'Neue Wiederherstellungsphrase';
|
||||
|
||||
@override
|
||||
String get passwordSectionPhraseHint =>
|
||||
'Deine alte Phrase gilt dann nicht mehr.';
|
||||
|
||||
@override
|
||||
String get passwordChangeTitle => 'Passwort ändern';
|
||||
|
||||
@override
|
||||
String get passwordChangeIntro =>
|
||||
'Deine anderen Geräte werden abgemeldet. Deine Schlüssel bleiben: Nichts geht verloren.';
|
||||
|
||||
@override
|
||||
String get passwordChangeAction => 'Passwort ändern';
|
||||
|
||||
@override
|
||||
String get passwordChanged =>
|
||||
'Passwort geändert. Deine anderen Geräte sind abgemeldet.';
|
||||
|
||||
@override
|
||||
String get passwordNewCodesIntro =>
|
||||
'Gib einen Code aus deiner Authenticator-App ein. Deine alten Wiederherstellungscodes gelten dann nicht mehr.';
|
||||
|
||||
@override
|
||||
String get passwordNewPhraseIntro =>
|
||||
'Deine neue Wiederherstellungsphrase. Schreib die 24 Wörter der Reihe nach auf: Sobald du sie bestätigst, öffnet deine alte Phrase nichts mehr.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseKept =>
|
||||
'Neue Wiederherstellungsphrase gesetzt. Die alte gilt nicht mehr.';
|
||||
|
||||
@override
|
||||
String get passwordWrongCredentials =>
|
||||
'Die Adresse, das Passwort oder der Code stimmt nicht.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyAttempts =>
|
||||
'Zu viele Versuche. Versuch es in einer Stunde wieder.';
|
||||
|
||||
@override
|
||||
String get passwordSetupIncomplete =>
|
||||
'Dein Konto wurde nie fertig eingerichtet: Richte einen Authenticator ein.';
|
||||
|
||||
@override
|
||||
String passwordNotDue(String date) {
|
||||
return 'Das wirkt am $date.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordNotDueYet => 'Das wirkt noch nicht.';
|
||||
|
||||
@override
|
||||
String get passwordNoReset => 'Für diese Adresse wartet kein Zurücksetzen.';
|
||||
|
||||
@override
|
||||
String get passwordExists =>
|
||||
'Das Konto dieser Adresse hat schon ein Passwort. Melde dich damit an oder komm zurück in dein Konto, falls du es vergessen hast.';
|
||||
|
||||
@override
|
||||
String get passwordAddressTaken =>
|
||||
'Diese Adresse gehört zu einem anderen Konto.';
|
||||
|
||||
@override
|
||||
String get passwordNoAddress => 'Dein Konto hat noch keine E-Mail-Adresse.';
|
||||
|
||||
@override
|
||||
String get passwordNoPassword => 'Dieses Konto hat kein Passwort.';
|
||||
|
||||
@override
|
||||
String get passwordWrongCode =>
|
||||
'Dieser Code stimmt nicht. Gib den ein, den deine App gerade zeigt.';
|
||||
|
||||
@override
|
||||
String get passwordWrongExistingCode =>
|
||||
'Der Code von deinem anderen Authenticator stimmt nicht.';
|
||||
|
||||
@override
|
||||
String get passwordLastAuthenticator =>
|
||||
'Dein letzter Authenticator kann nicht weg: Jede Anmeldung braucht einen.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyAuthenticators =>
|
||||
'Dein Konto hat so viele Authenticators, wie es darf. Entferne zuerst einen.';
|
||||
|
||||
@override
|
||||
String get passwordUnknownAuthenticator => 'Dieser Authenticator ist weg.';
|
||||
|
||||
@override
|
||||
String get passwordInvalidSetup =>
|
||||
'Die Instanz hat nicht angenommen, was dieses Gerät erstellt hat. Aktualisiere die App oder versuch es noch einmal.';
|
||||
|
||||
@override
|
||||
String get passwordChallengeExpired =>
|
||||
'Das hat zu lange gedauert. Versuch es noch einmal.';
|
||||
|
||||
@override
|
||||
String get passwordUnavailable =>
|
||||
'Diese Instanz bietet keine Anmeldung mit Passwort.';
|
||||
|
||||
@override
|
||||
String get passwordNotAnAddress => 'Das ist keine E-Mail-Adresse.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyCodes =>
|
||||
'Zu viele Codes angefordert. Versuch es in einer Stunde wieder.';
|
||||
|
||||
@override
|
||||
String get passwordWrongMailCode => 'Dieser Code stimmt nicht.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyMailTries =>
|
||||
'Zu viele falsche Codes. Fordere einen neuen an.';
|
||||
|
||||
@override
|
||||
String get passwordMailCodeExpired =>
|
||||
'Dieser Code gilt nicht mehr. Fordere einen neuen an.';
|
||||
|
||||
@override
|
||||
String get passwordMailFailed =>
|
||||
'Die Instanz konnte die E-Mail nicht senden. Versuch es später noch einmal.';
|
||||
|
||||
@override
|
||||
String get passwordOtherAddressLinked =>
|
||||
'Dein Konto hat eine andere E-Mail-Adresse.';
|
||||
|
||||
@override
|
||||
String get passwordNotAPhrase =>
|
||||
'Das ist keine Wiederherstellungsphrase: Prüf jedes Wort und ihre Anzahl.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseDoesNotFit =>
|
||||
'Die Adresse oder die Wiederherstellungsphrase stimmt nicht.';
|
||||
|
||||
@override
|
||||
String get passwordUnusable =>
|
||||
'Dieses Passwort geht nicht: Es ist leer oder enthält Zeichen, die kein Text sind.';
|
||||
|
||||
@override
|
||||
String get passwordInstanceUnproven =>
|
||||
'Die Instanz konnte nicht beweisen, dass sie dein Passwort kennt, daher vertraut dieses Gerät ihr nicht. Versuch es noch einmal; passiert es wieder, ist die Instanz nicht die, die sie zu sein behauptet.';
|
||||
|
||||
@override
|
||||
String get passwordKeysMissing =>
|
||||
'Die Schlüssel deines Kontos sind nicht auf diesem Gerät. Melde dich hier ab und mit deinem Passwort wieder an.';
|
||||
|
||||
@override
|
||||
String get passwordFailed =>
|
||||
'Die Instanz war nicht erreichbar. Prüf die Verbindung und versuch es noch einmal.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,6 +330,517 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get syncMismatchMore => 'More';
|
||||
|
||||
@override
|
||||
String get startSignIn => 'Sign in';
|
||||
|
||||
@override
|
||||
String get startCreateAccount => 'Create an account';
|
||||
|
||||
@override
|
||||
String startGitHubEnds(String date) {
|
||||
return 'Sign-in with GitHub ends on $date.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordAddress => 'Email address';
|
||||
|
||||
@override
|
||||
String get passwordPassword => 'Password';
|
||||
|
||||
@override
|
||||
String get passwordNewPassword => 'New password';
|
||||
|
||||
@override
|
||||
String get passwordCurrent => 'Current password';
|
||||
|
||||
@override
|
||||
String get passwordRepeat => 'New password again';
|
||||
|
||||
@override
|
||||
String get passwordsDiffer => 'The two passwords differ.';
|
||||
|
||||
@override
|
||||
String get passwordShow => 'Show password';
|
||||
|
||||
@override
|
||||
String get passwordHide => 'Hide password';
|
||||
|
||||
@override
|
||||
String get passwordContinue => 'Continue';
|
||||
|
||||
@override
|
||||
String get passwordBack => 'Back';
|
||||
|
||||
@override
|
||||
String get passwordDone => 'Done';
|
||||
|
||||
@override
|
||||
String get passwordCopy => 'Copy';
|
||||
|
||||
@override
|
||||
String get passwordCopied => 'Copied.';
|
||||
|
||||
@override
|
||||
String get passwordSave => 'Save as a file';
|
||||
|
||||
@override
|
||||
String get passwordQrCode => 'QR code for your authenticator app';
|
||||
|
||||
@override
|
||||
String get passwordMailCode => 'Code from the email';
|
||||
|
||||
@override
|
||||
String passwordMailCodeSent(String address) {
|
||||
return 'We emailed a code to $address. It\'s valid for 15 minutes.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordResendCode => 'Send a new code';
|
||||
|
||||
@override
|
||||
String get passwordTotpCode => 'Code from your authenticator app';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCode => 'Recovery code';
|
||||
|
||||
@override
|
||||
String get passwordUseRecoveryCode => 'Use a recovery code instead';
|
||||
|
||||
@override
|
||||
String get passwordUseTotpCode => 'Use your authenticator app instead';
|
||||
|
||||
@override
|
||||
String get passwordUnlocking => 'Unlocking your password on this device…';
|
||||
|
||||
@override
|
||||
String get passwordMakingKeys => 'Making your keys on this device…';
|
||||
|
||||
@override
|
||||
String get passwordSignInTitle => 'Sign in';
|
||||
|
||||
@override
|
||||
String get passwordSignInAction => 'Sign in';
|
||||
|
||||
@override
|
||||
String passwordSignInIntro(String instance) {
|
||||
return 'With the email address and password of your account on $instance.';
|
||||
}
|
||||
|
||||
@override
|
||||
String passwordSecondFactorIntro(String instance) {
|
||||
return 'Enter the code your authenticator app shows for $instance.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCodeIntro =>
|
||||
'Enter one of the recovery codes you kept. Each signs in once.';
|
||||
|
||||
@override
|
||||
String get passwordForgot => 'Forgot your password?';
|
||||
|
||||
@override
|
||||
String get passwordOtherPassword => 'Change the address or password';
|
||||
|
||||
@override
|
||||
String get passwordLostAuthenticator => 'Lost your authenticator?';
|
||||
|
||||
@override
|
||||
String get passwordLostAuthenticatorTitle => 'Lost authenticator';
|
||||
|
||||
@override
|
||||
String get passwordLostAuthenticatorIntro =>
|
||||
'With a recovery code you sign in as usual. Without one, ask to reset your authenticators: it takes effect 72 hours later, and signing in with a code before then stops it.';
|
||||
|
||||
@override
|
||||
String get passwordResetWithMail => 'Reset with a code by email';
|
||||
|
||||
@override
|
||||
String passwordResetWithMailHint(String address) {
|
||||
return 'We email a code to $address.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordResetWithPhrase => 'Reset with your recovery phrase';
|
||||
|
||||
@override
|
||||
String get passwordResetWithPhraseHint => 'The 24 words you wrote down.';
|
||||
|
||||
@override
|
||||
String get passwordSignInWithoutCode => 'Sign in without a code';
|
||||
|
||||
@override
|
||||
String get passwordSignInWithoutCodeHint =>
|
||||
'Once a reset you asked for has taken effect.';
|
||||
|
||||
@override
|
||||
String get passwordResetAction => 'Reset';
|
||||
|
||||
@override
|
||||
String passwordAuthenticatorResetAt(String date) {
|
||||
return 'Your authenticators are reset on $date. We emailed you: if it wasn\'t you, sign in with a code before then to stop it. After that, sign in with your password alone and set up a new authenticator.';
|
||||
}
|
||||
|
||||
@override
|
||||
String passwordUnfinishedIntro(String address) {
|
||||
return 'Your account was never finished. We emailed a code to $address: with it, you sign in and set up an authenticator.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordPhraseField => 'Recovery phrase';
|
||||
|
||||
@override
|
||||
String get passwordPhraseFieldHint =>
|
||||
'The 24 words, with spaces between them';
|
||||
|
||||
@override
|
||||
String get passwordPhraseFieldIntro =>
|
||||
'Enter your recovery phrase: it proves the account is yours.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorTitle => 'Set up an authenticator';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorIntro =>
|
||||
'Every sign-in takes a code from an authenticator app, such as Aegis, 2FAS or Proton Authenticator. Scan this code with it, or type in the key.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorKey => 'Key';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorOpen => 'Open in your authenticator app';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorNoApp =>
|
||||
'No app on this device opens it. Scan the code or type in the key.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorCodeIntro => 'Then enter the code it shows.';
|
||||
|
||||
@override
|
||||
String get passwordExistingCode =>
|
||||
'Code from an authenticator you added before';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCodesTitle => 'Recovery codes';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryCodesIntro =>
|
||||
'Each signs in once in place of a code from your authenticator, should you lose it. Keep them where only you find them: they\'re shown only now.';
|
||||
|
||||
@override
|
||||
String get passwordCodesKept => 'I\'ve kept them';
|
||||
|
||||
@override
|
||||
String get passwordPhraseTitle => 'Your recovery phrase';
|
||||
|
||||
@override
|
||||
String get passwordPhraseIntro =>
|
||||
'These 24 words open your account\'s keys should you forget your password: without them, what\'s encrypted is lost. Write them down in order, and keep them where only you find them. They\'re shown only now.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseWrittenDown => 'I\'ve written it down';
|
||||
|
||||
@override
|
||||
String get passwordPhraseCheckTitle => 'Check your recovery phrase';
|
||||
|
||||
@override
|
||||
String get passwordPhraseCheckIntro =>
|
||||
'Type the words asked for, from what you wrote down.';
|
||||
|
||||
@override
|
||||
String passwordPhraseWord(int number) {
|
||||
return 'Word $number';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordPhraseWrong =>
|
||||
'These aren\'t the words. Look again at what you wrote down.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseShowAgain => 'Show the phrase again';
|
||||
|
||||
@override
|
||||
String get passwordCreateTitle => 'Create an account';
|
||||
|
||||
@override
|
||||
String passwordCreateIntro(String instance) {
|
||||
return 'On $instance, with your email address, a password, and an authenticator app for the code every sign-in takes.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSetUpTitle => 'Set up a password';
|
||||
|
||||
@override
|
||||
String get passwordSetUpIntro =>
|
||||
'Sign in with your email address, a password and an authenticator app, on this device and others. Everything you have stays.';
|
||||
|
||||
@override
|
||||
String get passwordChooseIntro =>
|
||||
'Choose a password. It never leaves this device: the instance can\'t read it, and can\'t reset it without what it protects being lost.';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetTitle => 'Finish the reset';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetIntro =>
|
||||
'72 hours after you asked to reset your account, finish it here with a new code by email.';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetHavePhrase => 'I have my recovery phrase';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetKeepsKeys =>
|
||||
'Your account keeps its keys: nothing encrypted is lost.';
|
||||
|
||||
@override
|
||||
String get passwordFinishResetNewKeys =>
|
||||
'Without your recovery phrase, your account gets new keys: what was encrypted with the old ones is lost.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryTitle => 'Get back into your account';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithPhrase => 'I have my recovery phrase';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithPhraseHint =>
|
||||
'Set a new password at once. Everything stays.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithPhraseIntro =>
|
||||
'Your recovery phrase opens your account\'s keys, which your new password then protects. A code from your authenticator, or a recovery code, confirms it.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryNoCodeHint =>
|
||||
'Lost your authenticator as well? Reset your account 72 hours later with your recovery phrase: it keeps everything.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithoutPhrase => 'Reset my account';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryWithoutPhraseHint =>
|
||||
'Takes effect 72 hours later. Without your recovery phrase, what\'s encrypted is lost.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryStartIntro =>
|
||||
'We email a code to your address. Your account can then be reset 72 hours later; signing in with your password before then stops it.';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryFinish => 'Finish a reset';
|
||||
|
||||
@override
|
||||
String get passwordRecoveryFinishHint => '72 hours after you asked for it.';
|
||||
|
||||
@override
|
||||
String passwordResetStarted(String date) {
|
||||
return 'Your account can be reset from $date. We emailed you: if it wasn\'t you, sign in with your password before then to stop it.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSectionTitle => 'Password and authenticator';
|
||||
|
||||
@override
|
||||
String get passwordSectionFailed =>
|
||||
'Couldn\'t read how your account signs in.';
|
||||
|
||||
@override
|
||||
String passwordSectionGitHubEnds(String date) {
|
||||
return 'Sign-in with GitHub ends on $date. Set up a password before then to keep your account.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSectionSetUp => 'Set up a password';
|
||||
|
||||
@override
|
||||
String get passwordSectionSetUpHint =>
|
||||
'Sign in with your email address, a password and an authenticator app.';
|
||||
|
||||
@override
|
||||
String get passwordSectionUnfinished =>
|
||||
'Finish setting up: add an authenticator';
|
||||
|
||||
@override
|
||||
String get passwordSectionUnfinishedHint =>
|
||||
'Your password signs in once an authenticator is set up.';
|
||||
|
||||
@override
|
||||
String get passwordSectionChange => 'Change password';
|
||||
|
||||
@override
|
||||
String get passwordSectionKeysMissing =>
|
||||
'Sign in on this device with your password to change it.';
|
||||
|
||||
@override
|
||||
String get passwordSectionNotConfirmed => 'Not set up yet';
|
||||
|
||||
@override
|
||||
String passwordSectionRemove(String name) {
|
||||
return 'Remove $name?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordSectionRemoveIntro =>
|
||||
'Enter a code from one of your authenticators to confirm.';
|
||||
|
||||
@override
|
||||
String get passwordSectionRemoveAction => 'Remove';
|
||||
|
||||
@override
|
||||
String get passwordSectionAddAuthenticator => 'Add an authenticator';
|
||||
|
||||
@override
|
||||
String get passwordSectionRecoveryCodes => 'New recovery codes';
|
||||
|
||||
@override
|
||||
String get passwordSectionRecoveryCodesHint => 'The old ones stop working.';
|
||||
|
||||
@override
|
||||
String get passwordSectionNewPhrase => 'New recovery phrase';
|
||||
|
||||
@override
|
||||
String get passwordSectionPhraseHint => 'Your old phrase stops working.';
|
||||
|
||||
@override
|
||||
String get passwordChangeTitle => 'Change password';
|
||||
|
||||
@override
|
||||
String get passwordChangeIntro =>
|
||||
'Your other devices are signed out. Your keys stay: nothing is lost.';
|
||||
|
||||
@override
|
||||
String get passwordChangeAction => 'Change password';
|
||||
|
||||
@override
|
||||
String get passwordChanged =>
|
||||
'Password changed. Your other devices are signed out.';
|
||||
|
||||
@override
|
||||
String get passwordNewCodesIntro =>
|
||||
'Enter a code from your authenticator app. Your old recovery codes stop working.';
|
||||
|
||||
@override
|
||||
String get passwordNewPhraseIntro =>
|
||||
'Your new recovery phrase. Write the 24 words down in order: once you confirm it, your old phrase opens nothing any more.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseKept =>
|
||||
'New recovery phrase set. The old one no longer works.';
|
||||
|
||||
@override
|
||||
String get passwordWrongCredentials =>
|
||||
'The address, the password or the code isn\'t right.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyAttempts =>
|
||||
'Too many attempts. Try again in an hour.';
|
||||
|
||||
@override
|
||||
String get passwordSetupIncomplete =>
|
||||
'Your account was never finished: set up an authenticator.';
|
||||
|
||||
@override
|
||||
String passwordNotDue(String date) {
|
||||
return 'This takes effect on $date.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordNotDueYet => 'This hasn\'t taken effect yet.';
|
||||
|
||||
@override
|
||||
String get passwordNoReset => 'No reset waits for this address.';
|
||||
|
||||
@override
|
||||
String get passwordExists =>
|
||||
'This address\'s account has a password already. Sign in with it, or get back in if you\'ve forgotten it.';
|
||||
|
||||
@override
|
||||
String get passwordAddressTaken => 'This address belongs to another account.';
|
||||
|
||||
@override
|
||||
String get passwordNoAddress => 'Your account has no email address yet.';
|
||||
|
||||
@override
|
||||
String get passwordNoPassword => 'This account has no password.';
|
||||
|
||||
@override
|
||||
String get passwordWrongCode =>
|
||||
'This code isn\'t right. Enter the one your app shows now.';
|
||||
|
||||
@override
|
||||
String get passwordWrongExistingCode =>
|
||||
'The code from your other authenticator isn\'t right.';
|
||||
|
||||
@override
|
||||
String get passwordLastAuthenticator =>
|
||||
'Your last authenticator can\'t go: every sign-in needs one.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyAuthenticators =>
|
||||
'Your account has as many authenticators as it may. Remove one first.';
|
||||
|
||||
@override
|
||||
String get passwordUnknownAuthenticator => 'This authenticator is gone.';
|
||||
|
||||
@override
|
||||
String get passwordInvalidSetup =>
|
||||
'The instance didn\'t accept what this device made. Update the app, or try again.';
|
||||
|
||||
@override
|
||||
String get passwordChallengeExpired => 'That took too long. Try again.';
|
||||
|
||||
@override
|
||||
String get passwordUnavailable => 'This instance offers no password sign-in.';
|
||||
|
||||
@override
|
||||
String get passwordNotAnAddress => 'This isn\'t an email address.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyCodes =>
|
||||
'Too many codes asked for. Try again in an hour.';
|
||||
|
||||
@override
|
||||
String get passwordWrongMailCode => 'This code isn\'t right.';
|
||||
|
||||
@override
|
||||
String get passwordTooManyMailTries =>
|
||||
'Too many wrong codes. Ask for a new one.';
|
||||
|
||||
@override
|
||||
String get passwordMailCodeExpired =>
|
||||
'This code is no longer valid. Ask for a new one.';
|
||||
|
||||
@override
|
||||
String get passwordMailFailed =>
|
||||
'The instance couldn\'t send the email. Try again later.';
|
||||
|
||||
@override
|
||||
String get passwordOtherAddressLinked =>
|
||||
'Your account has another email address.';
|
||||
|
||||
@override
|
||||
String get passwordNotAPhrase =>
|
||||
'This isn\'t a recovery phrase: check every word and their number.';
|
||||
|
||||
@override
|
||||
String get passwordPhraseDoesNotFit =>
|
||||
'The address or the recovery phrase isn\'t right.';
|
||||
|
||||
@override
|
||||
String get passwordUnusable =>
|
||||
'This password can\'t be used: it\'s empty, or holds characters that aren\'t text.';
|
||||
|
||||
@override
|
||||
String get passwordInstanceUnproven =>
|
||||
'The instance couldn\'t prove it knows your password, so this device trusts nothing it says. Try again; if it happens again, the instance isn\'t the one it claims to be.';
|
||||
|
||||
@override
|
||||
String get passwordKeysMissing =>
|
||||
'Your account\'s keys aren\'t on this device. Sign out and in again with your password here.';
|
||||
|
||||
@override
|
||||
String get passwordFailed =>
|
||||
'Couldn\'t reach the instance. Check the connection and try again.';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:halkyon_app/src/auth/data/auth_repository.dart';
|
||||
import 'package:halkyon_app/src/auth/data/signed_out_mark.dart';
|
||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_app/src/files/file_saver.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
|
||||
/// What can be closed once it is no longer needed: an app's dependencies on
|
||||
/// an instance, or a person's.
|
||||
|
|
@ -26,6 +28,14 @@ abstract interface class AppDependencies<P extends Disposable>()
|
|||
/// Whether the person here signed out, so the app asks how to start.
|
||||
SignedOutMark get signedOut;
|
||||
|
||||
/// Signing in with a password and TOTP, signing up, getting back in, and
|
||||
/// what keeps an account (ADR 0017).
|
||||
PasswordRepository get passwords;
|
||||
|
||||
/// Keeps a file the app made where the person chooses (a recovery phrase,
|
||||
/// recovery codes, an app's exports).
|
||||
FileSaver get fileSaver;
|
||||
|
||||
/// Where the source of what the instance runs is, as it names it (its
|
||||
/// basics, halkyon_base's): both licences require offering it to the
|
||||
/// people who use the instance, which About does (`showAppAbout`).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_app/src/auth/model/email_sign_in.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||
|
||||
/// The boundary to the server's authentication: who is signed in, and signing
|
||||
|
|
@ -62,4 +63,14 @@ abstract interface class AuthGateway() {
|
|||
|
||||
/// How the signed-in person signs in.
|
||||
Future<SignInWays> signInWays();
|
||||
|
||||
/// Puts [grant] in place of this device's session: its person is signed
|
||||
/// in. An anonymous account left behind is deleted, as nobody could sign
|
||||
/// into it again (as switching to GitHub does).
|
||||
Future<void> useSession(SessionGrant grant);
|
||||
|
||||
/// The signed-in person was made identified on the instance (a password
|
||||
/// and address set, ADR 0017): this device's copy of their session
|
||||
/// follows.
|
||||
Future<void> markIdentified();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
|||
import 'package:halkyon_app/src/auth/model/email_sign_in.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||
import 'package:halkyon_app/src/base/base_client.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart'
|
||||
|
|
@ -288,6 +289,13 @@ class ServerpodAuthGateway(
|
|||
return SignInWays(gitHub: ways.gitHub, email: ways.email);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> useSession(SessionGrant grant) =>
|
||||
_switchTo(grant.session as AuthSuccess);
|
||||
|
||||
@override
|
||||
Future<void> markIdentified() => _becomeIdentified();
|
||||
|
||||
/// The instance's refusal of a GitHub sign-in as [GitHubSignInFailed].
|
||||
static GitHubSignInFailed _gitHubFailure(GitHubSignInRefusedException e) =>
|
||||
GitHubSignInFailed(
|
||||
|
|
|
|||
19
packages/halkyon_app/lib/src/auth/model/session_grant.dart
Normal file
19
packages/halkyon_app/lib/src/auth/model/session_grant.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A session an instance granted a person, not yet the device's: a sign-in
|
||||
/// or sign-up kept back while an authenticator is set up with it, then put
|
||||
/// in place of the device's session (`AuthGateway.useSession`). Opaque to
|
||||
/// all but the gateway that made it.
|
||||
final class const SessionGrant(
|
||||
/// Whose it is (their `AuthUser` id).
|
||||
final String authUserId,
|
||||
|
||||
/// The session itself, as the gateway that made it keeps it.
|
||||
final Object session,
|
||||
) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [authUserId, session];
|
||||
|
||||
@override
|
||||
String toString() => 'SessionGrant($authUserId)';
|
||||
}
|
||||
|
|
@ -5,30 +5,81 @@ import 'package:halkyon_app/src/app/app_scope.dart';
|
|||
import 'package:halkyon_app/src/auth/bloc/session_bloc.dart';
|
||||
import 'package:halkyon_app/src/instance/instance_scope.dart';
|
||||
import 'package:halkyon_app/src/instance/widget/account_switcher_sheet.dart';
|
||||
import 'package:halkyon_app/src/password/bloc/account_setup_cubit.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/password/widget/account_recovery_sheet.dart';
|
||||
import 'package:halkyon_app/src/password/widget/account_setup_sheet.dart';
|
||||
import 'package:halkyon_app/src/password/widget/password_sign_in_sheet.dart';
|
||||
import 'package:halkyon_app/src/password/widget/password_texts.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// How to start once signed out: on the instance in use, with GitHub or an
|
||||
/// email address where it offers them, or without an account; or on
|
||||
/// another instance. Nothing is made until the person chooses.
|
||||
/// How to start once signed out: on the instance in use, signing in with a
|
||||
/// password or creating an account where it offers password sign-in (ADR
|
||||
/// 0017), with GitHub (saying when it ends) or an email address where it
|
||||
/// offers them, or without an account; or on another instance. Nothing is
|
||||
/// made until the person chooses.
|
||||
class const StartScreen({required final SessionStateSignedOut state, super.key})
|
||||
extends StatefulWidget {
|
||||
@override
|
||||
State<StartScreen> createState() => _StartScreenState();
|
||||
}
|
||||
|
||||
typedef _Offers = ({
|
||||
bool gitHub,
|
||||
bool email,
|
||||
bool password,
|
||||
DateTime? gitHubEnds,
|
||||
});
|
||||
|
||||
class _StartScreenState() extends State<StartScreen> {
|
||||
late final Future<({bool gitHub, bool email})> _offers = () async {
|
||||
final auth = AppScope.of(context).auth;
|
||||
final (gitHub, email) = await (
|
||||
late final Future<_Offers> _offers = () async {
|
||||
final dependencies = AppScope.of(context);
|
||||
final auth = dependencies.auth;
|
||||
Future<({bool offered, DateTime? gitHubEnds})> passwords() async {
|
||||
try {
|
||||
return await dependencies.passwords.offers();
|
||||
} on PasswordFailed {
|
||||
return (offered: false, gitHubEnds: null);
|
||||
}
|
||||
}
|
||||
|
||||
final (gitHub, email, password) = await (
|
||||
auth.canSignInWithGitHub(),
|
||||
auth.canSignInWithEmail(),
|
||||
passwords(),
|
||||
).wait;
|
||||
return (gitHub: gitHub, email: email);
|
||||
return (
|
||||
gitHub: gitHub,
|
||||
email: email,
|
||||
password: password.offered,
|
||||
gitHubEnds: password.gitHubEnds,
|
||||
);
|
||||
}();
|
||||
|
||||
void _send(SessionEvent event) => context.read<SessionBloc>().add(event);
|
||||
|
||||
/// Signs in with a password; a forgotten one leads to getting back in,
|
||||
/// and a reset to finish to the account's setup. Signed in, the person's
|
||||
/// app takes this screen's place.
|
||||
Future<void> _signIn(String instance) async {
|
||||
final outcome = await showPasswordSignInSheet(context, instance: instance);
|
||||
if (outcome != PasswordSignInOutcome.forgotPassword || !mounted) {
|
||||
return;
|
||||
}
|
||||
final next = await showAccountRecoverySheet(context);
|
||||
if (next case FinishAccountReset(:final address, :final phrase)
|
||||
when mounted) {
|
||||
await showAccountSetupSheet(
|
||||
context,
|
||||
mode: AccountSetupMode.finishReset,
|
||||
instance: instance,
|
||||
address: address,
|
||||
phrase: phrase,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
|
|
@ -47,8 +98,14 @@ class _StartScreenState() extends State<StartScreen> {
|
|||
child: FutureBuilder(
|
||||
future: _offers,
|
||||
builder: (context, offers) {
|
||||
final (:gitHub, :email) =
|
||||
offers.data ?? (gitHub: false, email: false);
|
||||
final (:gitHub, :email, :password, :gitHubEnds) =
|
||||
offers.data ??
|
||||
(
|
||||
gitHub: false,
|
||||
email: false,
|
||||
password: false,
|
||||
gitHubEnds: null,
|
||||
);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
@ -64,19 +121,59 @@ class _StartScreenState() extends State<StartScreen> {
|
|||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (gitHub) ...[
|
||||
if (password) ...[
|
||||
FilledButton(
|
||||
onPressed: busy ? null : () => _signIn(instance),
|
||||
child: Text(l10n.startSignIn),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: busy
|
||||
? null
|
||||
: () => _send(
|
||||
const SessionEventGitHubRequested(),
|
||||
: () => showAccountSetupSheet(
|
||||
context,
|
||||
mode: AccountSetupMode.signUp,
|
||||
instance: instance,
|
||||
),
|
||||
child: busy
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(l10n.startWithGitHub),
|
||||
child: Text(l10n.startCreateAccount),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (gitHub) ...[
|
||||
if (password)
|
||||
OutlinedButton(
|
||||
onPressed: busy
|
||||
? null
|
||||
: () => _send(
|
||||
const SessionEventGitHubRequested(),
|
||||
),
|
||||
child: busy
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(l10n.startWithGitHub),
|
||||
)
|
||||
else
|
||||
FilledButton(
|
||||
onPressed: busy
|
||||
? null
|
||||
: () => _send(
|
||||
const SessionEventGitHubRequested(),
|
||||
),
|
||||
child: busy
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(l10n.startWithGitHub),
|
||||
),
|
||||
if (gitHubEnds case final ends?) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.startGitHubEnds(passwordMoment(context, ends)),
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (email) ...[
|
||||
OutlinedButton(
|
||||
onPressed: busy
|
||||
|
|
|
|||
44
packages/halkyon_app/lib/src/files/file_saver.dart
Normal file
44
packages/halkyon_app/lib/src/files/file_saver.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart' as file_picker;
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
|
||||
/// Lets the person keep a file the app made, where they choose.
|
||||
abstract interface class FileSaver() {
|
||||
/// Asks where to keep [bytes], a file of the media type [mimeType] named
|
||||
/// [name] for a start; completes with whether it was kept, false when the
|
||||
/// person cancelled.
|
||||
Future<bool> save({
|
||||
required String name,
|
||||
required Uint8List bytes,
|
||||
required String mimeType,
|
||||
});
|
||||
}
|
||||
|
||||
/// [FileSaver] with the platform's own dialog: `file_selector`'s on the
|
||||
/// desktop; on Android and iOS, where that has none, the system's through
|
||||
/// `file_picker` (Android's Storage Access Framework, iOS's document picker).
|
||||
class PlatformFileSaver() implements FileSaver {
|
||||
@override
|
||||
Future<bool> save({
|
||||
required String name,
|
||||
required Uint8List bytes,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
return await file_picker.FilePicker.saveFile(
|
||||
fileName: name,
|
||||
bytes: bytes,
|
||||
mimeType: mimeType,
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
final location = await getSaveLocation(suggestedName: name);
|
||||
if (location == null) {
|
||||
return false;
|
||||
}
|
||||
await File(location.path).writeAsBytes(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:halkyon_app/src/instance/instance.dart';
|
||||
import 'package:halkyon_app/src/password/data/account_key_store.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
|
||||
|
||||
|
|
@ -29,9 +30,15 @@ class InstanceStorage(
|
|||
? null
|
||||
: 'serverpod_auth_success_key@${_nameOf(instance)}';
|
||||
|
||||
/// The prefix [instance]'s account keys are kept under in the keyring
|
||||
/// (`SecureAccountKeyStore`, ADR 0017).
|
||||
String keysOf(Instance instance) => instance == builtIn
|
||||
? 'halkyon_account_keys'
|
||||
: 'halkyon_account_keys@${_nameOf(instance)}';
|
||||
|
||||
/// Deletes what the device holds of the account on [instance]: its
|
||||
/// people's folder and its session. The session itself stays valid on the
|
||||
/// instance until it expires.
|
||||
/// people's folder, its session and its account's keys. The session itself
|
||||
/// stays valid on the instance until it expires.
|
||||
Future<void> forget(Instance instance) async {
|
||||
final folder = instance == builtIn ? _peopleOfBuiltIn : _folderOf(instance);
|
||||
try {
|
||||
|
|
@ -42,6 +49,7 @@ class InstanceStorage(
|
|||
await SecureClientAuthSuccessStorage(
|
||||
authSuccessStorageKey: sessionKeyOf(instance),
|
||||
).set(null);
|
||||
await SecureAccountKeyStore(keysOf(instance)).forgetAllBut(null);
|
||||
}
|
||||
|
||||
Directory get _peopleOfBuiltIn => Directory(p.join(_support, 'people'));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
|
||||
/// Where getting back into an account stands.
|
||||
enum AccountRecoveryStep() {
|
||||
/// Which way: with the recovery phrase, a reset without it, or finishing
|
||||
/// a reset.
|
||||
choose,
|
||||
|
||||
/// The address, the phrase, a new password and a code.
|
||||
withPhrase,
|
||||
|
||||
/// The address for a reset of the whole account.
|
||||
startAddress,
|
||||
|
||||
/// The code mailed for it.
|
||||
startCode,
|
||||
|
||||
/// The reset was asked for: it takes effect at `resetAt`.
|
||||
started,
|
||||
|
||||
/// The address (and the phrase, if kept) to finish a reset with.
|
||||
finishAddress,
|
||||
|
||||
/// Signed in.
|
||||
done,
|
||||
}
|
||||
|
||||
/// What [AccountRecoveryCubit] shows.
|
||||
final class const AccountRecoveryState({
|
||||
final AccountRecoveryStep step = AccountRecoveryStep.choose,
|
||||
final bool busy = false,
|
||||
final PasswordFailed? problem,
|
||||
final bool passwordsDiffer = false,
|
||||
final bool recoveryCode = false,
|
||||
final String address = '',
|
||||
final DateTime? resetAt,
|
||||
}) with Equatable {
|
||||
AccountRecoveryState copyWith({
|
||||
AccountRecoveryStep? step,
|
||||
bool? busy,
|
||||
PasswordFailed? problem,
|
||||
bool clearProblem = false,
|
||||
bool? passwordsDiffer,
|
||||
bool? recoveryCode,
|
||||
String? address,
|
||||
DateTime? resetAt,
|
||||
}) => AccountRecoveryState(
|
||||
step: step ?? this.step,
|
||||
busy: busy ?? this.busy,
|
||||
problem: clearProblem ? null : problem ?? this.problem,
|
||||
passwordsDiffer: passwordsDiffer ?? this.passwordsDiffer,
|
||||
recoveryCode: recoveryCode ?? this.recoveryCode,
|
||||
address: address ?? this.address,
|
||||
resetAt: resetAt ?? this.resetAt,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
step,
|
||||
busy,
|
||||
problem,
|
||||
passwordsDiffer,
|
||||
recoveryCode,
|
||||
address,
|
||||
resetAt,
|
||||
];
|
||||
}
|
||||
|
||||
/// Getting back into an account without its password (ADR 0017): with the
|
||||
/// recovery phrase (a new password, the keys staying, and a code from the
|
||||
/// authenticator or a recovery code), or by a reset of the whole account,
|
||||
/// asked for with a code by mail and finished 72 hours later (by the
|
||||
/// account's setup, `AccountSetupMode.finishReset`).
|
||||
class AccountRecoveryCubit(
|
||||
final PasswordRepository _passwords, {
|
||||
required final String _language,
|
||||
}) extends Cubit<AccountRecoveryState> {
|
||||
this : super(const AccountRecoveryState());
|
||||
|
||||
String? _request;
|
||||
|
||||
/// The way chosen.
|
||||
void chosen(AccountRecoveryStep step) =>
|
||||
emit(state.copyWith(step: step, clearProblem: true));
|
||||
|
||||
/// Switches between a code from the authenticator and a recovery code.
|
||||
void recoveryCodeChosen({required bool chosen}) =>
|
||||
emit(state.copyWith(recoveryCode: chosen, clearProblem: true));
|
||||
|
||||
/// A new password with the recovery phrase: signed in once it's set.
|
||||
Future<void> phraseEntered({
|
||||
required String address,
|
||||
required String phrase,
|
||||
required String password,
|
||||
required String again,
|
||||
required String code,
|
||||
}) async {
|
||||
if (password != again) {
|
||||
emit(state.copyWith(passwordsDiffer: true, clearProblem: true));
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(passwordsDiffer: false, address: address.trim()));
|
||||
await _run(() async {
|
||||
final pending = await _passwords.resetPassword(
|
||||
address: address,
|
||||
phrase: phrase,
|
||||
newPassword: password,
|
||||
totpCode: state.recoveryCode ? null : code,
|
||||
recoveryCode: state.recoveryCode ? code : null,
|
||||
);
|
||||
await _passwords.enter(pending);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(step: AccountRecoveryStep.done));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The address to reset the account of: a code is mailed to it.
|
||||
Future<void> startAddressEntered(String address) => _run(() async {
|
||||
_request = await _passwords.sendCode(address, language: _language);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: AccountRecoveryStep.startCode,
|
||||
address: address.trim(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// Mails a new code.
|
||||
Future<void> mailCodeResent() => _run(() async {
|
||||
_request = await _passwords.sendCode(state.address, language: _language);
|
||||
});
|
||||
|
||||
/// The code: the reset is asked for.
|
||||
Future<void> startCodeEntered(String code) => _run(() async {
|
||||
final at = await _passwords.startAccountReset(
|
||||
address: state.address,
|
||||
language: _language,
|
||||
request: _request!,
|
||||
code: code,
|
||||
);
|
||||
emit(state.copyWith(step: AccountRecoveryStep.started, resetAt: at));
|
||||
});
|
||||
|
||||
Future<void> _run(Future<void> Function() step) async {
|
||||
emit(state.copyWith(busy: true, clearProblem: true));
|
||||
try {
|
||||
await step();
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false, problem: failed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_security.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/password/model/phrase_check.dart';
|
||||
|
||||
/// What the account section shows of what keeps the signed-in person's
|
||||
/// account: loading, the account's security, or that it couldn't be read.
|
||||
final class const AccountSecurityState({
|
||||
final AccountSecurity? security,
|
||||
final bool failed = false,
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [security, failed];
|
||||
}
|
||||
|
||||
/// Reads what keeps the signed-in person's account ([load] again after a
|
||||
/// change).
|
||||
class AccountSecurityCubit(final PasswordRepository _passwords)
|
||||
extends Cubit<AccountSecurityState> {
|
||||
this : super(const AccountSecurityState());
|
||||
|
||||
/// Reads it anew.
|
||||
Future<void> load() async {
|
||||
try {
|
||||
final security = await _passwords.security();
|
||||
if (!isClosed) {
|
||||
emit(AccountSecurityState(security: security));
|
||||
}
|
||||
} on PasswordFailed {
|
||||
if (!isClosed) {
|
||||
emit(AccountSecurityState(security: state.security, failed: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a one-step sheet of the account section shows: busy, a refusal, or
|
||||
/// its result.
|
||||
final class const SecurityStepState<T>({
|
||||
final bool busy = false,
|
||||
final PasswordFailed? problem,
|
||||
final T? result,
|
||||
|
||||
/// Whether a field's two entries differ (a new password typed twice).
|
||||
final bool mismatch = false,
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [busy, problem, result, mismatch];
|
||||
}
|
||||
|
||||
/// A step of the account section ([run]), busy meanwhile, its refusal or
|
||||
/// result shown.
|
||||
class SecurityStepCubit<T>() extends Cubit<SecurityStepState<T>> {
|
||||
this : super(SecurityStepState<T>());
|
||||
|
||||
/// Runs [step]; true once it went through.
|
||||
Future<bool> run(Future<T> Function() step) async {
|
||||
emit(SecurityStepState<T>(busy: true, result: state.result));
|
||||
try {
|
||||
final result = await step();
|
||||
if (!isClosed) {
|
||||
emit(SecurityStepState<T>(result: result));
|
||||
}
|
||||
return true;
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (!isClosed) {
|
||||
emit(SecurityStepState<T>(problem: failed, result: state.result));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Says the two entries of a field differ.
|
||||
void mismatched() => emit(SecurityStepState<T>(mismatch: true));
|
||||
}
|
||||
|
||||
/// Where adding an authenticator stands.
|
||||
enum AddAuthenticatorStep() {
|
||||
/// Enrolling it.
|
||||
enrolling,
|
||||
|
||||
/// Its QR code and its code.
|
||||
code,
|
||||
|
||||
/// The recovery codes the account's first brought.
|
||||
recoveryCodes,
|
||||
|
||||
/// Added.
|
||||
done,
|
||||
}
|
||||
|
||||
/// What [AddAuthenticatorCubit] shows.
|
||||
final class const AddAuthenticatorState({
|
||||
final AddAuthenticatorStep step = AddAuthenticatorStep.enrolling,
|
||||
final bool busy = false,
|
||||
final PasswordFailed? problem,
|
||||
final AuthenticatorSetup? setup,
|
||||
final List<String> recoveryCodes = const [],
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [step, busy, problem, setup, recoveryCodes];
|
||||
}
|
||||
|
||||
/// Adds an authenticator to the signed-in person's account: enrolled as it
|
||||
/// opens, confirmed with its code (and, once one is confirmed, a code from
|
||||
/// one of those: [needsExistingCode]), the first one's recovery codes shown.
|
||||
class AddAuthenticatorCubit(
|
||||
final PasswordRepository _passwords, {
|
||||
required final String _label,
|
||||
required final String _name,
|
||||
required final bool needsExistingCode,
|
||||
}) extends Cubit<AddAuthenticatorState> {
|
||||
this : super(const AddAuthenticatorState());
|
||||
|
||||
/// Enrols it.
|
||||
Future<void> start() async {
|
||||
emit(const AddAuthenticatorState(busy: true));
|
||||
try {
|
||||
final setup = await _passwords.enrol(name: _name, label: _label);
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
AddAuthenticatorState(step: AddAuthenticatorStep.code, setup: setup),
|
||||
);
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (!isClosed) {
|
||||
emit(AddAuthenticatorState(problem: failed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirms it with its [code] and, where needed, [existingCode].
|
||||
Future<void> codeEntered(String code, {String? existingCode}) async {
|
||||
final setup = state.setup!;
|
||||
emit(
|
||||
AddAuthenticatorState(
|
||||
step: AddAuthenticatorStep.code,
|
||||
busy: true,
|
||||
setup: setup,
|
||||
),
|
||||
);
|
||||
try {
|
||||
final codes = await _passwords.confirm(
|
||||
setup.id,
|
||||
code,
|
||||
existingCode: needsExistingCode ? existingCode : null,
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
AddAuthenticatorState(
|
||||
step: codes.isEmpty
|
||||
? AddAuthenticatorStep.done
|
||||
: AddAuthenticatorStep.recoveryCodes,
|
||||
setup: setup,
|
||||
recoveryCodes: codes,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
AddAuthenticatorState(
|
||||
step: AddAuthenticatorStep.code,
|
||||
setup: setup,
|
||||
problem: failed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where making a new recovery phrase stands.
|
||||
enum NewPhraseStep() {
|
||||
/// Making it.
|
||||
making,
|
||||
|
||||
/// Shown, to write down.
|
||||
shown,
|
||||
|
||||
/// Three words typed back, and a code.
|
||||
check,
|
||||
|
||||
/// Kept: the old one opens nothing any more.
|
||||
done,
|
||||
}
|
||||
|
||||
/// What [NewPhraseCubit] shows.
|
||||
final class const NewPhraseState({
|
||||
final NewPhraseStep step = NewPhraseStep.making,
|
||||
final bool busy = false,
|
||||
final PasswordFailed? problem,
|
||||
final bool phraseWrong = false,
|
||||
final String phrase = '',
|
||||
final List<int> checked = const [],
|
||||
}) with Equatable {
|
||||
/// Never the phrase.
|
||||
@override
|
||||
String toString() => 'NewPhraseState($step, busy: $busy, $problem)';
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
step,
|
||||
busy,
|
||||
problem,
|
||||
phraseWrong,
|
||||
phrase,
|
||||
checked,
|
||||
];
|
||||
}
|
||||
|
||||
/// A new recovery phrase for the signed-in person's keys (on this device):
|
||||
/// made, shown, three of its words typed back, then kept by the instance
|
||||
/// for a code from the authenticator.
|
||||
class NewPhraseCubit(final PasswordRepository _passwords)
|
||||
extends Cubit<NewPhraseState> {
|
||||
this : super(const NewPhraseState());
|
||||
|
||||
NewPhrase? _made;
|
||||
|
||||
/// Makes it.
|
||||
Future<void> start() async {
|
||||
emit(const NewPhraseState(busy: true));
|
||||
try {
|
||||
final made = _made = await _passwords.newPhrase();
|
||||
if (!isClosed) {
|
||||
emit(NewPhraseState(step: NewPhraseStep.shown, phrase: made.phrase));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (!isClosed) {
|
||||
emit(NewPhraseState(problem: failed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Written down: three words to type back.
|
||||
void writtenDown() => emit(
|
||||
NewPhraseState(
|
||||
step: NewPhraseStep.check,
|
||||
phrase: state.phrase,
|
||||
checked: wordsToCheck(wordsOf(state.phrase).length),
|
||||
),
|
||||
);
|
||||
|
||||
/// Back to the phrase.
|
||||
void shownAgain() =>
|
||||
emit(NewPhraseState(step: NewPhraseStep.shown, phrase: state.phrase));
|
||||
|
||||
/// The words typed back, and a [code]: the instance keeps it.
|
||||
Future<void> checked(List<String> words, String code) async {
|
||||
if (!checksPhrase(state.phrase, state.checked, words)) {
|
||||
emit(_again(phraseWrong: true));
|
||||
return;
|
||||
}
|
||||
emit(_again(busy: true));
|
||||
try {
|
||||
await _passwords.keepPhrase(_made!, code);
|
||||
if (!isClosed) {
|
||||
emit(NewPhraseState(step: NewPhraseStep.done, phrase: state.phrase));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (!isClosed) {
|
||||
emit(_again(problem: failed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NewPhraseState _again({
|
||||
bool busy = false,
|
||||
bool phraseWrong = false,
|
||||
PasswordFailed? problem,
|
||||
}) => NewPhraseState(
|
||||
step: NewPhraseStep.check,
|
||||
busy: busy,
|
||||
problem: problem,
|
||||
phraseWrong: phraseWrong,
|
||||
phrase: state.phrase,
|
||||
checked: state.checked,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/data/account_crypto.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/password/model/phrase_check.dart';
|
||||
|
||||
/// What an account's setup is for.
|
||||
enum AccountSetupMode() {
|
||||
/// A new account, from a device signed out (or an account with an address
|
||||
/// and no password yet): put in place once its authenticator is set up.
|
||||
signUp,
|
||||
|
||||
/// A password for the signed-in person (anonymous, or signing in with
|
||||
/// GitHub or codes by mail): they keep everything.
|
||||
setPassword,
|
||||
|
||||
/// The reset of a whole account, once due: a new password, and new keys
|
||||
/// unless the recovery phrase keeps the old.
|
||||
finishReset,
|
||||
}
|
||||
|
||||
/// Where an account's setup stands.
|
||||
enum AccountSetupStep() {
|
||||
/// The email address.
|
||||
address,
|
||||
|
||||
/// The password, twice.
|
||||
password,
|
||||
|
||||
/// The recovery phrase, to write down.
|
||||
phrase,
|
||||
|
||||
/// Three of its words typed back.
|
||||
phraseCheck,
|
||||
|
||||
/// The code mailed to the address.
|
||||
mailCode,
|
||||
|
||||
/// The authenticator.
|
||||
authenticator,
|
||||
|
||||
/// The recovery codes it brought.
|
||||
recoveryCodes,
|
||||
|
||||
/// Done.
|
||||
done,
|
||||
}
|
||||
|
||||
/// What [AccountSetupCubit] shows.
|
||||
final class const AccountSetupState({
|
||||
required final AccountSetupStep step,
|
||||
final bool busy = false,
|
||||
final PasswordFailed? problem,
|
||||
final bool passwordsDiffer = false,
|
||||
final bool phraseWrong = false,
|
||||
final String address = '',
|
||||
final String phrase = '',
|
||||
final List<int> checked = const [],
|
||||
final AuthenticatorSetup? authenticator,
|
||||
final List<String> recoveryCodes = const [],
|
||||
}) with Equatable {
|
||||
AccountSetupState copyWith({
|
||||
AccountSetupStep? step,
|
||||
bool? busy,
|
||||
PasswordFailed? problem,
|
||||
bool clearProblem = false,
|
||||
bool? passwordsDiffer,
|
||||
bool? phraseWrong,
|
||||
String? address,
|
||||
String? phrase,
|
||||
List<int>? checked,
|
||||
AuthenticatorSetup? authenticator,
|
||||
List<String>? recoveryCodes,
|
||||
}) => AccountSetupState(
|
||||
step: step ?? this.step,
|
||||
busy: busy ?? this.busy,
|
||||
problem: clearProblem ? null : problem ?? this.problem,
|
||||
passwordsDiffer: passwordsDiffer ?? this.passwordsDiffer,
|
||||
phraseWrong: phraseWrong ?? this.phraseWrong,
|
||||
address: address ?? this.address,
|
||||
phrase: phrase ?? this.phrase,
|
||||
checked: checked ?? this.checked,
|
||||
authenticator: authenticator ?? this.authenticator,
|
||||
recoveryCodes: recoveryCodes ?? this.recoveryCodes,
|
||||
);
|
||||
|
||||
/// Never the phrase.
|
||||
@override
|
||||
String toString() => 'AccountSetupState($step, busy: $busy, $problem)';
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
step,
|
||||
busy,
|
||||
problem,
|
||||
passwordsDiffer,
|
||||
phraseWrong,
|
||||
address,
|
||||
phrase,
|
||||
checked,
|
||||
authenticator,
|
||||
recoveryCodes,
|
||||
];
|
||||
}
|
||||
|
||||
/// Setting up an account's password (ADR 0017): the address (where it is
|
||||
/// needed), the password twice, the recovery phrase shown once with three
|
||||
/// of its words typed back, the code mailed to the address, then the
|
||||
/// authenticator and the recovery codes it brings. Nothing reaches the
|
||||
/// instance before the code: the phrase is checked first, so no account
|
||||
/// exists whose phrase was never written down. [_mode] says what for; a
|
||||
/// reset that keeps the keys takes the recovery phrase [_phrase], and shows
|
||||
/// none.
|
||||
class AccountSetupCubit(
|
||||
final PasswordRepository _passwords, {
|
||||
required final AccountSetupMode _mode,
|
||||
required final String _language,
|
||||
|
||||
/// The address, where it is known already (a reset's, or the signed-in
|
||||
/// person's own); null where the setup asks for it.
|
||||
final String? address,
|
||||
|
||||
/// Whether the signed-in person has an address already, so no code is
|
||||
/// needed ([AccountSetupMode.setPassword]).
|
||||
final bool _hasAddress = false,
|
||||
final String? _phrase,
|
||||
}) extends Cubit<AccountSetupState> {
|
||||
this
|
||||
: super(
|
||||
AccountSetupState(
|
||||
step: address == null && !_hasAddress
|
||||
? AccountSetupStep.address
|
||||
: AccountSetupStep.password,
|
||||
address: address ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
NewAccountMaterial? _material;
|
||||
String? _request;
|
||||
PendingSignIn? _pending;
|
||||
|
||||
/// Whether the steps end in a session to put in place.
|
||||
bool get _entersSession => _mode != AccountSetupMode.setPassword;
|
||||
|
||||
/// The address to set up the account with.
|
||||
void addressEntered(String address) {
|
||||
final typed = address.trim();
|
||||
if (!typed.contains('@')) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
problem: const PasswordFailed(PasswordProblem.notAnAddress),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: AccountSetupStep.password,
|
||||
address: typed,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The password, typed twice: made into keys and a recovery phrase on the
|
||||
/// device (or, keeping the keys, wrapped with the phrase given).
|
||||
Future<void> passwordEntered(String password, String again) async {
|
||||
if (password != again) {
|
||||
emit(state.copyWith(passwordsDiffer: true, clearProblem: true));
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(passwordsDiffer: false));
|
||||
await _run(() async {
|
||||
final phrase = _phrase;
|
||||
final material = phrase == null
|
||||
? await _passwords.makePassword(password)
|
||||
: await _passwords.keepKeys(
|
||||
address: state.address,
|
||||
phrase: phrase,
|
||||
newPassword: password,
|
||||
);
|
||||
_material = material;
|
||||
if (phrase == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: AccountSetupStep.phrase,
|
||||
phrase: material.phrase,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await _toCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The phrase is written down: three of its words to type back.
|
||||
void phraseWrittenDown() => emit(
|
||||
state.copyWith(
|
||||
step: AccountSetupStep.phraseCheck,
|
||||
checked: wordsToCheck(wordsOf(state.phrase).length),
|
||||
phraseWrong: false,
|
||||
),
|
||||
);
|
||||
|
||||
/// Back to the phrase.
|
||||
void phraseShownAgain() => emit(
|
||||
state.copyWith(step: AccountSetupStep.phrase, phraseWrong: false),
|
||||
);
|
||||
|
||||
/// The words typed back.
|
||||
Future<void> phraseChecked(List<String> words) async {
|
||||
if (!checksPhrase(state.phrase, state.checked, words)) {
|
||||
emit(state.copyWith(phraseWrong: true));
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(phraseWrong: false));
|
||||
await _run(_toCode);
|
||||
}
|
||||
|
||||
/// The code mailed to the address, where one is needed; else the setup
|
||||
/// goes to the instance at once.
|
||||
Future<void> _toCode() async {
|
||||
if (_mode == AccountSetupMode.setPassword && _hasAddress) {
|
||||
await _submit();
|
||||
return;
|
||||
}
|
||||
_request = await _passwords.sendCode(state.address, language: _language);
|
||||
emit(state.copyWith(step: AccountSetupStep.mailCode, clearProblem: true));
|
||||
}
|
||||
|
||||
/// Mails a new code.
|
||||
Future<void> mailCodeResent() => _run(() async {
|
||||
_request = await _passwords.sendCode(state.address, language: _language);
|
||||
});
|
||||
|
||||
/// The code mailed: the setup goes to the instance.
|
||||
Future<void> mailCodeEntered(String code) => _run(() => _submit(code));
|
||||
|
||||
Future<void> _submit([String? code]) async {
|
||||
final material = _material!;
|
||||
switch (_mode) {
|
||||
case AccountSetupMode.signUp:
|
||||
_pending = await _passwords.signUp(
|
||||
material,
|
||||
address: state.address,
|
||||
request: _request!,
|
||||
code: code!,
|
||||
);
|
||||
case AccountSetupMode.finishReset:
|
||||
_pending = await _passwords.completeAccountReset(
|
||||
material,
|
||||
address: state.address,
|
||||
request: _request!,
|
||||
code: code!,
|
||||
);
|
||||
case AccountSetupMode.setPassword:
|
||||
await _passwords.setPassword(
|
||||
material,
|
||||
address: _hasAddress ? null : state.address,
|
||||
request: _request,
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
final setup = await _passwords.enrol(
|
||||
name: _authenticatorName,
|
||||
label: state.address,
|
||||
pending: _pending,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: AccountSetupStep.authenticator,
|
||||
authenticator: setup,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static const _authenticatorName = 'Authenticator';
|
||||
|
||||
/// Confirms the authenticator with its [code].
|
||||
Future<void> authenticatorCodeEntered(String code) => _run(() async {
|
||||
final codes = await _passwords.confirm(
|
||||
state.authenticator!.id,
|
||||
code,
|
||||
pending: _pending,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: AccountSetupStep.recoveryCodes,
|
||||
recoveryCodes: codes,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// The recovery codes are kept: done, and signed in where the setup
|
||||
/// signs in.
|
||||
Future<void> recoveryCodesKept() => _run(() async {
|
||||
if (_entersSession) {
|
||||
await _passwords.enter(_pending!);
|
||||
}
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(step: AccountSetupStep.done));
|
||||
}
|
||||
});
|
||||
|
||||
/// Runs [step], busy meanwhile; a refusal is shown on the step it came
|
||||
/// from, or, for the address's own refusals, on the address.
|
||||
Future<void> _run(Future<void> Function() step) async {
|
||||
emit(state.copyWith(busy: true, clearProblem: true));
|
||||
try {
|
||||
await step();
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
final aboutAddress = switch (failed.problem) {
|
||||
PasswordProblem.notAnAddress ||
|
||||
PasswordProblem.addressTaken ||
|
||||
PasswordProblem.passwordExists ||
|
||||
PasswordProblem.otherAddressLinked =>
|
||||
_mode != AccountSetupMode.finishReset,
|
||||
_ => false,
|
||||
};
|
||||
emit(
|
||||
state.copyWith(
|
||||
busy: false,
|
||||
problem: failed,
|
||||
step: aboutAddress ? AccountSetupStep.address : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
|
||||
/// Where signing in with a password stands.
|
||||
enum PasswordSignInStep() {
|
||||
/// The address and the password.
|
||||
credentials,
|
||||
|
||||
/// A code from the authenticator, or a recovery code.
|
||||
secondFactor,
|
||||
|
||||
/// The account was never finished: a code by mail signs in.
|
||||
mailCode,
|
||||
|
||||
/// The authenticator is lost: a reset by a code by mail or the recovery
|
||||
/// phrase, or signing in without a code once one took effect.
|
||||
lostAuthenticator,
|
||||
|
||||
/// A code by mail for the reset of the authenticators.
|
||||
resetMailCode,
|
||||
|
||||
/// The recovery phrase for the reset of the authenticators.
|
||||
resetPhrase,
|
||||
|
||||
/// The reset was asked for: it takes effect at [PasswordSignInState.resetAt].
|
||||
resetRequested,
|
||||
|
||||
/// An authenticator to set up before the sign-in is put in place.
|
||||
authenticator,
|
||||
|
||||
/// The recovery codes the first authenticator brought, shown once.
|
||||
recoveryCodes,
|
||||
|
||||
/// Signed in.
|
||||
done,
|
||||
}
|
||||
|
||||
/// What [PasswordSignInCubit] shows.
|
||||
final class const PasswordSignInState({
|
||||
final PasswordSignInStep step = PasswordSignInStep.credentials,
|
||||
final bool busy = false,
|
||||
final PasswordFailed? problem,
|
||||
final String address = '',
|
||||
|
||||
/// Whether the second factor is a recovery code rather than a TOTP code.
|
||||
final bool recoveryCode = false,
|
||||
final AuthenticatorSetup? authenticator,
|
||||
final List<String> recoveryCodes = const [],
|
||||
final DateTime? resetAt,
|
||||
}) with Equatable {
|
||||
PasswordSignInState copyWith({
|
||||
PasswordSignInStep? step,
|
||||
bool? busy,
|
||||
PasswordFailed? problem,
|
||||
bool clearProblem = false,
|
||||
String? address,
|
||||
bool? recoveryCode,
|
||||
AuthenticatorSetup? authenticator,
|
||||
List<String>? recoveryCodes,
|
||||
DateTime? resetAt,
|
||||
}) => PasswordSignInState(
|
||||
step: step ?? this.step,
|
||||
busy: busy ?? this.busy,
|
||||
problem: clearProblem ? null : problem ?? this.problem,
|
||||
address: address ?? this.address,
|
||||
recoveryCode: recoveryCode ?? this.recoveryCode,
|
||||
authenticator: authenticator ?? this.authenticator,
|
||||
recoveryCodes: recoveryCodes ?? this.recoveryCodes,
|
||||
resetAt: resetAt ?? this.resetAt,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
step,
|
||||
busy,
|
||||
problem,
|
||||
address,
|
||||
recoveryCode,
|
||||
authenticator,
|
||||
recoveryCodes,
|
||||
resetAt,
|
||||
];
|
||||
}
|
||||
|
||||
/// Signing in with an address, a password and a code from an authenticator
|
||||
/// (ADR 0017), from a device signed out: also an account never finished
|
||||
/// (a code by mail, then an authenticator), a lost authenticator (asking
|
||||
/// for its reset, signing in once it took effect), and an authenticator
|
||||
/// to set up before the sign-in is put in place.
|
||||
class PasswordSignInCubit(
|
||||
final PasswordRepository _passwords, {
|
||||
required final String _language,
|
||||
}) extends Cubit<PasswordSignInState> {
|
||||
this : super(const PasswordSignInState());
|
||||
|
||||
PasswordUnlocked? _unlocked;
|
||||
PendingSignIn? _pending;
|
||||
String? _request;
|
||||
|
||||
/// Unlocks [password] for [address]: Argon2id, about a second. The
|
||||
/// instance answers alike for an address nobody has, so nothing is known
|
||||
/// until the code.
|
||||
Future<void> credentialsEntered(String address, String password) =>
|
||||
_run(() async {
|
||||
_unlocked = await _passwords.unlock(address, password);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: PasswordSignInStep.secondFactor,
|
||||
address: address.trim(),
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// Switches between a code from the authenticator and a recovery code.
|
||||
void recoveryCodeChosen({required bool chosen}) =>
|
||||
emit(state.copyWith(recoveryCode: chosen, clearProblem: true));
|
||||
|
||||
/// Signs in with [code], a TOTP code or a recovery code as chosen.
|
||||
Future<void> codeEntered(String code) => _signIn(
|
||||
totpCode: state.recoveryCode ? null : code,
|
||||
recoveryCode: state.recoveryCode ? code : null,
|
||||
);
|
||||
|
||||
/// Signs in with a code by mail, for an account never finished.
|
||||
Future<void> mailCodeEntered(String code) =>
|
||||
_signIn(request: _request, code: code);
|
||||
|
||||
/// Mails a new code for the step shown.
|
||||
Future<void> mailCodeResent() => _run(() async {
|
||||
_request = await _passwords.sendCode(state.address, language: _language);
|
||||
emit(state.copyWith(clearProblem: true));
|
||||
});
|
||||
|
||||
/// Back to the address and password.
|
||||
void backToCredentials() => emit(
|
||||
state.copyWith(step: PasswordSignInStep.credentials, clearProblem: true),
|
||||
);
|
||||
|
||||
/// The authenticator is lost.
|
||||
void authenticatorLost() => emit(
|
||||
state.copyWith(
|
||||
step: PasswordSignInStep.lostAuthenticator,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
|
||||
/// Back to the code.
|
||||
void backToCode() => emit(
|
||||
state.copyWith(step: PasswordSignInStep.secondFactor, clearProblem: true),
|
||||
);
|
||||
|
||||
/// Asks for the reset of the authenticators with a code by mail: the code
|
||||
/// first.
|
||||
Future<void> resetByMailChosen() => _run(() async {
|
||||
_request = await _passwords.sendCode(state.address, language: _language);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: PasswordSignInStep.resetMailCode,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// Asks for it with the recovery phrase.
|
||||
void resetByPhraseChosen() => emit(
|
||||
state.copyWith(step: PasswordSignInStep.resetPhrase, clearProblem: true),
|
||||
);
|
||||
|
||||
/// The reset asked for with [code] by mail.
|
||||
Future<void> resetMailCodeEntered(String code) => _run(() async {
|
||||
final at = await _passwords.resetAuthenticator(
|
||||
_unlocked!,
|
||||
language: _language,
|
||||
request: _request,
|
||||
code: code,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: PasswordSignInStep.resetRequested,
|
||||
resetAt: at,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// The reset asked for with the recovery [phrase].
|
||||
Future<void> resetPhraseEntered(String phrase) => _run(() async {
|
||||
final at = await _passwords.resetAuthenticator(
|
||||
_unlocked!,
|
||||
language: _language,
|
||||
phrase: phrase,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: PasswordSignInStep.resetRequested,
|
||||
resetAt: at,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// Signs in without a code: once a reset of the authenticators took
|
||||
/// effect, the password alone does, until one is set up.
|
||||
Future<void> withoutCodeChosen() => _signIn();
|
||||
|
||||
/// Confirms the new authenticator with its [code].
|
||||
Future<void> authenticatorCodeEntered(String code) => _run(() async {
|
||||
final codes = await _passwords.confirm(
|
||||
state.authenticator!.id,
|
||||
code,
|
||||
pending: _pending,
|
||||
);
|
||||
if (codes.isEmpty) {
|
||||
await _enter();
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: PasswordSignInStep.recoveryCodes,
|
||||
recoveryCodes: codes,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// The recovery codes are kept: signed in.
|
||||
Future<void> recoveryCodesKept() => _run(_enter);
|
||||
|
||||
Future<void> _signIn({
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
}) => _run(() async {
|
||||
final PendingSignIn pending;
|
||||
try {
|
||||
pending = await _passwords.signIn(
|
||||
_unlocked!,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
);
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (failed.problem != PasswordProblem.setupIncomplete) {
|
||||
rethrow;
|
||||
}
|
||||
// The password is right, but the account was never finished: a
|
||||
// code by mail signs in.
|
||||
_request = await _passwords.sendCode(
|
||||
state.address,
|
||||
language: _language,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(step: PasswordSignInStep.mailCode, clearProblem: true),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_pending = pending;
|
||||
if (!pending.needsAuthenticator) {
|
||||
await _enter();
|
||||
return;
|
||||
}
|
||||
final setup = await _passwords.enrol(
|
||||
name: _authenticatorName,
|
||||
label: pending.address,
|
||||
pending: pending,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
step: PasswordSignInStep.authenticator,
|
||||
authenticator: setup,
|
||||
clearProblem: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// What a new authenticator is called until its owner renames it.
|
||||
static const _authenticatorName = 'Authenticator';
|
||||
|
||||
Future<void> _enter() async {
|
||||
await _passwords.enter(_pending!);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(step: PasswordSignInStep.done, clearProblem: true));
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs [step], busy meanwhile; a refusal is shown on the step it came
|
||||
/// from.
|
||||
Future<void> _run(Future<void> Function() step) async {
|
||||
emit(state.copyWith(busy: true, clearProblem: true));
|
||||
try {
|
||||
await step();
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false, problem: failed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
packages/halkyon_app/lib/src/password/data/account_crypto.dart
Normal file
131
packages/halkyon_app/lib/src/password/data/account_crypto.dart
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
|
||||
/// 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.
|
||||
final class const PasswordUnlock({
|
||||
/// The salt and costs they were derived with.
|
||||
required final Uint8List salt,
|
||||
required final protocol.PasswordCosts costs,
|
||||
|
||||
/// SRP's x, 32 bytes, big-endian.
|
||||
required final Uint8List srpSecret,
|
||||
|
||||
/// The key-encryption key, 32 bytes.
|
||||
required final Uint8List keyEncryptionKey,
|
||||
}) {
|
||||
/// Whether these were derived for [salt] and [costs].
|
||||
bool fits(Uint8List salt, protocol.PasswordCosts costs) =>
|
||||
_same(this.salt, salt) &&
|
||||
this.costs.algorithm == costs.algorithm &&
|
||||
this.costs.opsLimit == costs.opsLimit &&
|
||||
this.costs.memLimit == costs.memLimit;
|
||||
|
||||
static bool _same(Uint8List a, Uint8List b) {
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// A new account's password as the device made it: the setup it sends, the
|
||||
/// recovery phrase it shows once, the keys it keeps unlocked, and what the
|
||||
/// password unlocks.
|
||||
final class const NewAccountMaterial({
|
||||
required final protocol.PasswordSetup setup,
|
||||
required final String phrase,
|
||||
required final UnlockedAccountKeys keys,
|
||||
}) {
|
||||
/// Never the phrase or the keys.
|
||||
@override
|
||||
String toString() => 'NewAccountMaterial(…)';
|
||||
}
|
||||
|
||||
/// The device's answer to SRP's first step: A and M1 for the register, and
|
||||
/// whether the register's M2 is the one that proves it holds the verifier.
|
||||
final class const SrpAnswer({
|
||||
required final ByteData clientValue,
|
||||
required final ByteData clientProof,
|
||||
required final bool Function(ByteData serverProof) _verifies,
|
||||
}) {
|
||||
/// Whether [serverProof] is the M2 this answer expects (in constant time).
|
||||
bool verifiesServer(ByteData serverProof) => _verifies(serverProof);
|
||||
}
|
||||
|
||||
/// The account's cryptography on the device (ADR 0017), behind an
|
||||
/// interface: libsodium (halkyon_keys) and SRP's client half
|
||||
/// (halkyon_base_client) in the app, each heavy step off the UI's isolate;
|
||||
/// a fake in tests. Every refusal is a `PasswordFailed`.
|
||||
abstract interface class AccountCrypto() {
|
||||
/// A new account's password: new key pairs, a new recovery phrase, and
|
||||
/// the setup that wraps the keys under both.
|
||||
Future<NewAccountMaterial> newAccount(String password);
|
||||
|
||||
/// A new password for [keys] (and the recovery phrase [phrase], which
|
||||
/// stays), as a whole setup: an account reset that keeps the keys.
|
||||
Future<NewAccountMaterial> keepingKeys(
|
||||
String password,
|
||||
UnlockedAccountKeys keys, {
|
||||
required String phrase,
|
||||
});
|
||||
|
||||
/// What [password] unlocks for an account with [salt] and [costs]:
|
||||
/// Argon2id, about a second on a phone.
|
||||
Future<PasswordUnlock> unlock(
|
||||
String password, {
|
||||
required Uint8List salt,
|
||||
required protocol.PasswordCosts costs,
|
||||
});
|
||||
|
||||
/// The answer to [challenge] (SRP's B and the account's salt) with what
|
||||
/// [unlock] holds.
|
||||
Future<SrpAnswer> answer(
|
||||
PasswordUnlock unlock,
|
||||
protocol.PasswordChallenge challenge,
|
||||
);
|
||||
|
||||
/// The account's keys [wrapped] under the password, opened with
|
||||
/// [unlock]'s key and checked against its public keys.
|
||||
Future<UnlockedAccountKeys> openKeys(
|
||||
PasswordUnlock unlock,
|
||||
protocol.WrappedKeys wrapped, {
|
||||
required Uint8List encryptionPublicKey,
|
||||
required Uint8List signingPublicKey,
|
||||
});
|
||||
|
||||
/// A new password for [keys], which stay: the salt, costs, verifier and
|
||||
/// the keys wrapped under its key.
|
||||
Future<protocol.NewPassword> newPassword(
|
||||
String password,
|
||||
UnlockedAccountKeys keys,
|
||||
);
|
||||
|
||||
/// The account's keys [wrapped] under the recovery phrase [phrase],
|
||||
/// opened and checked against its public keys.
|
||||
Future<UnlockedAccountKeys> openWithPhrase(
|
||||
String phrase,
|
||||
protocol.WrappedKeys wrapped, {
|
||||
required Uint8List encryptionPublicKey,
|
||||
required Uint8List signingPublicKey,
|
||||
});
|
||||
|
||||
/// [keys]' signature proving the recovery phrase for [challenge].
|
||||
Future<ByteData> signKeyProof(
|
||||
UnlockedAccountKeys keys,
|
||||
protocol.KeyProofChallenge challenge,
|
||||
);
|
||||
|
||||
/// A new recovery phrase for [keys], and the keys wrapped under it.
|
||||
Future<({String phrase, protocol.WrappedKeys recoveryKeys})> newPhrase(
|
||||
UnlockedAccountKeys keys,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
||||
|
||||
/// Where the device keeps the account's private keys once unlocked (ADR
|
||||
/// 0017): in the system's keyring, as sessions are, one person at a time
|
||||
/// per instance, gone once they sign out or another person signs in. Tests
|
||||
/// keep them in memory.
|
||||
abstract interface class AccountKeyStore() {
|
||||
/// Keeps [keys] as [person]'s.
|
||||
Future<void> keep(String person, UnlockedAccountKeys keys);
|
||||
|
||||
/// [person]'s keys; null where none are kept.
|
||||
Future<UnlockedAccountKeys?> read(String person);
|
||||
|
||||
/// Forgets every person's keys but [person]'s (everyone's with null).
|
||||
Future<void> forgetAllBut(String? person);
|
||||
}
|
||||
|
||||
/// [AccountKeyStore] in the system's keyring (flutter_secure_storage:
|
||||
/// Android's Keystore, iOS's and macOS's Keychain, Linux's Secret Service,
|
||||
/// Windows' Credential Manager), each person's under [_prefix] and their
|
||||
/// ID, the instance's own prefix (`InstanceStorage.keysOf`).
|
||||
class SecureAccountKeyStore(
|
||||
final String _prefix, {
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage(),
|
||||
}) implements AccountKeyStore {
|
||||
String _keyOf(String person) => '$_prefix:$person';
|
||||
|
||||
@override
|
||||
Future<void> keep(String person, UnlockedAccountKeys keys) =>
|
||||
_storage.write(key: _keyOf(person), value: jsonEncode(keys.toJson()));
|
||||
|
||||
@override
|
||||
Future<UnlockedAccountKeys?> read(String person) async {
|
||||
final kept = await _storage.read(key: _keyOf(person));
|
||||
if (kept == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return UnlockedAccountKeys.fromJson(
|
||||
jsonDecode(kept) as Map<String, Object?>,
|
||||
);
|
||||
} on Object catch (error) {
|
||||
// Its type only: never what was kept.
|
||||
log('Unreadable account keys: ${error.runtimeType}', name: 'Keys');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> forgetAllBut(String? person) async {
|
||||
final kept = await _storage.readAll();
|
||||
for (final key in kept.keys) {
|
||||
if (key.startsWith('$_prefix:') &&
|
||||
(person == null || key != _keyOf(person))) {
|
||||
await _storage.delete(key: key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
packages/halkyon_app/lib/src/password/data/password_gateway.dart
Normal file
152
packages/halkyon_app/lib/src/password/data/password_gateway.dart
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||
import 'package:halkyon_app/src/password/data/account_crypto.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
|
||||
/// A password sign-in's second step as the instance answered it: the
|
||||
/// session it grants, SRP's M2, and the account's keys as the register
|
||||
/// keeps them.
|
||||
final class const PasswordSignedIn({
|
||||
required final SessionGrant grant,
|
||||
required final protocol.PasswordSignedIn answer,
|
||||
});
|
||||
|
||||
/// How the signed-in person signs in, as the instance knows it.
|
||||
typedef PasswordWays = ({
|
||||
bool gitHub,
|
||||
bool email,
|
||||
bool password,
|
||||
bool authenticator,
|
||||
});
|
||||
|
||||
/// The boundary to the instance's password sign-in (ADR 0017): halkyon_base's
|
||||
/// endpoints, the steps without a session through a client holding none,
|
||||
/// the signed-in person's through the device's session or, where `as` is
|
||||
/// given, a session granted but not yet the device's (an authenticator set
|
||||
/// up before a new sign-in is put in place). Every refusal is a
|
||||
/// `PasswordFailed`. Tests replace it with a fake.
|
||||
abstract interface class PasswordGateway() {
|
||||
/// Whether the instance offers password sign-in, and until when GitHub
|
||||
/// sign-in lasts there (null for no end set).
|
||||
Future<({bool offered, DateTime? gitHubEnds})> offers();
|
||||
|
||||
/// Mails a code to [address] in [language]; the request it answers.
|
||||
Future<String> sendCode(String address, {required String language});
|
||||
|
||||
/// A sign-in's first step for [address].
|
||||
Future<protocol.PasswordChallenge> begin(String address);
|
||||
|
||||
/// A sign-in's second step: [answer] to [challenge], with a TOTP code, a
|
||||
/// recovery code, or a code mailed for [request] (a setup never finished).
|
||||
Future<PasswordSignedIn> complete(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
});
|
||||
|
||||
/// Signs up with the address a [code] mailed for [request] proves.
|
||||
Future<SessionGrant> signUp(
|
||||
protocol.PasswordSetup setup, {
|
||||
required String request,
|
||||
required String code,
|
||||
});
|
||||
|
||||
/// Gives the signed-in person the password [setup] describes, with the
|
||||
/// address a [code] mailed for [request] proves unless they have one.
|
||||
Future<void> setPassword(
|
||||
protocol.PasswordSetup setup, {
|
||||
String? request,
|
||||
String? code,
|
||||
});
|
||||
|
||||
/// A new authenticator called [name], naming the account [label].
|
||||
Future<AuthenticatorSetup> enrol({
|
||||
required String name,
|
||||
required String label,
|
||||
SessionGrant? as,
|
||||
});
|
||||
|
||||
/// Confirms authenticator [id] with its [code] (and [existingCode] from
|
||||
/// one confirmed before); the recovery codes the first brings.
|
||||
Future<List<String>> confirm(
|
||||
String id,
|
||||
String code, {
|
||||
String? existingCode,
|
||||
SessionGrant? as,
|
||||
});
|
||||
|
||||
/// The person's authenticators, the oldest first.
|
||||
Future<List<AccountAuthenticator>> authenticators();
|
||||
|
||||
/// Removes authenticator [id], a confirmed one with a [code].
|
||||
Future<void> remove(String id, {String? code});
|
||||
|
||||
/// New recovery codes for a [code].
|
||||
Future<List<String>> newRecoveryCodes(String code);
|
||||
|
||||
/// How the signed-in person signs in.
|
||||
Future<PasswordWays> ways();
|
||||
|
||||
/// A change of password's first step.
|
||||
Future<protocol.PasswordChallenge> beginChange();
|
||||
|
||||
/// Changes the password: [answer] to [challenge] proves the old one.
|
||||
Future<void> change(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required protocol.NewPassword password,
|
||||
});
|
||||
|
||||
/// A new recovery phrase's wrapping of the keys, for a [code].
|
||||
Future<void> setRecoveryKeys(String code, protocol.WrappedKeys keys);
|
||||
|
||||
/// Asks for the reset of a lost authenticator: [answer] to [challenge]
|
||||
/// proves the password, and a code mailed for [request] or the recovery
|
||||
/// phrase's [signature] for [keyChallenge] proves the rest; when it takes
|
||||
/// effect.
|
||||
Future<DateTime> resetAuthenticator(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
required String address,
|
||||
required String language,
|
||||
String? request,
|
||||
String? code,
|
||||
protocol.KeyProofChallenge? keyChallenge,
|
||||
ByteData? signature,
|
||||
});
|
||||
|
||||
/// The recovery phrase's first step for [address].
|
||||
Future<protocol.KeyProofChallenge> beginKeyProof(String address);
|
||||
|
||||
/// A new password with the recovery phrase's [signature] and a code.
|
||||
Future<SessionGrant> resetPassword(
|
||||
protocol.KeyProofChallenge challenge,
|
||||
ByteData signature, {
|
||||
required protocol.NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
});
|
||||
|
||||
/// Asks for the reset of the account at [address]; when it takes effect.
|
||||
Future<DateTime> startAccountReset({
|
||||
required String address,
|
||||
required String language,
|
||||
required String request,
|
||||
required String code,
|
||||
});
|
||||
|
||||
/// Carries out the reset of the account at [address] once due.
|
||||
Future<SessionGrant> completeAccountReset(
|
||||
protocol.PasswordSetup setup, {
|
||||
required String address,
|
||||
required String request,
|
||||
required String code,
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue