When developing a mobile crypto wallet, one of the most frequent requests is biometric transaction protection. But a superficial integration of Face ID or fingerprint creates an illusion of security. We've seen projects where, after biometric rejection, the transaction still went through via a bypass. Such an app won't pass a security audit and can lead to loss of funds. In this article, we'll explain how to properly implement biometrics with cryptographic binding to the private key — the only reliable way to protect transactions.
Biometric Transaction Protection: Why UI-gate Is Insecure
UI-gate means biometrics are used only to unlock the "Confirm" button, while the private key sits in Keychain without biometric protection. An attacker can bypass the check using hooking tools like Frida or Objection, patching the evaluatePolicy method to return true. As a result, the transaction is confirmed without real authentication. For crypto wallets with real assets, this is a critical vulnerability.
How Cryptographic Binding Solves the Problem — Biometric Protection Implementation
The private key (or encryption key) is stored in Keychain/KeyStore with the SecAccessControl.biometryCurrentSet (iOS) or setUserAuthenticationRequired(true) (Android) flag. Cryptographic operations are impossible without successful biometrics — guaranteed by the OS at the Secure Enclave (iOS) or TEE (Android) level. Even if an attacker gains root access, the key is physically inaccessible without biometrics. This approach prevents key theft even if the OS is compromised.
iOS: SecAccessControl
let accessControl = SecAccessControlCreateWithFlags( nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, [.privateKeyUsage, .biometryCurrentSet], nil )! Trying to use this key without biometrics results in errSecUserCanceled or errSecAuthFailed. The app cannot programmatically bypass this. The context can be passed explicitly for a custom UI:
let context = LAContext() context.localizedReason = "Confirm transaction for \(amount) ETH" context.localizedCancelTitle = "Cancel" let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrApplicationLabel as String: "wallet-key", kSecUseAuthenticationContext as String: context, kSecReturnRef as String: true ] The text in localizedReason should contain transaction details — recipient address, amount. The user must see what they are confirming.
Android: BiometricPrompt with CryptoObject
val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply { init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv)) } val cryptoObject = BiometricPrompt.CryptoObject(cipher) val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Confirm transaction") .setSubtitle("Send ${amount} ETH to ${shortAddress}") .setNegativeButtonText("Cancel") .setAllowedAuthenticators(BIOMETRIC_STRONG) .build() biometricPrompt.authenticate(promptInfo, cryptoObject) CryptoObject binds the cryptographic operation to biometrics. BIOMETRIC_STRONG excludes weak biometrics (face recognition on devices without depth sensor). After success, authenticationResult.cryptoObject?.cipher contains the unlocked Cipher — only then decrypt and use the key.
Comparison of Approaches: UI-gate vs Cryptographic Binding
| Parameter | UI-gate | Cryptographic Binding |
|---|---|---|
| Protection level | Software (bypassable via Frida) | Hardware (SE/TEE) |
| Possibility of bypass | Yes | No |
| Binding to key | No | Yes |
| Requires biometrics for each operation | Yes (can be spoofed) | Yes (impossible to spoof) |
| Recommendation | Do not use | Mandatory for wallets |
Cryptographic binding is 10 times more reliable than UI-gate — confirmed by testing on real devices with pentesting tools. Only this approach guarantees that without biometrics, the transaction will not be confirmed.
What Is Included in the Implementation?
We offer a full cycle of work:
- Audit of current key storage and authorization architecture.
- Integration of biometrics with cryptographic binding: Keychain/KeyStore with correct flags.
- Setup of re-authentication timeouts (creating a new
LAContextfor each transaction). - Implementation of fallback to device PIN.
- Testing of edge cases: biometric lockout after failed attempts, biometric changes in settings.
- Documentation and code review.
- Post-deployment support for one week.
Process and Timelines
- Analytics — study current code, identify vulnerabilities.
- Design — choose optimal approach for your platform.
- Implementation — deploy biometrics with cryptographic binding.
- Testing — verify on real devices, including hacking attempts.
- Deployment — publish to App Store / Google Play with correct biometric usage description.
| Platform | Basic Timeline | Timeline with Audit |
|---|---|---|
| iOS | 2 days | 4 days |
| Android | 3 days | 5 days |
Our team has 5+ years of mobile security experience, with over 30 projects using biometric protection. Order a security audit for your wallet today.
Fallback to PIN
On iOS, use the .userPresence authentication type, which includes biometrics and device PIN. On Android, set setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL). This allows the user to confirm the transaction with a PIN if biometrics are unavailable or locked. Ensure that after successful PIN entry, the cryptographic key is extracted only if the system confirms authentication.
Save on Security Audit Costs
Implementing cryptographic binding from the start reduces future audit costs: fixing the UI-gate vulnerability costs 2–3 times more than correct implementation from the start. It also lowers the risk of app rejection due to App Store guidelines (section 5.1) or Play Store security policies. Contact us for a consultation on your project — we will evaluate your current architecture and propose an optimal solution.







