diff --git a/.gitignore b/.gitignore index 38567756..af2dc726 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ xcuserdata *.xcuserstate *.xcscmblueprint +## VSCode +.vscode + ## Obj-C/Swift specific *.hmap *.ipa diff --git a/docs/PowerAuth-SDK-for-Android.md b/docs/PowerAuth-SDK-for-Android.md index 8ff919f3..b4e36947 100644 --- a/docs/PowerAuth-SDK-for-Android.md +++ b/docs/PowerAuth-SDK-for-Android.md @@ -597,7 +597,7 @@ This code has created activation with two factors: possession (key stored using ```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 } @@ -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 @@ -1772,15 +1772,13 @@ In order to obtain an encrypted biometry factor-related key for the purpose of a ```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) { @@ -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 @@ -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: @@ -1879,6 +1877,44 @@ BiometricAuthentication.setBiometricDialogResources(resources); ``` +#### 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: @@ -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") @@ -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 @@ -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: @@ -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(); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java index f49bd9ad..979ec659 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java @@ -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(); @@ -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); + } } } @@ -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. + *

+ * 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. * @@ -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. */ @@ -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 diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricErrorInfo.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricErrorInfo.java new file mode 100644 index 00000000..4f544d01 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricErrorInfo.java @@ -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: + *

