Added support for API levels lower than 23 using Facebook’s conceal.
This commit is contained in:
parent
025aab835a
commit
808a7000da
|
@ -1,5 +1,6 @@
|
|||
package com.oblador.keychain;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
@ -14,6 +15,8 @@ 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;
|
||||
|
@ -22,6 +25,7 @@ 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.exceptions.CryptoFailedException;
|
||||
import com.oblador.keychain.exceptions.EmptyParameterException;
|
||||
import com.oblador.keychain.exceptions.KeyStoreAccessException;
|
||||
|
@ -73,18 +77,6 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
private final SharedPreferences prefs;
|
||||
private final PrefsUtils prefsUtils;
|
||||
|
||||
private class ResultSet {
|
||||
final String service;
|
||||
final byte[] decryptedUsername;
|
||||
final byte[] decryptedPassword;
|
||||
|
||||
public ResultSet(String service, byte[] decryptedUsername, byte[] decryptedPassword) {
|
||||
this.service = service;
|
||||
this.decryptedUsername = decryptedUsername;
|
||||
this.decryptedPassword = decryptedPassword;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return KEYCHAIN_MODULE;
|
||||
|
@ -102,10 +94,18 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
@ReactMethod
|
||||
public void setGenericPasswordForOptions(String service, String username, String password, Promise promise) {
|
||||
try {
|
||||
setGenericPasswordForOptions(service, username, password);
|
||||
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);
|
||||
|
||||
// Clean legacy values (if any)
|
||||
resetGenericPasswordForOptionsLegacy(service);
|
||||
// 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());
|
||||
|
@ -122,10 +122,8 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
}
|
||||
|
||||
private void setGenericPasswordForOptions(String service, String username, String password) throws EmptyParameterException, CryptoFailedException, KeyStoreException, KeyStoreAccessException {
|
||||
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
|
||||
throw new EmptyParameterException("you passed empty or null username/password");
|
||||
}
|
||||
@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();
|
||||
|
@ -133,7 +131,6 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
try {
|
||||
if (!keyStore.containsAlias(service)) {
|
||||
AlgorithmParameterSpec spec;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
spec = new KeyGenParameterSpec.Builder(
|
||||
service,
|
||||
KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
|
||||
|
@ -143,9 +140,6 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
//.setUserAuthenticationRequired(true) // Will throw InvalidAlgorithmParameterException if there is no fingerprint enrolled on the device
|
||||
.setKeySize(ENCRYPTION_KEY_SIZE)
|
||||
.build();
|
||||
} else {
|
||||
throw new CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT);
|
||||
}
|
||||
|
||||
KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_TYPE);
|
||||
generator.init(spec);
|
||||
|
@ -187,47 +181,24 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
|
||||
@ReactMethod
|
||||
public void getGenericPasswordForOptions(String service, Promise promise) {
|
||||
String originalService = service;
|
||||
service = service == null ? DEFAULT_ALIAS : service;
|
||||
|
||||
try {
|
||||
final byte[] decryptedUsername;
|
||||
final byte[] decryptedPassword;
|
||||
byte[] recuser = prefsUtils.getBytesForUsername(service, DELIMITER);
|
||||
byte[] recpass = prefsUtils.getBytesForPassword(service, DELIMITER);
|
||||
if (recuser == null || recpass == null) {
|
||||
// Check if the values are stored using the LEGACY_DELIMITER and thus encrypted using FaceBook's Conceal
|
||||
ResultSet resultSet = getGenericPasswordForOptionsUsingConceal(originalService);
|
||||
if (resultSet != null) {
|
||||
// Store the values using the new delimiter and the KeyStore
|
||||
setGenericPasswordForOptions(
|
||||
originalService,
|
||||
new String(resultSet.decryptedUsername, Charset.forName("UTF-8")),
|
||||
new String(resultSet.decryptedPassword, Charset.forName("UTF-8")));
|
||||
// Remove the legacy value(s)
|
||||
resetGenericPasswordForOptionsLegacy(originalService);
|
||||
decryptedUsername = resultSet.decryptedUsername;
|
||||
decryptedPassword = resultSet.decryptedPassword;
|
||||
} else {
|
||||
Log.e(KEYCHAIN_MODULE, "no keychain entry found for service: " + service);
|
||||
promise.resolve(false);
|
||||
return;
|
||||
}
|
||||
final ResultSet resultSet;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
resultSet = getGenericPasswordForOptions(service);
|
||||
}
|
||||
else {
|
||||
KeyStore keyStore = getKeyStoreAndLoad();
|
||||
|
||||
Key key = keyStore.getKey(service, null);
|
||||
|
||||
decryptedUsername = decryptBytes(key, recuser);
|
||||
decryptedPassword = decryptBytes(key, recpass);
|
||||
resultSet = getGenericPasswordForOptionsUsingConceal(service);
|
||||
}
|
||||
if (resultSet == null) {
|
||||
Log.e(KEYCHAIN_MODULE, "no keychain entry found for service: " + service);
|
||||
promise.resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap credentials = Arguments.createMap();
|
||||
|
||||
credentials.putString("service", service);
|
||||
credentials.putString("username", new String(decryptedUsername, Charset.forName("UTF-8")));
|
||||
credentials.putString("password", new String(decryptedPassword, Charset.forName("UTF-8")));
|
||||
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")));
|
||||
|
||||
promise.resolve(credentials);
|
||||
} catch (KeyStoreException e) {
|
||||
|
@ -239,12 +210,45 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
} catch (UnrecoverableKeyException | NoSuchAlgorithmException | CryptoFailedException e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getMessage());
|
||||
promise.reject(E_CRYPTO_FAILED, e);
|
||||
} catch (EmptyParameterException e) {
|
||||
Log.e(KEYCHAIN_MODULE, e.getMessage());
|
||||
promise.reject(E_EMPTY_PARAMETERS, 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);
|
||||
|
@ -282,9 +286,8 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
service = service == null ? EMPTY_STRING : service;
|
||||
|
||||
byte[] recuser = prefsUtils.getBytesForUsername(service, LEGACY_DELIMITER);
|
||||
byte[] recpass = prefsUtils.getBytesForPassword(service, LEGACY_DELIMITER);
|
||||
if (recuser == null || recpass == null) {
|
||||
ResultSet legacyResultSet = prefsUtils.getBytesForUsernameAndPassword(service, LEGACY_DELIMITER);
|
||||
if (legacyResultSet == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -292,8 +295,8 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
Entity pwentity = Entity.create(KEYCHAIN_DATA + ":" + service + "pass");
|
||||
|
||||
try {
|
||||
byte[] decryptedUsername = crypto.decrypt(recuser, userentity);
|
||||
byte[] decryptedPassword = crypto.decrypt(recpass, pwentity);
|
||||
byte[] decryptedUsername = crypto.decrypt(legacyResultSet.usernameBytes, userentity);
|
||||
byte[] decryptedPassword = crypto.decrypt(legacyResultSet.passwordBytes, pwentity);
|
||||
|
||||
return new ResultSet(service, decryptedUsername, decryptedPassword);
|
||||
} catch (Exception e) {
|
||||
|
@ -301,6 +304,30 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -10,7 +10,6 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class KeychainPackage implements ReactPackage {
|
||||
|
||||
public KeychainPackage() {
|
||||
|
|
|
@ -3,25 +3,34 @@ package com.oblador.keychain;
|
|||
import android.content.SharedPreferences;
|
||||
import android.util.Base64;
|
||||
|
||||
/**
|
||||
* Created by pcoltau on 6/15/17.
|
||||
*/
|
||||
|
||||
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 byte[] getBytesForUsername(String service, String delimiter) {
|
||||
String key = getKeyForUsername(service, delimiter);
|
||||
return getBytes(key);
|
||||
}
|
||||
|
||||
public byte[] getBytesForPassword(String service, String delimiter) {
|
||||
String key = getKeyForPassword(service, delimiter);
|
||||
return getBytes(key);
|
||||
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) {
|
||||
|
@ -43,6 +52,16 @@ public class PrefsUtils {
|
|||
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";
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue