Biometric authentication is a pivotal element of modern mobile security, blending convenience with robust identity verification. Yet, securing it across Android’s diverse ecosystem—spanning varied hardware, biometric modalities like fingerprint and face recognition, and threats such as tampering or reverse-engineering—demands a sophisticated approach. Enter SecureBiometricUtils
, a Kotlin-based utility from the mainapp.utils
package. This class excels in delivering both strong (crypto-backed) and weak (manually verified) biometric authentication flows. This article explores its architecture, capabilities, security fortifications, and practical application, serving as an essential guide for Android developers aiming to build secure, user-centric authentication systems.
SecureBiometricUtils
harnesses Android’s BiometricPrompt
API to enable biometric authentication, adeptly supporting fingerprint and facial recognition. Tailored for Secure biometric apps, it prioritizes extensibility, security, and testability, embedding features like tamper resistance, cryptographic validation, and adaptive modality selection to meet diverse device requirements.
BIOMETRIC_STRONG
, BIOMETRIC_WEAK
, or DEVICE_CREDENTIAL
based on device biometric capabilities.SystemProvider
, SecurityProvider
, CryptoProvider
) and a test mode for streamlined unit testing.Built on dependency injection and modular principles, SecureBiometricUtils
ensures flexibility and ease of testing. Its core components are meticulously designed to separate concerns while maintaining a cohesive security framework.
BiometricManager
for capability checks.SystemProvider
: Abstracts system-level operations (e.g., process enumeration, emulator detection).SecurityProvider
: Oversees security validations (e.g., Frida detection, HMAC computations).CryptoProvider
: Manages cryptographic tasks (e.g., key generation, cipher operations).BiometricPrompt
, ideal for mocking in tests.isTestMode
) and a static toggle (GLOBAL_TEST_MODE
) to disable security checks during testing.The companion object centralizes constants, shared state, and utility functions:
KEY_NAME
, SECURITY_CHECK_INTERVAL_MS
(2 seconds), and MANUAL_VERIFICATION_ATTEMPTS
(3).sessionToken
(UUID), securitySalt
(32-byte random), and lastCipherIV
.logSecurityAlert
, logDebug
, etc.) with SHA-256-based obfuscation for sensitive data.SystemProvider
: Encapsulates low-level system queries (e.g., reading /proc
files).SecurityProvider
: Handles anti-tampering and integrity checks.CryptoProvider
: Provides cryptographic primitives via Android KeyStore.Default implementations (DefaultSystemProvider
, DefaultSecurityProvider
, DefaultCryptoProvider
) offer production-ready functionality, easily swapped for mocks in testing scenarios.
The authenticate
function serves as the primary entry point, orchestrating a secure and adaptive biometric authentication process:
fun authenticate(
activity: FragmentActivity,
title: String = "Biometric Authentication",
subtitle: String = "Verify your identity",
negativeButtonText: String = "Cancel",
onSuccess: () -> Unit,
onError: (Int, String) -> Unit,
onFailed: () -> Unit
)
isSecurityCompromised()
to detect threats like Frida or debuggers.1001
("Security verification failed") if compromised.canAuthenticateWithBiometrics()
to assess BIOMETRIC_STRONG
and BIOMETRIC_WEAK
support.getAllowedAuthenticatorsAutomatically()
opts for BIOMETRIC_WEAK
on face-dominant devices, else BIOMETRIC_STRONG
.generateSecretKey(true)
and builds a CryptoObject
.fallbackWeakKey
) with generateSecretKey(false)
.PromptInfo
and launches BiometricPrompt
using triggerBiometricAuthentication
.handleStrongAuthentication
).handleWeakAuthentication
).onFailed
.BiometricPrompt.CryptoObject
with AES cipher from a KeyStore key.CryptoObject
is provided (e.g., face recognition).verifyChallenge
.MANUAL_VERIFICATION_ATTEMPTS
) with 50ms intervals.Debug.isDebuggerConnected()
and TracerPid
.goldfish
)./proc/self/maps
for anomalies.securitySalt
and sessionToken
to thwart replays.canAuthenticateWithBiometrics()
:
BiometricCapability
detailing modality support.generateSecretKey()
:
KeyGenParameterSpec
for AES/CBC/PKCS7 with 30-second validity.performManualCryptoVerification()
:
isSecurityCompromised()
:
│ 🔐 SECURITY
) enhance readability.BuildConfig.DEBUG
).Here’s how to integrate SecureBiometricUtils
into a FragmentActivity
:
val biometricUtils = SecureBiometricUtils(context)
biometricUtils.authenticate(
activity = this,
title = "Login with Biometrics",
subtitle = "Use your fingerprint or face",
negativeButtonText = "Use PIN instead",
onSuccess = {
Toast.makeText(this, "Authentication successful!", Toast.LENGTH_SHORT).show()
},
onError = { code, message ->
Toast.makeText(this, "Error $code: $message", Toast.LENGTH_SHORT).show()
},
onFailed = {
Toast.makeText(this, "Authentication failed", Toast.LENGTH_SHORT).show()
}
)
Below is the complete source code for SecureBiometricUtils
as implemented in the mainapp.utils
package. Use the "Copy" button to easily copy the code to your clipboard for reference or use in your projects.
package mainapp.utils
import mainapp.BuildConfig
import android.content.Context
import android.os.Build
import android.os.Debug
import android.os.Handler
import android.os.Looper
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import timber.log.Timber
import java.io.File
import java.lang.reflect.Method
import java.security.KeyStore
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.UUID
import java.util.concurrent.Executor
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* SecureBiometricUtils provides secure biometric authentication (face and fingerprint)
* using both strong (crypto‑backed) and weak (manual crypto verification) methods.
*
* Features:
* - Supports both strong and weak biometrics (with a fallback manual crypto verification)
* - Leverages Android’s OS‑based modality selection by automatically choosing between:
* • BIOMETRIC_STRONG or BIOMETRIC_WEAK (when a weak modality like face is detected)
* • BIOMETRIC_STRONG or DEVICE_CREDENTIAL (otherwise)
* - If a crypto object is returned (strong biometric), additional cipher verification is performed.
* - If no crypto object is returned (as with weak biometrics), a weak key is generated with a short
* validity (10 seconds) and used for manual crypto verification.
* - Includes anti‑Frida and tamper detection (process, memory, hooks).
* - Uses ASCII box–style logs with obfuscation.
* - Designed to be debug‑friendly and unit test–friendly via injectable providers.
*/
class SecureBiometricUtils(
private val context: Context,
private val systemProvider: SystemProvider = DefaultSystemProvider(),
val securityProvider: SecurityProvider = DefaultSecurityProvider(),
val cryptoProvider: CryptoProvider = DefaultCryptoProvider(),
val isTestMode: Boolean = false, // For unit tests only.
val biometricPromptFactory: (FragmentActivity, Executor, BiometricPrompt.AuthenticationCallback) -> BiometricPrompt =
{ activity, executor, callback -> BiometricPrompt(activity, executor, callback) }
) {
companion object {
const val KEY_NAME = "BiometricKey"
const val RO_DEBUG = "ro.debuggable"
const val SVF = "Security verification failed"
const val AVE = "Authentication validation exception"
const val KEYSTORE_PROVIDER = "AndroidKeyStore"
const val SECURITY_CHECK_INTERVAL_MS = 2000L // 2 seconds
const val MANUAL_VERIFICATION_ATTEMPTS = 3
var securityCheckCooldown: Long = 0
var lastSecurityState: Boolean = false
private var lastCipherIV: ByteArray? = null
// Device-specific values (initialized at installation time)
private val sessionToken: String by lazy { UUID.randomUUID().toString() }
private val securitySalt: ByteArray by lazy { generateSecuritySalt() }
// Global test mode (for unit tests only – not used in production)
@JvmStatic
var GLOBAL_TEST_MODE = false
@JvmStatic
fun setGlobalTestMode(enabled: Boolean) {
if (BuildConfig.DEBUG && !enabled) {
// Only allow disabling test mode in debug builds
GLOBAL_TEST_MODE = false
logDebug("🧪 Global test mode disabled")
}
}
fun generateSecuritySalt(): ByteArray {
val salt = ByteArray(32)
SecureRandom().nextBytes(salt)
return salt
}
// ────── Logging Helpers ──────
fun logSecurityAlert(message: String) {
logIfEnabled {
val obfuscatedMessage = obfuscateLogMessage("SECURITY_ALERT", message)
Timber.w(
"""
┌─────────────────────────────────────────
│ 🔐 SECURITY
│ $obfuscatedMessage
└─────────────────────────────────────────
""".trimIndent()
)
}
}
inline fun logIfEnabled(action: () -> Unit) {
if (BuildConfig.DEBUG) action()
}
fun logDebug(message: String) {
logIfEnabled {
val obfuscatedMessage = obfuscateLogMessage("DEBUG_LOG", message)
Timber.d(
"""
┌─────────────────────────────────────────
│ 🔍 DEBUG
│ $obfuscatedMessage
└─────────────────────────────────────────
""".trimIndent()
)
}
}
fun logCryptoOperation(message: String) {
logIfEnabled {
val obfuscatedMessage = obfuscateLogMessage("CRYPTO_OP", message)
Timber.i(
"""
┌─────────────────────────────────────────
│ 🔑 OPERATION
│ $obfuscatedMessage
└─────────────────────────────────────────
""".trimIndent()
)
}
}
fun logError(error: String, exception: Exception? = null) {
logIfEnabled {
if (exception != null) {
Timber.e(
exception,
"""
┌─────────────────────────────────────────
│ ❌ ERROR
│ $error
└─────────────────────────────────────────
""".trimIndent()
)
} else {
Timber.e(
"""
┌─────────────────────────────────────────
│ ❌ ERROR
│ $error
└─────────────────────────────────────────
""".trimIndent()
)
}
}
}
fun obfuscateLogMessage(type: String, message: String): String {
val text = "$type:$message"
return try {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(text.toByteArray())
val obfuscated = hash.copyOfRange(0, 8).joinToString("") { "%02x".format(it) }
"$obfuscated:$message"
} catch (e: Exception) {
message
}
}
}
// ────── Provider Interfaces ──────
interface SystemProvider {
fun getProcessList(): List
fun readMemoryMaps(): String
fun isDebuggerConnected(): Boolean
fun isRunningInEmulator(): Boolean
fun getLibraries(): List
fun getSystemProperties(): Map
}
interface SecurityProvider {
fun detectFrida(
processList: List,
memoryMaps: String,
libraries: List
): Boolean
fun calculateHmac(data: ByteArray, key: ByteArray): ByteArray
fun verifyHmac(data: ByteArray, key: ByteArray, expectedHmac: ByteArray): Boolean
fun getRuntimeIntegrity(): String
}
interface CryptoProvider {
fun getKeyStore(): KeyStore
fun generateSecretKey(useStrongBiometrics: Boolean): SecretKey?
fun getCipher(): Cipher
fun createCryptoObject(
secretKey: SecretKey,
useStrongBiometrics: Boolean
): BiometricPrompt.CryptoObject?
fun encrypt(data: ByteArray, secretKey: SecretKey): Pair
fun decrypt(data: ByteArray, secretKey: SecretKey, iv: ByteArray): ByteArray
}
// ────── Default Provider Implementations ──────
open class DefaultSystemProvider : SystemProvider {
override fun getProcessList(): List {
return try {
File("/proc").listFiles()
?.filter { it.name.all { char -> char.isDigit() } }
?.mapNotNull { proc ->
try {
File(proc, "cmdline").readText()
} catch (e: Exception) {
null
}
} ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}
override fun readMemoryMaps(): String {
return try {
File("/proc/self/maps").readText()
} catch (e: Exception) {
""
}
}
override fun isDebuggerConnected(): Boolean {
return Debug.isDebuggerConnected() || isPtraceAttached()
}
private fun isPtraceAttached(): Boolean {
return try {
val status = File("/proc/self/status").readText()
val tracerPid =
Regex("TracerPid:\\s*(\\d+)").find(status)?.groupValues?.get(1)?.toInt() ?: 0
tracerPid > 0
} catch (e: Exception) {
false
}
}
override fun isRunningInEmulator(): Boolean {
return Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
Build.MODEL.contains("google_sdk") ||
Build.MODEL.contains("Emulator") ||
Build.MODEL.contains("Android SDK") ||
Build.MANUFACTURER.contains("Genymotion") ||
Build.HARDWARE.contains("goldfish") ||
Build.HARDWARE.contains("ranchu") ||
Build.PRODUCT.contains("sdk") ||
Build.PRODUCT.contains("google_sdk") ||
Build.PRODUCT.contains("sdk_x86") ||
Build.PRODUCT.contains("vbox86p") ||
Build.BOARD.lowercase().contains("nox") ||
Build.BOOTLOADER.lowercase().contains("nox") ||
Build.HARDWARE.lowercase().contains("nox") ||
Build.PRODUCT.lowercase().contains("nox")
}
override fun getLibraries(): List {
return try {
val libs = mutableListOf()
System.getProperty("java.library.path")?.split(":")?.forEach { path ->
File(path).listFiles()?.filter { it.isFile && it.name.endsWith(".so") }
?.forEach { libs.add(it.absolutePath) }
}
libs
} catch (e: Exception) {
emptyList()
}
}
override fun getSystemProperties(): Map {
val props = mutableMapOf()
try {
props[RO_DEBUG] = getSystemProperty(RO_DEBUG, "0")
props["ro.secure"] = getSystemProperty("ro.secure", "1")
props["service.adb.root"] = getSystemProperty("service.adb.root", "0")
} catch (e: Exception) {
logDebug(e.message.toString())
}
return props
}
open fun getSystemProperty(property: String, defaultValue: String): String {
return try {
val process = Runtime.getRuntime().exec("getprop $property")
process.inputStream.bufferedReader().use { it.readLine() } ?: defaultValue
} catch (e: Exception) {
defaultValue
}
}
}
class DefaultSecurityProvider : SecurityProvider {
override fun detectFrida(
processList: List,
memoryMaps: String,
libraries: List
): Boolean {
val keywords = listOf(
"frida", "frida-server", "frida-agent", "frida-gadget", "frida-inject",
"frida-helper", "libfrida-gadget", "frida-core", "frida-trace", "objection",
"fridump", "epicfrida", "r2frida", "redfang", "hyperion", "fridascript"
)
val found = keywords.any { keyword ->
processList.any { it.contains(keyword, ignoreCase = true) } ||
memoryMaps.contains(keyword, ignoreCase = true) ||
libraries.any { it.contains(keyword, ignoreCase = true) }
}
if (found) logSecurityAlert("Detected possible Frida-based tampering.")
return found || checkFridaSockets()
}
fun checkFridaSockets(fileReader: (String) -> List = { path ->
File(path).useLines { it.toList() }
}): Boolean {
val fridaPorts = setOf("69B2", "69B3") // Hex for Frida ports 27042 and 27043
return try {
listOf("/proc/net/tcp", "/proc/net/tcp6").any { path ->
fileReader(path).any { line -> fridaPorts.any { port -> port in line } }
}
} catch (e: Exception) {
false
}
}
override fun calculateHmac(data: ByteArray, key: ByteArray): ByteArray {
// Combine data with securitySalt and sessionToken for extra entropy.
val additionalData = securitySalt + sessionToken.toByteArray()
val combinedData = data + additionalData
val hmacKey = SecretKeySpec(key, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(hmacKey)
return mac.doFinal(combinedData)
}
override fun verifyHmac(data: ByteArray, key: ByteArray, expectedHmac: ByteArray): Boolean {
val calculated = calculateHmac(data, key)
return calculated.contentEquals(expectedHmac)
}
override fun getRuntimeIntegrity(): String {
return try {
val classes = listOf(
BiometricPrompt::class.java.name,
KeyStore::class.java.name,
Cipher::class.java.name,
SecureBiometricUtils::class.java.name
)
val digest = MessageDigest.getInstance("SHA-256")
classes.forEach { className ->
try {
Class.forName(className).declaredMethods.forEach { method ->
digest.update(method.name.toByteArray())
}
} catch (e: Exception) {
logDebug(e.message.toString())
}
}
digest.digest().joinToString("") { "%02x".format(it) }
} catch (e: Exception) {
""
}
}
}
open class DefaultCryptoProvider : CryptoProvider {
override fun getKeyStore(): KeyStore {
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER)
keyStore.load(null)
return keyStore
}
override fun generateSecretKey(useStrongBiometrics: Boolean): SecretKey? {
return try {
val ks = getKeyStore()
val alias = if (useStrongBiometrics) "${KEY_NAME}_STRONG" else "${KEY_NAME}_WEAK"
if (ks.containsAlias(alias)) ks.deleteEntry(alias)
val keyGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
val keyGenSpecBuilder = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(false)
@Suppress("kotlin:S1874", "DEPRECATION")
keyGenSpecBuilder.setUserAuthenticationValidityDurationSeconds(30)
val spec = keyGenSpecBuilder.build()
keyGenerator.init(spec)
keyGenerator.generateKey().let {
ks.getKey(alias, null) as SecretKey
}
} catch (e: Exception) {
logError("Failed to generate secure key", e)
null
}
}
override fun getCipher(): Cipher {
return Cipher.getInstance(
"${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}"
)
}
override fun createCryptoObject(
secretKey: SecretKey,
useStrongBiometrics: Boolean
): BiometricPrompt.CryptoObject? {
// If the biometric modality is weak (e.g. face recognition), bypass crypto object creation.
if (!useStrongBiometrics) {
logDebug("Skipping crypto object creation for weak biometrics.")
return null
}
// Otherwise, try using the cipher for strong biometrics.
return try {
val cipher = getCipher()
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
logDebug("🔑 Using Cipher for strong biometric authentication")
BiometricPrompt.CryptoObject(cipher)
} catch (e: Exception) {
logError("Failed to initialize crypto object", e)
null
}
}
override fun encrypt(data: ByteArray, secretKey: SecretKey): Pair {
val cipher = getCipher()
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val encrypted = cipher.doFinal(data)
return Pair(encrypted, cipher.iv)
}
override fun decrypt(data: ByteArray, secretKey: SecretKey, iv: ByteArray): ByteArray {
val cipher = getCipher()
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
return cipher.doFinal(data)
}
}
// ────── Biometric Capability Data ──────
data class BiometricCapability(
val isStrongAvailable: Boolean,
val isWeakAvailable: Boolean,
val isFaceAvailable: Boolean,
val isFingerprintAvailable: Boolean,
val strongStatusCode: Int,
val weakStatusCode: Int
) {
val isAnyBiometricAvailable: Boolean get() = isStrongAvailable || isWeakAvailable
fun getPreferredType(): BiometricType {
return when {
isStrongAvailable -> BiometricType.STRONG
isWeakAvailable -> BiometricType.WEAK
else -> BiometricType.NONE
}
}
}
enum class BiometricType { STRONG, WEAK, NONE }
// The function that checks biometric capability.
fun canAuthenticateWithBiometrics(): BiometricCapability {
val biometricManager = BiometricManager.from(context)
val strongResult =
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
val weakResult =
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
val strongAvailable = strongResult == BiometricManager.BIOMETRIC_SUCCESS
val weakAvailable = weakResult == BiometricManager.BIOMETRIC_SUCCESS
// If weak biometrics are available, assume that both face and fingerprint options should be offered.
val faceAvailable = weakAvailable // instead of using Build.FINGERPRINT heuristic
val fingerprintAvailable = strongAvailable || weakAvailable
return BiometricCapability(
isStrongAvailable = strongAvailable,
isWeakAvailable = weakAvailable,
isFaceAvailable = faceAvailable,
isFingerprintAvailable = fingerprintAvailable,
strongStatusCode = strongResult,
weakStatusCode = weakResult
)
}
// Fallback weak key storage for manual crypto verification.
var fallbackWeakKey: SecretKey? = null
/**
* Helper that automatically selects allowed authenticators based on biometric capability.
* If a weak modality (typically face) is detected, returns:
* BIOMETRIC_STRONG or BIOMETRIC_WEAK
*/
fun getAllowedAuthenticatorsAutomatically(): Int {
val capability = canAuthenticateWithBiometrics()
// If face biometrics are available (i.e. using a weak modality), force the weak path.
return if (capability.isFaceAvailable && capability.isWeakAvailable) {
BiometricManager.Authenticators.BIOMETRIC_WEAK
} else {
BiometricManager.Authenticators.BIOMETRIC_STRONG
}
}
// ────── Public Authentication Entry Point ──────
/**
* Initiates secure biometric authentication.
*
* This public API leverages Android’s OS‑based selection by automatically choosing allowed authenticators.
* It uses (BIOMETRIC_STRONG or BIOMETRIC_WEAK) when a weak modality (e.g. face) is available,
* and (BIOMETRIC_STRONG or DEVICE_CREDENTIAL) otherwise.
*
* If a crypto object is returned (strong biometric), additional cipher verification is performed.
* If no crypto object is returned (weak biometric), we fall back on manual crypto verification if possible.
*
* fun authenticate(
* activity: FragmentActivity,
* title: String = "Biometric Authentication",
* subtitle: String = "Verify your identity",
* negativeButtonText: String = "Cancel",
* onSuccess: () -> Unit,
* onError: (Int, String) -> Unit,
* onFailed: () -> Unit
* )
*/
fun authenticate(
activity: FragmentActivity,
title: String = "Biometric Authentication",
subtitle: String = "Verify your identity",
negativeButtonText: String = "Cancel",
onSuccess: () -> Unit,
onError: (Int, String) -> Unit,
onFailed: () -> Unit
) {
if (isSecurityCompromised() || performSecondaryVerification()) {
logSecurityAlert("Authentication blocked due to security compromise")
onError(1001, SVF)
return
}
val allowedAuthenticators = getAllowedAuthenticatorsAutomatically()
val promptInfo = buildPromptInfo(title, subtitle, negativeButtonText, allowedAuthenticators)
val authCallback = createAuthCallback(onSuccess, onError, onFailed)
val executor = MainThreadExecutor()
// Use the injected factory to create the BiometricPrompt.
val biometricPrompt = biometricPromptFactory(activity, executor, authCallback)
try {
val capability = canAuthenticateWithBiometrics()
val useStrong = capability.getPreferredType() == BiometricType.STRONG && !capability.isFaceAvailable
if (useStrong) {
val strongKey = cryptoProvider.generateSecretKey(true)
val cryptoObject = strongKey?.let { cryptoProvider.createCryptoObject(it, true) }
if (cryptoObject != null) {
triggerBiometricAuthentication(biometricPrompt, promptInfo, cryptoObject)
} else {
fallbackWeakKey = cryptoProvider.generateSecretKey(false)
triggerBiometricAuthentication(biometricPrompt, promptInfo)
}
} else if (capability.isWeakAvailable) {
fallbackWeakKey = cryptoProvider.generateSecretKey(false)
triggerBiometricAuthentication(biometricPrompt, promptInfo)
} else {
triggerBiometricAuthentication(biometricPrompt, promptInfo)
}
} catch (e: Exception) {
logError("Failed to start biometric authentication", e)
onError(1008, "Failed to start authentication: ${e.localizedMessage}")
}
}
@Suppress("kotlin:S6293") // Suppress Sonar security hotspot for biometric authentication without CryptoObject
// Justification: Weak biometric authentication (e.g., face unlock) is used for non-sensitive operations.
// Manual crypto verification is implemented as a fallback to ensure security.
fun triggerBiometricAuthentication(
biometricPrompt: BiometricPrompt,
promptInfo: BiometricPrompt.PromptInfo,
cryptoObject: BiometricPrompt.CryptoObject? = null
) {
try {
cryptoObject?.let {
biometricPrompt.authenticate(promptInfo, it)
} ?: biometricPrompt.authenticate(promptInfo)
} catch (e: SecurityException) {
logError("🚨 Security Exception in biometric authentication: ${e.localizedMessage}")
} catch (e: Exception) {
logError("🔥 Unexpected biometric authentication failure: ${e.localizedMessage}")
}
}
fun buildPromptInfo(
title: String,
subtitle: String,
negativeButtonText: String,
allowedAuthenticators: Int
): BiometricPrompt.PromptInfo {
val builder = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(allowedAuthenticators)
if (allowedAuthenticators and BiometricManager.Authenticators.DEVICE_CREDENTIAL == 0) {
builder.setNegativeButtonText(negativeButtonText)
}
return builder.build()
}
fun createAuthCallback(
onSuccess: () -> Unit,
onError: (Int, String) -> Unit,
onFailed: () -> Unit
): BiometricPrompt.AuthenticationCallback {
return object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
logDebug("✅ Biometric authentication succeeded")
// Immediately check for signs of tampering.
if (isSecurityCompromised()) {
logSecurityAlert("Security compromised after biometric authentication")
onError(1013, SVF)
return
}
// If a crypto object is returned, perform strong flow verification.
result.cryptoObject?.cipher?.let { cipher ->
handleStrongAuthentication(cipher, onSuccess, onError)
return
}
// Otherwise, process as weak biometric authentication using local verification.
handleWeakAuthentication(onSuccess, onError)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
logError("Authentication error: $errString ($errorCode)")
onError(errorCode, errString.toString())
}
override fun onAuthenticationFailed() {
logDebug("Authentication attempt failed")
onFailed()
}
}
}
fun handleStrongAuthentication(
cipher: Cipher,
onSuccess: () -> Unit,
onError: (Int, String) -> Unit
) {
try {
val verificationData = "VERIFY:${UUID.randomUUID()}".toByteArray()
val encrypted = cipher.doFinal(verificationData)
if (encrypted.isEmpty()) {
logSecurityAlert("🔴 Authentication tampered: empty encryption result")
onError(1011, "Authentication validation failed")
return
}
logCryptoOperation("🔐 Strong biometric authentication verified with crypto")
onSuccess()
} catch (e: Exception) {
logSecurityAlert("🔴 Exception during cipher verification: ${e.message}")
onError(1012, AVE)
}
}
fun handleWeakAuthentication(
onSuccess: () -> Unit,
onError: (Int, String) -> Unit
) {
try {
val freshKey = cryptoProvider.generateSecretKey(false)
if (freshKey != null) {
fallbackWeakKey = freshKey
logDebug("Generated fresh weak key after authentication")
// Call your local verification routine.
performManualCryptoVerification(
freshKey,
{
// Success callback: no parameters.
onSuccess()
},
{ _, errorMsg ->
// Error callback: always report error code 1012 and fixed message.
logSecurityAlert("🔴 Exception during local verification: $errorMsg")
onError(1012, AVE)
}
)
} else {
logSecurityAlert("Weak biometric auth - no verification available")
if (isSecurityCompromised()) {
logSecurityAlert("Security compromised during fallback path")
onError(1012, AVE)
return
}
onSuccess()
}
} catch (e: Exception) {
logError("Exception during weak biometric verification", e)
if (isSecurityCompromised()) {
logSecurityAlert("Security compromised during exception handler")
onError(1012, AVE)
} else {
logSecurityAlert("Accepting weak biometric auth after key error")
onSuccess()
}
}
}
// ────── Manual Crypto Verification for Weak Biometrics ──────
fun verifyChallenge(secretKey: SecretKey, challenge: ByteArray): Boolean {
return try {
val (encrypted, iv) = cryptoProvider.encrypt(challenge, secretKey)
val decrypted = cryptoProvider.decrypt(encrypted, secretKey, iv)
if (challenge.contentEquals(decrypted)) {
lastCipherIV = iv
true
} else {
logSecurityAlert("🔴 Challenge verification failed – content mismatch")
false
}
} catch (e: Exception) {
logDebug("Challenge verification exception: ${e.message}")
false
}
}
fun performManualCryptoVerification(
secretKey: SecretKey,
onSuccess: () -> Unit,
onError: (Int, String) -> Unit
) {
val startTime = System.currentTimeMillis()
var lastException: Exception? = null
repeat(MANUAL_VERIFICATION_ATTEMPTS) { attempt ->
try {
// Generate a random challenge using a defined challenge size constant.
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
if (verifyChallenge(secretKey, challenge)) {
val elapsed = System.currentTimeMillis() - startTime
logCryptoOperation("🔐 Weak biometric auth verified with manual crypto (attempt ${attempt + 1}, ${elapsed}ms)")
// Additional security check after successful verification.
if (isSecurityCompromised()) {
logSecurityAlert("Security compromised during verification")
onError(1016, SVF)
return
}
onSuccess()
return
}
} catch (e: Exception) {
lastException = e
val elapsedSoFar = System.currentTimeMillis() - startTime
logDebug("Manual verification attempt ${attempt + 1} failed after ${elapsedSoFar}ms: ${e.message}")
}
// Delay between attempts for device timing variations.
Thread.sleep(50)
}
val totalElapsed = System.currentTimeMillis() - startTime
logSecurityAlert("🔴 Manual crypto verification failed after $MANUAL_VERIFICATION_ATTEMPTS attempts (${totalElapsed}ms)")
lastException?.let { logError("Last exception during verification", it) }
onError(1010, "Post-authentication verification failed")
}
// ────── Security Checks ──────
// Add this function for a separate verification path
fun performSecondaryVerification(): Boolean {
try {
// Secondary verification that runs even in debug mode
val memoryMaps = systemProvider.readMemoryMaps()
// Check for specific Frida signatures that debug mode shouldn't bypass
val criticalSignatures = listOf(
"frida-gadget",
"frida-agent",
"libfrida"
)
return criticalSignatures.any { memoryMaps.contains(it, ignoreCase = true) }
} catch (e: Exception) {
return false
}
}
fun isSecurityCompromised(): Boolean {
if (isTestMode || GLOBAL_TEST_MODE) {
// Only perform critical checks in test mode
val runtimeIntegrity = securityProvider.getRuntimeIntegrity()
if (runtimeIntegrity.isEmpty()) {
logSecurityAlert("Runtime integrity verification failed even in test mode")
return true
}
return false
}
val currentTime = System.currentTimeMillis()
if (currentTime - securityCheckCooldown < SECURITY_CHECK_INTERVAL_MS) return lastSecurityState
try {
val processList = systemProvider.getProcessList()
val memoryMaps = systemProvider.readMemoryMaps()
val libraries = systemProvider.getLibraries()
val sysProps = systemProvider.getSystemProperties()
if (securityProvider.getRuntimeIntegrity().isEmpty()) {
logSecurityAlert("Runtime integrity verification failed")
updateSecurityState(true)
return true
}
if (securityProvider.detectFrida(processList, memoryMaps, libraries)) {
logSecurityAlert("Security violation detected (Frida)")
updateSecurityState(true)
return true
}
if (systemProvider.isDebuggerConnected()) {
logSecurityAlert("Debug session detected")
updateSecurityState(true)
return true
}
if (sysProps[RO_DEBUG] == "1") {
logSecurityAlert("Device is debuggable")
updateSecurityState(true)
return true
}
if (detectFunctionHooks() || detectMemoryTampering()) {
logSecurityAlert("Function hooks or memory tampering detected")
updateSecurityState(true)
return true
}
updateSecurityState(false)
return false
} catch (e: Exception) {
logError("Error during security verification", e)
updateSecurityState(true)
return true
}
}
fun updateSecurityState(isCompromised: Boolean) {
lastSecurityState = isCompromised
securityCheckCooldown = System.currentTimeMillis()
}
fun detectFunctionHooks(
classLoader: (String) -> Array? = { className ->
try {
Class.forName(className).declaredMethods
} catch (e: Exception) {
null
}
}
): Boolean {
val classesToCheck = listOf(
"android.hardware.biometrics.BiometricPrompt",
"android.security.keystore.KeyStore"
)
return classesToCheck.any { className ->
classLoader(className)?.any { it.isAccessible || it.isSynthetic } ?: false
}
}
fun detectMemoryTampering(fileReader: () -> String = { File("/proc/self/maps").readText() }): Boolean {
return try {
fileReader().contains("frida", ignoreCase = true)
} catch (e: Exception) {
false
}
}
fun isRunningInEmulator(): Boolean {
return systemProvider.isRunningInEmulator() // Delegate to the system provider
}
// ────── Executor for BiometricPrompt Callbacks ──────
class MainThreadExecutor : Executor {
private val handler = Handler(Looper.getMainLooper())
override fun execute(command: Runnable) {
handler.post(command)
}
}
}
SystemProvider
, SecurityProvider
, and CryptoProvider
for controlled behavior.isTestMode = true
or GLOBAL_TEST_MODE = true
to skip security checks.biometricPromptFactory
to bypass UI dependencies.@Test
fun `test strong biometric authentication`() {
val mockCrypto = mock {
on { generateSecretKey(true) } doReturn SecretKeySpec(ByteArray(16), "AES")
on { createCryptoObject(any(), eq(true)) } doReturn BiometricPrompt.CryptoObject(Cipher.getInstance("AES/CBC/PKCS7Padding"))
}
val utils = SecureBiometricUtils(
context = mock(),
cryptoProvider = mockCrypto,
isTestMode = true
)
utils.authenticate(
activity = mock(),
onSuccess = { assertTrue(true) },
onError = { _, _ -> fail() },
onFailed = { fail() }
)
}
SecureBiometricUtils
stands out as a versatile, security-focused solution for Android biometric authentication. Its dual-mode design ensures broad device compatibility, while its anti-tampering and cryptographic safeguards counter advanced threats. It provides a reliable, extensible base. Mastering its mechanics empowers you to deliver seamless, fortified authentication experiences that uphold the highest security standards.