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
4 changes: 4 additions & 0 deletions flutter_secure_storage/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
* [Android] Enabled StrongBox by default, use fallback if it's not available.
* [Android] Method to check if an Android device supports Strongbox
* [Android] Use old algorithms as default (migration to AES_GCM_NoPadding is broken and fails)
* [Android] Set invalidatedByBiometricEnrollment to false
* [Android] Create separate instances of FlutterSecureStorage with different configs/options
* [Android] Use separate keys for different storage instances
* [iOS] Add option to use secure enclave (based on [#989 PR](https://github.com/juliansteenbakker/flutter_secure_storage/pull/989))

## 10.0.0
This major release brings significant security improvements, platform updates, and modernization across all supported platforms.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ 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 static final String SHARED_PREFERENCES_CONFIG_NAME_SUFFIX = "Configuration";

private FlutterSecureStorageConfig config;
@NonNull
Expand Down Expand Up @@ -158,7 +158,7 @@ protected void initialize(FlutterSecureStorageConfig config, SecurePreferencesCa
);

SharedPreferences configSource = context.getSharedPreferences(
SHARED_PREFERENCES_CONFIG_NAME,
config.getSharedPreferencesName() + SHARED_PREFERENCES_CONFIG_NAME_SUFFIX,
Context.MODE_PRIVATE
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,32 @@ public class FlutterSecureStoragePlugin implements MethodCallHandler, FlutterPlu

private static final String TAG = "FlutterSecureStoragePlugin";
private MethodChannel channel;
private FlutterSecureStorage secureStorage;
private HandlerThread workerThread;
private Handler workerThreadHandler;
private boolean isStrongBoxAvailable;
private FlutterPluginBinding binding;

public void initInstance(BinaryMessenger messenger, Context context) {
public FlutterSecureStorage initInstance(Context context) {
try {
secureStorage = new FlutterSecureStorage(context);
isStrongBoxAvailable = context.getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);

workerThread = new HandlerThread("com.it_nomads.fluttersecurestorage.worker");
workerThread.start();
workerThreadHandler = new Handler(workerThread.getLooper());

channel = new MethodChannel(messenger, "plugins.it_nomads.com/flutter_secure_storage");
channel.setMethodCallHandler(this);
return new FlutterSecureStorage(context);
} catch (Exception e) {
Log.e(TAG, "Registration failed", e);
return null;
}
}

@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
initInstance(binding.getBinaryMessenger(), binding.getApplicationContext());
this.binding = binding;

isStrongBoxAvailable = binding.getApplicationContext().getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);

workerThread = new HandlerThread("com.it_nomads.fluttersecurestorage.worker");
workerThread.start();
workerThreadHandler = new Handler(workerThread.getLooper());

channel = new MethodChannel(binding.getBinaryMessenger(), "plugins.it_nomads.com/flutter_secure_storage");
channel.setMethodCallHandler(this);
}

@Override
Expand All @@ -59,7 +61,6 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
channel = null;
}
secureStorage = null;
}

@Override
Expand All @@ -70,7 +71,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result rawResult) {
}

