Skip to content

Commit

Permalink
Android: Fix #259: Handle errors after biometric authentication success
Browse files Browse the repository at this point in the history
  • Loading branch information
hvge committed Dec 10, 2019
1 parent 6ebb514 commit 13bca0a
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.support.v4.app.FragmentManager;
import android.util.Pair;

import javax.crypto.SecretKey;

Expand Down Expand Up @@ -130,6 +131,12 @@ public void onCompletion() {
ctx.finishPendingBiometricAuthentication();
}
}

@Override
public void onBiometricKeyUnavailable() {
// Remove the default key, because the biometric key is no longer available.
device.getBiometricKeystore().removeDefaultKey();
}
});
final PrivateRequestData requestData = new PrivateRequestData(request, dispatcher, ctx.getBiometricDialogResources());

Expand All @@ -145,6 +152,7 @@ public void onCompletion() {

} catch (PowerAuthErrorException e) {
// Failed to authenticate. Show an error dialog and report that exception to the callback.
PA2Log.e("BiometricAuthentication.authenticate() failed with exception: " + e.getMessage());
exception = e;
status = BiometricStatus.NOT_AVAILABLE;
}
Expand Down Expand Up @@ -178,7 +186,7 @@ public void onCompletion() {
} else {
key = keystore.getDefaultKey();
if (key == null) {
throw new PowerAuthErrorException(PowerAuthErrorCodes.PA2ErrorCodeBiometryNotSupported, "Cannot get biometric key from the keystore.");
throw new PowerAuthErrorException(PowerAuthErrorCodes.PA2ErrorCodeBiometryNotAvailable, "Cannot get biometric key from the keystore.");
}
}
return key;
Expand All @@ -204,29 +212,11 @@ public void onCompletion() {
final CancelableTask cancelableTask = requestData.getDispatcher().getCancelableTask();

final BiometricDialogResources resources = requestData.getResources();
final @StringRes int errorTitle;
final @StringRes int errorDescription;
if (status == BiometricStatus.NOT_ENROLLED) {
// User must enroll at least one fingerprint
errorTitle = resources.strings.errorEnrollFingerprintTitle;
errorDescription = resources.strings.errorEnrollFingerprintDescription;
} else if (status == BiometricStatus.NOT_SUPPORTED) {
// Fingerprint scanner is not supported on the authenticator
errorTitle = resources.strings.errorNoFingerprintScannerTitle;
errorDescription = resources.strings.errorNoFingerprintScannerDescription;
} else if (status == BiometricStatus.NOT_AVAILABLE) {
// Fingerprint scanner is disabled in the system, or permission was not granted.
errorTitle = resources.strings.errorFingerprintDisabledTitle;
errorDescription = resources.strings.errorFingerprintDisabledDescription;
} else {
// Fallback...
errorTitle = resources.strings.errorFingerprintDisabledTitle;
errorDescription = resources.strings.errorFingerprintDisabledDescription;
}
final Pair<Integer, Integer> titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(status, resources);

final BiometricErrorDialogFragment dialogFragment = new BiometricErrorDialogFragment.Builder(context)
.setTitle(errorTitle)
.setMessage(errorDescription)
.setTitle(titleDescription.first)
.setMessage(titleDescription.second)
.setCloseButton(resources.strings.ok, resources.colors.closeButtonText)
.setIcon(resources.drawables.errorIcon)
.setOnCloseListener(new BiometricErrorDialogFragment.OnCloseListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.app.FragmentManager;
import android.util.Pair;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
Expand All @@ -39,6 +40,7 @@
import io.getlime.security.powerauth.networking.interfaces.ICancelable;
import io.getlime.security.powerauth.sdk.impl.CancelableTask;
import io.getlime.security.powerauth.sdk.impl.CompositeCancelableTask;
import io.getlime.security.powerauth.system.PA2Log;

/**
* The {@code BiometricAuthenticator} implements {@link IBiometricAuthenticator} interface with using new
Expand Down Expand Up @@ -215,12 +217,24 @@ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult resul
final byte[] protectedKey = protectKeyWithCipher(request.getKeyToProtect(), cipher);
if (protectedKey != null) {
dispatcher.dispatchSuccess(protectedKey);
} else {
dispatcher.dispatchError(PowerAuthErrorCodes.PA2ErrorCodeEncryptionError, "Failed to encrypt biometric key.");
return;
}
PA2Log.e("Failed to encrypt biometric key.");
} else {
dispatcher.dispatchError(PowerAuthErrorCodes.PA2ErrorCodeEncryptionError, "The device has broken biometric implementation.");
PA2Log.e("Failed to get Cipher from CryptoObject.");
}
// If the code ends here, it mostly means that the vendor's implementation is quite off the standard.
// The device reports success, but we're unable to derive our cryptographic key, due to malfunction in cipher
// or due to fact, that the previously constructed cipher is not available. The right response for this state
// is to remove the biometric key from the keychain, show an error dialog and then, finally report "not available" state.
dispatcher.reportBiometricKeyUnavailable();
dispatcher.dispatchRunnable(new Runnable() {
@Override
public void run() {
final PowerAuthErrorException exception = new PowerAuthErrorException(PowerAuthErrorCodes.PA2ErrorCodeBiometryNotAvailable, "Failed to encrypt biometric key.");
showErrorDialogAfterSuccess(fragmentManager, requestData, exception);
}
});
}

@Override
Expand Down Expand Up @@ -385,4 +399,50 @@ public void onCancel() {
}
return context.getString(resources.strings.errorCodeGeneric);
}

/**
* Shows error dialog despite the fact, that biometric authentication succeeded. This might happen
* in rare cases, when the vendor's implementation is unable to encrypt the provided biometric key.
*
* @param fragmentManager Fragment manager.
* @param requestData Private request data.
* @param exception Exception to be reported later to the operation's callback.
*/
private void showErrorDialogAfterSuccess(
@NonNull final FragmentManager fragmentManager,
@NonNull final PrivateRequestData requestData,
@NonNull final PowerAuthErrorException exception) {

final BiometricResultDispatcher dispatcher = requestData.getDispatcher();
if (dispatcher.getCancelableTask().isCancelled()) {
// Do nothing. Looks like the whole operation was canceled from the application.
return;
}

final BiometricDialogResources resources = requestData.getResources();
final Pair<Integer, Integer> titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(BiometricStatus.NOT_AVAILABLE, resources);

final BiometricErrorDialogFragment dialogFragment = new BiometricErrorDialogFragment.Builder(context)
.setTitle(titleDescription.first)
.setMessage(titleDescription.second)
.setCloseButton(resources.strings.ok, resources.colors.closeButtonText)
.setIcon(resources.drawables.errorIcon)
.setOnCloseListener(new BiometricErrorDialogFragment.OnCloseListener() {
@Override
public void onClose() {
dispatcher.dispatchError(exception);
}
})
.build();
// Handle cancel from the application. Note that this overrides the previous cancel listener.
dispatcher.setOnCancelListener(new CancelableTask.OnCancelListener() {
@Override
public void onCancel() {
dialogFragment.dismiss();
}
});

// Show fragment
dialogFragment.show(fragmentManager, BiometricErrorDialogFragment.FRAGMENT_DEFAULT_TAG);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.StringRes;
import android.util.Pair;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
Expand All @@ -34,9 +36,11 @@
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

import io.getlime.security.powerauth.biometry.BiometricDialogResources;
import io.getlime.security.powerauth.biometry.BiometricStatus;
import io.getlime.security.powerauth.exception.PowerAuthErrorCodes;
import io.getlime.security.powerauth.exception.PowerAuthErrorException;
import io.getlime.security.powerauth.system.PA2Log;

/**
* The {@code BiometricHelper} class provides helper methods for PowerAuth Mobile SDK. The class
Expand Down Expand Up @@ -65,6 +69,36 @@ public class BiometricHelper {
}
}

/**
* Translate {@link BiometricStatus} into pair of string resources, representing title and description for error dialog.
*
* @param status Status to be translated to error dialog resources.
* @param resources {@link BiometricDialogResources} object with resource identifiers.
* @return Pair of string resource identifiers, with appropriate title and description.
*/
public static @NonNull Pair<Integer, Integer> getErrorDialogStringsForBiometricStatus(@BiometricStatus int status, @NonNull BiometricDialogResources resources) {
final @StringRes int errorTitle;
final @StringRes int errorDescription;
if (status == BiometricStatus.NOT_ENROLLED) {
// User must enroll at least one fingerprint
errorTitle = resources.strings.errorEnrollFingerprintTitle;
errorDescription = resources.strings.errorEnrollFingerprintDescription;
} else if (status == BiometricStatus.NOT_SUPPORTED) {
// Fingerprint scanner is not supported on the authenticator
errorTitle = resources.strings.errorNoFingerprintScannerTitle;
errorDescription = resources.strings.errorNoFingerprintScannerDescription;
} else if (status == BiometricStatus.NOT_AVAILABLE) {
// Fingerprint scanner is disabled in the system, or permission was not granted.
errorTitle = resources.strings.errorFingerprintDisabledTitle;
errorDescription = resources.strings.errorFingerprintDisabledDescription;
} else {
// Fallback...
errorTitle = resources.strings.errorFingerprintDisabledTitle;
errorDescription = resources.strings.errorFingerprintDisabledDescription;
}
return Pair.create(errorTitle, errorDescription);
}

/**
* Create AES/CBC with PKCS7 padding cipher with given secret key.
*
Expand All @@ -79,13 +113,8 @@ public class BiometricHelper {
AlgorithmParameterSpec algorithmSpec = new IvParameterSpec(zero_iv);
cipher.init(Cipher.ENCRYPT_MODE, key, algorithmSpec);
return cipher;
} catch (NoSuchPaddingException e) {
return null;
} catch (InvalidAlgorithmParameterException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (InvalidKeyException e) {
} catch (NoSuchPaddingException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | InvalidKeyException e) {
PA2Log.e("BiometricHelper.createAesCipher failed: " + e.getMessage());
return null;
}
}
Expand All @@ -100,9 +129,8 @@ public class BiometricHelper {
public static @Nullable byte[] protectKeyWithCipher(@NonNull byte[] keyToProtect, @NonNull Cipher cipher) {
try {
return cipher.doFinal(keyToProtect);
} catch (IllegalBlockSizeException e) {
return null;
} catch (BadPaddingException e) {
} catch (IllegalBlockSizeException | BadPaddingException e) {
PA2Log.e("BiometricHelper.protectKeyWithCipher failed: " + e.getMessage());
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public interface IResultCompletion {
* Called on any kind of completion (e.g. on success, failure or cancel)
*/
void onCompletion();

/**
* Called when the biometric key is no longer available, due to unexpected malfunction.
* If the method is called, then it's always called before {@link #onCompletion()}.
*/
void onBiometricKeyUnavailable();
}

private @NonNull final IResultCompletion resultCompletion;
Expand All @@ -49,6 +55,7 @@ public interface IResultCompletion {
private @NonNull final CancelableTask cancelable;
private @Nullable CancelableTask.OnCancelListener onCancelListener;
private boolean isDispatched = false;
private boolean isBiometricKeyUnavailableDispatched = false;

/**
* @param callback Callback to the application. It's always executed after the {@link #resultCompletion}.
Expand Down Expand Up @@ -150,6 +157,33 @@ public void run() {
});
}

/**
* Execute runnable object in the internal callback dispatcher. Unlike other public "dispatch" methods,
* in this class, this method only executes the runnable, but doesn't mark this result dispatcher
* as completed with the result. The method is useful when you need to execute an arbitrary
* code at the completion thread.
*
* @param runnable {@link Runnable} to execute on the callback dispatcher.
*/
public void dispatchRunnable(@NonNull final Runnable runnable) {
callbackDispatcher.dispatchCallback(runnable);
}

/**
* Report that biometric key is no longer available.
*/
public void reportBiometricKeyUnavailable() {
callbackDispatcher.dispatchCallback(new Runnable() {
@Override
public void run() {
if (!isDispatched && !isBiometricKeyUnavailableDispatched) {
isBiometricKeyUnavailableDispatched = true;
resultCompletion.onBiometricKeyUnavailable();
}
}
});
}

/**
* Execute runnable task with result on the callback dispatcher. Only one task can be executed
* from this result dispatcher. That means that if operation has been cancelled, or some other
Expand Down
Loading

0 comments on commit 13bca0a

Please sign in to comment.