Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ pubspec_overrides.yaml
.dart_tool/
.packages
build/
/flutter_secure_storage/example/android/app/.cxx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,88 +8,134 @@

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
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<String, Object> 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<String, Object> options, String key, String defaultValue) {
final Object value = options.get(key);
return value != null ? value.toString() : defaultValue;
public StorageCipherFactory(SharedPreferences prefs, Map<String, Object> 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() {
return savedKeyAlgorithm != currentKeyAlgorithm || savedStorageAlgorithm != currentStorageAlgorithm;
}

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<String, Object> options, String key, String fallback) {
Object value = options.get(key);
return value != null ? value.toString() : fallback;
}

private <T extends Enum<T>> T parseEnumOrDefault(Class<T> 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);
}
}
}
Loading