Biometrics.kt

3.7 KB · raw

1package com.peachpasswords.ui.lock23import android.content.Context4import androidx.biometric.BiometricManager5import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG6import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL7import androidx.biometric.BiometricPrompt8import androidx.core.content.ContextCompat9import androidx.fragment.app.FragmentActivity1011/** Thin wrapper over androidx BiometricPrompt for the re-auth gate. */12object Biometrics {13    private const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL1415    fun isAvailable(context: Context): Boolean =16        BiometricManager.from(context).canAuthenticate(AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS1718    /** Strong (Class-3) biometrics specifically — required for CryptoObject key gating. */19    fun strongAvailable(context: Context): Boolean =20        BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS2122    /** A user-facing reason biometrics can't be used right now, or null when available. */23    fun statusMessage(context: Context): String? =24        when (BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)) {25            BiometricManager.BIOMETRIC_SUCCESS -> null26            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> "No biometrics enrolled"27            BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE,28            BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> "Biometric hardware unavailable"29            BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> "Security update required"30            else -> null31        }3233    fun authenticate(34        activity: FragmentActivity,35        title: String,36        subtitle: String,37        onSuccess: () -> Unit,38        onFail: () -> Unit = {},39    ) {40        val prompt = BiometricPrompt(41            activity,42            ContextCompat.getMainExecutor(activity),43            object : BiometricPrompt.AuthenticationCallback() {44                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = onSuccess()45                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = onFail()46            },47        )48        val info = BiometricPrompt.PromptInfo.Builder()49            .setTitle(title)50            .setSubtitle(subtitle)51            .setAllowedAuthenticators(AUTHENTICATORS)52            .build()53        prompt.authenticate(info)54    }5556    /**57     * Authenticate with a [BiometricPrompt.CryptoObject] (key-gated). Uses58     * BIOMETRIC_STRONG only — device credential is not permitted alongside a59     * CryptoObject. [onSuccess] receives the authenticated result so the caller60     * can use `result.cryptoObject?.cipher`.61     */62    fun authenticateCrypto(63        activity: FragmentActivity,64        title: String,65        subtitle: String,66        crypto: BiometricPrompt.CryptoObject,67        onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit,68        onFail: () -> Unit = {},69    ) {70        val prompt = BiometricPrompt(71            activity,72            ContextCompat.getMainExecutor(activity),73            object : BiometricPrompt.AuthenticationCallback() {74                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = onSuccess(result)75                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = onFail()76            },77        )78        val info = BiometricPrompt.PromptInfo.Builder()79            .setTitle(title)80            .setSubtitle(subtitle)81            .setNegativeButtonText("Use password")82            .setAllowedAuthenticators(BIOMETRIC_STRONG)83            .build()84        prompt.authenticate(info, crypto)85    }86}