Merge pull request #67 from TeletronicsDotAe/feature/change_to_android_keystore
use KeyStore on Android >= 6.0
This commit is contained in:
commit
c4b2b4dcfe
|
@ -31,5 +31,5 @@ repositories {
|
|||
|
||||
dependencies {
|
||||
compile 'com.facebook.react:react-native:0.19.+'
|
||||
compile 'com.facebook.conceal:conceal:1.1.2@aar'
|
||||
compile 'com.facebook.conceal:conceal:1.1.3@aar'
|
||||
}
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
package com.oblador.keychain;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
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.keychain.KeyChain;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
@ -18,17 +11,29 @@ 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.PrefsStorage.ResultSet;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage.DecryptionResult;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAESCBC;
|
||||
import com.oblador.keychain.exceptions.CryptoFailedException;
|
||||
import com.oblador.keychain.exceptions.EmptyParameterException;
|
||||
import com.oblador.keychain.exceptions.KeyStoreAccessException;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
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_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 = "";
|
||||
|
||||
private final Crypto crypto;
|
||||
private final SharedPreferences prefs;
|
||||
private final Map<String, CipherStorage> cipherStorageMap = new HashMap<>();
|
||||
private final PrefsStorage prefsStorage;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
|
@ -37,99 +42,108 @@ 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);
|
||||
addCipherStorageToMap(new CipherStorageFacebookConceal(reactContext));
|
||||
addCipherStorageToMap(new CipherStorageKeystoreAESCBC());
|
||||
}
|
||||
|
||||
private void addCipherStorageToMap(CipherStorage cipherStorage) {
|
||||
cipherStorageMap.put(cipherStorage.getCipherStorageName(), cipherStorage);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setGenericPasswordForOptions(String service, String username, String password, Promise promise) {
|
||||
if (!crypto.isAvailable()) {
|
||||
Log.e(KEYCHAIN_MODULE, "Crypto is missing");
|
||||
promise.reject("KeychainModule: crypto is missing");
|
||||
return;
|
||||
}
|
||||
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
|
||||
Log.e(KEYCHAIN_MODULE, "you passed empty or null username/password");
|
||||
promise.reject("KeychainModule: you passed empty or null username/password");
|
||||
return;
|
||||
}
|
||||
service = service == null ? EMPTY_STRING : service;
|
||||
//Log.d("Crypto", service + username + password);
|
||||
|
||||
Entity userentity = Entity.create(KEYCHAIN_DATA + ":" + service + "user");
|
||||
Entity pwentity = Entity.create(KEYCHAIN_DATA + ":" + service + "pass");
|
||||
|
||||
|
||||
String encryptedUsername = encryptWithEntity(username, userentity, promise);
|
||||
String encryptedPassword = encryptWithEntity(password, pwentity, promise);
|
||||
|
||||
SharedPreferences.Editor prefsEditor = prefs.edit();
|
||||
prefsEditor.putString(service + ":u", encryptedUsername);
|
||||
prefsEditor.putString(service + ":p", encryptedPassword);
|
||||
prefsEditor.apply();
|
||||
Log.d(KEYCHAIN_MODULE, "saved the data");
|
||||
promise.resolve("KeychainModule saved the data");
|
||||
}
|
||||
|
||||
private String encryptWithEntity(String toEncypt, Entity entity, Promise promise) {
|
||||
try {
|
||||
byte[] encryptedBytes = crypto.encrypt(toEncypt.getBytes(Charset.forName("UTF-8")), entity);
|
||||
return Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
|
||||
} catch (Exception e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getLocalizedMessage());
|
||||
promise.reject(e.getLocalizedMessage(), e);
|
||||
return null;
|
||||
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
|
||||
throw new EmptyParameterException("you passed empty or null username/password");
|
||||
}
|
||||
service = getDefaultServiceIfNull(service);
|
||||
|
||||
CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel();
|
||||
|
||||
EncryptionResult result = currentCipherStorage.encrypt(service, username, password);
|
||||
prefsStorage.storeEncryptedEntry(service, result);
|
||||
|
||||
promise.resolve("KeychainModule saved the data");
|
||||
} catch (EmptyParameterException e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getMessage());
|
||||
promise.reject(E_EMPTY_PARAMETERS, e);
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getMessage());
|
||||
promise.reject(E_CRYPTO_FAILED, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getGenericPasswordForOptions(String service, Promise promise) {
|
||||
service = service == null ? EMPTY_STRING : service;
|
||||
|
||||
String username = prefs.getString(service + ":u", "user_not_found");
|
||||
String password = prefs.getString(service + ":p", "pass_not_found");
|
||||
if (username.equals("user_not_found") || password.equals("pass_not_found")) {
|
||||
Log.e(KEYCHAIN_MODULE, "no keychain entry found for service: " + service);
|
||||
promise.resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] recuser = Base64.decode(username, Base64.DEFAULT);
|
||||
byte[] recpass = Base64.decode(password, Base64.DEFAULT);
|
||||
|
||||
Entity userentity = Entity.create(KEYCHAIN_DATA + ":" + service + "user");
|
||||
Entity pwentity = Entity.create(KEYCHAIN_DATA + ":" + service + "pass");
|
||||
|
||||
try {
|
||||
byte[] decryptedUsername = crypto.decrypt(recuser, userentity);
|
||||
byte[] decryptedPass = crypto.decrypt(recpass, pwentity);
|
||||
service = getDefaultServiceIfNull(service);
|
||||
|
||||
CipherStorage currentCipherStorage = getCipherStorageForCurrentAPILevel();
|
||||
|
||||
final DecryptionResult decryptionResult;
|
||||
ResultSet resultSet = prefsStorage.getEncryptedEntry(service);
|
||||
if (resultSet == null) {
|
||||
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 first, then encrypt it using the current CipherStorage, then store it again and return
|
||||
CipherStorage oldCipherStorage = getCipherStorageByName(resultSet.cipherStorageName);
|
||||
// decrypt using the older cipher storage
|
||||
decryptionResult = 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", service);
|
||||
credentials.putString("username", new String(decryptedUsername, Charset.forName("UTF-8")));
|
||||
credentials.putString("password", new String(decryptedPass, Charset.forName("UTF-8")));
|
||||
credentials.putString("username", decryptionResult.username);
|
||||
credentials.putString("password", decryptionResult.password);
|
||||
|
||||
promise.resolve(credentials);
|
||||
} catch (Exception e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getLocalizedMessage());
|
||||
promise.reject(e.getLocalizedMessage(), e);
|
||||
} catch (KeyStoreAccessException e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getMessage());
|
||||
promise.reject(E_KEYSTORE_ACCESS_ERROR, e);
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getMessage());
|
||||
promise.reject(E_CRYPTO_FAILED, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void resetGenericPasswordForOptions(String service, Promise promise) {
|
||||
service = service == null ? EMPTY_STRING : service;
|
||||
SharedPreferences.Editor prefsEditor = prefs.edit();
|
||||
try {
|
||||
service = getDefaultServiceIfNull(service);
|
||||
|
||||
if (prefs.contains(service + ":u")) {
|
||||
prefsEditor.remove(service + ":u");
|
||||
prefsEditor.remove(service + ":p");
|
||||
prefsEditor.apply();
|
||||
// First we clean up the cipher storage (using the cipher storage that was used to store the entry)
|
||||
ResultSet resultSet = prefsStorage.getEncryptedEntry(service);
|
||||
if (resultSet != null) {
|
||||
CipherStorage cipherStorage = getCipherStorageByName(resultSet.cipherStorageName);
|
||||
if (cipherStorage != null) {
|
||||
cipherStorage.removeKey(service);
|
||||
}
|
||||
}
|
||||
// And then we remove the entry in the shared preferences
|
||||
prefsStorage.removeEntry(service);
|
||||
|
||||
promise.resolve(true);
|
||||
} catch (KeyStoreAccessException e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getMessage());
|
||||
promise.reject(E_KEYSTORE_ACCESS_ERROR, e);
|
||||
}
|
||||
promise.resolve(true);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
|
@ -147,5 +161,31 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
resetGenericPasswordForOptions(server, promise);
|
||||
}
|
||||
|
||||
// The "Current" CipherStorage is the cipherStorage with the highest API level that is lower than or equal to the current API level
|
||||
private CipherStorage getCipherStorageForCurrentAPILevel() throws CryptoFailedException {
|
||||
int currentAPILevel = Build.VERSION.SDK_INT;
|
||||
CipherStorage currentCipherStorage = null;
|
||||
for (CipherStorage cipherStorage : cipherStorageMap.values()) {
|
||||
int cipherStorageAPILevel = cipherStorage.getMinSupportedApiLevel();
|
||||
// Is the cipherStorage supported on the current API level?
|
||||
boolean isSupported = (cipherStorageAPILevel <= currentAPILevel);
|
||||
// Is the API level better than the one we previously selected (if any)?
|
||||
if (isSupported && (currentCipherStorage == null || cipherStorageAPILevel > currentCipherStorage.getMinSupportedApiLevel())) {
|
||||
currentCipherStorage = cipherStorage;
|
||||
}
|
||||
}
|
||||
if (currentCipherStorage == null) {
|
||||
throw new CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT);
|
||||
}
|
||||
return currentCipherStorage;
|
||||
}
|
||||
|
||||
}
|
||||
private CipherStorage getCipherStorageByName(String cipherStorageName) {
|
||||
return cipherStorageMap.get(cipherStorageName);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String getDefaultServiceIfNull(String service) {
|
||||
return service == null ? EMPTY_STRING : service;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class KeychainPackage implements ReactPackage {
|
||||
|
||||
public KeychainPackage() {
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
package com.oblador.keychain;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
|
||||
|
||||
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(@NonNull String service) {
|
||||
byte[] bytesForUsername = getBytesForUsername(service);
|
||||
byte[] bytesForPassword = getBytesForPassword(service);
|
||||
String cipherStorageName = getCipherStorageName(service);
|
||||
if (bytesForUsername != null && bytesForPassword != null) {
|
||||
if (cipherStorageName == null) {
|
||||
// If the CipherStorage name is not found, we assume it is because the entry was written by an older version of this library. The older version used Facebook Conceal, so we default to that.
|
||||
cipherStorageName = CipherStorageFacebookConceal.CIPHER_STORAGE_NAME;
|
||||
}
|
||||
return new ResultSet(cipherStorageName, bytesForUsername, bytesForPassword);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void removeEntry(@NonNull 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(@NonNull String service, @NonNull EncryptionResult encryptionResult) {
|
||||
String keyForUsername = getKeyForUsername(service);
|
||||
String keyForPassword = getKeyForPassword(service);
|
||||
String keyForCipherStorage = getKeyForCipherStorage(service);
|
||||
|
||||
prefs.edit()
|
||||
.putString(keyForUsername, Base64.encodeToString(encryptionResult.username, Base64.DEFAULT))
|
||||
.putString(keyForPassword, Base64.encodeToString(encryptionResult.password, Base64.DEFAULT))
|
||||
.putString(keyForCipherStorage, 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 + ":" + "c";
|
||||
}
|
||||
|
||||
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,43 @@
|
|||
package com.oblador.keychain.cipherStorage;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
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(@NonNull String service, @NonNull String username, @NonNull String password) throws CryptoFailedException;
|
||||
|
||||
DecryptionResult decrypt(@NonNull String service, @NonNull byte[] username, @NonNull byte[] password) throws CryptoFailedException;
|
||||
|
||||
void removeKey(@NonNull String service) throws KeyStoreAccessException;
|
||||
|
||||
String getCipherStorageName();
|
||||
|
||||
int getMinSupportedApiLevel();
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package com.oblador.keychain.cipherStorage;
|
||||
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
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";
|
||||
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 getMinSupportedApiLevel() {
|
||||
return Build.VERSION_CODES.JELLY_BEAN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password) throws CryptoFailedException {
|
||||
if (!crypto.isAvailable()) {
|
||||
throw new CryptoFailedException("Crypto is missing");
|
||||
}
|
||||
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(@NonNull String service, @NonNull byte[] username, @NonNull byte[] password) throws CryptoFailedException {
|
||||
if (!crypto.isAvailable()) {
|
||||
throw new CryptoFailedException("Crypto is missing");
|
||||
}
|
||||
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(@NonNull 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,192 @@
|
|||
package com.oblador.keychain.cipherStorage;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
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_SERVICE = "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 getMinSupportedApiLevel() {
|
||||
return Build.VERSION_CODES.M;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password) throws CryptoFailedException {
|
||||
service = getDefaultServiceIfEmpty(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(@NonNull String service, @NonNull byte[] username, @NonNull byte[] password) throws CryptoFailedException {
|
||||
service = getDefaultServiceIfEmpty(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(@NonNull String service) throws KeyStoreAccessException {
|
||||
service = getDefaultServiceIfEmpty(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);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String getDefaultServiceIfEmpty(@NonNull String service) {
|
||||
return service.isEmpty() ? DEFAULT_SERVICE : service;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.oblador.keychain.exceptions;
|
||||
|
||||
public class CryptoFailedException extends Exception {
|
||||
public CryptoFailedException (String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CryptoFailedException (String message, Throwable t) {
|
||||
super(message, t);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.oblador.keychain.exceptions;
|
||||
|
||||
public class EmptyParameterException extends Exception {
|
||||
public EmptyParameterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.oblador.keychain.exceptions;
|
||||
|
||||
public class KeyStoreAccessException extends Exception {
|
||||
public KeyStoreAccessException(String message, Throwable t) {
|
||||
super(message, t);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue