From 02464e5c7f5f7cd8c590818ff7ccb5f187da19ea Mon Sep 17 00:00:00 2001 From: Miroslav Genov Date: Wed, 26 Jun 2019 08:15:59 +0300 Subject: [PATCH] keychain/android: let client app access keychain from native code This change extracts the Keychain related code into separate class which to be used by the client apps which would like to access it from the native side. --- .../java/com/oblador/keychain/Keychain.java | 64 ++++++ .../com/oblador/keychain/KeychainModule.java | 167 ++-------------- .../com/oblador/keychain/KeychainPackage.java | 4 - .../java/com/oblador/keychain/Keychains.java | 34 ++++ .../com/oblador/keychain/PrefsStorage.java | 5 +- .../oblador/keychain/ServiceCredentials.java | 18 ++ .../oblador/keychain/SharedRefKeychain.java | 187 ++++++++++++++++++ .../CipherStorageFacebookConceal.java | 6 +- 8 files changed, 320 insertions(+), 165 deletions(-) create mode 100644 android/src/main/java/com/oblador/keychain/Keychain.java create mode 100644 android/src/main/java/com/oblador/keychain/Keychains.java create mode 100644 android/src/main/java/com/oblador/keychain/ServiceCredentials.java create mode 100644 android/src/main/java/com/oblador/keychain/SharedRefKeychain.java diff --git a/android/src/main/java/com/oblador/keychain/Keychain.java b/android/src/main/java/com/oblador/keychain/Keychain.java new file mode 100644 index 00000000..efce9de5 --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/Keychain.java @@ -0,0 +1,64 @@ +package com.oblador.keychain; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.oblador.keychain.exceptions.CryptoFailedException; +import com.oblador.keychain.exceptions.EmptyParameterException; +import com.oblador.keychain.exceptions.KeyStoreAccessException; + +/** + * Keychain is an Keychain module which abstracts an encrypted the keychain related lookups and updates. + *

+ *

+ * The flow is the following: + *

+ * setGenericPassword(service1, "user1", "pass1") -> get service1 keys -> encrypt user/pass -> store user/pass.
+ * 
+ * + * @author Miroslav Genov + */ +public interface Keychain { + /** + * Gets the SecurityLevel of the Keychain, e.g software, hardware and etc. + * @return the security level + */ + SecurityLevel getSecurityLevel(); + + /** + * Gets the {@link ServiceCredentials} associated with the provided service + * + * @param service the service to which credentials are associated + * @return the service credentials associated with the provided service + * @throws CryptoFailedException in case of crypto failure + * @throws KeyStoreAccessException in case of error during accessing the keystore + */ + @Nullable + ServiceCredentials getGenericPasswordForOptions(String service) throws CryptoFailedException, KeyStoreAccessException; + + /** + * Checks whether the provided credentials exists. + * @param server the service id + * @return true if credentials exists and false in other case + */ + boolean hasInternetCredentialsForServer(@NonNull String server); + + /** + * Sets new credentials of the provided service + * @param service the name of the service + * @param username the username value + * @param password the password value + * @param minimumSecurityLevel the minimum security level + * @throws EmptyParameterException is password is empty + * @throws CryptoFailedException in case of crypto failure + */ + void setGenericPasswordForOptions(String service, String username, String password, String minimumSecurityLevel) throws EmptyParameterException, CryptoFailedException; + + /** + * Resets credentials of a given service. + * + * @param service the service of which credentials to be reset + * @throws KeyStoreAccessException in case of keystore access error + */ + void resetGenericPasswordForOptions(String service) throws KeyStoreAccessException; +} diff --git a/android/src/main/java/com/oblador/keychain/KeychainModule.java b/android/src/main/java/com/oblador/keychain/KeychainModule.java index 61d061e8..ef19a92a 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainModule.java +++ b/android/src/main/java/com/oblador/keychain/KeychainModule.java @@ -1,6 +1,5 @@ package com.oblador.keychain; -import android.os.Build; import androidx.annotation.NonNull; import android.util.Log; @@ -11,12 +10,6 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; -import com.oblador.keychain.PrefsStorage.ResultSet; -import com.oblador.keychain.cipherStorage.CipherStorage; -import com.oblador.keychain.cipherStorage.CipherStorage.DecryptionResult; -import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult; -import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal; -import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAESCBC; import com.oblador.keychain.exceptions.CryptoFailedException; import com.oblador.keychain.exceptions.EmptyParameterException; import com.oblador.keychain.exceptions.KeyStoreAccessException; @@ -34,10 +27,8 @@ public class KeychainModule extends ReactContextBaseJavaModule { public static final String E_SUPPORTED_BIOMETRY_ERROR = "E_SUPPORTED_BIOMETRY_ERROR"; public static final String KEYCHAIN_MODULE = "RNKeychainManager"; public static final String FINGERPRINT_SUPPORTED_NAME = "Fingerprint"; - public static final String EMPTY_STRING = ""; - private final Map cipherStorageMap = new HashMap<>(); - private final PrefsStorage prefsStorage; + private final Keychain keyChain; @Override public String getName() { @@ -46,15 +37,9 @@ public String getName() { public KeychainModule(ReactApplicationContext reactContext) { super(reactContext); - prefsStorage = new PrefsStorage(reactContext); - - addCipherStorageToMap(new CipherStorageFacebookConceal(reactContext)); - addCipherStorageToMap(new CipherStorageKeystoreAESCBC()); + keyChain = Keychains.create(reactContext); } - private void addCipherStorageToMap(CipherStorage cipherStorage) { - cipherStorageMap.put(cipherStorage.getCipherStorageName(), cipherStorage); - } @Nullable @Override @@ -68,24 +53,13 @@ public Map getConstants() { @ReactMethod public void getSecurityLevel(Promise promise) { - promise.resolve(getSecurityLevel().name()); + promise.resolve(keyChain.getSecurityLevel().name()); } @ReactMethod public void setGenericPasswordForOptions(String service, String username, String password, String minimumSecurityLevel, Promise promise) { try { - SecurityLevel level = SecurityLevel.valueOf(minimumSecurityLevel); - if (username == null || username.isEmpty() || password == null || password.isEmpty()) { - throw new EmptyParameterException("you passed empty or null username/password"); - } - service = getDefaultServiceIfNull(service); - - CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel(); - validateCipherStorageSecurityLevel(currentCipherStorage, level); - - EncryptionResult result = currentCipherStorage.encrypt(service, username, password, level); - prefsStorage.storeEncryptedEntry(service, result); - + keyChain.setGenericPasswordForOptions(service, username, password, minimumSecurityLevel); promise.resolve(true); } catch (EmptyParameterException e) { Log.e(KEYCHAIN_MODULE, e.getMessage()); @@ -99,24 +73,19 @@ public void setGenericPasswordForOptions(String service, String username, String @ReactMethod public void getGenericPasswordForOptions(String service, Promise promise) { try { - service = getDefaultServiceIfNull(service); - - CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel(); + ServiceCredentials savedCredentials = keyChain.getGenericPasswordForOptions(service); - ResultSet resultSet = prefsStorage.getEncryptedEntry(service); - if (resultSet == null) { + if (savedCredentials == null) { Log.e(KEYCHAIN_MODULE, "No entry found for service: " + service); promise.resolve(false); return; } - final DecryptionResult decryptionResult = decryptCredentials(service, currentCipherStorage, resultSet); WritableMap credentials = Arguments.createMap(); - credentials.putString("service", service); - credentials.putString("username", decryptionResult.username); - credentials.putString("password", decryptionResult.password); + credentials.putString("username", savedCredentials.username); + credentials.putString("password", savedCredentials.password); promise.resolve(credentials); } catch (KeyStoreAccessException e) { @@ -128,53 +97,10 @@ public void getGenericPasswordForOptions(String service, Promise promise) { } } - private DecryptionResult decryptCredentials(String service, CipherStorage currentCipherStorage, ResultSet resultSet) throws CryptoFailedException, KeyStoreAccessException { - if (resultSet.cipherStorageName.equals(currentCipherStorage.getCipherStorageName())) { - // The encrypted data is encrypted using the current CipherStorage, so we just decrypt and return - return currentCipherStorage.decrypt(service, resultSet.usernameBytes, resultSet.passwordBytes); - } - - // The encrypted data is encrypted using an older CipherStorage, so we need to decrypt the data first, then encrypt it using the current CipherStorage, then store it again and return - CipherStorage oldCipherStorage = getCipherStorageByName(resultSet.cipherStorageName); - // decrypt using the older cipher storage - - DecryptionResult decryptionResult = oldCipherStorage.decrypt(service, resultSet.usernameBytes, resultSet.passwordBytes); - // encrypt using the current cipher storage - - try { - migrateCipherStorage(service, currentCipherStorage, oldCipherStorage, decryptionResult); - } catch (CryptoFailedException e) { - Log.e(KEYCHAIN_MODULE, "Migrating to a less safe storage is not allowed. Keeping the old one"); - } - - return decryptionResult; - } - - private void migrateCipherStorage(String service, CipherStorage newCipherStorage, CipherStorage oldCipherStorage, DecryptionResult decryptionResult) throws KeyStoreAccessException, CryptoFailedException { - // don't allow to degrade security level when transferring, the new storage should be as safe as the old one. - EncryptionResult encryptionResult = newCipherStorage.encrypt(service, decryptionResult.username, decryptionResult.password, decryptionResult.getSecurityLevel()); - // store the encryption result - prefsStorage.storeEncryptedEntry(service, encryptionResult); - // clean up the old cipher storage - oldCipherStorage.removeKey(service); - } - @ReactMethod public void resetGenericPasswordForOptions(String service, Promise promise) { try { - service = getDefaultServiceIfNull(service); - - // First we clean up the cipher storage (using the cipher storage that was used to store the entry) - ResultSet resultSet = prefsStorage.getEncryptedEntry(service); - if (resultSet != null) { - CipherStorage cipherStorage = getCipherStorageByName(resultSet.cipherStorageName); - if (cipherStorage != null) { - cipherStorage.removeKey(service); - } - } - // And then we remove the entry in the shared preferences - prefsStorage.removeEntry(service); - + keyChain.resetGenericPasswordForOptions(service); promise.resolve(true); } catch (KeyStoreAccessException e) { Log.e(KEYCHAIN_MODULE, e.getMessage()); @@ -184,11 +110,10 @@ public void resetGenericPasswordForOptions(String service, Promise promise) { @ReactMethod public void hasInternetCredentialsForServer(@NonNull String server, Promise promise) { - final String defaultService = getDefaultServiceIfNull(server); + boolean hasInternetCredentials = keyChain.hasInternetCredentialsForServer(server); - ResultSet resultSet = prefsStorage.getEncryptedEntry(defaultService); - if (resultSet == null) { - Log.e(KEYCHAIN_MODULE, "No entry found for service: " + defaultService); + if (!hasInternetCredentials) { + Log.e(KEYCHAIN_MODULE, "No entry found for service: " + server); promise.resolve(false); return; } @@ -226,78 +151,10 @@ public void getSupportedBiometryType(Promise promise) { } } - // The "Current" CipherStorage is the cipherStorage with the highest API level that is lower than or equal to the current API level - private CipherStorage getCipherStorageForCurrentAPILevel() throws CryptoFailedException { - int currentAPILevel = Build.VERSION.SDK_INT; - CipherStorage currentCipherStorage = null; - for (CipherStorage cipherStorage : cipherStorageMap.values()) { - int cipherStorageAPILevel = cipherStorage.getMinSupportedApiLevel(); - // Is the cipherStorage supported on the current API level? - boolean isSupported = (cipherStorageAPILevel <= currentAPILevel); - if (!isSupported) { - continue; - } - // Is the API level better than the one we previously selected (if any)? - if (currentCipherStorage == null || cipherStorageAPILevel > currentCipherStorage.getMinSupportedApiLevel()) { - currentCipherStorage = cipherStorage; - } - } - if (currentCipherStorage == null) { - throw new CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT); - } - return currentCipherStorage; - } - - private void validateCipherStorageSecurityLevel(CipherStorage cipherStorage, SecurityLevel requiredLevel) throws CryptoFailedException { - if (cipherStorage.securityLevel().satisfiesSafetyThreshold(requiredLevel)) { - return; - } - - throw new CryptoFailedException( - String.format( - "Cipher Storage is too weak. Required security level is: %s, but only %s is provided", - requiredLevel.name(), - cipherStorage.securityLevel().name())); - } - - - private CipherStorage getCipherStorageByName(String cipherStorageName) { - return cipherStorageMap.get(cipherStorageName); - } private boolean isFingerprintAuthAvailable() { return DeviceAvailability.isFingerprintAuthAvailable(getReactApplicationContext()); } - private boolean isSecureHardwareAvailable() { - try { - return getCipherStorageForCurrentAPILevel().supportsSecureHardware(); - } catch (CryptoFailedException e) { - return false; - } - } - - private SecurityLevel getSecurityLevel() { - try { - CipherStorage storage = getCipherStorageForCurrentAPILevel(); - if (!storage.securityLevel().satisfiesSafetyThreshold(SecurityLevel.SECURE_SOFTWARE)) { - return SecurityLevel.ANY; - } - if (isSecureHardwareAvailable()) { - return SecurityLevel.SECURE_HARDWARE; - } else { - return SecurityLevel.SECURE_SOFTWARE; - } - } catch (CryptoFailedException e) { - return SecurityLevel.ANY; - } - } - - - - @NonNull - private String getDefaultServiceIfNull(String service) { - return service == null ? EMPTY_STRING : service; - } } diff --git a/android/src/main/java/com/oblador/keychain/KeychainPackage.java b/android/src/main/java/com/oblador/keychain/KeychainPackage.java index d3aa6511..52a52f36 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainPackage.java +++ b/android/src/main/java/com/oblador/keychain/KeychainPackage.java @@ -12,10 +12,6 @@ public class KeychainPackage implements ReactPackage { - public KeychainPackage() { - - } - @Override public List createNativeModules( ReactApplicationContext reactContext) { diff --git a/android/src/main/java/com/oblador/keychain/Keychains.java b/android/src/main/java/com/oblador/keychain/Keychains.java new file mode 100644 index 00000000..e91b2b33 --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/Keychains.java @@ -0,0 +1,34 @@ +package com.oblador.keychain; + +import android.content.Context; + +import com.oblador.keychain.cipherStorage.CipherStorage; +import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal; +import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAESCBC; + +import java.util.LinkedHashMap; + +/** + * Keychains is a factory class used to create Keychain objects used for storing of secrets. + * + * @author Miroslav Genov + */ +public final class Keychains { + + /** + * Creates a new {@link Keychain} instance that is using shared preferences as storage. + * + * @param context the android context + * @return the newly created Keychain instance + */ + public static Keychain create(Context context) { + final CipherStorage facebookConcealCipherStorage = new CipherStorageFacebookConceal(context); + final CipherStorage keystoreCipherStorage = new CipherStorageKeystoreAESCBC(); + + return new SharedRefKeychain(context, new LinkedHashMap() {{ + put(facebookConcealCipherStorage.getCipherStorageName(), facebookConcealCipherStorage); + put(keystoreCipherStorage.getCipherStorageName(), keystoreCipherStorage); + }}); + } + +} diff --git a/android/src/main/java/com/oblador/keychain/PrefsStorage.java b/android/src/main/java/com/oblador/keychain/PrefsStorage.java index 99448b16..e109a62a 100644 --- a/android/src/main/java/com/oblador/keychain/PrefsStorage.java +++ b/android/src/main/java/com/oblador/keychain/PrefsStorage.java @@ -5,7 +5,6 @@ import androidx.annotation.NonNull; import android.util.Base64; -import com.facebook.react.bridge.ReactApplicationContext; import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult; import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal; @@ -26,8 +25,8 @@ public ResultSet(String cipherStorageName, byte[] usernameBytes, byte[] password private final SharedPreferences prefs; - public PrefsStorage(ReactApplicationContext reactContext) { - this.prefs = reactContext.getSharedPreferences(KEYCHAIN_DATA, Context.MODE_PRIVATE); + public PrefsStorage(Context context) { + this.prefs = context.getSharedPreferences(KEYCHAIN_DATA, Context.MODE_PRIVATE); } public ResultSet getEncryptedEntry(@NonNull String service) { diff --git a/android/src/main/java/com/oblador/keychain/ServiceCredentials.java b/android/src/main/java/com/oblador/keychain/ServiceCredentials.java new file mode 100644 index 00000000..c585a672 --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/ServiceCredentials.java @@ -0,0 +1,18 @@ +package com.oblador.keychain; + +/** + * ServiceCredentials is representing a single pair (user/pass) of credentials stored in the Keychain. + * + * @author Miroslav Genov + */ +public final class ServiceCredentials { + public final String service; + public final String username; + public final String password; + + public ServiceCredentials(String service, String username, String password) { + this.service = service; + this.username = username; + this.password = password; + } +} diff --git a/android/src/main/java/com/oblador/keychain/SharedRefKeychain.java b/android/src/main/java/com/oblador/keychain/SharedRefKeychain.java new file mode 100644 index 00000000..4cad0298 --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/SharedRefKeychain.java @@ -0,0 +1,187 @@ +package com.oblador.keychain; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.oblador.keychain.cipherStorage.CipherStorage; +import com.oblador.keychain.exceptions.CryptoFailedException; +import com.oblador.keychain.exceptions.EmptyParameterException; +import com.oblador.keychain.exceptions.KeyStoreAccessException; + +import java.util.Map; + +/** + * SharedRefKeychain an {@link Keychain} implementation which stores the the encrypted values as shared + * preferences maintained by the helper class {@link PrefsStorage}. + * + * @author Miroslav Genov + */ +class SharedRefKeychain implements Keychain { + private static final String EMPTY_STRING = ""; + + private final PrefsStorage prefsStorage; + private final Map nameToCipher; + + + SharedRefKeychain(Context context, Map nameToCipher) { + this.nameToCipher = nameToCipher; + this.prefsStorage = new PrefsStorage(context); + } + + @Override + public SecurityLevel getSecurityLevel() { + try { + CipherStorage storage = getCipherStorageForCurrentAPILevel(); + if (!storage.securityLevel().satisfiesSafetyThreshold(SecurityLevel.SECURE_SOFTWARE)) { + return SecurityLevel.ANY; + } + + if (isSecureHardwareAvailable()) { + return SecurityLevel.SECURE_HARDWARE; + } else { + return SecurityLevel.SECURE_SOFTWARE; + } + } catch (CryptoFailedException e) { + return SecurityLevel.ANY; + } + } + + @Override + @Nullable + public ServiceCredentials getGenericPasswordForOptions(String service) throws CryptoFailedException, KeyStoreAccessException { + String targetService = getDefaultServiceIfNull(service); + + CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel(); + + PrefsStorage.ResultSet resultSet = prefsStorage.getEncryptedEntry(targetService); + if (resultSet == null) { + return null; + } + + final CipherStorage.DecryptionResult decryptionResult = decryptCredentials(targetService, currentCipherStorage, resultSet); + + return new ServiceCredentials(targetService, decryptionResult.username, decryptionResult.password); + } + + @Override + public boolean hasInternetCredentialsForServer(@NonNull String server) { + final String defaultService = getDefaultServiceIfNull(server); + PrefsStorage.ResultSet resultSet = prefsStorage.getEncryptedEntry(defaultService); + return resultSet != null; + } + + @Override + public void setGenericPasswordForOptions(String service, String username, String password, String minimumSecurityLevel) throws EmptyParameterException, CryptoFailedException { + SecurityLevel level = SecurityLevel.valueOf(minimumSecurityLevel); + if (username == null || username.isEmpty() || password == null || password.isEmpty()) { + throw new EmptyParameterException("you passed empty or null username/password"); + } + service = getDefaultServiceIfNull(service); + + CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel(); + validateCipherStorageSecurityLevel(currentCipherStorage, level); + + CipherStorage.EncryptionResult result = currentCipherStorage.encrypt(service, username, password, level); + prefsStorage.storeEncryptedEntry(service, result); + } + + @Override + public void resetGenericPasswordForOptions(String service) throws KeyStoreAccessException { + service = getDefaultServiceIfNull(service); + + // First we clean up the cipher storage (using the cipher storage that was used to store the entry) + PrefsStorage.ResultSet resultSet = prefsStorage.getEncryptedEntry(service); + if (resultSet != null) { + CipherStorage cipherStorage = getCipherStorageByName(resultSet.cipherStorageName); + if (cipherStorage != null) { + cipherStorage.removeKey(service); + } + } + // And then we remove the entry in the shared preferences + prefsStorage.removeEntry(service); + } + + + private CipherStorage.DecryptionResult decryptCredentials(String service, CipherStorage currentCipherStorage, PrefsStorage.ResultSet resultSet) throws CryptoFailedException, KeyStoreAccessException { + if (resultSet.cipherStorageName.equals(currentCipherStorage.getCipherStorageName())) { + // The encrypted data is encrypted using the current CipherStorage, so we just decrypt and return + return currentCipherStorage.decrypt(service, resultSet.usernameBytes, resultSet.passwordBytes); + } + + // The encrypted data is encrypted using an older CipherStorage, so we need to decrypt the data first, then encrypt it using the current CipherStorage, then store it again and return + CipherStorage oldCipherStorage = getCipherStorageByName(resultSet.cipherStorageName); + // decrypt using the older cipher storage + + CipherStorage.DecryptionResult decryptionResult = oldCipherStorage.decrypt(service, resultSet.usernameBytes, resultSet.passwordBytes); + // encrypt using the current cipher storage + + + migrateCipherStorage(service, currentCipherStorage, oldCipherStorage, decryptionResult); + + + return decryptionResult; + } + + private void migrateCipherStorage(String service, CipherStorage newCipherStorage, CipherStorage oldCipherStorage, CipherStorage.DecryptionResult decryptionResult) throws KeyStoreAccessException, CryptoFailedException { + // don't allow to degrade security level when transferring, the new storage should be as safe as the old one. + CipherStorage.EncryptionResult encryptionResult = newCipherStorage.encrypt(service, decryptionResult.username, decryptionResult.password, decryptionResult.getSecurityLevel()); + // store the encryption result + prefsStorage.storeEncryptedEntry(service, encryptionResult); + // clean up the old cipher storage + oldCipherStorage.removeKey(service); + } + + + private void validateCipherStorageSecurityLevel(CipherStorage cipherStorage, SecurityLevel requiredLevel) throws CryptoFailedException { + if (cipherStorage.securityLevel().satisfiesSafetyThreshold(requiredLevel)) { + return; + } + + throw new CryptoFailedException( + String.format( + "Cipher Storage is too weak. Required security level is: %s, but only %s is provided", + requiredLevel.name(), + cipherStorage.securityLevel().name())); + } + + private boolean isSecureHardwareAvailable() { + try { + return getCipherStorageForCurrentAPILevel().supportsSecureHardware(); + } catch (CryptoFailedException e) { + return false; + } + } + + // The "Current" CipherStorage is the cipherStorage with the highest API level that is lower than or equal to the current API level + private CipherStorage getCipherStorageForCurrentAPILevel() throws CryptoFailedException { + int currentAPILevel = Build.VERSION.SDK_INT; + CipherStorage currentCipherStorage = null; + for (CipherStorage cipherStorage : nameToCipher.values()) { + int cipherStorageAPILevel = cipherStorage.getMinSupportedApiLevel(); + // Is the cipherStorage supported on the current API level? + boolean isSupported = (cipherStorageAPILevel <= currentAPILevel); + if (!isSupported) { + continue; + } + // Is the API level better than the one we previously selected (if any)? + if (currentCipherStorage == null || cipherStorageAPILevel > currentCipherStorage.getMinSupportedApiLevel()) { + currentCipherStorage = cipherStorage; + } + } + if (currentCipherStorage == null) { + throw new CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT); + } + return currentCipherStorage; + } + + private CipherStorage getCipherStorageByName(String cipherStorageName) { + return nameToCipher.get(cipherStorageName); + } + + @NonNull + private String getDefaultServiceIfNull(String service) { + return service == null ? EMPTY_STRING : service; + } +} diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java index 3162f994..821665fc 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java @@ -1,5 +1,6 @@ package com.oblador.keychain.cipherStorage; +import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; @@ -9,7 +10,6 @@ import com.facebook.crypto.CryptoConfig; import com.facebook.crypto.Entity; import com.facebook.crypto.keychain.KeyChain; -import com.facebook.react.bridge.ReactApplicationContext; import com.oblador.keychain.SecurityLevel; import com.oblador.keychain.exceptions.CryptoFailedException; @@ -20,8 +20,8 @@ public class CipherStorageFacebookConceal implements CipherStorage { public static final String KEYCHAIN_DATA = "RN_KEYCHAIN"; private final Crypto crypto; - public CipherStorageFacebookConceal(ReactApplicationContext reactContext) { - KeyChain keyChain = new SharedPrefsBackedKeyChain(reactContext, CryptoConfig.KEY_256); + public CipherStorageFacebookConceal(Context context) { + KeyChain keyChain = new SharedPrefsBackedKeyChain(context, CryptoConfig.KEY_256); this.crypto = AndroidConceal.get().createDefaultCrypto(keyChain); }