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

Fix User Disabled Security Pattern Flow #329

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
60 changes: 33 additions & 27 deletions src/android/BiometricActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,12 @@ public class BiometricActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(null);
int layout = getResources()
.getIdentifier("biometric_activity", "layout", getPackageName());
int layout = getResources().getIdentifier("biometric_activity", "layout", getPackageName());
setContentView(layout);

if (savedInstanceState != null) {
return;
}

mCryptographyManager = new CryptographyManagerImpl();
mPromptInfo = new PromptInfo.Builder(getIntent().getExtras()).build();
final Handler handler = new Handler(Looper.getMainLooper());
Expand All @@ -53,15 +51,15 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {

private void authenticate() throws CryptoException {
switch (mPromptInfo.getType()) {
case JUST_AUTHENTICATE:
justAuthenticate();
return;
case REGISTER_SECRET:
authenticateToEncrypt(mPromptInfo.invalidateOnEnrollment());
return;
case LOAD_SECRET:
authenticateToDecrypt();
return;
case JUST_AUTHENTICATE:
justAuthenticate();
return;
case REGISTER_SECRET:
authenticateToEncrypt(mPromptInfo.invalidateOnEnrollment());
return;
case LOAD_SECRET:
authenticateToDecrypt();
return;
}
throw new CryptoException(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
}
Expand All @@ -70,9 +68,12 @@ private void authenticateToEncrypt(boolean invalidateOnEnrollment) throws Crypto
if (mPromptInfo.getSecret() == null) {
throw new CryptoException(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
}
Cipher cipher = mCryptographyManager
.getInitializedCipherForEncryption(SECRET_KEY, invalidateOnEnrollment, this);
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
try {
Cipher cipher = mCryptographyManager.getInitializedCipherForEncryption(SECRET_KEY, invalidateOnEnrollment, this);
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
} catch (Exception e){
throw new CryptoException(e.getMessage(), e);
}
}

private void justAuthenticate() {
Expand All @@ -81,9 +82,16 @@ private void justAuthenticate() {

private void authenticateToDecrypt() throws CryptoException {
byte[] initializationVector = EncryptedData.loadInitializationVector(this);
Cipher cipher = mCryptographyManager
.getInitializedCipherForDecryption(SECRET_KEY, initializationVector, this);
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
try {
Cipher cipher = mCryptographyManager.getInitializedCipherForDecryption(SECRET_KEY, initializationVector, this);
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
} catch (Exception e){
if(e.getCause() instanceof KeyInvalidatedException || e instanceof KeyInvalidatedException){
EncryptedData.remove(this);
throw new CryptoException(PluginError.BIOMETRIC_NO_SECRET_FOUND);
}
throw new CryptoException(e.getMessage(), e);
}
}

private BiometricPrompt.PromptInfo createPromptInfo() {
Expand All @@ -93,9 +101,7 @@ private BiometricPrompt.PromptInfo createPromptInfo() {
.setConfirmationRequired(mPromptInfo.getConfirmationRequired())
.setDescription(mPromptInfo.getDescription());

if (mPromptInfo.isDeviceCredentialAllowed()
&& mPromptInfo.getType() == BiometricActivityType.JUST_AUTHENTICATE
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // TODO: remove after fix https://issuetracker.google.com/issues/142740104
if (mPromptInfo.isDeviceCredentialAllowed() && mPromptInfo.getType() == BiometricActivityType.JUST_AUTHENTICATE && Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // TODO: remove after fix https://issuetracker.google.com/issues/142740104
promptInfoBuilder.setDeviceCredentialAllowed(true);
} else {
promptInfoBuilder.setNegativeButtonText(mPromptInfo.getCancelButtonTitle());
Expand Down Expand Up @@ -195,12 +201,12 @@ private void finishWithSuccess() {
private void finishWithSuccess(BiometricPrompt.CryptoObject cryptoObject) throws CryptoException {
Intent intent = null;
switch (mPromptInfo.getType()) {
case REGISTER_SECRET:
encrypt(cryptoObject);
break;
case LOAD_SECRET:
intent = getDecryptedIntent(cryptoObject);
break;
case REGISTER_SECRET:
encrypt(cryptoObject);
break;
case LOAD_SECRET:
intent = getDecryptedIntent(cryptoObject);
break;
}
if (intent == null) {
setResult(RESULT_OK);
Expand Down
58 changes: 38 additions & 20 deletions src/android/CryptographyManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.Calendar;

import javax.crypto.Cipher;
Expand All @@ -35,9 +36,17 @@ private Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmExcepti
return Cipher.getInstance(transformation);
}

private SecretKey getOrCreateSecretKey(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException {
private SecretKey createSecretKey(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return getOrCreateSecretKeyNew(keyName, invalidateOnEnrollment);
return createSecretKeyNew(keyName, invalidateOnEnrollment);
} else {
return getOrCreateSecretKeyOld(keyName, context);
}
}

private SecretKey getSecretKey(String keyName, Context context) throws CryptoException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return getSecretKey(keyName);
} else {
return getOrCreateSecretKeyOld(keyName, context);
}
Expand Down Expand Up @@ -65,19 +74,19 @@ private SecretKey getOrCreateSecretKeyOld(String keyName, Context context) throw
}
}

@RequiresApi(api = Build.VERSION_CODES.M)
private SecretKey getOrCreateSecretKeyNew(String keyName, boolean invalidateOnEnrollment) throws CryptoException {
private SecretKey getSecretKey(String keyName) throws CryptoException {
try {
// If Secretkey was previously created for that keyName, then grab and return it.
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null); // Keystore must be loaded before it can be accessed
return (SecretKey) keyStore.getKey(keyName, null);
} catch (Exception e){
throw new CryptoException(e.getMessage(), e);
}
}


SecretKey key = (SecretKey) keyStore.getKey(keyName, null);
if (key != null) {
return key;
}

@RequiresApi(api = Build.VERSION_CODES.M)
private SecretKey createSecretKeyNew(String keyName, boolean invalidateOnEnrollment) throws CryptoException {
try {
// if you reach here, then a new SecretKey must be generated for that keyName
KeyGenParameterSpec.Builder keyGenParamsBuilder = new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
Expand All @@ -90,10 +99,8 @@ private SecretKey getOrCreateSecretKeyNew(String keyName, boolean invalidateOnEn
keyGenParamsBuilder.setInvalidatedByBiometricEnrollment(invalidateOnEnrollment);
}

KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM_AES,
ANDROID_KEYSTORE);
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
keyGenerator.init(keyGenParamsBuilder.build());

return keyGenerator.generateKey();
} catch (Exception e) {
throw new CryptoException(e.getMessage(), e);
Expand All @@ -104,7 +111,16 @@ private SecretKey getOrCreateSecretKeyNew(String keyName, boolean invalidateOnEn
public Cipher getInitializedCipherForEncryption(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException {
try {
Cipher cipher = getCipher();
SecretKey secretKey = getOrCreateSecretKey(keyName, invalidateOnEnrollment, context);
SecretKey secretKey;
try {
secretKey = getSecretKey(keyName, context);
if(secretKey == null) {
secretKey = createSecretKey(keyName, invalidateOnEnrollment, context);
}
} catch (CryptoException e) {
EncryptedData.remove(context);
secretKey = createSecretKey(keyName, invalidateOnEnrollment, context);
}
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher;
} catch (Exception e) {
Expand All @@ -118,8 +134,7 @@ public Cipher getInitializedCipherForEncryption(String keyName, boolean invalida
}

private void handleException(Exception e, String keyName) throws CryptoException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& e instanceof KeyPermanentlyInvalidatedException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && e instanceof KeyPermanentlyInvalidatedException || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && e.getCause() instanceof UnrecoverableKeyException) {
removeKey(keyName);
throw new KeyInvalidatedException();
}
Expand All @@ -129,7 +144,10 @@ private void handleException(Exception e, String keyName) throws CryptoException
public Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector, Context context) throws CryptoException {
try {
Cipher cipher = getCipher();
SecretKey secretKey = getOrCreateSecretKey(keyName, true, context);
SecretKey secretKey = getSecretKey(keyName, context);
if(secretKey == null){
throw new KeyInvalidatedException();
}
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, initializationVector));
return cipher;
} catch (Exception e) {
Expand All @@ -142,9 +160,9 @@ private void removeKey(String keyName) throws CryptoException {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null); // Keystore must be loaded before it can be accessed
keyStore.deleteEntry(keyName);
keyStore.deleteEntry(keyName); //TODO Check why can't delete a key was previously invalidated by new enrollment or disable security pattern
} catch (Exception e) {
throw new CryptoException(e.getMessage(), e);
throw new KeyInvalidatedException(); //TODO Manage proper exception after deal with deleteEntry issue instead return always BIOMETRIC_NO_SECRET_FOUND and retry flow
}
}

Expand Down
17 changes: 14 additions & 3 deletions src/android/EncryptedData.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,22 @@ void save(Context context) {
save(CIPHERTEXT_KEY_NAME, ciphertext, context);
}

static void remove(Context context) {
remove(IV_KEY_NAME, context);
remove(CIPHERTEXT_KEY_NAME, context);
}

private void save(String key, byte[] value, Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
preferences.edit()
.putString(key, Base64.encodeToString(value, Base64.DEFAULT))
.apply();
preferences.edit().putString(key, Base64.encodeToString(value, Base64.DEFAULT)).apply();
}

private static void remove(String key, Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String res = preferences.getString(key, null);
if(res != null){
preferences.edit().remove(key).apply();
}
}

private static byte[] load(String key, Context context) throws CryptoException {
Expand Down