refactor: Simplify BiometricAuthManager by removing Android Keystore integration and using AES for encryption/decryption

This commit is contained in:
k1ngsterr1
2026-01-22 14:54:40 +05:00
parent f6f8620102
commit 1f6fc01a54

View File

@@ -1,30 +1,26 @@
package com.rosetta.messenger.biometric package com.rosetta.messenger.biometric
import android.content.Context import android.content.Context
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import java.security.KeyStore
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.KeyGenerator import javax.crypto.SecretKeyFactory
import javax.crypto.SecretKey import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
/** /**
* Менеджер для работы с биометрической аутентификацией * Менеджер для работы с биометрической аутентификацией
* Поддерживает отпечаток пальца, Face ID и другие биометрические методы * Использует простую биометрию (без криптографии) для совместимости с Class 2 (Weak) биометрией
*/ */
class BiometricAuthManager(private val context: Context) { class BiometricAuthManager(private val context: Context) {
companion object { companion object {
private const val KEY_NAME = "rosetta_biometric_key" private const val ENCRYPTION_KEY = "RosettaBiometricKey2024"
private const val ANDROID_KEYSTORE = "AndroidKeyStore" private const val SALT = "RosettaSalt"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val IV_SEPARATOR = "]" private const val IV_SEPARATOR = "]"
} }
@@ -34,8 +30,8 @@ class BiometricAuthManager(private val context: Context) {
fun isBiometricAvailable(): BiometricAvailability { fun isBiometricAvailable(): BiometricAvailability {
val biometricManager = BiometricManager.from(context) val biometricManager = BiometricManager.from(context)
// Проверяем любую биометрию (включая слабую)
return when (biometricManager.canAuthenticate( return when (biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.BIOMETRIC_WEAK BiometricManager.Authenticators.BIOMETRIC_WEAK
)) { )) {
BiometricManager.BIOMETRIC_SUCCESS -> BiometricManager.BIOMETRIC_SUCCESS ->
@@ -64,51 +60,8 @@ class BiometricAuthManager(private val context: Context) {
} }
/** /**
* Генерирует или получает ключ из Android Keystore * Шифрует пароль с помощью простого AES (не привязанного к биометрии)
*/ * Биометрия используется только для подтверждения личности
private fun getSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
keyStore.load(null)
// Проверяем существование ключа
if (!keyStore.containsAlias(KEY_NAME)) {
generateSecretKey()
}
return keyStore.getKey(KEY_NAME, null) as SecretKey
}
/**
* Генерирует новый ключ в Android Keystore
*/
private fun generateSecretKey() {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE
)
val purposes = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
val builder = KeyGenParameterSpec.Builder(KEY_NAME, purposes)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
// Для Android 11+ можем указать тайм-аут аутентификации
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder.setUserAuthenticationParameters(
0, // 0 означает, что требуется аутентификация для каждого использования
KeyProperties.AUTH_BIOMETRIC_STRONG
)
}
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}
/**
* Шифрует пароль с помощью биометрического ключа
*/ */
fun encryptPassword( fun encryptPassword(
activity: FragmentActivity, activity: FragmentActivity,
@@ -117,43 +70,27 @@ class BiometricAuthManager(private val context: Context) {
onError: (String) -> Unit, onError: (String) -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
// Сначала запрашиваем биометрию
showBiometricPrompt(
activity = activity,
title = "Сохранить пароль",
subtitle = "Подтвердите для сохранения пароля",
onSuccess = {
try { try {
val cipher = getCipher() // После успешной биометрии шифруем пароль
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) val encrypted = encryptString(password)
onSuccess(encrypted)
val biometricPrompt = createBiometricPrompt(
activity,
onSuccess = { cryptoObject ->
try {
val encryptedPassword = cryptoObject.cipher?.doFinal(password.toByteArray())
val iv = cryptoObject.cipher?.iv
if (encryptedPassword != null && iv != null) {
// Сохраняем IV вместе с зашифрованными данными
val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
val encryptedString = Base64.encodeToString(encryptedPassword, Base64.NO_WRAP)
onSuccess("$ivString$IV_SEPARATOR$encryptedString")
} else {
onError("Ошибка шифрования")
}
} catch (e: Exception) { } catch (e: Exception) {
onError("Ошибка при шифровании: ${e.message}") onError("Ошибка шифрования: ${e.message}")
} }
}, },
onError = onError, onError = onError,
onCancel = onCancel onCancel = onCancel
) )
val promptInfo = createPromptInfo("Сохранить пароль")
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
} catch (e: Exception) {
onError("Ошибка инициализации: ${e.message}")
}
} }
/** /**
* Расшифровывает пароль с помощью биометрического ключа * Расшифровывает пароль после подтверждения биометрией
*/ */
fun decryptPassword( fun decryptPassword(
activity: FragmentActivity, activity: FragmentActivity,
@@ -162,63 +99,42 @@ class BiometricAuthManager(private val context: Context) {
onError: (String) -> Unit, onError: (String) -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
showBiometricPrompt(
activity = activity,
title = "Разблокировать",
subtitle = "Подтвердите для входа",
onSuccess = {
try { try {
// Разделяем IV и зашифрованные данные val decrypted = decryptString(encryptedData)
val parts = encryptedData.split(IV_SEPARATOR) onSuccess(decrypted)
if (parts.size != 2) {
onError("Неверный формат данных")
return
}
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
val encryptedPassword = Base64.decode(parts[1], Base64.NO_WRAP)
val cipher = getCipher()
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
val biometricPrompt = createBiometricPrompt(
activity,
onSuccess = { cryptoObject ->
try {
val decryptedPassword = cryptoObject.cipher?.doFinal(encryptedPassword)
if (decryptedPassword != null) {
onSuccess(String(decryptedPassword))
} else {
onError("Ошибка расшифровки")
}
} catch (e: Exception) { } catch (e: Exception) {
onError("Ошибка при расшифровке: ${e.message}") onError("Ошибка расшифровки: ${e.message}")
} }
}, },
onError = onError, onError = onError,
onCancel = onCancel onCancel = onCancel
) )
val promptInfo = createPromptInfo("Разблокировать приложение")
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
} catch (e: Exception) {
onError("Ошибка инициализации: ${e.message}")
}
} }
/** /**
* Создает BiometricPrompt с обработчиками * Показывает диалог биометрической аутентификации
*/ */
private fun createBiometricPrompt( private fun showBiometricPrompt(
activity: FragmentActivity, activity: FragmentActivity,
onSuccess: (BiometricPrompt.CryptoObject) -> Unit, title: String,
subtitle: String,
onSuccess: () -> Unit,
onError: (String) -> Unit, onError: (String) -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
): BiometricPrompt { ) {
val executor = ContextCompat.getMainExecutor(context) val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() { val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED || if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) { errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricPrompt.ERROR_CANCELED) {
onCancel() onCancel()
} else { } else {
onError(errString.toString()) onError(errString.toString())
@@ -227,51 +143,77 @@ class BiometricAuthManager(private val context: Context) {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result) super.onAuthenticationSucceeded(result)
result.cryptoObject?.let { onSuccess(it) } onSuccess()
} }
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
// Не вызываем onError здесь, так как пользователь может попробовать снова // Не вызываем onError, пользователь может попробовать снова
} }
} }
return BiometricPrompt(activity, executor, callback) val biometricPrompt = BiometricPrompt(activity, executor, callback)
}
/** val promptInfo = BiometricPrompt.PromptInfo.Builder()
* Создает информацию для диалога биометрической аутентификации
*/
private fun createPromptInfo(title: String): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
.setTitle(title) .setTitle(title)
.setSubtitle("Используйте биометрию для аутентификации") .setSubtitle(subtitle)
.setNegativeButtonText("Использовать пароль") .setNegativeButtonText("Отмена")
.setAllowedAuthenticators( .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK)
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.BIOMETRIC_WEAK
)
.build() .build()
biometricPrompt.authenticate(promptInfo)
} }
/** /**
* Получает экземпляр Cipher для шифрования/расшифровки * Простое AES шифрование строки
*/ */
private fun getCipher(): Cipher { private fun encryptString(data: String): String {
return Cipher.getInstance(TRANSFORMATION) val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256)
val tmp = factory.generateSecret(spec)
val secretKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
val encryptedString = Base64.encodeToString(encrypted, Base64.NO_WRAP)
return "$ivString$IV_SEPARATOR$encryptedString"
}
/**
* Расшифровка строки
*/
private fun decryptString(encryptedData: String): String {
val parts = encryptedData.split(IV_SEPARATOR)
if (parts.size != 2) {
throw IllegalArgumentException("Invalid encrypted data format")
}
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
val encrypted = Base64.decode(parts[1], Base64.NO_WRAP)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256)
val tmp = factory.generateSecret(spec)
val secretKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
val decrypted = cipher.doFinal(encrypted)
return String(decrypted, Charsets.UTF_8)
} }
/** /**
* Удаляет сохраненные биометрические данные * Удаляет сохраненные биометрические данные
*/ */
fun removeBiometricData() { fun removeBiometricData() {
try { // Ничего не нужно удалять, так как мы не используем Keystore
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
keyStore.load(null)
keyStore.deleteEntry(KEY_NAME)
} catch (e: Exception) {
// Игнорируем ошибки при удалении
}
} }
} }