fix(app): an Apple device's keychain gives the keys only while it is unlocked

The reading of the full reading's fixes found that first_unlock_this_device
left the account's private keys readable on a locked phone once it had
been unlocked after starting, which is what forensic tools extract from a
stolen one; and that, the accessibility being part of every keychain
lookup, an entry kept with another class stayed unseen and made the next
write fail as a duplicate, so that person couldn't finish signing in on
that device.

As the developer decided, the keys are kept unlocked_this_device (still
never in a backup nor in iCloud Keychain); nothing decrypts with them
while the device is locked. Keeping keys deletes the entry first, which a
delete finds whatever its class.

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:23:20 +02:00
commit 9348504163
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,19 +1,69 @@
import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
/// A keyring noting what it is asked, in order.
final class _Recording() extends Fake implements FlutterSecureStorage {
final calls = <String>[];
@override
Future<void> delete({
required String key,
AppleOptions? iOptions,
AndroidOptions? aOptions,
LinuxOptions? lOptions,
WebOptions? webOptions,
AppleOptions? mOptions,
WindowsOptions? wOptions,
}) async => calls.add('delete $key');
@override
Future<void> write({
required String key,
required String? value,
AppleOptions? iOptions,
AndroidOptions? aOptions,
LinuxOptions? lOptions,
WebOptions? webOptions,
AppleOptions? mOptions,
WindowsOptions? wOptions,
}) async => calls.add('write $key');
}
void main() {
test("the account's keys stay on this device on Apple's systems: never in "
'a backup restored elsewhere, nor in iCloud Keychain', () {
test("the account's keys open only while an Apple device is unlocked, and "
'stay on it: never in a backup restored elsewhere, nor in iCloud '
'Keychain (the reading of the fixes)', () {
for (final options in [
accountKeyStorage.iOptions,
accountKeyStorage.mOptions,
]) {
expect(
options.accessibility,
KeychainAccessibility.first_unlock_this_device,
KeychainAccessibility.unlocked_this_device,
);
expect(options.synchronizable, isFalse);
}
});
test('keeping keys deletes what the keyring held for that person first: '
'an entry kept with another accessibility is never left, nor makes '
'the write fail', () async {
final keyring = _Recording();
final store = SecureAccountKeyStore('learn', storage: keyring);
await store.keep(
'anna',
UnlockedAccountKeys(
encryptionPublicKey: Uint8List(32),
encryptionSecretKey: Uint8List(32),
signingPublicKey: Uint8List(32),
signingSecretKey: Uint8List(64),
),
);
expect(keyring.calls, ['delete learn:anna', 'write learn:anna']);
});
}