diff --git a/android/src/main/java/com/oblador/keychain/KeychainModule.java b/android/src/main/java/com/oblador/keychain/KeychainModule.java index 102c08c..397e107 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainModule.java +++ b/android/src/main/java/com/oblador/keychain/KeychainModule.java @@ -1,23 +1,9 @@ package com.oblador.keychain; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.SharedPreferences; import android.os.Build; -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; import android.support.annotation.NonNull; -import android.util.Base64; import android.util.Log; -import com.facebook.android.crypto.keychain.AndroidConceal; -import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain; -import com.facebook.crypto.Crypto; -import com.facebook.crypto.CryptoConfig; -import com.facebook.crypto.Entity; -import com.facebook.crypto.exception.CryptoInitializationException; -import com.facebook.crypto.exception.KeyChainException; -import com.facebook.crypto.keychain.KeyChain; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; @@ -25,57 +11,28 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; -import com.oblador.keychain.PrefsUtils.ResultSet; +import com.oblador.keychain.PrefsStorage.ResultSet; import com.oblador.keychain.exceptions.CryptoFailedException; import com.oblador.keychain.exceptions.EmptyParameterException; import com.oblador.keychain.exceptions.KeyStoreAccessException; +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 java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.security.InvalidAlgorithmParameterException; -import java.security.Key; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.security.spec.AlgorithmParameterSpec; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.KeyGenerator; -import javax.crypto.spec.IvParameterSpec; +import java.util.HashMap; +import java.util.Map; public class KeychainModule extends ReactContextBaseJavaModule { public static final String E_EMPTY_PARAMETERS = "E_EMPTY_PARAMETERS"; public static final String E_CRYPTO_FAILED = "E_CRYPTO_FAILED"; - public static final String E_UNSUPPORTED_KEYSTORE = "E_UNSUPPORTED_KEYSTORE"; public static final String E_KEYSTORE_ACCESS_ERROR = "E_KEYSTORE_ACCESS_ERROR"; - public static final String KEYCHAIN_MODULE = "RNKeychainManager"; - public static final String KEYCHAIN_DATA = "RN_KEYCHAIN"; - public static final String EMPTY_STRING = ""; - public static final String DEFAULT_ALIAS = "RN_KEYCHAIN_DEFAULT_ALIAS"; - public static final String LEGACY_DELIMITER = ":"; - public static final String DELIMITER = "_"; - public static final String KEYSTORE_TYPE = "AndroidKeyStore"; - public static final String ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES; - public static final String ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC; - public static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7; - public static final String ENCRYPTION_TRANSFORMATION = - ENCRYPTION_ALGORITHM + "/" + - ENCRYPTION_BLOCK_MODE + "/" + - ENCRYPTION_PADDING; - public static final int ENCRYPTION_KEY_SIZE = 256; - private final Crypto crypto; - private final SharedPreferences prefs; - private final PrefsUtils prefsUtils; + private final Map cipherStorageMap = new HashMap<>(); + private final PrefsStorage prefsStorage; @Override public String getName() { @@ -84,11 +41,14 @@ public class KeychainModule extends ReactContextBaseJavaModule { public KeychainModule(ReactApplicationContext reactContext) { super(reactContext); + prefsStorage = new PrefsStorage(reactContext); - KeyChain keyChain = new SharedPrefsBackedKeyChain(getReactApplicationContext(), CryptoConfig.KEY_256); - crypto = AndroidConceal.get().createDefaultCrypto(keyChain); - prefs = this.getReactApplicationContext().getSharedPreferences(KEYCHAIN_DATA, Context.MODE_PRIVATE); - prefsUtils = new PrefsUtils(this.prefs); + addCipherStorageToMap(new CipherStorageFacebookConceal(reactContext)); + addCipherStorageToMap(new CipherStorageKeystoreAESCBC()); + } + + private void addCipherStorageToMap(CipherStorage cipherStorage) { + cipherStorageMap.put(cipherStorage.getCipherStorageName(), cipherStorage); } @ReactMethod @@ -97,15 +57,14 @@ public class KeychainModule extends ReactContextBaseJavaModule { if (username == null || username.isEmpty() || password == null || password.isEmpty()) { throw new EmptyParameterException("you passed empty or null username/password"); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setGenericPasswordForOptions(service, username, password); + CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel(); + if (currentCipherStorage == null) { + throw new CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT); + } + + EncryptionResult result = currentCipherStorage.encrypt(service, username, password); + prefsStorage.storeEncryptedEntry(service, result); - // Clean legacy values (if any) - resetGenericPasswordForOptionsLegacy(service); - } - else { - setGenericPasswordForOptionsUsingConceal(service, username, password); - } promise.resolve("KeychainModule saved the data"); } catch (EmptyParameterException e) { Log.e(KEYCHAIN_MODULE, e.getMessage()); @@ -113,253 +72,79 @@ public class KeychainModule extends ReactContextBaseJavaModule { } catch (CryptoFailedException e) { Log.e(KEYCHAIN_MODULE, e.getMessage()); promise.reject(E_CRYPTO_FAILED, e); - } catch (KeyStoreException e) { - Log.e(KEYCHAIN_MODULE, e.getMessage()); - promise.reject(E_UNSUPPORTED_KEYSTORE, e); - } catch (KeyStoreAccessException e) { - Log.e(KEYCHAIN_MODULE, e.getMessage()); - promise.reject(E_KEYSTORE_ACCESS_ERROR, e); - } - } - - @TargetApi(Build.VERSION_CODES.M) - private void setGenericPasswordForOptions(String service, String username, String password) throws CryptoFailedException, KeyStoreException, KeyStoreAccessException { - service = service == null ? DEFAULT_ALIAS : service; - - KeyStore keyStore = getKeyStoreAndLoad(); - - try { - if (!keyStore.containsAlias(service)) { - AlgorithmParameterSpec spec; - spec = new KeyGenParameterSpec.Builder( - service, - KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) - .setBlockModes(ENCRYPTION_BLOCK_MODE) - .setEncryptionPaddings(ENCRYPTION_PADDING) - .setRandomizedEncryptionRequired(true) - //.setUserAuthenticationRequired(true) // Will throw InvalidAlgorithmParameterException if there is no fingerprint enrolled on the device - .setKeySize(ENCRYPTION_KEY_SIZE) - .build(); - - KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_TYPE); - generator.init(spec); - - generator.generateKey(); - } - - Key key = keyStore.getKey(service, null); - - String encryptedUsername = encryptString(key, service, username); - String encryptedPassword = encryptString(key, service, password); - - prefsUtils.storeEncryptedValues(service, DELIMITER, encryptedUsername, encryptedPassword); - Log.d(KEYCHAIN_MODULE, "saved the data"); - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException | UnrecoverableKeyException e) { - throw new CryptoFailedException("Could not encrypt data for service " + service, e); - } - } - - private String encryptString(Key key, String service, String value) throws CryptoFailedException { - try { - Cipher cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION); - cipher.init(Cipher.ENCRYPT_MODE, key); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - // write initialization vector to the beginning of the stream - byte[] iv = cipher.getIV(); - outputStream.write(iv, 0, iv.length); - // encrypt the value using a CipherOutputStream - CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); - cipherOutputStream.write(value.getBytes("UTF-8")); - cipherOutputStream.close(); - // return a Base64 encoded String of the stream - byte[] encryptedBytes = outputStream.toByteArray(); - return Base64.encodeToString(encryptedBytes, Base64.DEFAULT); - } catch (Exception e) { - throw new CryptoFailedException("Could not encrypt value for service " + service, e); } } @ReactMethod public void getGenericPasswordForOptions(String service, Promise promise) { try { - final ResultSet resultSet; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - resultSet = getGenericPasswordForOptions(service); - } - else { - resultSet = getGenericPasswordForOptionsUsingConceal(service); + CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel(); + if (currentCipherStorage == null) { + throw new CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT); } + + final DecryptionResult decryptionResult; + ResultSet resultSet = prefsStorage.getEncryptedEntry(service); if (resultSet == null) { - Log.e(KEYCHAIN_MODULE, "no keychain entry found for service: " + service); + Log.e(KEYCHAIN_MODULE, "No entry found for service: " + service); promise.resolve(false); return; } + + if (resultSet.cipherStorageName.equals(currentCipherStorage.getCipherStorageName())) { + // The encrypted data is encrypted using the current CipherStorage, so we just decrypt and return + decryptionResult = currentCipherStorage.decrypt(service, resultSet.usernameBytes, resultSet.passwordBytes); + } + else { + // The encrypted data is encrypted using an older CipherStorage, so we need to decrypt the data, encrypt it using the current CipherStorage and then store it again + CipherStorage oldCipherStorage = cipherStorageMap.get(resultSet.cipherStorageName); + // decrypt using the older cipher storage + decryptionResult = oldCipherStorage.decrypt(service, resultSet.usernameBytes, resultSet.passwordBytes); + // encrypt using the current cipher storage + EncryptionResult encryptionResult = currentCipherStorage.encrypt(service, decryptionResult.username, decryptionResult.password); + // store the encryption result + prefsStorage.storeEncryptedEntry(service, encryptionResult); + // clean up the old cipher storage + oldCipherStorage.removeKey(service); + } + WritableMap credentials = Arguments.createMap(); - credentials.putString("service", resultSet.service); - credentials.putString("username", new String(resultSet.usernameBytes, Charset.forName("UTF-8"))); - credentials.putString("password", new String(resultSet.passwordBytes, Charset.forName("UTF-8"))); + credentials.putString("service", service); + credentials.putString("username", decryptionResult.username); + credentials.putString("password", decryptionResult.password); promise.resolve(credentials); - } catch (KeyStoreException e) { - Log.e(KEYCHAIN_MODULE, e.getMessage()); - promise.reject(E_UNSUPPORTED_KEYSTORE, e); } catch (KeyStoreAccessException e) { Log.e(KEYCHAIN_MODULE, e.getMessage()); promise.reject(E_KEYSTORE_ACCESS_ERROR, e); - } catch (UnrecoverableKeyException | NoSuchAlgorithmException | CryptoFailedException e) { + } catch (CryptoFailedException e) { Log.e(KEYCHAIN_MODULE, e.getMessage()); promise.reject(E_CRYPTO_FAILED, e); } } - @TargetApi(Build.VERSION_CODES.M) - private ResultSet getGenericPasswordForOptions(String service) throws CryptoFailedException, KeyStoreException, KeyStoreAccessException, UnrecoverableKeyException, NoSuchAlgorithmException { - String originalService = service; - service = service == null ? DEFAULT_ALIAS : service; - - final byte[] decryptedUsername; - final byte[] decryptedPassword; - ResultSet encryptedResultSet = prefsUtils.getBytesForUsernameAndPassword(service, DELIMITER); - if (encryptedResultSet == null) { - // Check if the values are stored using the LEGACY_DELIMITER and thus encrypted using FaceBook's Conceal - ResultSet legacyResultSet = getGenericPasswordForOptionsUsingConceal(originalService); - if (legacyResultSet != null) { - // Store the values using the new delimiter and the KeyStore - setGenericPasswordForOptions( - originalService, - new String(legacyResultSet.usernameBytes, Charset.forName("UTF-8")), - new String(legacyResultSet.passwordBytes, Charset.forName("UTF-8"))); - // Remove the legacy value(s) - resetGenericPasswordForOptionsLegacy(originalService); - decryptedUsername = legacyResultSet.usernameBytes; - decryptedPassword = legacyResultSet.passwordBytes; - } else { - return null; - } - } - else { - KeyStore keyStore = getKeyStoreAndLoad(); - - Key key = keyStore.getKey(service, null); - - decryptedUsername = decryptBytes(key, encryptedResultSet.usernameBytes); - decryptedPassword = decryptBytes(key, encryptedResultSet.passwordBytes); - } - return new ResultSet(service, decryptedUsername, decryptedPassword); - } - - private byte[] decryptBytes(Key key, byte[] bytes) throws CryptoFailedException { - try { - Cipher cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION); - ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); - // read the initialization vector from the beginning of the stream - IvParameterSpec ivParams = readIvFromStream(inputStream); - cipher.init(Cipher.DECRYPT_MODE, key, ivParams); - // decrypt the bytes using a CipherInputStream - CipherInputStream cipherInputStream = new CipherInputStream( - inputStream, cipher); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - while (true) { - int n = cipherInputStream.read(buffer, 0, buffer.length); - if (n <= 0) { - break; - } - output.write(buffer, 0, n); - } - return output.toByteArray(); - } catch (Exception e) { - throw new CryptoFailedException("Could not decrypt bytes", e); - } - } - - private IvParameterSpec readIvFromStream(ByteArrayInputStream inputStream) { - byte[] iv = new byte[16]; - inputStream.read(iv, 0, iv.length); - return new IvParameterSpec(iv); - } - - private ResultSet getGenericPasswordForOptionsUsingConceal(String service) throws CryptoFailedException { - if (!crypto.isAvailable()) { - throw new CryptoFailedException("Crypto is missing"); - } - service = service == null ? EMPTY_STRING : service; - - ResultSet legacyResultSet = prefsUtils.getBytesForUsernameAndPassword(service, LEGACY_DELIMITER); - if (legacyResultSet == null) { - return null; - } - - Entity userentity = Entity.create(KEYCHAIN_DATA + ":" + service + "user"); - Entity pwentity = Entity.create(KEYCHAIN_DATA + ":" + service + "pass"); - - try { - byte[] decryptedUsername = crypto.decrypt(legacyResultSet.usernameBytes, userentity); - byte[] decryptedPassword = crypto.decrypt(legacyResultSet.passwordBytes, pwentity); - - return new ResultSet(service, decryptedUsername, decryptedPassword); - } catch (Exception e) { - throw new CryptoFailedException("Decryption failed for service " + service, e); - } - } - - private void setGenericPasswordForOptionsUsingConceal(String service, String username, String password) throws CryptoFailedException { - if (!crypto.isAvailable()) { - throw new CryptoFailedException("Crypto is missing"); - } - service = service == null ? EMPTY_STRING : service; - - Entity userentity = Entity.create(KEYCHAIN_DATA + ":" + service + "user"); - Entity pwentity = Entity.create(KEYCHAIN_DATA + ":" + service + "pass"); - - try { - String encryptedUsername = encryptWithEntity(username, userentity); - String encryptedPassword = encryptWithEntity(password, pwentity); - - prefsUtils.storeEncryptedValues(service, LEGACY_DELIMITER, encryptedUsername, encryptedPassword); - } catch (Exception e) { - throw new CryptoFailedException("Encryption failed for service " + service, e); - } - } - - private String encryptWithEntity(String toEncypt, Entity entity) throws KeyChainException, CryptoInitializationException, IOException { - byte[] encryptedBytes = crypto.encrypt(toEncypt.getBytes(Charset.forName("UTF-8")), entity); - return Base64.encodeToString(encryptedBytes, Base64.DEFAULT); - } - @ReactMethod public void resetGenericPasswordForOptions(String service, Promise promise) { try { - resetGenericPasswordForOptions(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 = cipherStorageMap.get(resultSet.cipherStorageName); + if (cipherStorage != null) { + cipherStorage.removeKey(service); + } + } + // And then we reset + prefsStorage.resetPassword(service); + promise.resolve(true); - } catch (KeyStoreException e) { - Log.e(KEYCHAIN_MODULE, e.getMessage()); - promise.reject(E_UNSUPPORTED_KEYSTORE, e); } catch (KeyStoreAccessException e) { Log.e(KEYCHAIN_MODULE, e.getMessage()); promise.reject(E_KEYSTORE_ACCESS_ERROR, e); } } - private void resetGenericPasswordForOptions(String service) throws KeyStoreException, KeyStoreAccessException { - service = service == null ? DEFAULT_ALIAS : service; - - KeyStore keyStore = getKeyStoreAndLoad(); - - if (keyStore.containsAlias(service)) { - keyStore.deleteEntry(service); - } - - prefsUtils.resetPassword(service, DELIMITER); - } - - private void resetGenericPasswordForOptionsLegacy(String service) throws KeyStoreException, KeyStoreAccessException { - service = service == null ? EMPTY_STRING : service; - - prefsUtils.resetPassword(service, LEGACY_DELIMITER); - } - @ReactMethod public void setInternetCredentialsForServer(@NonNull String server, String username, String password, ReadableMap unusedOptions, Promise promise) { setGenericPasswordForOptions(server, username, password, promise); @@ -375,17 +160,19 @@ public class KeychainModule extends ReactContextBaseJavaModule { resetGenericPasswordForOptions(server, promise); } - private KeyStore getKeyStore() throws KeyStoreException { - return KeyStore.getInstance(KEYSTORE_TYPE); - } - - private KeyStore getKeyStoreAndLoad() throws KeyStoreException, KeyStoreAccessException { - try { - KeyStore keyStore = getKeyStore(); - keyStore.load(null); - return keyStore; - } catch (NoSuchAlgorithmException | CertificateException | IOException e) { - throw new KeyStoreAccessException("Could not access KeyStore", e); + // The "Current" CipherStorage is the cipherStorage with the highest API level that is lower than the current API level + private CipherStorage getCipherStorageForCurrentAPILevel() { + int currentAPILevel = Build.VERSION.SDK_INT; + CipherStorage currentCipherStorage = null; + for (CipherStorage cipherStorage : cipherStorageMap.values()) { + int cipherStorageAPILevel = cipherStorage.getAPILevel(); + // Is the cipherStorage supported on the current API level? + boolean isSupported = (cipherStorageAPILevel <= currentAPILevel); + // Is the API level better than the one we previously selected (if any)? + if (isSupported && (currentCipherStorage == null || cipherStorageAPILevel > currentCipherStorage.getAPILevel())) { + currentCipherStorage = cipherStorage; + } } + return currentCipherStorage; } } \ No newline at end of file diff --git a/android/src/main/java/com/oblador/keychain/PrefsStorage.java b/android/src/main/java/com/oblador/keychain/PrefsStorage.java new file mode 100644 index 0000000..d22953f --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/PrefsStorage.java @@ -0,0 +1,90 @@ +package com.oblador.keychain; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult; + +public class PrefsStorage { + public static final String KEYCHAIN_DATA = "RN_KEYCHAIN"; + + static public class ResultSet { + public final String cipherStorageName; + public final byte[] usernameBytes; + public final byte[] passwordBytes; + + public ResultSet(String cipherStorageName, byte[] usernameBytes, byte[] passwordBytes) { + this.cipherStorageName = cipherStorageName; + this.usernameBytes = usernameBytes; + this.passwordBytes = passwordBytes; + } + } + + private final SharedPreferences prefs; + + public PrefsStorage(ReactApplicationContext reactContext) { + this.prefs = reactContext.getSharedPreferences(KEYCHAIN_DATA, Context.MODE_PRIVATE); + } + + public ResultSet getEncryptedEntry(String service) { + byte[] bytesForUsername = getBytesForUsername(service); + byte[] bytesForPassword = getBytesForPassword(service); + String cipherStorageName = getCipherStorageName(service); + if (bytesForUsername != null && bytesForPassword != null) { + return new ResultSet(cipherStorageName, bytesForUsername, bytesForPassword); + } + return null; + } + + public void resetPassword(String service) { + String keyForUsername = getKeyForUsername(service); + String keyForPassword = getKeyForPassword(service); + String keyForCipherStorage = getKeyForCipherStorage(service); + + prefs.edit().remove(keyForUsername).remove(keyForPassword).remove(keyForCipherStorage).apply(); + } + + public void storeEncryptedEntry(String service, EncryptionResult encryptionResult) { + prefs.edit().putString(getKeyForUsername(service), Base64.encodeToString(encryptionResult.username, Base64.DEFAULT)) + .putString(getKeyForPassword(service), Base64.encodeToString(encryptionResult.password, Base64.DEFAULT)) + .putString(getKeyForCipherStorage(service), encryptionResult.cipherStorage.getCipherStorageName()) + .apply(); + } + + private byte[] getBytesForUsername(String service) { + String key = getKeyForUsername(service); + return getBytes(key); + } + + private byte[] getBytesForPassword(String service) { + String key = getKeyForPassword(service); + return getBytes(key); + } + + private String getCipherStorageName(String service) { + String key = getKeyForCipherStorage(service); + return this.prefs.getString(key, null); + } + + private String getKeyForUsername(String service) { + return service + ":" + "u"; + } + + private String getKeyForPassword(String service) { + return service + ":" + "p"; + } + + private String getKeyForCipherStorage(String service) { + return service + ":" + "i"; + } + + private byte[] getBytes(String key) { + String value = this.prefs.getString(key, null); + if (value != null) { + return Base64.decode(value, Base64.DEFAULT); + } + return null; + } +} diff --git a/android/src/main/java/com/oblador/keychain/PrefsUtils.java b/android/src/main/java/com/oblador/keychain/PrefsUtils.java deleted file mode 100644 index 4ed7e50..0000000 --- a/android/src/main/java/com/oblador/keychain/PrefsUtils.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.oblador.keychain; - -import android.content.SharedPreferences; -import android.util.Base64; - -public class PrefsUtils { - static public class ResultSet { - final String service; - final byte[] usernameBytes; - final byte[] passwordBytes; - - public ResultSet(String service, byte[] usernameBytes, byte[] passwordBytes) { - this.service = service; - this.usernameBytes = usernameBytes; - this.passwordBytes = passwordBytes; - } - } - - private final SharedPreferences prefs; - - public PrefsUtils(SharedPreferences prefs) { - this.prefs = prefs; - } - - public ResultSet getBytesForUsernameAndPassword(String service, String delimiter) { - String keyForUsername = getKeyForUsername(service, delimiter); - String keyForPassword = getKeyForPassword(service, delimiter); - byte[] bytesForUsername = getBytes(keyForUsername); - byte[] bytesForPassword = getBytes(keyForPassword); - if (bytesForUsername != null && bytesForPassword != null) { - return new ResultSet(service, bytesForUsername, bytesForPassword); - } - return null; - } - - public void resetPassword(String service, String delimiter) { - SharedPreferences.Editor prefsEditor = prefs.edit(); - String keyForUsername = getKeyForUsername(service, delimiter); - String keyForPassword = getKeyForPassword(service, delimiter); - - if (prefs.contains(keyForUsername) || prefs.contains(keyForPassword)) { - prefsEditor.remove(keyForUsername); - prefsEditor.remove(keyForPassword); - prefsEditor.apply(); - } - } - - public void storeEncryptedValues(String service, String delimiter, String encryptedUsername, String encryptedPassword) { - SharedPreferences.Editor prefsEditor = prefs.edit(); - prefsEditor.putString(getKeyForUsername(service, delimiter), encryptedUsername); - prefsEditor.putString(getKeyForPassword(service, delimiter), encryptedPassword); - prefsEditor.apply(); - } - - private byte[] getBytesForUsername(String service, String delimiter) { - String key = getKeyForUsername(service, delimiter); - return getBytes(key); - } - - private byte[] getBytesForPassword(String service, String delimiter) { - String key = getKeyForPassword(service, delimiter); - return getBytes(key); - } - - private String getKeyForUsername(String service, String delimiter) { - return service + delimiter + "u"; - } - - private String getKeyForPassword(String service, String delimiter) { - return service + delimiter + "p"; - } - - private byte[] getBytes(String key) { - String value = this.prefs.getString(key, null); - if (value != null) { - return Base64.decode(value, Base64.DEFAULT); - } - return null; - } -} diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.java b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.java new file mode 100644 index 0000000..bb76944 --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorage.java @@ -0,0 +1,41 @@ +package com.oblador.keychain.cipherStorage; + +import com.oblador.keychain.exceptions.CryptoFailedException; +import com.oblador.keychain.exceptions.KeyStoreAccessException; + +public interface CipherStorage { + abstract class CipherResult { + public final T username; + public final T password; + + public CipherResult(T username, T password) { + this.username = username; + this.password = password; + } + } + + class EncryptionResult extends CipherResult { + public CipherStorage cipherStorage; + + public EncryptionResult(byte[] username, byte[] password, CipherStorage cipherStorage) { + super(username, password); + this.cipherStorage = cipherStorage; + } + } + + class DecryptionResult extends CipherResult { + public DecryptionResult(String username, String password) { + super(username, password); + } + } + + EncryptionResult encrypt(String service, String username, String password) throws CryptoFailedException; + + DecryptionResult decrypt(String service, byte[] username, byte[] password) throws CryptoFailedException; + + void removeKey(String service) throws KeyStoreAccessException; + + String getCipherStorageName(); + + int getAPILevel(); +} diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java new file mode 100644 index 0000000..9a706d6 --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java @@ -0,0 +1,96 @@ +package com.oblador.keychain.cipherStorage; + +import android.os.Build; + +import com.facebook.android.crypto.keychain.AndroidConceal; +import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain; +import com.facebook.crypto.Crypto; +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.exceptions.CryptoFailedException; + +import java.nio.charset.Charset; + +public class CipherStorageFacebookConceal implements CipherStorage { + public static final String CIPHER_STORAGE_NAME = "FacebookConceal"; + public static final String KEYCHAIN_DATA = "RN_KEYCHAIN"; + public static final String EMPTY_STRING = ""; + private final Crypto crypto; + + public CipherStorageFacebookConceal(ReactApplicationContext reactContext) { + KeyChain keyChain = new SharedPrefsBackedKeyChain(reactContext, CryptoConfig.KEY_256); + this.crypto = AndroidConceal.get().createDefaultCrypto(keyChain); + } + + @Override + public String getCipherStorageName() { + return CIPHER_STORAGE_NAME; + } + + @Override + public int getAPILevel() { + return Build.VERSION_CODES.JELLY_BEAN; + } + + @Override + public EncryptionResult encrypt(String service, String username, String password) throws CryptoFailedException { + if (!crypto.isAvailable()) { + throw new CryptoFailedException("Crypto is missing"); + } + service = service == null ? EMPTY_STRING : service; + + Entity usernameEntity = createUsernameEntity(service); + Entity passwordEntity = createPasswordEntity(service); + + try { + byte[] encryptedUsername = crypto.encrypt(username.getBytes(Charset.forName("UTF-8")), usernameEntity); + byte[] encryptedPassword = crypto.encrypt(password.getBytes(Charset.forName("UTF-8")), passwordEntity); + + return new EncryptionResult(encryptedUsername, encryptedPassword, this); + } catch (Exception e) { + throw new CryptoFailedException("Encryption failed for service " + service, e); + } + } + + @Override + public DecryptionResult decrypt(String service, byte[] username, byte[] password) throws CryptoFailedException { + if (!crypto.isAvailable()) { + throw new CryptoFailedException("Crypto is missing"); + } + service = service == null ? EMPTY_STRING : service; + + Entity usernameEntity = createUsernameEntity(service); + Entity passwordEntity = createPasswordEntity(service); + + try { + byte[] decryptedUsername = crypto.decrypt(username, usernameEntity); + byte[] decryptedPassword = crypto.decrypt(password, passwordEntity); + + return new DecryptionResult( + new String(decryptedUsername, Charset.forName("UTF-8")), + new String(decryptedPassword, Charset.forName("UTF-8"))); + } catch (Exception e) { + throw new CryptoFailedException("Decryption failed for service " + service, e); + } + } + + @Override + public void removeKey(String service) { + // Facebook Conceal stores only one key across all services, so we cannot delete the key (otherwise decryption will fail for encrypted data of other services). + } + private Entity createUsernameEntity(String service) { + String prefix = getEntityPrefix(service); + return Entity.create(prefix + "user"); + } + + private Entity createPasswordEntity(String service) { + String prefix = getEntityPrefix(service); + return Entity.create(prefix + "pass"); + } + + private String getEntityPrefix(String service) { + return KEYCHAIN_DATA + ":" + service; + } +} diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAESCBC.java b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAESCBC.java new file mode 100644 index 0000000..bbe375f --- /dev/null +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAESCBC.java @@ -0,0 +1,186 @@ +package com.oblador.keychain.cipherStorage; + +import android.annotation.TargetApi; +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import com.oblador.keychain.exceptions.CryptoFailedException; +import com.oblador.keychain.exceptions.KeyStoreAccessException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.IvParameterSpec; + +public class CipherStorageKeystoreAESCBC implements CipherStorage { + public static final String CIPHER_STORAGE_NAME = "KeystoreAESCBC"; + public static final String DEFAULT_ALIAS = "RN_KEYCHAIN_DEFAULT_ALIAS"; + public static final String KEYSTORE_TYPE = "AndroidKeyStore"; + public static final String ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES; + public static final String ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC; + public static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7; + public static final String ENCRYPTION_TRANSFORMATION = + ENCRYPTION_ALGORITHM + "/" + + ENCRYPTION_BLOCK_MODE + "/" + + ENCRYPTION_PADDING; + public static final int ENCRYPTION_KEY_SIZE = 256; + + @Override + public String getCipherStorageName() { + return CIPHER_STORAGE_NAME; + } + + @Override + public int getAPILevel() { + return Build.VERSION_CODES.M; + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public EncryptionResult encrypt(String service, String username, String password) throws CryptoFailedException { + service = service == null ? DEFAULT_ALIAS : service; + + try { + KeyStore keyStore = getKeyStoreAndLoad(); + + if (!keyStore.containsAlias(service)) { + AlgorithmParameterSpec spec; + spec = new KeyGenParameterSpec.Builder( + service, + KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) + .setBlockModes(ENCRYPTION_BLOCK_MODE) + .setEncryptionPaddings(ENCRYPTION_PADDING) + .setRandomizedEncryptionRequired(true) + //.setUserAuthenticationRequired(true) // Will throw InvalidAlgorithmParameterException if there is no fingerprint enrolled on the device + .setKeySize(ENCRYPTION_KEY_SIZE) + .build(); + + KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_TYPE); + generator.init(spec); + + generator.generateKey(); + } + + Key key = keyStore.getKey(service, null); + + byte[] encryptedUsername = encryptString(key, service, username); + byte[] encryptedPassword = encryptString(key, service, password); + + return new EncryptionResult(encryptedUsername, encryptedPassword, this); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException | UnrecoverableKeyException e) { + throw new CryptoFailedException("Could not encrypt data for service " + service, e); + } catch (KeyStoreException | KeyStoreAccessException e) { + throw new CryptoFailedException("Could not access Keystore for service " + service, e); + } + } + + @Override + public DecryptionResult decrypt(String service, byte[] username, byte[] password) throws CryptoFailedException { + service = service == null ? DEFAULT_ALIAS : service; + + try { + KeyStore keyStore = getKeyStoreAndLoad(); + + Key key = keyStore.getKey(service, null); + + String decryptedUsername = decryptBytes(key, username); + String decryptedPassword = decryptBytes(key, password); + + return new DecryptionResult(decryptedUsername, decryptedPassword); + } catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException e) { + throw new CryptoFailedException("Could not get key from Keystore", e); + } catch (KeyStoreAccessException e) { + throw new CryptoFailedException("Could not access Keystore", e); + } + } + + @Override + public void removeKey(String service) throws KeyStoreAccessException { + service = service == null ? DEFAULT_ALIAS : service; + + try { + KeyStore keyStore = getKeyStoreAndLoad(); + + if (keyStore.containsAlias(service)) { + keyStore.deleteEntry(service); + } + } catch (KeyStoreException e) { + throw new KeyStoreAccessException("Failed to access Keystore", e); + } + } + + private byte[] encryptString(Key key, String service, String value) throws CryptoFailedException { + try { + Cipher cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, key); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + // write initialization vector to the beginning of the stream + byte[] iv = cipher.getIV(); + outputStream.write(iv, 0, iv.length); + // encrypt the value using a CipherOutputStream + CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); + cipherOutputStream.write(value.getBytes("UTF-8")); + cipherOutputStream.close(); + return outputStream.toByteArray(); + } catch (Exception e) { + throw new CryptoFailedException("Could not encrypt value for service " + service, e); + } + } + + private String decryptBytes(Key key, byte[] bytes) throws CryptoFailedException { + try { + Cipher cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + // read the initialization vector from the beginning of the stream + IvParameterSpec ivParams = readIvFromStream(inputStream); + cipher.init(Cipher.DECRYPT_MODE, key, ivParams); + // decrypt the bytes using a CipherInputStream + CipherInputStream cipherInputStream = new CipherInputStream( + inputStream, cipher); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + while (true) { + int n = cipherInputStream.read(buffer, 0, buffer.length); + if (n <= 0) { + break; + } + output.write(buffer, 0, n); + } + return new String(output.toByteArray(), Charset.forName("UTF-8")); + } catch (Exception e) { + throw new CryptoFailedException("Could not decrypt bytes", e); + } + } + + private IvParameterSpec readIvFromStream(ByteArrayInputStream inputStream) { + byte[] iv = new byte[16]; + inputStream.read(iv, 0, iv.length); + return new IvParameterSpec(iv); + } + + private KeyStore getKeyStoreAndLoad() throws KeyStoreException, KeyStoreAccessException { + try { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + keyStore.load(null); + return keyStore; + } catch (NoSuchAlgorithmException | CertificateException | IOException e) { + throw new KeyStoreAccessException("Could not access Keystore", e); + } + } +}