Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@
import javax.crypto.Cipher;

public class FlutterSecureStorage {

private static final String TAG = "FlutterSecureStorage";
private static final Charset charset = StandardCharsets.UTF_8;
private static final String SHARED_PREFERENCES_CONFIG_NAME = "FlutterSecureStorageConfiguration";


private FlutterSecureStorageConfig config;
@NonNull
private final Context context;
Expand Down Expand Up @@ -157,10 +155,8 @@ protected void initialize(FlutterSecureStorageConfig config, SecurePreferencesCa
Context.MODE_PRIVATE
);

SharedPreferences configSource = context.getSharedPreferences(
SHARED_PREFERENCES_CONFIG_NAME,
Context.MODE_PRIVATE
);
// Use namespaced config with legacy fallback for backwards compatibility
NamespacedConfigSource configSource = new NamespacedConfigSource(context, config.getSharedPreferencesName());

Boolean isAlreadyMigrated = getEncryptedPrefsMigrated(configSource);

Expand Down Expand Up @@ -259,7 +255,7 @@ public void onError(Exception e) {
}
}

private void initializeStorageCipher(SharedPreferences configSource, SecurePreferencesCallback<Void> callback) {
private void initializeStorageCipher(NamespacedConfigSource configSource, SecurePreferencesCallback<Void> callback) {
try {
storageCipherFactory = new StorageCipherFactory(configSource, config.getPrefOptionKeyCipherAlgorithm(), config.getPrefOptionStorageCipherAlgorithm(), config);

Expand Down Expand Up @@ -335,7 +331,7 @@ public void onError(Exception e) {
* @param dataSource SharedPreferences containing encrypted data
* @param callback Callback to notify of success/failure
*/
private void migrateData(SharedPreferences configSource, SharedPreferences dataSource,
private void migrateData(NamespacedConfigSource configSource, SharedPreferences dataSource,
SecurePreferencesCallback<Void> callback) {
Log.i(TAG, "Starting data migration from saved to current cipher algorithms...");

Expand Down Expand Up @@ -440,7 +436,7 @@ private boolean isBiometricAlgorithm(String algorithmName) {
* @param dataSource SharedPreferences containing encrypted data
* @param callback Callback to notify of success/failure
*/
private void migrateNonBiometric(SharedPreferences configSource, SharedPreferences dataSource,
private void migrateNonBiometric(NamespacedConfigSource configSource, SharedPreferences dataSource,
SecurePreferencesCallback<Void> callback) {
Log.i(TAG, "Starting non-biometric migration (no authentication required)...");

Expand Down Expand Up @@ -499,7 +495,7 @@ private void migrateNonBiometric(SharedPreferences configSource, SharedPreferenc
/**
* Updates algorithm markers in config to match current cipher algorithms.
*/
private void updateAlgorithmMarkers(SharedPreferences configSource) {
private void updateAlgorithmMarkers(NamespacedConfigSource configSource) {
SharedPreferences.Editor editor = configSource.edit();
storageCipherFactory.storeCurrentAlgorithms(editor);
editor.commit();
Expand All @@ -519,7 +515,7 @@ private void updateAlgorithmMarkers(SharedPreferences configSource) {
* @param toBiometric True if migrating TO a biometric algorithm
* @param callback Callback to notify of success/failure
*/
private void migrateBiometric(SharedPreferences configSource, SharedPreferences dataSource,
private void migrateBiometric(NamespacedConfigSource configSource, SharedPreferences dataSource,
boolean fromBiometric, boolean toBiometric,
SecurePreferencesCallback<Void> callback) {
Log.i(TAG, "Starting biometric migration (authentication required)...");
Expand All @@ -546,7 +542,7 @@ private void migrateBiometric(SharedPreferences configSource, SharedPreferences
* Migrates FROM biometric → TO non-biometric.
* Requires authentication with OLD biometric cipher to decrypt.
*/
private void migrateFromBiometricToNonBiometric(SharedPreferences configSource, SharedPreferences dataSource,
private void migrateFromBiometricToNonBiometric(NamespacedConfigSource configSource, SharedPreferences dataSource,
SecurePreferencesCallback<Void> callback) {
try {
// Step 1: Get OLD biometric cipher (requires authentication)
Expand Down Expand Up @@ -624,7 +620,7 @@ public void onError(Exception e) {
* Migrates FROM non-biometric → TO biometric.
* Requires authentication with NEW biometric cipher to encrypt.
*/
private void migrateFromNonBiometricToBiometric(SharedPreferences configSource, SharedPreferences dataSource,
private void migrateFromNonBiometricToBiometric(NamespacedConfigSource configSource, SharedPreferences dataSource,
SecurePreferencesCallback<Void> callback) {
try {
// Step 1: Decrypt with OLD non-biometric cipher (no auth)
Expand Down Expand Up @@ -703,7 +699,7 @@ public void onError(Exception e) {
* Migrates FROM biometric → TO biometric (changing biometric algorithms).
* Requires authentication with both OLD and NEW biometric ciphers.
*/
private void migrateBiometricToBiometric(SharedPreferences configSource, SharedPreferences dataSource,
private void migrateBiometricToBiometric(NamespacedConfigSource configSource, SharedPreferences dataSource,
SecurePreferencesCallback<Void> callback) {
try {
// Step 1: Get OLD biometric cipher
Expand Down Expand Up @@ -806,13 +802,13 @@ public void onError(Exception e) {
}
}

private void setEncryptedPrefsMigrated(SharedPreferences configSource) {
private void setEncryptedPrefsMigrated(NamespacedConfigSource configSource) {
SharedPreferences.Editor editor = configSource.edit();
editor.putBoolean("ENCRYPTED_PREFERENCES_MIGRATED", true);
editor.commit();
}

private Boolean getEncryptedPrefsMigrated(SharedPreferences configSource) {
private Boolean getEncryptedPrefsMigrated(NamespacedConfigSource configSource) {
return configSource.getBoolean("ENCRYPTED_PREFERENCES_MIGRATED", false);
}

Expand All @@ -825,7 +821,7 @@ private Boolean getEncryptedPrefsMigrated(SharedPreferences configSource) {
* @param exception The original exception (BadPaddingException, InvalidKeyException, etc.)
* @param errorType Human-readable description of the error type
*/
private void handleKeyMismatch(SharedPreferences configSource, SecurePreferencesCallback<Void> callback,
private void handleKeyMismatch(NamespacedConfigSource configSource, SecurePreferencesCallback<Void> callback,
Exception exception, String errorType) {
Log.e(TAG, "Key mismatch detected during cipher initialization: " + errorType, exception);
Log.e(TAG, "This typically occurs after an algorithm change.");
Expand Down Expand Up @@ -890,7 +886,7 @@ public void onError(Exception migrationError) {
* Deletes all encrypted data, keys, and algorithm markers, then reinitializes.
* Extracted from handleKeyMismatch for reuse.
*/
private void deleteAllDataAndKeys(SharedPreferences configSource, SecurePreferencesCallback<Void> callback) {
private void deleteAllDataAndKeys(NamespacedConfigSource configSource, SecurePreferencesCallback<Void> callback) {
try {
// Delete keys from AndroidKeyStore
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
Expand All @@ -23,13 +24,14 @@ public class FlutterSecureStoragePlugin implements MethodCallHandler, FlutterPlu

private static final String TAG = "FlutterSecureStoragePlugin";
private MethodChannel channel;
private FlutterSecureStorage secureStorage;
private Context applicationContext;
private final Map<String, FlutterSecureStorage> storagesBySharedPreferencesName = new HashMap<>();
private HandlerThread workerThread;
private Handler workerThreadHandler;

public void initInstance(BinaryMessenger messenger, Context context) {
try {
secureStorage = new FlutterSecureStorage(context);
applicationContext = context.getApplicationContext();

workerThread = new HandlerThread("com.it_nomads.fluttersecurestorage.worker");
workerThread.start();
Expand All @@ -56,7 +58,10 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
channel = null;
}
secureStorage = null;
synchronized (storagesBySharedPreferencesName) {
storagesBySharedPreferencesName.clear();
}
applicationContext = null;
}

@Override
Expand All @@ -67,17 +72,32 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result rawResult) {
}

@SuppressWarnings("unchecked")
private String getKeyFromCall(MethodCall call) {
private static String getKeyFromCall(FlutterSecureStorage storage, MethodCall call) {
Map<String, Object> arguments = (Map<String, Object>) call.arguments;
return secureStorage.addPrefixToKey((String) arguments.get("key"));
String key = (String) arguments.get("key");
return storage.addPrefixToKey(key);
}

@SuppressWarnings("unchecked")
private String getValueFromCall(MethodCall call) {
private static String getValueFromCall(MethodCall call) {
Map<String, Object> arguments = (Map<String, Object>) call.arguments;
return (String) arguments.get("value");
}

private FlutterSecureStorage getOrCreateStorage(FlutterSecureStorageConfig config) {
// Key by sharedPreferencesName only, matching AndroidOptions.sharedPreferencesName.
final String name = config.getSharedPreferencesName();
synchronized (storagesBySharedPreferencesName) {
FlutterSecureStorage existing = storagesBySharedPreferencesName.get(name);
if (existing != null) {
return existing;
}
FlutterSecureStorage created = new FlutterSecureStorage(applicationContext);
storagesBySharedPreferencesName.put(name, created);
return created;
}
}

/**
* MethodChannel.Result wrapper that responds on the platform thread.
*/
Expand Down Expand Up @@ -123,14 +143,15 @@ class MethodRunner implements Runnable {
public void run() {
Map<String, Object> options = (Map<String, Object>) ((Map<String, Object>) call.arguments).get("options");
FlutterSecureStorageConfig config = new FlutterSecureStorageConfig(options);
FlutterSecureStorage secureStorage = getOrCreateStorage(config);

secureStorage.initialize(config, new SecurePreferencesCallback<>() {
@Override
public void onSuccess(Void unused) {
try {
switch (call.method) {
case "write": {
String key = getKeyFromCall(call);
String key = getKeyFromCall(secureStorage, call);
String value = getValueFromCall(call);

if (value != null) {
Expand All @@ -142,7 +163,7 @@ public void onSuccess(Void unused) {
break;
}
case "read": {
String key = getKeyFromCall(call);
String key = getKeyFromCall(secureStorage, call);

if (secureStorage.containsKey(key)) {
String value = secureStorage.read(key);
Expand All @@ -157,14 +178,14 @@ public void onSuccess(Void unused) {
break;
}
case "containsKey": {
String key = getKeyFromCall(call);
String key = getKeyFromCall(secureStorage, call);

boolean containsKey = secureStorage.containsKey(key);
result.success(containsKey);
break;
}
case "delete": {
String key = getKeyFromCall(call);
String key = getKeyFromCall(secureStorage, call);

secureStorage.delete(key);
result.success(null);
Expand Down Expand Up @@ -219,4 +240,4 @@ private void handleException(Exception e) {
result.error("Exception encountered", errorMessage, stringWriter.toString());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.it_nomads.fluttersecurestorage;

import android.content.Context;
import android.content.SharedPreferences;

/**
* Helper class that provides dual-read fallback for config SharedPreferences.
* Reads from namespaced config first, falls back to legacy global config if not found.
* Writes always go to the namespaced config.
*/
public class NamespacedConfigSource {
// Legacy global config name (used for backwards compatibility fallback reads only)
private static final String LEGACY_GLOBAL_CONFIG_NAME = "FlutterSecureStorageConfiguration";

private final SharedPreferences namespacedConfig;
private final SharedPreferences legacyConfig;

public NamespacedConfigSource(Context context, String sharedPreferencesName) {
String namespacedName = getNamespacedConfigPrefsName(sharedPreferencesName);
this.namespacedConfig = context.getSharedPreferences(namespacedName, Context.MODE_PRIVATE);
this.legacyConfig = context.getSharedPreferences(LEGACY_GLOBAL_CONFIG_NAME, Context.MODE_PRIVATE);
}

/**
* Returns the namespaced config SharedPreferences name for a given sharedPreferencesName.
* Config markers (algorithm and migration flags) are now isolated per namespace.
*
* @param sharedPreferencesName The namespace identifier
* @return Namespaced config prefs name
*/
private static String getNamespacedConfigPrefsName(String sharedPreferencesName) {
// Use a delimiter to avoid collisions with legacy global name
return "FlutterSecureStorageConfiguration:" + sharedPreferencesName;
}

/**
* Reads a string value with fallback: namespaced first, then legacy global.
*/
public String getString(String key, String defaultValue) {
String value = namespacedConfig.getString(key, null);
if (value != null) {
return value;
}
return legacyConfig.getString(key, defaultValue);
}

/**
* Reads a boolean value with fallback: namespaced first, then legacy global.
*/
public boolean getBoolean(String key, boolean defaultValue) {
// Check if key exists in namespaced config (even if value is default)
if (namespacedConfig.contains(key)) {
return namespacedConfig.getBoolean(key, defaultValue);
}
return legacyConfig.getBoolean(key, defaultValue);
}

/**
* Returns an editor for the namespaced config (writes always go to namespaced).
*/
public SharedPreferences.Editor edit() {
return namespacedConfig.edit();
}

/**
* Checks if a key exists in either namespaced or legacy config.
*/
public boolean contains(String key) {
return namespacedConfig.contains(key) || legacyConfig.contains(key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.os.Build;

import com.it_nomads.fluttersecurestorage.FlutterSecureStorageConfig;
import com.it_nomads.fluttersecurestorage.NamespacedConfigSource;

import javax.crypto.Cipher;

Expand All @@ -21,7 +22,7 @@ public class StorageCipherFactory {
private final StorageCipherAlgorithm currentStorageAlgorithm;
private final FlutterSecureStorageConfig config;

public StorageCipherFactory(SharedPreferences configSource, String keyCipherAlgorithm, String storageCipherAlgorithm, FlutterSecureStorageConfig config) {
public StorageCipherFactory(NamespacedConfigSource configSource, String keyCipherAlgorithm, String storageCipherAlgorithm, FlutterSecureStorageConfig config) {
this.config = config;
final String savedKeyCipherAlgorithm = configSource.getString(ELEMENT_PREFERENCES_ALGORITHM_KEY, null);
final String savedStorageCipherAlgorithm = configSource.getString(ELEMENT_PREFERENCES_ALGORITHM_STORAGE, null);
Expand Down
Loading