Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
be1fdce
Enable StrongBox by default on Android with a fallback
westracer Mar 5, 2025
733b438
fix(app): flag to force strongbox by removing fallback
Numoy Mar 6, 2025
ea794b8
fix(app): update changelog
Numoy Mar 6, 2025
4aa2c13
fix(app): add function to check if the device supports strongbox
Numoy Mar 6, 2025
12d92c9
fix(app): add method to check if strongbox is supported
Numoy Mar 6, 2025
ccab95c
fix(app): use own interface definition
Numoy Mar 6, 2025
c024bc7
fix(app): use git platform interface
Numoy Mar 6, 2025
c6475d6
fix(app): fix test
Numoy Mar 6, 2025
7b738c0
fix(app): add unsported error to windows
Numoy Mar 6, 2025
750e0e2
fix(app): default impl for strong box
Numoy Mar 6, 2025
3c20c9d
fix(app): update dependencies
Numoy Mar 6, 2025
0f80b5f
fix(app): fix formatting
Numoy Mar 6, 2025
05a98d5
fix(app): emtpy impl strongbox windows
Numoy Mar 6, 2025
c24f4e0
fix(app): add check for strongbox
Numoy Mar 6, 2025
05b9a6f
fix(app): use correct application context
Numoy Mar 6, 2025
849c36b
fix(app): fix method signature
Numoy Mar 6, 2025
768e282
fix(app): add argument
Numoy Mar 6, 2025
575fb74
fix(app): update isStrongBoxSupported
Numoy Mar 6, 2025
d9ee174
fix(app): add options to method interface
Numoy Mar 6, 2025
f3db17c
fix(app): update changelog
Numoy Mar 7, 2025
77934bb
Merge pull request #1 from QuickBirdEng/strongbox-enabled
Numoy Mar 7, 2025
bc8dfa3
fix(app): update refs
Numoy Mar 7, 2025
c615965
Merge pull request #6 from QuickBirdEng/feature/update-ref
Numoy Mar 7, 2025
417ded8
chore(deps): bump com.android.tools.build:gradle
dependabot[bot] Aug 4, 2025
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
7 changes: 7 additions & 0 deletions flutter_secure_storage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 10.0.1
* Enabled StrongBox by default, use fallback if it's not available.
* [Android] Allow to force StrongBox with a flag (onlyAllowStrongBox)
* [Android] Method to check if an Android device supports Strongbox

# Before fork

## 10.0.0-beta.4
* [Apple] Merged ios and macos implementation into a new package flutter_secure_storage_darwin
* [Apple] Refactored code and added missing options
Expand Down
2 changes: 1 addition & 1 deletion flutter_secure_storage/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:8.8.0'
classpath 'com.android.tools.build:gradle:8.12.0'
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.StrongBoxUnavailableException;
import android.util.Base64;
import android.util.Log;

Expand Down Expand Up @@ -31,6 +33,7 @@ public class FlutterSecureStorage {
private static final String PREF_OPTION_PREFIX = "preferencesKeyPrefix";
private static final String PREF_OPTION_DELETE_ON_FAILURE = "resetOnError";
private static final String PREF_KEY_MIGRATED = "preferencesMigrated";
private static final String PREF_OPTION_ONLY_ALLOW_STRONGBOX = "onlyAllowStrongBox";
@NonNull
private final SharedPreferences encryptedPreferences;
@NonNull
Expand Down Expand Up @@ -61,7 +64,15 @@ public FlutterSecureStorage(Context context, Map<String, Object> options) throws
}
}

encryptedPreferences = getEncryptedSharedPreferences(deleteOnFailure, options, context.getApplicationContext(), sharedPreferencesName);
boolean onlyAllowStrongbox = false;
if (options.containsKey(PREF_OPTION_ONLY_ALLOW_STRONGBOX)) {
var value = options.get(PREF_OPTION_ONLY_ALLOW_STRONGBOX);
if (value instanceof String) {
onlyAllowStrongbox = Boolean.parseBoolean((String) value);
}
}

encryptedPreferences = getEncryptedSharedPreferences(deleteOnFailure, options, context.getApplicationContext(), sharedPreferencesName, true, onlyAllowStrongbox);
}

