Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Renew Key on KeyPermanentlyInvalidatedException #34

Merged
merged 6 commits into from
Feb 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ Disposable disposable = RxFingerprint.decrypt(this, encryptedString)
Be aware that all encryption keys will be invalidated once the user changes his lockscreen or changes his enrolled fingerprints. If you receive an `onError` event
during decryption check if the keys were invalidated with `RxFingerprint.keyInvalidated(Throwable)` and prompt the user to authenticate and encrypt his data again.

Once the encryption keys are invalidated RxFingerprint will delete and renew the keys in the Android Keystore on the next call to `RxFingerprint.encrypt(...)`.

### Best-practices

To prevent errors and ensure a good user experience, make sure to think of these cases:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
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;
import android.util.Log;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
Expand All @@ -47,97 +49,118 @@
*/
class CryptoProvider {

private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String DEFAULT_KEY_NAME = "rxfingerprint_default";
private static final int DEFAULT_KEY_SIZE = 256;

private final String keyName;

/**
* Creates a new CryptoProvider. If a keyName is provided, uses the given key in the KeyStore
* for cryptographic operations. The given key name is {@code null} a default key name will be
* used.
* <p/>
* The default key name will consist of the applications package name appended with
* {@link CryptoProvider#DEFAULT_KEY_NAME}.
*
* @param context context to use, may not be null
* @param keyName keyName to use, can be null
*/
CryptoProvider(@NonNull Context context, @Nullable String keyName) {
if (keyName == null) {
this.keyName = ContextUtils.getPackageName(context) + "." + DEFAULT_KEY_NAME;
} else {
this.keyName = keyName;
}
}

/**
* @return Initialized cipher for encryption operations in RxFinerprint
*/
@TargetApi(Build.VERSION_CODES.M)
Cipher initEncryptionCipher() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidAlgorithmParameterException, UnrecoverableKeyException, CertificateException, KeyStoreException, IOException {
Cipher cipher = createCipher();
SecretKey key = findOrCreateKey(keyName);
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher;
}

/**
* @param iv initialization vector used during encryption
* @return Initialized cipher for decryption operations in RxFingerprint
*/
Cipher initDecryptionCipher(byte[] iv) throws CertificateException, NoSuchAlgorithmException, IOException, InvalidKeyException, UnrecoverableKeyException, KeyStoreException, InvalidAlgorithmParameterException, NoSuchPaddingException {
Cipher cipher = createCipher();
SecretKey key = getKey(keyName);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
return cipher;
}

@TargetApi(Build.VERSION_CODES.M)
private Cipher createCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
}

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

private boolean keyExists(String keyName) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
Enumeration<String> aliases = keyStore.aliases();

while (aliases.hasMoreElements()) {
if (keyName.equals(aliases.nextElement())) {
return true;
}
}

return false;
}

@TargetApi(Build.VERSION_CODES.M)
private 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(DEFAULT_KEY_SIZE)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
return keyGenerator.generateKey();
}

private SecretKey getKey(String keyName) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
return (SecretKey) keyStore.getKey(keyName, null);
}
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String DEFAULT_KEY_NAME = "rxfingerprint_default";
private static final int DEFAULT_KEY_SIZE = 256;

private final String keyName;

/**
* Creates a new CryptoProvider. If a keyName is provided, uses the given key in the KeyStore
* for cryptographic operations. The given key name is {@code null} a default key name will be
* used.
* <p/>
* The default key name will consist of the applications package name appended with
* {@link CryptoProvider#DEFAULT_KEY_NAME}.
*
* @param context context to use, may not be null
* @param keyName keyName to use, can be null
*/
CryptoProvider(@NonNull Context context, @Nullable String keyName) {
if (keyName == null) {
this.keyName = ContextUtils.getPackageName(context) + "." + DEFAULT_KEY_NAME;
} else {
this.keyName = keyName;
}
}

/**
* Gets or creates a key and initializes a cipher with it in {@link Cipher.ENCRYPT_MODE}
* In case the key was permanently invalidated the key will be deleted and re-created.
*
* @return Initialized cipher for encryption operations in RxFingerprint
*/
@TargetApi(Build.VERSION_CODES.M)
Cipher initEncryptionCipher() throws IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchProviderException, KeyStoreException {
try {
return cipherForEncryption();
} catch (KeyPermanentlyInvalidatedException e) {
Log.w("RxFingerprint", "Renewing invalidated key.");
removeKey(keyName);
return cipherForEncryption();
}
}

/**
* @param iv initialization vector used during encryption
* @return Initialized cipher for decryption operations in RxFingerprint
*/
Cipher initDecryptionCipher(byte[] iv) throws CertificateException, NoSuchAlgorithmException, IOException, InvalidKeyException, UnrecoverableKeyException, KeyStoreException, InvalidAlgorithmParameterException, NoSuchPaddingException {
Cipher cipher = createCipher();
SecretKey key = getKey(keyName);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
return cipher;
}

private Cipher cipherForEncryption() throws NoSuchAlgorithmException, NoSuchPaddingException, CertificateException, UnrecoverableKeyException, KeyStoreException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, InvalidKeyException {
Cipher cipher = createCipher();
SecretKey key = findOrCreateKey(keyName);
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher;
}

private void removeKey(String keyName) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
if (keyExists(keyName)) {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
keyStore.deleteEntry(keyName);
}
}

@TargetApi(Build.VERSION_CODES.M)
private Cipher createCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
}

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

private boolean keyExists(String keyName) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
Enumeration<String> aliases = keyStore.aliases();

while (aliases.hasMoreElements()) {
if (keyName.equals(aliases.nextElement())) {
return true;
}
}

return false;
}

@TargetApi(Build.VERSION_CODES.M)
private 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(DEFAULT_KEY_SIZE)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
return keyGenerator.generateKey();
}

private SecretKey getKey(String keyName) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
return (SecretKey) keyStore.getKey(keyName, null);
}
}