Skip to content

Commit

Permalink
Fix #134: Improved biometry on iOS
Browse files Browse the repository at this point in the history
- Unified errors reported on failed biometry
- Added support fallback button to iOS biometry dialog
- Biometric dialog is now displayed on iOS simulator
  • Loading branch information
hvge committed Nov 3, 2022
1 parent a5cc531 commit ddbb6ff
Show file tree
Hide file tree
Showing 18 changed files with 271 additions and 90 deletions.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ repositories {

dependencies {
implementation 'com.facebook.react:react-native:+'
api "com.wultra.android.powerauth:powerauth-sdk:1.7.4"
api "com.wultra.android.powerauth:powerauth-sdk:1.7.5"
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class Errors {
static final String EC_BIOMETRY_NOT_AVAILABLE = "BIOMETRY_NOT_AVAILABLE";
static final String EC_BIOMETRY_NOT_RECOGNIZED = "BIOMETRY_NOT_RECOGNIZED";
static final String EC_BIOMETRY_NOT_CONFIGURED = "BIOMETRY_NOT_CONFIGURED";
static final String EC_BIOMETRY_NOT_ENROLLED = "BIOMETRY_NOT_ENROLLED";
static final String EC_INSUFFICIENT_KEYCHAIN_PROTECTION = "INSUFFICIENT_KEYCHAIN_PROTECTION";
static final String EC_BIOMETRY_LOCKOUT = "BIOMETRY_LOCKpublic OUT";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -792,12 +792,39 @@ public void onRecoveryCodeConfirmFailed(@NonNull Throwable t) {
});
}

/**
* Validate biometric status before use.
* @param sdk PowerAuthSDK instance
* @throws WrapperException In case that biometry is not available for any reason.
*/
private void validateBiometryBeforeUse(PowerAuthSDK sdk) throws WrapperException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
switch (BiometricAuthentication.canAuthenticate(context)) {
case BiometricStatus.OK:
if (sdk.hasValidActivation() && !sdk.hasBiometryFactor(context)) {
// Has valid activation, but factor is not set
throw new WrapperException(Errors.EC_BIOMETRY_NOT_CONFIGURED, "Biometry factor is not configured");
}
break;
case BiometricStatus.NOT_AVAILABLE:
throw new WrapperException(Errors.EC_BIOMETRY_NOT_AVAILABLE, "Biometry is not available");
case BiometricStatus.NOT_ENROLLED:
throw new WrapperException(Errors.EC_BIOMETRY_NOT_ENROLLED, "Biometry is not enrolled on device");
case BiometricStatus.NOT_SUPPORTED:
throw new WrapperException(Errors.EC_BIOMETRY_NOT_SUPPORTED, "Biometry is not supported");
}
} else {
throw new WrapperException(Errors.EC_BIOMETRY_NOT_SUPPORTED, "Biometry is not supported");
}
}

