Biometric auth
Skill almasumdev/awesome-mobile-security-agent-skills/.github/skills/auth/biometric-auth
Biometric authentication on mobile using BiometricPrompt and LAContext, with keys cryptographically gated by user biometry. Use when adding unlock, re-auth, or step-up auth flows.From its SKILL.md
npx -y skills add almasumdev/awesome-mobile-security-agent-skills --skill biometric-authAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
4.6 KB, ~1.0k tokens by cl100k_base, as published. Nobody here has run it
Biometric Authentication on Mobile
Instructions
Biometric auth on mobile is only meaningful when it is bound to a cryptographic operation. A biometric prompt that merely returns true can be bypassed by a patched binary.
1. Threat Model
Biometrics protect against:
- Casual device sharing.
- Lost / stolen devices where the PIN is strong.
- Local attackers without the user's finger/face.
Biometrics do not protect against:
- A compromised app process (same binary asking for biometry).
- A coerced user.
- Fingerprint false accepts on cheap sensors (rare but non-zero).
2. Android: BiometricPrompt (Class 3 / Strong)
// Require Class 3 (Strong) biometrics so a CryptoObject is usable.
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setSubtitle("Sign in to Example")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setNegativeButtonText("Cancel")
.build()
val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply {
init(Cipher.ENCRYPT_MODE, keyStore.getKey("token_key", null) as SecretKey)
}
BiometricPrompt(activity, mainExecutor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
val c = result.cryptoObject?.cipher ?: return
val ciphertext = c.doFinal(tokenBytes) // only succeeds post-biometry
store(ciphertext, c.iv)
}
}).authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
The key must be generated with setUserAuthenticationRequired(true) and KeyProperties.AUTH_BIOMETRIC_STRONG.
3. iOS: LAContext + Secure Enclave
let context = LAContext()
context.localizedReason = "Sign in to Example"
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
throw BioError.unavailable(error)
}
// Key generated with .biometryCurrentSet (see keystore-keychain skill).
let privateKey: SecKey = try loadAuthKey()
// Signing operation transparently triggers the biometric prompt.
let signature = try SecKeyCreateSignature(
privateKey, .ecdsaSignatureMessageX962SHA256,
payload as CFData, nil
) as Data? ?? { throw BioError.sign }()
Prefer .deviceOwnerAuthenticationWithBiometrics only. Using .deviceOwnerAuthentication (which falls back to PIN) changes your threat model — PIN is typically much weaker than Face/Touch ID.
4. Invalidation on Enrollment Changes
Always set:
- Android:
setInvalidatedByBiometricEnrollment(true). - iOS:
.biometryCurrentSet(not.biometryAny).
This guarantees that adding a new fingerprint/face invalidates your key, forcing a re-auth.
Handle the resulting exception (KeyPermanentlyInvalidatedException / errSecItemNotFound) by walking the user through re-enrollment rather than crashing.
5. What Biometric Unlock Should Actually Do
A correct biometric unlock flow:
- On first login, derive / receive a secret (e.g., the refresh token).
- Encrypt it with a biometric-gated key and store the ciphertext.
- On next launch, attempt to decrypt → this triggers the biometric prompt.
- Use the plaintext to resume the session.
A wrong flow: if (fingerprintOk) showHomeScreen() — easily bypassable.
6. React Native / Flutter
- React Native:
react-native-keychainwithaccessControl: BIOMETRY_CURRENT_SETcombined withstorage: KEYCHAIN(iOS) /AES_GCM(Android) provides the same cryptographic gating. - Flutter:
local_authis only a boolean check — pair it withflutter_secure_storageusingKeychainAccessibility.first_unlock_this_deviceand aniOptions/aOptionsentry that requires authentication.
7. UX
- Offer a PIN / password fallback for users who can't or won't enroll biometry.
- Never silently fall back from biometry → PIN without telling the user.
- Rate-limit repeated failures on your side; the OS already does basic lockout but your server-side session should also detect anomalies.
Checklist
- Biometric auth gates a cryptographic operation (decrypt or sign), not a boolean.
- Key uses
BIOMETRIC_STRONG/.biometryCurrentSet. - Key is invalidated on new biometric enrollment and UI handles re-enrollment.
- No
.deviceOwnerAuthentication(PIN fallback) unless the threat model allows it. - Fallback to PIN / password is offered and clearly signposted.
- Failure paths do not leak whether the user exists or not.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.