Skip to content

Commit 77934bb

Browse files
authored
Merge pull request #1 from QuickBirdEng/strongbox-enabled
Enable StrongBox by default on Android with a fallback
2 parents 56d75d5 + f3db17c commit 77934bb

File tree

17 files changed

+213
-38
lines changed

17 files changed

+213
-38
lines changed

flutter_secure_storage/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 10.0.1
2+
* Enabled StrongBox by default, use fallback if it's not available.
3+
* [Android] Allow to force StrongBox with a flag (onlyAllowStrongBox)
4+
* [Android] Method to check if an Android device supports Strongbox
5+
6+
# Before fork
7+
18
## 10.0.0-beta.4
29
* [Apple] Merged ios and macos implementation into a new package flutter_secure_storage_darwin
310
* [Apple] Refactored code and added missing options

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

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import android.content.Context;
44
import android.content.SharedPreferences;
5+
import android.os.Build;
56
import android.security.keystore.KeyGenParameterSpec;
67
import android.security.keystore.KeyProperties;
8+
import android.security.keystore.StrongBoxUnavailableException;
79
import android.util.Base64;
810
import android.util.Log;
911

@@ -31,6 +33,7 @@ public class FlutterSecureStorage {
3133
private static final String PREF_OPTION_PREFIX = "preferencesKeyPrefix";
3234
private static final String PREF_OPTION_DELETE_ON_FAILURE = "resetOnError";
3335
private static final String PREF_KEY_MIGRATED = "preferencesMigrated";
36+
private static final String PREF_OPTION_ONLY_ALLOW_STRONGBOX = "onlyAllowStrongBox";
3437
@NonNull
3538
private final SharedPreferences encryptedPreferences;
3639
@NonNull
@@ -61,7 +64,15 @@ public FlutterSecureStorage(Context context, Map<String, Object> options) throws
6164
}
6265
}
6366

64-
encryptedPreferences = getEncryptedSharedPreferences(deleteOnFailure, options, context.getApplicationContext(), sharedPreferencesName);
67+
boolean onlyAllowStrongbox = false;
68+
if (options.containsKey(PREF_OPTION_ONLY_ALLOW_STRONGBOX)) {
69+
var value = options.get(PREF_OPTION_ONLY_ALLOW_STRONGBOX);
70+
if (value instanceof String) {
71+
onlyAllowStrongbox = Boolean.parseBoolean((String) value);
72+
}
73+
}
74+
75+
encryptedPreferences = getEncryptedSharedPreferences(deleteOnFailure, options, context.getApplicationContext(), sharedPreferencesName, true, onlyAllowStrongbox);
6576
}
6677

6778
public boolean containsKey(String key) {
@@ -83,6 +94,7 @@ public void delete(String key) {
8394
public void deleteAll() {
8495
encryptedPreferences.edit().clear().apply();
8596
}
97+
8698

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

105-
private SharedPreferences getEncryptedSharedPreferences(boolean deleteOnFailure, Map<String, Object> options, Context context, String sharedPreferencesName) throws GeneralSecurityException, IOException {
117+
private SharedPreferences getEncryptedSharedPreferences(boolean deleteOnFailure, Map<String, Object> options, Context context, String sharedPreferencesName, boolean isStrongBoxBacked, boolean isOnlyStrongBoxAllowed) throws GeneralSecurityException, IOException {
106118
try {
107-
final SharedPreferences encryptedPreferences = initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName);
119+
final SharedPreferences encryptedPreferences = initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName, isStrongBoxBacked);
108120
boolean migrated = encryptedPreferences.getBoolean(PREF_KEY_MIGRATED, false);
109121
if (!migrated) {
110122
migrateToEncryptedPreferences(context, sharedPreferencesName, encryptedPreferences, deleteOnFailure, options);
111123
}
112124
return encryptedPreferences;
113125
} catch (GeneralSecurityException | IOException e) {
126+
if (e instanceof GeneralSecurityException) {
127+
Throwable cause = e.getCause();
128+
129+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
130+
if (cause instanceof StrongBoxUnavailableException && !isOnlyStrongBoxAllowed) {
131+
// Fallback to not using Strongbox
132+
return getEncryptedSharedPreferences(deleteOnFailure, options, context, sharedPreferencesName, false, isOnlyStrongBoxAllowed);
133+
}
134+
}
135+
}
114136

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

123145
try {
124-
return initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName);
146+
return initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName, isStrongBoxBacked);
125147
} catch (Exception f) {
126148
Log.e(TAG, "initialization after reset failed", f);
127149
throw f;
128150
}
129151
}
130152
}
131153

