fix(app): what a password unlocked is wiped once done with

The reading of the full reading's fixes noted that the main isolate's
copies of SRP's x and the key-encryption key, Dart's own bytes, stayed
for as long as the sign-in sheet's cubit lived, and after every change's
proof.

PasswordUnlock.forget zeroes them: the sign-in cubit wipes what the
password unlocked once signed in, once another password replaced it, and
as it closes; a change's or deletion's proof wipes what it unlocked once
its answer is made. The fake crypto's proof copies its bytes, as a real
one is new bytes. halkyon_keys' README says what is wiped and what isn't.

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-21 16:48:48 +02:00
commit 0c9bc3e007
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 120 additions and 25 deletions

View file

@ -53,6 +53,31 @@ void main() {
finished: finished,
);
bool wiped(PasswordUnlock unlock) =>
unlock.srpSecret.every((byte) => byte == 0) &&
unlock.keyEncryptionKey.every((byte) => byte == 0);
test(
'wipes what the password unlocked once signed in, once another '
'password replaced it, and as it closes (the reading of the fixes)',
() async {
anna();
await cubit.credentialsEntered('anna@example.org', 'wrong horse!');
await cubit.credentialsEntered('anna@example.org', 'correct horse');
expect(wiped(crypto.unlocks.first), isTrue, reason: 'replaced');
expect(wiped(crypto.unlocks.last), isFalse, reason: 'a code to come');
await cubit.codeEntered(fakeTotpCode);
expect(cubit.state.step, PasswordSignInStep.done);
expect(crypto.unlocks.every(wiped), isTrue);
final other = PasswordSignInCubit(passwords, language: 'en');
await other.credentialsEntered('anna@example.org', 'correct horse');
await other.close();
expect(crypto.unlocks.every(wiped), isTrue, reason: 'closed');
},
);
test('signs in with the address, the password and a code: the code asked '
'for once the password is unlocked, signed in only at the end', () async {
final account = anna();

View file

@ -0,0 +1,24 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
void main() {
test("forgetting what a password unlocked zeroes SRP's x and the "
'key-encryption key (the reading of the fixes)', () {
final unlock = PasswordUnlock(
salt: Uint8List(16),
costs: protocol.PasswordCosts(
algorithm: 'argon2id13',
opsLimit: 3,
memLimit: 64 * 1024 * 1024,
),
srpSecret: Uint8List.fromList(List.filled(32, 7)),
keyEncryptionKey: Uint8List.fromList(List.filled(32, 9)),
)..forget();
expect(unlock.srpSecret, everyElement(0));
expect(unlock.keyEncryptionKey, everyElement(0));
});
}