@ReactMethod
public void authenticateWithBiometry(String instanceId, final ReadableMap prompt, final boolean makeReusable, final Promise promise) {
final Context context = this.context;
this.usePowerAuthOnMainThread(instanceId, promise, sdk -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
validateBiometryBeforeUse(sdk);
final FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
if (fragmentActivity == null) {
throw new WrapperException(Errors.EC_REACT_NATIVE_ERROR, "Current fragment activity is not available");
Expand Down Expand Up @@ -831,7 +858,7 @@ public void onBiometricDialogSuccess(@NonNull BiometricKeyData biometricKeyData)

@Override
public void onBiometricDialogFailed(@NonNull PowerAuthErrorException error) {
promise.reject(Errors.EC_BIOMETRY_FAILED, "Biometry dialog failed");
Errors.rejectPromise(promise, error);
}
}
);
Expand Down
7 changes: 5 additions & 2 deletions docs/Data-Signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ To sign data with biomtry simply create different authentication object:
```javascript
// 2FA signature, uses device related key and biometry
const auth = PowerAuthAuthentication.biometry({
promptTitle: 'Authenticate',
promptMessage: 'Authenticate to process payment'
promptMessage: 'Authenticate to process payment', // Required on both platforms
promptTitle: 'Authenticate', // Android specific, not used on iOS
fallbackButton: 'Enter PIN' // iOS specific, if provided, then the fallback button is displayed
});

// Sign POST call with provided data made to URI with custom identifier "/payment/create"
Expand All @@ -70,6 +71,8 @@ try {
} catch(e) {
if (e.code === PowerAuthErrorCode.BIOMETRY_CANCEL) {
// User did cancel the dialog
} else if (e.code === PowerAuthErrorCode.BIOMETRY_FALLBACK) {
// iOS specific, can occur only if you provide the fallback button
} else {
// other errors
}
Expand Down
4 changes: 3 additions & 1 deletion ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ target 'PowerAuth' do
use_react_native!(
:path => config["reactNativePath"]
)
pod 'PowerAuth2', '~> 1.7.4'
pod 'PowerAuth2', '~> 1.7.5'
# Uncomment to use not-published SDK in project. This is effective only if you manually open 'ios/PowerAuth.xcworkspace'
#pod 'PowerAuth2', :git => 'https://github.com/wultra/powerauth-mobile-sdk.git', :branch => 'develop', :submodules => true
end
14 changes: 7 additions & 7 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ PODS:
- ReactCommon/turbomodule/core (= 0.68.2)
- fmt (6.2.1)
- glog (0.3.5)
- PowerAuth2 (1.7.4):
- PowerAuthCore (~> 1.7.4)
- PowerAuthCore (1.7.4)
- PowerAuth2 (1.7.5):
- PowerAuthCore (~> 1.7.5)
- PowerAuthCore (1.7.5)
- RCT-Folly (2021.06.28.00-v2):
- boost
- DoubleConversion
Expand Down Expand Up @@ -294,7 +294,7 @@ DEPENDENCIES:
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- PowerAuth2 (~> 1.7.4)
- PowerAuth2 (~> 1.7.5)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
Expand Down Expand Up @@ -401,8 +401,8 @@ SPEC CHECKSUMS:
FBReactNativeSpec: 81ce99032d5b586fddd6a38d450f8595f7e04be4
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
PowerAuth2: c597eecdb348b231a024a527bf49237573819448
PowerAuthCore: d7e5e8cb9307da8033c1d41694cb5cbb35ec332a
PowerAuth2: 00a96bf634778dbf633742cdd832c9386a76d6a8
PowerAuthCore: 58eb5ccea0d45a3e39ef18df1927ce5d2a600bee
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
RCTRequired: 3e917ea5377751094f38145fdece525aa90545a0
RCTTypeSafety: c43c072a4bd60feb49a9570b0517892b4305c45e
Expand Down Expand Up @@ -430,6 +430,6 @@ SPEC CHECKSUMS:
ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2
Yoga: 99652481fcd320aefa4a7ef90095b95acd181952

PODFILE CHECKSUM: b70f1f607024ce52b4368c347cb75ebbca6c008f
PODFILE CHECKSUM: 67188858cd5ee0a31fccc0b81ce4049da72d5922

COCOAPODS: 1.11.3
6 changes: 5 additions & 1 deletion ios/PowerAuth/Errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ PA_EXTERN_C NSString * __nonnull const EC_PROTOCOL_UPGRADE;
PA_EXTERN_C NSString * __nonnull const EC_PENDING_PROTOCOL_UPGRADE;
PA_EXTERN_C NSString * __nonnull const EC_WATCH_CONNECTIVITY;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_CANCEL;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_NOT_AVAILABLE;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_FALLBACK;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_FAILED;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_LOCKOUT;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_NOT_AVAILABLE;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_NOT_SUPPORTED;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_NOT_CONFIGURED;
PA_EXTERN_C NSString * __nonnull const EC_BIOMETRY_NOT_ENROLLED;
PA_EXTERN_C NSString * __nonnull const EC_AUTHENTICATION_ERROR;
PA_EXTERN_C NSString * __nonnull const EC_RESPONSE_ERROR;
PA_EXTERN_C NSString * __nonnull const EC_UNKNOWN_ERROR;
Expand Down
7 changes: 6 additions & 1 deletion ios/PowerAuth/Errors.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@
NSString * const EC_PENDING_PROTOCOL_UPGRADE = @"PENDING_PROTOCOL_UPGRADE";
NSString * const EC_WATCH_CONNECTIVITY = @"WATCH_CONNECTIVITY";
NSString * const EC_BIOMETRY_CANCEL = @"BIOMETRY_CANCEL";
NSString * const EC_BIOMETRY_NOT_AVAILABLE = @"BIOMETRY_NOT_AVAILABLE";
NSString * const EC_BIOMETRY_FALLBACK = @"BIOMETRY_FALLBACK";
NSString * const EC_BIOMETRY_FAILED = @"BIOMETRY_FAILED";
NSString * const EC_BIOMETRY_LOCKOUT = @"BIOMETRY_LOCKOUT";
NSString * const EC_BIOMETRY_NOT_AVAILABLE = @"BIOMETRY_NOT_AVAILABLE";
NSString * const EC_BIOMETRY_NOT_SUPPORTED = @"BIOMETRY_NOT_SUPPORTED";
NSString * const EC_BIOMETRY_NOT_CONFIGURED = @"BIOMETRY_NOT_CONFIGURED";
NSString * const EC_BIOMETRY_NOT_ENROLLED = @"BIOMETRY_NOT_ENROLLED";
NSString * const EC_AUTHENTICATION_ERROR = @"AUTHENTICATION_ERROR";
NSString * const EC_RESPONSE_ERROR = @"RESPONSE_ERROR";
NSString * const EC_UNKNOWN_ERROR = @"UNKNOWN_ERROR";
Expand Down Expand Up @@ -74,6 +78,7 @@
case PowerAuthErrorCode_BiometryNotAvailable: return EC_BIOMETRY_NOT_AVAILABLE;
case PowerAuthErrorCode_WatchConnectivity: return EC_WATCH_CONNECTIVITY;
case PowerAuthErrorCode_BiometryFailed: return EC_BIOMETRY_FAILED;
case PowerAuthErrorCode_BiometryFallback: return EC_BIOMETRY_FALLBACK;
default: return [[NSString alloc] initWithFormat:@"UNKNOWN_%li", code];
}
}
Expand Down
54 changes: 51 additions & 3 deletions ios/PowerAuth/PowerAuthModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,48 @@ + (BOOL) requiresMainQueueSetup
PA_BLOCK_END
}


/// Function validate the status of biometry before the biometry is used. The function also
/// validate whether activation is present, or whether biometry key is configured.
/// - Parameters:
/// - sdk: PowerAuthSDK instance
/// - reject: Reject promise in case that biometry cannot be used.
/// - Returns: YES in case biometry can be used.
- (BOOL) validateBiometryStatusBeforeUse:(PowerAuthSDK*)sdk reject:(RCTPromiseRejectBlock)reject
{
// Determine the biometry state in advance. This is due to fact that iOS impl.
// doesn't support all biometry related error codes such as Android.
// This will be fixed in 1.8.x release, so this code will be obsolete.
NSString * errorCode = nil;
NSString * errorMessage = nil;
switch ([PowerAuthKeychain biometricAuthenticationInfo].currentStatus) {
case PowerAuthBiometricAuthenticationStatus_Available:
if ([sdk hasValidActivation] && ![sdk hasBiometryFactor]) {
// Has activation, but biometry factor is not set
errorCode = EC_BIOMETRY_NOT_CONFIGURED; errorMessage = @"Biometry factor is not configured";
}
break;
case PowerAuthBiometricAuthenticationStatus_NotEnrolled:
errorCode = EC_BIOMETRY_NOT_ENROLLED; errorMessage = @"Biometry is not enrolled on device";
break;
case PowerAuthBiometricAuthenticationStatus_NotSupported:
errorCode = EC_BIOMETRY_NOT_SUPPORTED; errorMessage = @"Biometry is not supported";
break;
case PowerAuthBiometricAuthenticationStatus_NotAvailable:
errorCode = EC_BIOMETRY_NOT_AVAILABLE; errorMessage = @"Biometry is not available";
break;
case PowerAuthBiometricAuthenticationStatus_Lockout:
errorCode = EC_BIOMETRY_LOCKOUT; errorMessage = @"Biometry is locked out";
break;
default:
break;
}
if (errorCode) {
reject(errorCode, errorMessage, nil);
return NO;
} else {
return YES;
}
}

RCT_REMAP_METHOD(authenticateWithBiometry,
instanceId:(nonnull NSString*)instanceId
Expand All @@ -689,9 +730,16 @@ + (BOOL) requiresMainQueueSetup
reject:(RCTPromiseRejectBlock)reject)
{
PA_BLOCK_START
if (![self validateBiometryStatusBeforeUse:powerAuth reject:reject]) {
return;
}
NSString * promptMessage = GetNSStringValueFromDict(prompt, @"promptMessage");
NSString * cancelButton = GetNSStringValueFromDict(prompt, @"cancelButton");
NSString * fallbackButton = GetNSStringValueFromDict(prompt, @"fallbackButton");
LAContext * context = [[LAContext alloc] init];
context.localizedReason = GetNSStringValueFromDict(prompt, @"promptMessage");
context.localizedCancelTitle = GetNSStringValueFromDict(prompt, @"cancelButton");
context.localizedReason = promptMessage;
context.localizedCancelTitle = cancelButton;
context.localizedFallbackTitle = fallbackButton ? fallbackButton : @""; // empty string hides the button
[powerAuth authenticateUsingBiometryWithContext:context callback:^(PowerAuthAuthentication * authentication, NSError * error) {
if (authentication) {
// Allocate native object
Expand Down
2 changes: 1 addition & 1 deletion react-native-powerauth-mobile-sdk.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ Pod::Spec.new do |s|
s.requires_arc = true

s.dependency "React"
s.dependency "PowerAuth2", "~> 1.7.4"
s.dependency "PowerAuth2", "~> 1.7.5"
end
15 changes: 4 additions & 11 deletions src/internal/AuthResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
// limitations under the License.
//

import { Platform } from 'react-native';
import { PowerAuthAuthentication, PowerAuthBiometricPrompt, PowerAuthError, PowerAuthErrorCode } from '../index';
import { PowerAuthAuthentication, PowerAuthError, PowerAuthErrorCode } from '../index';
import { NativeWrapper } from './NativeWrapper';
import { NativeObject } from './NativeObject';

Expand All @@ -35,7 +34,6 @@ export class AuthResolver {
* Method will process `PowerAuthAuthentication` object are will return object according to the platform.
* The method should be used only for the signing purposes.
* @param authentication Authentication configuration
* @param forCommit If true, then resolve authentication for activation commit.
* @param makeReusable if the object should be forced to be reusable
* @returns configured authorization object
*/
Expand All @@ -50,14 +48,9 @@ export class AuthResolver {
}
// Validate whether biometric key is set
const useBiometry = privateAuth.isBiometry
if (useBiometry && !await NativeWrapper.thisCallBool('hasBiometryFactor', this.instanceId)) {
// Biometry is requested but there's no biometry factor set
throw new PowerAuthError(undefined, "Biometry factor is not configured", PowerAuthErrorCode.BIOMETRY_NOT_CONFIGURED)
}
// On android, we need to fetch the key for every biometric authentication.
// If the key is already set, use it (we're processing reusable biometric authentication)
if ((Platform.OS == 'android' && useBiometry && (!privateAuth.biometryKeyId || makeReusable)) ||
(Platform.OS == 'ios' && makeReusable)) {
// On both platforms we need to fetch the key for every biometric authentication.
// If the key is already set, use it.
if (useBiometry && !privateAuth.biometryKeyId) {
try {
const prompt = correctAuth.biometricPrompt
// Acquire biometry key. The function returns ID to underlying data object with a limited validity.
Expand Down
43 changes: 23 additions & 20 deletions src/model/PowerAuthAuthentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface PowerAuthBiometricPrompt {
* iOS specific title for a cancel button, displayed to the user.
*/
cancelButton?: string
/**
* iOS specific title for a fallback button, displayed to the user.
*/
fallbackButton?: string
}

/**
Expand Down Expand Up @@ -86,11 +90,11 @@ export class PowerAuthAuthentication {

/**
* Create object configured to authenticate with combination of possession and biometry factors.
* @param biometricPrompt Prompt to be displayed. Parameter is required on Android platform.
* @param biometricPrompt Prompt to be displayed.
* @returns Authentication object configured to authenticate with possession and biometry factors.
*/
static biometry(biometricPrompt: PowerAuthBiometricPrompt | undefined = undefined): PowerAuthAuthentication {
return new PowerAuthAuthentication(undefined, biometricPrompt).configure(false, true)
static biometry(biometricPrompt: PowerAuthBiometricPrompt): PowerAuthAuthentication {
return new PowerAuthAuthentication(undefined, biometricPrompt ?? FALLBACK_PROMPT).configure(false, true)
}

/**
Expand All @@ -113,9 +117,9 @@ export class PowerAuthAuthentication {

/**
* Create object configured to commit activation with password and biometry.
* @param password
* @param biometricPrompt
* @returns
* @param password User's password. You can provide string or `PowerAuthPassword` object.
* @param biometricPrompt Required on Android, only when biometry config has `authenticateOnBiometricKeySetup` set to `true`.
* @returns Object configured to commit activation with password and biometry.
*/
static commitWithPasswordAndBiometry(password: PasswordType, biometricPrompt: PowerAuthBiometricPrompt | undefined = undefined): PowerAuthAuthentication {
return new PowerAuthAuthentication(password, biometricPrompt).configure(true, true)
Expand Down Expand Up @@ -155,27 +159,17 @@ export class PowerAuthAuthentication {
/**
* Construct `PowerAuthBiometricPrompt` object from data available in this authentication object.
* This is a temporary solution for compatibility with older apps that still use old way of authentication setup.
* The method is used in `AuthResolver.ts` implementation.
* @returns PowerAuthBiometricPrompt object.
*/
private getBiometricPrompt(): PowerAuthBiometricPrompt {
if (this.biometricPrompt) {
// Test whether prompt fulfil requirements on all platforms
if (this.biometricPrompt.promptTitle) {
return this.biometricPrompt
}
// Otherwise construct a copy
return {
promptMessage: this.biometricPrompt.promptMessage,
cancelButton: this.biometricPrompt.cancelButton,
promptTitle: '< missing title >'
}
return this.biometricPrompt
}
// Authentication object was constructed in legacy mode,
// so create a fallback object.
return {
promptMessage: this.biometryMessage ?? '< missing message >',
promptTitle: this.biometryTitle ?? '< missing title >'
promptMessage: this.biometryMessage ?? FALLBACK_MESSAGE,
promptTitle: this.biometryTitle ?? FALLBACK_TITLE
}
}

Expand Down Expand Up @@ -229,4 +223,13 @@ export class PowerAuthAuthentication {
* @deprecated Direct access to property is now deprecated, use new static methods to construct `PowerAuthAuthentication` object.
*/
biometryTitle?: string
};
}

// Fallback strings

const FALLBACK_TITLE = '< missing title >'
const FALLBACK_MESSAGE = '< missing message >'
const FALLBACK_PROMPT: PowerAuthBiometricPrompt = {
promptMessage: FALLBACK_MESSAGE,
promptTitle: FALLBACK_TITLE
}
Loading

0 comments on commit ddbb6ff

Please sign in to comment.