İçeriğe geç
Tüm yazılar

Android'de biyometrik kimlik doğrulama, 2026: BiometricPrompt, Keystore ve yerel öncelikli bir uygulamayı kilitlemek

androidx.biometric için pratik bir rehber — BiometricPrompt, CryptoObject destekli anahtarlar, cihaz kimlik bilgisi yedeği ve bir parmak izi kontrolünün hiçbir şeyi korumamasına yol açan hatalar.

MFKAPPS 5 dk okuma

Sadece bir ekranı kilitleyen bir parmak izi istemi süstür. Altındaki veri o süre boyunca düz bir Room veritabanında duruyorsa, adb’ye ve root’lanmış bir cihaza — ya da bir yedek çıkarma aracına — sahip herkes sensöre hiç dokunmadan bu veriyi okur. Android’de gerçek biyometrik koruma, istemin bir kriptografik anahtarın kilidini açması demektir; saldırganla veri arasında gerçekten duran şey o anahtardır. Çoğu BiometricPrompt rehberinin atladığı kısım burası, ve önemli olan tek kısım da bu.

Bunu Granyn’in bütçe verisini cihaz üzerinde kilitlemek için ekledim; aynı desen bir ilaç kaydını veya bir abonelik listesini yerel öncelikli herhangi bir uygulamada korur. İşte gerçekten işlevsel olan versiyonu.

”Biyometrik doğrulama” denen iki farklı şey

androidx.biometric iki kimlik doğrulama modunu destekler ve bunlar tamamen farklı şeyleri korur:

  • Sadece doğrulama. CryptoObject olmadan BiometricPrompt.authenticate(PromptInfo), sadece “kayıtlı biyometrik eşleşti mi?” diye sorar ve evet ya da hayır olarak geri döner. Bu bir arayüz kapısıdır. Hiçbir şey şifrelenmez, hiçbir şeyin şifresi çözülmez — root’lanmış bir cihaz veya bir content provider hatası bunu tamamen atlar, çünkü veri hiçbir zaman istemin başarılı olmasına bağlı olmamıştır.
  • Kripto destekli doğrulama. BiometricPrompt.authenticate(PromptInfo, CryptoObject), istemi setUserAuthenticationRequired(true) ile işaretlenmiş bir Android Keystore anahtarından oluşturulan bir Cipher’a bağlar. İşletim sisteminin kendisi, eşleşen bir biyometrik veya cihaz kimlik bilgisi doğrulaması az önce başarılı olmadan o anahtarın hiçbir şey yapmasına — şifreleme, şifre çözme, imzalama — izin vermez. Anahtar, sadece geleneksel anlamda kapılı değil, istem olmadan fiziksel olarak kullanılamaz hale gelir.

Hedef “bütçe rakamlarını kilit açılmadan okunamaz tutmak” ise, bunu yapan sadece ikincisidir. Birincisi, dinlenme halindeki hassas veriyi işlemeyen bir ekranda yıkıcı bir eylemden önce yeniden onay almak gibi daha düşük riskli bir şey için uygundur.

Kimlik doğrulama gerektiren bir Keystore anahtarı oluşturmak

private const val KEY_ALIAS = "granyn_db_key"

fun getOrCreateKey(): SecretKey {
    val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }

    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
    )
    val spec = KeyGenParameterSpec.Builder(
        KEY_ALIAS,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
        .build()
    keyGenerator.init(spec)
    return keyGenerator.generateKey()
}

setUserAuthenticationParameters(0, AUTH_BIOMETRIC_STRONG) önemli satırdır: 0 süresi, anahtarın kayan bir zaman penceresi için değil, her doğrulama başına tam olarak bir işlem için kullanılabilir olduğu anlamına gelir. Bu, herhangi bir başarılı kontrolden sonra bir anahtarın N saniye boyunca kilidi açık kalmasına izin veren eski setUserAuthenticationValidityDurationSeconds’tan bilinçli olarak daha katıdır — kullanışlıydı, ve aynı zamanda “biyometrik korumalı” birçok uygulamanın arka plandaki bir servis anahtara ikinci kez dokunduğunda sessizce hiçbir şeyi korumamasının nedeniydi. Özellikle BIOMETRIC_STRONG isteyin; BIOMETRIC_WEAK, bir fotoğrafın bazen kandırabildiği bir açıdan yüzünüzle kilit açmak gibi şeyleri kapsar — bu, finansal veriyi korumak için istediğiniz bir eşik değildir.

İstemi cipher’a bağlamak

