Skip to content

Commit 5b77e5f

Browse files
committed
Enable StrongBox by default on Android with a fallback
1 parent 05b1c4b commit 5b77e5f

File tree

19 files changed

+183
-26
lines changed

19 files changed

+183
-26
lines changed

flutter_secure_storage/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Fork
2+
3+
* Enabled StrongBox by default, use fallback if it's not available.
4+
* [Android] Allow to force StrongBox with a flag (onlyAllowStrongBox)
5+
* [Android] Method to check if an Android device supports Strongbox
6+
17
## 10.0.0
28
This major release brings significant security improvements, platform updates, and modernization across all supported platforms.
39

flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,7 @@ private SharedPreferences initializeEncryptedSharedPreferencesManager(Context co
11451145
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
11461146
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
11471147
.setKeySize(256).build())
1148-
.build();
1148+
.build(config.getOnlyAllowStrongBox());
11491149
return EncryptedSharedPreferences.create(
11501150
context,
11511151
config.getSharedPreferencesName(),

flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorageConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class FlutterSecureStorageConfig {
1212
private static final Boolean DEFAULT_MIGRATE_ON_ALGORITHM_CHANGE = true;
1313
private static final Boolean DEFAULT_ENCRYPTED_SHARED_PREFERENCES = false;
1414
private static final Boolean DEFAULT_ENFORCE_BIOMETRICS = false;
15+
private static final Boolean DEFAULT_ONLY_ALLOW_STRONGBOX = false;
1516
private static final String DEFAULT_BIOMETRIC_PROMPT_TITLE = "Authenticate to access";
1617
private static final String DEFAULT_BIOMETRIC_PROMPT_SUBTITLE = "Use biometrics or device credentials";
1718
private static final String DEFAULT_STORAGE_CIPHER_ALGORITHM = "AES_GCM_NoPadding";
@@ -23,6 +24,7 @@ public class FlutterSecureStorageConfig {
2324
public static final String PREF_OPTION_MIGRATE_ON_ALGORITHM_CHANGE = "migrateOnAlgorithmChange";
2425
public static final String PREF_OPTION_ENCRYPTED_SHARED_PREFERENCES = "encryptedSharedPreferences";
2526
public static final String PREF_OPTION_ENFORCE_BIOMETRICS = "enforceBiometrics";
27+
public static final String PREF_OPTION_ONLY_ALLOW_STRONGBOX = "onlyAllowStrongBox";
2628
public static final String PREF_OPTION_BIOMETRIC_PROMPT_TITLE = "prefOptionBiometricPromptTitle";
2729
public static final String PREF_OPTION_BIOMETRIC_PROMPT_SUBTITLE = "prefOptionBiometricPromptSubtitle";
2830
public static final String PREF_OPTION_STORAGE_CIPHER_ALGORITHM = "storageCipherAlgorithm";
@@ -34,6 +36,7 @@ public class FlutterSecureStorageConfig {
3436
private final boolean migrateOnAlgorithmChange;
3537
private final boolean useEncryptedSharedPreferences;
3638
private final boolean enforceBiometrics;
39+
private final boolean onlyAllowStrongBox;
3740
private final String biometricPromptTitle;
3841
private final String biometricPromptSubtitle;
3942
private final String keyCipherAlgorithm;
@@ -46,6 +49,7 @@ public FlutterSecureStorageConfig(Map<String, Object> options) {
4649
this.migrateOnAlgorithmChange = getBooleanOption(options, PREF_OPTION_MIGRATE_ON_ALGORITHM_CHANGE, DEFAULT_MIGRATE_ON_ALGORITHM_CHANGE);
4750
this.useEncryptedSharedPreferences = getBooleanOption(options, PREF_OPTION_ENCRYPTED_SHARED_PREFERENCES, DEFAULT_ENCRYPTED_SHARED_PREFERENCES);
4851
this.enforceBiometrics = getBooleanOption(options, PREF_OPTION_ENFORCE_BIOMETRICS, DEFAULT_ENFORCE_BIOMETRICS);
52+
this.onlyAllowStrongBox = getBooleanOption(options, PREF_OPTION_ONLY_ALLOW_STRONGBOX, DEFAULT_ONLY_ALLOW_STRONGBOX);
4953
this.biometricPromptTitle = getStringOption(options, PREF_OPTION_BIOMETRIC_PROMPT_TITLE, DEFAULT_BIOMETRIC_PROMPT_TITLE);
5054
this.biometricPromptSubtitle = getStringOption(options, PREF_OPTION_BIOMETRIC_PROMPT_SUBTITLE, DEFAULT_BIOMETRIC_PROMPT_SUBTITLE);
5155
this.storageCipherAlgorithm = getStringOption(options, PREF_OPTION_STORAGE_CIPHER_ALGORITHM, DEFAULT_STORAGE_CIPHER_ALGORITHM);
@@ -80,6 +84,7 @@ private boolean getBooleanOption(Map<String, Object> options, String key, boolea
8084

8185
public boolean isUseEncryptedSharedPreferences() { return useEncryptedSharedPreferences; }
8286
public boolean getEnforceBiometrics() { return enforceBiometrics; }
87+
public boolean getOnlyAllowStrongBox() { return onlyAllowStrongBox; }
8388

8489
public String getBiometricPromptTitle() { return biometricPromptTitle; }
8590
public String getPrefOptionBiometricPromptSubtitle() { return biometricPromptSubtitle; }
@@ -95,6 +100,7 @@ public String toString() {
95100
", deleteOnFailure=" + deleteOnFailure +
96101
", migrateOnAlgorithmChange=" + migrateOnAlgorithmChange +
97102
", enforceBiometrics=" + enforceBiometrics +
103+
", onlyAllowStrongBox=" + onlyAllowStrongBox +
98104
'}';
99105
}
100106
}

flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.it_nomads.fluttersecurestorage;
22

33
import android.content.Context;
4+
import android.content.pm.PackageManager;
45
import android.os.Handler;
56
import android.os.HandlerThread;
67
import android.os.Looper;
@@ -26,10 +27,12 @@ public class FlutterSecureStoragePlugin implements MethodCallHandler, FlutterPlu
2627
private FlutterSecureStorage secureStorage;
2728
private HandlerThread workerThread;
2829
private Handler workerThreadHandler;
30+
private boolean isStrongBoxAvailable;
2931

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

3437
workerThread = new HandlerThread("com.it_nomads.fluttersecurestorage.worker");
3538
workerThread.start();
@@ -180,6 +183,10 @@ public void onSuccess(Void unused) {
180183
result.success(available);
181184
break;
182185
}
186+
case "isStrongBoxSupported": {
187+
result.success(isStrongBoxAvailable);
188+
break;
189+
}
183190
case "isDeviceSecure": {
184191
boolean secure = secureStorage.isDeviceSecure();
185192
result.success(secure);

flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/KeyCipherImplementationAES23.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public void generateSymmetricKey() throws Exception {
138138
// Check if device has security (PIN/biometric) configured
139139
boolean deviceHasSecurity = isDeviceSecure();
140140
boolean enforceBiometrics = config.getEnforceBiometrics();
141+
boolean isStrongBoxBacked = config.getOnlyAllowStrongBox();
141142

142143
// ENFORCEMENT MODE: Fail if enforcement enabled but no device security
143144
if (enforceBiometrics && !deviceHasSecurity) {
@@ -177,7 +178,7 @@ public void generateSymmetricKey() throws Exception {
177178
builder.setUnlockedDeviceRequired(true);
178179

179180
// Only enable StrongBox if it's available
180-
if (isStrongBoxAvailable()) {
181+
if (isStrongBoxBacked && isStrongBoxAvailable()) {
181182
builder.setIsStrongBoxBacked(true);
182183
Log.d(TAG, "StrongBox is available and enabled for biometric key");
183184
} else {

flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/KeyCipherImplementationRSA18.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import android.os.Build;
66
import android.security.keystore.KeyGenParameterSpec;
77
import android.security.keystore.KeyProperties;
8+
import android.security.keystore.StrongBoxUnavailableException;
9+
10+
import androidx.annotation.RequiresApi;
811

912
import com.it_nomads.fluttersecurestorage.FlutterSecureStorageConfig;
1013

@@ -137,26 +140,38 @@ private void setLocale(Locale locale) {
137140
context.createConfigurationContext(config);
138141
}
139142

143+
private AlgorithmParameterSpec getSpec(boolean isStrongBoxBacked) {
144+
Calendar start = Calendar.getInstance();
145+
Calendar end = Calendar.getInstance();
146+
end.add(Calendar.YEAR, 25);
147+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
148+
return makeAlgorithmParameterSpecLegacy(context, start, end);
149+
}
150+
151+
return makeAlgorithmParameterSpec(context, start, end, isStrongBoxBacked);
152+
}
153+
154+
@RequiresApi(api = Build.VERSION_CODES.P)
140155
private void createKeys(Context context) throws Exception {
141156
final Locale localeBeforeFakingEnglishLocale = Locale.getDefault();
142157
try {
143158
setLocale(Locale.ENGLISH);
144-
Calendar start = Calendar.getInstance();
145-
Calendar end = Calendar.getInstance();
146-
end.add(Calendar.YEAR, 25);
147159

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

150162
AlgorithmParameterSpec spec;
151163

152-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
153-
spec = makeAlgorithmParameterSpecLegacy(context, start, end);
154-
} else {
155-
spec = makeAlgorithmParameterSpec(context, start, end);
156-
}
164+
try {
165+
spec = getSpec(true);
166+
167+
kpGenerator.initialize(spec);
168+
kpGenerator.generateKeyPair();
169+
} catch (StrongBoxUnavailableException e) {
170+
spec = getSpec(false);
157171

158-
kpGenerator.initialize(spec);
159-
kpGenerator.generateKeyPair();
172+
kpGenerator.initialize(spec);
173+
kpGenerator.generateKeyPair();
174+
}
160175
} finally {
161176
setLocale(localeBeforeFakingEnglishLocale);
162177
}
@@ -174,7 +189,8 @@ private AlgorithmParameterSpec makeAlgorithmParameterSpecLegacy(Context context,
174189
.build();
175190
}
176191

177-
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end) {
192+
@RequiresApi(api = Build.VERSION_CODES.M)
193+
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end, boolean isStrongBoxBacked) {
178194
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
179195
.setCertificateSubject(new X500Principal("CN=" + keyAlias))
180196
.setDigests(KeyProperties.DIGEST_SHA256)
@@ -183,6 +199,9 @@ protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Cal
183199
.setCertificateSerialNumber(BigInteger.valueOf(1))
184200
.setCertificateNotBefore(start.getTime())
185201
.setCertificateNotAfter(end.getTime());
202+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
203+
builder.setIsStrongBoxBacked(true);
204+
}
186205
return builder.build();
187206
}
188207
}

flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/KeyCipherImplementationRSAOAEP.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected String createKeyAlias() {
3232

3333
@RequiresApi(api = Build.VERSION_CODES.M)
3434
@Override
35-
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end) {
35+
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end, boolean isStrongBoxBacked) {
3636
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
3737
.setCertificateSubject(new X500Principal("CN=" + keyAlias))
3838
.setDigests(KeyProperties.DIGEST_SHA256)
@@ -41,6 +41,9 @@ protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Cal
4141
.setCertificateSerialNumber(BigInteger.valueOf(1))
4242
.setCertificateNotBefore(start.getTime())
4343
.setCertificateNotAfter(end.getTime());
44+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
45+
builder.setIsStrongBoxBacked(true);
46+
}
4447
return builder.build();
4548
}
4649

flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/crypto/MasterKey.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,8 @@ public Builder setKeyGenParameterSpec(@NonNull KeyGenParameterSpec keyGenParamet
264264
* @return The master key.
265265
*/
266266
@NonNull
267-
public MasterKey build() throws GeneralSecurityException, IOException {
268-
return Api23Impl.build(this);
267+
public MasterKey build(boolean isStrongBoxBacked) throws GeneralSecurityException, IOException {
268+
return Api23Impl.build(this, isStrongBoxBacked);
269269
}
270270

271271
static class Api23Impl {
@@ -277,7 +277,7 @@ static String getKeystoreAlias(KeyGenParameterSpec keyGenParameterSpec) {
277277
return keyGenParameterSpec.getKeystoreAlias();
278278
}
279279
@SuppressWarnings("deprecation")
280-
static MasterKey build(Builder builder) throws GeneralSecurityException, IOException {
280+
static MasterKey build(Builder builder, boolean isStrongBoxBacked) throws GeneralSecurityException, IOException {
281281
if (builder.mKeyScheme == null && builder.mKeyGenParameterSpec == null) {
282282
throw new IllegalArgumentException("build() called before "
283283
+ "setKeyGenParameterSpec or setKeyScheme.");
@@ -289,6 +289,9 @@ static MasterKey build(Builder builder) throws GeneralSecurityException, IOExcep
289289
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
290290
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
291291
.setKeySize(DEFAULT_AES_GCM_MASTER_KEY_SIZE);
292+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
293+
keyGenBuilder.setIsStrongBoxBacked(true);
294+
}
292295
if (builder.mAuthenticationRequired) {
293296
keyGenBuilder.setUserAuthenticationRequired(true);
294297
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

flutter_secure_storage/lib/flutter_secure_storage.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,19 @@ class FlutterSecureStorage {
344344
});
345345
}
346346

347+
/// [aOptions] optional Android options
348+
Future<bool> isStrongBoxSupported({
349+
AndroidOptions? aOptions,
350+
}) async {
351+
if (defaultTargetPlatform == TargetPlatform.android) {
352+
return _platform.isStrongBoxSupported(
353+
options: aOptions?.params ?? this.aOptions.params,
354+
);
355+
} else {
356+
throw UnsupportedError(_unsupportedPlatform);
357+
}
358+
}
359+
347360
/// Select correct options based on current platform
348361
Map<String, String> _selectOptions(
349362
AppleOptions? iOptions,

flutter_secure_storage/lib/options/android_options.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class AndroidOptions extends Options {
5151
bool resetOnError = true,
5252
bool migrateOnAlgorithmChange = true,
5353
bool enforceBiometrics = false,
54+
bool onlyAllowStrongBox = false,
5455
KeyCipherAlgorithm keyCipherAlgorithm =
5556
KeyCipherAlgorithm.RSA_ECB_OAEPwithSHA_256andMGF1Padding,
5657
StorageCipherAlgorithm storageCipherAlgorithm =
@@ -63,6 +64,7 @@ class AndroidOptions extends Options {
6364
_resetOnError = resetOnError,
6465
_migrateOnAlgorithmChange = migrateOnAlgorithmChange,
6566
_enforceBiometrics = enforceBiometrics,
67+
_onlyAllowStrongBox = onlyAllowStrongBox,
6668
_keyCipherAlgorithm = keyCipherAlgorithm,
6769
_storageCipherAlgorithm = storageCipherAlgorithm;
6870

@@ -83,6 +85,7 @@ class AndroidOptions extends Options {
8385
bool resetOnError = true,
8486
bool migrateOnAlgorithmChange = true,
8587
bool enforceBiometrics = false,
88+
bool onlyAllowStrongBox = false,
8689
this.sharedPreferencesName,
8790
this.preferencesKeyPrefix,
8891
this.biometricPromptTitle,
@@ -91,6 +94,7 @@ class AndroidOptions extends Options {
9194
_resetOnError = resetOnError,
9295
_migrateOnAlgorithmChange = migrateOnAlgorithmChange,
9396
_enforceBiometrics = enforceBiometrics,
97+
_onlyAllowStrongBox = onlyAllowStrongBox,
9498
_keyCipherAlgorithm = KeyCipherAlgorithm.AES_GCM_NoPadding,
9599
_storageCipherAlgorithm = StorageCipherAlgorithm.AES_GCM_NoPadding;
96100

@@ -128,6 +132,13 @@ class AndroidOptions extends Options {
128132
/// Defaults to false.
129133
final bool _enforceBiometrics;
130134

135+
/// If true, only allow keys to be stored in StrongBox backed keymaster.
136+
/// This option is only available on API 28 and greater.
137+
/// If set to true some phones might not work.
138+
/// Defaults to false.
139+
/// https://developer.android.com/training/articles/keystore#HardwareSecurityModule
140+
final bool _onlyAllowStrongBox;
141+
131142
/// Algorithm used to encrypt the secret key.
132143
/// By default RSA/ECB/OAEPWithSHA-256AndMGF1Padding is used (API 23+).
133144
/// Legacy RSA/ECB/PKCS1Padding is available for backwards compatibility.
@@ -171,6 +182,7 @@ class AndroidOptions extends Options {
171182
'resetOnError': '$_resetOnError',
172183
'migrateOnAlgorithmChange': '$_migrateOnAlgorithmChange',
173184
'enforceBiometrics': '$_enforceBiometrics',
185+
'onlyAllowStrongBox': '$_onlyAllowStrongBox',
174186
'keyCipherAlgorithm': _keyCipherAlgorithm.name,
175187
'storageCipherAlgorithm': _storageCipherAlgorithm.name,
176188
'sharedPreferencesName': sharedPreferencesName ?? '',
@@ -187,6 +199,7 @@ class AndroidOptions extends Options {
187199
bool? resetOnError,
188200
bool? migrateOnAlgorithmChange,
189201
bool? enforceBiometrics,
202+
bool? onlyAllowStrongBox,
190203
KeyCipherAlgorithm? keyCipherAlgorithm,
191204
StorageCipherAlgorithm? storageCipherAlgorithm,
192205
String? preferencesKeyPrefix,
@@ -203,6 +216,7 @@ class AndroidOptions extends Options {
203216
migrateOnAlgorithmChange:
204217
migrateOnAlgorithmChange ?? _migrateOnAlgorithmChange,
205218
enforceBiometrics: enforceBiometrics ?? _enforceBiometrics,
219+
onlyAllowStrongBox: onlyAllowStrongBox ?? _onlyAllowStrongBox,
206220
keyCipherAlgorithm: keyCipherAlgorithm ?? _keyCipherAlgorithm,
207221
storageCipherAlgorithm:
208222
storageCipherAlgorithm ?? _storageCipherAlgorithm,

0 commit comments

Comments
 (0)