@SuppressWarnings("unchecked")
private String getKeyFromCall(MethodCall call) {
private String getKeyFromCall(MethodCall call, FlutterSecureStorage secureStorage) {
Map<String, Object> arguments = (Map<String, Object>) call.arguments;
return secureStorage.addPrefixToKey((String) arguments.get("key"));
}
Expand Down Expand Up @@ -132,13 +133,19 @@ public void run() {
return;
}

FlutterSecureStorage secureStorage = initInstance(binding.getApplicationContext());
if (secureStorage == null) {
result.error("Could not initialize FlutterSecureStorage", null, null);
return;
}

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

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

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

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

secureStorage.delete(key);
result.success(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ class KeyCipherImplementationAES23 implements KeyCipher {

private static final String TAG = "AESCipher23";
private static final String KEYSTORE_PROVIDER_ANDROID = "AndroidKeyStore";
private static final String SHARED_PREFERENCES_NAME = "FlutterSecureKeyStorage";
private static final String SHARED_PREFERENCES_KEY = "KeyStoreIV1";
private static final int IV_SIZE = 16;
private static final int KEY_SIZE = 256;
protected final String keyAlias;
protected final String ivStorageKey;
protected final String ivStoragePrefsName;

protected final Context context;
protected final FlutterSecureStorageConfig config;
Expand All @@ -42,6 +42,17 @@ public KeyCipherImplementationAES23(Context context, FlutterSecureStorageConfig
this.context = context;
this.config = config;
keyAlias = createKeyAlias(context);

// Backward compatibility: use original storage names for default config
if ("FlutterSecureStorage".equals(config.getSharedPreferencesName())) {
ivStoragePrefsName = "FlutterSecureKeyStorage";
ivStorageKey = "KeyStoreIV1";
} else {
String configId = config.getSharedPreferencesName() + "_" + config.getSharedPreferencesKeyPrefix();
ivStoragePrefsName = "FlutterSecureKeyStorage_" + configId;
ivStorageKey = "KeyStoreIV1_" + configId;
}

KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID);
ks.load(null);
Key privateKey = ks.getKey(keyAlias, null);
Expand All @@ -61,7 +72,13 @@ public Key unwrap(byte[] wrappedKey, String algorithm) throws UnsupportedOperati
}

protected String createKeyAlias(Context context) {
return context.getPackageName() + ".FlutterSecureStoragePluginKey";
// Backward compatibility: use original key name for default config
if ("FlutterSecureStorage".equals(config.getSharedPreferencesName())) {
return context.getPackageName() + ".FlutterSecureStoragePluginKey";
}

String configId = config.getSharedPreferencesName() + "_" + config.getSharedPreferencesKeyPrefix();
return context.getPackageName() + ".FlutterSecureStoragePluginKey_" + configId;
}

@Override
Expand All @@ -70,8 +87,8 @@ public void deleteKey() throws Exception {
ks.load(null);
ks.deleteEntry(keyAlias);

SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
preferences.edit().remove(SHARED_PREFERENCES_KEY).apply();
SharedPreferences preferences = context.getSharedPreferences(ivStoragePrefsName, Context.MODE_PRIVATE);
preferences.edit().remove(ivStorageKey).apply();
}

@Override
Expand All @@ -90,8 +107,8 @@ public Cipher getCipher(Context context) throws Exception {

public Cipher getEncryptionCipher(Context context, Key key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
String ivBase64 = preferences.getString(SHARED_PREFERENCES_KEY, null);
SharedPreferences preferences = context.getSharedPreferences(ivStoragePrefsName, Context.MODE_PRIVATE);
String ivBase64 = preferences.getString(ivStorageKey, null);

if (ivBase64 != null) {
byte[] iv = Base64.decode(ivBase64, Base64.DEFAULT);
Expand All @@ -103,7 +120,7 @@ public Cipher getEncryptionCipher(Context context, Key key) throws NoSuchPadding

byte[] iv = cipher.getIV();
SharedPreferences.Editor editor = preferences.edit();
editor.putString(SHARED_PREFERENCES_KEY, Base64.encodeToString(iv, Base64.DEFAULT));
editor.putString(ivStorageKey, Base64.encodeToString(iv, Base64.DEFAULT));
editor.apply();
}

Expand Down Expand Up @@ -167,7 +184,7 @@ public void generateSymmetricKey() throws Exception {
configureLegacyAuth(builder);
}

builder.setInvalidatedByBiometricEnrollment(true);
builder.setInvalidatedByBiometricEnrollment(false);
} else {
// Explicitly set to false for clarity (default behavior)
builder.setUserAuthenticationRequired(false);
Expand Down Expand Up @@ -212,7 +229,7 @@ public void generateSymmetricKey() throws Exception {
configureLegacyAuth(builder);
}

builder.setInvalidatedByBiometricEnrollment(true);
builder.setInvalidatedByBiometricEnrollment(false);
}

keyGenerator.init(builder.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ public KeyCipherImplementationRSA18(Context context, FlutterSecureStorageConfig
}

protected String createKeyAlias() {
return context.getPackageName() + ".FlutterSecureStoragePluginKey";
// Backward compatibility: use original key name for default config
if ("FlutterSecureStorage".equals(config.getSharedPreferencesName())) {
return context.getPackageName() + ".FlutterSecureStoragePluginKey";
}

String configId = config.getSharedPreferencesName() + "_" + config.getSharedPreferencesKeyPrefix();
return context.getPackageName() + ".FlutterSecureStoragePluginKey_" + configId;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ public KeyCipherImplementationRSAOAEP(Context context, FlutterSecureStorageConfi

@Override
protected String createKeyAlias() {
return context.getPackageName() + ".FlutterSecureStoragePluginKeyOAEP";
// Backward compatibility: use original key name for default config
if ("FlutterSecureStorage".equals(config.getSharedPreferencesName())) {
return context.getPackageName() + ".FlutterSecureStoragePluginKeyOAEP";
}

String configId = config.getSharedPreferencesName() + "_" + config.getSharedPreferencesKeyPrefix();
return context.getPackageName() + ".FlutterSecureStoragePluginKeyOAEP_" + configId;
}

@RequiresApi(api = Build.VERSION_CODES.M)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,18 @@ private StorageCipher createStorageCipher(Context context, KeyCipher keyCipher,
if (algorithm == StorageCipherAlgorithm.AES_GCM_NoPadding) {
if (isKeyStoreKeyCipher(keyCipher)) {
// Use KeyStore-based implementation (biometric/PIN auth capable)
return new StorageCipherImplementationAES23(context, keyCipher, cipher);
return new StorageCipherImplementationAES23(context, keyCipher, cipher, config);
} else {
// Use RSA-wrapped implementation (standard secure storage)
return new StorageCipherImplementationGCM(context, keyCipher, cipher);
return new StorageCipherImplementationGCM(context, keyCipher, cipher, config);
}
}

// For other algorithms, use the function from enum
if (algorithm.storageCipher == null) {
throw new Exception("No implementation available for algorithm: " + algorithm.name());
}
return algorithm.storageCipher.apply(context, keyCipher, cipher);
return algorithm.storageCipher.apply(context, keyCipher, cipher, config);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import android.content.Context;

import com.it_nomads.fluttersecurestorage.FlutterSecureStorageConfig;

import javax.crypto.Cipher;

@FunctionalInterface
interface StorageCipherFunction {
StorageCipher apply(Context context, KeyCipher keyCipher, Cipher cipher) throws Exception;
StorageCipher apply(Context context, KeyCipher keyCipher, Cipher cipher, FlutterSecureStorageConfig config) throws Exception;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import android.content.SharedPreferences;
import android.util.Base64;

import com.it_nomads.fluttersecurestorage.FlutterSecureStorageConfig;

import java.security.Key;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
Expand All @@ -15,19 +17,29 @@
public class StorageCipherImplementationAES18 implements StorageCipher {
private static final int keySize = 16;
private static final String KEY_ALGORITHM = "AES";
private static final String SHARED_PREFERENCES_NAME = "FlutterSecureKeyStorage";
private static final String SHARED_PREFERENCES_KEY = "VGhpcyBpcyB0aGUga2V5IGZvciBhIHNlY3VyZSBzdG9yYWdlIEFFUyBLZXkK";
private final String sharedPreferencesName;
private final String sharedPreferencesKey;
private final Cipher cipher;
private final SecureRandom secureRandom;
private final Key secretKey;

public StorageCipherImplementationAES18(Context context, KeyCipher rsaCipher, Cipher ignoredStorageCipher) throws Exception {
public StorageCipherImplementationAES18(Context context, KeyCipher rsaCipher, Cipher ignoredStorageCipher, FlutterSecureStorageConfig config) throws Exception {
secureRandom = new SecureRandom();

SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
// Backward compatibility: use original storage names for default config
if ("FlutterSecureStorage".equals(config.getSharedPreferencesName())) {
this.sharedPreferencesName = "FlutterSecureKeyStorage";
this.sharedPreferencesKey = "VGhpcyBpcyB0aGUga2V5IGZvciBhIHNlY3VyZSBzdG9yYWdlIEFFUyBLZXkK";
} else {
String configId = config.getSharedPreferencesName() + "_" + config.getSharedPreferencesKeyPrefix();
this.sharedPreferencesName = "FlutterSecureKeyStorage_" + configId;
this.sharedPreferencesKey = "VGhpcyBpcyB0aGUga2V5IGZvciBhIHNlY3VyZSBzdG9yYWdlIEFFUyBLZXkK_" + configId;
}

SharedPreferences preferences = context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();

String aesKey = preferences.getString(SHARED_PREFERENCES_KEY, null);
String aesKey = preferences.getString(sharedPreferencesKey, null);

cipher = getCipher();

Expand All @@ -44,14 +56,14 @@ public StorageCipherImplementationAES18(Context context, KeyCipher rsaCipher, Ci
secretKey = new SecretKeySpec(key, KEY_ALGORITHM);

byte[] encryptedKey = rsaCipher.wrap(secretKey);
editor.putString(SHARED_PREFERENCES_KEY, Base64.encodeToString(encryptedKey, Base64.DEFAULT));
editor.putString(sharedPreferencesKey, Base64.encodeToString(encryptedKey, Base64.DEFAULT));
editor.apply();
}

@Override
public void deleteKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
preferences.edit().remove(SHARED_PREFERENCES_KEY).apply();
SharedPreferences preferences = context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
preferences.edit().remove(sharedPreferencesKey).apply();
}

protected Cipher getCipher() throws Exception {
Expand Down
Loading
Loading