package com.peachpasswords.ui.lock import android.content.Context import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity /** Thin wrapper over androidx BiometricPrompt for the re-auth gate. */ object Biometrics { private const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL fun isAvailable(context: Context): Boolean = BiometricManager.from(context).canAuthenticate(AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS /** Strong (Class-3) biometrics specifically — required for CryptoObject key gating. */ fun strongAvailable(context: Context): Boolean = BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS /** A user-facing reason biometrics can't be used right now, or null when available. */ fun statusMessage(context: Context): String? = when (BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)) { BiometricManager.BIOMETRIC_SUCCESS -> null BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> "No biometrics enrolled" BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> "Biometric hardware unavailable" BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> "Security update required" else -> null } fun authenticate( activity: FragmentActivity, title: String, subtitle: String, onSuccess: () -> Unit, onFail: () -> Unit = {}, ) { val prompt = BiometricPrompt( activity, ContextCompat.getMainExecutor(activity), object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = onSuccess() override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = onFail() }, ) val info = BiometricPrompt.PromptInfo.Builder() .setTitle(title) .setSubtitle(subtitle) .setAllowedAuthenticators(AUTHENTICATORS) .build() prompt.authenticate(info) } /** * Authenticate with a [BiometricPrompt.CryptoObject] (key-gated). Uses * BIOMETRIC_STRONG only — device credential is not permitted alongside a * CryptoObject. [onSuccess] receives the authenticated result so the caller * can use `result.cryptoObject?.cipher`. */ fun authenticateCrypto( activity: FragmentActivity, title: String, subtitle: String, crypto: BiometricPrompt.CryptoObject, onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit, onFail: () -> Unit = {}, ) { val prompt = BiometricPrompt( activity, ContextCompat.getMainExecutor(activity), object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = onSuccess(result) override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = onFail() }, ) val info = BiometricPrompt.PromptInfo.Builder() .setTitle(title) .setSubtitle(subtitle) .setNegativeButtonText("Use password") .setAllowedAuthenticators(BIOMETRIC_STRONG) .build() prompt.authenticate(info, crypto) } }