Added support for API levels lower than 23 using Facebook’s conceal.

This commit is contained in:
Pelle Stenild Coltau 2017-06-15 16:20:33 +04:00
parent 025aab835a
commit 808a7000da
3 changed files with 124 additions and 79 deletions

View File

@ -1,5 +1,6 @@
package com.oblador.keychain; package com.oblador.keychain;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
@ -14,6 +15,8 @@ import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain;
import com.facebook.crypto.Crypto; import com.facebook.crypto.Crypto;
import com.facebook.crypto.CryptoConfig; import com.facebook.crypto.CryptoConfig;
import com.facebook.crypto.Entity; 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.crypto.keychain.KeyChain;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise; 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.ReactMethod;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.oblador.keychain.PrefsUtils.ResultSet;
import com.oblador.keychain.exceptions.CryptoFailedException; import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.EmptyParameterException; import com.oblador.keychain.exceptions.EmptyParameterException;
import com.oblador.keychain.exceptions.KeyStoreAccessException; import com.oblador.keychain.exceptions.KeyStoreAccessException;
@ -73,18 +77,6 @@ public class KeychainModule extends ReactContextBaseJavaModule {
private final SharedPreferences prefs; private final SharedPreferences prefs;
private final PrefsUtils prefsUtils; 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 @Override
public String getName() { public String getName() {
return KEYCHAIN_MODULE; return KEYCHAIN_MODULE;
@ -102,10 +94,18 @@ public class KeychainModule extends ReactContextBaseJavaModule {
@ReactMethod @ReactMethod
public void setGenericPasswordForOptions(String service, String username, String password, Promise promise) { public void setGenericPasswordForOptions(String service, String username, String password, Promise promise) {
try { 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) // Clean legacy values (if any)
resetGenericPasswordForOptionsLegacy(service); resetGenericPasswordForOptionsLegacy(service);
}
else {
setGenericPasswordForOptionsUsingConceal(service, username, password);
}
promise.resolve("KeychainModule saved the data"); promise.resolve("KeychainModule saved the data");
} catch (EmptyParameterException e) { } catch (EmptyParameterException e) {
Log.e(KEYCHAIN_MODULE, e.getMessage()); 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 { @TargetApi(Build.VERSION_CODES.M)
if (username == null || username.isEmpty() || password == null || password.isEmpty()) { private void setGenericPasswordForOptions(String service, String username, String password) throws CryptoFailedException, KeyStoreException, KeyStoreAccessException {
throw new EmptyParameterException("you passed empty or null username/password");
}
service = service == null ? DEFAULT_ALIAS : service; service = service == null ? DEFAULT_ALIAS : service;
KeyStore keyStore = getKeyStoreAndLoad(); KeyStore keyStore = getKeyStoreAndLoad();
@ -133,7 +131,6 @@ public class KeychainModule extends ReactContextBaseJavaModule {
try { try {
if (!keyStore.containsAlias(service)) { if (!keyStore.containsAlias(service)) {
AlgorithmParameterSpec spec; AlgorithmParameterSpec spec;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
spec = new KeyGenParameterSpec.Builder( spec = new KeyGenParameterSpec.Builder(
service, service,
KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) 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 //.setUserAuthenticationRequired(true) // Will throw InvalidAlgorithmParameterException if there is no fingerprint enrolled on the device
.setKeySize(ENCRYPTION_KEY_SIZE) .setKeySize(ENCRYPTION_KEY_SIZE)
.build(); .build();
} else {
throw new CryptoFailedException("Unsupported Android SDK " + Build.VERSION.SDK_INT);
}
KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_TYPE); KeyGenerator generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_TYPE);
generator.init(spec); generator.init(spec);
@ -187,47 +181,24 @@ public class KeychainModule extends ReactContextBaseJavaModule {
@ReactMethod @ReactMethod
public void getGenericPasswordForOptions(String service, Promise promise) { public void getGenericPasswordForOptions(String service, Promise promise) {
String originalService = service;
service = service == null ? DEFAULT_ALIAS : service;
try { try {
final byte[] decryptedUsername; final ResultSet resultSet;
final byte[] decryptedPassword; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
byte[] recuser = prefsUtils.getBytesForUsername(service, DELIMITER); resultSet = getGenericPasswordForOptions(service);
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;
}
} }
else { else {
KeyStore keyStore = getKeyStoreAndLoad(); resultSet = getGenericPasswordForOptionsUsingConceal(service);
}
Key key = keyStore.getKey(service, null); if (resultSet == null) {
Log.e(KEYCHAIN_MODULE, "no keychain entry found for service: " + service);
decryptedUsername = decryptBytes(key, recuser); promise.resolve(false);
decryptedPassword = decryptBytes(key, recpass); return;
} }
WritableMap credentials = Arguments.createMap(); WritableMap credentials = Arguments.createMap();
credentials.putString("service", service); credentials.putString("service", resultSet.service);
credentials.putString("username", new String(decryptedUsername, Charset.forName("UTF-8"))); credentials.putString("username", new String(resultSet.usernameBytes, Charset.forName("UTF-8")));
credentials.putString("password", new String(decryptedPassword, Charset.forName("UTF-8"))); credentials.putString("password", new String(resultSet.passwordBytes, Charset.forName("UTF-8")));
promise.resolve(credentials); promise.resolve(credentials);
} catch (KeyStoreException e) { } catch (KeyStoreException e) {
@ -239,12 +210,45 @@ public class KeychainModule extends ReactContextBaseJavaModule {
} catch (UnrecoverableKeyException | NoSuchAlgorithmException | CryptoFailedException e) { } catch (UnrecoverableKeyException | NoSuchAlgorithmException | CryptoFailedException e) {
Log.e(KEYCHAIN_MODULE, e.getMessage()); Log.e(KEYCHAIN_MODULE, e.getMessage());
promise.reject(E_CRYPTO_FAILED, e); 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 { private byte[] decryptBytes(Key key, byte[] bytes) throws CryptoFailedException {
try { try {
Cipher cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION); Cipher cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION);
@ -282,9 +286,8 @@ public class KeychainModule extends ReactContextBaseJavaModule {
} }
service = service == null ? EMPTY_STRING : service; service = service == null ? EMPTY_STRING : service;
byte[] recuser = prefsUtils.getBytesForUsername(service, LEGACY_DELIMITER); ResultSet legacyResultSet = prefsUtils.getBytesForUsernameAndPassword(service, LEGACY_DELIMITER);
byte[] recpass = prefsUtils.getBytesForPassword(service, LEGACY_DELIMITER); if (legacyResultSet == null) {
if (recuser == null || recpass == null) {
return null; return null;
} }
@ -292,8 +295,8 @@ public class KeychainModule extends ReactContextBaseJavaModule {
Entity pwentity = Entity.create(KEYCHAIN_DATA + ":" + service + "pass"); Entity pwentity = Entity.create(KEYCHAIN_DATA + ":" + service + "pass");
try { try {
byte[] decryptedUsername = crypto.decrypt(recuser, userentity); byte[] decryptedUsername = crypto.decrypt(legacyResultSet.usernameBytes, userentity);
byte[] decryptedPassword = crypto.decrypt(recpass, pwentity); byte[] decryptedPassword = crypto.decrypt(legacyResultSet.passwordBytes, pwentity);
return new ResultSet(service, decryptedUsername, decryptedPassword); return new ResultSet(service, decryptedUsername, decryptedPassword);
} catch (Exception e) { } 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 @ReactMethod
public void resetGenericPasswordForOptions(String service, Promise promise) { public void resetGenericPasswordForOptions(String service, Promise promise) {
try { try {

View File

@ -10,7 +10,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
public class KeychainPackage implements ReactPackage { public class KeychainPackage implements ReactPackage {
public KeychainPackage() { public KeychainPackage() {

View File

@ -3,25 +3,34 @@ package com.oblador.keychain;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.util.Base64; import android.util.Base64;
/**
* Created by pcoltau on 6/15/17.
*/
public class PrefsUtils { 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; private final SharedPreferences prefs;
public PrefsUtils(SharedPreferences prefs) { public PrefsUtils(SharedPreferences prefs) {
this.prefs = prefs; this.prefs = prefs;
} }
public byte[] getBytesForUsername(String service, String delimiter) { public ResultSet getBytesForUsernameAndPassword(String service, String delimiter) {
String key = getKeyForUsername(service, delimiter); String keyForUsername = getKeyForUsername(service, delimiter);
return getBytes(key); String keyForPassword = getKeyForPassword(service, delimiter);
} byte[] bytesForUsername = getBytes(keyForUsername);
byte[] bytesForPassword = getBytes(keyForPassword);
public byte[] getBytesForPassword(String service, String delimiter) { if (bytesForUsername != null && bytesForPassword != null) {
String key = getKeyForPassword(service, delimiter); return new ResultSet(service, bytesForUsername, bytesForPassword);
return getBytes(key); }
return null;
} }
public void resetPassword(String service, String delimiter) { public void resetPassword(String service, String delimiter) {
@ -43,6 +52,16 @@ public class PrefsUtils {
prefsEditor.apply(); 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) { private String getKeyForUsername(String service, String delimiter) {
return service + delimiter + "u"; return service + delimiter + "u";
} }