132-
private SharedPreferences initializeEncryptedSharedPreferencesManager(Context context, String sharedPreferencesName) throws GeneralSecurityException, IOException {
154+
private SharedPreferences initializeEncryptedSharedPreferencesManager(Context context, String sharedPreferencesName, boolean isStrongBoxBacked) throws GeneralSecurityException, IOException {
155+
KeyGenParameterSpec.Builder keyGenBuilder = new KeyGenParameterSpec.Builder(
156+
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
157+
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
158+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
159+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
160+
.setKeySize(256);
161+
162+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
163+
keyGenBuilder.setIsStrongBoxBacked(true);
164+
}
165+
133166
MasterKey masterKey = new MasterKey.Builder(context)
134-
.setKeyGenParameterSpec(new KeyGenParameterSpec.Builder(
135-
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
136-
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
137-
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
138-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
139-
.setKeySize(256)
140-
.build())
141-
.build();
167+
.setKeyGenParameterSpec(keyGenBuilder.build())
168+
.build(isStrongBoxBacked);
142169

143170
return EncryptedSharedPreferences.create(
144171
context,

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

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

3+
import android.content.pm.PackageManager;
34
import android.os.Handler;
45
import android.os.HandlerThread;
56
import android.os.Looper;
@@ -24,6 +25,7 @@ public class FlutterSecureStoragePlugin implements MethodCallHandler, FlutterPlu
2425
private HandlerThread workerThread;
2526
private Handler workerThreadHandler;
2627
private FlutterPluginBinding binding;
28+
private boolean isStrongBoxAvailable;
2729

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

5456
try {
57+
isStrongBoxAvailable = binding.getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
5558
secureStorage = new FlutterSecureStorage(binding.getApplicationContext(), options);
5659
return true;
5760
} catch (Exception e) {
@@ -123,6 +126,9 @@ private void handleMethodCall(MethodCall call, Result result) {
123126
case "deleteAll":
124127
handleDeleteAll(result);
125128
break;
129+
case "isStrongBoxSupported":
130+
handleStrongBoxAvailable(result);
131+
break;
126132
default:
127133
result.notImplemented();
128134
}
@@ -164,6 +170,10 @@ private void handleDeleteAll(Result result) {
164170
result.success(null);
165171
}
166172

173+
private void handleStrongBoxAvailable(Result result) {
174+
result.success(isStrongBoxAvailable);
175+
}
176+
167177
@SuppressWarnings("unchecked")
168178
private Map<String, Object> extractMapFromObject(Object object) {
169179
if (!(object instanceof Map)) {

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.os.Build;
66
import android.security.keystore.KeyGenParameterSpec;
77
import android.security.keystore.KeyProperties;
8+
import android.security.keystore.StrongBoxUnavailableException;
89

910
import androidx.annotation.RequiresApi;
1011

@@ -123,26 +124,38 @@ private void setLocale(Locale locale) {
123124
context.createConfigurationContext(config);
124125
}
125126

127+
private AlgorithmParameterSpec getSpec(boolean isStrongBoxBacked) {
128+
Calendar start = Calendar.getInstance();
129+
Calendar end = Calendar.getInstance();
130+
end.add(Calendar.YEAR, 25);
131+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
132+
return makeAlgorithmParameterSpecLegacy(context, start, end);
133+
}
134+
135+
return makeAlgorithmParameterSpec(context, start, end, isStrongBoxBacked);
136+
}
137+
138+
@RequiresApi(api = Build.VERSION_CODES.P)
126139
private void createKeys(Context context) throws Exception {
127140
final Locale localeBeforeFakingEnglishLocale = Locale.getDefault();
128141
try {
129142
setLocale(Locale.ENGLISH);
130-
Calendar start = Calendar.getInstance();
131-
Calendar end = Calendar.getInstance();
132-
end.add(Calendar.YEAR, 25);
133143

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

136146
AlgorithmParameterSpec spec;
137147

138-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
139-
spec = makeAlgorithmParameterSpecLegacy(context, start, end);
140-
} else {
141-
spec = makeAlgorithmParameterSpec(context, start, end);
142-
}
148+
try {
149+
spec = getSpec(true);
150+
151+
kpGenerator.initialize(spec);
152+
kpGenerator.generateKeyPair();
153+
} catch (StrongBoxUnavailableException e) {
154+
spec = getSpec(false);
143155

144-
kpGenerator.initialize(spec);
145-
kpGenerator.generateKeyPair();
156+
kpGenerator.initialize(spec);
157+
kpGenerator.generateKeyPair();
158+
}
146159
} finally {
147160
setLocale(localeBeforeFakingEnglishLocale);
148161
}
@@ -161,7 +174,7 @@ private AlgorithmParameterSpec makeAlgorithmParameterSpecLegacy(Context context,
161174
}
162175

163176
@RequiresApi(api = Build.VERSION_CODES.M)
164-
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end) {
177+
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end, boolean isStrongBoxBacked) {
165178
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
166179
.setCertificateSubject(new X500Principal("CN=" + keyAlias))
167180
.setDigests(KeyProperties.DIGEST_SHA256)
@@ -170,6 +183,9 @@ protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Cal
170183
.setCertificateSerialNumber(BigInteger.valueOf(1))
171184
.setCertificateNotBefore(start.getTime())
172185
.setCertificateNotAfter(end.getTime());
186+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
187+
builder.setIsStrongBoxBacked(true);
188+
}
173189
return builder.build();
174190
}
175191
}

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

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

3131
@RequiresApi(api = Build.VERSION_CODES.M)
3232
@Override
33-
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end) {
33+
protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Calendar start, Calendar end, boolean isStrongBoxBacked) {
3434
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
3535
.setCertificateSubject(new X500Principal("CN=" + keyAlias))
3636
.setDigests(KeyProperties.DIGEST_SHA256)
@@ -39,6 +39,9 @@ protected AlgorithmParameterSpec makeAlgorithmParameterSpec(Context context, Cal
3939
.setCertificateSerialNumber(BigInteger.valueOf(1))
4040
.setCertificateNotBefore(start.getTime())
4141
.setCertificateNotAfter(end.getTime());
42+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isStrongBoxBacked) {
43+
builder.setIsStrongBoxBacked(true);
44+
}
4245
return builder.build();
4346
}
4447

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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ class AndroidOptions extends Options {
2727
StorageCipherAlgorithm.AES_CBC_PKCS7Padding,
2828
this.sharedPreferencesName,
2929
this.preferencesKeyPrefix,
30+
bool onlyAllowStrongBox = false,
3031
}) : _encryptedSharedPreferences = encryptedSharedPreferences,
3132
_resetOnError = resetOnError,
3233
_keyCipherAlgorithm = keyCipherAlgorithm,
33-
_storageCipherAlgorithm = storageCipherAlgorithm;
34+
_storageCipherAlgorithm = storageCipherAlgorithm,
35+
_onlyAllowStrongBox = onlyAllowStrongBox;
3436

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

75+
/// If true, only allow keys to be stored in StrongBox backed keymaster.
76+
/// This option is only available on API 28 and greater. If set to true some phones might now work
77+
/// Defaults to false.
78+
/// https://developer.android.com/training/articles/keystore#HardwareSecurityModule
79+
final bool _onlyAllowStrongBox;
80+
7381
static const AndroidOptions defaultOptions = AndroidOptions();
7482

7583
@override
@@ -80,6 +88,7 @@ class AndroidOptions extends Options {
8088
'storageCipherAlgorithm': _storageCipherAlgorithm.name,
8189
'sharedPreferencesName': sharedPreferencesName ?? '',
8290
'preferencesKeyPrefix': preferencesKeyPrefix ?? '',
91+
'onlyAllowStrongBox': '$_onlyAllowStrongBox',
8392
};
8493

8594
AndroidOptions copyWith({
@@ -89,6 +98,7 @@ class AndroidOptions extends Options {
8998
StorageCipherAlgorithm? storageCipherAlgorithm,
9099
String? preferencesKeyPrefix,
91100
String? sharedPreferencesName,
101+
bool? onlyAllowStrongBox,
92102
}) =>
93103
AndroidOptions(
94104
encryptedSharedPreferences:
@@ -99,5 +109,6 @@ class AndroidOptions extends Options {
99109
storageCipherAlgorithm ?? _storageCipherAlgorithm,
100110
sharedPreferencesName: sharedPreferencesName,
101111
preferencesKeyPrefix: preferencesKeyPrefix,
112+
onlyAllowStrongBox: onlyAllowStrongBox ?? _onlyAllowStrongBox,
102113
);
103114
}

flutter_secure_storage/lib/test/test_flutter_secure_storage_platform.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,10 @@ class TestFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
5252
required Map<String, String> options,
5353
}) async =>
5454
data[key] = value;
55+
56+
@override
57+
Future<bool> isStrongBoxSupported({
58+
required Map<String, String> options,
59+
}) async =>
60+
true;
5561
}

0 commit comments

Comments
 (0)