From 33791da6ff0332a63d01977059cd7dad3f38b327 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Thu, 12 Nov 2026 08:21:26 +0100 Subject: [PATCH] crdt_sync_flutter 0.6.1: keystore wrapper over dart open Todo: fda71ed1 Co-Authored-By: Claude Sonnet 5.5 --- crdt_sync_flutter/lib/src/e2e_bootstrap.dart | 60 +++---------------- crdt_sync_flutter/pubspec.lock | 6 +- crdt_sync_flutter/pubspec.yaml | 4 +- .../test/e2e_bootstrap_test.dart | 14 ++--- 4 files changed, 21 insertions(+), 63 deletions(-) diff --git a/crdt_sync_flutter/lib/src/e2e_bootstrap.dart b/crdt_sync_flutter/lib/src/e2e_bootstrap.dart index f97b66c..2ef1a04 100644 --- a/crdt_sync_flutter/lib/src/e2e_bootstrap.dart +++ b/crdt_sync_flutter/lib/src/e2e_bootstrap.dart @@ -2,14 +2,12 @@ /// crdt-sync passphrase kept in the OS keystore (prompt-free after setup). library; -import 'dart:developer'; - import 'package:crdt_sync/crdt_sync.dart'; import 'package:crdt_sync_flutter/src/keystore.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; /// Keystore key holding the shared crdt-sync passphrase. -const String kE2ePassphraseKey = 'crdt_sync_e2e_passphrase'; +const String kE2ePassphraseKey = kE2ePassphraseStorageKey; /// Stores the passphrase this device derives its key from. Future saveE2ePassphrase( @@ -22,55 +20,15 @@ Future clearE2ePassphrase({ FlutterSecureStorage storage = kSecureStorage, }) => storage.delete(key: kE2ePassphraseKey); -/// Opens the encrypted store for [oldPrefix] (the app's former plaintext -/// prefix; data lives on `E2eLayout.of(oldPrefix)`). -/// -/// Fail closed: no passphrase, a wrong one, or an unreadable header returns -/// [E2eLocked] and the caller must not sync (show an icon-only lock state). -/// On success the old plaintext prefix is copied over once per call, which is -/// idempotent and picks up writes from builds not yet migrated; the old -/// prefix is never deleted here. -Future openEncryptedStore({ +/// [openEncryptedStore] reading the passphrase from the OS keystore. +Future openEncryptedStoreFromKeystore({ required RemoteStore store, required String oldPrefix, FlutterSecureStorage storage = kSecureStorage, Argon2Params params = Argon2Params.recommended, -}) async { - final passphrase = await storage.read(key: kE2ePassphraseKey); - if (passphrase == null || passphrase.isEmpty) { - return E2eLocked(E2eLockReason.needsPassphrase); - } - final layout = E2eLayout.of(oldPrefix); - final E2eKey key; - try { - key = await unlockE2e( - store: store, - pathPrefix: layout.prefix, - passphrase: passphrase, - params: params, - e2ePath: layout.e2ePath, - ); - } on E2eWrongPassphraseError { - return E2eLocked(E2eLockReason.wrongKey); - } on E2eHeaderError { - return E2eLocked(E2eLockReason.badHeader); - } - try { - await copyToEncrypted( - store: store, - key: key, - fromPrefix: oldPrefix, - layout: layout, - ); - } on Object catch (error, stackTrace) { - // The copy is retried next open; sync itself must not stop for it. - log('e2e copy failed', level: 900, error: error, stackTrace: stackTrace); - } - return await openE2e( - store: store, - pathPrefix: layout.prefix, - key: key, - e2ePath: layout.e2ePath, - revsPath: layout.revsPath, - ); -} +}) => openEncryptedStore( + store: store, + oldPrefix: oldPrefix, + readPassphrase: () => storage.read(key: kE2ePassphraseKey), + params: params, +); diff --git a/crdt_sync_flutter/pubspec.lock b/crdt_sync_flutter/pubspec.lock index 4d32d60..e3294fb 100644 --- a/crdt_sync_flutter/pubspec.lock +++ b/crdt_sync_flutter/pubspec.lock @@ -61,11 +61,11 @@ packages: dependency: "direct main" description: path: crdt_sync_dart - ref: "crdt_sync_dart-v0.13.0" - resolved-ref: "51e562a76460eb25202a92644b2f1d202dba8dc5" + ref: "crdt_sync_dart-v0.13.1" + resolved-ref: eebeb0b51053f160f163140141aba10a447ba1e8 url: "https://github.com/kuhyx/utils" source: git - version: "0.13.0" + version: "0.13.1" crypto: dependency: "direct main" description: diff --git a/crdt_sync_flutter/pubspec.yaml b/crdt_sync_flutter/pubspec.yaml index ba05931..3afb9b3 100644 --- a/crdt_sync_flutter/pubspec.yaml +++ b/crdt_sync_flutter/pubspec.yaml @@ -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.6.0 +version: 0.6.1 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.0 + ref: crdt_sync_dart-v0.13.1 path: crdt_sync_dart crypto: 3.0.8 flutter: diff --git a/crdt_sync_flutter/test/e2e_bootstrap_test.dart b/crdt_sync_flutter/test/e2e_bootstrap_test.dart index 56fe4eb..e76ef33 100644 --- a/crdt_sync_flutter/test/e2e_bootstrap_test.dart +++ b/crdt_sync_flutter/test/e2e_bootstrap_test.dart @@ -52,7 +52,7 @@ void main() { setUp(installFakeSecureStorage); test('no passphrase on the device is locked, never plaintext', () async { - final state = await openEncryptedStore( + final state = await openEncryptedStoreFromKeystore( store: _Remote(), oldPrefix: _old, params: _cheap, @@ -64,7 +64,7 @@ void main() { test('right passphrase unlocks and copies old plaintext once', () async { final remote = _Remote()..files['$_old/phone/log.json'] = _log('milk'); await saveE2ePassphrase('pw', storage: storage); - final state = await openEncryptedStore( + final state = await openEncryptedStoreFromKeystore( store: remote, oldPrefix: _old, params: _cheap, @@ -76,7 +76,7 @@ void main() { isNot(contains('milk')), ); expect(remote.files['$_old/phone/log.json'], contains('milk')); - final again = await openEncryptedStore( + final again = await openEncryptedStoreFromKeystore( store: remote, oldPrefix: _old, params: _cheap, @@ -88,7 +88,7 @@ void main() { final remote = _Remote(); await saveE2ePassphrase('pw', storage: storage); final first = - await openEncryptedStore(store: remote, oldPrefix: _old, params: _cheap) + await openEncryptedStoreFromKeystore(store: remote, oldPrefix: _old, params: _cheap) as E2eReady; final layout = E2eLayout.of(_old); await first.store.putFileText( @@ -97,7 +97,7 @@ void main() { message: 'm', ); final second = - await openEncryptedStore(store: remote, oldPrefix: _old, params: _cheap) + await openEncryptedStoreFromKeystore(store: remote, oldPrefix: _old, params: _cheap) as E2eReady; final text = await second.store.getFileText( '${layout.prefix}/phone/log.json', @@ -108,9 +108,9 @@ void main() { test('wrong passphrase is locked and merges nothing', () async { final remote = _Remote(); await saveE2ePassphrase('pw', storage: storage); - await openEncryptedStore(store: remote, oldPrefix: _old, params: _cheap); + await openEncryptedStoreFromKeystore(store: remote, oldPrefix: _old, params: _cheap); await saveE2ePassphrase('nope', storage: storage); - final state = await openEncryptedStore( + final state = await openEncryptedStoreFromKeystore( store: remote, oldPrefix: _old, params: _cheap,