From f7452832eabb495550a0d35c0d013c61bb5db83e Mon Sep 17 00:00:00 2001 From: randogoth Date: Tue, 29 Sep 2026 20:35:16 +0300 Subject: [PATCH] fix: copy the master at each layer so zeroization stays local --- lib/native/client.dart | 7 ++++++- lib/smol/store.dart | 7 +++++-- test/store_test.dart | 10 ++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/native/client.dart b/lib/native/client.dart index f445693..6685b74 100644 --- a/lib/native/client.dart +++ b/lib/native/client.dart @@ -67,7 +67,12 @@ class FumiNative { /// operation, so it never needs an isolate — and widget tests can settle /// it inside their fake-async zones. void setMaster(Uint8List master, {int? rotations}) { - _master = master; + // Own copy: the caller keeps its buffer (the onboarding screen zeroes + // its reference on dispose, and wipe zeroes Hive's), and the account + // rebuilds after register/restore/rotate must derive from untouched + // bytes — a shared buffer zeroized elsewhere would rebuild an account + // that matches none of our keys. + _master = Uint8List.fromList(master); _rebuildAccount(rotations: rotations); } diff --git a/lib/smol/store.dart b/lib/smol/store.dart index 9ace821..ef4a414 100644 --- a/lib/smol/store.dart +++ b/lib/smol/store.dart @@ -145,14 +145,17 @@ class SmolStore { void setMaster(Uint8List fresh) { // Hive's in-memory state updates synchronously and persists in the // background, so the store is consistent without awaiting the write. - unawaited(_meta.put("master", fresh)); + // The put takes its own copy: the caller's buffer is the caller's to + // zeroize (the onboarding screen does, on dispose), and a shared object + // would scrub the store's view with it. + unawaited(_meta.put("master", Uint8List.fromList(fresh))); _native.setMaster(fresh, rotations: 0); } /// The master restored from a backup, already at the rotation index the /// server bound. void restoreMaster(Uint8List master, int index) { - unawaited(_meta.put("master", master)); + unawaited(_meta.put("master", Uint8List.fromList(master))); _native.setMaster(master, rotations: index); } diff --git a/test/store_test.dart b/test/store_test.dart index 1b783a6..5e296af 100644 --- a/test/store_test.dart +++ b/test/store_test.dart @@ -77,14 +77,20 @@ void main() { final store = await freshStore("wipe"); final master = randomBytes(32); store.setMaster(master); + // The store's own copy, read back before the wipe. + final stored = store.master()!; store.pinServer("example.org", "lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq"); store.markRead("cd" * 32); await store.wipe(); // The bytes the store owned are overwritten, not just dereferenced — - // the caller's reference sees the zeros too. - expect(master.every((b) => b == 0), isTrue); + // the reference read back through the store sees the zeros. The + // caller's own buffer is the caller's to zeroize: the store holds a + // copy precisely so nobody else's zeroization can reach it, and vice + // versa a wipe never reaches a buffer the store handed out. + expect(stored.every((b) => b == 0), isTrue); + expect(master.every((b) => b == 0), isFalse); expect(store.master(), isNull); expect(store.identity(), isNull); expect(store.serverPin("example.org"), isNull);