fix(auth): every call with a code after the password takes the address
Whoever guessed codes decided whether the owner was mailed (the re-review's fourth finding): the address a sign-in sent was optional, so a guesser left it out and no mail went, and a wait begun without a mail used up the one mail there was. Every call carrying a code after the password or the phrase proved (signing in, the phrase's reset, a change of password, a new phrase, deletion) now takes the account's address and the language of its mails, and is refused (wrongCredentials, as anything wrong after the password) before any code is checked when the address isn't the account's; every wait mails the owner, not only the first. The app sends the address the person typed, or for changes while signed in the one this device signed in with, and says so where it doesn't know it (addressUnknown). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e8c4a3d472
commit
2a21672b43
56 changed files with 795 additions and 347 deletions
|
|
@ -211,5 +211,6 @@
|
||||||
"passwordTooShort": "Ein neues Passwort braucht mindestens {count} Zeichen: Sollte die Datenbank der Instanz je nach außen gelangen, wird ein kurzes zuerst erraten.",
|
"passwordTooShort": "Ein neues Passwort braucht mindestens {count} Zeichen: Sollte die Datenbank der Instanz je nach außen gelangen, wird ein kurzes zuerst erraten.",
|
||||||
"passwordCodesWait": "Zu viele falsche Codes hintereinander: Bis {date} wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann.",
|
"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.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -458,5 +458,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1374,6 +1374,12 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Too many wrong codes in a row from this device: it was signed out. Sign in again with your password and a code.'**
|
/// **'Too many wrong codes in a row from this device: it was signed out. Sign in again with your password and a code.'**
|
||||||
String get passwordSessionEnded;
|
String get passwordSessionEnded;
|
||||||
|
|
||||||
|
/// 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.'**
|
||||||
|
String get passwordAddressUnknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HalkyonAppLocalizationsDelegate
|
class _HalkyonAppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -796,4 +796,8 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get passwordSessionEnded =>
|
String get passwordSessionEnded =>
|
||||||
'Zu viele falsche Codes hintereinander von diesem Gerät: Es wurde abgemeldet. Melde dich wieder mit deinem Passwort und einem Code an.';
|
'Zu viele falsche Codes hintereinander von diesem Gerät: Es wurde abgemeldet. Melde dich wieder mit deinem Passwort und einem Code an.';
|
||||||
|
|
||||||
|
@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.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -779,6 +779,10 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get passwordSessionEnded =>
|
String get passwordSessionEnded =>
|
||||||
'Too many wrong codes in a row from this device: it was signed out. Sign in again with your password and a code.';
|
'Too many wrong codes in a row from this device: it was signed out. Sign in again with your password and a code.';
|
||||||
|
|
||||||
|
@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.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -264,11 +264,12 @@ class NewPhraseCubit(final PasswordRepository _passwords)
|
||||||
emit(NewPhraseState(step: NewPhraseStep.shown, phrase: state.phrase));
|
emit(NewPhraseState(step: NewPhraseStep.shown, phrase: state.phrase));
|
||||||
|
|
||||||
/// The words typed back, the [password] and a [code]: the instance keeps
|
/// The words typed back, the [password] and a [code]: the instance keeps
|
||||||
/// it.
|
/// it; [language] words the owner's mail should codes come to wait.
|
||||||
Future<void> checked(
|
Future<void> checked(
|
||||||
List<String> words, {
|
List<String> words, {
|
||||||
required String password,
|
required String password,
|
||||||
required String code,
|
required String code,
|
||||||
|
required String language,
|
||||||
}) async {
|
}) async {
|
||||||
if (!checksPhrase(state.phrase, state.checked, words)) {
|
if (!checksPhrase(state.phrase, state.checked, words)) {
|
||||||
emit(_again(phraseWrong: true));
|
emit(_again(phraseWrong: true));
|
||||||
|
|
@ -276,7 +277,12 @@ class NewPhraseCubit(final PasswordRepository _passwords)
|
||||||
}
|
}
|
||||||
emit(_again(busy: true));
|
emit(_again(busy: true));
|
||||||
try {
|
try {
|
||||||
await _passwords.keepPhrase(_made!, password: password, code: code);
|
await _passwords.keepPhrase(
|
||||||
|
_made!,
|
||||||
|
password: password,
|
||||||
|
code: code,
|
||||||
|
language: language,
|
||||||
|
);
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
emit(NewPhraseState(step: NewPhraseStep.done, phrase: state.phrase));
|
emit(NewPhraseState(step: NewPhraseStep.done, phrase: state.phrase));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ abstract interface class PasswordGateway() {
|
||||||
|
|
||||||
/// A sign-in's second step: [answer] to [challenge], with a TOTP code, a
|
/// 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);
|
/// recovery code, or a code mailed for [request] (a setup never finished);
|
||||||
/// [address] and [language] only so its owner is told should codes come
|
/// [address], the one begun with, and [language]: the instance checks it
|
||||||
/// to wait.
|
/// before any code, and tells its owner should codes come to wait.
|
||||||
Future<PasswordSignedIn> complete(
|
Future<PasswordSignedIn> complete(
|
||||||
protocol.PasswordChallenge challenge,
|
protocol.PasswordChallenge challenge,
|
||||||
SrpAnswer answer, {
|
SrpAnswer answer, {
|
||||||
|
|
@ -55,8 +55,8 @@ abstract interface class PasswordGateway() {
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? request,
|
String? request,
|
||||||
String? code,
|
String? code,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Signs up with the address a [code] mailed for [request] proves.
|
/// Signs up with the address a [code] mailed for [request] proves.
|
||||||
|
|
@ -107,21 +107,28 @@ abstract interface class PasswordGateway() {
|
||||||
|
|
||||||
/// Deletes the signed-in person's account, which has a password: [answer]
|
/// Deletes the signed-in person's account, which has a password: [answer]
|
||||||
/// to [challenge] ([beginChange]) proves it, with a [totpCode] or
|
/// to [challenge] ([beginChange]) proves it, with a [totpCode] or
|
||||||
/// [recoveryCode]; this device then forgets its session.
|
/// [recoveryCode], with the account's [address] and [language] (as a
|
||||||
|
/// change takes them); this device then forgets its session.
|
||||||
Future<void> deleteAccount(
|
Future<void> deleteAccount(
|
||||||
protocol.PasswordChallenge challenge,
|
protocol.PasswordChallenge challenge,
|
||||||
SrpAnswer answer, {
|
SrpAnswer answer, {
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Changes the password: [answer] to [challenge] proves the old one.
|
/// 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.
|
||||||
Future<void> change(
|
Future<void> change(
|
||||||
protocol.PasswordChallenge challenge,
|
protocol.PasswordChallenge challenge,
|
||||||
SrpAnswer answer, {
|
SrpAnswer answer, {
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required protocol.NewPassword password,
|
required protocol.NewPassword password,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// A new recovery phrase's wrapping of the keys and its [proofKey]'s
|
/// A new recovery phrase's wrapping of the keys and its [proofKey]'s
|
||||||
|
|
@ -134,6 +141,8 @@ abstract interface class PasswordGateway() {
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required protocol.WrappedKeys keys,
|
required protocol.WrappedKeys keys,
|
||||||
required ByteData proofKey,
|
required ByteData proofKey,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Asks for the reset of a lost authenticator: [answer] to [challenge]
|
/// Asks for the reset of a lost authenticator: [answer] to [challenge]
|
||||||
|
|
@ -161,8 +170,8 @@ abstract interface class PasswordGateway() {
|
||||||
required protocol.NewPassword password,
|
required protocol.NewPassword password,
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Asks for the reset of the account at [address]; when it takes effect.
|
/// Asks for the reset of the account at [address]; when it takes effect.
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ class PasswordRepository(
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? request,
|
String? request,
|
||||||
String? code,
|
String? code,
|
||||||
String? language,
|
required String language,
|
||||||
}) async {
|
}) async {
|
||||||
final challenge = await _challengeOf(unlocked);
|
final challenge = await _challengeOf(unlocked);
|
||||||
final answer = await _crypto.answer(
|
final answer = await _crypto.answer(
|
||||||
|
|
@ -325,8 +325,10 @@ class PasswordRepository(
|
||||||
required String newPassword,
|
required String newPassword,
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
|
required String language,
|
||||||
}) async {
|
}) async {
|
||||||
final keys = await _keysHere();
|
final keys = await _keysHere();
|
||||||
|
final address = await _addressHere();
|
||||||
final challenge = await _gateway.beginChange();
|
final challenge = await _gateway.beginChange();
|
||||||
final unlock = await _crypto.unlock(
|
final unlock = await _crypto.unlock(
|
||||||
oldPassword,
|
oldPassword,
|
||||||
|
|
@ -340,6 +342,8 @@ class PasswordRepository(
|
||||||
totpCode: totpCode?.replaceAll(' ', ''),
|
totpCode: totpCode?.replaceAll(' ', ''),
|
||||||
recoveryCode: recoveryCode,
|
recoveryCode: recoveryCode,
|
||||||
password: await _crypto.newPassword(newPassword, keys),
|
password: await _crypto.newPassword(newPassword, keys),
|
||||||
|
address: address,
|
||||||
|
language: language,
|
||||||
);
|
);
|
||||||
_changed();
|
_changed();
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +356,9 @@ class PasswordRepository(
|
||||||
required String password,
|
required String password,
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
|
required String language,
|
||||||
}) async {
|
}) async {
|
||||||
|
final address = await _addressHere();
|
||||||
final challenge = await _gateway.beginChange();
|
final challenge = await _gateway.beginChange();
|
||||||
final unlock = await _crypto.unlock(
|
final unlock = await _crypto.unlock(
|
||||||
password,
|
password,
|
||||||
|
|
@ -364,6 +370,8 @@ class PasswordRepository(
|
||||||
await _crypto.answer(unlock, challenge, host: _gateway.host),
|
await _crypto.answer(unlock, challenge, host: _gateway.host),
|
||||||
totpCode: totpCode?.replaceAll(' ', ''),
|
totpCode: totpCode?.replaceAll(' ', ''),
|
||||||
recoveryCode: recoveryCode,
|
recoveryCode: recoveryCode,
|
||||||
|
address: address,
|
||||||
|
language: language,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,7 +391,9 @@ class PasswordRepository(
|
||||||
NewPhrase phrase, {
|
NewPhrase phrase, {
|
||||||
required String password,
|
required String password,
|
||||||
required String code,
|
required String code,
|
||||||
|
required String language,
|
||||||
}) async {
|
}) async {
|
||||||
|
final address = await _addressHere();
|
||||||
final challenge = await _gateway.beginChange();
|
final challenge = await _gateway.beginChange();
|
||||||
final unlock = await _crypto.unlock(
|
final unlock = await _crypto.unlock(
|
||||||
password,
|
password,
|
||||||
|
|
@ -396,6 +406,8 @@ class PasswordRepository(
|
||||||
totpCode: code.replaceAll(' ', ''),
|
totpCode: code.replaceAll(' ', ''),
|
||||||
keys: phrase._recoveryKeys,
|
keys: phrase._recoveryKeys,
|
||||||
proofKey: phrase._recoveryProofKey,
|
proofKey: phrase._recoveryProofKey,
|
||||||
|
address: address,
|
||||||
|
language: language,
|
||||||
);
|
);
|
||||||
_changed();
|
_changed();
|
||||||
}
|
}
|
||||||
|
|
@ -453,7 +465,7 @@ class PasswordRepository(
|
||||||
required String newPassword,
|
required String newPassword,
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? language,
|
required String language,
|
||||||
}) async {
|
}) async {
|
||||||
final typed = address.trim();
|
final typed = address.trim();
|
||||||
final challenge = await _gateway.beginKeyProof(typed);
|
final challenge = await _gateway.beginKeyProof(typed);
|
||||||
|
|
@ -551,4 +563,13 @@ class PasswordRepository(
|
||||||
Future<UnlockedAccountKeys> _keysHere() async =>
|
Future<UnlockedAccountKeys> _keysHere() async =>
|
||||||
await _keys.read(_signedIn()) ??
|
await _keys.read(_signedIn()) ??
|
||||||
(throw const PasswordFailed(PasswordProblem.keysMissing));
|
(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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ class ServerpodPasswordGateway(
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? request,
|
String? request,
|
||||||
String? code,
|
String? code,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
}) => _signedOut((base) async {
|
}) => _signedOut((base) async {
|
||||||
final signedIn = await base.modules.passwordSignIn.complete(
|
final signedIn = await base.modules.passwordSignIn.complete(
|
||||||
challenge: challenge.challenge,
|
challenge: challenge.challenge,
|
||||||
|
|
@ -185,6 +185,8 @@ class ServerpodPasswordGateway(
|
||||||
SrpAnswer answer, {
|
SrpAnswer answer, {
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) => _failing(() async {
|
}) => _failing(() async {
|
||||||
await _base.account.delete(
|
await _base.account.delete(
|
||||||
proof: protocol.DeletionProof(
|
proof: protocol.DeletionProof(
|
||||||
|
|
@ -194,6 +196,8 @@ class ServerpodPasswordGateway(
|
||||||
totpCode: totpCode,
|
totpCode: totpCode,
|
||||||
recoveryCode: recoveryCode,
|
recoveryCode: recoveryCode,
|
||||||
host: host,
|
host: host,
|
||||||
|
address: address,
|
||||||
|
language: language,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// The server ended every session of the person; forget this device's
|
// The server ended every session of the person; forget this device's
|
||||||
|
|
@ -208,6 +212,8 @@ class ServerpodPasswordGateway(
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required protocol.NewPassword password,
|
required protocol.NewPassword password,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) => _failing(
|
}) => _failing(
|
||||||
() => _base.password.change(
|
() => _base.password.change(
|
||||||
challenge: challenge.challenge,
|
challenge: challenge.challenge,
|
||||||
|
|
@ -217,6 +223,8 @@ class ServerpodPasswordGateway(
|
||||||
recoveryCode: recoveryCode,
|
recoveryCode: recoveryCode,
|
||||||
password: password,
|
password: password,
|
||||||
host: host,
|
host: host,
|
||||||
|
address: address,
|
||||||
|
language: language,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -228,6 +236,8 @@ class ServerpodPasswordGateway(
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required protocol.WrappedKeys keys,
|
required protocol.WrappedKeys keys,
|
||||||
required ByteData proofKey,
|
required ByteData proofKey,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) => _failing(
|
}) => _failing(
|
||||||
() => _base.password.setRecoveryKeys(
|
() => _base.password.setRecoveryKeys(
|
||||||
challenge: challenge.challenge,
|
challenge: challenge.challenge,
|
||||||
|
|
@ -238,6 +248,8 @@ class ServerpodPasswordGateway(
|
||||||
recoveryKeys: keys,
|
recoveryKeys: keys,
|
||||||
recoveryProofKey: proofKey,
|
recoveryProofKey: proofKey,
|
||||||
host: host,
|
host: host,
|
||||||
|
address: address,
|
||||||
|
language: language,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -279,8 +291,8 @@ class ServerpodPasswordGateway(
|
||||||
required protocol.NewPassword password,
|
required protocol.NewPassword password,
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
}) => _signedOut(
|
}) => _signedOut(
|
||||||
(base) async => _grantOf(
|
(base) async => _grantOf(
|
||||||
await base.modules.accountRecovery.resetPassword(
|
await base.modules.accountRecovery.resetPassword(
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ enum PasswordProblem() {
|
||||||
/// The account has no address yet.
|
/// The account has no address yet.
|
||||||
noAddress,
|
noAddress,
|
||||||
|
|
||||||
|
/// This device doesn't know the account's address, which a change while
|
||||||
|
/// signed in takes: sign in with it again.
|
||||||
|
addressUnknown,
|
||||||
|
|
||||||
/// The account has no password yet.
|
/// The account has no password yet.
|
||||||
noPassword,
|
noPassword,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -240,11 +240,13 @@ class _ChangePasswordState() extends State<_ChangePassword> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final passwords = _passwordsOf(context);
|
final passwords = _passwordsOf(context);
|
||||||
|
final language = mailLanguageOf(context);
|
||||||
if (await _step.run(() async {
|
if (await _step.run(() async {
|
||||||
await passwords.changePassword(
|
await passwords.changePassword(
|
||||||
oldPassword: _old.text,
|
oldPassword: _old.text,
|
||||||
newPassword: _new.text,
|
newPassword: _new.text,
|
||||||
totpCode: _code.text,
|
totpCode: _code.text,
|
||||||
|
language: language,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
})) {
|
})) {
|
||||||
|
|
@ -601,6 +603,7 @@ class _NewPhraseState() extends State<_NewPhrase> {
|
||||||
[for (final word in _words) word.text],
|
[for (final word in _words) word.text],
|
||||||
password: _password.text,
|
password: _password.text,
|
||||||
code: _code.text,
|
code: _code.text,
|
||||||
|
language: mailLanguageOf(context),
|
||||||
);
|
);
|
||||||
case NewPhraseStep.making || NewPhraseStep.done:
|
case NewPhraseStep.making || NewPhraseStep.done:
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,12 @@ class _DeleteAccountState() extends State<_DeleteAccount> {
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
final passwords = AppScope.of(context).passwords;
|
final passwords = AppScope.of(context).passwords;
|
||||||
|
final language = mailLanguageOf(context);
|
||||||
await _step.run(() async {
|
await _step.run(() async {
|
||||||
await passwords.deleteAccount(
|
await passwords.deleteAccount(
|
||||||
password: _password.text,
|
password: _password.text,
|
||||||
totpCode: _code.text,
|
totpCode: _code.text,
|
||||||
|
language: language,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ String passwordProblemText(BuildContext context, PasswordFailed failed) {
|
||||||
PasswordProblem.passwordExists => l10n.passwordExists,
|
PasswordProblem.passwordExists => l10n.passwordExists,
|
||||||
PasswordProblem.addressTaken => l10n.passwordAddressTaken,
|
PasswordProblem.addressTaken => l10n.passwordAddressTaken,
|
||||||
PasswordProblem.noAddress => l10n.passwordNoAddress,
|
PasswordProblem.noAddress => l10n.passwordNoAddress,
|
||||||
|
PasswordProblem.addressUnknown => l10n.passwordAddressUnknown,
|
||||||
PasswordProblem.noPassword => l10n.passwordNoPassword,
|
PasswordProblem.noPassword => l10n.passwordNoPassword,
|
||||||
PasswordProblem.wrongCode => l10n.passwordWrongCode,
|
PasswordProblem.wrongCode => l10n.passwordWrongCode,
|
||||||
PasswordProblem.wrongExistingCode => l10n.passwordWrongExistingCode,
|
PasswordProblem.wrongExistingCode => l10n.passwordWrongExistingCode,
|
||||||
|
|
|
||||||
|
|
@ -294,11 +294,13 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? request,
|
String? request,
|
||||||
String? code,
|
String? code,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
}) => _call('complete', () {
|
}) => _call('complete', () {
|
||||||
lastCompletedFor = (address: address, language: language);
|
lastCompletedFor = (address: address, language: language);
|
||||||
final account = _proved(challenge, answer);
|
final account = _proved(challenge, answer);
|
||||||
|
// The address begun with, checked before any code.
|
||||||
|
_requireAddress(account, address);
|
||||||
final bool needsAuthenticator;
|
final bool needsAuthenticator;
|
||||||
if (request != null && code != null && !account.finished) {
|
if (request != null && code != null && !account.finished) {
|
||||||
if (_proven(request, code) != account.address) {
|
if (_proven(request, code) != account.address) {
|
||||||
|
|
@ -332,6 +334,14 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Refused as the register refuses [address] when it isn't [account]'s:
|
||||||
|
/// before any code is checked.
|
||||||
|
void _requireAddress(FakePasswordAccount account, String address) {
|
||||||
|
if (address.trim().toLowerCase() != account.address) {
|
||||||
|
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _setUp(FakePasswordAccount account, protocol.PasswordSetup setup) {
|
void _setUp(FakePasswordAccount account, protocol.PasswordSetup setup) {
|
||||||
account
|
account
|
||||||
..password = utf8.decode(Uint8List.sublistView(setup.verifier))
|
..password = utf8.decode(Uint8List.sublistView(setup.verifier))
|
||||||
|
|
@ -518,10 +528,12 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
SrpAnswer answer, {
|
SrpAnswer answer, {
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) async {
|
}) async {
|
||||||
final account = await _call(
|
final account = await _call(
|
||||||
'deleteAccount',
|
'deleteAccount',
|
||||||
() => _ownerProved(challenge, answer, totpCode, recoveryCode),
|
() => _ownerProved(challenge, answer, totpCode, recoveryCode, address),
|
||||||
);
|
);
|
||||||
accounts.remove(account.address);
|
accounts.remove(account.address);
|
||||||
await _auth.deleteAccount();
|
await _auth.deleteAccount();
|
||||||
|
|
@ -534,8 +546,10 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required protocol.NewPassword password,
|
required protocol.NewPassword password,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) => _call('change', () {
|
}) => _call('change', () {
|
||||||
_ownerProved(challenge, answer, totpCode, recoveryCode)
|
_ownerProved(challenge, answer, totpCode, recoveryCode, address)
|
||||||
..password = utf8.decode(Uint8List.sublistView(password.verifier))
|
..password = utf8.decode(Uint8List.sublistView(password.verifier))
|
||||||
..passwordKeys = password.passwordKeys;
|
..passwordKeys = password.passwordKeys;
|
||||||
});
|
});
|
||||||
|
|
@ -548,12 +562,14 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
SrpAnswer answer,
|
SrpAnswer answer,
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
|
String address,
|
||||||
) {
|
) {
|
||||||
final account = _proved(challenge, answer);
|
final account = _proved(challenge, answer);
|
||||||
if (account.person != _signedIn() ||
|
if (account.person != _signedIn() ||
|
||||||
((totpCode ?? '').isEmpty && (recoveryCode ?? '').isEmpty)) {
|
((totpCode ?? '').isEmpty && (recoveryCode ?? '').isEmpty)) {
|
||||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||||
}
|
}
|
||||||
|
_requireAddress(account, address);
|
||||||
if (_secondFactor(
|
if (_secondFactor(
|
||||||
account,
|
account,
|
||||||
totpCode: totpCode,
|
totpCode: totpCode,
|
||||||
|
|
@ -572,8 +588,10 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required protocol.WrappedKeys keys,
|
required protocol.WrappedKeys keys,
|
||||||
required ByteData proofKey,
|
required ByteData proofKey,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) => _call('setRecoveryKeys', () {
|
}) => _call('setRecoveryKeys', () {
|
||||||
_ownerProved(challenge, answer, totpCode, recoveryCode)
|
_ownerProved(challenge, answer, totpCode, recoveryCode, address)
|
||||||
..recoveryKeys = keys
|
..recoveryKeys = keys
|
||||||
..recoveryProofKey = proofKey;
|
..recoveryProofKey = proofKey;
|
||||||
});
|
});
|
||||||
|
|
@ -636,8 +654,8 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
required protocol.NewPassword password,
|
required protocol.NewPassword password,
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
}) => _call('resetPassword', () {
|
}) => _call('resetPassword', () {
|
||||||
// The challenge names the account, whose phrase's proof key must have
|
// The challenge names the account, whose phrase's proof key must have
|
||||||
// signed: never the account's own keys.
|
// signed: never the account's own keys.
|
||||||
|
|
@ -646,6 +664,7 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
!FakeAccountCrypto.proves(signature, account!.recoveryProofKey)) {
|
!FakeAccountCrypto.proves(signature, account!.recoveryProofKey)) {
|
||||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||||
}
|
}
|
||||||
|
_requireAddress(account, address);
|
||||||
_secondFactor(account, totpCode: totpCode, recoveryCode: recoveryCode);
|
_secondFactor(account, totpCode: totpCode, recoveryCode: recoveryCode);
|
||||||
account
|
account
|
||||||
..password = utf8.decode(Uint8List.sublistView(password.verifier))
|
..password = utf8.decode(Uint8List.sublistView(password.verifier))
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,8 @@ void main() {
|
||||||
password: 'correct horse',
|
password: 'correct horse',
|
||||||
crypto: dependencies.crypto,
|
crypto: dependencies.crypto,
|
||||||
);
|
);
|
||||||
|
// The address this device signed in with, which deleting sends.
|
||||||
|
dependencies.addresses.addresses[person] = 'anna@example.org';
|
||||||
Finder field(String label) => find.widgetWithText(TextField, label);
|
Finder field(String label) => find.widgetWithText(TextField, label);
|
||||||
|
|
||||||
await tester.tap(find.text('Delete account'));
|
await tester.tap(find.text('Delete account'));
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ void main() {
|
||||||
await passwords.signIn(
|
await passwords.signIn(
|
||||||
await passwords.unlock('anna@example.org', 'correct horse'),
|
await passwords.unlock('anna@example.org', 'correct horse'),
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
gateway.calls.clear();
|
gateway.calls.clear();
|
||||||
|
|
@ -263,6 +264,7 @@ void main() {
|
||||||
['wrong', 'wrong', 'wrong'],
|
['wrong', 'wrong', 'wrong'],
|
||||||
password: 'correct horse',
|
password: 'correct horse',
|
||||||
code: fakeTotpCode,
|
code: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
expect(cubit.state, at(NewPhraseStep.check, phraseWrong: true));
|
expect(cubit.state, at(NewPhraseStep.check, phraseWrong: true));
|
||||||
// Nothing reached the instance before the words were right.
|
// Nothing reached the instance before the words were right.
|
||||||
|
|
@ -275,6 +277,7 @@ void main() {
|
||||||
wordsAsked(cubit.state),
|
wordsAsked(cubit.state),
|
||||||
password: 'wrong',
|
password: 'wrong',
|
||||||
code: fakeTotpCode,
|
code: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
|
|
@ -286,6 +289,7 @@ void main() {
|
||||||
wordsAsked(cubit.state),
|
wordsAsked(cubit.state),
|
||||||
password: 'correct horse',
|
password: 'correct horse',
|
||||||
code: '000000',
|
code: '000000',
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
cubit.state,
|
cubit.state,
|
||||||
|
|
@ -300,6 +304,7 @@ void main() {
|
||||||
wordsAsked(cubit.state),
|
wordsAsked(cubit.state),
|
||||||
password: 'correct horse',
|
password: 'correct horse',
|
||||||
code: fakeTotpCode,
|
code: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ void main() {
|
||||||
final pending = await passwords.signIn(
|
final pending = await passwords.signIn(
|
||||||
unlocked,
|
unlocked,
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(pending.needsAuthenticator, isFalse);
|
expect(pending.needsAuthenticator, isFalse);
|
||||||
|
|
@ -71,7 +72,7 @@ void main() {
|
||||||
'and begins a new challenge for the next try', () async {
|
'and begins a new challenge for the next try', () async {
|
||||||
final wrong = await passwords.unlock('anna@example.org', 'wrong');
|
final wrong = await passwords.unlock('anna@example.org', 'wrong');
|
||||||
await expectLater(
|
await expectLater(
|
||||||
passwords.signIn(wrong, totpCode: fakeTotpCode),
|
passwords.signIn(wrong, totpCode: fakeTotpCode, language: 'en'),
|
||||||
failsWith(PasswordProblem.wrongCredentials),
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
);
|
);
|
||||||
final right = await passwords.unlock(
|
final right = await passwords.unlock(
|
||||||
|
|
@ -79,11 +80,11 @@ void main() {
|
||||||
'correct horse',
|
'correct horse',
|
||||||
);
|
);
|
||||||
await expectLater(
|
await expectLater(
|
||||||
passwords.signIn(right, totpCode: '000000'),
|
passwords.signIn(right, totpCode: '000000', language: 'en'),
|
||||||
failsWith(PasswordProblem.wrongCredentials),
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
);
|
);
|
||||||
|
|
||||||
await passwords.signIn(right, totpCode: fakeTotpCode);
|
await passwords.signIn(right, totpCode: fakeTotpCode, language: 'en');
|
||||||
|
|
||||||
expect(gateway.calls.where((call) => call == 'begin'), hasLength(3));
|
expect(gateway.calls.where((call) => call == 'begin'), hasLength(3));
|
||||||
expect(auth.status, const SignedOut());
|
expect(auth.status, const SignedOut());
|
||||||
|
|
@ -98,7 +99,8 @@ void main() {
|
||||||
|
|
||||||
await withClock(
|
await withClock(
|
||||||
Clock.fixed(DateTime.utc(2026, 10, 20, 12, 2)),
|
Clock.fixed(DateTime.utc(2026, 10, 20, 12, 2)),
|
||||||
() => passwords.signIn(unlocked, totpCode: fakeTotpCode),
|
() =>
|
||||||
|
passwords.signIn(unlocked, totpCode: fakeTotpCode, language: 'en'),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(gateway.calls, ['begin', 'begin', 'complete']);
|
expect(gateway.calls, ['begin', 'begin', 'complete']);
|
||||||
|
|
@ -113,7 +115,7 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
passwords.signIn(unlocked, totpCode: fakeTotpCode),
|
passwords.signIn(unlocked, totpCode: fakeTotpCode, language: 'en'),
|
||||||
failsWith(PasswordProblem.instanceUnproven),
|
failsWith(PasswordProblem.instanceUnproven),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -131,7 +133,7 @@ void main() {
|
||||||
'battery staple',
|
'battery staple',
|
||||||
);
|
);
|
||||||
await expectLater(
|
await expectLater(
|
||||||
passwords.signIn(unlocked),
|
passwords.signIn(unlocked, language: 'en'),
|
||||||
failsWith(PasswordProblem.setupIncomplete),
|
failsWith(PasswordProblem.setupIncomplete),
|
||||||
);
|
);
|
||||||
final request = await passwords.sendCode(
|
final request = await passwords.sendCode(
|
||||||
|
|
@ -143,6 +145,7 @@ void main() {
|
||||||
unlocked,
|
unlocked,
|
||||||
request: request,
|
request: request,
|
||||||
code: gateway.codeFor('carla@example.org'),
|
code: gateway.codeFor('carla@example.org'),
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
expect(pending.needsAuthenticator, isTrue);
|
expect(pending.needsAuthenticator, isTrue);
|
||||||
final setup = await passwords.enrol(
|
final setup = await passwords.enrol(
|
||||||
|
|
@ -255,6 +258,7 @@ void main() {
|
||||||
final pending = await passwords.signIn(
|
final pending = await passwords.signIn(
|
||||||
await passwords.unlock('anna@example.org', 'correct horse'),
|
await passwords.unlock('anna@example.org', 'correct horse'),
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
await passwords.enter(pending);
|
await passwords.enter(pending);
|
||||||
await settle();
|
await settle();
|
||||||
|
|
@ -266,6 +270,7 @@ void main() {
|
||||||
oldPassword: 'correct horse',
|
oldPassword: 'correct horse',
|
||||||
newPassword: 'better horse',
|
newPassword: 'better horse',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(anna.password, 'better horse');
|
expect(anna.password, 'better horse');
|
||||||
|
|
@ -276,6 +281,7 @@ void main() {
|
||||||
final again = await passwords.signIn(
|
final again = await passwords.signIn(
|
||||||
unlocked,
|
unlocked,
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
expect(again.keys, keys.kept[anna.person]);
|
expect(again.keys, keys.kept[anna.person]);
|
||||||
});
|
});
|
||||||
|
|
@ -287,6 +293,7 @@ void main() {
|
||||||
oldPassword: 'wrong',
|
oldPassword: 'wrong',
|
||||||
newPassword: 'better horse',
|
newPassword: 'better horse',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
failsWith(PasswordProblem.wrongCredentials),
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
);
|
);
|
||||||
|
|
@ -296,20 +303,58 @@ void main() {
|
||||||
oldPassword: 'correct horse',
|
oldPassword: 'correct horse',
|
||||||
newPassword: 'better horse',
|
newPassword: 'better horse',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
failsWith(PasswordProblem.keysMissing),
|
failsWith(PasswordProblem.keysMissing),
|
||||||
);
|
);
|
||||||
expect(anna.password, 'correct horse');
|
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 {
|
||||||
|
await passwords.changePassword(
|
||||||
|
oldPassword: 'correct horse',
|
||||||
|
newPassword: 'better horse',
|
||||||
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
|
);
|
||||||
|
expect(anna.password, 'better horse');
|
||||||
|
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
failsWith(PasswordProblem.addressUnknown),
|
||||||
|
);
|
||||||
|
expect(gateway.calls, hasLength(calls));
|
||||||
|
expect(anna.password, 'better horse');
|
||||||
|
});
|
||||||
|
|
||||||
test('deletes the account with the password and a code alone, then '
|
test('deletes the account with the password and a code alone, then '
|
||||||
'signed out here; a wrong one or none deletes nothing', () async {
|
'signed out here; a wrong one or none deletes nothing', () async {
|
||||||
await expectLater(
|
await expectLater(
|
||||||
passwords.deleteAccount(password: 'wrong', totpCode: fakeTotpCode),
|
passwords.deleteAccount(
|
||||||
|
password: 'wrong',
|
||||||
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
|
),
|
||||||
failsWith(PasswordProblem.wrongCredentials),
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
);
|
);
|
||||||
await expectLater(
|
await expectLater(
|
||||||
passwords.deleteAccount(password: 'correct horse'),
|
passwords.deleteAccount(password: 'correct horse', language: 'en'),
|
||||||
failsWith(PasswordProblem.wrongCredentials),
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
);
|
);
|
||||||
expect(gateway.accounts, contains('anna@example.org'));
|
expect(gateway.accounts, contains('anna@example.org'));
|
||||||
|
|
@ -318,6 +363,7 @@ void main() {
|
||||||
await passwords.deleteAccount(
|
await passwords.deleteAccount(
|
||||||
password: 'correct horse',
|
password: 'correct horse',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(gateway.accounts, isNot(contains('anna@example.org')));
|
expect(gateway.accounts, isNot(contains('anna@example.org')));
|
||||||
|
|
@ -333,6 +379,7 @@ void main() {
|
||||||
phrase,
|
phrase,
|
||||||
password: 'correct horse',
|
password: 'correct horse',
|
||||||
code: '000000',
|
code: '000000',
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
failsWith(PasswordProblem.wrongCredentials),
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
);
|
);
|
||||||
|
|
@ -341,6 +388,7 @@ void main() {
|
||||||
phrase,
|
phrase,
|
||||||
password: 'wrong',
|
password: 'wrong',
|
||||||
code: fakeTotpCode,
|
code: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
failsWith(PasswordProblem.wrongCredentials),
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
);
|
);
|
||||||
|
|
@ -350,6 +398,7 @@ void main() {
|
||||||
phrase,
|
phrase,
|
||||||
password: 'correct horse',
|
password: 'correct horse',
|
||||||
code: fakeTotpCode,
|
code: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
|
|
||||||
await auth.signOut();
|
await auth.signOut();
|
||||||
|
|
@ -358,6 +407,7 @@ void main() {
|
||||||
phrase: otherFakePhrase,
|
phrase: otherFakePhrase,
|
||||||
newPassword: 'after the phrase',
|
newPassword: 'after the phrase',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
expect(pending.grant.authUserId, anna.person);
|
expect(pending.grant.authUserId, anna.person);
|
||||||
});
|
});
|
||||||
|
|
@ -392,6 +442,7 @@ void main() {
|
||||||
phrase: otherFakePhrase,
|
phrase: otherFakePhrase,
|
||||||
newPassword: 'a new password',
|
newPassword: 'a new password',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
failsWith(PasswordProblem.phraseDoesNotFit),
|
failsWith(PasswordProblem.phraseDoesNotFit),
|
||||||
);
|
);
|
||||||
|
|
@ -401,6 +452,7 @@ void main() {
|
||||||
phrase: 'too short',
|
phrase: 'too short',
|
||||||
newPassword: 'a new password',
|
newPassword: 'a new password',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
failsWith(PasswordProblem.notAPhrase),
|
failsWith(PasswordProblem.notAPhrase),
|
||||||
);
|
);
|
||||||
|
|
@ -410,6 +462,7 @@ void main() {
|
||||||
phrase: fakePhrase,
|
phrase: fakePhrase,
|
||||||
newPassword: 'a new password',
|
newPassword: 'a new password',
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(anna.password, 'a new password');
|
expect(anna.password, 'a new password');
|
||||||
|
|
@ -440,7 +493,10 @@ void main() {
|
||||||
|
|
||||||
expect(at, now.add(fakeResetDelay));
|
expect(at, now.add(fakeResetDelay));
|
||||||
await expectLater(
|
await expectLater(
|
||||||
withClock(Clock.fixed(now), () => passwords.signIn(unlocked)),
|
withClock(
|
||||||
|
Clock.fixed(now),
|
||||||
|
() => passwords.signIn(unlocked, language: 'en'),
|
||||||
|
),
|
||||||
throwsA(
|
throwsA(
|
||||||
isA<PasswordFailed>()
|
isA<PasswordFailed>()
|
||||||
.having((f) => f.problem, 'problem', PasswordProblem.notDue)
|
.having((f) => f.problem, 'problem', PasswordProblem.notDue)
|
||||||
|
|
@ -449,7 +505,7 @@ void main() {
|
||||||
);
|
);
|
||||||
final pending = await withClock(
|
final pending = await withClock(
|
||||||
Clock.fixed(at),
|
Clock.fixed(at),
|
||||||
() => passwords.signIn(unlocked),
|
() => passwords.signIn(unlocked, language: 'en'),
|
||||||
);
|
);
|
||||||
expect(pending.needsAuthenticator, isTrue);
|
expect(pending.needsAuthenticator, isTrue);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,7 @@ void main() {
|
||||||
await passwords.signIn(
|
await passwords.signIn(
|
||||||
await passwords.unlock('anna@example.org', 'correct horse'),
|
await passwords.unlock('anna@example.org', 'correct horse'),
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await show(tester);
|
await show(tester);
|
||||||
|
|
@ -549,6 +550,7 @@ void main() {
|
||||||
await passwords.signIn(
|
await passwords.signIn(
|
||||||
await passwords.unlock('anna@example.org', 'correct horse'),
|
await passwords.unlock('anna@example.org', 'correct horse'),
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final before = anna.recoveryKeys;
|
final before = anna.recoveryKeys;
|
||||||
|
|
@ -597,6 +599,7 @@ void main() {
|
||||||
await passwords.signIn(
|
await passwords.signIn(
|
||||||
await passwords.unlock('anna@example.org', 'correct horse'),
|
await passwords.unlock('anna@example.org', 'correct horse'),
|
||||||
totpCode: fakeTotpCode,
|
totpCode: fakeTotpCode,
|
||||||
|
language: 'en',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return anna;
|
return anna;
|
||||||
|
|
|
||||||
|
|
@ -278,8 +278,8 @@ class EndpointAccountRecovery extends _isc.EndpointRef {
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required String host,
|
required String host,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
}) => caller.callServerEndpoint<_iacc.AuthSuccess>(
|
}) => caller.callServerEndpoint<_iacc.AuthSuccess>(
|
||||||
'halkyon_base.accountRecovery',
|
'halkyon_base.accountRecovery',
|
||||||
'resetPassword',
|
'resetPassword',
|
||||||
|
|
@ -455,8 +455,10 @@ class EndpointPassword extends _isc.EndpointRef {
|
||||||
/// Changes the caller's password to [password] (the same keys, wrapped
|
/// Changes the caller's password to [password] (the same keys, wrapped
|
||||||
/// under the new password's key on the device), with the old password's
|
/// under the new password's key on the device), with the old password's
|
||||||
/// proof for [challenge] ([beginChange]) and a [totpCode] or
|
/// proof for [challenge] ([beginChange]) and a [totpCode] or
|
||||||
/// [recoveryCode]. Every other session of theirs ends, in every service of
|
/// [recoveryCode], and the account's [address] as the device typed it
|
||||||
/// the instance; this one stays. Refusals as `changePassword`'s.
|
/// (checked before the code; the owner is mailed in [language] should
|
||||||
|
/// codes come to wait). Every other session of theirs ends, in every
|
||||||
|
/// service of the instance; this one stays. Refusals as `changePassword`'s.
|
||||||
_ida.Future<void> change({
|
_ida.Future<void> change({
|
||||||
required _isc.UuidValue challenge,
|
required _isc.UuidValue challenge,
|
||||||
required _idt.ByteData clientValue,
|
required _idt.ByteData clientValue,
|
||||||
|
|
@ -465,6 +467,8 @@ class EndpointPassword extends _isc.EndpointRef {
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required _i95l60wq.NewPassword password,
|
required _i95l60wq.NewPassword password,
|
||||||
required String host,
|
required String host,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) => caller.callServerEndpoint<void>(
|
}) => caller.callServerEndpoint<void>(
|
||||||
'halkyon_base.password',
|
'halkyon_base.password',
|
||||||
'change',
|
'change',
|
||||||
|
|
@ -476,14 +480,17 @@ class EndpointPassword extends _isc.EndpointRef {
|
||||||
'recoveryCode': recoveryCode,
|
'recoveryCode': recoveryCode,
|
||||||
'password': password,
|
'password': password,
|
||||||
'host': host,
|
'host': host,
|
||||||
|
'address': address,
|
||||||
|
'language': language,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
||||||
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
||||||
/// public half of its proof key, the old one, with the password's proof
|
/// public half of its proof key, the old one, with the password's proof
|
||||||
/// for [challenge] ([beginChange]) and a [totpCode] or [recoveryCode], as
|
/// for [challenge] ([beginChange]) and a [totpCode] or [recoveryCode], with
|
||||||
/// [change] takes. Refusals as `setRecoveryKeys`'.
|
/// the account's [address] and [language], as [change] takes. Refusals as
|
||||||
|
/// `setRecoveryKeys`'.
|
||||||
_ida.Future<void> setRecoveryKeys({
|
_ida.Future<void> setRecoveryKeys({
|
||||||
required _isc.UuidValue challenge,
|
required _isc.UuidValue challenge,
|
||||||
required _idt.ByteData clientValue,
|
required _idt.ByteData clientValue,
|
||||||
|
|
@ -493,6 +500,8 @@ class EndpointPassword extends _isc.EndpointRef {
|
||||||
required _i3so7e0p.WrappedKeys recoveryKeys,
|
required _i3so7e0p.WrappedKeys recoveryKeys,
|
||||||
required _idt.ByteData recoveryProofKey,
|
required _idt.ByteData recoveryProofKey,
|
||||||
required String host,
|
required String host,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) => caller.callServerEndpoint<void>(
|
}) => caller.callServerEndpoint<void>(
|
||||||
'halkyon_base.password',
|
'halkyon_base.password',
|
||||||
'setRecoveryKeys',
|
'setRecoveryKeys',
|
||||||
|
|
@ -505,6 +514,8 @@ class EndpointPassword extends _isc.EndpointRef {
|
||||||
'recoveryKeys': recoveryKeys,
|
'recoveryKeys': recoveryKeys,
|
||||||
'recoveryProofKey': recoveryProofKey,
|
'recoveryProofKey': recoveryProofKey,
|
||||||
'host': host,
|
'host': host,
|
||||||
|
'address': address,
|
||||||
|
'language': language,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -550,8 +561,8 @@ class EndpointPasswordSignIn extends _isc.EndpointRef {
|
||||||
_isc.UuidValue? request,
|
_isc.UuidValue? request,
|
||||||
String? code,
|
String? code,
|
||||||
required String host,
|
required String host,
|
||||||
String? address,
|
required String address,
|
||||||
String? language,
|
required String language,
|
||||||
}) => caller.callServerEndpoint<_ix90myvx.PasswordSignedIn>(
|
}) => caller.callServerEndpoint<_ix90myvx.PasswordSignedIn>(
|
||||||
'halkyon_base.passwordSignIn',
|
'halkyon_base.passwordSignIn',
|
||||||
'complete',
|
'complete',
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ abstract class DeletionProof
|
||||||
this.totpCode,
|
this.totpCode,
|
||||||
this.recoveryCode,
|
this.recoveryCode,
|
||||||
required this.host,
|
required this.host,
|
||||||
|
required this.address,
|
||||||
|
required this.language,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory DeletionProof({
|
factory DeletionProof({
|
||||||
|
|
@ -36,6 +38,8 @@ abstract class DeletionProof
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required String host,
|
required String host,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) = _DeletionProofImpl;
|
}) = _DeletionProofImpl;
|
||||||
|
|
||||||
factory DeletionProof.fromJson(Map<String, dynamic> jsonSerialization) {
|
factory DeletionProof.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||||
|
|
@ -52,6 +56,8 @@ abstract class DeletionProof
|
||||||
totpCode: jsonSerialization['totpCode'] as String?,
|
totpCode: jsonSerialization['totpCode'] as String?,
|
||||||
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
||||||
host: jsonSerialization['host'] as String,
|
host: jsonSerialization['host'] as String,
|
||||||
|
address: jsonSerialization['address'] as String,
|
||||||
|
language: jsonSerialization['language'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,6 +75,13 @@ abstract class DeletionProof
|
||||||
/// checks it is its own before the register binds the proof to it.
|
/// checks it is its own before the register binds the proof to it.
|
||||||
String host;
|
String host;
|
||||||
|
|
||||||
|
/// The account's address as the device typed it, and the language its
|
||||||
|
/// mails are worded in: checked before the code, so the owner is mailed
|
||||||
|
/// should codes come to wait (the re-review's fourth finding).
|
||||||
|
String address;
|
||||||
|
|
||||||
|
String language;
|
||||||
|
|
||||||
/// Returns a shallow copy of this [DeletionProof]
|
/// Returns a shallow copy of this [DeletionProof]
|
||||||
/// with some or all fields replaced by the given arguments.
|
/// with some or all fields replaced by the given arguments.
|
||||||
@_isc.useResult
|
@_isc.useResult
|
||||||
|
|
@ -79,6 +92,8 @@ abstract class DeletionProof
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
String? host,
|
String? host,
|
||||||
|
String? address,
|
||||||
|
String? language,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
|
|
@ -90,6 +105,8 @@ abstract class DeletionProof
|
||||||
if (totpCode != null) 'totpCode': totpCode,
|
if (totpCode != null) 'totpCode': totpCode,
|
||||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||||
'host': host,
|
'host': host,
|
||||||
|
'address': address,
|
||||||
|
'language': language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +120,8 @@ abstract class DeletionProof
|
||||||
if (totpCode != null) 'totpCode': totpCode,
|
if (totpCode != null) 'totpCode': totpCode,
|
||||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||||
'host': host,
|
'host': host,
|
||||||
|
'address': address,
|
||||||
|
'language': language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,6 +141,8 @@ class _DeletionProofImpl extends DeletionProof {
|
||||||
String? totpCode,
|
String? totpCode,
|
||||||
String? recoveryCode,
|
String? recoveryCode,
|
||||||
required String host,
|
required String host,
|
||||||
|
required String address,
|
||||||
|
required String language,
|
||||||
}) : super._(
|
}) : super._(
|
||||||
challenge: challenge,
|
challenge: challenge,
|
||||||
clientValue: clientValue,
|
clientValue: clientValue,
|
||||||
|
|
@ -129,6 +150,8 @@ class _DeletionProofImpl extends DeletionProof {
|
||||||
totpCode: totpCode,
|
totpCode: totpCode,
|
||||||
recoveryCode: recoveryCode,
|
recoveryCode: recoveryCode,
|
||||||
host: host,
|
host: host,
|
||||||
|
address: address,
|
||||||
|
language: language,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Returns a shallow copy of this [DeletionProof]
|
/// Returns a shallow copy of this [DeletionProof]
|
||||||
|
|
@ -142,6 +165,8 @@ class _DeletionProofImpl extends DeletionProof {
|
||||||
Object? totpCode = _Undefined,
|
Object? totpCode = _Undefined,
|
||||||
Object? recoveryCode = _Undefined,
|
Object? recoveryCode = _Undefined,
|
||||||
String? host,
|
String? host,
|
||||||
|
String? address,
|
||||||
|
String? language,
|
||||||
}) {
|
}) {
|
||||||
return DeletionProof(
|
return DeletionProof(
|
||||||
challenge: challenge ?? this.challenge,
|
challenge: challenge ?? this.challenge,
|
||||||
|
|
@ -150,6 +175,8 @@ class _DeletionProofImpl extends DeletionProof {
|
||||||
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
||||||
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||||
host: host ?? this.host,
|
host: host ?? this.host,
|
||||||
|
address: address ?? this.address,
|
||||||
|
language: language ?? this.language,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue