fix(keys): new passwords have at least 12 characters
The register can't check a password it never sees, and should its database leak, the verifier and the keys wrapped under the password fall to guessing at Argon2id's cost (the second reading's M8). Devices now refuse a new password shorter than 12 characters, counted as code points once normalised (halkyon_keys' checkNewPassword), at sign-up, a reset keeping the keys and a change alike, before anything is derived; the new password's field says so, and the refusal has its own text (en, de). Signing in takes any length, so a password set before keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
dab2c80524
commit
b33b99a50a
5 changed files with 216 additions and 29 deletions
|
|
@ -51,7 +51,7 @@ void main() {
|
|||
|
||||
Future<void> withPhrase({
|
||||
String phrase = fakePhraseForTests,
|
||||
String password = 'new horse',
|
||||
String password = 'a new horse here',
|
||||
String? again,
|
||||
String code = fakeTotpCode,
|
||||
}) => cubit.phraseEntered(
|
||||
|
|
@ -86,7 +86,7 @@ void main() {
|
|||
at(AccountRecoveryStep.done),
|
||||
],
|
||||
);
|
||||
expect(anna.password, 'new horse');
|
||||
expect(anna.password, 'a new horse here');
|
||||
expect(anna.passwordKeys, isNot(same(keysBefore)));
|
||||
expect(
|
||||
auth.status,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ void main() {
|
|||
);
|
||||
expect(cubit.state.address, 'anna@example.org');
|
||||
|
||||
// Shorter than 12 characters: refused on the device, nothing sent.
|
||||
await cubit.passwordEntered('short horse', 'short horse');
|
||||
expect(cubit.state.step, AccountSetupStep.password);
|
||||
expect(cubit.state.problem?.problem, PasswordProblem.passwordTooShort);
|
||||
expect(gateway.calls, isEmpty);
|
||||
await cubit.passwordEntered('correct horse', 'correct hrose');
|
||||
expect(cubit.state.passwordsDiffer, isTrue);
|
||||
expect(gateway.calls, isEmpty);
|
||||
|
|
@ -263,7 +268,7 @@ void main() {
|
|||
address: 'anna@example.org',
|
||||
);
|
||||
expect(cubit.state, at(AccountSetupStep.password));
|
||||
await cubit.passwordEntered('new horse', 'new horse');
|
||||
await cubit.passwordEntered('a new horse here', 'a new horse here');
|
||||
expect(cubit.state, at(AccountSetupStep.phrase));
|
||||
cubit.phraseWrittenDown();
|
||||
await cubit.phraseChecked(wordsAsked(cubit.state));
|
||||
|
|
@ -283,7 +288,7 @@ void main() {
|
|||
await cubit.recoveryCodesKept();
|
||||
|
||||
expect(cubit.state, at(AccountSetupStep.done));
|
||||
expect(account.password, 'new horse');
|
||||
expect(account.password, 'a new horse here');
|
||||
expect(
|
||||
auth.status,
|
||||
SignedIn(authUserId: account.person, isIdentified: true),
|
||||
|
|
@ -307,7 +312,7 @@ void main() {
|
|||
expect(
|
||||
await statesOf(
|
||||
cubit,
|
||||
() => cubit.passwordEntered('new horse', 'new horse'),
|
||||
() => cubit.passwordEntered('a new horse here', 'a new horse here'),
|
||||
),
|
||||
[
|
||||
// A cubit's first state goes out even when it equals the one it
|
||||
|
|
@ -324,7 +329,7 @@ void main() {
|
|||
|
||||
expect(cubit.state, at(AccountSetupStep.done));
|
||||
expect(Uint8List.sublistView(account.encryptionPublicKey), keysBefore);
|
||||
expect(account.password, 'new horse');
|
||||
expect(account.password, 'a new horse here');
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ void main() {
|
|||
|
||||
await expectLater(
|
||||
passwords.signUp(
|
||||
await passwords.makePassword('another'),
|
||||
await passwords.makePassword('another password'),
|
||||
address: 'anna@example.org',
|
||||
request: request,
|
||||
code: gateway.codeFor('anna@example.org'),
|
||||
|
|
@ -368,7 +368,7 @@ void main() {
|
|||
passwords.resetPassword(
|
||||
address: 'anna@example.org',
|
||||
phrase: otherFakePhrase,
|
||||
newPassword: 'new',
|
||||
newPassword: 'a new password',
|
||||
totpCode: fakeTotpCode,
|
||||
),
|
||||
failsWith(PasswordProblem.phraseDoesNotFit),
|
||||
|
|
@ -377,7 +377,7 @@ void main() {
|
|||
passwords.resetPassword(
|
||||
address: 'anna@example.org',
|
||||
phrase: 'too short',
|
||||
newPassword: 'new',
|
||||
newPassword: 'a new password',
|
||||
totpCode: fakeTotpCode,
|
||||
),
|
||||
failsWith(PasswordProblem.notAPhrase),
|
||||
|
|
@ -386,11 +386,11 @@ void main() {
|
|||
final pending = await passwords.resetPassword(
|
||||
address: 'anna@example.org',
|
||||
phrase: fakePhrase,
|
||||
newPassword: 'new',
|
||||
newPassword: 'a new password',
|
||||
totpCode: fakeTotpCode,
|
||||
);
|
||||
|
||||
expect(anna.password, 'new');
|
||||
expect(anna.password, 'a new password');
|
||||
expect(pending.keys, crypto.keysNumbered(200));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,32 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'a new password has at least 12 characters, for a new account, a '
|
||||
'reset keeping the keys and a change alike; signing in takes any',
|
||||
() async {
|
||||
Matcher tooShort() => throwsA(
|
||||
isA<PasswordFailed>().having(
|
||||
(failed) => failed.problem,
|
||||
'problem',
|
||||
PasswordProblem.passwordTooShort,
|
||||
),
|
||||
);
|
||||
await expectLater(crypto.newAccount('eleven char'), tooShort());
|
||||
await expectLater(
|
||||
crypto.keepingKeys('eleven char', account.keys, phrase: account.phrase),
|
||||
tooShort(),
|
||||
);
|
||||
await expectLater(
|
||||
crypto.newPassword('eleven char', account.keys),
|
||||
tooShort(),
|
||||
);
|
||||
// A password set before the minimum (none outside development) still
|
||||
// unlocks: unlocking checks no length.
|
||||
expect(await unlock('short'), isA<PasswordUnlock>());
|
||||
},
|
||||
);
|
||||
|
||||
test('a wrong password opens nothing; an empty one is refused', () async {
|
||||
await expectLater(
|
||||
open(
|
||||
|
|
|
|||
|
|
@ -259,8 +259,8 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
await tester.enterText(field('Email address'), 'anna@example.org');
|
||||
await tester.enterText(field('Recovery phrase'), fakePhrase);
|
||||
await tester.enterText(field('New password'), 'remembered');
|
||||
await tester.enterText(field('New password again'), 'remembered');
|
||||
await tester.enterText(field('New password'), 'remembered at last');
|
||||
await tester.enterText(field('New password again'), 'remembered at last');
|
||||
await tester.enterText(
|
||||
field('Code from your authenticator app'),
|
||||
fakeTotpCode,
|
||||
|
|
@ -268,7 +268,7 @@ void main() {
|
|||
await tapButton(tester, 'Sign in');
|
||||
|
||||
expect(find.text('App of ${anna.person}'), findsOneWidget);
|
||||
expect(anna.password, 'remembered');
|
||||
expect(anna.password, 'remembered at last');
|
||||
});
|
||||
|
||||
/// Signs in as far as the code: the sign-in sheet, [address] and
|
||||
|
|
@ -429,8 +429,8 @@ void main() {
|
|||
);
|
||||
await tapButton(tester, 'Continue');
|
||||
|
||||
await tester.enterText(field('New password'), 'a new start');
|
||||
await tester.enterText(field('New password again'), 'a new start');
|
||||
await tester.enterText(field('New password'), 'a new start again');
|
||||
await tester.enterText(field('New password again'), 'a new start again');
|
||||
await tapButton(tester, 'Continue');
|
||||
expect(find.text('Your recovery phrase'), findsOneWidget);
|
||||
await tapButton(tester, "I've written it down");
|
||||
|
|
@ -449,7 +449,7 @@ void main() {
|
|||
await tapButton(tester, "I've kept them");
|
||||
|
||||
expect(find.text('App of ${anna.person}'), findsOneWidget);
|
||||
expect(anna.password, 'a new start');
|
||||
expect(anna.password, 'a new start again');
|
||||
expect(anna.passwordKeys, isNot(same(keysBefore)));
|
||||
expect(anna.accountReset, isNull);
|
||||
expect(anna.finished, isTrue);
|
||||
|
|
|
|||
Loading…
Reference in a new issue