Biometric Authentication Bypass
Biometric Authentication Bypass
Description
A secure implementation of mobile biometric authentication guarantees the need to use Face ID or Touch ID authentication to access the application’s sensitive data. Such secure implementation goes beyond just verifying the fingerprint or the face to log in. It also includes encrypting the application's sensitive data using the biometric data.
This encryption adds an extra layer of protection, making it highly challenging for unauthorized individuals to access or use sensitive information. Encryption with biometric data becomes crucial in case an unauthorized party gains access to the device, via Malware or physical access.
Android
Android provides mechanisms to enforce biometric authentication to protect sensitive information. Biometric authentication has evolved over time to provide improved user experience, developer experience and improved security.
Previous implementation using FingerprintManager
is deprecated and must not be used. Proper implementation must useBiometricManager
with BiometricPrompt
and CryptoObject
.
CryptoObject
provides cryptographic primitives for encryption, decryption and signature validation.
In the example below, calling the authenticate
method without cryptoObject
is vulnerable to authentication bypass:
fun showBiometricPrompt(
title: String = "Biometric Authentication",
subtitle: String = "Enter biometric credentials to proceed.",
description: String = "Input your Fingerprint or FaceID to ensure it's you!",
activity: AppCompatActivity,
listener: BiometricAuthListener,
cryptoObject: BiometricPrompt.CryptoObject? = null,
allowDeviceCredential: Boolean = false
) {
// 1
val promptInfo = setBiometricPromptInfo(
title,
subtitle,
description,
allowDeviceCredential
)
// 2
val biometricPrompt = initBiometricPrompt(activity, listener)
// 3
biometricPrompt.apply {
if (cryptoObject == null) authenticate(promptInfo)
else authenticate(promptInfo, cryptoObject)
}
}
iOS
The Local Authentication framework enables developers to request Touch ID authentication from users. To initiate this process, developers can invoke an authentication prompt using the evaluatePolicy function within the LAContext class. However, it's important to note that this approach is insecure: the function returns a boolean value, rather than providing a cryptographic object that can be utilized for decrypting sensitive data stored within the Keychain.
Without a cryptographic object, an attacker can manipulate the memory to bypass the biometric check and log in successfully to the application. However, they would be unable to interpret or utilize the application data if the encryption is used with the biometric data. This helps to maintain the confidentiality of the application's sensitive information, thereby safeguarding the privacy and security of users' data.
In the example below from DVIA, it is possible to bypass biometric authentication by hooking evaluatePolicy using frida:
+(void)authenticateWithTouchID {
LAContext *myContext = [[LAContext alloc] init];
NSError *authError = nil;
NSString *myLocalizedReasonString = @"Please authenticate yourself";
if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) {
[myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:myLocalizedReasonString
reply:^(BOOL success, NSError *error) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
[TouchIDAuthentication showAlert:@"Authentication Successful" withTitle:@"Success"];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[TouchIDAuthentication showAlert:@"Authentication Failed !" withTitle:@"Error"];
});
}
}];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[TouchIDAuthentication showAlert:@"Your device doesn't support Touch ID or you haven't configured Touch ID authentication on your device" withTitle:@"Error"];
});
}
}
Recommendation
Kotlin
For native Android, Implement biometric authentication with CryptoObject
usage.
The authentication flow would be as follows when using CryptoObject
:
- The app creates a key in the KeyStore with:
setUserAuthenticationRequired
set totrue
setInvalidatedByBiometricEnrollment
set totrue
setUserAuthenticationValidityDurationSeconds
set to-1
.
val paramsBuilder = KeyGenParameterSpec.Builder(keyName, KeyProperties.PURPOSE_SIGN)
paramsBuilder.apply {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
setDigests(KeyProperties.DIGEST_SHA256)
setUserAuthenticationRequired(true)
setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) // ECDSA parameter (P-256) curve
setInvalidatedByBiometricEnrollment(true)
setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q -> {
setDigests(KeyProperties.DIGEST_SHA256)
setUserAuthenticationRequired(true)
setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
setInvalidatedByBiometricEnrollment(true)
setUserAuthenticationValidityDurationSeconds(-1)
}
else -> {
setDigests(KeyProperties.DIGEST_SHA256)
setUserAuthenticationRequired(true)
setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
setUserAuthenticationValidityDurationSeconds(-1)
}
}
}
-
The keystore key must be used to encrypt information that is authenticating the user, like session information or authentication token.
-
Biometrics are presented before the key is accessed from the KeyStore to decrypt the data. The biometric is validated with
authenticate
method and theCryptoObject
. This solution cannot be bypassed, even on rooted devices as the keystore key can only be used after successful biometric authentication.
fun showBiometricPrompt(
title: String = "Biometric Authentication",
subtitle: String = "Enter biometric credentials to proceed.",
description: String = "Input your Fingerprint or FaceID
to ensure it's you!",
activity: AppCompatActivity,
listener: BiometricAuthListener
) {
val promptInfo = setBiometricPromptInfo(title, subtitle, description)
val biometricPrompt = initBiometricPrompt(activity, listener)
biometricPrompt.authenticate(
promptInfo, BiometricPrompt.CryptoObject(
CryptoUtil.getOrCreateSignature()
)
)
}
- If
CryptoObject
is not used as part of the authenticate method, it can be bypassed by using dynamic instrumentation with a debugger or with tools like Frida.
Swift
Use the Keychain to store the secretKey, and enforce the usage of the Biometric authentication to access the item from the Keychain.
1- Create a biometry-protected keychain item:
Use SecAccessControlCreateWithFlags to create a SecAccessControl with the following parameters:
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly: keychain entry can only be read when the iOS device is unlocked. Also it won’t be copied to other devices via iCloud and won’t be added to backups.
- .biometryCurrentSet: sets the requirement of Touch ID or Face ID authentication. It strictly ties your entry to the currently enrolled biometric data.
static func getBioSecAccessControl() -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
access = SecAccessControlCreateWithFlags(nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
&error)
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access!
}
static func createBioProtectedEntry(key: String, data: Data) -> OSStatus {
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key,
kSecAttrAccessControl as String: getBioSecAccessControl(),
kSecValueData as String: data ] as CFDictionary
return SecItemAdd(query as CFDictionary, nil)
}
2- Read a biometry-protected entry:
static func loadBioProtected(key: String, context: LAContext? = nil,
prompt: String? = nil) -> Data? {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: kCFBooleanTrue,
kSecAttrAccessControl as String: getBioSecAccessControl(),
kSecMatchLimit as String: kSecMatchLimitOne ]
if let context = context {
query[kSecUseAuthenticationContext as String] = context
query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUISkip
}
if let prompt = prompt {
query[kSecUseOperationPrompt as String] = prompt
}
var dataTypeRef: AnyObject? = nil
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == noErr {
return (dataTypeRef! as! Data)
} else {
return nil
}
}
static func redBioProtectedEntry(entryName: String) {
let authContext = LAContext()
let accessControl = SecAccessControlCreateWithFlags(nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
&error)
authContext.evaluateAccessControl(accessControl, operation: .useItem, localizedReason: "Access sample keychain entry") {
(success, error) in
var result = ""
if success, let data = loadBioProtected(key: entryName, context: authContext) {
let result = String(decoding: data, as: UTF8.self)
} else {
result = "Can't read entry, error: \(error?.localizedDescription ?? "-")"
}
}
}
Flutter
For Flutter (both Android and iOS), biometric_storage
is a plugin that allows using biometric authentication to write and read encrypted data to the device.
The underhood implementation applies the best practices and uses a SecAccessControl with the right SecAccessControlCreateFlags to constraint access with Touch ID fo Face ID.
- The first step is to create the access object where we will write and read the data after the biometric authentication:
```dart /// Retrieves the given biometric storage file. Each store is completely separated and has its own encryption and biometric lock.
FutureauthenticationRequired=true
andauthenticationValidityDurationSeconds = -1
to ensure the secure implementation of bioùetric authentication.
authenticationValidityDurationSeconds: -1,
authenticationRequired: true,
androidBiometricOnly: true,
));
return authStorage;
}
```
- Write data to the secure storage:
```dart /// Retrieves the given biometric storage file. Each store is completely separated and has its own encryption and biometric lock.
Future
- To read the data:
```dart
Future
} ```
Links
- Secure Mobile Biometric Authentication
- Bypass Biometric Authentication
- Using BiometricPrompt with CryptoObject: How and Why
- Android Biometric API: Getting Started
- MOBILE PENTESTING 101 – BYPASSING BIOMETRIC AUTHENTICATION
Standards
- OWASP_MASVS_L2:
- MSTG_AUTH_8
- GDPR:
- ART_5
- ART_32
- PCI_STANDARDS:
- REQ_6_2
- REQ_6_3
- REQ_8_3
- OWASP_MASVS_v2_1:
- MASVS_AUTH_2
- SOC2_CONTROLS:
- CC_2_1
- CC_4_1
- CC_6_7
- CC_7_1
- CC_7_2
- CC_7_4
- CC_7_5