fix(app): every change of how an account is secured asks for the password
The app's side of the owner's proof (OwnerProof, the third reading): adding an authenticator once one is confirmed, removing a confirmed one and new recovery codes now ask for the current password beside the code, as changing the password, a new phrase and deleting already did; one challenge and one answer per sheet (OwnerAnswer). A wrong password or code reads as the one refusal the instance gives. Deleting chooses a code by mail from what the instance says (secondFactorWorks), no longer from whether an authenticator was ever confirmed: with the authenticators' reset taken effect but not carried out, the app offered a code field the instance refused (the fourth finding). Signed-in calls take no address any more, so a device that doesn't know it changes the password as any other; deleting by mail asks for the address only where the device doesn't know it, as the code goes there (the fifth finding). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
a9459bb875
commit
a0ae08f975
21 changed files with 468 additions and 315 deletions
|
|
@ -117,6 +117,7 @@
|
|||
"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.",
|
||||
"passwordAuthenticatorOwnerIntro": "Bestätige zum Hinzufügen, dass du es bist: mit deinem Passwort und einem Code von einem bisherigen Authenticator.",
|
||||
"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.",
|
||||
|
|
@ -161,7 +162,7 @@
|
|||
"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.",
|
||||
"passwordSectionRemoveIntro": "Gib zur Bestätigung dein Passwort und einen Code aus einem deiner Authenticators ein.",
|
||||
"passwordSectionRemoveAction": "Entfernen",
|
||||
"passwordSectionAddAuthenticator": "Authenticator hinzufügen",
|
||||
"passwordSectionRecoveryCodes": "Neue Wiederherstellungscodes",
|
||||
|
|
@ -172,7 +173,7 @@
|
|||
"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.",
|
||||
"passwordNewCodesIntro": "Gib dein Passwort und 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.",
|
||||
|
|
@ -186,7 +187,6 @@
|
|||
"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.",
|
||||
|
|
@ -212,8 +212,8 @@
|
|||
"passwordCodesWait": "Zu viele falsche Codes hintereinander: Bis {date} wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann.",
|
||||
"passwordDeleteIntro": "Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Gib zur Bestätigung dein Passwort und einen Code deiner Authenticator-App ein.",
|
||||
"passwordSessionEnded": "Zu viele falsche Codes hintereinander von diesem Gerät: Es wurde abgemeldet. Melde dich wieder mit deinem Passwort und einem Code an.",
|
||||
"passwordAddressUnknown": "Dieses Gerät kennt die Adresse deines Kontos nicht, die das braucht. Melde dich ab und wieder mit deiner Adresse und deinem Passwort an.",
|
||||
"passwordDeleteIntroByMail": "Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Seine Authenticator-App ist nicht eingerichtet, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken.",
|
||||
"passwordAddressUnknown": "Gib die E-Mail-Adresse deines Kontos ein: Dorthin geht der Code.",
|
||||
"passwordDeleteIntroByMail": "Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Keiner seiner Authenticators funktioniert gerade, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken.",
|
||||
"passwordDeleteSendCode": "Code per E-Mail schicken",
|
||||
"passwordChangedSignInAgain": "Dein Passwort wurde geändert, aber dieser Dienst konnte dich gerade noch nicht anmelden. Melde dich wieder mit dem neuen Passwort an."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@
|
|||
"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.",
|
||||
"passwordAuthenticatorOwnerIntro": "To add it, confirm it is you: your password and a code from an authenticator you added before.",
|
||||
"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.",
|
||||
|
|
@ -380,7 +381,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"passwordSectionRemoveIntro": "Enter a code from one of your authenticators to confirm.",
|
||||
"passwordSectionRemoveIntro": "Enter your password and a code from one of your authenticators to confirm.",
|
||||
"passwordSectionRemoveAction": "Remove",
|
||||
"passwordSectionAddAuthenticator": "Add an authenticator",
|
||||
"passwordSectionRecoveryCodes": "New recovery codes",
|
||||
|
|
@ -391,7 +392,7 @@
|
|||
"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.",
|
||||
"passwordNewCodesIntro": "Enter your password and 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.",
|
||||
|
|
@ -412,7 +413,6 @@
|
|||
"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.",
|
||||
|
|
@ -459,8 +459,8 @@
|
|||
},
|
||||
"passwordDeleteIntro": "Your account goes with everything in it, in every service of this instance. To confirm, type your password and a code from your authenticator app.",
|
||||
"passwordSessionEnded": "Too many wrong codes in a row from this device: it was signed out. Sign in again with your password and a code.",
|
||||
"passwordAddressUnknown": "This device doesn't know your account's address, which this takes. Sign out, then sign in again with your address and password.",
|
||||
"passwordDeleteIntroByMail": "Your account goes with everything in it, in every service of this instance. Its authenticator isn't set up, so a code by mail confirms it: type your password, then the code we email to your address.",
|
||||
"passwordAddressUnknown": "Enter your account's email address: the code goes there.",
|
||||
"passwordDeleteIntroByMail": "Your account goes with everything in it, in every service of this instance. None of its authenticators works now, so a code by mail confirms it: type your password, then the code we email to your address.",
|
||||
"passwordDeleteSendCode": "Email me a code",
|
||||
"passwordChangedSignInAgain": "Your password was changed, but this service couldn't sign you in just yet. Sign in again with the new password."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -805,6 +805,12 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Then enter the code it shows.'**
|
||||
String get passwordAuthenticatorCodeIntro;
|
||||
|
||||
/// No description provided for @passwordAuthenticatorOwnerIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To add it, confirm it is you: your password and a code from an authenticator you added before.'**
|
||||
String get passwordAuthenticatorOwnerIntro;
|
||||
|
||||
/// No description provided for @passwordExistingCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1072,7 +1078,7 @@ abstract class HalkyonAppLocalizations {
|
|||
/// No description provided for @passwordSectionRemoveIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter a code from one of your authenticators to confirm.'**
|
||||
/// **'Enter your password and a code from one of your authenticators to confirm.'**
|
||||
String get passwordSectionRemoveIntro;
|
||||
|
||||
/// No description provided for @passwordSectionRemoveAction.
|
||||
|
|
@ -1138,7 +1144,7 @@ abstract class HalkyonAppLocalizations {
|
|||
/// No description provided for @passwordNewCodesIntro.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter a code from your authenticator app. Your old recovery codes stop working.'**
|
||||
/// **'Enter your password and a code from your authenticator app. Your old recovery codes stop working.'**
|
||||
String get passwordNewCodesIntro;
|
||||
|
||||
/// No description provided for @passwordNewPhraseIntro.
|
||||
|
|
@ -1219,12 +1225,6 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'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:
|
||||
|
|
@ -1378,13 +1378,13 @@ abstract class HalkyonAppLocalizations {
|
|||
/// No description provided for @passwordAddressUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This device doesn\'t know your account\'s address, which this takes. Sign out, then sign in again with your address and password.'**
|
||||
/// **'Enter your account\'s email address: the code goes there.'**
|
||||
String get passwordAddressUnknown;
|
||||
|
||||
/// No description provided for @passwordDeleteIntroByMail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account goes with everything in it, in every service of this instance. Its authenticator isn\'t set up, so a code by mail confirms it: type your password, then the code we email to your address.'**
|
||||
/// **'Your account goes with everything in it, in every service of this instance. None of its authenticators works now, so a code by mail confirms it: type your password, then the code we email to your address.'**
|
||||
String get passwordDeleteIntroByMail;
|
||||
|
||||
/// No description provided for @passwordDeleteSendCode.
|
||||
|
|
|
|||
|
|
@ -438,6 +438,10 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
String get passwordAuthenticatorCodeIntro =>
|
||||
'Gib dann den Code ein, den sie zeigt.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorOwnerIntro =>
|
||||
'Bestätige zum Hinzufügen, dass du es bist: mit deinem Passwort und einem Code von einem bisherigen Authenticator.';
|
||||
|
||||
@override
|
||||
String get passwordExistingCode => 'Code von einem bisherigen Authenticator';
|
||||
|
||||
|
|
@ -604,7 +608,7 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordSectionRemoveIntro =>
|
||||
'Gib zur Bestätigung einen Code aus einem deiner Authenticators ein.';
|
||||
'Gib zur Bestätigung dein Passwort und einen Code aus einem deiner Authenticators ein.';
|
||||
|
||||
@override
|
||||
String get passwordSectionRemoveAction => 'Entfernen';
|
||||
|
|
@ -642,7 +646,7 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordNewCodesIntro =>
|
||||
'Gib einen Code aus deiner Authenticator-App ein. Deine alten Wiederherstellungscodes gelten dann nicht mehr.';
|
||||
'Gib dein Passwort und einen Code aus deiner Authenticator-App ein. Deine alten Wiederherstellungscodes gelten dann nicht mehr.';
|
||||
|
||||
@override
|
||||
String get passwordNewPhraseIntro =>
|
||||
|
|
@ -693,10 +697,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
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.';
|
||||
|
|
@ -799,11 +799,11 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordAddressUnknown =>
|
||||
'Dieses Gerät kennt die Adresse deines Kontos nicht, die das braucht. Melde dich ab und wieder mit deiner Adresse und deinem Passwort an.';
|
||||
'Gib die E-Mail-Adresse deines Kontos ein: Dorthin geht der Code.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteIntroByMail =>
|
||||
'Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Seine Authenticator-App ist nicht eingerichtet, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken.';
|
||||
'Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Keiner seiner Authenticators funktioniert gerade, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteSendCode => 'Code per E-Mail schicken';
|
||||
|
|
|
|||
|
|
@ -428,6 +428,10 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get passwordAuthenticatorCodeIntro => 'Then enter the code it shows.';
|
||||
|
||||
@override
|
||||
String get passwordAuthenticatorOwnerIntro =>
|
||||
'To add it, confirm it is you: your password and a code from an authenticator you added before.';
|
||||
|
||||
@override
|
||||
String get passwordExistingCode =>
|
||||
'Code from an authenticator you added before';
|
||||
|
|
@ -592,7 +596,7 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordSectionRemoveIntro =>
|
||||
'Enter a code from one of your authenticators to confirm.';
|
||||
'Enter your password and a code from one of your authenticators to confirm.';
|
||||
|
||||
@override
|
||||
String get passwordSectionRemoveAction => 'Remove';
|
||||
|
|
@ -628,7 +632,7 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordNewCodesIntro =>
|
||||
'Enter a code from your authenticator app. Your old recovery codes stop working.';
|
||||
'Enter your password and a code from your authenticator app. Your old recovery codes stop working.';
|
||||
|
||||
@override
|
||||
String get passwordNewPhraseIntro =>
|
||||
|
|
@ -678,10 +682,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
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.';
|
||||
|
|
@ -782,11 +782,11 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordAddressUnknown =>
|
||||
'This device doesn\'t know your account\'s address, which this takes. Sign out, then sign in again with your address and password.';
|
||||
'Enter your account\'s email address: the code goes there.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteIntroByMail =>
|
||||
'Your account goes with everything in it, in every service of this instance. Its authenticator isn\'t set up, so a code by mail confirms it: type your password, then the code we email to your address.';
|
||||
'Your account goes with everything in it, in every service of this instance. None of its authenticators works now, so a code by mail confirms it: type your password, then the code we email to your address.';
|
||||
|
||||
@override
|
||||
String get passwordDeleteSendCode => 'Email me a code';
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class const AccountTiles({
|
|||
// Without a second factor that works, a code by mail confirms it.
|
||||
await showDeleteAccountWithPassword(
|
||||
context,
|
||||
byMail: !security.finished,
|
||||
byMail: !security.secondFactorWorks,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,8 +119,9 @@ final class const AddAuthenticatorState({
|
|||
}
|
||||
|
||||
/// 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.
|
||||
/// opens, confirmed with its code (and, once one is confirmed, the owner's
|
||||
/// proof: the password and a code from one of those, [needsExistingCode]),
|
||||
/// the first one's recovery codes shown.
|
||||
class AddAuthenticatorCubit(
|
||||
final PasswordRepository _passwords, {
|
||||
required final String _label,
|
||||
|
|
@ -146,8 +147,13 @@ class AddAuthenticatorCubit(
|
|||
}
|
||||
}
|
||||
|
||||
/// Confirms it with its [code] and, where needed, [existingCode].
|
||||
Future<void> codeEntered(String code, {String? existingCode}) async {
|
||||
/// Confirms it with its [code] and, where needed, the [password] and
|
||||
/// [existingCode].
|
||||
Future<void> codeEntered(
|
||||
String code, {
|
||||
String? password,
|
||||
String? existingCode,
|
||||
}) async {
|
||||
final setup = state.setup!;
|
||||
emit(
|
||||
AddAuthenticatorState(
|
||||
|
|
@ -160,6 +166,7 @@ class AddAuthenticatorCubit(
|
|||
final codes = await _passwords.confirm(
|
||||
setup.id,
|
||||
code,
|
||||
password: needsExistingCode ? password : null,
|
||||
existingCode: needsExistingCode ? existingCode : null,
|
||||
);
|
||||
if (!isClosed) {
|
||||
|
|
@ -264,12 +271,11 @@ class NewPhraseCubit(final PasswordRepository _passwords)
|
|||
emit(NewPhraseState(step: NewPhraseStep.shown, phrase: state.phrase));
|
||||
|
||||
/// The words typed back, the [password] and a [code]: the instance keeps
|
||||
/// it; [language] words the owner's mail should codes come to wait.
|
||||
/// it.
|
||||
Future<void> checked(
|
||||
List<String> words, {
|
||||
required String password,
|
||||
required String code,
|
||||
required String language,
|
||||
}) async {
|
||||
if (!checksPhrase(state.phrase, state.checked, words)) {
|
||||
emit(_again(phraseWrong: true));
|
||||
|
|
@ -277,12 +283,7 @@ class NewPhraseCubit(final PasswordRepository _passwords)
|
|||
}
|
||||
emit(_again(busy: true));
|
||||
try {
|
||||
await _passwords.keepPhrase(
|
||||
_made!,
|
||||
password: password,
|
||||
code: code,
|
||||
language: language,
|
||||
);
|
||||
await _passwords.keepPhrase(_made!, password: password, code: code);
|
||||
if (!isClosed) {
|
||||
emit(NewPhraseState(step: NewPhraseStep.done, phrase: state.phrase));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@ typedef PasswordWays = ({
|
|||
bool email,
|
||||
bool password,
|
||||
bool authenticator,
|
||||
bool secondFactorWorks,
|
||||
});
|
||||
|
||||
/// What a signed-in owner proves to change how their account is secured
|
||||
/// (`OwnerProof`, ADR 0017; the third reading): the `answer` to the
|
||||
/// `challenge` (`beginChange`) proving the password, and a TOTP `totpCode`
|
||||
/// or a `recoveryCode`. A session alone gets no code checked.
|
||||
typedef OwnerAnswer = ({
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
});
|
||||
|
||||
/// The boundary to the instance's password sign-in (ADR 0017): halkyon_base's
|
||||
|
|
@ -87,23 +99,23 @@ abstract interface class PasswordGateway() {
|
|||
SessionGrant? as,
|
||||
});
|
||||
|
||||
/// Confirms authenticator [id] with its [code] (and [existingCode] from
|
||||
/// one confirmed before); the recovery codes the first brings.
|
||||
/// Confirms authenticator [id] with its [code] (and, once one is
|
||||
/// confirmed, the [owner]'s proof); the recovery codes the first brings.
|
||||
Future<List<String>> confirm(
|
||||
String id,
|
||||
String code, {
|
||||
String? existingCode,
|
||||
OwnerAnswer? owner,
|
||||
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});
|
||||
/// Removes authenticator [id], a confirmed one with the [owner]'s proof.
|
||||
Future<void> remove(String id, {OwnerAnswer? owner});
|
||||
|
||||
/// New recovery codes for a [code].
|
||||
Future<List<String>> newRecoveryCodes(String code);
|
||||
/// New recovery codes for the [owner]'s proof.
|
||||
Future<List<String>> newRecoveryCodes(OwnerAnswer owner);
|
||||
|
||||
/// How the signed-in person signs in.
|
||||
Future<PasswordWays> ways();
|
||||
|
|
@ -114,9 +126,7 @@ abstract interface class PasswordGateway() {
|
|||
/// Deletes the signed-in person's account, which has a password: [answer]
|
||||
/// to [challenge] ([beginChange]) proves it, with a [totpCode] or
|
||||
/// [recoveryCode], or where no second factor works a [code] mailed for
|
||||
/// [request] asked for deleting, with the account's [address] and
|
||||
/// [language] (as a change takes them); this device then forgets its
|
||||
/// session.
|
||||
/// [request] asked for deleting; this device then forgets its session.
|
||||
Future<void> deleteAccount(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
|
|
@ -124,35 +134,21 @@ abstract interface class PasswordGateway() {
|
|||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String address,
|
||||
required String language,
|
||||
});
|
||||
|
||||
/// Changes the password: [answer] to [challenge] proves the old one; the
|
||||
/// account's [address], checked before the code, and [language] tell its
|
||||
/// owner should codes come to wait.
|
||||
/// Changes the password to [password], the [owner]'s proof proving the
|
||||
/// old one.
|
||||
Future<void> change(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
OwnerAnswer owner, {
|
||||
required protocol.NewPassword password,
|
||||
required String address,
|
||||
required String language,
|
||||
});
|
||||
|
||||
/// A new recovery phrase's wrapping of the keys and its [proofKey]'s
|
||||
/// public half: [answer] to [challenge] ([beginChange]) proves the
|
||||
/// password, with a [totpCode] or [recoveryCode].
|
||||
/// public half, for the [owner]'s proof.
|
||||
Future<void> setRecoveryKeys(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
OwnerAnswer owner, {
|
||||
required protocol.WrappedKeys keys,
|
||||
required ByteData proofKey,
|
||||
required String address,
|
||||
required String language,
|
||||
});
|
||||
|
||||
/// Asks for the reset of a lost authenticator: [answer] to [challenge]
|
||||
|
|
|
|||
|
|
@ -267,35 +267,76 @@ class PasswordRepository(
|
|||
PendingSignIn? pending,
|
||||
}) => _gateway.enrol(name: name, label: label, as: pending?.grant);
|
||||
|
||||
/// Confirms authenticator [id] with its [code], and [existingCode] from
|
||||
/// one confirmed before where the account has one; the recovery codes the
|
||||
/// first brings (shown once), none for another.
|
||||
/// Confirms authenticator [id] with its [code], and, where the account
|
||||
/// has one confirmed already, the person's [password] and [existingCode]
|
||||
/// from one of those (the owner's proof: a session alone adds no second
|
||||
/// factor); the recovery codes the first brings (shown once), none for
|
||||
/// another.
|
||||
Future<List<String>> confirm(
|
||||
String id,
|
||||
String code, {
|
||||
String? password,
|
||||
String? existingCode,
|
||||
PendingSignIn? pending,
|
||||
}) async {
|
||||
final codes = await _gateway.confirm(
|
||||
id,
|
||||
code.replaceAll(' ', ''),
|
||||
existingCode: existingCode?.replaceAll(' ', ''),
|
||||
owner: password == null
|
||||
? null
|
||||
: await _ownerAnswer(password, totpCode: existingCode),
|
||||
as: pending?.grant,
|
||||
);
|
||||
_changed();
|
||||
return codes;
|
||||
}
|
||||
|
||||
/// Removes authenticator [id]: a confirmed one with a [code] from one of
|
||||
/// the person's, while another stays.
|
||||
Future<void> removeAuthenticator(String id, {String? code}) async {
|
||||
await _gateway.remove(id, code: code?.replaceAll(' ', ''));
|
||||
/// Removes authenticator [id]: a confirmed one with the person's
|
||||
/// [password] and a [code] from one of theirs, while another stays; one
|
||||
/// never confirmed with neither.
|
||||
Future<void> removeAuthenticator(
|
||||
String id, {
|
||||
String? password,
|
||||
String? code,
|
||||
}) async {
|
||||
await _gateway.remove(
|
||||
id,
|
||||
owner: password == null
|
||||
? null
|
||||
: await _ownerAnswer(password, totpCode: code),
|
||||
);
|
||||
_changed();
|
||||
}
|
||||
|
||||
/// New recovery codes in place of the old, for a [code]: shown once.
|
||||
Future<List<String>> newRecoveryCodes(String code) =>
|
||||
_gateway.newRecoveryCodes(code.replaceAll(' ', ''));
|
||||
/// New recovery codes in place of the old, for the person's [password]
|
||||
/// and a [code]: shown once.
|
||||
Future<List<String>> newRecoveryCodes({
|
||||
required String password,
|
||||
required String code,
|
||||
}) async => await _gateway.newRecoveryCodes(
|
||||
await _ownerAnswer(password, totpCode: code),
|
||||
);
|
||||
|
||||
/// The owner's proof (`OwnerProof`): [password] answering a change's
|
||||
/// challenge, with a [totpCode] or [recoveryCode].
|
||||
Future<OwnerAnswer> _ownerAnswer(
|
||||
String password, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
}) async {
|
||||
final challenge = await _gateway.beginChange();
|
||||
final unlock = await _crypto.unlock(
|
||||
password,
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
costs: challenge.costs,
|
||||
);
|
||||
return (
|
||||
challenge: challenge,
|
||||
answer: await _crypto.answer(unlock, challenge, host: _gateway.host),
|
||||
totpCode: totpCode?.replaceAll(' ', ''),
|
||||
recoveryCode: recoveryCode,
|
||||
);
|
||||
}
|
||||
|
||||
/// What keeps the signed-in person's account.
|
||||
Future<AccountSecurity> security() async {
|
||||
|
|
@ -312,6 +353,7 @@ class PasswordRepository(
|
|||
gitHub: ways.gitHub,
|
||||
gitHubEnds: offers.gitHubEnds,
|
||||
hasPassword: ways.password,
|
||||
secondFactorWorks: ways.secondFactorWorks,
|
||||
authenticators: ways.password
|
||||
? await _gateway.authenticators()
|
||||
: const <AccountAuthenticator>[],
|
||||
|
|
@ -330,25 +372,15 @@ class PasswordRepository(
|
|||
required String newPassword,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String language,
|
||||
}) async {
|
||||
final keys = await _keysHere();
|
||||
final address = await _addressHere();
|
||||
final challenge = await _gateway.beginChange();
|
||||
final unlock = await _crypto.unlock(
|
||||
oldPassword,
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
costs: challenge.costs,
|
||||
);
|
||||
final answer = await _crypto.answer(unlock, challenge, host: _gateway.host);
|
||||
await _gateway.change(
|
||||
challenge,
|
||||
answer,
|
||||
totpCode: totpCode?.replaceAll(' ', ''),
|
||||
recoveryCode: recoveryCode,
|
||||
await _ownerAnswer(
|
||||
oldPassword,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
),
|
||||
password: await _crypto.newPassword(newPassword, keys),
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
_changed();
|
||||
}
|
||||
|
|
@ -365,9 +397,7 @@ class PasswordRepository(
|
|||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String language,
|
||||
}) async {
|
||||
final address = await _addressHere();
|
||||
final challenge = await _gateway.beginChange();
|
||||
final unlock = await _crypto.unlock(
|
||||
password,
|
||||
|
|
@ -381,23 +411,26 @@ class PasswordRepository(
|
|||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
}
|
||||
|
||||
/// Mails a code for deleting the signed-in person's account to the
|
||||
/// address this device signed in with, where no second factor works (a
|
||||
/// setup never finished): that address, and the request the code
|
||||
/// answers.
|
||||
/// Mails a code for deleting the signed-in person's account to [address]
|
||||
/// (the one this device signed in with where it knows it, [addressHere];
|
||||
/// the sheet asks for it otherwise), where no second factor works: that
|
||||
/// address, and the request the code answers. The instance takes the code
|
||||
/// only for the account's own address.
|
||||
Future<({String address, String request})> mailDeletionCode({
|
||||
required String language,
|
||||
String? address,
|
||||
}) async {
|
||||
final address = await _addressHere();
|
||||
final to =
|
||||
address?.trim() ??
|
||||
await addressHere() ??
|
||||
(throw const PasswordFailed(PasswordProblem.addressUnknown));
|
||||
return (
|
||||
address: address,
|
||||
address: to,
|
||||
request: await sendCode(
|
||||
address,
|
||||
to,
|
||||
language: language,
|
||||
purpose: MailCodePurpose.deletion,
|
||||
),
|
||||
|
|
@ -420,23 +453,11 @@ class PasswordRepository(
|
|||
NewPhrase phrase, {
|
||||
required String password,
|
||||
required String code,
|
||||
required String language,
|
||||
}) async {
|
||||
final address = await _addressHere();
|
||||
final challenge = await _gateway.beginChange();
|
||||
final unlock = await _crypto.unlock(
|
||||
password,
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
costs: challenge.costs,
|
||||
);
|
||||
await _gateway.setRecoveryKeys(
|
||||
challenge,
|
||||
await _crypto.answer(unlock, challenge, host: _gateway.host),
|
||||
totpCode: code.replaceAll(' ', ''),
|
||||
await _ownerAnswer(password, totpCode: code),
|
||||
keys: phrase._recoveryKeys,
|
||||
proofKey: phrase._recoveryProofKey,
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
_changed();
|
||||
}
|
||||
|
|
@ -593,12 +614,8 @@ class PasswordRepository(
|
|||
await _keys.read(_signedIn()) ??
|
||||
(throw const PasswordFailed(PasswordProblem.keysMissing));
|
||||
|
||||
/// The address the signed-in person signed in with on this device: what
|
||||
/// changes while signed in send, which the instance checks against the
|
||||
/// account's before any code (it keeps none, and mails its owner should
|
||||
/// codes come to wait). Refused ([PasswordProblem.addressUnknown]) where
|
||||
/// this device doesn't know it.
|
||||
Future<String> _addressHere() async =>
|
||||
await _addresses.of(_signedIn()) ??
|
||||
(throw const PasswordFailed(PasswordProblem.addressUnknown));
|
||||
/// The address the signed-in person signed in with on this device, where
|
||||
/// it knows it: where a code by mail goes (deleting an account whose
|
||||
/// second factor doesn't work), which the sheet asks for otherwise.
|
||||
Future<String?> addressHere() => _addresses.of(_signedIn());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,14 +143,14 @@ class ServerpodPasswordGateway(
|
|||
Future<List<String>> confirm(
|
||||
String id,
|
||||
String code, {
|
||||
String? existingCode,
|
||||
OwnerAnswer? owner,
|
||||
SessionGrant? as,
|
||||
}) => _as(
|
||||
as,
|
||||
(base) => base.authenticator.confirm(
|
||||
authenticator: _idOf(id)!,
|
||||
code: code,
|
||||
existingCode: existingCode,
|
||||
proof: _proofOf(owner),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -168,13 +168,31 @@ class ServerpodPasswordGateway(
|
|||
);
|
||||
|
||||
@override
|
||||
Future<void> remove(String id, {String? code}) => _failing(
|
||||
() => _base.authenticator.remove(authenticator: _idOf(id)!, code: code),
|
||||
Future<void> remove(String id, {OwnerAnswer? owner}) => _failing(
|
||||
() => _base.authenticator.remove(
|
||||
authenticator: _idOf(id)!,
|
||||
proof: _proofOf(owner),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<String>> newRecoveryCodes(String code) =>
|
||||
_failing(() => _base.authenticator.newRecoveryCodes(code: code));
|
||||
Future<List<String>> newRecoveryCodes(OwnerAnswer owner) => _failing(
|
||||
() => _base.authenticator.newRecoveryCodes(proof: _proofOf(owner)!),
|
||||
);
|
||||
|
||||
/// [owner] as the instance takes it, bound to the [host] addressed.
|
||||
protocol.OwnerProof? _proofOf(OwnerAnswer? owner) => switch (owner) {
|
||||
null => null,
|
||||
(:final challenge, :final answer, :final totpCode, :final recoveryCode) =>
|
||||
protocol.OwnerProof(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
host: host,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
),
|
||||
};
|
||||
|
||||
@override
|
||||
Future<PasswordWays> ways() => _failing(() async {
|
||||
|
|
@ -184,6 +202,7 @@ class ServerpodPasswordGateway(
|
|||
email: ways.email,
|
||||
password: ways.password,
|
||||
authenticator: ways.authenticator,
|
||||
secondFactorWorks: ways.secondFactorWorks,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -199,8 +218,6 @@ class ServerpodPasswordGateway(
|
|||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _failing(() async {
|
||||
await _base.account.delete(
|
||||
proof: protocol.DeletionProof(
|
||||
|
|
@ -212,8 +229,6 @@ class ServerpodPasswordGateway(
|
|||
request: _idOf(request),
|
||||
code: code,
|
||||
host: host,
|
||||
address: address,
|
||||
language: language,
|
||||
),
|
||||
);
|
||||
// The server ended every session of the person; forget this device's
|
||||
|
|
@ -223,49 +238,22 @@ class ServerpodPasswordGateway(
|
|||
|
||||
@override
|
||||
Future<void> change(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
OwnerAnswer owner, {
|
||||
required protocol.NewPassword password,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _failing(
|
||||
() => _base.password.change(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
password: password,
|
||||
host: host,
|
||||
address: address,
|
||||
language: language,
|
||||
),
|
||||
() => _base.password.change(proof: _proofOf(owner)!, password: password),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> setRecoveryKeys(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
OwnerAnswer owner, {
|
||||
required protocol.WrappedKeys keys,
|
||||
required ByteData proofKey,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _failing(
|
||||
() => _base.password.setRecoveryKeys(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
proof: _proofOf(owner)!,
|
||||
recoveryKeys: keys,
|
||||
recoveryProofKey: proofKey,
|
||||
host: host,
|
||||
address: address,
|
||||
language: language,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -454,8 +442,6 @@ PasswordProblem _problemOf(protocol.PasswordRefusal reason) => switch (reason) {
|
|||
protocol.PasswordRefusal.wrongCode => PasswordProblem.wrongCode,
|
||||
protocol.PasswordRefusal.notDue => PasswordProblem.notDue,
|
||||
protocol.PasswordRefusal.noReset => PasswordProblem.noReset,
|
||||
protocol.PasswordRefusal.wrongExistingCode =>
|
||||
PasswordProblem.wrongExistingCode,
|
||||
// The instance answers by another name than the device addressed it by:
|
||||
// what the device proved is bound to that name, so nothing it says can
|
||||
// be taken as the instance's.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ final class const AccountSecurity({
|
|||
/// Whether the person has a password.
|
||||
required final bool hasPassword,
|
||||
|
||||
/// Whether a second factor works: an authenticator confirmed, and no
|
||||
/// reset of them taken effect (as the instance tells it). Where none
|
||||
/// does, deleting the account takes a code by mail.
|
||||
final bool secondFactorWorks = false,
|
||||
|
||||
/// Their authenticators, the oldest first; empty without a password.
|
||||
final List<AccountAuthenticator> authenticators = const [],
|
||||
|
||||
|
|
@ -36,6 +41,7 @@ final class const AccountSecurity({
|
|||
gitHub,
|
||||
gitHubEnds,
|
||||
hasPassword,
|
||||
secondFactorWorks,
|
||||
authenticators,
|
||||
keysHere,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ enum PasswordProblem() {
|
|||
/// The account has no address yet.
|
||||
noAddress,
|
||||
|
||||
/// This device doesn't know the account's address, which a change while
|
||||
/// signed in takes: sign in with it again.
|
||||
/// The account's address, where a code by mail goes, is missing: type
|
||||
/// it.
|
||||
addressUnknown,
|
||||
|
||||
/// The account has no password yet.
|
||||
|
|
@ -51,9 +51,6 @@ enum PasswordProblem() {
|
|||
/// The authenticator's code isn't right.
|
||||
wrongCode,
|
||||
|
||||
/// The code from an authenticator added before isn't right.
|
||||
wrongExistingCode,
|
||||
|
||||
/// The last authenticator can't go: the second factor is mandatory.
|
||||
lastAuthenticator,
|
||||
|
||||
|
|
|
|||
|
|
@ -240,13 +240,11 @@ class _ChangePasswordState() extends State<_ChangePassword> {
|
|||
return;
|
||||
}
|
||||
final passwords = _passwordsOf(context);
|
||||
final language = mailLanguageOf(context);
|
||||
if (await _step.run(() async {
|
||||
await passwords.changePassword(
|
||||
oldPassword: _old.text,
|
||||
newPassword: _new.text,
|
||||
totpCode: _code.text,
|
||||
language: language,
|
||||
);
|
||||
return true;
|
||||
})) {
|
||||
|
|
@ -308,7 +306,7 @@ class _ChangePasswordState() extends State<_ChangePassword> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Removes an authenticator: a confirmed one for a code.
|
||||
/// Removes an authenticator: a confirmed one for the password and a code.
|
||||
class const _RemoveAuthenticator(final AccountAuthenticator authenticator)
|
||||
extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -316,11 +314,13 @@ class const _RemoveAuthenticator(final AccountAuthenticator authenticator)
|
|||
}
|
||||
|
||||
class _RemoveAuthenticatorState() extends State<_RemoveAuthenticator> {
|
||||
final _password = TextEditingController();
|
||||
final _code = TextEditingController();
|
||||
late final _step = SecurityStepCubit<bool>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_password.dispose();
|
||||
_code.dispose();
|
||||
_step.close();
|
||||
super.dispose();
|
||||
|
|
@ -333,6 +333,7 @@ class _RemoveAuthenticatorState() extends State<_RemoveAuthenticator> {
|
|||
if (await _step.run(() async {
|
||||
await passwords.removeAuthenticator(
|
||||
authenticator.id,
|
||||
password: authenticator.confirmed ? _password.text : null,
|
||||
code: authenticator.confirmed ? _code.text : null,
|
||||
);
|
||||
return true;
|
||||
|
|
@ -354,10 +355,15 @@ class _RemoveAuthenticatorState() extends State<_RemoveAuthenticator> {
|
|||
children: [
|
||||
if (widget.authenticator.confirmed) ...[
|
||||
SheetIntro(l10n.passwordSectionRemoveIntro),
|
||||
PasswordField(
|
||||
controller: _password,
|
||||
label: l10n.passwordCurrent,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CodeField(
|
||||
controller: _code,
|
||||
kind: CodeKind.totp,
|
||||
autofocus: true,
|
||||
errorText: _problemOf(context, state),
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
|
|
@ -377,7 +383,8 @@ class _RemoveAuthenticatorState() extends State<_RemoveAuthenticator> {
|
|||
}
|
||||
|
||||
/// Adds an authenticator: its QR code, its code and, where one is confirmed
|
||||
/// already, a code from that one; the first one's recovery codes.
|
||||
/// already, the password and a code from that one; the first one's recovery
|
||||
/// codes.
|
||||
class const _AddAuthenticator({
|
||||
required final String label,
|
||||
required final bool needsExistingCode,
|
||||
|
|
@ -388,6 +395,7 @@ class const _AddAuthenticator({
|
|||
|
||||
class _AddAuthenticatorState() extends State<_AddAuthenticator> {
|
||||
final _code = TextEditingController();
|
||||
final _password = TextEditingController();
|
||||
final _existing = TextEditingController();
|
||||
late final AddAuthenticatorCubit _cubit;
|
||||
|
||||
|
|
@ -405,6 +413,7 @@ class _AddAuthenticatorState() extends State<_AddAuthenticator> {
|
|||
@override
|
||||
void dispose() {
|
||||
_code.dispose();
|
||||
_password.dispose();
|
||||
_existing.dispose();
|
||||
_cubit.close();
|
||||
super.dispose();
|
||||
|
|
@ -413,7 +422,11 @@ class _AddAuthenticatorState() extends State<_AddAuthenticator> {
|
|||
void _submit(AddAuthenticatorState state) {
|
||||
switch (state.step) {
|
||||
case AddAuthenticatorStep.code:
|
||||
_cubit.codeEntered(_code.text, existingCode: _existing.text);
|
||||
_cubit.codeEntered(
|
||||
_code.text,
|
||||
password: _password.text,
|
||||
existingCode: _existing.text,
|
||||
);
|
||||
case AddAuthenticatorStep.recoveryCodes ||
|
||||
AddAuthenticatorStep.done ||
|
||||
AddAuthenticatorStep.enrolling:
|
||||
|
|
@ -449,16 +462,18 @@ class _AddAuthenticatorState() extends State<_AddAuthenticator> {
|
|||
AddAuthenticatorStep.code => AuthenticatorSetupView(
|
||||
setup: state.setup!,
|
||||
code: _code,
|
||||
password: widget.needsExistingCode ? _password : null,
|
||||
existingCode: widget.needsExistingCode ? _existing : null,
|
||||
enabled: !state.busy,
|
||||
errorText:
|
||||
state.problem?.problem == PasswordProblem.wrongExistingCode
|
||||
? null
|
||||
: problem,
|
||||
existingErrorText:
|
||||
state.problem?.problem == PasswordProblem.wrongExistingCode
|
||||
// The new one's own code under it; the owner's proof (the
|
||||
// password and the code from another) under that.
|
||||
errorText: state.problem?.problem == PasswordProblem.wrongCode
|
||||
? problem
|
||||
: null,
|
||||
existingErrorText:
|
||||
state.problem?.problem == PasswordProblem.wrongCode
|
||||
? null
|
||||
: problem,
|
||||
onDone: () => _submit(state),
|
||||
),
|
||||
AddAuthenticatorStep.recoveryCodes => Column(
|
||||
|
|
@ -488,18 +503,20 @@ class _AddAuthenticatorState() extends State<_AddAuthenticator> {
|
|||
}
|
||||
}
|
||||
|
||||
/// New recovery codes for a code: shown once.
|
||||
/// New recovery codes for the password and a code: shown once.
|
||||
class const _NewRecoveryCodes() extends StatefulWidget {
|
||||
@override
|
||||
State<_NewRecoveryCodes> createState() => _NewRecoveryCodesState();
|
||||
}
|
||||
|
||||
class _NewRecoveryCodesState() extends State<_NewRecoveryCodes> {
|
||||
final _password = TextEditingController();
|
||||
final _code = TextEditingController();
|
||||
late final _step = SecurityStepCubit<List<String>>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_password.dispose();
|
||||
_code.dispose();
|
||||
_step.close();
|
||||
super.dispose();
|
||||
|
|
@ -507,7 +524,12 @@ class _NewRecoveryCodesState() extends State<_NewRecoveryCodes> {
|
|||
|
||||
Future<void> _submit() async {
|
||||
final passwords = _passwordsOf(context);
|
||||
await _step.run(() => passwords.newRecoveryCodes(_code.text));
|
||||
await _step.run(
|
||||
() => passwords.newRecoveryCodes(
|
||||
password: _password.text,
|
||||
code: _code.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -528,10 +550,15 @@ class _NewRecoveryCodesState() extends State<_NewRecoveryCodes> {
|
|||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SheetIntro(l10n.passwordNewCodesIntro),
|
||||
PasswordField(
|
||||
controller: _password,
|
||||
label: l10n.passwordCurrent,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CodeField(
|
||||
controller: _code,
|
||||
kind: CodeKind.totp,
|
||||
autofocus: true,
|
||||
errorText: _problemOf(context, state),
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
|
|
@ -603,7 +630,6 @@ class _NewPhraseState() extends State<_NewPhrase> {
|
|||
[for (final word in _words) word.text],
|
||||
password: _password.text,
|
||||
code: _code.text,
|
||||
language: mailLanguageOf(context),
|
||||
);
|
||||
case NewPhraseStep.making || NewPhraseStep.done:
|
||||
Navigator.of(context).pop();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
|
||||
import 'package:halkyon_app/src/app/app_scope.dart';
|
||||
|
|
@ -10,9 +12,12 @@ import 'package:material_ui/material_ui.dart';
|
|||
/// Deletes the signed-in person's account, which has a password: the
|
||||
/// password and a code from an authenticator, as changing the password
|
||||
/// takes (ADR 0017; the second reading), or [byMail], where no second
|
||||
/// factor works (a setup never finished), the password and a code by mail
|
||||
/// asked for deleting. Once deleted, the person is signed out here, and the
|
||||
/// app asks how to start.
|
||||
/// factor works (a setup never finished, or the authenticators' reset taken
|
||||
/// effect: `AccountSecurity.secondFactorWorks`, as the instance tells it),
|
||||
/// the password and a code by mail asked for deleting, to the address this
|
||||
/// device signed in with, or one typed where it doesn't know it (the third
|
||||
/// reading). Once deleted, the person is signed out here, and the app asks
|
||||
/// how to start.
|
||||
Future<void> showDeleteAccountWithPassword(
|
||||
BuildContext context, {
|
||||
bool byMail = false,
|
||||
|
|
@ -31,8 +36,26 @@ class const _DeleteAccount({required final bool byMail})
|
|||
class _DeleteAccountState() extends State<_DeleteAccount> {
|
||||
final _password = TextEditingController();
|
||||
final _code = TextEditingController();
|
||||
final _address = TextEditingController();
|
||||
late final _step = SecurityStepCubit<bool>();
|
||||
|
||||
/// Whether this device knows the account's address; null until read.
|
||||
bool? _addressKnown;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.byMail) {
|
||||
unawaited(
|
||||
AppScope.of(context).passwords.addressHere().then((address) {
|
||||
if (mounted) {
|
||||
setState(() => _addressKnown = address != null);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The code mailed for deleting, once asked for: its address and request.
|
||||
({String address, String request})? _mailed;
|
||||
|
||||
|
|
@ -40,6 +63,7 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
void dispose() {
|
||||
_password.dispose();
|
||||
_code.dispose();
|
||||
_address.dispose();
|
||||
_step.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -51,7 +75,10 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
if (widget.byMail && mailed == null) {
|
||||
// The code first; the password goes with it.
|
||||
await _step.run(() async {
|
||||
final asked = await passwords.mailDeletionCode(language: language);
|
||||
final asked = await passwords.mailDeletionCode(
|
||||
language: language,
|
||||
address: (_addressKnown ?? false) ? null : _address.text,
|
||||
);
|
||||
setState(() => _mailed = asked);
|
||||
return false;
|
||||
});
|
||||
|
|
@ -63,7 +90,6 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
totpCode: widget.byMail ? null : _code.text,
|
||||
request: mailed?.request,
|
||||
code: widget.byMail ? _code.text : null,
|
||||
language: language,
|
||||
);
|
||||
return true;
|
||||
});
|
||||
|
|
@ -95,6 +121,12 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
|||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.byMail &&
|
||||
_mailed == null &&
|
||||
_addressKnown == false) ...[
|
||||
AddressField(controller: _address),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (widget.byMail && _mailed == null)
|
||||
if (state.problem case final failed?)
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ String passwordProblemText(BuildContext context, PasswordFailed failed) {
|
|||
PasswordProblem.addressUnknown => l10n.passwordAddressUnknown,
|
||||
PasswordProblem.noPassword => l10n.passwordNoPassword,
|
||||
PasswordProblem.wrongCode => l10n.passwordWrongCode,
|
||||
PasswordProblem.wrongExistingCode => l10n.passwordWrongExistingCode,
|
||||
PasswordProblem.lastAuthenticator => l10n.passwordLastAuthenticator,
|
||||
PasswordProblem.tooManyAuthenticators => l10n.passwordTooManyAuthenticators,
|
||||
PasswordProblem.unknownAuthenticator => l10n.passwordUnknownAuthenticator,
|
||||
|
|
|
|||
|
|
@ -482,10 +482,12 @@ class _QrPainter(final QrImage _code) extends CustomPainter {
|
|||
|
||||
/// What setting up an authenticator shows: the QR code of its URI, the key
|
||||
/// to type by hand, on phones a button opening the authenticator app, and
|
||||
/// the fields for its code (and one from an authenticator added before).
|
||||
/// the fields for its code (and, once one is confirmed, the password and a
|
||||
/// code from an authenticator added before: the owner's proof).
|
||||
class const AuthenticatorSetupView({
|
||||
required final AuthenticatorSetup setup,
|
||||
required final TextEditingController code,
|
||||
final TextEditingController? password,
|
||||
final TextEditingController? existingCode,
|
||||
final String? errorText,
|
||||
final String? existingErrorText,
|
||||
|
|
@ -566,6 +568,16 @@ class const AuthenticatorSetupView({
|
|||
: TextInputAction.next,
|
||||
onSubmitted: existingCode == null ? (_) => onDone?.call() : null,
|
||||
),
|
||||
if (password case final password?) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(l10n.passwordAuthenticatorOwnerIntro),
|
||||
const SizedBox(height: 8),
|
||||
PasswordField(
|
||||
controller: password,
|
||||
label: l10n.passwordCurrent,
|
||||
enabled: enabled,
|
||||
),
|
||||
],
|
||||
if (existingCode case final existing?) ...[
|
||||
const SizedBox(height: 8),
|
||||
CodeField(
|
||||
|
|
|
|||
|
|
@ -446,20 +446,25 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
Future<List<String>> confirm(
|
||||
String id,
|
||||
String code, {
|
||||
String? existingCode,
|
||||
OwnerAnswer? owner,
|
||||
SessionGrant? as,
|
||||
}) => _call('confirm', () {
|
||||
final account = _accountAs(as);
|
||||
final index = account.authenticators.indexWhere((each) => each.id == id);
|
||||
if (index < 0) {
|
||||
if (index < 0 || account.authenticators[index].confirmed) {
|
||||
throw const PasswordFailed(PasswordProblem.unknownAuthenticator);
|
||||
}
|
||||
if (code != fakeTotpCode) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCode);
|
||||
}
|
||||
final first = !account.finished;
|
||||
if (!first && existingCode != fakeTotpCode) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongExistingCode);
|
||||
if (!first) {
|
||||
if (owner == null) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
_ownerProved(owner);
|
||||
} else if (owner != null) {
|
||||
throw const PasswordFailed(PasswordProblem.invalidSetup);
|
||||
}
|
||||
final added = account.authenticators[index];
|
||||
account.authenticators[index] = AccountAuthenticator(
|
||||
|
|
@ -483,7 +488,7 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
_call('authenticators', () => [..._accountAs(null).authenticators]);
|
||||
|
||||
@override
|
||||
Future<void> remove(String id, {String? code}) => _call('remove', () {
|
||||
Future<void> remove(String id, {OwnerAnswer? owner}) => _call('remove', () {
|
||||
final account = _accountAs(null);
|
||||
final authenticator = account.authenticators
|
||||
.where((each) => each.id == id)
|
||||
|
|
@ -495,21 +500,21 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
if (account.authenticators.where((each) => each.confirmed).length < 2) {
|
||||
throw const PasswordFailed(PasswordProblem.lastAuthenticator);
|
||||
}
|
||||
if (code != fakeTotpCode) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCode);
|
||||
if (owner == null) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
_ownerProved(owner);
|
||||
} else if (owner != null) {
|
||||
throw const PasswordFailed(PasswordProblem.invalidSetup);
|
||||
}
|
||||
account.authenticators.remove(authenticator);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<List<String>> newRecoveryCodes(String code) => _call(
|
||||
Future<List<String>> newRecoveryCodes(OwnerAnswer owner) => _call(
|
||||
'newRecoveryCodes',
|
||||
() {
|
||||
final account = _accountAs(null);
|
||||
if (code != fakeTotpCode) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCode);
|
||||
}
|
||||
final account = _ownerProved(owner);
|
||||
account.recoveryCodes
|
||||
..clear()
|
||||
..addAll([
|
||||
|
|
@ -528,6 +533,9 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
email: account != null,
|
||||
password: account?.password != null,
|
||||
authenticator: account?.finished ?? false,
|
||||
secondFactorWorks:
|
||||
(account?.finished ?? false) &&
|
||||
(account?.authenticatorReset?.isAfter(clock.now()) ?? true),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -548,21 +556,22 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
String? recoveryCode,
|
||||
String? request,
|
||||
String? code,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
final account = await _call('deleteAccount', () {
|
||||
final proved = _proved(challenge, answer);
|
||||
if (proved.finished) {
|
||||
return _ownerCoded(proved, totpCode, recoveryCode, address);
|
||||
}
|
||||
// No second factor works: the password and a code by mail asked for
|
||||
// deleting, as the register takes them.
|
||||
if (proved.person != _signedIn()) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
_requireAddress(proved, address);
|
||||
if (request == null ||
|
||||
if ((totpCode ?? '').isNotEmpty || (recoveryCode ?? '').isNotEmpty) {
|
||||
return _ownerCoded(proved, totpCode, recoveryCode);
|
||||
}
|
||||
// No second factor works: the password and a code by mail asked for
|
||||
// deleting, as the register takes them.
|
||||
final works =
|
||||
proved.finished &&
|
||||
(proved.authenticatorReset?.isAfter(clock.now()) ?? true);
|
||||
if (works ||
|
||||
request == null ||
|
||||
(code ?? '').trim().isEmpty ||
|
||||
_proven(request, code, MailCodePurpose.deletion) != proved.address) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
|
|
@ -575,65 +584,58 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
|
||||
@override
|
||||
Future<void> change(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
OwnerAnswer owner, {
|
||||
required protocol.NewPassword password,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _call('change', () {
|
||||
_ownerProved(challenge, answer, totpCode, recoveryCode, address)
|
||||
_ownerProved(owner)
|
||||
..password = utf8.decode(Uint8List.sublistView(password.verifier))
|
||||
..passwordKeys = password.passwordKeys;
|
||||
..passwordKeys = password.passwordKeys
|
||||
..codesWaitUntil = null;
|
||||
});
|
||||
|
||||
/// The signed-in person's account, once [answer] to [challenge] proved
|
||||
/// its password and a code came: refused as the register refuses a
|
||||
/// change.
|
||||
FakePasswordAccount _ownerProved(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String address,
|
||||
) => _ownerCoded(_proved(challenge, answer), totpCode, recoveryCode, address);
|
||||
/// The signed-in person's account, once the [owner]'s proof proved its
|
||||
/// password and a code came: refused as the register refuses a change.
|
||||
FakePasswordAccount _ownerProved(OwnerAnswer owner) => _ownerCoded(
|
||||
_proved(owner.challenge, owner.answer),
|
||||
owner.totpCode,
|
||||
owner.recoveryCode,
|
||||
);
|
||||
|
||||
/// [account], whose password proved right, once it is the signed-in
|
||||
/// person's and a code came: refused as the register refuses a change.
|
||||
/// person's and a code from it came: refused as the register refuses a
|
||||
/// change. Never held back by the account's waits, as the register's
|
||||
/// signed-in proofs aren't.
|
||||
FakePasswordAccount _ownerCoded(
|
||||
FakePasswordAccount account,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
String address,
|
||||
) {
|
||||
if (account.person != _signedIn() ||
|
||||
((totpCode ?? '').isEmpty && (recoveryCode ?? '').isEmpty)) {
|
||||
if (account.person != _signedIn()) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
_requireAddress(account, address);
|
||||
if (_secondFactor(
|
||||
account,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
)) {
|
||||
if (!account.finished) {
|
||||
throw const PasswordFailed(PasswordProblem.setupIncomplete);
|
||||
}
|
||||
final totp = totpCode?.trim() ?? '';
|
||||
final recovery = recoveryCode?.trim().toUpperCase() ?? '';
|
||||
if (totp.isNotEmpty
|
||||
? totp != fakeTotpCode
|
||||
: !account.recoveryCodes.remove(recovery)) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
account
|
||||
..authenticatorReset = null
|
||||
..accountReset = null;
|
||||
return account;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRecoveryKeys(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer, {
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
OwnerAnswer owner, {
|
||||
required protocol.WrappedKeys keys,
|
||||
required ByteData proofKey,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => _call('setRecoveryKeys', () {
|
||||
_ownerProved(challenge, answer, totpCode, recoveryCode, address)
|
||||
_ownerProved(owner)
|
||||
..recoveryKeys = keys
|
||||
..recoveryProofKey = proofKey;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
|
|
@ -220,6 +221,53 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("deleting an account whose authenticators' reset took effect "
|
||||
'asks for a code by mail, as the instance takes it, and for the '
|
||||
"address where this device doesn't know it", (tester) async {
|
||||
// The first person the fake password gateway makes.
|
||||
const person = '00000000-0000-4000-8000-000000000001';
|
||||
gateway = FakeAuthGateway(
|
||||
status: const SignedIn(authUserId: person, isIdentified: true),
|
||||
);
|
||||
await start(tester);
|
||||
// Its authenticator still confirmed, the reset taken effect but not
|
||||
// carried out (the third reading's fourth finding).
|
||||
dependencies.passwordGateway
|
||||
.addAccount(
|
||||
'anna@example.org',
|
||||
password: 'correct horse',
|
||||
crypto: dependencies.crypto,
|
||||
)
|
||||
.authenticatorReset = clock.now().subtract(
|
||||
const Duration(hours: 1),
|
||||
);
|
||||
Finder field(String label) => find.widgetWithText(TextField, label);
|
||||
|
||||
await tester.tap(find.text('Delete account'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete account').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('a code by mail confirms it'), findsOneWidget);
|
||||
await tester.enterText(field('Current password'), 'correct horse');
|
||||
await tester.enterText(field('Email address'), 'anna@example.org');
|
||||
await tester.tap(find.text('Email me a code'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
dependencies.passwordGateway.mailed.single.purpose,
|
||||
MailCodePurpose.deletion,
|
||||
);
|
||||
|
||||
await tester.enterText(
|
||||
field('Code from the email'),
|
||||
dependencies.passwordGateway.codeFor('anna@example.org'),
|
||||
);
|
||||
await tester.tap(find.text('Delete account').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.accountDeletions, 1);
|
||||
});
|
||||
|
||||
testWidgets('deleting an account with a password asks for it and a '
|
||||
'code, which a wrong one fails, deleting nothing', (tester) async {
|
||||
// The first person the fake password gateway makes.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ void main() {
|
|||
await passwords.confirm(
|
||||
added.id,
|
||||
fakeTotpCode,
|
||||
password: 'correct horse',
|
||||
existingCode: fakeTotpCode,
|
||||
);
|
||||
await pumpEventQueue();
|
||||
|
|
@ -132,8 +133,9 @@ void main() {
|
|||
.having((state) => state.busy, 'busy', busy)
|
||||
.having((state) => state.problem?.problem, 'problem', problem);
|
||||
|
||||
test('takes its code and one from an authenticator added before, '
|
||||
'refusing a wrong one, nothing added meanwhile', () async {
|
||||
test("takes its code and the owner's proof (the password and a code "
|
||||
'from an authenticator added before), refusing a wrong one, nothing '
|
||||
'added meanwhile', () async {
|
||||
final cubit = AddAuthenticatorCubit(
|
||||
passwords,
|
||||
label: 'anna@example.org',
|
||||
|
|
@ -151,17 +153,37 @@ void main() {
|
|||
expect(
|
||||
await statesOf(
|
||||
cubit,
|
||||
() => cubit.codeEntered(fakeTotpCode, existingCode: '000000'),
|
||||
() => cubit.codeEntered(
|
||||
fakeTotpCode,
|
||||
password: 'correct horse',
|
||||
existingCode: '000000',
|
||||
),
|
||||
),
|
||||
[
|
||||
at(AddAuthenticatorStep.code, busy: true),
|
||||
at(
|
||||
AddAuthenticatorStep.code,
|
||||
problem: PasswordProblem.wrongExistingCode,
|
||||
problem: PasswordProblem.wrongCredentials,
|
||||
),
|
||||
],
|
||||
);
|
||||
await cubit.codeEntered('000000', existingCode: fakeTotpCode);
|
||||
await cubit.codeEntered(
|
||||
fakeTotpCode,
|
||||
password: 'wrong',
|
||||
existingCode: fakeTotpCode,
|
||||
);
|
||||
expect(
|
||||
cubit.state,
|
||||
at(
|
||||
AddAuthenticatorStep.code,
|
||||
problem: PasswordProblem.wrongCredentials,
|
||||
),
|
||||
);
|
||||
await cubit.codeEntered(
|
||||
'000000',
|
||||
password: 'correct horse',
|
||||
existingCode: fakeTotpCode,
|
||||
);
|
||||
expect(
|
||||
cubit.state,
|
||||
at(AddAuthenticatorStep.code, problem: PasswordProblem.wrongCode),
|
||||
|
|
@ -171,7 +193,11 @@ void main() {
|
|||
expect(
|
||||
await statesOf(
|
||||
cubit,
|
||||
() => cubit.codeEntered(fakeTotpCode, existingCode: fakeTotpCode),
|
||||
() => cubit.codeEntered(
|
||||
fakeTotpCode,
|
||||
password: 'correct horse',
|
||||
existingCode: fakeTotpCode,
|
||||
),
|
||||
),
|
||||
[
|
||||
at(AddAuthenticatorStep.code, busy: true),
|
||||
|
|
@ -264,7 +290,6 @@ void main() {
|
|||
['wrong', 'wrong', 'wrong'],
|
||||
password: 'correct horse',
|
||||
code: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
expect(cubit.state, at(NewPhraseStep.check, phraseWrong: true));
|
||||
// Nothing reached the instance before the words were right.
|
||||
|
|
@ -277,7 +302,6 @@ void main() {
|
|||
wordsAsked(cubit.state),
|
||||
password: 'wrong',
|
||||
code: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
),
|
||||
[
|
||||
|
|
@ -289,7 +313,6 @@ void main() {
|
|||
wordsAsked(cubit.state),
|
||||
password: 'correct horse',
|
||||
code: '000000',
|
||||
language: 'en',
|
||||
);
|
||||
expect(
|
||||
cubit.state,
|
||||
|
|
@ -304,7 +327,6 @@ void main() {
|
|||
wordsAsked(cubit.state),
|
||||
password: 'correct horse',
|
||||
code: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ void main() {
|
|||
await settle();
|
||||
|
||||
await expectLater(
|
||||
passwords.deleteAccount(password: 'battery staple', language: 'en'),
|
||||
passwords.deleteAccount(password: 'battery staple'),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
final mailed = await passwords.mailDeletionCode(language: 'en');
|
||||
|
|
@ -204,7 +204,6 @@ void main() {
|
|||
password: 'battery staple',
|
||||
request: mailed.request,
|
||||
code: gateway.codeFor('carla@example.org'),
|
||||
language: 'en',
|
||||
);
|
||||
expect(gateway.accounts, isNot(contains('carla@example.org')));
|
||||
});
|
||||
|
|
@ -317,7 +316,6 @@ void main() {
|
|||
oldPassword: 'correct horse',
|
||||
newPassword: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
expect(anna.password, 'better horse');
|
||||
|
|
@ -340,7 +338,6 @@ void main() {
|
|||
oldPassword: 'wrong',
|
||||
newPassword: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
|
|
@ -350,44 +347,36 @@ void main() {
|
|||
oldPassword: 'correct horse',
|
||||
newPassword: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.keysMissing),
|
||||
);
|
||||
expect(anna.password, 'correct horse');
|
||||
});
|
||||
|
||||
test('sends the address this device signed in with, and says when it '
|
||||
"doesn't know it, sending nothing", () async {
|
||||
test("takes no address while signed in: a device that doesn't know it "
|
||||
'changes the password, and asks for one only to mail a code for '
|
||||
'deleting', () async {
|
||||
addresses.addresses.clear();
|
||||
await passwords.changePassword(
|
||||
oldPassword: 'correct horse',
|
||||
newPassword: 'better horse',
|
||||
newPassword: 'better horses',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
expect(anna.password, 'better horse');
|
||||
expect(anna.password, 'better horses');
|
||||
|
||||
addresses.addresses.clear();
|
||||
final calls = gateway.calls.length;
|
||||
await expectLater(
|
||||
passwords.changePassword(
|
||||
oldPassword: 'better horse',
|
||||
newPassword: 'best horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.addressUnknown),
|
||||
);
|
||||
await expectLater(
|
||||
passwords.deleteAccount(
|
||||
password: 'better horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
passwords.mailDeletionCode(language: 'en'),
|
||||
failsWith(PasswordProblem.addressUnknown),
|
||||
);
|
||||
expect(gateway.calls, hasLength(calls));
|
||||
expect(anna.password, 'better horse');
|
||||
expect(
|
||||
(await passwords.mailDeletionCode(
|
||||
language: 'en',
|
||||
address: ' anna@example.org ',
|
||||
)).address,
|
||||
'anna@example.org',
|
||||
);
|
||||
});
|
||||
|
||||
test('deletes the account with the password and a code alone, then '
|
||||
|
|
@ -396,12 +385,11 @@ void main() {
|
|||
passwords.deleteAccount(
|
||||
password: 'wrong',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
passwords.deleteAccount(password: 'correct horse', language: 'en'),
|
||||
passwords.deleteAccount(password: 'correct horse'),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
expect(gateway.accounts, contains('anna@example.org'));
|
||||
|
|
@ -410,7 +398,6 @@ void main() {
|
|||
await passwords.deleteAccount(
|
||||
password: 'correct horse',
|
||||
totpCode: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
expect(gateway.accounts, isNot(contains('anna@example.org')));
|
||||
|
|
@ -426,7 +413,6 @@ void main() {
|
|||
phrase,
|
||||
password: 'correct horse',
|
||||
code: '000000',
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
|
|
@ -435,7 +421,6 @@ void main() {
|
|||
phrase,
|
||||
password: 'wrong',
|
||||
code: fakeTotpCode,
|
||||
language: 'en',
|
||||
),
|
||||
failsWith(PasswordProblem.wrongCredentials),
|
||||
);
|
||||
|
|
@ -445,7 +430,6 @@ void main() {
|
|||
phrase,
|
||||
password: 'correct horse',
|
||||
code: fakeTotpCode,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
await auth.signOut();
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue