Skip to content

Commit

Permalink
Merge pull request oblador#67 from TeletronicsDotAe/feature/change_to…
Browse files Browse the repository at this point in the history
…_android_keystore

use KeyStore on Android >= 6.0
  • Loading branch information
vonovak authored Jun 30, 2017
2 parents d3282cd + ef9b525 commit c4b2b4d
Show file tree
Hide file tree
Showing 10 changed files with 582 additions and 86 deletions.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
208 changes: 124 additions & 84 deletions android/src/main/java/com/oblador/keychain/KeychainModule.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
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;
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 java.nio.charset.Charset;
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.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() {
Expand All @@ -37,99 +42,108 @@ public String getName() {

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

@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 void addCipherStorageToMap(CipherStorage cipherStorage) {
cipherStorageMap.put(cipherStorage.getCipherStorageName(), cipherStorage);
}

private String encryptWithEntity(String toEncypt, Entity entity, Promise promise) {
@ReactMethod
public void setGenericPasswordForOptions(String service, String username, String password, 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();

if (prefs.contains(service + ":u")) {
prefsEditor.remove(service + ":u");
prefsEditor.remove(service + ":p");
prefsEditor.apply();
try {
service = getDefaultServiceIfNull(service);

// First we clean up the cipher storage (using the cipher storage that was used to store the entry)
ResultSet resultSet = prefsStorage.getEncryptedEntry(service);
if (resultSet != null) {
CipherStorage cipherStorage = getCipherStorageByName(resultSet.cipherStorageName);
if (cipherStorage != null) {
cipherStorage.removeKey(service);
}
}
// And then we remove the entry in the shared preferences
prefsStorage.removeEntry(service);

promise.resolve(true);
} catch (KeyStoreAccessException e) {
Log.e(KEYCHAIN_MODULE, e.getMessage());
promise.reject(E_KEYSTORE_ACCESS_ERROR, e);
}
promise.resolve(true);
}

@ReactMethod
Expand All @@ -147,5 +161,31 @@ public void resetInternetCredentialsForServer(@NonNull String server, ReadableMa
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.util.Collections;
import java.util.List;


public class KeychainPackage implements ReactPackage {

public KeychainPackage() {
Expand Down
104 changes: 104 additions & 0 deletions android/src/main/java/com/oblador/keychain/PrefsStorage.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit c4b2b4d

Please sign in to comment.