diff --git a/.gitignore b/.gitignore index 16ebce7e..f55f1423 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ pubspec_overrides.yaml .dart_tool/ .packages build/ +/flutter_secure_storage/example/android/app/.cxx diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipher18Implementation.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipher18Implementation.java deleted file mode 100644 index 75124ae0..00000000 --- a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipher18Implementation.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.it_nomads.fluttersecurestorage.ciphers; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Base64; -import android.util.Log; - -import java.security.Key; -import java.security.SecureRandom; -import java.security.spec.AlgorithmParameterSpec; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -public class StorageCipher18Implementation implements StorageCipher { - private static final int keySize = 16; - private static final String KEY_ALGORITHM = "AES"; - private static final String SHARED_PREFERENCES_NAME = "FlutterSecureKeyStorage"; - private final Cipher cipher; - private final SecureRandom secureRandom; - private Key secretKey; - - public StorageCipher18Implementation(Context context, KeyCipher rsaCipher) throws Exception { - secureRandom = new SecureRandom(); - String aesPreferencesKey = getAESPreferencesKey(); - - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - - String aesKey = preferences.getString(aesPreferencesKey, null); - - cipher = getCipher(); - - if (aesKey != null) { - byte[] encrypted; - try { - encrypted = Base64.decode(aesKey, Base64.DEFAULT); - secretKey = rsaCipher.unwrap(encrypted, KEY_ALGORITHM); - return; - } catch (Exception e) { - Log.e("StorageCipher18Impl", "unwrap key failed", e); - } - } - - byte[] key = new byte[keySize]; - secureRandom.nextBytes(key); - secretKey = new SecretKeySpec(key, KEY_ALGORITHM); - - byte[] encryptedKey = rsaCipher.wrap(secretKey); - editor.putString(aesPreferencesKey, Base64.encodeToString(encryptedKey, Base64.DEFAULT)); - editor.apply(); - } - - protected String getAESPreferencesKey() { - return "VGhpcyBpcyB0aGUga2V5IGZvciBhIHNlY3VyZSBzdG9yYWdlIEFFUyBLZXkK"; - } - - protected Cipher getCipher() throws Exception { - return Cipher.getInstance("AES/CBC/PKCS7Padding"); - } - - @Override - public byte[] encrypt(byte[] input) throws Exception { - byte[] iv = new byte[getIvSize()]; - secureRandom.nextBytes(iv); - - AlgorithmParameterSpec ivParameterSpec = getParameterSpec(iv); - - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); - - byte[] payload = cipher.doFinal(input); - byte[] combined = new byte[iv.length + payload.length]; - - System.arraycopy(iv, 0, combined, 0, iv.length); - System.arraycopy(payload, 0, combined, iv.length, payload.length); - - return combined; - } - - @Override - public byte[] decrypt(byte[] input) throws Exception { - byte[] iv = new byte[getIvSize()]; - System.arraycopy(input, 0, iv, 0, iv.length); - AlgorithmParameterSpec ivParameterSpec = getParameterSpec(iv); - - int payloadSize = input.length - getIvSize(); - byte[] payload = new byte[payloadSize]; - System.arraycopy(input, iv.length, payload, 0, payloadSize); - - cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); - - return cipher.doFinal(payload); - } - - protected int getIvSize() { - return 16; - } - - protected AlgorithmParameterSpec getParameterSpec(byte[] iv) { - return new IvParameterSpec(iv); - } - -} diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherFactory.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherFactory.java index 32acf1b5..de4c98d9 100644 --- a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherFactory.java +++ b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherFactory.java @@ -8,33 +8,36 @@ enum KeyCipherAlgorithm { RSA_ECB_PKCS1Padding(RSACipher18Implementation::new, 1), - @SuppressWarnings({"UnusedDeclaration"}) + @SuppressWarnings("UnusedDeclaration") RSA_ECB_OAEPwithSHA_256andMGF1Padding(RSACipherOAEPImplementation::new, Build.VERSION_CODES.M); - final KeyCipherFunction keyCipher; - final int minVersionCode; - KeyCipherAlgorithm(KeyCipherFunction keyCipher, int minVersionCode) { - this.keyCipher = keyCipher; - this.minVersionCode = minVersionCode; + final KeyCipherFunction factory; + final int minSdk; + + KeyCipherAlgorithm(KeyCipherFunction factory, int minSdk) { + this.factory = factory; + this.minSdk = minSdk; + } + + public boolean isSupported() { + return Build.VERSION.SDK_INT >= minSdk; } } enum StorageCipherAlgorithm { - AES_CBC_PKCS7Padding(StorageCipher18Implementation::new, 1), - @SuppressWarnings({"UnusedDeclaration"}) AES_GCM_NoPadding(StorageCipherGCMImplementation::new, Build.VERSION_CODES.M); - final StorageCipherFunction storageCipher; - final int minVersionCode; - StorageCipherAlgorithm(StorageCipherFunction storageCipher, int minVersionCode) { - this.storageCipher = storageCipher; - this.minVersionCode = minVersionCode; + final StorageCipherFunction factory; + final int minSdk; + + StorageCipherAlgorithm(StorageCipherFunction factory, int minSdk) { + this.factory = factory; + this.minSdk = minSdk; } -} -@FunctionalInterface -interface StorageCipherFunction { - StorageCipher apply(Context context, KeyCipher keyCipher) throws Exception; + public boolean isSupported() { + return Build.VERSION.SDK_INT >= minSdk; + } } @FunctionalInterface @@ -42,31 +45,50 @@ interface KeyCipherFunction { KeyCipher apply(Context context) throws Exception; } +@FunctionalInterface +interface StorageCipherFunction { + StorageCipher apply(Context context, KeyCipher keyCipher) throws Exception; +} + public class StorageCipherFactory { - private static final String ELEMENT_PREFERENCES_ALGORITHM_PREFIX = "FlutterSecureSAlgorithm"; - private static final String ELEMENT_PREFERENCES_ALGORITHM_KEY = ELEMENT_PREFERENCES_ALGORITHM_PREFIX + "Key"; - private static final String ELEMENT_PREFERENCES_ALGORITHM_STORAGE = ELEMENT_PREFERENCES_ALGORITHM_PREFIX + "Storage"; - private static final KeyCipherAlgorithm DEFAULT_KEY_ALGORITHM = KeyCipherAlgorithm.RSA_ECB_PKCS1Padding; - private static final StorageCipherAlgorithm DEFAULT_STORAGE_ALGORITHM = StorageCipherAlgorithm.AES_CBC_PKCS7Padding; + private static final String PREFS_KEY_PREFIX = "FlutterSecureSAlgorithm"; + private static final String PREFS_KEY_KEY_CIPHER = PREFS_KEY_PREFIX + "Key"; + private static final String PREFS_KEY_STORAGE_CIPHER = PREFS_KEY_PREFIX + "Storage"; + + private static final KeyCipherAlgorithm DEFAULT_KEY_ALGO = KeyCipherAlgorithm.RSA_ECB_PKCS1Padding; + private static final StorageCipherAlgorithm DEFAULT_STORAGE_ALGO = StorageCipherAlgorithm.AES_GCM_NoPadding; private final KeyCipherAlgorithm savedKeyAlgorithm; private final StorageCipherAlgorithm savedStorageAlgorithm; private final KeyCipherAlgorithm currentKeyAlgorithm; private final StorageCipherAlgorithm currentStorageAlgorithm; - public StorageCipherFactory(SharedPreferences source, Map options) { - savedKeyAlgorithm = KeyCipherAlgorithm.valueOf(source.getString(ELEMENT_PREFERENCES_ALGORITHM_KEY, DEFAULT_KEY_ALGORITHM.name())); - savedStorageAlgorithm = StorageCipherAlgorithm.valueOf(source.getString(ELEMENT_PREFERENCES_ALGORITHM_STORAGE, DEFAULT_STORAGE_ALGORITHM.name())); - - final KeyCipherAlgorithm currentKeyAlgorithmTmp = KeyCipherAlgorithm.valueOf(getFromOptionsWithDefault(options, "keyCipherAlgorithm", DEFAULT_KEY_ALGORITHM.name())); - currentKeyAlgorithm = (currentKeyAlgorithmTmp.minVersionCode <= Build.VERSION.SDK_INT) ? currentKeyAlgorithmTmp : DEFAULT_KEY_ALGORITHM; - final StorageCipherAlgorithm currentStorageAlgorithmTmp = StorageCipherAlgorithm.valueOf(getFromOptionsWithDefault(options, "storageCipherAlgorithm", DEFAULT_STORAGE_ALGORITHM.name())); - currentStorageAlgorithm = (currentStorageAlgorithmTmp.minVersionCode <= Build.VERSION.SDK_INT) ? currentStorageAlgorithmTmp : DEFAULT_STORAGE_ALGORITHM; - } - - private String getFromOptionsWithDefault(Map options, String key, String defaultValue) { - final Object value = options.get(key); - return value != null ? value.toString() : defaultValue; + public StorageCipherFactory(SharedPreferences prefs, Map options) { + this.savedKeyAlgorithm = parseEnumOrDefault( + KeyCipherAlgorithm.class, + prefs.getString(PREFS_KEY_KEY_CIPHER, null), + DEFAULT_KEY_ALGO + ); + + this.savedStorageAlgorithm = parseEnumOrDefault( + StorageCipherAlgorithm.class, + prefs.getString(PREFS_KEY_STORAGE_CIPHER, null), + DEFAULT_STORAGE_ALGO + ); + + KeyCipherAlgorithm selectedKeyAlgo = parseEnumOrDefault( + KeyCipherAlgorithm.class, + getOptionOrDefault(options, "keyCipherAlgorithm", DEFAULT_KEY_ALGO.name()), + DEFAULT_KEY_ALGO + ); + this.currentKeyAlgorithm = selectedKeyAlgo.isSupported() ? selectedKeyAlgo : DEFAULT_KEY_ALGO; + + StorageCipherAlgorithm selectedStorageAlgo = parseEnumOrDefault( + StorageCipherAlgorithm.class, + getOptionOrDefault(options, "storageCipherAlgorithm", DEFAULT_STORAGE_ALGO.name()), + DEFAULT_STORAGE_ALGO + ); + this.currentStorageAlgorithm = selectedStorageAlgo.isSupported() ? selectedStorageAlgo : DEFAULT_STORAGE_ALGO; } public boolean requiresReEncryption() { @@ -74,22 +96,46 @@ public boolean requiresReEncryption() { } public StorageCipher getSavedStorageCipher(Context context) throws Exception { - final KeyCipher keyCipher = savedKeyAlgorithm.keyCipher.apply(context); - return savedStorageAlgorithm.storageCipher.apply(context, keyCipher); + return createCipher(savedStorageAlgorithm, savedKeyAlgorithm, context, true); } public StorageCipher getCurrentStorageCipher(Context context) throws Exception { - final KeyCipher keyCipher = currentKeyAlgorithm.keyCipher.apply(context); - return currentStorageAlgorithm.storageCipher.apply(context, keyCipher); + return createCipher(currentStorageAlgorithm, currentKeyAlgorithm, context, false); } public void storeCurrentAlgorithms(SharedPreferences.Editor editor) { - editor.putString(ELEMENT_PREFERENCES_ALGORITHM_KEY, currentKeyAlgorithm.name()); - editor.putString(ELEMENT_PREFERENCES_ALGORITHM_STORAGE, currentStorageAlgorithm.name()); + editor.putString(PREFS_KEY_KEY_CIPHER, currentKeyAlgorithm.name()); + editor.putString(PREFS_KEY_STORAGE_CIPHER, currentStorageAlgorithm.name()); } public void removeCurrentAlgorithms(SharedPreferences.Editor editor) { - editor.remove(ELEMENT_PREFERENCES_ALGORITHM_KEY); - editor.remove(ELEMENT_PREFERENCES_ALGORITHM_STORAGE); + editor.remove(PREFS_KEY_KEY_CIPHER); + editor.remove(PREFS_KEY_STORAGE_CIPHER); + } + + // --- Helpers --- + + private String getOptionOrDefault(Map options, String key, String fallback) { + Object value = options.get(key); + return value != null ? value.toString() : fallback; + } + + private > T parseEnumOrDefault(Class enumClass, String name, T defaultValue) { + if (name == null) return defaultValue; + try { + return Enum.valueOf(enumClass, name); + } catch (IllegalArgumentException e) { + return defaultValue; + } + } + + private StorageCipher createCipher(StorageCipherAlgorithm storageAlgo, KeyCipherAlgorithm keyAlgo, Context context, boolean isSaved) throws Exception { + try { + KeyCipher keyCipher = keyAlgo.factory.apply(context); + return storageAlgo.factory.apply(context, keyCipher); + } catch (Exception e) { + String label = isSaved ? "saved" : "current"; + throw new Exception("Failed to initialize " + label + " storage cipher securely", e); + } } } diff --git a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherGCMImplementation.java b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherGCMImplementation.java index 93b2bc30..abd4f049 100644 --- a/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherGCMImplementation.java +++ b/flutter_secure_storage/android/src/main/java/com/it_nomads/fluttersecurestorage/ciphers/StorageCipherGCMImplementation.java @@ -1,41 +1,114 @@ package com.it_nomads.fluttersecurestorage.ciphers; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; -import android.content.Context; -import android.os.Build; - -import androidx.annotation.RequiresApi; - +import java.security.Key; +import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; +import javax.crypto.AEADBadTagException; import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import android.content.Context; + import javax.crypto.spec.GCMParameterSpec; -public class StorageCipherGCMImplementation extends StorageCipher18Implementation { +public class StorageCipherGCMImplementation implements StorageCipher { private static final int AUTHENTICATION_TAG_SIZE = 128; public StorageCipherGCMImplementation(Context context, KeyCipher keyCipher) throws Exception { - super(context, keyCipher); + secureRandom = new SecureRandom(); + String aesPreferencesKey = getAESPreferencesKey(); + + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + + String aesKey = preferences.getString(aesPreferencesKey, null); + + if (aesKey != null) { + byte[] encrypted; + try { + encrypted = Base64.decode(aesKey, Base64.DEFAULT); + secretKey = keyCipher.unwrap(encrypted, KEY_ALGORITHM); + return; + } catch (Exception e) { + Log.e("StorageCipher18Impl", "unwrap key failed", e); + } + } + + byte[] key = new byte[keySize]; + secureRandom.nextBytes(key); + secretKey = new SecretKeySpec(key, KEY_ALGORITHM); + + byte[] encryptedKey = keyCipher.wrap(secretKey); + editor.putString(aesPreferencesKey, Base64.encodeToString(encryptedKey, Base64.DEFAULT)); + editor.apply(); } - @Override protected String getAESPreferencesKey() { - return "VGhpcyBpcyB0aGUga2V5IGZvcihBIHNlY3XyZZBzdG9yYWdlIEFFUyBLZXkK"; + return "flutter_secure_storage_aes_gcm_key"; } - @Override - protected Cipher getCipher() throws Exception { - return Cipher.getInstance("AES/GCM/NoPadding"); - } protected int getIvSize() { return 12; } - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - @Override protected AlgorithmParameterSpec getParameterSpec(byte[] iv) { return new GCMParameterSpec(AUTHENTICATION_TAG_SIZE, iv); } -} + private static final int keySize = 16; + private static final String KEY_ALGORITHM = "AES"; + private static final String SHARED_PREFERENCES_NAME = "FlutterSecureKeyStorage"; + private final SecureRandom secureRandom; + private Key secretKey; + + private Cipher getCipherInstance() throws Exception { + return Cipher.getInstance("AES/GCM/NoPadding"); + } + + @Override + public byte[] encrypt(byte[] input) throws Exception { + Cipher cipher = getCipherInstance(); + + byte[] iv = new byte[getIvSize()]; + secureRandom.nextBytes(iv); + + AlgorithmParameterSpec ivParameterSpec = getParameterSpec(iv); + + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); + + byte[] payload = cipher.doFinal(input); + byte[] combined = new byte[iv.length + payload.length]; + + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(payload, 0, combined, iv.length, payload.length); + + return combined; + } + + @Override + public byte[] decrypt(byte[] input) throws Exception { + Cipher cipher = getCipherInstance(); + + byte[] iv = new byte[getIvSize()]; + System.arraycopy(input, 0, iv, 0, iv.length); + AlgorithmParameterSpec ivParameterSpec = getParameterSpec(iv); + + int payloadSize = input.length - getIvSize(); + byte[] payload = new byte[payloadSize]; + System.arraycopy(input, iv.length, payload, 0, payloadSize); + + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); + + try { + return cipher.doFinal(payload); + } catch (AEADBadTagException e) { + Log.w("StorageCipherGCM", "GCM authentication failed"); + throw new SecurityException("Decryption failed: authentication tag mismatch", e); + } + } +} \ No newline at end of file