Refactored implementation to support various encryption algorithms and key storage depending on API level.

This commit is contained in:
Pelle Stenild Coltau 2017-06-18 11:38:00 +04:00
parent 808a7000da
commit 32c5caff39
6 changed files with 490 additions and 370 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}