From 1a5ea819b97c4b69fb58ea805e277d2f432ad87f Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Fri, 3 Jan 2025 14:21:35 +0100 Subject: [PATCH] imp: refactor plugin and method calls --- .../FlutterSecureStorage.java | 227 ++++++++--------- .../FlutterSecureStoragePlugin.java | 236 +++++++++--------- .../example/android/settings.gradle.kts | 4 +- 3 files changed, 229 insertions(+), 238 deletions(-) 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 eb16a1ec..66577587 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 @@ -7,6 +7,7 @@ import android.util.Base64; import android.util.Log; +import com.it_nomads.fluttersecurestorage.ciphers.StorageCipher; import com.it_nomads.fluttersecurestorage.ciphers.StorageCipherFactory; import com.it_nomads.fluttersecurestorage.crypto.EncryptedSharedPreferences; import com.it_nomads.fluttersecurestorage.crypto.MasterKey; @@ -20,159 +21,161 @@ public class FlutterSecureStorage { - private final String TAG = "SecureStorageAndroid"; - private final Charset charset = StandardCharsets.UTF_8; + private static final String TAG = "SecureStorageAndroid"; + 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 final Context applicationContext; - protected String ELEMENT_PREFERENCES_KEY_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIHNlY3VyZSBzdG9yYWdlCg"; - protected Map options; - private String SHARED_PREFERENCES_NAME = "FlutterSecureStorage"; - private SharedPreferences preferences; + private final Map options; + + private String sharedPreferencesName = DEFAULT_PREF_NAME; + private String preferencesKeyPrefix = DEFAULT_KEY_PREFIX; + private SharedPreferences encryptedPreferences; public FlutterSecureStorage(Context context, Map options) { + this.applicationContext = context.getApplicationContext(); this.options = options; - applicationContext = context.getApplicationContext(); - } - - boolean getResetOnError() { - Object value = options.containsKey("resetOnError") ? options.get("resetOnError") : "false"; - return String.valueOf(value).equals("true"); + ensureOptions(); + getEncryptedSharedPreferences(); } public boolean containsKey(String key) { - ensureInitialized(); - return preferences.contains(key); - } - - public String addPrefixToKey(String key) { - return ELEMENT_PREFERENCES_KEY_PREFIX + "_" + key; + SharedPreferences preferences = getEncryptedSharedPreferences(); + return preferences != null && preferences.contains(addPrefixToKey(key)); } public String read(String key) { - ensureInitialized(); - - return preferences.getString(key, null); - } - - @SuppressWarnings("unchecked") - public Map readAll() { - ensureInitialized(); - - Map raw = (Map) preferences.getAll(); - - Map all = new HashMap<>(); - for (Map.Entry entry : raw.entrySet()) { - String keyWithPrefix = entry.getKey(); - if (keyWithPrefix.contains(ELEMENT_PREFERENCES_KEY_PREFIX)) { - String key = entry.getKey().replaceFirst(ELEMENT_PREFERENCES_KEY_PREFIX + '_', ""); - all.put(key, entry.getValue()); - } - } - return all; + SharedPreferences preferences = getEncryptedSharedPreferences(); + return preferences != null ? preferences.getString(addPrefixToKey(key), null) : null; } public void write(String key, String value) { - ensureInitialized(); - - SharedPreferences.Editor editor = preferences.edit(); - - editor.putString(key, value); - editor.apply(); + SharedPreferences preferences = getEncryptedSharedPreferences(); + if (preferences != null) { + preferences.edit().putString(addPrefixToKey(key), value).apply(); + } } public void delete(String key) { - ensureInitialized(); - - SharedPreferences.Editor editor = preferences.edit(); - editor.remove(key); - editor.apply(); + SharedPreferences preferences = getEncryptedSharedPreferences(); + if (preferences != null) { + preferences.edit().remove(addPrefixToKey(key)).apply(); + } } public void deleteAll() { - ensureInitialized(); - - final SharedPreferences.Editor editor = preferences.edit(); - editor.clear(); - editor.apply(); - } - - protected void ensureOptions() { - String sharedPreferencesName = getStringOption("sharedPreferencesName"); - if (!sharedPreferencesName.isEmpty()) { - SHARED_PREFERENCES_NAME = sharedPreferencesName; + SharedPreferences preferences = getEncryptedSharedPreferences(); + if (preferences != null) { + preferences.edit().clear().apply(); } + } - String preferencesKeyPrefix = getStringOption("preferencesKeyPrefix"); - if (!preferencesKeyPrefix.isEmpty()) { - ELEMENT_PREFERENCES_KEY_PREFIX = preferencesKeyPrefix; + public Map readAll() { + SharedPreferences preferences = getEncryptedSharedPreferences(); + Map result = new HashMap<>(); + if (preferences != null) { + Map allEntries = preferences.getAll(); + 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 + "_", ""); + result.put(originalKey, (String) value); + } + } } + return result; } - private String getStringOption(String key) { - Object value = options.get(key); - return value instanceof String ? (String) value : ""; + private String addPrefixToKey(String key) { + return preferencesKeyPrefix + "_" + key; } - private void ensureInitialized() { - ensureOptions(); + private void ensureOptions() { + sharedPreferencesName = options.containsKey("sharedPreferencesName") && options.get("sharedPreferencesName") instanceof String + ? (String) options.get("sharedPreferencesName") + : DEFAULT_PREF_NAME; - try { - preferences = initializeEncryptedSharedPreferencesManager(applicationContext); - checkAndMigrateToEncrypted(preferences); - } catch (Exception e) { - Log.e(TAG, "EncryptedSharedPreferences initialization failed", e); - } + preferencesKeyPrefix = options.containsKey("preferencesKeyPrefix") && options.get("preferencesKeyPrefix") instanceof String + ? (String) options.get("preferencesKeyPrefix") + : DEFAULT_KEY_PREFIX; } - private void checkAndMigrateToEncrypted(SharedPreferences target) { - SharedPreferences source = applicationContext.getSharedPreferences( - SHARED_PREFERENCES_NAME, - Context.MODE_PRIVATE - ); - try { - final var storageCipherFactory = new StorageCipherFactory(source, options); - final var storageCipher = storageCipherFactory.getSavedStorageCipher(applicationContext); - + private SharedPreferences getEncryptedSharedPreferences() { + if (encryptedPreferences == null) { try { - for (Map.Entry entry : source.getAll().entrySet()) { - Object v = entry.getValue(); - String key = entry.getKey(); - if (v instanceof String && key.contains(ELEMENT_PREFERENCES_KEY_PREFIX)) { - - byte[] data = Base64.decode((String) v, 0); - byte[] result = storageCipher.decrypt(data); - - final String decodedValue = new String(result, charset); - - target.edit().putString(key, (decodedValue)).apply(); - source.edit().remove(key).apply(); - } - } - final SharedPreferences.Editor sourceEditor = source.edit(); - storageCipherFactory.removeCurrentAlgorithms(sourceEditor); - sourceEditor.apply(); + encryptedPreferences = initializeEncryptedSharedPreferencesManager(applicationContext); + migrateToEncryptedPreferences(encryptedPreferences); } catch (Exception e) { - Log.e(TAG, "Data migration failed", e); + Log.e(TAG, "EncryptedSharedPreferences initialization failed", e); } - } catch (Exception e) { - Log.e(TAG, "StorageCipher initialization failed", e); + } else { + return encryptedPreferences; } + return null; } private SharedPreferences initializeEncryptedSharedPreferencesManager(Context context) throws GeneralSecurityException, IOException { - MasterKey key = new MasterKey.Builder(context) - .setKeyGenParameterSpec( - new KeyGenParameterSpec - .Builder(MasterKey.DEFAULT_MASTER_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setKeySize(256).build()) + 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(); + return EncryptedSharedPreferences.create( context, - SHARED_PREFERENCES_NAME, - key, + sharedPreferencesName, + masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } + + private void migrateToEncryptedPreferences(SharedPreferences target) { + SharedPreferences source = applicationContext.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE); + + try { + StorageCipher cipher = new StorageCipherFactory(source, options).getSavedStorageCipher(applicationContext); + + Map sourceEntries = source.getAll(); + if (sourceEntries.isEmpty()) return; + + int succesfull = 0; + int failed = 0; + for (Map.Entry entry : sourceEntries.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (key.startsWith(preferencesKeyPrefix) && value instanceof String) { + try { + String decryptedValue = decryptValue((String) value, cipher); + target.edit().putString(key, decryptedValue).apply(); + source.edit().remove(key).apply(); + succesfull++; + } catch (Exception e) { + Log.e(TAG, "Migration failed for key: " + key, e); + failed++; + } + } + } + + if (succesfull > 0) { + Log.i(TAG, "Succesfully migrated " + succesfull + " keys."); + } + if (failed > 0) { + Log.i(TAG, "Failed to migrate " + failed + " keys."); + } + } catch(Exception e) { + Log.e(TAG, "Migration failed due to initialisation error.", e); + } + } + + private String decryptValue(String value, StorageCipher cipher) throws Exception { + byte[] data = Base64.decode(value, Base64.DEFAULT); + return new String(cipher.decrypt(data), CHARSET); + } } 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 94f957e2..1500b066 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 @@ -28,21 +28,6 @@ public class FlutterSecureStoragePlugin implements MethodCallHandler, FlutterPlu private HandlerThread workerThread; private Handler workerThreadHandler; - public void initInstance(BinaryMessenger messenger, Context context) { - try { - secureStorage = new FlutterSecureStorage(context, new HashMap<>()); - - workerThread = new HandlerThread("com.it_nomads.fluttersecurestorage.worker"); - workerThread.start(); - workerThreadHandler = new Handler(workerThread.getLooper()); - - channel = new MethodChannel(messenger, "plugins.it_nomads.com/flutter_secure_storage"); - channel.setMethodCallHandler(this); - } catch (Exception e) { - Log.e(TAG, "Registration failed", e); - } - } - @Override public void onAttachedToEngine(FlutterPluginBinding binding) { initInstance(binding.getBinaryMessenger(), binding.getApplicationContext()); @@ -51,39 +36,134 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { if (channel != null) { - workerThread.quitSafely(); - workerThread = null; - + if (workerThread != null) { + workerThread.quitSafely(); + workerThread = null; + } channel.setMethodCallHandler(null); channel = null; } secureStorage = null; } + private void initInstance(BinaryMessenger messenger, Context context) { + try { + secureStorage = new FlutterSecureStorage(context, new HashMap<>()); + workerThread = new HandlerThread("fluttersecurestorage.worker"); + workerThread.start(); + workerThreadHandler = new Handler(workerThread.getLooper()); + channel = new MethodChannel(messenger, "plugins.it_nomads.com/flutter_secure_storage"); + channel.setMethodCallHandler(this); + } catch (Exception e) { + Log.e(TAG, "Plugin initialization failed", e); + } + } + @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result rawResult) { MethodResultWrapper result = new MethodResultWrapper(rawResult); - // Run all method calls inside the worker thread instead of the platform thread. workerThreadHandler.post(new MethodRunner(call, result)); } - @SuppressWarnings("unchecked") - private String getKeyFromCall(MethodCall call) { - Map arguments = (Map) call.arguments; - return secureStorage.addPrefixToKey((String) arguments.get("key")); - } + class MethodRunner implements Runnable { + private final MethodCall call; + private final Result result; + + MethodRunner(MethodCall call, Result result) { + this.call = call; + this.result = result; + } + + @Override + public void run() { + try { + handleMethodCall(call, result); + } catch (Exception e) { + handleException(e); + } + } + + private void handleMethodCall(MethodCall call, Result result) { + String method = call.method; + Map args = extractArguments(call); + + switch (method) { + case "write": + handleWrite(args, result); + break; + case "read": + handleRead(args, result); + break; + case "readAll": + handleReadAll(result); + break; + case "containsKey": + handleContainsKey(args, result); + break; + case "delete": + handleDelete(args, result); + break; + case "deleteAll": + handleDeleteAll(result); + break; + default: + result.notImplemented(); + } + } + + private void handleWrite(Map args, Result result) { + String key = (String) args.get("key"); + String value = (String) args.get("value"); + if (value != null) { + secureStorage.write(key, value); + result.success(null); + } else { + result.error("InvalidArgument", "Value is null", null); + } + } + + private void handleRead(Map args, Result result) { + String key = (String) args.get("key"); + if (secureStorage.containsKey(key)) { + result.success(secureStorage.read(key)); + } else { + result.success(null); + } + } + + private void handleReadAll(Result result) { + result.success(secureStorage.readAll()); + } + + private void handleContainsKey(Map args, Result result) { + String key = (String) args.get("key"); + result.success(secureStorage.containsKey(key)); + } + + private void handleDelete(Map args, Result result) { + String key = (String) args.get("key"); + secureStorage.delete(key); + result.success(null); + } - @SuppressWarnings("unchecked") - private String getValueFromCall(MethodCall call) { - Map arguments = (Map) call.arguments; - return (String) arguments.get("value"); + private void handleDeleteAll(Result result) { + secureStorage.deleteAll(); + result.success(null); + } + + @SuppressWarnings("unchecked") + private Map extractArguments(MethodCall call) { + return (Map) call.arguments; + } + + private void handleException(Exception e) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + result.error("Exception", "Error while executing method: " + call.method, stringWriter.toString()); + } } - /** - * MethodChannel.Result wrapper that responds on the platform thread. - */ static class MethodResultWrapper implements Result { - private final Result methodResult; private final Handler handler = new Handler(Looper.getMainLooper()); @@ -106,96 +186,4 @@ public void notImplemented() { handler.post(methodResult::notImplemented); } } - - /** - * Wraps the functionality of onMethodCall() in a Runnable for execution in the worker thread. - */ - class MethodRunner implements Runnable { - private final MethodCall call; - private final Result result; - - MethodRunner(MethodCall call, Result result) { - this.call = call; - this.result = result; - } - - @SuppressWarnings("unchecked") - @Override - public void run() { - boolean resetOnError = false; - try { - secureStorage.options = (Map) ((Map) call.arguments).get("options"); - secureStorage.ensureOptions(); - resetOnError = secureStorage.getResetOnError(); - switch (call.method) { - case "write": { - String key = getKeyFromCall(call); - String value = getValueFromCall(call); - - if (value != null) { - secureStorage.write(key, value); - result.success(null); - } else { - result.error("null", null, null); - } - break; - } - case "read": { - String key = getKeyFromCall(call); - - if (secureStorage.containsKey(key)) { - String value = secureStorage.read(key); - result.success(value); - } else { - result.success(null); - } - break; - } - case "readAll": { - result.success(secureStorage.readAll()); - break; - } - case "containsKey": { - String key = getKeyFromCall(call); - - boolean containsKey = secureStorage.containsKey(key); - result.success(containsKey); - break; - } - case "delete": { - String key = getKeyFromCall(call); - - secureStorage.delete(key); - result.success(null); - break; - } - case "deleteAll": { - secureStorage.deleteAll(); - result.success(null); - break; - } - default: - result.notImplemented(); - break; - } - } catch (Exception e) { - if (resetOnError) { - try { - secureStorage.deleteAll(); - result.success("Data has been reset"); - } catch (Exception ex) { - handleException(ex); - } - } else { - handleException(e); - } - } - } - - private void handleException(Exception e) { - StringWriter stringWriter = new StringWriter(); - e.printStackTrace(new PrintWriter(stringWriter)); - result.error("Exception encountered", call.method, stringWriter.toString()); - } - } } diff --git a/flutter_secure_storage/example/android/settings.gradle.kts b/flutter_secure_storage/example/android/settings.gradle.kts index 13e40edc..8e2cf65e 100644 --- a/flutter_secure_storage/example/android/settings.gradle.kts +++ b/flutter_secure_storage/example/android/settings.gradle.kts @@ -19,8 +19,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.2" apply false - id("org.jetbrains.kotlin.android") version "1.9.24" apply false + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "1.9.25" apply false } include(":app")