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 .github/workflows/soup-approval-verification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: SOUP - Approval Verification

on:
pull_request_review:
types: [submitted]

jobs:
soups:
uses: QuickBirdEng/workflows/.github/workflows/soup-approval-verification-workflow.yml@main
secrets: inherit
108 changes: 108 additions & 0 deletions .soups/dart/flutter_secure_storage-10.x.x.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"package": "flutter_secure_storage",
"version": "10.x.x",
"purpose": "A Flutter plugin to securely store sensitive data in a key-value pair format using platform-specific secure storage solutions. It supports Android, iOS, macOS, Windows, and Linux.",
"provider": "QuickBird GmbH",
"license": "QuickBird",
"urls": {
"provider": "https://github.com/QuickBirdEng/flutter_secure_storage",
"documentation": "https://github.com/QuickBirdEng/flutter_secure_storage",
"known_issues": "https://github.com/QuickBirdEng/flutter_secure_storage/issues"
},
"requirements": {
"grq-1": {
"description": "Has a suitable license",
"fulfilled": true,
"fulfilled_visual": "✅",
"reason_if_requirement_not_fulfilled": "",
"metadata": {
"license": "QuickBird",
"license_key": "qb",
"license_spdx_id": "BSD-3-Clause",
"license_url": "https://github.com/QuickBirdEng/flutter_secure_storage/blob/develop/LICENSE",
"compliance_status": "ALLOWED: License 'qb' is in allowed list"
}
},
"grq-2": {
"description": "Comprehensive documentation is available",
"fulfilled": true,
"fulfilled_visual": "✅",
"reason_if_requirement_not_fulfilled": "",
"metadata": "https://github.com/QuickBirdEng/flutter_secure_storage"
},
"grq-3": {
"description": "Is maintained and support is available",
"fulfilled": false,
"fulfilled_visual": "❌",
"reason_if_requirement_not_fulfilled": "The package is QuickBird's internal package and releases can be provisioned (on demand) when needed.",
"metadata": {
"analysis_period": "12 months",
"releases_found": 1,
"min_expected": 2,
"recent_versions": {
"v10.0.1": "March 10, 2025"
}
}
},
"grq-4": {
"description": "Does not contain major or critical security issues.",
"fulfilled": true,
"fulfilled_visual": "✅",
"reason_if_requirement_not_fulfilled": "",
"metadata": {
"package": "flutter_secure_storage",
"commit_checked": {
"version": "10.0.1",
"commit": "c61596586e27ac06c70b2ba2680d15a10fa4c87a"
},
"vulnerabilities_count": 0,
"vulnerabilities": {}
}
},
"grq-5": {
"description": "Provider is reliable, trustworthy and communicative",
"fulfilled": true,
"fulfilled_visual": "✅",
"reason_if_requirement_not_fulfilled": "",
"metadata": {
"stars": 0,
"forks": 0,
"subscribers": 0,
"one_of_following_conditions": {
"stars_or_forks_or_subscribers_any_greater_than_or_equal_to_1500": "❌",
"sum_of_stars_forks_subscribers_greater_than_or_equal_to_3000": "❌",
"self_owned": "✅"
}
}
},
"grq-6": {
"description": "Conforms to Semantic Versioning",
"fulfilled": true,
"fulfilled_visual": "✅",
"reason_if_requirement_not_fulfilled": "",
"metadata": {
"total_versions_checked": "1"
}
},
"version-check": {
"description": "Is within the latest stable semantic family (Major.X.X or 0.Minor.X)",
"fulfilled": true,
"fulfilled_visual": "✅",
"reason_if_requirement_not_fulfilled": "",
"metadata": {
"latest_stable": "10.0.1",
"latest_stable_tag_url": "https://github.com/QuickBirdEng/flutter_secure_storage/releases/tag/v10.0.1"
}
}
},
"metadata": {
"created": "2025-11-06T09:29:36Z",
"updated": "2025-11-06T09:29:36Z",
"input_version": "10.0.1",
"approval": {
"date": "",
"by": "",
"condition": ""
}
}
}
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
Loading
Loading