public boolean containsKey(String key) {
Expand All @@ -83,6 +94,7 @@ public void delete(String key) {
public void deleteAll() {
encryptedPreferences.edit().clear().apply();
}


public Map<String, String> readAll() {
Map<String, String> result = new HashMap<>();
Expand All @@ -102,15 +114,25 @@ private String addPrefixToKey(String key) {
return preferencesKeyPrefix + "_" + key;
}

private SharedPreferences getEncryptedSharedPreferences(boolean deleteOnFailure, Map<String, Object> options, Context context, String sharedPreferencesName) throws GeneralSecurityException, IOException {
private SharedPreferences getEncryptedSharedPreferences(boolean deleteOnFailure, Map<String, Object> options, Context context, String sharedPreferencesName, boolean isStrongBoxBacked, boolean isOnlyStrongBoxAllowed) throws GeneralSecurityException, IOException {
try {
final SharedPreferences encryptedPreferences = initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName);
final SharedPreferences encryptedPreferences = initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName, isStrongBoxBacked);
boolean migrated = encryptedPreferences.getBoolean(PREF_KEY_MIGRATED, false);
if (!migrated) {
migrateToEncryptedPreferences(context, sharedPreferencesName, encryptedPreferences, deleteOnFailure, options);
}
return encryptedPreferences;
} catch (GeneralSecurityException | IOException e) {
if (e instanceof GeneralSecurityException) {
Throwable cause = e.getCause();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (cause instanceof StrongBoxUnavailableException && !isOnlyStrongBoxAllowed) {
// Fallback to not using Strongbox
return getEncryptedSharedPreferences(deleteOnFailure, options, context, sharedPreferencesName, false, isOnlyStrongBoxAllowed);
}
}
}

if (!deleteOnFailure) {
Log.w(TAG, "initialization failed, resetOnError false, so throwing exception.", e);
Expand All @@ -121,24 +143,29 @@ private SharedPreferences getEncryptedSharedPreferences(boolean deleteOnFailure,
context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE).edit().clear().apply();

try {
return initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName);
return initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName, isStrongBoxBacked);
} catch (Exception f) {
Log.e(TAG, "initialization after reset failed", f);
throw f;
}
}
}

