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:
Tizian 「ティツィアーン」 2026-10-20 21:25:47 +02:00
commit b33b99a50a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 216 additions and 29 deletions

View file

@ -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,

View file

@ -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');
}),
);
});