diff --git a/flutter_secure_storage/android/src/main/AndroidManifest.xml b/flutter_secure_storage/android/src/main/AndroidManifest.xml
index 64a69a4b..d1612d7a 100644
--- a/flutter_secure_storage/android/src/main/AndroidManifest.xml
+++ b/flutter_secure_storage/android/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
-
+
diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java
index da7183a6..18940707 100644
--- a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java
+++ b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorage.java
@@ -2,12 +2,18 @@
import android.content.Context;
import android.content.SharedPreferences;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Build;
+import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
+import android.security.keystore.UserNotAuthenticatedException;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import com.it_nomads.fluttersecurestorage.ciphers.StorageCipher;
import com.it_nomads.fluttersecurestorage.ciphers.StorageCipherFactory;
@@ -18,50 +24,69 @@
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
+import java.security.KeyStoreException;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
public class FlutterSecureStorage {
private static final String TAG = "FlutterSecureStorage";
private static final Charset CHARSET = StandardCharsets.UTF_8;
- private static final String DEFAULT_PREF_NAME = "FlutterSecureStorage";
- private static final String DEFAULT_KEY_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIHNlY3VyZSBzdG9yYWdlCg";
- private static final String PREF_OPTION_NAME = "sharedPreferencesName";
- 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";
+
@NonNull
- private final SharedPreferences encryptedPreferences;
+ private final FlutterSecureStorageConfig config;
@NonNull
- private String preferencesKeyPrefix = DEFAULT_KEY_PREFIX;
-
- public FlutterSecureStorage(Context context, Map options) throws GeneralSecurityException, IOException {
- String sharedPreferencesName = DEFAULT_PREF_NAME;
- if (options.containsKey(PREF_OPTION_NAME)) {
- var value = options.get(PREF_OPTION_NAME);
- if (value instanceof String && !((String) value).isEmpty()) {
- sharedPreferencesName = (String) value;
- }
- }
+ private final Context context;
- if (options.containsKey(PREF_OPTION_PREFIX)) {
- var value = options.get(PREF_OPTION_PREFIX);
- if (value instanceof String && !((String) value).isEmpty()) {
- preferencesKeyPrefix = (String) value;
- }
- }
+ private SharedPreferences encryptedPreferences;
- boolean deleteOnFailure = false;
+ private FlutterSecureStorage(@NonNull Context context, Map options) {
+ this.context = context;
+ this.config = new FlutterSecureStorageConfig(options);
+ }
- if (options.containsKey(PREF_OPTION_DELETE_ON_FAILURE)) {
- var value = options.get(PREF_OPTION_DELETE_ON_FAILURE);
- if (value instanceof String) {
- deleteOnFailure = Boolean.parseBoolean((String) value);
- }
- }
+ public static void create(@NonNull Context context, Map options, SecureStorageInitCallback callback) {
+ FlutterSecureStorage storage = new FlutterSecureStorage(context, options);
+
+ storage.authenticateIfNeeded(() -> {
+ try {
+ SharedPreferences encryptedPreferences = storage.initializeEncryptedSharedPreferencesManager(storage.config.getSharedPreferencesName());
+ boolean migrated = encryptedPreferences.getBoolean(PREF_KEY_MIGRATED, false);
+ if (!migrated) {
+ storage.migrateToEncryptedPreferences(storage.config.getSharedPreferencesName(), encryptedPreferences, storage.config.shouldDeleteOnFailure(), options);
+ }
+
+ Log.d(TAG, "Encrypted preferences initialized successfully.");
+ storage.encryptedPreferences = encryptedPreferences;
+ callback.onComplete(storage, null); // Initialization successful
+
+ } catch (KeyStoreException | UserNotAuthenticatedException e) {
+ Log.w(TAG, "Authentication failed: Unable to access secure keystore. Check biometric settings.", e);
+ callback.onComplete(null, e);
+
+ } catch (GeneralSecurityException | IOException e) {
+ if (!storage.config.shouldDeleteOnFailure()) {
+ Log.w(TAG, "Initialization failed: Secure storage could not be initialized. 'deleteOnFailure' is false, skipping reset.", e);
+ callback.onComplete(null, e);
+ } else {
+ Log.w(TAG, "Initialization failed: Resetting storage as 'deleteOnFailure' is enabled.", e);
+ context.getSharedPreferences(storage.config.getSharedPreferencesName(), Context.MODE_PRIVATE).edit().clear().apply();
- encryptedPreferences = getEncryptedSharedPreferences(deleteOnFailure, options, context.getApplicationContext(), sharedPreferencesName);
+ try {
+ SharedPreferences encryptedPreferences = storage.initializeEncryptedSharedPreferencesManager(storage.config.getSharedPreferencesName());
+ Log.i(TAG, "Secure storage successfully re-initialized after reset.");
+ storage.encryptedPreferences = encryptedPreferences;
+ callback.onComplete(storage, null); // Re-initialization successful
+ } catch (Exception f) {
+ Log.e(TAG, "Critical failure: Initialization after reset failed.", f);
+ callback.onComplete(null, f);
+ }
+ }
+ }
+ });
}
public boolean containsKey(String key) {
@@ -90,55 +115,66 @@ public Map readAll() {
for (Map.Entry entry : allEntries.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
- if (key.startsWith(preferencesKeyPrefix) && value instanceof String) {
- String originalKey = key.replaceFirst(preferencesKeyPrefix + "_", "");
+ if (key.startsWith(config.getSharedPreferencesKeyPrefix()) && value instanceof String) {
+ String originalKey = key.replaceFirst(config.getSharedPreferencesKeyPrefix() + "_", "");
result.put(originalKey, (String) value);
}
}
return result;
}
- private String addPrefixToKey(String key) {
- return preferencesKeyPrefix + "_" + key;
+ private void authenticateIfNeeded(Runnable onSuccess) {
+ if (config.shouldUseBiometrics()) {
+ authenticateUser(onSuccess);
+ } else {
+ onSuccess.run();
+ }
}
- private SharedPreferences getEncryptedSharedPreferences(boolean deleteOnFailure, Map options, Context context, String sharedPreferencesName) throws GeneralSecurityException, IOException {
- try {
- final SharedPreferences encryptedPreferences = initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName);
- boolean migrated = encryptedPreferences.getBoolean(PREF_KEY_MIGRATED, false);
- if (!migrated) {
- migrateToEncryptedPreferences(context, sharedPreferencesName, encryptedPreferences, deleteOnFailure, options);
- }
- return encryptedPreferences;
- } catch (GeneralSecurityException | IOException e) {
-
- if (!deleteOnFailure) {
- Log.w(TAG, "initialization failed, resetOnError false, so throwing exception.", e);
- throw e;
- }
- Log.w(TAG, "initialization failed, resetting storage", e);
-
- context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE).edit().clear().apply();
+ private String addPrefixToKey(String key) {
+ return config.getSharedPreferencesKeyPrefix() + "_" + key;
+ }
- try {
- return initializeEncryptedSharedPreferencesManager(context, sharedPreferencesName);
- } catch (Exception f) {
- Log.e(TAG, "initialization after reset failed", f);
- throw f;
- }
- }
+ @RequiresApi(api = Build.VERSION_CODES.R)
+ private MasterKey createSecretKeyApi30() throws GeneralSecurityException, IOException {
+ return new MasterKey.Builder(context)
+ .setUserAuthenticationRequired(config.shouldUseBiometrics(), 2)
+ .setRequestStrongBoxBacked(true)
+ .setKeyGenParameterSpec(new KeyGenParameterSpec.Builder(
+ MasterKey.DEFAULT_MASTER_KEY_ALIAS,
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setIsStrongBoxBacked(true)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setUserAuthenticationRequired(config.shouldUseBiometrics()) // Enforce user authentication
+ .setUserAuthenticationValidityDurationSeconds(2)
+ .setInvalidatedByBiometricEnrollment(true)
+ .setKeySize(256)
+ .build())
+ .build();
}
- private SharedPreferences initializeEncryptedSharedPreferencesManager(Context context, String sharedPreferencesName) throws GeneralSecurityException, IOException {
- MasterKey masterKey = new MasterKey.Builder(context)
+ private MasterKey createSecretKeyApi2329() throws GeneralSecurityException, IOException {
+ return 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)
+ .setUserAuthenticationRequired(config.shouldUseBiometrics()) // Enforce user authentication
+ .setUserAuthenticationValidityDurationSeconds(1)
.setKeySize(256)
.build())
.build();
+ }
+ private SharedPreferences initializeEncryptedSharedPreferencesManager(String sharedPreferencesName) throws GeneralSecurityException, IOException {
+ MasterKey masterKey;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ masterKey = createSecretKeyApi30();
+ } else {
+ masterKey = createSecretKeyApi2329();
+ }
return EncryptedSharedPreferences.create(
context,
@@ -149,7 +185,7 @@ private SharedPreferences initializeEncryptedSharedPreferencesManager(Context co
);
}
- private void migrateToEncryptedPreferences(Context context, String sharedPreferencesName, SharedPreferences target, boolean deleteOnFailure, Map options) {
+ private void migrateToEncryptedPreferences(String sharedPreferencesName, SharedPreferences target, boolean deleteOnFailure, Map options) {
SharedPreferences source = context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
Map sourceEntries = source.getAll();
@@ -164,7 +200,7 @@ private void migrateToEncryptedPreferences(Context context, String sharedPrefere
for (Map.Entry entry : sourceEntries.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
- if (key.startsWith(preferencesKeyPrefix) && value instanceof String) {
+ if (key.startsWith(config.getSharedPreferencesKeyPrefix()) && value instanceof String) {
try {
String decryptedValue = decryptValue((String) value, cipher);
target.edit().putString(key, decryptedValue).apply();
@@ -208,4 +244,51 @@ private String decryptValue(String value, StorageCipher cipher) throws Exception
byte[] data = Base64.decode(value, Base64.DEFAULT);
return new String(cipher.decrypt(data), CHARSET);
}
+
+ private void authenticateUser(Runnable onSuccess) {
+ BiometricPrompt promptInfo = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ promptInfo = new BiometricPrompt.Builder(context)
+ .setTitle(config.getBiometricPromptTitle())
+ .setSubtitle(config.getPrefOptionBiometricPromptSubtitle())
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL)
+ .build();
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ promptInfo = new BiometricPrompt.Builder(context)
+ .setTitle(config.getBiometricPromptTitle())
+ .setSubtitle(config.getPrefOptionBiometricPromptSubtitle())
+ .build();
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ CancellationSignal cancellationSignal = new CancellationSignal();
+ Executor executor = Executors.newSingleThreadExecutor();
+
+ BiometricPrompt.AuthenticationCallback callback = new BiometricPrompt.AuthenticationCallback() {
+ @Override
+ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
+ super.onAuthenticationSucceeded(result);
+ Log.d(TAG, "Authentication Succeeded!");
+ onSuccess.run();
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ super.onAuthenticationFailed();
+ Log.d(TAG, "Authentication Failed. Try again.");
+ }
+
+ @Override
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
+ super.onAuthenticationError(errorCode, errString);
+ Log.e(TAG, "Authentication Error: " + errString);
+ }
+ };
+
+ promptInfo.authenticate(cancellationSignal, executor, callback);
+ } else {
+ onSuccess.run(); // Proceed without authentication on unsupported devices
+ }
+ }
+
}
diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorageConfig.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorageConfig.java
new file mode 100644
index 00000000..acde167f
--- /dev/null
+++ b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStorageConfig.java
@@ -0,0 +1,77 @@
+package com.it_nomads.fluttersecurestorage;
+
+import androidx.annotation.NonNull;
+
+import java.util.Map;
+
+public class FlutterSecureStorageConfig {
+
+ private static final String DEFAULT_PREF_NAME = "FlutterSecureStorage";
+ private static final String DEFAULT_KEY_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIHNlY3VyZSBzdG9yYWdlCg";
+
+ public static final String PREF_OPTION_NAME = "sharedPreferencesName";
+ public static final String PREF_OPTION_PREFIX = "preferencesKeyPrefix";
+ public static final String PREF_OPTION_DELETE_ON_FAILURE = "resetOnError";
+ public static final String PREF_OPTION_SHOULD_USE_BIOMETRICS = "shouldUseBiometrics";
+
+ public static final String PREF_OPTION_BIOMETRIC_PROMPT_TITLE = "prefOptionBiometricPromptTitle";
+ public static final String PREF_OPTION_BIOMETRIC_PROMPT_SUBTITLE = "prefOptionBiometricPromptSubtitle";
+
+ private final String sharedPreferencesName;
+ private final String sharedPreferencesKeyPrefix;
+ private final boolean deleteOnFailure;
+ private final boolean shouldUseBiometrics;
+
+ private final String biometricPromptTitle;
+ private final String biometricPromptSubtitle;
+
+ public FlutterSecureStorageConfig(Map options) {
+ this.sharedPreferencesName = getStringOption(options, PREF_OPTION_NAME, DEFAULT_PREF_NAME);
+ this.sharedPreferencesKeyPrefix = getStringOption(options, PREF_OPTION_PREFIX, DEFAULT_KEY_PREFIX);
+ this.deleteOnFailure = getBooleanOption(options, PREF_OPTION_DELETE_ON_FAILURE, false);
+ this.biometricPromptTitle = getStringOption(options, PREF_OPTION_BIOMETRIC_PROMPT_TITLE, "Authenticate to access");
+ this.biometricPromptSubtitle = getStringOption(options, PREF_OPTION_BIOMETRIC_PROMPT_SUBTITLE, "Use biometrics or device credentials");
+
+ this.shouldUseBiometrics = getBooleanOption(options, PREF_OPTION_SHOULD_USE_BIOMETRICS, false);
+ }
+
+ private String getStringOption(Map options, String key, String defaultValue) {
+ if (options.containsKey(key)) {
+ Object value = options.get(key);
+ if (value instanceof String strValue) {
+ if (!strValue.isEmpty()) {
+ return strValue;
+ }
+ }
+ }
+ return defaultValue;
+ }
+
+ private boolean getBooleanOption(Map options, String key, boolean defaultValue) {
+ Object value = options.get(key);
+ if (value instanceof String) {
+ return Boolean.parseBoolean((String) value);
+ }
+
+ return defaultValue;
+ }
+
+ public String getSharedPreferencesName() { return sharedPreferencesName; }
+ public String getSharedPreferencesKeyPrefix() { return sharedPreferencesKeyPrefix; }
+ public boolean shouldDeleteOnFailure() { return deleteOnFailure; }
+
+ public boolean shouldUseBiometrics() { return shouldUseBiometrics; }
+
+ public String getBiometricPromptTitle() { return biometricPromptTitle; }
+ public String getPrefOptionBiometricPromptSubtitle() { return biometricPromptSubtitle; }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "FlutterSecureStorageConfig{" +
+ "sharedPreferencesName='" + sharedPreferencesName + '\'' +
+ ", sharedPreferencesKeyPrefix='" + sharedPreferencesKeyPrefix + '\'' +
+ ", deleteOnFailure=" + deleteOnFailure +
+ '}';
+ }
+}
diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java
index bf28a2ee..45b75a03 100644
--- a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java
+++ b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/FlutterSecureStoragePlugin.java
@@ -3,6 +3,7 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
+import android.security.keystore.UserNotAuthenticatedException;
import androidx.annotation.NonNull;
@@ -19,11 +20,11 @@
public class FlutterSecureStoragePlugin implements MethodCallHandler, FlutterPlugin {
+ private FlutterPluginBinding binding;
private MethodChannel channel;
private FlutterSecureStorage secureStorage;
private HandlerThread workerThread;
private Handler workerThreadHandler;
- private FlutterPluginBinding binding;
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
@@ -48,22 +49,20 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
secureStorage = null;
}
- private boolean initSecureStorage(Result result, Map options) {
- if (secureStorage != null) return true;
-
- try {
- secureStorage = new FlutterSecureStorage(binding.getApplicationContext(), options);
- return true;
- } catch (Exception e) {
- if (result != null) {
- result.error(
- "RESET_FAILED", // Error code
- "Failed to reset and initialize encrypted preferences", // Error message
- e.toString() // Details (stack trace or additional info)
- );
- }
- return false;
+ public void initSecureStorage(Map options, SecureStorageInitCallback callback) {
+ if (secureStorage != null) {
+ callback.onComplete(secureStorage, null);
+ return;
}
+
+ FlutterSecureStorage.create(binding.getApplicationContext(), options, (initializedStorage, exception) -> {
+ if (initializedStorage != null) {
+ secureStorage = initializedStorage;
+ callback.onComplete(secureStorage, null);
+ } else {
+ callback.onComplete(null, exception);
+ }
+ });
}
@Override
@@ -101,31 +100,39 @@ private void handleMethodCall(MethodCall call, Result result) {
Map options = extractMapFromObject(arguments.get("options"));
- boolean isInitialized = initSecureStorage(result, options);
- if (!isInitialized) return;
-
- switch (method) {
- case "write":
- handleWrite(arguments, result);
- break;
- case "read":
- handleRead(arguments, result);
- break;
- case "readAll":
- handleReadAll(result);
- break;
- case "containsKey":
- handleContainsKey(arguments, result);
- break;
- case "delete":
- handleDelete(arguments, result);
- break;
- case "deleteAll":
- handleDeleteAll(result);
- break;
- default:
- result.notImplemented();
- }
+ initSecureStorage(options, (secureStorage, exception) -> {
+ if (secureStorage == null) {
+ String code = "INIT_FAILED";
+ if (exception instanceof UserNotAuthenticatedException) {
+ code = "AUTHENTICATION_FAILED";
+ }
+ result.error(code, exception.getMessage(), null);
+ return;
+ }
+
+ switch (method) {
+ case "write":
+ handleWrite(arguments, result);
+ break;
+ case "read":
+ handleRead(arguments, result);
+ break;
+ case "readAll":
+ handleReadAll(result);
+ break;
+ case "containsKey":
+ handleContainsKey(arguments, result);
+ break;
+ case "delete":
+ handleDelete(arguments, result);
+ break;
+ case "deleteAll":
+ handleDeleteAll(result);
+ break;
+ default:
+ result.notImplemented();
+ }
+ });
}
private void handleWrite(Map args, Result result) {
diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/SecureStorageInitCallback.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/SecureStorageInitCallback.java
new file mode 100644
index 00000000..15d3859e
--- /dev/null
+++ b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/SecureStorageInitCallback.java
@@ -0,0 +1,7 @@
+package com.it_nomads.fluttersecurestorage;
+
+public interface SecureStorageInitCallback {
+ void onComplete(FlutterSecureStorage success, Exception e);
+
+}
+
diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/SharedPreferencesCallback.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/SharedPreferencesCallback.java
new file mode 100644
index 00000000..e33561ed
--- /dev/null
+++ b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/SharedPreferencesCallback.java
@@ -0,0 +1,7 @@
+package com.it_nomads.fluttersecurestorage;
+
+import android.content.SharedPreferences;
+
+public interface SharedPreferencesCallback {
+ void onComplete(SharedPreferences sharedPreferences);
+}
\ No newline at end of file
diff --git a/flutter_secure_storage/example/lib/main.dart b/flutter_secure_storage/example/lib/main.dart
index 1b2285c1..44d97094 100644
--- a/flutter_secure_storage/example/lib/main.dart
+++ b/flutter_secure_storage/example/lib/main.dart
@@ -2,13 +2,14 @@ import 'dart:math' show Random;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void main() {
runApp(const MaterialApp(home: HomePage()));
}
-enum _Actions { deleteAll, isProtectedDataAvailable }
+enum _Actions { deleteAll, isProtectedDataAvailable, readALl }
enum _ItemActions { delete, edit, containsKey, read }
@@ -30,11 +31,33 @@ class HomePageState extends State {
TextEditingController(text: AppleOptions.defaultAccountName);
final List<_SecItem> _items = [];
+ bool useBiometrics = false;
void _initializeFlutterSecureStorage(String accountName) {
_storage = FlutterSecureStorage(
- iOptions: IOSOptions(accountName: accountName),
- mOptions: MacOsOptions(accountName: accountName),
+ aOptions: AndroidOptions(
+ biometricPromptTitle: 'Flutter Secure Storage Example',
+ biometricPromptSubtitle: 'Please unlock to access data.',
+ shouldUseBiometrics: useBiometrics,
+ ),
+ iOptions: IOSOptions(
+ accountName: accountName,
+ synchronizable: true,
+ accessControlFlags: [
+ AccessControlFlag.biometryCurrentSet,
+ AccessControlFlag.devicePasscode,
+ AccessControlFlag.and,
+ ],
+ ),
+ mOptions: MacOsOptions(
+ accountName: accountName,
+ synchronizable: true,
+ accessControlFlags: [
+ AccessControlFlag.biometryCurrentSet,
+ AccessControlFlag.devicePasscode,
+ AccessControlFlag.and,
+ ],
+ ),
);
}
@@ -63,41 +86,91 @@ class HomePageState extends State {
}
Future _readAll() async {
- final all = await _storage.readAll();
- setState(() {
- _items
- ..clear()
- ..addAll(all.entries.map((e) => _SecItem(e.key, e.value)))
- ..sort(
- (a, b) =>
- (int.tryParse(a.key) ?? 10).compareTo(int.tryParse(b.key) ?? 11),
- );
- });
+ try {
+ final all = await _storage.readAll();
+ setState(() {
+ _items
+ ..clear()
+ ..addAll(all.entries.map((e) => _SecItem(e.key, e.value)))
+ ..sort(
+ (a, b) => (int.tryParse(a.key) ?? 10)
+ .compareTo(int.tryParse(b.key) ?? 11),
+ );
+ });
+ } on PlatformException catch (e) {
+ _handleInitializationError(e);
+ }
}
Future _deleteAll() async {
- await _storage.deleteAll();
- await _readAll();
+ try {
+ await _storage.deleteAll();
+ await _readAll();
+ } on PlatformException catch (e) {
+ _handleInitializationError(e);
+ }
}
Future _isProtectedDataAvailable() async {
final scaffold = ScaffoldMessenger.of(context);
- final result = await _storage.isCupertinoProtectedDataAvailable();
+ try {
+ final result = await _storage.isCupertinoProtectedDataAvailable();
- scaffold.showSnackBar(
- SnackBar(
- content: Text('Protected data available: $result'),
- backgroundColor: result != null && result ? Colors.green : Colors.red,
- ),
- );
+ scaffold.showSnackBar(
+ SnackBar(
+ content: Text('Protected data available: $result'),
+ backgroundColor: result != null && result ? Colors.green : Colors.red,
+ ),
+ );
+ } on PlatformException catch (e) {
+ _handleInitializationError(e);
+ }
}
Future _addNewItem() async {
- await _storage.write(
- key: DateTime.timestamp().microsecondsSinceEpoch.toString(),
- value: _randomValue(),
- );
- await _readAll();
+ try {
+ await _storage.write(
+ key: DateTime.timestamp().microsecondsSinceEpoch.toString(),
+ value: _randomValue(),
+ );
+ await _readAll();
+ } on PlatformException catch (e) {
+ _handleInitializationError(e);
+ }
+ }
+
+ void _handleInitializationError(PlatformException e) {
+ switch (e.code) {
+ case 'INIT_FAILED':
+ _showErrorDialog('Initialization Failed',
+ e.message ?? 'An unknown error occurred during initialization.');
+ case 'AUTHENTICATION_FAILED':
+ _showErrorDialog('Authentication Failed',
+ 'Biometric authentication failed. Please try again.');
+ case 'InvalidArgument':
+ _showErrorDialog(
+ 'Argument Error', 'A with an argument occured. ${e.message}');
+ default:
+ _showErrorDialog('Error', 'An unexpected error occurred: ${e.message}');
+ }
+ }
+
+ void _showErrorDialog(String title, String message) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(title),
+ content: Text(message),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+ });
}
@override
@@ -117,6 +190,8 @@ class HomePageState extends State {
switch (action) {
case _Actions.deleteAll:
_deleteAll();
+ case _Actions.readALl:
+ _readAll();
case _Actions.isProtectedDataAvailable:
_isProtectedDataAvailable();
}
@@ -127,11 +202,30 @@ class HomePageState extends State {
value: _Actions.deleteAll,
child: Text('Delete all'),
),
+ const PopupMenuItem(
+ key: Key('read_all'),
+ value: _Actions.readALl,
+ child: Text('Read all'),
+ ),
const PopupMenuItem(
key: Key('is_protected_data_available'),
value: _Actions.isProtectedDataAvailable,
child: Text('IsProtectedDataAvailable'),
),
+ PopupMenuItem(
+ key: const Key('use_biometrics'),
+ child: StatefulBuilder(
+ builder: (_context, _setState) => CheckboxListTile(
+ value: useBiometrics,
+ onChanged: (value) {
+ _setState(() => useBiometrics = !useBiometrics);
+ _initializeFlutterSecureStorage(
+ _accountNameController.text);
+ },
+ title: const Text('Use Biometrics'),
+ ),
+ ),
+ ),
],
),
],
@@ -214,44 +308,48 @@ class HomePageState extends State {
_SecItem item,
BuildContext context,
) async {
- switch (action) {
- case _ItemActions.delete:
- await _storage.delete(
- key: item.key,
- );
- await _readAll();
- case _ItemActions.edit:
- if (!context.mounted) return;
- final result = await showDialog(
- context: context,
- builder: (_) => _EditItemWidget(item.value),
- );
- if (result != null) {
- await _storage.write(
+ try {
+ switch (action) {
+ case _ItemActions.delete:
+ await _storage.delete(
key: item.key,
- value: result,
);
await _readAll();
- }
- case _ItemActions.containsKey:
- final key = await _displayTextInputDialog(context, item.key);
- final result = await _storage.containsKey(key: key);
- if (!context.mounted) return;
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Contains Key: $result, key checked: $key'),
- backgroundColor: result ? Colors.green : Colors.red,
- ),
- );
- case _ItemActions.read:
- final key = await _displayTextInputDialog(context, item.key);
- final result = await _storage.read(key: key);
- if (!context.mounted) return;
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('value: $result'),
- ),
- );
+ case _ItemActions.edit:
+ if (!context.mounted) return;
+ final result = await showDialog(
+ context: context,
+ builder: (_) => _EditItemWidget(item.value),
+ );
+ if (result != null) {
+ await _storage.write(
+ key: item.key,
+ value: result,
+ );
+ await _readAll();
+ }
+ case _ItemActions.containsKey:
+ final key = await _displayTextInputDialog(context, item.key);
+ final result = await _storage.containsKey(key: key);
+ if (!context.mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Contains Key: $result, key checked: $key'),
+ backgroundColor: result ? Colors.green : Colors.red,
+ ),
+ );
+ case _ItemActions.read:
+ final key = await _displayTextInputDialog(context, item.key);
+ final result = await _storage.read(key: key);
+ if (!context.mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('value: $result'),
+ ),
+ );
+ }
+ } on PlatformException catch (e) {
+ _handleInitializationError(e);
}
}
diff --git a/flutter_secure_storage/lib/options/android_options.dart b/flutter_secure_storage/lib/options/android_options.dart
index b6609e52..265d8bf4 100644
--- a/flutter_secure_storage/lib/options/android_options.dart
+++ b/flutter_secure_storage/lib/options/android_options.dart
@@ -27,6 +27,9 @@ class AndroidOptions extends Options {
StorageCipherAlgorithm.AES_CBC_PKCS7Padding,
this.sharedPreferencesName,
this.preferencesKeyPrefix,
+ this.biometricPromptTitle,
+ this.biometricPromptSubtitle,
+ this.shouldUseBiometrics = false,
}) : _encryptedSharedPreferences = encryptedSharedPreferences,
_resetOnError = resetOnError,
_keyCipherAlgorithm = keyCipherAlgorithm,
@@ -70,6 +73,10 @@ class AndroidOptions extends Options {
/// WARNING: If you change this you can't retrieve already saved preferences.
final String? preferencesKeyPrefix;
+ final String? biometricPromptTitle;
+ final String? biometricPromptSubtitle;
+ final bool shouldUseBiometrics;
+
static const AndroidOptions defaultOptions = AndroidOptions();
@override
@@ -80,6 +87,12 @@ class AndroidOptions extends Options {
'storageCipherAlgorithm': _storageCipherAlgorithm.name,
'sharedPreferencesName': sharedPreferencesName ?? '',
'preferencesKeyPrefix': preferencesKeyPrefix ?? '',
+ 'biometricPromptTitle': biometricPromptTitle ??
+ 'Authenticate to access',
+ 'biometricPromptSubtitle': biometricPromptSubtitle ??
+ 'Use biometrics or device credentials',
+ 'shouldUseBiometrics': '$shouldUseBiometrics',
+
};
AndroidOptions copyWith({