Skip to content

Commit

Permalink
Allow key to be not invalidated upon biometric enrollment. (#67)
Browse files Browse the repository at this point in the history
* Allow key to be not invalidated upon biometric enrollment.

* Adjust formatting

* Match formatting

* Fix formatting

* Add API methods in RxFingerprint class

* Update documentation
  • Loading branch information
NullPointeRR authored and Mauin committed Nov 27, 2017
1 parent 910c11e commit 4586ae1
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
Expand All @@ -43,32 +42,32 @@ class AesCipherProvider extends CipherProvider {
private static final int AES_KEY_SIZE = 256;

AesCipherProvider(@NonNull Context context, @Nullable String keyName) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
super(context, keyName);
}
this(context, keyName, true);
}

AesCipherProvider(@NonNull Context context, @Nullable String keyName, boolean keyInvalidatedByBiometricEnrollment) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
super(context, keyName, keyInvalidatedByBiometricEnrollment);
}

private SecretKey findOrCreateKey(String keyName) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, UnrecoverableKeyException, CertificateException, KeyStoreException, IOException {
if (keyExists(keyName)) {
return getKey(keyName);
}
return createKey(keyName);
}
return createKey(keyName, invalidatedByBiometricEnrollment);
}

private SecretKey getKey(String keyName) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
return (SecretKey) keyStore.getKey(keyName, null);
}

@TargetApi(Build.VERSION_CODES.M)
private static SecretKey createKey(String keyName) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setKeySize(AES_KEY_SIZE)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
return keyGenerator.generateKey();
}
private static SecretKey createKey(String keyName, boolean invalidatedByBiometricEnrollment) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
keyGenerator.init(getKeyGenParameterSpecBuilder(keyName, KeyProperties.BLOCK_MODE_CBC, KeyProperties.ENCRYPTION_PADDING_PKCS7, invalidatedByBiometricEnrollment)
.setKeySize(AES_KEY_SIZE)
.build());
return keyGenerator.generateKey();
}

