@@ -41,155 +41,201 @@ class StorageLocator {
4141
4242 late final KeyValueStorageDatasource <String > secureStorageDatasource;
4343
44- switch (existingLibrary) {
45- case null :
46- // No flag — fresh install, 6.5.2 user, or first run of 6.10.0.
47- // Try FSS10 first with migrateOnAlgorithmChange:false. A 6.5.2 user
48- // with ESP data will hit the explicit error branch in
49- // FlutterSecureStorage.java that throws "EncryptedSharedPreferences
50- // data found but migration is disabled" — caught here and routed to
51- // FSS9 which can still read ESP data via encryptedSharedPreferences:true.
52- log.fine ('StorageLocator: no existing flag — attempting fss10 init' );
53- try {
54- final storage = fss10.FlutterSecureStorage (
55- aOptions: const fss10.AndroidOptions (
56- // Never auto-delete data on errors. v10 default is true.
57- resetOnError: false ,
58- // Never run any migration. With the fork's StorageCipherFactory
59- // patch, this also makes fresh installs initialize the cipher
60- // cleanly (saved=current when no markers exist). 6.5.2 ESP
61- // users hit the line-195 error branch and get caught into the
62- // FSS9 fallback below.
63- migrateOnAlgorithmChange: false ,
64- ),
65- iOptions: const fss10.IOSOptions (
66- accessibility:
67- fss10.KeychainAccessibility .first_unlock_this_device,
68- ),
69- );
70- // Trigger native init by reading. The constructor alone never
71- // throws — failures only surface on data access.
72- final data = await storage.readAll ();
73- log.fine (
74- 'StorageLocator: fss10 readAll returned ${data .length } entries' ,
75- );
76-
77- // Belt-and-suspenders: if FSS10 returns empty but the SQLite DB
78- // from a prior install exists, treat it as a silent failure and
79- // route to FSS9.
80- if (data.isEmpty) {
81- final docsDir = await getApplicationDocumentsDirectory ();
82- final dbFile = File (
83- p.join (docsDir.path, 'bullbitcoin_sqlite.sqlite' ),
84- );
85- if (await dbFile.exists ()) {
86- log.warning (
87- 'StorageLocator: fss10 readAll returned empty but database '
88- 'exists — silent failure detected, falling back to fss9' ,
89- );
90- throw Exception (
91- 'FSS10 silent failure: prior install data exists '
92- 'but readAll returned 0 entries' ,
93- );
94- }
95- log.fine (
96- 'StorageLocator: readAll empty + no database = fresh install' ,
97- );
98- }
99-
100- secureStorageDatasource = SecureStorageDatasourceImpl (storage);
101- // Only commit the flag AFTER readAll confirms FSS10 works.
102- await seedStoreTypeDatasource.write (
103- SeedStoreTypeModel .fromEntity (
104- const SeedStoreType (storageLibrary: SeedStorageLibrary .fss10),
105- ),
106- );
107- log.fine ('StorageLocator: fss10 verified and flag written' );
108- } catch (fss10Error) {
109- log.warning (
110- 'StorageLocator: fss10 readAll failed — falling back to fss9. '
111- 'Error: ${fss10Error .runtimeType }: $fss10Error ' ,
112- error: fss10Error,
113- );
114-
44+ if (Platform .isAndroid) {
45+ // The FSS9/FSS10 hybrid fallback chain below exists solely to
46+ // recover 6.5.2 Android users whose wallet data lives in Jetpack
47+ // Security's EncryptedSharedPreferences (ESP, Tink-backed). ESP
48+ // is `androidx.security.crypto` — Android-only. The whole probe
49+ // + fallback dance, including the eager `storage.readAll()` call
50+ // in the `case null:` branch, exists for that one cohort.
51+ //
52+ // On every other platform (the `else` branch below) we skip the
53+ // hybrid entirely: fss9 and fss10 route to the same OS-native
54+ // secure store with byte-identical wire semantics (e.g. iOS:
55+ // both hit Keychain via `SecItemAdd` / `SecItemCopyMatching`
56+ // with the same service id + key + accessibility class), so the
57+ // fallback serves no purpose there and the eager keychain read
58+ // is what causes the pre-warm `-25308` failure class.
59+ switch (existingLibrary) {
60+ case null :
61+ // No flag — fresh install, 6.5.2 user, or first run of 6.10.0.
62+ // Try FSS10 first with migrateOnAlgorithmChange:false. A 6.5.2 user
63+ // with ESP data will hit the explicit error branch in
64+ // FlutterSecureStorage.java that throws "EncryptedSharedPreferences
65+ // data found but migration is disabled" — caught here and routed to
66+ // FSS9 which can still read ESP data via encryptedSharedPreferences:true.
67+ log.fine ('StorageLocator: no existing flag — attempting fss10 init' );
11568 try {
116- final storage = fss9.FlutterSecureStorage (
117- aOptions: const fss9.AndroidOptions (
69+ final storage = fss10.FlutterSecureStorage (
70+ aOptions: const fss10.AndroidOptions (
71+ // Never auto-delete data on errors. v10 default is true.
11872 resetOnError: false ,
119- // ESP path used by 6.5.2 — same MasterKey alias and Tink
120- // scheme, so existing data decrypts cleanly.
121- encryptedSharedPreferences: true ,
73+ // Never run any migration. With the fork's StorageCipherFactory
74+ // patch, this also makes fresh installs initialize the cipher
75+ // cleanly (saved=current when no markers exist). 6.5.2 ESP
76+ // users hit the line-195 error branch and get caught into the
77+ // FSS9 fallback below.
78+ migrateOnAlgorithmChange: false ,
12279 ),
123- iOptions: const fss9 .IOSOptions (
80+ iOptions: const fss10 .IOSOptions (
12481 accessibility:
125- fss9 .KeychainAccessibility .first_unlock_this_device,
82+ fss10 .KeychainAccessibility .first_unlock_this_device,
12683 ),
12784 );
85+ // Trigger native init by reading. The constructor alone never
86+ // throws — failures only surface on data access.
12887 final data = await storage.readAll ();
12988 log.fine (
130- 'StorageLocator: fss9 readAll returned ${data .length } entries' ,
131- );
132- secureStorageDatasource = SecureStorageLegacyDatasourceImpl (
133- storage,
89+ 'StorageLocator: fss10 readAll returned ${data .length } entries' ,
13490 );
91+
92+ // Belt-and-suspenders: if FSS10 returns empty but the SQLite DB
93+ // from a prior install exists, treat it as a silent failure and
94+ // route to FSS9.
95+ if (data.isEmpty) {
96+ final docsDir = await getApplicationDocumentsDirectory ();
97+ final dbFile = File (
98+ p.join (docsDir.path, 'bullbitcoin_sqlite.sqlite' ),
99+ );
100+ if (await dbFile.exists ()) {
101+ log.warning (
102+ 'StorageLocator: fss10 readAll returned empty but database '
103+ 'exists — silent failure detected, falling back to fss9' ,
104+ );
105+ throw Exception (
106+ 'FSS10 silent failure: prior install data exists '
107+ 'but readAll returned 0 entries' ,
108+ );
109+ }
110+ log.fine (
111+ 'StorageLocator: readAll empty + no database = fresh install' ,
112+ );
113+ }
114+
115+ secureStorageDatasource = SecureStorageDatasourceImpl (storage);
116+ // Only commit the flag AFTER readAll confirms FSS10 works.
135117 await seedStoreTypeDatasource.write (
136118 SeedStoreTypeModel .fromEntity (
137- const SeedStoreType (storageLibrary: SeedStorageLibrary .fss9 ),
119+ const SeedStoreType (storageLibrary: SeedStorageLibrary .fss10 ),
138120 ),
139121 );
140- log.fine ('StorageLocator: fss9 fallback verified and flag written' );
141- } catch (fss9Error) {
142- log.severe (
143- message:
144- 'StorageLocator: both fss10 and fss9 failed. '
145- 'fss10: ${fss10Error .runtimeType }, '
146- 'fss9: ${fss9Error .runtimeType }' ,
147- error: fss9Error,
148- trace: StackTrace .current,
149- category: ReportCategory .migration,
122+ log.fine ('StorageLocator: fss10 verified and flag written' );
123+ } catch (fss10Error) {
124+ log.warning (
125+ 'StorageLocator: fss10 readAll failed — falling back to fss9. '
126+ 'Error: ${fss10Error .runtimeType }: $fss10Error ' ,
127+ error: fss10Error,
150128 );
151- rethrow ;
129+
130+ try {
131+ final storage = fss9.FlutterSecureStorage (
132+ aOptions: const fss9.AndroidOptions (
133+ resetOnError: false ,
134+ // ESP path used by 6.5.2 — same MasterKey alias and Tink
135+ // scheme, so existing data decrypts cleanly.
136+ encryptedSharedPreferences: true ,
137+ ),
138+ iOptions: const fss9.IOSOptions (
139+ accessibility:
140+ fss9.KeychainAccessibility .first_unlock_this_device,
141+ ),
142+ );
143+ final data = await storage.readAll ();
144+ log.fine (
145+ 'StorageLocator: fss9 readAll returned ${data .length } entries' ,
146+ );
147+ secureStorageDatasource = SecureStorageLegacyDatasourceImpl (
148+ storage,
149+ );
150+ await seedStoreTypeDatasource.write (
151+ SeedStoreTypeModel .fromEntity (
152+ const SeedStoreType (storageLibrary: SeedStorageLibrary .fss9),
153+ ),
154+ );
155+ log.fine (
156+ 'StorageLocator: fss9 fallback verified and flag written' ,
157+ );
158+ } catch (fss9Error) {
159+ log.severe (
160+ message:
161+ 'StorageLocator: both fss10 and fss9 failed. '
162+ 'fss10: ${fss10Error .runtimeType }, '
163+ 'fss9: ${fss9Error .runtimeType }' ,
164+ error: fss9Error,
165+ trace: StackTrace .current,
166+ category: ReportCategory .migration,
167+ );
168+ rethrow ;
169+ }
152170 }
153- }
154-
155- case SeedStorageLibrary .fss9:
156- // Previous session committed to FSS9 (6.5.2 ESP user). Stay on FSS9
157- // with the same options that succeeded then. The app shows a
158- // LegacyStorageWarningOverlay prompting backup + reinstall.
159- log.fine (
160- 'StorageLocator: existing flag is fss9 — using legacy storage' ,
161- );
162- final storage = fss9.FlutterSecureStorage (
163- aOptions: const fss9.AndroidOptions (
164- resetOnError: false ,
165- encryptedSharedPreferences: true ,
166- ),
167- iOptions: const fss9.IOSOptions (
168- accessibility: fss9.KeychainAccessibility .first_unlock_this_device,
169- ),
170- );
171- secureStorageDatasource = SecureStorageLegacyDatasourceImpl (storage);
172- log.fine ('StorageLocator: fss9 legacy storage initialized from flag' );
173-
174- case SeedStorageLibrary .fss10:
175- // Previous session committed to FSS10 (cohort A, prior fresh
176- // install, or a 6.5.2 user that fell through to FSS10 after the
177- // dance — this last case shouldn't happen since fss9-flag is
178- // written for that path).
179- log.fine (
180- 'StorageLocator: existing flag is fss10 — using current storage' ,
181- );
182- final storage = fss10.FlutterSecureStorage (
183- aOptions: const fss10.AndroidOptions (
184- resetOnError: false ,
185- migrateOnAlgorithmChange: false ,
186- ),
187- iOptions: const fss10.IOSOptions (
188- accessibility: fss10.KeychainAccessibility .first_unlock_this_device,
171+
172+ case SeedStorageLibrary .fss9:
173+ // Previous session committed to FSS9 (6.5.2 ESP user). Stay on FSS9
174+ // with the same options that succeeded then. The app shows a
175+ // LegacyStorageWarningOverlay prompting backup + reinstall.
176+ log.fine (
177+ 'StorageLocator: existing flag is fss9 — using legacy storage' ,
178+ );
179+ final storage = fss9.FlutterSecureStorage (
180+ aOptions: const fss9.AndroidOptions (
181+ resetOnError: false ,
182+ encryptedSharedPreferences: true ,
183+ ),
184+ iOptions: const fss9.IOSOptions (
185+ accessibility:
186+ fss9.KeychainAccessibility .first_unlock_this_device,
187+ ),
188+ );
189+ secureStorageDatasource = SecureStorageLegacyDatasourceImpl (storage);
190+ log.fine ('StorageLocator: fss9 legacy storage initialized from flag' );
191+
192+ case SeedStorageLibrary .fss10:
193+ // Previous session committed to FSS10 (cohort A, prior fresh
194+ // install, or a 6.5.2 user that fell through to FSS10 after the
195+ // dance — this last case shouldn't happen since fss9-flag is
196+ // written for that path).
197+ log.fine (
198+ 'StorageLocator: existing flag is fss10 — using current storage' ,
199+ );
200+ final storage = fss10.FlutterSecureStorage (
201+ aOptions: const fss10.AndroidOptions (
202+ resetOnError: false ,
203+ migrateOnAlgorithmChange: false ,
204+ ),
205+ iOptions: const fss10.IOSOptions (
206+ accessibility:
207+ fss10.KeychainAccessibility .first_unlock_this_device,
208+ ),
209+ );
210+ secureStorageDatasource = SecureStorageDatasourceImpl (storage);
211+ log.fine ('StorageLocator: fss10 storage initialized from flag' );
212+ }
213+ } else {
214+ // Non-Android: fss10 direct, no probe, no fallback. See the
215+ // `if (Platform.isAndroid)` comment above for the rationale.
216+ // Normalize any stray `fss9` flag (practically unreachable here
217+ // but cheap to enforce) so the cohort-based "legacy storage" UI
218+ // warning in `wallet_bloc.dart` / `home_errors.dart` doesn't
219+ // surface for current iOS installs.
220+ log.fine ('StorageLocator: non-Android — fss10 direct, no probe' );
221+ final storage = fss10.FlutterSecureStorage (
222+ aOptions: const fss10.AndroidOptions (
223+ resetOnError: false ,
224+ migrateOnAlgorithmChange: false ,
225+ ),
226+ iOptions: const fss10.IOSOptions (
227+ accessibility: fss10.KeychainAccessibility .first_unlock_this_device,
228+ ),
229+ );
230+ secureStorageDatasource = SecureStorageDatasourceImpl (storage);
231+
232+ if (existingLibrary != SeedStorageLibrary .fss10) {
233+ await seedStoreTypeDatasource.write (
234+ SeedStoreTypeModel .fromEntity (
235+ const SeedStoreType (storageLibrary: SeedStorageLibrary .fss10),
189236 ),
190237 );
191- secureStorageDatasource = SecureStorageDatasourceImpl (storage);
192- log.fine ('StorageLocator: fss10 storage initialized from flag' );
238+ }
193239 }
194240
195241 locator.registerLazySingleton <KeyValueStorageDatasource <String >>(
0 commit comments