Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/code-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: "17"
Expand Down
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
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