suspend fun authenticateAndDecrypt(
    activity: FragmentActivity,
    ciphertext: ByteArray,
    iv: ByteArray,
): ByteArray = suspendCancellableCoroutine { cont ->
    val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply {
        init(Cipher.DECRYPT_MODE, getOrCreateKey(), GCMParameterSpec(128, iv))
    }

    val callback = object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            val decrypted = result.cryptoObject?.cipher?.doFinal(ciphertext)
            if (decrypted != null) cont.resume(decrypted) else cont.cancel()
        }
        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            cont.cancel()
        }
    }

    val prompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), callback)
    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Unlock Granyn")
        .setSubtitle("Confirm it's you to view your budget")
        .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        .build()

    prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}

Burada iki şey insanları takıldığı yerdir. Birincisi, güçlü biyometriği cihazın PIN/desen/şifre yedeğiyle birleştiren setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL), setNegativeButtonText() ile birbirini dışlar. İkisini birden çağırmak, istem derleme zamanında hata fırlatır. Cihaz kimlik bilgisi seçeneği zaten sensör başarısız olduğunda veya hiçbir şey kayıtlı olmadığında kullanıcılara bir çıkış yolu verir, bu yüzden manuel bir negatif düğme onunla birlikte değil, onun yerine geçen gereksiz bir şeydir.

İkincisi, onAuthenticationSucceeded içinde geri aldığınız CryptoObject bir nezaket kopyası değildir — şimdi kilidi açılmış, üzerinde hemen doFinal() çağırmanız gereken aynı Cipher örneğidir. Geri çağrı tetiklendikten sonra taze bir Cipher yeniden oluşturmak tüm amacı boşa çıkarır: o taze cipher hiçbir zaman doğrulama kontrolü tarafından onaylanmamıştır, bu yüzden onu kullanmaya çalışmak UserNotAuthenticatedException fırlatır — ya da daha kötüsü, doğrulama gereksinimi olmadan oluşturduysanız, sessizce çalışır ve kapatmaya çalıştığınız açığı sessizce yeniden açar.

İstemi hiç göstermeden önce kullanılabilirliği kontrol etmek

val biometricManager = BiometricManager.from(context)
when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
    BiometricManager.BIOMETRIC_SUCCESS -> { /* devam et */ }
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
        // Ne parmak izi/yüz NE DE PIN/desen/şifre ayarlı. Sessizce
        // authenticate() çağırıp başarısız olmasına izin vermek yerine
        // Settings.ACTION_BIOMETRIC_ENROLL'a yönlendirin.
    }
    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
    BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
        // Kendi yönettiğiniz bir uygulama seviyesi PIN'e geri dönün, ya da
        // erişimi engellemek yerine bu cihazda kilit özelliğini tamamen atlayın.
    }
    else -> { /* kalan hata kodlarını ele al */ }
}

canAuthenticate() isteğe bağlı bir boilerplate değildir — net bir “bunu etkinleştirmek için bir ekran kilidi kurun” istemi ile kayıtlı kimlik bilgisi olmayan bir kullanıcının anahtarı ilk kez açtığında yaşadığı kafa karıştırıcı bir başarısızlık arasındaki farktır. Bu yolu bilinçli olarak test edin: emülatörün kayıtlı parmak izlerini silin (Ayarlar → Güvenlik → Parmak İzi, ya da emülatörün genişletilmiş kontrollerinde Parmak İzi panelinden) ve uygulamanın çökmek yerine düzgün bir şekilde geri çekildiğini doğrulayın.

Kilide gerçekten neyin ihtiyacı var

Her şey bundan faydalanmaz. Kripto destekli bir anahtar gerçek bir sürtünme ekler — şifre çözme başına bir doğrulama, okuma başına bir Keystore gidiş-dönüşü — bu yüzden tüm bir veritabanı okuma yolunu bununla kapatmak, “uygulamayı aç”ı fark edilir şekilde daha yavaş bir işleme dönüştürür. Granyn için karar verdiğim şey: telefon kaybolursa gerçekten önemli olan alanları — hesap bakiyelerini ve işlem notlarını — şifrelemek, kategori adlarını ve arayüz tercihlerini ise düz Room sütunlarında bırakmak. Kilit gerçekten hassas olanı korur; geri kalan her şey hızlı kalır.

Bunlardan tutulması gereken alışkanlık şu: “parmak izinizle koruyun” özelliğini göndermeden önce, istem tamamen atlanırsa ne olacağını sorun — root, bir debugger, bir content-provider atlatması. Cevap “hiçbir şey, veri hâlâ okunabilir” ise, istem hiçbir zaman görünüşte yaptığı işi yapmıyordu demektir.