@Override
Cipher cipherForEncryption() throws NoSuchAlgorithmException, NoSuchPaddingException, CertificateException, UnrecoverableKeyException, KeyStoreException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, InvalidKeyException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ class AesEncryptionObservable extends FingerprintObservable<FingerprintEncryptio
* @param keyName name of the key in the keystore
* @param toEncrypt data to encrypt @return Observable {@link FingerprintEncryptionResult}
*/
static Observable<FingerprintEncryptionResult> create(Context context, String keyName, String toEncrypt) {
static Observable<FingerprintEncryptionResult> create(Context context, String keyName, String toEncrypt, boolean keyInvalidatedByBiometricEnrollment) {
try {
return Observable.create(new AesEncryptionObservable(new FingerprintApiWrapper(context),
new AesCipherProvider(context, keyName),
new AesCipherProvider(context, keyName, keyInvalidatedByBiometricEnrollment),
toEncrypt,
new Base64Provider()));
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

Expand All @@ -40,14 +42,15 @@ abstract class CipherProvider {

final String keyName;
final KeyStore keyStore;
final boolean invalidatedByBiometricEnrollment;

CipherProvider(@NonNull Context context, @Nullable String keyName) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
CipherProvider(@NonNull Context context, @Nullable String keyName, boolean keyInvalidatedByBiometricEnrollment) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
if (keyName == null) {
this.keyName = ContextUtils.getPackageName(context) + "." + DEFAULT_KEY_NAME;
} else {
this.keyName = keyName;
}

invalidatedByBiometricEnrollment = keyInvalidatedByBiometricEnrollment;
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
}
Expand All @@ -60,6 +63,20 @@ abstract class CipherProvider {
@TargetApi(Build.VERSION_CODES.M)
abstract Cipher createCipher() throws NoSuchPaddingException, NoSuchAlgorithmException;

@NonNull
@TargetApi(Build.VERSION_CODES.M)
static KeyGenParameterSpec.Builder getKeyGenParameterSpecBuilder(String keyName, String blockModes, String encryptionPaddings, boolean invalidatedByBiometricEnrollment) {
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(blockModes)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(encryptionPaddings);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment);
}
return builder;
}

@TargetApi(Build.VERSION_CODES.M)
Cipher getCipherForEncryption() throws IOException, GeneralSecurityException {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
Expand All @@ -42,19 +41,18 @@

class RsaCipherProvider extends CipherProvider {
RsaCipherProvider(@NonNull Context context, @Nullable String keyName) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
super(context, keyName);
this(context, keyName, true);
}

RsaCipherProvider(@NonNull Context context, @Nullable String keyName, boolean keyInvalidatedByBiometricEnrollment) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
super(context, keyName, keyInvalidatedByBiometricEnrollment);
}
@Override
@TargetApi(Build.VERSION_CODES.M)
Cipher cipherForEncryption() throws GeneralSecurityException, IOException {
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE);

keyGenerator.initialize(new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
keyGenerator.initialize(getKeyGenParameterSpecBuilder(keyName, KeyProperties.BLOCK_MODE_ECB, KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1, invalidatedByBiometricEnrollment)
.build());

keyGenerator.generateKeyPair();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ class RsaEncryptionObservable implements ObservableOnSubscribe<FingerprintEncryp
* @param keyName name of the key in the keystore
* @param toEncrypt data to encrypt @return Observable {@link FingerprintEncryptionResult}
*/
static Observable<FingerprintEncryptionResult> create(Context context, String keyName, String toEncrypt) {
static Observable<FingerprintEncryptionResult> create(Context context, String keyName, String toEncrypt, boolean keyInvalidatedByBiometricEnrollment) {
if (toEncrypt == null) {
return Observable.error(new IllegalArgumentException("String to be encrypted is null. Can only encrypt valid strings"));
}
try {
return Observable.create(new RsaEncryptionObservable(new FingerprintApiWrapper(context),
new RsaCipherProvider(context, keyName),
new RsaCipherProvider(context, keyName, keyInvalidatedByBiometricEnrollment),
toEncrypt,
new Base64Provider()));
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,38 @@ public static Observable<FingerprintAuthenticationResult> authenticate(@NonNull
* Will complete once the authentication and encryption were successful or have failed entirely.
*/
public static Observable<FingerprintEncryptionResult> encrypt(@NonNull Context context, @NonNull String toEncrypt) {
return encrypt(EncryptionMethod.AES, context, null, toEncrypt);
return encrypt(EncryptionMethod.AES, context, null, toEncrypt, true);
}

/**
* Encrypt data and authenticate the user with his fingerprint. The encrypted data can only be
* accessed again by calling {@link #decrypt(Context, String)}. Will use a default keyName in
* the Android keystore unique to this applications package name.
* If you want to provide a custom key name use {@link #encrypt(Context, String, String)}
* instead.
* <p/>
* Encrypted data is only accessible after the user has authenticated with
* fingerprint authentication.
* <p/>
* Encryption uses AES encryption with CBC blocksize and PKCS7 padding.
* The key-length for AES encryption is set to 265 bits by default.
* <p/>
* The resulting {@link FingerprintEncryptionResult} will contain the encrypted data as a String
* and is accessible via {@link FingerprintEncryptionResult#getEncrypted()} if the
* authentication was successful. Save this data where you please, but don't change it if you
* want to decrypt it again!
*
* @param context context to use
* @param toEncrypt data to encrypt
* @param keyInvalidatedByBiometricEnrollment whether or not the key will be invalidated when fingerprints are added
* or changed. Works only on Android N(API 24) and above.
* @return Observable {@link FingerprintEncryptionResult} that will contain the encrypted data.
* Will complete once the authentication and encryption were successful or have failed entirely.
*/
public static Observable<FingerprintEncryptionResult> encrypt(@NonNull Context context, @NonNull String toEncrypt, boolean keyInvalidatedByBiometricEnrollment) {
return encrypt(EncryptionMethod.AES, context, null, toEncrypt, keyInvalidatedByBiometricEnrollment);
}

/**
* Decrypt data previously encrypted with {@link #encrypt(Context, String)}.
* <p/>
Expand Down Expand Up @@ -132,7 +161,7 @@ public static Observable<FingerprintDecryptionResult> decrypt(@NonNull Context c
* Will complete once the authentication and encryption were successful or have failed entirely.
*/
public static Observable<FingerprintEncryptionResult> encrypt(@NonNull Context context, @Nullable String keyName, @NonNull String toEncrypt) {
return encrypt(EncryptionMethod.AES, context, keyName, toEncrypt);
return encrypt(EncryptionMethod.AES, context, keyName, toEncrypt, true);
}

/**
Expand Down Expand Up @@ -184,11 +213,42 @@ public static Observable<FingerprintEncryptionResult> encrypt(@NonNull Encryptio
@NonNull Context context,
@Nullable String keyName,
@NonNull String toEncrypt) {
return encrypt(method, context, keyName, toEncrypt, true);
}

/**
* Encrypt data with the given {@link EncryptionMethod}. Depending on the given method, the
* fingerprint sensor might be enabled and waiting for the user to authenticate before the
* encryption step. All encrypted data can only be accessed again by calling
* {@link #decrypt(EncryptionMethod, Context, String, String)} with the same
* {@link EncryptionMethod} that was used for encryption of the given value.
* <p>
* Take more details about the encryption method and how they behave from {@link EncryptionMethod}
* <p>
* The resulting {@link FingerprintEncryptionResult} will contain the encrypted data as a String
* and is accessible via {@link FingerprintEncryptionResult#getEncrypted()} if the
* operation was successful. Save this data where you please, but don't change it if you
* want to decrypt it again!
*
* @param method the encryption method to use
* @param context context to use
* @param keyName name of the key to store in the Android {@link java.security.KeyStore}
* @param toEncrypt data to encrypt
* @param keyInvalidatedByBiometricEnrollment whether or not the key will be invalidated when fingerprints are added
* or changed. Works only on Android N(API 24) and above.
* @return Observable {@link FingerprintEncryptionResult} that will contain the encrypted data.
* Will complete once the operation was successful or failed entirely.
*/
public static Observable<FingerprintEncryptionResult> encrypt(@NonNull EncryptionMethod method,
@NonNull Context context,
@Nullable String keyName,
@NonNull String toEncrypt,
boolean keyInvalidatedByBiometricEnrollment) {
switch (method) {
case AES:
return AesEncryptionObservable.create(context, keyName, toEncrypt);
return AesEncryptionObservable.create(context, keyName, toEncrypt, keyInvalidatedByBiometricEnrollment);
case RSA:
return RsaEncryptionObservable.create(context, keyName, toEncrypt);
return RsaEncryptionObservable.create(context, keyName, toEncrypt, keyInvalidatedByBiometricEnrollment);
default:
return Observable.error(new IllegalArgumentException("Unknown encryption method: " + method));
}
Expand Down

0 comments on commit 4586ae1

Please sign in to comment.