Refactored implementation to support various encryption algorithms and key storage depending on API level.
This commit is contained in:
parent
808a7000da
commit
32c5caff39
|
@ -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<String, CipherStorage> 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
public final T username;
|
||||
public final T password;
|
||||
|
||||
public CipherResult(T username, T password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
class EncryptionResult extends CipherResult<byte[]> {
|
||||
public CipherStorage cipherStorage;
|
||||
|
||||
public EncryptionResult(byte[] username, byte[] password, CipherStorage cipherStorage) {
|
||||
super(username, password);
|
||||
this.cipherStorage = cipherStorage;
|
||||
}
|
||||
}
|
||||
|
||||
class DecryptionResult extends CipherResult<String> {
|
||||
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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue