From 778efd1805fd51f9e6069d21c5d2c64d986907cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20=C4=8Eurech?= <1719814+hvge@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:55:39 +0100 Subject: [PATCH] Improved authenticateUsingBiometry() on iOS (#496) * Android: Upgrade NDK to latest LTS * Apple: Fix #495: Use LAContext in authenticateUsingBiometry() * Apple: Fix #495: Added PowerAuthErrorCode_BiometryFallback error code. --- docs/Migration-from-1.6-to-1.7.md | 19 +- docs/PowerAuth-SDK-for-iOS.md | 32 ++- proj-android/build.gradle | 2 +- .../PowerAuth2/PowerAuthErrorConstants.h | 4 + .../PowerAuth2/PowerAuthErrorConstants.m | 3 +- proj-xcode/PowerAuth2/PowerAuthSDK.h | 6 +- proj-xcode/PowerAuth2/PowerAuthSDK.m | 268 +++++++++++++----- .../PowerAuthErrorConstants.m | 2 +- .../PowerAuthErrorConstants.m | 2 +- .../PowerAuthSDKDefaultTests.m | 19 +- 10 files changed, 269 insertions(+), 88 deletions(-) diff --git a/docs/Migration-from-1.6-to-1.7.md b/docs/Migration-from-1.6-to-1.7.md index 14a0449c..7d08162e 100644 --- a/docs/Migration-from-1.6-to-1.7.md +++ b/docs/Migration-from-1.6-to-1.7.md @@ -68,7 +68,7 @@ PowerAuth Mobile SDK in version `1.7.0` is a maintenance release that brings mul - `IOException` is no longer reported from SDK's internal networking. Now all such exceptions are wrapped into `PowerAuthErrorException` with `NETWORK_ERROR` code set. -- Please read also changes introduced in [1.7.2](#changes-in-172) and [1.7.3](#changes-in-173) versions. +- Please read also changes introduced in [1.7.2](#changes-in-172), [1.7.3](#changes-in-173) and [1.7.5](#changes-in-175) versions. ## iOS & tvOS @@ -198,4 +198,19 @@ If you still have to compile our SDK for older operating systems, then you need ./scripts/ios-build-extensions.sh extensions watchos --legacy-archs --use-bitcode --out-dir ./Build ``` -If you use cocoapds for PowerAuth mobile SDK integration, then please let us know and we'll prepare a special release branch for you. \ No newline at end of file +If you use cocoapds for PowerAuth mobile SDK integration, then please let us know and we'll prepare a special release branch for you. + +## Changes in 1.7.5+ + +### iOS + +The behavior of `PowerAuthSDK.authenticateUsingBiometry()` has been slightly changed and improved: + +- Function now properly treat biometry lockout and increase failed attempts counter on the server, See [Biometry lockout](PowerAuth-SDK-for-iOS.md#biometry-lockout) chapter. +- Function now returns new `PowerAuthErrorCode_BiometryFallback` error code in case that user tap on the fallback button. +- As a benefit, function now properly handles situations when the user press home or power button during the biometric authentication. +- You can also cancel the pending authentication with using `LAContext.invalidate()` method. + +### tvOS + +The `PowerAuthSDK.authenticateUsingBiometry()` function is no longer available on tvOS platform. diff --git a/docs/PowerAuth-SDK-for-iOS.md b/docs/PowerAuth-SDK-for-iOS.md index 4e27bacd..64f788b0 100644 --- a/docs/PowerAuth-SDK-for-iOS.md +++ b/docs/PowerAuth-SDK-for-iOS.md @@ -1116,6 +1116,32 @@ context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason } ``` +Be aware that the example above doesn't handle all quirks related to the PowerAuth protocol, so you should prefer to use `authenticateUsingBiometry()` function instead: + +```swift +let context = LAContext() +context.localizedReason = "Please authenticate with biometry" +PowerAuthSDK.sharedInstance().authenticateUsingBiometry(withContext: context) { authentication, error in + guard let authentication = authentication else { + if let nsError = error as? NSError { + if nsError.domain == PowerAuthErrorDomain { + if nsError.powerAuthErrorCode == .biometryCancel { + // cancel, app cancel, system cancel... + } else if nsError.powerAuthErrorCode == .biometryFallback { + // fallback button pressed + } + // If you're interested in exact failure reason, then extract + // the underlying LAError. + if let laError = nsError.userInfo[NSUnderlyingErrorKey] as? LAError { + // Investigate error codes... + } + } + } + return + } + // Now use authentication in other APIs +} +``` ## Activation Removal @@ -1785,7 +1811,10 @@ if error == nil { case .biometryCancel: print("Error code for TouchID/FaceID action cancel error") - + + case .biometryFallback: + print("Error code for TouchID/FaceID fallback action") + case .biometryFailed: print("Error code for TouchID/FaceID action failure") @@ -1825,6 +1854,7 @@ Note that you typically don't need to handle all error codes reported in the `Er Here's the list of important error codes, which the application should properly handle: - `PowerAuthErrorCode.biometryCancel` is reported when the user cancels the biometric authentication dialog +- `PowerAuthErrorCode.biometryFallback` is reported when the user cancels the biometric authentication dialog with a fallback button - `PowerAuthErrorCode.protocolUpgrade` is reported when SDK failed to upgrade itself to a newer protocol version. The code may be reported from `PowerAuthSDK.fetchActivationStatus()`. This is an unrecoverable error resulting in the broken activation on the device, so the best situation is to inform the user about the situation and remove the activation locally. - `PowerAuthErrorCode.pendingProtocolUpgrade` is reported when the requested SDK operation cannot be completed due to a pending PowerAuth protocol upgrade. You can retry the operation later. The code is typically reported in the situations when SDK is performing protocol upgrade on the background (as a part of activation status fetch), and the application want's to calculate PowerAuth signature in parallel operation. Such kind of concurrency is forbidden since SDK version `1.0.0` - `PowerAuthErrorCode.externalPendingOperation` is reported when the requested operation collide with the same operation type already started in the external application. diff --git a/proj-android/build.gradle b/proj-android/build.gradle index 7cdfc659..9b289b09 100644 --- a/proj-android/build.gradle +++ b/proj-android/build.gradle @@ -40,7 +40,7 @@ ext { minSdkVersion = 19 buildToolsVersion = "30.0.3" // NDK, check https://developer.android.com/ndk/downloads for updates - ndkVersion = "25.0.8775105" // r25 + ndkVersion = "25.1.8937393" // r25b } allprojects { diff --git a/proj-xcode/PowerAuth2/PowerAuthErrorConstants.h b/proj-xcode/PowerAuth2/PowerAuthErrorConstants.h index a39bdfe1..9c3505a3 100644 --- a/proj-xcode/PowerAuth2/PowerAuthErrorConstants.h +++ b/proj-xcode/PowerAuth2/PowerAuthErrorConstants.h @@ -139,6 +139,10 @@ typedef NS_ENUM(NSInteger, PowerAuthErrorCode) { object available via `NSError.powerAuthExternalPendingOperation` property. */ PowerAuthErrorCode_ExternalPendingOperation = 18, + /** + User canceled the biometric authentication dialog with a fallback button. + */ + PowerAuthErrorCode_BiometryFallback = 19, }; @interface NSError (PowerAuthErrorCode) diff --git a/proj-xcode/PowerAuth2/PowerAuthErrorConstants.m b/proj-xcode/PowerAuth2/PowerAuthErrorConstants.m index b40b102f..bbc3461b 100644 --- a/proj-xcode/PowerAuth2/PowerAuthErrorConstants.m +++ b/proj-xcode/PowerAuth2/PowerAuthErrorConstants.m @@ -43,6 +43,7 @@ _CODE_DESC(PowerAuthErrorCode_ActivationPending, @"Pending activation") _CODE_DESC(PowerAuthErrorCode_BiometryNotAvailable, @"Biometry is not supported or is unavailable") _CODE_DESC(PowerAuthErrorCode_BiometryCancel, @"User did cancel biometry authentication dialog") + _CODE_DESC(PowerAuthErrorCode_BiometryFallback, @"Used did press fallback at biometry authentication dialog") _CODE_DESC(PowerAuthErrorCode_BiometryFailed, @"Biometry authentication failed") _CODE_DESC(PowerAuthErrorCode_OperationCancelled, @"Operation was cancelled by SDK") _CODE_DESC(PowerAuthErrorCode_Encryption, @"General encryption failure") @@ -66,7 +67,7 @@ NSError * PA2MakeErrorInfo(NSInteger errorCode, NSString * message, NSDictionary * info) { - NSMutableDictionary * mutableInfo = [info mutableCopy]; + NSMutableDictionary * mutableInfo = info ? [info mutableCopy] : [NSMutableDictionary dictionary]; mutableInfo[NSLocalizedDescriptionKey] = PA2MakeDefaultErrorDescription(errorCode, message); return [NSError errorWithDomain:PowerAuthErrorDomain code:errorCode userInfo:mutableInfo]; } diff --git a/proj-xcode/PowerAuth2/PowerAuthSDK.h b/proj-xcode/PowerAuth2/PowerAuthSDK.h index 7971098e..9d3741f0 100644 --- a/proj-xcode/PowerAuth2/PowerAuthSDK.h +++ b/proj-xcode/PowerAuth2/PowerAuthSDK.h @@ -517,7 +517,8 @@ */ - (void) authenticateUsingBiometryWithPrompt:(nonnull NSString *)prompt callback:(nonnull void(^)(PowerAuthAuthentication * _Nullable authentication, NSError * _Nullable error))callback - NS_SWIFT_NAME(authenticateUsingBiometry(withPrompt:callback:)); + NS_SWIFT_NAME(authenticateUsingBiometry(withPrompt:callback:)) + API_UNAVAILABLE(tvos); /** Prepare PowerAuthAuthentication object for future PowerAuth signature calculation with a biometry and possession factors involved. @@ -539,7 +540,8 @@ */ - (void) unlockBiometryKeysWithPrompt:(nonnull NSString*)prompt withBlock:(nonnull void(^)(NSDictionary * _Nullable keys, BOOL userCanceled))block - NS_SWIFT_NAME(unlockBiometryKeys(withPrompt:callback:)); + NS_SWIFT_NAME(unlockBiometryKeys(withPrompt:callback:)) + API_UNAVAILABLE(tvos); /** Unlock all keys stored in a biometry related keychain and keeps them cached for the scope of the block. diff --git a/proj-xcode/PowerAuth2/PowerAuthSDK.m b/proj-xcode/PowerAuth2/PowerAuthSDK.m index b9bf0f0a..457a0829 100644 --- a/proj-xcode/PowerAuth2/PowerAuthSDK.m +++ b/proj-xcode/PowerAuth2/PowerAuthSDK.m @@ -285,33 +285,83 @@ - (NSData*) deviceRelatedKey return possessionKey; } -- (NSData*) biometryRelatedKeyWithAuthentication:(nonnull PowerAuthKeychainAuthentication*)authentication status:(nonnull OSStatus *)status +/// Acquire biometry related key from the keychain. +/// - Parameters: +/// - authentication: Keychain authentication object. +/// - error: Pointer to error object to fill when operation fails. +/// - Returns: Biometry related key or nil. +- (NSData*) biometryRelatedKeyWithAuthentication:(nonnull PowerAuthKeychainAuthentication*)authentication error:(NSError **)error { - if ([_biometryOnlyKeychain containsDataForKey:_biometryKeyIdentifier]) { - __block NSData *key = nil; - BOOL executed = [PowerAuthKeychain tryLockBiometryAndExecuteBlock:^{ - key = [_biometryOnlyKeychain dataForKey:_biometryKeyIdentifier status:status authentication:authentication]; - }]; - if (key) { - // Key has been successfully retrieved. - *status = errSecSuccess; - } else if (!executed) { - // Failed to acquire biometric lock. Simulate cancel in this case. - *status = errSecUserCanceled; - } #if PA2_HAS_LACONTEXT - if (key && _keychainConfiguration.invalidateLocalAuthenticationContextAfterUse) { - [authentication.context invalidate]; + // + // LAContext is available on this platform + // + __block NSData *key = nil; + __block OSStatus status; + BOOL executed = [PowerAuthKeychain tryLockBiometryAndExecuteBlock:^{ + key = [_biometryOnlyKeychain dataForKey:_biometryKeyIdentifier status:&status authentication:authentication]; + }]; + if (key) { + // Key has been successfully retrieved. + status = errSecSuccess; + } else if (!executed) { + // Failed to acquire biometric lock. Simulate cancel in this case. + status = errSecUserCanceled; + } + if (status != errSecSuccess) { + NSError * localError; + PowerAuthLog(@"ERROR: Getting key for biometric authentication failed with OSStatus = %@.", @(status)); + // The key was not fetched, try to translate OSStatus to a reasonable meaning. + if (status == errSecUserCanceled) { + // User canceled the operation. + localError = PA2MakeError(PowerAuthErrorCode_BiometryCancel, nil); + } else if (status == errSecItemNotFound) { + // Biometric key was not found. + // Note, that previously we treated this as an authentication error, but this might be + // an issue in application logic. For example, if app try to authenticate and immediately + // remove the biometry key. + localError = PA2MakeError(PowerAuthErrorCode_BiometryFailed, @"Biometric key not found"); + } else if (status == errSecInvalidContext) { + // Invalid LAContext provided. + // Be aware that this code is generated in our keychain impl. Don't be confused with the naming, + // if LAContext is already invalidated, then general `errSecAuthFailed` is returned. + localError = PA2MakeError(PowerAuthErrorCode_BiometryFailed, @"Invalid LAContext"); + } else if (status == errSecUnimplemented) { + // PowerAuthKeychainAuthentication was provided on platform that doesn't support it. + // This may happen only if tvOS application proactively create biometric key in the biometry keychain. + // In regular and expected setup, accessing biometry protected item on tvOS fails with errSecItemNotFound. + localError = PA2MakeError(PowerAuthErrorCode_BiometryFailed, @"PowerAuthKeychainAuthentication not supported"); + } else { + localError = nil; } -#endif - return key; - } else { - // Item is not present in keychain. - *status = errSecItemNotFound; - return nil; + // If localError variable is set, then we need to report an error. + if (localError) { + if (error) { *error = localError; } + return nil; + } + // No error generated, so create a fake biometry key to fail on the server. + key = [self generateInvalidBiometricKey]; + } else if (error) { + // Success, so we should reset object at error pointer. + *error = nil; + } + + if (key && _keychainConfiguration.invalidateLocalAuthenticationContextAfterUse) { + [authentication.context invalidate]; + } + return key; +#else + // + // LAContext is not available on this platform + // + if (error) { + *error = PA2MakeError(PowerAuthErrorCode_BiometryNotAvailable, nil); } + return nil; +#endif } + - (PowerAuthCoreSignatureUnlockKeys*) signatureKeysForAuthentication:(nonnull PowerAuthAuthentication*)authentication error:(NSError **)error { @@ -334,42 +384,9 @@ - (PowerAuthCoreSignatureUnlockKeys*) signatureKeysForAuthentication:(nonnull Po biometryKey = authentication.overridenBiometryKey; } else { // default biometry key should be fetched - OSStatus status; - biometryKey = [self biometryRelatedKeyWithAuthentication:authentication.keychainAuthentication status:&status]; - if (biometryKey == nil) { - PowerAuthLog(@"ERROR: Getting key for biometric authentication failed with OSStatus = %@.", @(status)); - NSError * localError; - // The key was not fetched, try to translate OSStatus to a reasonable meaning. - if (status == errSecUserCanceled) { - // User canceled the operation. - localError = PA2MakeError(PowerAuthErrorCode_BiometryCancel, nil); - } else if (status == errSecItemNotFound) { - // Biometric key was not found. - // Note, that previously we treated this as an authentication error, but this might be - // an issue in application logic. For example, if app try to authenticate and immediately - // remove the biometry key. - localError = PA2MakeError(PowerAuthErrorCode_BiometryFailed, @"Biometric key not found"); - } else if (status == errSecInvalidContext) { - // Invalid LAContext provided. - // Be aware that this code is generated in our keychain impl. Don't be confused with the naming, - // if LAContext is already invalidated, then general `errSecAuthFailed` is returned. - localError = PA2MakeError(PowerAuthErrorCode_BiometryFailed, @"Invalid LAContext"); - } else if (status == errSecUnimplemented) { - // PowerAuthKeychainAuthentication was provided on platform that doesn't support it. - // This may happen only if tvOS application proactively create biometric key in the biometry keychain. - // In regular and expected setup, accessing biometry protected item on tvOS fails with errSecItemNotFound. - localError = PA2MakeError(PowerAuthErrorCode_BiometryFailed, @"PowerAuthKeychainAuthentication not supported"); - } else { - localError = nil; - } - // If localError variable is set, then we need to report an error. - if (localError) { - if (error) { *error = localError; } - return nil; - } - // No error generated, so create a fake biometry key to fail on the server. - PowerAuthLog(@"WARNING: Generating fake biometry key to increase failed attempts counter on the server."); - biometryKey = [PowerAuthCoreSession generateSignatureUnlockKey]; + biometryKey = [self biometryRelatedKeyWithAuthentication:authentication.keychainAuthentication error:error]; + if (!biometryKey) { + return nil; } } } @@ -1210,6 +1227,10 @@ - (BOOL) removeBiometryFactor }]; } +#if PA2_HAS_LACONTEXT + +// If LAContext is available then we assume that biometry is also available on the platform. + - (void) authenticateUsingBiometryWithPrompt:(NSString *)prompt callback:(void(^)(PowerAuthAuthentication * authentication, NSError * error))callback { @@ -1222,8 +1243,6 @@ - (void) unlockBiometryKeysWithPrompt:(NSString*)prompt [self unlockBiometryKeysImpl:[[PowerAuthKeychainAuthentication alloc] initWithPrompt:prompt] withBlock:block]; } -#if PA2_HAS_LACONTEXT == 1 - - (void) authenticateUsingBiometryWithContext:(LAContext *)context callback:(void (^)(PowerAuthAuthentication *, NSError *))callback { @@ -1236,46 +1255,130 @@ - (void) unlockBiometryKeysWithContext:(LAContext *)context [self unlockBiometryKeysImpl:[[PowerAuthKeychainAuthentication alloc] initWithContext:context] withBlock:block]; } -#endif // PA2_HAS_LACONTEXT - - (void) authenticateUsingBiometryImpl:(PowerAuthKeychainAuthentication *)keychainAuthentication callback:(void(^)(PowerAuthAuthentication * authentication, NSError * error))callback { [self checkForValidSetup]; + // Check if activation is present if (!_sessionInterface.hasValidActivation) { callback(nil, PA2MakeError(PowerAuthErrorCode_MissingActivation, nil)); return; } - // Check if biometry can be used + + // Check biometric status in advance, to do not increase failed attempts counter + // in case that biometry is already locked out. if (![PowerAuthKeychain canUseBiometricAuthentication]) { callback(nil, PA2MakeError(PowerAuthErrorCode_BiometryNotAvailable, nil)); return; } - // Delegate operation to the background thread, because access to keychain is blocking. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Use app provided, or create a new LAContext if "prompt" variant is used. + NSString * prompt = keychainAuthentication.prompt; + LAContext * context = keychainAuthentication.context; + if (!context) { + // No context is provided, so we have to create a new one and re-create keychain authentication + // to use this context. + if (!prompt) { + prompt = @"< missing prompt >"; + } + context = [[LAContext alloc] init]; + context.localizedReason = prompt; + context.localizedFallbackTitle = @""; // hide fallback button to match our original behavior + keychainAuthentication = [[PowerAuthKeychainAuthentication alloc] initWithContext:context]; + } else { + // Application provided context is available, simply make sure that some prompt is set. + prompt = context.localizedReason; + if (!prompt) { + prompt = @"< missing prompt >"; + } + } + // Prepare policy based on keychain configuration. + LAPolicy policy; + if (_keychainConfiguration.biometricItemAccess == PowerAuthKeychainItemAccess_AnyBiometricSetOrDevicePasscode) { + // The naming is awkward, but 'LAPolicyDeviceOwnerAuthentication' really means that + // we're requesting biometry and the device's passcode + policy = LAPolicyDeviceOwnerAuthentication; + } else { + // In this case, only biometry can be used. + policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + } + // Now evaluate the policy + [context evaluatePolicy:policy localizedReason:prompt reply:^(BOOL success, NSError * _Nullable error) { PowerAuthAuthentication * authentication; - NSError * error; - // Acquire key to unlock biometric factor - OSStatus status; - NSData * biometryKey = [self biometryRelatedKeyWithAuthentication:keychainAuthentication status:&status]; - if (biometryKey) { - // The biometry key is available, so create a new PowerAuthAuthentication object preconfigured - // with possession+biometry factors. - authentication = [PowerAuthAuthentication possessionWithBiometryWithCustomBiometryKey:biometryKey - customPossessionKey:nil]; - error = nil; + if (success) { + // The LAContext should be pre-authorized now, so the operation is no longer blocking. + // Acquire key to unlock biometric factor + NSData * biometryKey = [self biometryRelatedKeyWithAuthentication:keychainAuthentication error:&error]; + if (biometryKey) { + // The biometry key is available, so create a new PowerAuthAuthentication object preconfigured + // with possession+biometry factors. + authentication = [PowerAuthAuthentication possessionWithBiometryWithCustomBiometryKey:biometryKey + customPossessionKey:nil]; + error = nil; + } else { + // Otherwise report an error depending on whether the operation was canceled by the user. + authentication = nil; + } } else { - // Otherwise report an error depending on whether the operation was canceled by the user. + // Evaluation failed, we should investigate LAError authentication = nil; - error = PA2MakeError(status == errSecUserCanceled ? PowerAuthErrorCode_BiometryCancel : PowerAuthErrorCode_BiometryFailed, nil); + // Embed an original error + NSDictionary * errorInfo = error ? @{ NSUnderlyingErrorKey: error } : nil; + if ([error.domain isEqualToString:LAErrorDomain]) { + switch (error.code) { + case LAErrorAuthenticationFailed: // User failed to provide valid credentials. + case LAErrorBiometryLockout: // Too many failed attempts, biometry is now locked out. + // Authentication failed, now it's time to generate the fake key + authentication = [PowerAuthAuthentication possessionWithBiometryWithCustomBiometryKey:[self generateInvalidBiometricKey] + customPossessionKey:nil]; + error = nil; + break; + + case LAErrorPasscodeNotSet: + // Passcode is not set, so the biometric authentication cannot start. + error = PA2MakeErrorInfo(PowerAuthErrorCode_BiometryNotAvailable, @"Device's passcode is not set", errorInfo); + break; + + case LAErrorBiometryNotAvailable: + error = PA2MakeErrorInfo(PowerAuthErrorCode_BiometryNotAvailable, @"Biometry not supported", errorInfo); + break; + + case LAErrorBiometryNotEnrolled: + error = PA2MakeErrorInfo(PowerAuthErrorCode_BiometryNotAvailable, @"Biometry not enrolled", errorInfo); + break; + + case LAErrorSystemCancel: // Systme cancel (e.g. user pressed power or home button) + case LAErrorAppCancel: // App cancel, (e.g. application called invalidate on its context) + case LAErrorUserCancel: // User tapped on cancel button + // All cancel types leads to our cancel + error = PA2MakeErrorInfo(PowerAuthErrorCode_BiometryCancel, nil, errorInfo); + break; + + case LAErrorUserFallback: // Canceled, because user tapped on the fallback button. + // All cancel types leads to our cancel + error = PA2MakeErrorInfo(PowerAuthErrorCode_BiometryFallback, nil, errorInfo); + break; + + case LAErrorNotInteractive: // App should not set interactionNotAllowed property to true + case LAErrorInvalidContext: // Context is already invalidated + error = PA2MakeErrorInfo(PowerAuthErrorCode_WrongParameter, @"LAContext is not valid", errorInfo); + break; + + default: + error = PA2MakeErrorInfo(PowerAuthErrorCode_BiometryFailed, @"Biometry failed", errorInfo); + break; + } + } else { + error = PA2MakeErrorInfo(PowerAuthErrorCode_BiometryFailed, @"Biometry failed with unknown error", errorInfo); + } } + // Report result back to the main thread dispatch_async(dispatch_get_main_queue(), ^{ callback(authentication, error); }); - }); + }]; } - (void) unlockBiometryKeysImpl:(PowerAuthKeychainAuthentication*)keychainAuthentication @@ -1292,6 +1395,17 @@ - (void) unlockBiometryKeysImpl:(PowerAuthKeychainAuthentication*)keychainAuthen }); } +/// Generate new invalid biometric key. The function is used in situations when biometric authentication failed +/// and SDK needs to increase fail attempts count on the server. By generating invalid key we pretend that +/// everything's OK but the final result is that server rejects such signature. +- (NSData*) generateInvalidBiometricKey +{ + PowerAuthLog(@"WARNING: Generating fake biometry key to increase failed attempts counter on the server."); + return [PowerAuthCoreSession generateSignatureUnlockKey]; +} + +#endif // PA2_HAS_LACONTEXT + #pragma mark - Secure vault support diff --git a/proj-xcode/PowerAuth2ForExtensions/PowerAuthErrorConstants.m b/proj-xcode/PowerAuth2ForExtensions/PowerAuthErrorConstants.m index f0b876f5..5c144089 100644 --- a/proj-xcode/PowerAuth2ForExtensions/PowerAuthErrorConstants.m +++ b/proj-xcode/PowerAuth2ForExtensions/PowerAuthErrorConstants.m @@ -66,7 +66,7 @@ NSError * PA2MakeErrorInfo(NSInteger errorCode, NSString * message, NSDictionary * info) { - NSMutableDictionary * mutableInfo = [info mutableCopy]; + NSMutableDictionary * mutableInfo = info ? [info mutableCopy] : [NSMutableDictionary dictionary]; mutableInfo[NSLocalizedDescriptionKey] = PA2MakeDefaultErrorDescription(errorCode, message); return [NSError errorWithDomain:PowerAuthErrorDomain code:errorCode userInfo:mutableInfo]; } diff --git a/proj-xcode/PowerAuth2ForWatch/PowerAuthErrorConstants.m b/proj-xcode/PowerAuth2ForWatch/PowerAuthErrorConstants.m index 7b8989d0..ba701fbc 100644 --- a/proj-xcode/PowerAuth2ForWatch/PowerAuthErrorConstants.m +++ b/proj-xcode/PowerAuth2ForWatch/PowerAuthErrorConstants.m @@ -66,7 +66,7 @@ NSError * PA2MakeErrorInfo(NSInteger errorCode, NSString * message, NSDictionary * info) { - NSMutableDictionary * mutableInfo = [info mutableCopy]; + NSMutableDictionary * mutableInfo = info ? [info mutableCopy] : [NSMutableDictionary dictionary]; mutableInfo[NSLocalizedDescriptionKey] = PA2MakeDefaultErrorDescription(errorCode, message); return [NSError errorWithDomain:PowerAuthErrorDomain code:errorCode userInfo:mutableInfo]; } diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m index 44fdaa1a..171b9945 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m @@ -1116,6 +1116,12 @@ - (void) testCancelEnqueuedHttpOperation - (void) testBiometrySignatureWhenNotConfigured { CHECK_TEST_CONFIG(); + +#if defined(PA2_BIOMETRY_SUPPORT) + BOOL supportsBiometry = YES; +#else + BOOL supportsBiometry = NO; +#endif // // This test validates that signing with biometry doesn't work when @@ -1134,13 +1140,22 @@ - (void) testBiometrySignatureWhenNotConfigured authentication = [PowerAuthAuthentication possessionWithBiometry]; header = [_sdk requestSignatureWithAuthentication:authentication method:@"POST" uriId:@"/some/uri/id" body:[NSData data] error:&error]; XCTAssertNil(header); - XCTAssertEqual(PowerAuthErrorCode_BiometryFailed, error.powerAuthErrorCode); + if (supportsBiometry) { + XCTAssertEqual(PowerAuthErrorCode_BiometryFailed, error.powerAuthErrorCode); + } else { + XCTAssertEqual(PowerAuthErrorCode_BiometryNotAvailable, error.powerAuthErrorCode); + } error = nil; authentication = [PowerAuthAuthentication possessionWithBiometryPrompt:@"Authenticate with biometry"]; header = [_sdk requestSignatureWithAuthentication:authentication method:@"POST" uriId:@"/some/uri/id" body:[NSData data] error:&error]; XCTAssertNil(header); - XCTAssertEqual(PowerAuthErrorCode_BiometryFailed, error.powerAuthErrorCode); + + if (supportsBiometry) { + XCTAssertEqual(PowerAuthErrorCode_BiometryFailed, error.powerAuthErrorCode); + } else { + XCTAssertEqual(PowerAuthErrorCode_BiometryNotAvailable, error.powerAuthErrorCode); + } } #if defined(PA2_BIOMETRY_SUPPORT)