+ */ +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; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IAuthenticateWithBiometricsListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IAuthenticateWithBiometricsListener.java new file mode 100644 index 00000000..1865a742 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IAuthenticateWithBiometricsListener.java @@ -0,0 +1,54 @@ +/* + * 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 androidx.annotation.UiThread; +import io.getlime.security.powerauth.exception.PowerAuthErrorException; +import io.getlime.security.powerauth.sdk.PowerAuthAuthentication; + +/** + * Interface used as a callback for biometric authentication for general application use. + */ +public interface IAuthenticateWithBiometricsListener { + /** + * Biometric authentication dialog was cancelled by the user or externally, by calling {@code cancel()} + * on cancelable object returned from authenticate() method. + * + * @param userCancel If parameter is {@code true}, then the dialog was canceled by the user. The {@code false} + * value means that authentication request was canceled by your code, by calling {@code cancel()} + * on provided cancelable object. + */ + @UiThread + void onBiometricDialogCancelled(boolean userCancel); + + /** + * Biometric authentication succeeded. + * + * @param authentication {@link PowerAuthAuthentication} configured for combination of possession and biometry factors. + */ + @UiThread + void onBiometricDialogSuccess(@NonNull PowerAuthAuthentication authentication); + + /** + * Biometric authentication failed with the error. + * + * @param error {@link PowerAuthErrorException} contains reason of the failure. + */ + @UiThread + void onBiometricDialogFailed(@NonNull PowerAuthErrorException error); +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricAuthenticationCallback.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricAuthenticationCallback.java index c0ada97d..6fdd45d3 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricAuthenticationCallback.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IBiometricAuthenticationCallback.java @@ -22,7 +22,7 @@ import io.getlime.security.powerauth.exception.PowerAuthErrorException; /** - * Interface used as a callback for general biometric authentication. + * Interface used as a callback for biometric authentication for an internal SDK purposes. */ public interface IBiometricAuthenticationCallback { diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/ICommitActivationWithBiometryListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/ICommitActivationWithBiometryListener.java index 99f4ef52..48f8b771 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/ICommitActivationWithBiometryListener.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/ICommitActivationWithBiometryListener.java @@ -18,8 +18,8 @@ /** * Interface used as a callback for persisting the activation with biometric authentication. - * @deprecated Use {@link IPersistActivationWithBiometryListener} as a replacement. + * @deprecated Use {@link IPersistActivationWithBiometricsListener} as a replacement. */ @Deprecated // 1.8.0 -public interface ICommitActivationWithBiometryListener extends IPersistActivationWithBiometryListener { +public interface ICommitActivationWithBiometryListener extends IPersistActivationWithBiometricsListener { } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IPersistActivationWithBiometryListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IPersistActivationWithBiometricsListener.java similarity index 95% rename from proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IPersistActivationWithBiometryListener.java rename to proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IPersistActivationWithBiometricsListener.java index 38fe4939..b6f266d6 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IPersistActivationWithBiometryListener.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/IPersistActivationWithBiometricsListener.java @@ -22,7 +22,7 @@ /** * Interface used as a callback for persisting the activation with biometric authentication. */ -public interface IPersistActivationWithBiometryListener { +public interface IPersistActivationWithBiometricsListener { /** * Biometric dialog was cancelled. */ diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java index 0aa4bfbe..61f6fe0b 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java @@ -33,13 +33,7 @@ import javax.crypto.Cipher; -import io.getlime.security.powerauth.biometry.BiometricAuthenticationRequest; -import io.getlime.security.powerauth.biometry.BiometricDialogResources; -import io.getlime.security.powerauth.biometry.BiometricKeyData; -import io.getlime.security.powerauth.biometry.BiometricStatus; -import io.getlime.security.powerauth.biometry.BiometryType; -import io.getlime.security.powerauth.biometry.IBiometricKeyEncryptor; -import io.getlime.security.powerauth.biometry.IBiometricKeystore; +import io.getlime.security.powerauth.biometry.*; import io.getlime.security.powerauth.exception.PowerAuthErrorCodes; import io.getlime.security.powerauth.exception.PowerAuthErrorException; import io.getlime.security.powerauth.networking.interfaces.ICancelable; @@ -202,34 +196,65 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON; final boolean isQuickCancel = isCancel && detectQuickCancelAfterFailure(); final boolean isLockout = errorCode == BiometricPrompt.ERROR_LOCKOUT || errorCode == BiometricPrompt.ERROR_LOCKOUT_PERMANENT; + final boolean notAvailable = errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT; + final boolean notEnrolled = errorCode == BiometricPrompt.ERROR_NO_BIOMETRICS; if (isCancel && !isQuickCancel) { // User pressed the cancel button, or authentication was canceled by the system. // That may happen when user hit the power button and lock the device. We can - // both situations report as an user initiated cancel. + // both situations report as user initiated cancel. dispatcher.dispatchUserCancel(); } else { + // Build a proper exception with the reason of failure. final PowerAuthErrorException exception; + + // Determine whether the error dialog should be displayed + final boolean shouldDisplayError = shouldDisplayErrorDialog(requestData); + final boolean errorDialogDisabled = requestData.isErrorDialogDisabled(); + final boolean displayError; + final BiometricErrorInfo errorInfo; + + // Prepare error info in advance + if (shouldDisplayError && !errorDialogDisabled) { + // We should display error and dialog is not disabled. Our dialog will show the reason of failure. + errorInfo = BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON; + displayError = true; + } else if (shouldDisplayError) { + // We should display error, but dialog is disabled. The reason of failure was not presented yet. + errorInfo = BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON; + displayError = false; + } else { + // Error should not be displayed, so the reason was already presented in system UI. + errorInfo = BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON; + displayError = false; + } + if (isLockout || isQuickCancel) { if (authenticationFailedBefore > 0) { // Too many failed attempts, we should report the "not recognized" error after all. // If `authenticationFailedBefore` is greater than 0, then it means that sensor did a multiple failed attempts // in this round. So we're pretty sure that biometric authentication dialog was properly displayed. - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED, "Biometric image was not recognized."); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED, "Biometric image was not recognized", null, errorInfo); } else { // Too many failed attempts, but no authentication dialog was displayed in this round. It looks like that // the error was immediately reported back to us, so we can report "lockout" to the application. - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_LOCKOUT, "Too many failed attempts."); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_LOCKOUT, "Too many failed attempts", null, errorInfo); } + } else if (notEnrolled) { + // Biometry is not enrolled. + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED, "Biometry not enrolled", null, errorInfo); + } else if (notAvailable) { + // Biometry is not supported + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED, "Biometry not supported", null, errorInfo); } else { // Other error, we can use "not available" error code, due to that other // errors are mostly about an internal failures. - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, errString.toString()); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, errString.toString(), null, errorInfo); } - if (shouldDisplayErrorDialog(requestData)) { - // The response from API was too quick. We should display our own UI. + + // Show error dialog first or dispatch the failure immediately. + if (displayError) { showBiometricErrorDialog(errString, exception, requestData); } else { - // Otherwise dispatch the error. dispatcher.dispatchError(exception); } } @@ -263,13 +288,20 @@ public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationRes // 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.BIOMETRY_NOT_AVAILABLE, "Failed to encrypt biometric key."); + final boolean showDialog = !requestData.isErrorDialogDisabled(); + final BiometricErrorInfo errorInfo = showDialog + ? BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON + : BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON; + final PowerAuthErrorException exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, "Failed to encrypt biometric key.", null, errorInfo); + if (showDialog) { + // Display error dialog + dispatcher.dispatchRunnable(() -> { showErrorDialogAfterSuccess(requestData, exception); - } - }); + }); + } else { + // Just report the failure + dispatcher.dispatchError(exception); + } } @Override @@ -529,8 +561,8 @@ public void onCancel() { * Function determines whether user is trying to trick the sensor by quick pressing cancel * after last failure. The functionality is available only on devices older than Android 9, * where BiometricPrompt is implemented by `androidx.biometric` support library. - * - * See issue: https://github.com/wultra/powerauth-mobile-sdk/issues/422 + *

+ * See issue: wultra/powerauth-mobile-sdk#422 * @return true if quick cancel after failure is detected. */ private boolean detectQuickCancelAfterFailure() { diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/PrivateRequestData.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/PrivateRequestData.java index 273c4fd5..6f58a173 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/PrivateRequestData.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/PrivateRequestData.java @@ -34,6 +34,7 @@ public class PrivateRequestData { private final @NonNull BiometricResultDispatcher dispatcher; private final @NonNull BiometricDialogResources resources; private final @NonNull IBiometricKeyEncryptorProvider biometricKeyEncryptorProvider; + private final boolean errorDialogDisabled; private final long creationTime; /** @@ -43,16 +44,19 @@ public class PrivateRequestData { * @param biometricKeyEncryptorProvider Object that provide {@link IBiometricKeyEncryptor} on demand. * @param dispatcher Dispatcher that holds completion callback and callback dispatcher. * @param resources Resources required for the legacy implementation. + * @param errorDialogDisabled If true then error dialog should not be displayed. */ public PrivateRequestData(@NonNull BiometricAuthenticationRequest request, @NonNull IBiometricKeyEncryptorProvider biometricKeyEncryptorProvider, @NonNull BiometricResultDispatcher dispatcher, - @NonNull BiometricDialogResources resources) { + @NonNull BiometricDialogResources resources, + boolean errorDialogDisabled) { this.request = request; this.biometricKeyEncryptorProvider = biometricKeyEncryptorProvider; this.dispatcher = dispatcher; this.resources = resources; this.creationTime = SystemClock.elapsedRealtime(); + this.errorDialogDisabled = errorDialogDisabled; } /** @@ -90,6 +94,13 @@ public long getElapsedTime() { return SystemClock.elapsedRealtime() - creationTime; } + /** + * @return {@code true} if error dialog after failed authentication should not be displayed. + */ + public boolean isErrorDialogDisabled() { + return errorDialogDisabled; + } + /** * This helper method return {@link FragmentManager} from Fragment or FragmentActivity * provided in {@link BiometricAuthenticationRequest}. The method throws {@code IllegalStateException} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorCodes.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorCodes.java index 31eb1e22..a34d28ec 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorCodes.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorCodes.java @@ -33,7 +33,8 @@ INVALID_TOKEN, ENCRYPTION_ERROR, WRONG_PARAMETER, PROTOCOL_UPGRADE, PENDING_PROTOCOL_UPGRADE, BIOMETRY_NOT_SUPPORTED, BIOMETRY_NOT_AVAILABLE, BIOMETRY_NOT_RECOGNIZED, - INSUFFICIENT_KEYCHAIN_PROTECTION, BIOMETRY_LOCKOUT, TIME_SYNCHRONIZATION}) + INSUFFICIENT_KEYCHAIN_PROTECTION, BIOMETRY_LOCKOUT, TIME_SYNCHRONIZATION, + BIOMETRY_NOT_ENROLLED}) public @interface PowerAuthErrorCodes { /** @@ -162,4 +163,9 @@ * Failed to synchronize time with the server. */ int TIME_SYNCHRONIZATION = 23; + + /** + * The biometric authentication failed because there's no biometry enrolled on the device. + */ + int BIOMETRY_NOT_ENROLLED = 24; } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorException.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorException.java index 378d8d59..5d68f088 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorException.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/exception/PowerAuthErrorException.java @@ -17,6 +17,7 @@ package io.getlime.security.powerauth.exception; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; /** * Will be thrown, or will be returned to listener, in case that requested operation fails @@ -29,12 +30,17 @@ public class PowerAuthErrorException extends Exception { */ @PowerAuthErrorCodes private final int powerAuthErrorCode; + /** + * Additional information associated with the failure reason. The m + */ + private final Object additionalInformation; /** * @param powerAuthErrorCode Integer constant from {@link PowerAuthErrorCodes} */ public PowerAuthErrorException(@PowerAuthErrorCodes int powerAuthErrorCode) { this.powerAuthErrorCode = powerAuthErrorCode; + this.additionalInformation = null; } /** @@ -44,6 +50,7 @@ public PowerAuthErrorException(@PowerAuthErrorCodes int powerAuthErrorCode) { public PowerAuthErrorException(@PowerAuthErrorCodes int powerAuthErrorCode, String message) { super(message); this.powerAuthErrorCode = powerAuthErrorCode; + this.additionalInformation = null; } /** @@ -54,6 +61,19 @@ public PowerAuthErrorException(@PowerAuthErrorCodes int powerAuthErrorCode, Stri public PowerAuthErrorException(@PowerAuthErrorCodes int powerAuthErrorCode, String message, Throwable cause) { super(message, cause); this.powerAuthErrorCode = powerAuthErrorCode; + this.additionalInformation = null; + } + + /** + * @param powerAuthErrorCode Integer constant from {@link PowerAuthErrorCodes} + * @param message String with detailed error description. + * @param cause Original cause of failure. + * @param additionalInformation Additional information. + */ + public PowerAuthErrorException(@PowerAuthErrorCodes int powerAuthErrorCode, String message, Throwable cause, Object additionalInformation) { + super(message, cause); + this.powerAuthErrorCode = powerAuthErrorCode; + this.additionalInformation = additionalInformation; } /** @@ -64,6 +84,16 @@ public int getPowerAuthErrorCode() { return powerAuthErrorCode; } + /** + * Get additional information that may help with the error processing. If the error is biometry-related, then + * you can obtain {@link io.getlime.security.powerauth.biometry.BiometricErrorInfo} enumeration in this property. + * @return Additional information that help with error processing. + */ + @Nullable + public Object getAdditionalInformation() { + return additionalInformation; + } + /** * Wrap {@link Throwable} cause of failure into {@link PowerAuthErrorException} with provided * error code and message. In case that original exception is already {@link PowerAuthErrorException}, diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IGenerateTokenHeaderListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IGenerateTokenHeaderListener.java index 6ec24b8c..ed513a88 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IGenerateTokenHeaderListener.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IGenerateTokenHeaderListener.java @@ -26,6 +26,8 @@ public interface IGenerateTokenHeaderListener { /** * Called when generating token header succeeded. + * + * @param header Authorization header. */ @MainThread void onGenerateTokenHeaderSucceeded(@NonNull PowerAuthAuthorizationHttpHeader header); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IServerStatusListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IServerStatusListener.java index d9e8db2b..13e52b49 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IServerStatusListener.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IServerStatusListener.java @@ -25,6 +25,8 @@ public interface IServerStatusListener { /** * Called when getting server status succeeded. + * + * @param status Received server status. */ @MainThread void onServerStatusSucceeded(@NonNull ServerStatus status); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthConfiguration.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthConfiguration.java index e3bac289..0c9b4f1a 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthConfiguration.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthConfiguration.java @@ -16,15 +16,12 @@ package io.getlime.security.powerauth.sdk; -import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Arrays; import io.getlime.security.powerauth.core.SessionSetup; -import io.getlime.security.powerauth.sdk.impl.IPossessionFactorEncryptionKeyProvider; /** * Class representing a configuration of a single PowerAuthSDK instance. diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java index 44ee4a6e..38f2f54a 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java @@ -940,8 +940,8 @@ public ICancelable persistActivation( @NonNull String title, @NonNull String description, @NonNull final String password, - final @NonNull IPersistActivationWithBiometryListener callback) { - return persistActivationWithBiometryImpl(context, FragmentHelper.from(fragmentActivity), title, description, new Password(password), callback); + final @NonNull IPersistActivationWithBiometricsListener callback) { + return persistActivationWithBiometricsImpl(context, FragmentHelper.from(fragmentActivity), title, description, new Password(password), callback); } /** @@ -963,8 +963,8 @@ public ICancelable persistActivation( @NonNull String title, @NonNull String description, @NonNull final Password password, - final @NonNull IPersistActivationWithBiometryListener callback) { - return persistActivationWithBiometryImpl(context, FragmentHelper.from(fragmentActivity), title, description, password, callback); + final @NonNull IPersistActivationWithBiometricsListener callback) { + return persistActivationWithBiometricsImpl(context, FragmentHelper.from(fragmentActivity), title, description, password, callback); } /** @@ -986,8 +986,8 @@ public ICancelable persistActivation( @NonNull String title, @NonNull String description, @NonNull final String password, - final @NonNull IPersistActivationWithBiometryListener callback) { - return persistActivationWithBiometryImpl(context, FragmentHelper.from(fragment), title, description, new Password(password), callback); + final @NonNull IPersistActivationWithBiometricsListener callback) { + return persistActivationWithBiometricsImpl(context, FragmentHelper.from(fragment), title, description, new Password(password), callback); } /** @@ -1009,8 +1009,8 @@ public ICancelable persistActivation( @NonNull String title, @NonNull String description, @NonNull final Password password, - final @NonNull IPersistActivationWithBiometryListener callback) { - return persistActivationWithBiometryImpl(context, FragmentHelper.from(fragment), title, description, password, callback); + final @NonNull IPersistActivationWithBiometricsListener callback) { + return persistActivationWithBiometricsImpl(context, FragmentHelper.from(fragment), title, description, password, callback); } /** @@ -1026,14 +1026,14 @@ public ICancelable persistActivation( */ @UiThread @NonNull - private ICancelable persistActivationWithBiometryImpl( + private ICancelable persistActivationWithBiometricsImpl( final @NonNull Context context, @NonNull FragmentHelper fragmentHelper, @NonNull String title, @NonNull String description, @NonNull final Password password, - final @NonNull IPersistActivationWithBiometryListener callback) { - return authenticateUsingBiometry(context, fragmentHelper, title, description, true, new IBiometricAuthenticationCallback() { + final @NonNull IPersistActivationWithBiometricsListener callback) { + return authenticateUsingBiometrics(context, fragmentHelper, title, description, true, new IBiometricAuthenticationCallback() { @Override public void onBiometricDialogCancelled(boolean userCancel) { if (userCancel) { @@ -1237,7 +1237,7 @@ public int commitActivationWithPassword(@NonNull Context context, @NonNull Strin * @param password Password used to persist activation. * @param callback Callback with the authentication result. * @return {@link ICancelable} object associated with the biometric prompt. - * @deprecated Use {@link #persistActivation(Context, Fragment, String, String, Password, IPersistActivationWithBiometryListener)} as a replacement. + * @deprecated Use {@link #persistActivation(Context, Fragment, String, String, Password, IPersistActivationWithBiometricsListener)} as a replacement. */ @UiThread @NonNull @@ -1248,7 +1248,7 @@ public ICancelable commitActivation( @NonNull String title, @NonNull String description, @NonNull final Password password, - final @NonNull IPersistActivationWithBiometryListener callback) { + final @NonNull IPersistActivationWithBiometricsListener callback) { return persistActivation(context, fragment, title, description, password, callback); } @@ -1262,7 +1262,7 @@ public ICancelable commitActivation( * @param password Password used to persist activation. * @param callback Callback with the authentication result. * @return {@link ICancelable} object associated with the biometric prompt. - * @deprecated Use {@link #persistActivation(Context, Fragment, String, String, String, IPersistActivationWithBiometryListener)} as a replacement. + * @deprecated Use {@link #persistActivation(Context, Fragment, String, String, String, IPersistActivationWithBiometricsListener)} as a replacement. */ @UiThread @NonNull @@ -1273,7 +1273,7 @@ public ICancelable commitActivation( @NonNull String title, @NonNull String description, @NonNull final String password, - final @NonNull IPersistActivationWithBiometryListener callback) { + final @NonNull IPersistActivationWithBiometricsListener callback) { return persistActivation(context, fragment, title, description, password, callback); } @@ -1287,7 +1287,7 @@ public ICancelable commitActivation( * @param password Password used to persist activation. * @param callback Callback with the authentication result. * @return {@link ICancelable} object associated with the biometric prompt. - * @deprecated Use {@link #persistActivation(Context, FragmentActivity, String, String, String, IPersistActivationWithBiometryListener)} as a replacement. + * @deprecated Use {@link #persistActivation(Context, FragmentActivity, String, String, String, IPersistActivationWithBiometricsListener)} as a replacement. */ @UiThread @NonNull @@ -1298,7 +1298,7 @@ public ICancelable commitActivation( @NonNull String title, @NonNull String description, @NonNull final String password, - final @NonNull IPersistActivationWithBiometryListener callback) { + final @NonNull IPersistActivationWithBiometricsListener callback) { return persistActivation(context, fragmentActivity, title, description, password, callback); } @@ -1312,7 +1312,7 @@ public ICancelable commitActivation( * @param password Password used to persist activation. * @param callback Callback with the authentication result. * @return {@link ICancelable} object associated with the biometric prompt. - * @deprecated Use {@link #persistActivation(Context, FragmentActivity, String, String, Password, IPersistActivationWithBiometryListener)} as a replacement. + * @deprecated Use {@link #persistActivation(Context, FragmentActivity, String, String, Password, IPersistActivationWithBiometricsListener)} as a replacement. */ @UiThread @NonNull @@ -1323,7 +1323,7 @@ public ICancelable commitActivation( @NonNull String title, @NonNull String description, @NonNull final Password password, - final @NonNull IPersistActivationWithBiometryListener callback) { + final @NonNull IPersistActivationWithBiometricsListener callback) { return persistActivation(context, fragmentActivity, title, description, password, callback); } @@ -2067,7 +2067,7 @@ private ICancelable addBiometryFactorImpl( public void onFetchEncryptedVaultUnlockKeySucceed(final String encryptedEncryptionKey) { if (encryptedEncryptionKey != null) { // Authenticate using biometry to generate a key - final ICancelable biometricAuthentication = authenticateUsingBiometry(context, fragmentHelper, title, description, true, new IBiometricAuthenticationCallback() { + final ICancelable biometricAuthentication = authenticateUsingBiometrics(context, fragmentHelper, title, description, true, new IBiometricAuthenticationCallback() { @Override public void onBiometricDialogCancelled(boolean userCancel) { if (userCancel) { @@ -2301,8 +2301,78 @@ public void onCancel() { } /** - * Authenticate a client using biometric authentication. In case of the authentication is successful and {@link IBiometricAuthenticationCallback#onBiometricDialogSuccess(BiometricKeyData)} callback is called, - * you can use {@code biometricKeyEncrypted} as a parameter to {@link PowerAuthAuthentication#useBiometry} property. + * Authenticate a client using biometric authentication. In case of the authentication is successful and + * {@link IAuthenticateWithBiometricsListener#onBiometricDialogSuccess(PowerAuthAuthentication)} callback is called. + * + * @param context Context. + * @param fragment The fragment of the application that will host the prompt. + * @param title Dialog title. + * @param description Dialog description. + * @param listener Callback with the authentication result. + * @return {@link ICancelable} object associated with the biometric prompt. + */ + @UiThread + @NonNull + public ICancelable authenticateUsingBiometrics( + @NonNull Context context, + @NonNull Fragment fragment, + @NonNull String title, + @NonNull String description, + final @NonNull IAuthenticateWithBiometricsListener listener) { + return authenticateUsingBiometrics(context, FragmentHelper.from(fragment), title, description, false, getBiometricCallbackWithListener(listener)); + } + + /** + * Authenticate a client using biometric authentication. In case of the authentication is successful and + * {@link IAuthenticateWithBiometricsListener#onBiometricDialogSuccess(PowerAuthAuthentication)} callback is called. + * + * @param context Context. + * @param fragmentActivity The activity of the application that will host the prompt. + * @param title Dialog title. + * @param description Dialog description. + * @param listener Callback with the authentication result. + * @return {@link ICancelable} object associated with the biometric prompt. + */ + @UiThread + @NonNull + public ICancelable authenticateUsingBiometrics( + @NonNull Context context, + @NonNull FragmentActivity fragmentActivity, + @NonNull String title, + @NonNull String description, + final @NonNull IAuthenticateWithBiometricsListener listener) { + return authenticateUsingBiometrics(context, FragmentHelper.from(fragmentActivity), title, description, false, getBiometricCallbackWithListener(listener)); + } + + /** + * Create low level biometric authentication callback that bridge the result to the provided listener. + * @param listener Target listener. + * @return Instance of {@link IBiometricAuthenticationCallback}. + */ + @NonNull + private IBiometricAuthenticationCallback getBiometricCallbackWithListener(@NonNull IAuthenticateWithBiometricsListener listener) { + return new IBiometricAuthenticationCallback() { + @Override + public void onBiometricDialogCancelled(boolean userCancel) { + listener.onBiometricDialogCancelled(userCancel); + } + + @Override + public void onBiometricDialogSuccess(@NonNull BiometricKeyData biometricKeyData) { + final PowerAuthAuthentication authentication = PowerAuthAuthentication.possessionWithBiometry(biometricKeyData.getDerivedData()); + listener.onBiometricDialogSuccess(authentication); + } + + @Override + public void onBiometricDialogFailed(@NonNull PowerAuthErrorException error) { + listener.onBiometricDialogFailed(error); + } + }; + } + + /** + * Authenticate a client using biometric authentication. In case of the authentication is successful and + * {@link IBiometricAuthenticationCallback#onBiometricDialogSuccess(BiometricKeyData)} callback is called. * * @param context Context. * @param fragment The fragment of the application that will host the prompt. @@ -2310,21 +2380,23 @@ public void onCancel() { * @param description Dialog description. * @param callback Callback with the authentication result. * @return {@link ICancelable} object associated with the biometric prompt. + * @deprecated Use {@link #authenticateUsingBiometrics(Context, Fragment, String, String, IAuthenticateWithBiometricsListener)} as a replacement. */ @UiThread @NonNull + @Deprecated // 1.8.0 public ICancelable authenticateUsingBiometry( @NonNull Context context, @NonNull Fragment fragment, @NonNull String title, @NonNull String description, final @NonNull IBiometricAuthenticationCallback callback) { - return authenticateUsingBiometry(context, FragmentHelper.from(fragment), title, description, false, callback); + return authenticateUsingBiometrics(context, FragmentHelper.from(fragment), title, description, false, callback); } /** - * Authenticate a client using biometric authentication. In case of the authentication is successful and {@link IBiometricAuthenticationCallback#onBiometricDialogSuccess(BiometricKeyData)} callback is called, - * you can use {@code biometricKeyEncrypted} as a parameter to {@link PowerAuthAuthentication#useBiometry} property. + * Authenticate a client using biometric authentication. In case of the authentication is successful and + * {@link IBiometricAuthenticationCallback#onBiometricDialogSuccess(BiometricKeyData)} callback is called, * * @param context Context. * @param fragmentActivity The activity of the application that will host the prompt. @@ -2332,21 +2404,22 @@ public ICancelable authenticateUsingBiometry( * @param description Dialog description. * @param callback Callback with the authentication result. * @return {@link ICancelable} object associated with the biometric prompt. + * @deprecated Use {@link #authenticateUsingBiometrics(Context, FragmentActivity, String, String, IAuthenticateWithBiometricsListener)} as a replacement. */ @UiThread @NonNull + @Deprecated // 1.8.0 public ICancelable authenticateUsingBiometry( @NonNull Context context, @NonNull FragmentActivity fragmentActivity, @NonNull String title, @NonNull String description, final @NonNull IBiometricAuthenticationCallback callback) { - return authenticateUsingBiometry(context, FragmentHelper.from(fragmentActivity), title, description, false, callback); + return authenticateUsingBiometrics(context, FragmentHelper.from(fragmentActivity), title, description, false, callback); } /** - * Authenticate a client using biometric authentication. In case of the authentication is successful and {@link IBiometricAuthenticationCallback#onBiometricDialogSuccess(BiometricKeyData)} callback is called, - * you can use {@code biometricKeyEncrypted} as a parameter to {@link PowerAuthAuthentication#useBiometry} property. + * Authenticate a client using biometric authentication. * * @param context Context. * @param fragmentHelper Fragment helper for the dialog. @@ -2358,7 +2431,7 @@ public ICancelable authenticateUsingBiometry( */ @UiThread @NonNull - private ICancelable authenticateUsingBiometry( + private ICancelable authenticateUsingBiometrics( final @NonNull Context context, final @NonNull FragmentHelper fragmentHelper, final @NonNull String title, diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/KVHelper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/KVHelper.java index 1fcc24a1..e22781c5 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/KVHelper.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/KVHelper.java @@ -34,18 +34,35 @@ */ public class KVHelper { - public @NonNull Map map; + /** + * Map to use in the helper. + */ + public final @NonNull Map map; + /** + * Construct helper with given map. + * @param map Map to use in the helper. + */ public KVHelper(Map map) { this.map = map != null ? map : Collections.emptyMap(); } + /** + * Get a string value for given key. + * @param key Key to retrieve the string value. + * @return String for given key or null if no such item is stored in the map. + */ @Nullable public String valueAsString(@NonNull K key) { final Object v = map.get(key); return v instanceof String ? (String) v : null; } + /** + * Get a multiline string value for given key. The returned string doesn't contain CR-LF endings. + * @param key Key to retrieve the string value. + * @return Multiline string for given key or null if no such item is stored in the map. + */ @Nullable public String valueAsMultilineString(@NonNull K key) { final String s = valueAsString(key); @@ -55,11 +72,21 @@ public String valueAsMultilineString(@NonNull K key) { return s; } + /** + * Get a boolean value for given key. + * @param key Key to retrieve the boolean value. + * @return Boolean for given key or false if no such item is stored in the map. + */ public boolean valueAsBool(@NonNull K key) { final Object v = map.get(key); return v instanceof Boolean ? (Boolean) v : false; } + /** + * Get a map for given key. + * @param key Key to retrieve the map. + * @return Map for given key or false if no such item is stored. + */ @SuppressWarnings("unchecked") @Nullable public Map valueAsMap(@NonNull K key) { @@ -67,6 +94,11 @@ public Map valueAsMap(@NonNull K key) { return v instanceof Map ? (Map) v : null; } + /** + * Get a Date created from timestamp in seconds, stored for the given key. + * @param key Key to retrieve the timestamp. + * @return Date created from value for given key or null if no such item is stored in the map. + */ @Nullable public Date valueAsTimestamp(@NonNull K key) { final Object v = map.get(key); @@ -76,6 +108,12 @@ public Date valueAsTimestamp(@NonNull K key) { return null; } + /** + * Get a Date created from string value stored for the given key. + * @param key Key to retrieve the date. + * @param format Format of date. + * @return Date created from value for given key, or null if no such item is stored in the map. + */ @Nullable public Date valueAsDate(@NonNull K key, @NonNull String format) { final String v = valueAsString(key); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/VaultUnlockReason.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/VaultUnlockReason.java index 2d970e20..fbfc65ea 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/VaultUnlockReason.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/VaultUnlockReason.java @@ -31,8 +31,20 @@ @StringDef({ADD_BIOMETRY, FETCH_ENCRYPTION_KEY, SIGN_WITH_DEVICE_PRIVATE_KEY, RECOVERY_CODE}) public @interface VaultUnlockReason { + /** + * Add biometry factor is the reason for vault unlock. + */ String ADD_BIOMETRY = "ADD_BIOMETRY"; + /** + * Fetch encryption key is the reason for vault unlock. + */ String FETCH_ENCRYPTION_KEY = "FETCH_ENCRYPTION_KEY"; + /** + * Sign with device private key is the reason for vault unlock. + */ String SIGN_WITH_DEVICE_PRIVATE_KEY = "SIGN_WITH_DEVICE_PRIVATE_KEY"; + /** + * Get recovery code is the reason for vault unlock. + */ String RECOVERY_CODE = "RECOVERY_CODE"; } diff --git a/proj-android/build.gradle b/proj-android/build.gradle index 9b289b09..06a93eae 100644 --- a/proj-android/build.gradle +++ b/proj-android/build.gradle @@ -49,4 +49,8 @@ allprojects { mavenCentral() google() } + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:-html', '-quiet') + } } +