Skip to content

Commit

Permalink
Android: Option to disable error dialog after failed biometry (#552)
Browse files Browse the repository at this point in the history
* Android: Fix #523: Option to disable error dialog

- PowerAuthErrorException now may contain an additionalInformation
- Added BiometricErrorInfo

Other changes
- Simplified PowerAuthSDK.authenticateWithBiometry()
- Added support for missing BIOMETRY_NOT_ENROLLED error code (unification with iOS)
- Android: Fixed javadoc warnings
- Android: #553: Temporary disable HTML validation in Javadoc
- Fixed several Javadoc warnings with added documentation.
  • Loading branch information
hvge authored Sep 21, 2023
1 parent 3dbd907 commit cb40343
Show file tree
Hide file tree
Showing 19 changed files with 510 additions and 75 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ xcuserdata
*.xcuserstate
*.xcscmblueprint

## VSCode
.vscode

## Obj-C/Swift specific
*.hmap
*.ipa
Expand Down
76 changes: 64 additions & 12 deletions docs/PowerAuth-SDK-for-Android.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ This code has created activation with two factors: possession (key stored using
<!-- begin codetabs Kotlin Java -->
```kotlin
// Persist activation using given PIN and ad-hoc generated biometric related key
powerAuthSDK.persistActivation(context, fragment, "Enable Biometric Authentication", "To enable biometric authentication, use the biometric sensor on your device.", pin, object: IPersistActivationWithBiometryListener {
powerAuthSDK.persistActivation(context, fragment, "Enable Biometric Authentication", "To enable biometric authentication, use the biometric sensor on your device.", pin, object: IPersistActivationWithBiometricsListener {
override fun onBiometricDialogCancelled() {
// Biometric enrolment cancelled by user
}
Expand All @@ -613,7 +613,7 @@ powerAuthSDK.persistActivation(context, fragment, "Enable Biometric Authenticati
```
```java
// Persist activation using given PIN and ad-hoc generated biometric related key
powerAuthSDK.persistActivation(context, fragment, "Enable Biometric Authentication", "To enable biometric authentication, use the biometric sensor on your device.", pin, new IPersistActivationWithBiometryListener() {
powerAuthSDK.persistActivation(context, fragment, "Enable Biometric Authentication", "To enable biometric authentication, use the biometric sensor on your device.", pin, new IPersistActivationWithBiometricsListener() {
@Override
public void onBiometricDialogCancelled() {
// Biometric enrolment cancelled by user
Expand Down Expand Up @@ -1772,15 +1772,13 @@ In order to obtain an encrypted biometry factor-related key for the purpose of a
<!-- begin codetabs Kotlin Java -->
```kotlin
// Authenticate user with biometry and obtain encrypted biometry factor related key.
powerAuthSDK.authenticateUsingBiometry(context, fragment, "Sign in", "Use the biometric sensor on your device to continue", object: IBiometricAuthenticationCallback {
powerAuthSDK.authenticateUsingBiometrics(context, fragment, "Sign in", "Use the biometric sensor on your device to continue", object: IAuthenticateWithBiometricsListener {
override fun onBiometricDialogCancelled(userCancel: Boolean) {
// User cancelled the operation
}

override fun onBiometricDialogSuccess(biometricKeyData: BiometricKeyData) {
// User authenticated and biometry key was returned, now you can construct PowerAuthAuthentication object with proper signing capabilities.
val biometryFactorRelatedKey = biometricKeyData.derivedData
val twoFactorBiometry = PowerAuthAuthentication.possessionWithBiometry(biometryFactorRelatedKey)
override fun onBiometricDialogSuccess(authentication: PowerAuthAuthentication) {
// User authenticated use the provided authentication object for other tasks.
}

override fun onBiometricDialogFailed(error: PowerAuthErrorException) {
Expand All @@ -1790,17 +1788,15 @@ powerAuthSDK.authenticateUsingBiometry(context, fragment, "Sign in", "Use the bi
```
```java
// Authenticate user with biometry and obtain encrypted biometry factor related key.
powerAuthSDK.authenticateUsingBiometry(context, fragment, "Sign in", "Use the biometric sensor on your device to continue", new IBiometricAuthenticationCallback() {
powerAuthSDK.authenticateUsingBiometrics(context, fragment, "Sign in", "Use the biometric sensor on your device to continue", new IAuthenticateWithBiometricsListener() {
@Override
public void onBiometricDialogCancelled(boolean userCancel) {
// User cancelled the operation
}

@Override
public void onBiometricDialogSuccess(BiometricKeyData biometricKeyData) {
// User authenticated and biometry key was returned, now you can construct PowerAuthAuthentication object with proper signing capabilities.
final byte[] biometryFactorRelatedKey = biometricKeyData.getDerivedData();
final PowerAuthAuthentication twoFactorBiometry = PowerAuthAuthentication.possessionWithBiometry(biometryFactorRelatedKey);
public void onBiometricDialogSuccess(PowerAuthAuthentication authentication) {
// User authenticated use the provided authentication object for other tasks.
}

@Override
Expand Down Expand Up @@ -1848,6 +1844,8 @@ Be aware that the configuration above is effective only for the new keys. So, if

The `BiometricAuthentication` class is a high level interface that provides interfaces related to the biometric authentication for the SDK, or for the application purposes. The class hides all technical details, so it can be safely used also on the systems that doesn't provide biometric interfaces, or if the system has no biometric sensor available. The implementation under the hood uses `androidx.biometric.BiometricPrompt` and `androidx.biometric.BiometricManager` classes.

#### Customize Biometric Dialog Resources

To customize the strings used in biometric authentication, you can use `BiometricDialogResources` in the following manner:

<!-- begin codetabs Kotlin Java -->
Expand Down Expand Up @@ -1879,6 +1877,44 @@ BiometricAuthentication.setBiometricDialogResources(resources);
```
<!-- end -->

#### Disable Error Dialog After Failed Biometry

If you prefer not to allow the PowerAuth mobile SDK to display its own error dialog, you can disable this feature globally. In this case, your application will need to handle all error situations through its own user interface. Use the following code to disable the error dialog:

```kotlin
BiometricAuthentication.setBiometricErrorDialogDisabled(true)
```

When the error dialog is disabled, your application should inform the user of the reason for the failure. Handling this might be somewhat tricky because there are situations where the biometric authentication dialog is not displayed at all, and the failure is reported directly to the application. To address this, you can use the `BiometricErrorInfo` enumeration, which is associated with the reported `PowerAuthErrorException`. The code snippet below outlines how to determine the situation:

```kotlin
// Authenticate user with biometry and obtain encrypted biometry factor related key.
powerAuthSDK.authenticateUsingBiometrics(context, fragment, "Sign in", "Use the biometric sensor on your device to continue", object: IAuthenticateWithBiometricsListener {
override fun onBiometricDialogCancelled(userCancel: Boolean) {
// User or system cancelled the operation
}

override fun onBiometricDialogSuccess(authentication: PowerAuthAuthentication) {
// Success
}

override fun onBiometricDialogFailed(error: PowerAuthErrorException) {
if (error.additionalInformation == BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON) {
// Application should display error in its own UI
when (error.powerAuthErrorCode) {
PowerAuthErrorCodes.BIOMETRY_LOCKOUT -> println("Lockout")
PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE -> println("Not available, try later")
PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED -> println("Fingerprint or face not recognized") // check inline documentation for more details
PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED -> println("Device has no biometry sensor")
PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED -> println("Device has no biometry data enrolled")
}
}
}
})
```

#### Biometric Authentication Confirmation

On Android 10+ systems, it's possible to configure `BiometricPrompt` to ask for an additional confirmation after the user is successfully authenticated. The default behavior for PowerAuth Mobile SDK is that such confirmation is not required. To change this behavior, you have to provide `PowerAuthKeychainConfiguration` object with `confirmBiometricAuthentication` parameter set to `true` and use that configuration for the `PowerAuthSDK` instance construction:

<!-- begin codetabs Kotlin Java -->
Expand Down Expand Up @@ -2559,6 +2595,7 @@ when (t) {
PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED -> Log.d(TAG, "The device or operating system doesn't support biometric authentication.")
PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE -> Log.d(TAG, "The biometric authentication is temporarily unavailable.")
PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED -> Log.d(TAG, "The biometric authentication did not recognize the biometric image (fingerprint, face, etc...)")
PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED -> Log.d(TAG, "The biometric authentication failed because there's no biometry enrolled")
PowerAuthErrorCodes.BIOMETRY_LOCKOUT -> Log.d(TAG, "The biometric authentication is locked out due to too many failed attempts.")
PowerAuthErrorCodes.OPERATION_CANCELED -> Log.d(TAG, "Error code for cancelled operations")
PowerAuthErrorCodes.ENCRYPTION_ERROR -> Log.d(TAG, "Error code for errors related to end-to-end encryption")
Expand All @@ -2567,6 +2604,13 @@ when (t) {
PowerAuthErrorCodes.PENDING_PROTOCOL_UPGRADE -> Log.d(TAG, "The operation is temporarily unavailable, due to pending protocol upgrade.")
PowerAuthErrorCodes.TIME_SYNCHRONIZATION -> Log.d(TAG, "Failed to synchronize time with the server.")
}
// Process additional information
when (t.additionalInformation) {
BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON -> {
// Application should display error dialog after failed biometric authentication. This is relevant only
// if you disabled the biometric error dialog provided by PowerAuth mobile SDK.
}
}
}
is ErrorResponseApiException -> {
val errorResponse: Error? = t.errorResponse
Expand Down Expand Up @@ -2609,6 +2653,8 @@ if (t instanceof PowerAuthErrorException) {
android.util.Log.d(TAG,"The biometric authentication is temporarily unavailable."); break;
case PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED:
android.util.Log.d(TAG,"The biometric authentication did not recognize the biometric image (fingerprint, face, etc...)"); break;
case PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED:
android.util.Log.d(TAG,"The biometric authentication failed because there's no biometry enrolled"); break;
case PowerAuthErrorCodes.BIOMETRY_LOCKOUT:
android.util.Log.d(TAG,"The biometric authentication is locked out due to too many failed attempts."); break;
case PowerAuthErrorCodes.OPERATION_CANCELED:
Expand All @@ -2624,6 +2670,12 @@ if (t instanceof PowerAuthErrorException) {
case PowerAuthErrorCodes.TIME_SYNCHRONIZATION:
android.util.Log.d(TAG,"Failed to synchronize time with the server."); break;
}
// Process additional information
if (BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON.equals(exception.getAdditionalInformation())) {
// Application should display error dialog after failed biometric authentication. This is relevant only
// if you disabled the biometric error dialog provided by PowerAuth mobile SDK.
}

} else if (t instanceof ErrorResponseApiException) {
ErrorResponseApiException exception = (ErrorResponseApiException) t;
Error errorResponse = exception.getErrorResponse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public void onBiometricKeyUnavailable() {
}
});
final IBiometricKeyEncryptorProvider biometricKeyEncryptorProvider = new DefaultBiometricKeyEncryptorProvider(request, getBiometricKeystore());
final PrivateRequestData requestData = new PrivateRequestData(request, biometricKeyEncryptorProvider, dispatcher, ctx.getBiometricDialogResources());
final PrivateRequestData requestData = new PrivateRequestData(request, biometricKeyEncryptorProvider, dispatcher, ctx.getBiometricDialogResources(), ctx.isBiometricErrorDialogDisabled());

// Validate request status
@BiometricStatus int status = device.canAuthenticate();
Expand Down Expand Up @@ -176,7 +176,14 @@ public void onBiometricKeyUnavailable() {
if (exception == null) {
exception = BiometricHelper.getExceptionForBiometricStatus(status);
}
return showErrorDialog(status, exception, context, requestData);
if (requestData.isErrorDialogDisabled()) {
// Error dialog is disabled, so report the error immediately. Use "no visible reason" hint.
dispatcher.dispatchError(BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON.addToException(exception));
return dispatcher.getCancelableTask();
} else {
// Error dialog is not disabled, so we can show it. Use "visible reason" hint.
return showErrorDialog(status, BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON.addToException(exception), context, requestData);
}
}
}

Expand Down Expand Up @@ -310,6 +317,32 @@ public static void setBiometricDialogResources(@NonNull BiometricDialogResources
}
}

/**
* Disable or enable error dialog provided by PowerAuth mobile SDK and displayed after failed biometric authentication.
* <p>
* If set to {@code true}, then the custom error dialog provided by the PowerAuth mobile SDK will never
* be displayed in the case of authentication failure. The mobile application should handle all possible error
* states using its own UI elements. The default value for this property is {@code false}, and the PowerAuth mobile
* SDK may display its own error dialog.
*
* @param disabled If {@code true}, then the PowerAuth mobile SDK will never display its own error dialog.
*/
public static void setBiometricErrorDialogDisabled(boolean disabled) {
synchronized (SharedContext.class) {
getContext().setBiometricErrorDialogDisabled(disabled);
}
}

/**
* Return information whether error dialog provided by PowerAuth mobile SDK is disabled or enabled.
* @return {@code true} in case that the PowerAuth mobile SDK will never display its own error dialog, {@code false} otherwise.
*/
public static boolean isBiometricErrorDialogDisabled() {
synchronized (SharedContext.class) {
return getContext().isBiometricErrorDialogDisabled();
}
}

/**
* Return type of biometry supported on the system.
*
Expand Down Expand Up @@ -344,6 +377,12 @@ private static class SharedContext {
*/
private @Nullable IBiometricAuthenticator authenticator;

/**
* Contains {@code true} in case that application want's to deal with authentication errors in its own UI.
* The default value is {@code false};
*/
private boolean isBiometricErrorDialogDisabled = false;

/**
* Contains {@code true} in case that there's already pending biometric authentication.
*/
Expand Down Expand Up @@ -372,6 +411,20 @@ void setBiometricDialogResources(@NonNull BiometricDialogResources resources) {
return biometricDialogResources;
}

/**
* @param disabled if true, then error dialog provided by PowerAuth mobile SDK will be disabled.
*/
void setBiometricErrorDialogDisabled(boolean disabled) {
isBiometricErrorDialogDisabled = disabled;
}

/**
* @return true when error dialog provided by PowerAuth mobile SDK is be disabled.
*/
boolean isBiometricErrorDialogDisabled() {
return isBiometricErrorDialogDisabled;
}

/**
* Returns object implementing {@link IBiometricAuthenticator} interface. The returned implementation
* depends on the version of Android system and on the authenticator's capabilities. If current system
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2023 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getlime.security.powerauth.biometry;

import androidx.annotation.NonNull;
import io.getlime.security.powerauth.exception.PowerAuthErrorCodes;
import io.getlime.security.powerauth.exception.PowerAuthErrorException;

/**
* The {@code BiometricErrorInfo} enumeration contains an information associated with {@link PowerAuthErrorException}.
* The enumeration is available only if the exception's error code is one of:
* <ul>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_LOCKOUT}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_AVAILABLE}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_RECOGNIZED}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_SUPPORTED}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_ENROLLED}</li>
* </ul>
*/
public enum BiometricErrorInfo {
/**
* The biometric authentication failed and the reason of failure was already displayed in the authentication dialog.
*/
BIOMETRICS_FAILED_WITH_VISIBLE_REASON,
/**
* The biometric authentication failed and the reason of failure was not displayed in the authentication dialog.
* In this case, application should properly investigate the reason of the failure and display an appropriate
* error information.
*/
BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON
;

/**
* If the provided exception is biometry-related, then create a new instance of {@link PowerAuthErrorException}
* with the same error code, message and cause and use this enumeration as a source of additional information.
* THe additional information can be later retrieved with {@link PowerAuthErrorException#getAdditionalInformation()}.
* @param exception Exception to enhance.
* @return new exception enhanced with additional information or the original exception if it's not biometry-related.
*/
@NonNull
public PowerAuthErrorException addToException(@NonNull PowerAuthErrorException exception) {
final int errorCode = exception.getPowerAuthErrorCode();
if (errorCode == PowerAuthErrorCodes.BIOMETRY_LOCKOUT ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED) {
return new PowerAuthErrorException(errorCode, exception.getMessage(), exception.getCause(), this);
}
return exception;
}
}
Loading

0 comments on commit cb40343

Please sign in to comment.