mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-15 00:10:59 +01:00
crdt_sync_flutter 0.8.0: keystore key cache
Todo: fda71ed1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
84343aec2d
commit
e5990c9c15
@ -2,6 +2,8 @@
|
||||
/// crdt-sync passphrase kept in the OS keystore (prompt-free after setup).
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crdt_sync/crdt_sync.dart';
|
||||
import 'package:crdt_sync_flutter/src/keystore.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
@ -9,16 +11,54 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
/// Keystore key holding the shared crdt-sync passphrase.
|
||||
const String kE2ePassphraseKey = kE2ePassphraseStorageKey;
|
||||
|
||||
/// Stores the passphrase this device derives its key from.
|
||||
/// Stores the passphrase this device derives its key from, dropping keys
|
||||
/// cached from an earlier one so the new passphrase is what gets checked.
|
||||
Future<void> saveE2ePassphrase(
|
||||
String passphrase, {
|
||||
FlutterSecureStorage storage = kSecureStorage,
|
||||
}) => storage.write(key: kE2ePassphraseKey, value: passphrase);
|
||||
}) async {
|
||||
await _dropCachedKeys(storage);
|
||||
await storage.write(key: kE2ePassphraseKey, value: passphrase);
|
||||
}
|
||||
|
||||
/// Forgets the passphrase; the next open is locked.
|
||||
Future<void> _dropCachedKeys(FlutterSecureStorage storage) async {
|
||||
final all = await storage.readAll();
|
||||
for (final key in all.keys) {
|
||||
if (key.startsWith(kE2eKeyCachePrefix)) await storage.delete(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Keystore key prefix for derived keys, one per old prefix.
|
||||
const String kE2eKeyCachePrefix = 'crdt_sync_e2e_key:';
|
||||
|
||||
/// Forgets the passphrase and every cached key; the next open is locked.
|
||||
Future<void> clearE2ePassphrase({
|
||||
FlutterSecureStorage storage = kSecureStorage,
|
||||
}) => storage.delete(key: kE2ePassphraseKey);
|
||||
}) async {
|
||||
await storage.delete(key: kE2ePassphraseKey);
|
||||
await _dropCachedKeys(storage);
|
||||
}
|
||||
|
||||
/// The derived key for [oldPrefix], cached in the OS keystore so Argon2id
|
||||
/// runs once per device instead of on every sync tick.
|
||||
E2eKeyCache keystoreKeyCache(
|
||||
String oldPrefix, {
|
||||
FlutterSecureStorage storage = kSecureStorage,
|
||||
}) {
|
||||
final name = '$kE2eKeyCachePrefix$oldPrefix';
|
||||
return E2eKeyCache(
|
||||
read: () async {
|
||||
final text = await storage.read(key: name);
|
||||
if (text == null) return null;
|
||||
try {
|
||||
return base64.decode(text);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
write: (bytes) => storage.write(key: name, value: base64.encode(bytes)),
|
||||
);
|
||||
}
|
||||
|
||||
/// [openEncryptedStore] reading the passphrase from the OS keystore.
|
||||
Future<E2eState> openEncryptedStoreFromKeystore({
|
||||
@ -26,9 +66,12 @@ Future<E2eState> openEncryptedStoreFromKeystore({
|
||||
required String oldPrefix,
|
||||
FlutterSecureStorage storage = kSecureStorage,
|
||||
Argon2Params params = Argon2Params.recommended,
|
||||
void Function()? onDerive,
|
||||
}) => openEncryptedStore(
|
||||
store: store,
|
||||
oldPrefix: oldPrefix,
|
||||
readPassphrase: () => storage.read(key: kE2ePassphraseKey),
|
||||
params: params,
|
||||
keyCache: keystoreKeyCache(oldPrefix, storage: storage),
|
||||
onDerive: onDerive,
|
||||
);
|
||||
|
||||
@ -61,11 +61,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: crdt_sync_dart
|
||||
ref: "crdt_sync_dart-v0.13.1"
|
||||
resolved-ref: eebeb0b51053f160f163140141aba10a447ba1e8
|
||||
ref: "crdt_sync_dart-v0.14.0"
|
||||
resolved-ref: "84343aec2dd07c38cba40690c3b5f2c84ae9cdf6"
|
||||
url: "https://github.com/kuhyx/utils"
|
||||
source: git
|
||||
version: "0.13.1"
|
||||
version: "0.14.0"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@ -4,7 +4,7 @@ description: >-
|
||||
persisted per-install device id, and a one-call bootstrap. Removes the
|
||||
~150 lines of firebase_backend.dart every consuming app was copying.
|
||||
publish_to: 'none'
|
||||
version: 0.7.0
|
||||
version: 0.8.0
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
@ -13,7 +13,7 @@ dependencies:
|
||||
crdt_sync:
|
||||
git:
|
||||
url: https://github.com/kuhyx/utils
|
||||
ref: crdt_sync_dart-v0.13.1
|
||||
ref: crdt_sync_dart-v0.14.0
|
||||
path: crdt_sync_dart
|
||||
crypto: 3.0.8
|
||||
flutter:
|
||||
|
||||
@ -135,4 +135,40 @@ void main() {
|
||||
await clearE2ePassphrase(storage: storage);
|
||||
expect(await storage.read(key: kE2ePassphraseKey), isNull);
|
||||
});
|
||||
|
||||
test('the derived key is cached: Argon2 runs once per device', () async {
|
||||
final remote = _Remote();
|
||||
await saveE2ePassphrase('pw', storage: storage);
|
||||
var derived = 0;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
resetE2eCopyMemo();
|
||||
final state = await openEncryptedStoreFromKeystore(
|
||||
store: remote,
|
||||
oldPrefix: _old,
|
||||
params: _cheap,
|
||||
onDerive: () => derived++,
|
||||
);
|
||||
expect(state, isA<E2eReady>());
|
||||
}
|
||||
expect(derived, 1);
|
||||
await clearE2ePassphrase(storage: storage);
|
||||
final locked = await openEncryptedStoreFromKeystore(
|
||||
store: remote,
|
||||
oldPrefix: _old,
|
||||
params: _cheap,
|
||||
);
|
||||
expect(locked, isA<E2eLocked>());
|
||||
});
|
||||
|
||||
test('a corrupt cached key falls back to the passphrase', () async {
|
||||
final remote = _Remote();
|
||||
await saveE2ePassphrase('pw', storage: storage);
|
||||
await storage.write(key: '${kE2eKeyCachePrefix}$_old', value: '%%%');
|
||||
final state = await openEncryptedStoreFromKeystore(
|
||||
store: remote,
|
||||
oldPrefix: _old,
|
||||
params: _cheap,
|
||||
);
|
||||
expect(state, isA<E2eReady>());
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user