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
10 changes: 10 additions & 0 deletions flutter_secure_storage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## Fork

* [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 Expand Up @@ -1145,6 +1145,7 @@ private SharedPreferences initializeEncryptedSharedPreferencesManager(Context co
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setKeySize(256).build())
.setRequestStrongBoxBacked(true)
.build();
return EncryptedSharedPreferences.create(
context,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.it_nomads.fluttersecurestorage;

import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
Expand All @@ -23,28 +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);

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 @@ -56,7 +61,6 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
channel = null;
}
secureStorage = null;
}

@Override
Expand All @@ -67,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 @@ -124,13 +128,24 @@ public void run() {
Map<String, Object> options = (Map<String, Object>) ((Map<String, Object>) call.arguments).get("options");
FlutterSecureStorageConfig config = new FlutterSecureStorageConfig(options);

if (call.method.equals("isStrongBoxSupported")) {
result.success(isStrongBoxAvailable);
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 @@ -142,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 @@ -157,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 @@ -5,6 +5,9 @@
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.StrongBoxUnavailableException;

import androidx.annotation.RequiresApi;

import com.it_nomads.fluttersecurestorage.FlutterSecureStorageConfig;

Expand Down Expand Up @@ -38,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 Expand Up @@ -137,26 +146,38 @@ private void setLocale(Locale locale) {
context.createConfigurationContext(config);
}

private AlgorithmParameterSpec getSpec(boolean isStrongBoxBacked) {
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 25);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return makeAlgorithmParameterSpecLegacy(context, start, end);
}

return makeAlgorithmParameterSpec(context, start, end, isStrongBoxBacked);
}

@RequiresApi(api = Build.VERSION_CODES.P)
private void createKeys(Context context) throws Exception {
final Locale localeBeforeFakingEnglishLocale = Locale.getDefault();
try {
setLocale(Locale.ENGLISH);
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 25);

KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance(TYPE_RSA, KEYSTORE_PROVIDER_ANDROID);

AlgorithmParameterSpec spec;

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
spec = makeAlgorithmParameterSpecLegacy(context, start, end);
} else {
spec = makeAlgorithmParameterSpec(context, start, end);
}
try {
spec = getSpec(true);

kpGenerator.initialize(spec);
kpGenerator.generateKeyPair();
kpGenerator.initialize(spec);
kpGenerator.generateKeyPair();
} catch (StrongBoxUnavailableException e) {
spec = getSpec(false);

kpGenerator.initialize(spec);
kpGenerator.generateKeyPair();
}
} finally {
setLocale(localeBeforeFakingEnglishLocale);
}
Expand All @@ -174,7 +195,8 @@ private AlgorithmParameterSpec makeAlgorithmParameterSpecLegacy(Context context,
.build();
}

protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end) {
@RequiresApi(api = Build.VERSION_CODES.M)
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end, boolean isStrongBoxBacked) {
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
.setCertificateSubject(new X500Principal("CN=" + keyAlias))
.setDigests(KeyProperties.DIGEST_SHA256)
Expand All @@ -183,6 +205,9 @@ protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Cal
.setCertificateSerialNumber(BigInteger.valueOf(1))
.setCertificateNotBefore(start.getTime())
.setCertificateNotAfter(end.getTime());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
builder.setIsStrongBoxBacked(true);
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ 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)
@Override
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end) {
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end, boolean isStrongBoxBacked) {
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
.setCertificateSubject(new X500Principal("CN=" + keyAlias))
.setDigests(KeyProperties.DIGEST_SHA256)
Expand All @@ -41,6 +47,9 @@ protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Cal
.setCertificateSerialNumber(BigInteger.valueOf(1))
.setCertificateNotBefore(start.getTime())
.setCertificateNotAfter(end.getTime());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
builder.setIsStrongBoxBacked(true);
}
return builder.build();
}

Expand Down
Loading
Loading