private SharedPreferences initializeEncryptedSharedPreferencesManager(Context context, String sharedPreferencesName) throws GeneralSecurityException, IOException {
private SharedPreferences initializeEncryptedSharedPreferencesManager(Context context, String sharedPreferencesName, boolean isStrongBoxBacked) throws GeneralSecurityException, IOException {
KeyGenParameterSpec.Builder keyGenBuilder = new KeyGenParameterSpec.Builder(
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
keyGenBuilder.setIsStrongBoxBacked(true);
}

MasterKey masterKey = new MasterKey.Builder(context)
.setKeyGenParameterSpec(new KeyGenParameterSpec.Builder(
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build())
.build();
.setKeyGenParameterSpec(keyGenBuilder.build())
.build(isStrongBoxBacked);

return EncryptedSharedPreferences.create(
context,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.it_nomads.fluttersecurestorage;

import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
Expand All @@ -24,6 +25,7 @@ public class FlutterSecureStoragePlugin implements MethodCallHandler, FlutterPlu
private HandlerThread workerThread;
private Handler workerThreadHandler;
private FlutterPluginBinding binding;
private boolean isStrongBoxAvailable;

@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
Expand Down Expand Up @@ -52,6 +54,7 @@ private boolean initSecureStorage(Result result, Map<String, Object> options) {
if (secureStorage != null) return true;

try {
isStrongBoxAvailable = binding.getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
secureStorage = new FlutterSecureStorage(binding.getApplicationContext(), options);
return true;
} catch (Exception e) {
Expand Down Expand Up @@ -123,6 +126,9 @@ private void handleMethodCall(MethodCall call, Result result) {
case "deleteAll":
handleDeleteAll(result);
break;
case "isStrongBoxSupported":
handleStrongBoxAvailable(result);
break;
default:
result.notImplemented();
}
Expand Down Expand Up @@ -164,6 +170,10 @@ private void handleDeleteAll(Result result) {
result.success(null);
}

private void handleStrongBoxAvailable(Result result) {
result.success(isStrongBoxAvailable);
}

@SuppressWarnings("unchecked")
private Map<String, Object> extractMapFromObject(Object object) {
if (!(object instanceof Map)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.StrongBoxUnavailableException;

import androidx.annotation.RequiresApi;

Expand Down Expand Up @@ -123,26 +124,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();
} catch (StrongBoxUnavailableException e) {
spec = getSpec(false);

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

@RequiresApi(api = Build.VERSION_CODES.M)
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 @@ -170,6 +183,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 @@ -30,7 +30,7 @@ protected String createKeyAlias() {

@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 @@ -39,6 +39,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
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ public Builder setKeyGenParameterSpec(@NonNull KeyGenParameterSpec keyGenParamet
* @return The master key.
*/
@NonNull
public MasterKey build() throws GeneralSecurityException, IOException {
return Api23Impl.build(this);
public MasterKey build(boolean isStrongBoxBacked) throws GeneralSecurityException, IOException {
return Api23Impl.build(this, isStrongBoxBacked);
}

static class Api23Impl {
Expand All @@ -277,7 +277,7 @@ static String getKeystoreAlias(KeyGenParameterSpec keyGenParameterSpec) {
return keyGenParameterSpec.getKeystoreAlias();
}
@SuppressWarnings("deprecation")
static MasterKey build(Builder builder) throws GeneralSecurityException, IOException {
static MasterKey build(Builder builder, boolean isStrongBoxBacked) throws GeneralSecurityException, IOException {
if (builder.mKeyScheme == null && builder.mKeyGenParameterSpec == null) {
throw new IllegalArgumentException("build() called before "
+ "setKeyGenParameterSpec or setKeyScheme.");
Expand All @@ -289,6 +289,9 @@ static MasterKey build(Builder builder) throws GeneralSecurityException, IOExcep
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(DEFAULT_AES_GCM_MASTER_KEY_SIZE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
keyGenBuilder.setIsStrongBoxBacked(true);
}
if (builder.mAuthenticationRequired) {
keyGenBuilder.setUserAuthenticationRequired(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Expand Down
13 changes: 13 additions & 0 deletions flutter_secure_storage/lib/flutter_secure_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,19 @@ class FlutterSecureStorage {
});
}

/// [aOptions] optional Android options
Future<bool> isStrongBoxSupported({
AndroidOptions? aOptions,
}) async {
if (defaultTargetPlatform == TargetPlatform.android) {
return _platform.isStrongBoxSupported(
options: aOptions?.params ?? this.aOptions.params,
);
} else {
throw UnsupportedError(_unsupportedPlatform);
}
}

/// Select correct options based on current platform
Map<String, String> _selectOptions(
AppleOptions? iOptions,
Expand Down
13 changes: 12 additions & 1 deletion flutter_secure_storage/lib/options/android_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ class AndroidOptions extends Options {
StorageCipherAlgorithm.AES_CBC_PKCS7Padding,
this.sharedPreferencesName,
this.preferencesKeyPrefix,
bool onlyAllowStrongBox = false,
}) : _encryptedSharedPreferences = encryptedSharedPreferences,
_resetOnError = resetOnError,
_keyCipherAlgorithm = keyCipherAlgorithm,
_storageCipherAlgorithm = storageCipherAlgorithm;
_storageCipherAlgorithm = storageCipherAlgorithm,
_onlyAllowStrongBox = onlyAllowStrongBox;

/// EncryptedSharedPrefences are only available on API 23 and greater
final bool _encryptedSharedPreferences;
Expand Down Expand Up @@ -70,6 +72,12 @@ class AndroidOptions extends Options {
/// WARNING: If you change this you can't retrieve already saved preferences.
final String? preferencesKeyPrefix;

/// If true, only allow keys to be stored in StrongBox backed keymaster.
/// This option is only available on API 28 and greater. If set to true some phones might now work
/// Defaults to false.
/// https://developer.android.com/training/articles/keystore#HardwareSecurityModule
final bool _onlyAllowStrongBox;

static const AndroidOptions defaultOptions = AndroidOptions();

@override
Expand All @@ -80,6 +88,7 @@ class AndroidOptions extends Options {
'storageCipherAlgorithm': _storageCipherAlgorithm.name,
'sharedPreferencesName': sharedPreferencesName ?? '',
'preferencesKeyPrefix': preferencesKeyPrefix ?? '',
'onlyAllowStrongBox': '$_onlyAllowStrongBox',
};

AndroidOptions copyWith({
Expand All @@ -89,6 +98,7 @@ class AndroidOptions extends Options {
StorageCipherAlgorithm? storageCipherAlgorithm,
String? preferencesKeyPrefix,
String? sharedPreferencesName,
bool? onlyAllowStrongBox,
}) =>
AndroidOptions(
encryptedSharedPreferences:
Expand All @@ -99,5 +109,6 @@ class AndroidOptions extends Options {
storageCipherAlgorithm ?? _storageCipherAlgorithm,
sharedPreferencesName: sharedPreferencesName,
preferencesKeyPrefix: preferencesKeyPrefix,
onlyAllowStrongBox: onlyAllowStrongBox ?? _onlyAllowStrongBox,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,10 @@ class TestFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
required Map<String, String> options,
}) async =>
data[key] = value;

@override
Future<bool> isStrongBoxSupported({
required Map<String, String> options,
}) async =>
true;
}
Loading
Loading