Description
In flutter_secure_storage 10.0.0 on Android, the first sharedPreferencesName (and preferencesKeyPrefix) used in a process wins for the lifetime of that process. Every later Dart FlutterSecureStorage instance constructed with a different sharedPreferencesName is silently routed to the first instance's SharedPreferences file. The plugin's config field is updated on each call, but the preferences field is cached, so the symptom is "wrong file, right prefix" — silent data corruption, no exception.
This is a behaviour regression from v9, where ensureInitialized() re-resolved the SharedPreferences instance on every call.
Minimal reproducible example
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
const a = FlutterSecureStorage(
aOptions: AndroidOptions(sharedPreferencesName: 'storage_a'),
);
const b = FlutterSecureStorage(
aOptions: AndroidOptions(sharedPreferencesName: 'storage_b'),
);
await a.write(key: 'k', value: 'from-A'); // first call: opens storage_a
await b.write(key: 'k', value: 'from-B'); // expected: opens storage_b
debugPrint('a.read = ${await a.read(key: "k")}');
debugPrint('b.read = ${await b.read(key: "k")}');
}
Expected behavior
v9 behaviour:
a.read = from-A // a's write to storage_a
b.read = from-B // b's write to storage_b
Actual on v10:
a.read = from-B // b's write landed in storage_a, overwriting a's value
b.read = from-B // b reads from storage_a too
Context
flutter_secure_storage: v10.0.0
- Platform: Pixel 9 Android emulator (API 35)
- Flutter: 3.41.7 (also reproduced on 3.41.6)
Platforms
Root cause
Two changes between v9 and v10 combine to produce the regression.
1. v10 caches preferences and short-circuits initialize() —
android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java, lines 148-153:
protected void initialize(FlutterSecureStorageConfig config, SecurePreferencesCallback<Void> callback) {
this.config = config; // config IS updated…
if (preferences != null) { // …but preferences is reused
callback.onSuccess(null);
return;
}
// …open SharedPreferences with config.getSharedPreferencesName() and cache it
}
In v9 the equivalent guard was explicitly commented out, so the SharedPreferences reference was re-resolved each call:
// android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java (v9, line 152)
// if (preferences != null) return;
2. There is exactly one FlutterSecureStorage per process —
android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java, line 32:
secureStorage = new FlutterSecureStorage(context);
So the cache from change (1) is shared across every Dart-side instance, regardless of sharedPreferencesName.
The combined effect: config.getSharedPreferencesName() is read only once, when the cache is empty. After that, the file is locked in for the rest of the process; later instances quietly write into it.
Might be related to #914 and #1043
Description
In
flutter_secure_storage10.0.0 on Android, the firstsharedPreferencesName(andpreferencesKeyPrefix) used in a process wins for the lifetime of that process. Every later DartFlutterSecureStorageinstance constructed with a differentsharedPreferencesNameis silently routed to the first instance'sSharedPreferencesfile. The plugin'sconfigfield is updated on each call, but thepreferencesfield is cached, so the symptom is "wrong file, right prefix" — silent data corruption, no exception.This is a behaviour regression from v9, where
ensureInitialized()re-resolved theSharedPreferencesinstance on every call.Minimal reproducible example
Expected behavior
v9 behaviour:
Actual on v10:
Context
flutter_secure_storage: v10.0.0Platforms
Root cause
Two changes between v9 and v10 combine to produce the regression.
1. v10 caches
preferencesand short-circuitsinitialize()—android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java, lines 148-153:In v9 the equivalent guard was explicitly commented out, so the
SharedPreferencesreference was re-resolved each call:2. There is exactly one
FlutterSecureStorageper process —android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java, line 32:So the cache from change (1) is shared across every Dart-side instance, regardless of
sharedPreferencesName.The combined effect:
config.getSharedPreferencesName()is read only once, when the cache is empty. After that, the file is locked in for the rest of the process; later instances quietly write into it.Might be related to #914 and #1043