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.
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.
CryptoObjectolmadanBiometricPrompt.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 bircontent providerhatası 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), istemisetUserAuthenticationRequired(true)ile işaretlenmiş bir Android Keystore anahtarından oluşturulan birCipher’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.
// İlgili okumalar
Günlükten dahası
2026'da Android'de Health Connect: local-first'ten vazgeçmeden veri okuyup yazmak
Android'in Health Connect API'sine 2026 için pratik bir rehber — izinler, arka planda okuma ve local-first uygulamaların bunu neden gerçek değil, isteğe bağlı bir katman olarak görmesi gerektiği.
Android In-App Review API: sıkıcı olmadan puan istemek
Google'ın In-App Review API'sine pratik bir rehber — gerçekte nasıl çalıştığı, ne zaman tetiklenmesi gerektiği ve alışılmış 'bizi puanlayın' popup'ının Play Store puanınızı sessizce nasıl zedelediği.
Android uygulama kısayolları ve Hızlı Ayarlar Karosu: uygulamayı açmadan su kaydetmek
Android'in dinamik ShortcutManager API'sine ve TileService'e pratik bir rehber — tek dokunuşlu bir eylemin uygulamayı tamamen atlamasını nasıl sağlarsınız, çoğu uygulamayı çuvallatan tuzaklarla birlikte.