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
import android.content.Context
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
/**
* Менеджер для работы с биометрической аутентификацией
* Поддерживает отпечаток пальца, Face ID и другие биометрические методы
* Использует простую биометрию (без криптографии) для совместимости с Class 2 (Weak) биометрией
*/
class BiometricAuthManager(private val context: Context) {
companion object {
private const val KEY_NAME = "rosetta_biometric_key"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val ENCRYPTION_KEY = "RosettaBiometricKey2024"
private const val SALT = "RosettaSalt"
private const val IV_SEPARATOR = "]"
}
@@ -34,8 +30,8 @@ class BiometricAuthManager(private val context: Context) {
fun isBiometricAvailable(): BiometricAvailability {
val biometricManager = BiometricManager.from(context)
// Проверяем любую биометрию (включая слабую)
return when (biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.BIOMETRIC_WEAK
)) {
BiometricManager.BIOMETRIC_SUCCESS ->
@@ -64,51 +60,8 @@ class BiometricAuthManager(private val context: Context) {
}
/**
* Генерирует или получает ключ из Android Keystore
*/
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()
}
/**
* Шифрует пароль с помощью биометрического ключа
* Шифрует пароль с помощью простого AES (не привязанного к биометрии)
* Биометрия используется только для подтверждения личности
*/
fun encryptPassword(
activity: FragmentActivity,
@@ -117,43 +70,27 @@ class BiometricAuthManager(private val context: Context) {
onError: (String) -> Unit,
onCancel: () -> Unit
) {
// Сначала запрашиваем биометрию
showBiometricPrompt(
activity = activity,
title = "Сохранить пароль",
subtitle = "Подтвердите для сохранения пароля",
onSuccess = {
try {
val cipher = getCipher()
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
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("Ошибка шифрования")
}
// После успешной биометрии шифруем пароль
val encrypted = encryptString(password)
onSuccess(encrypted)
} catch (e: Exception) {
onError("Ошибка при шифровании: ${e.message}")
onError("Ошибка шифрования: ${e.message}")
}
},
onError = onError,
onCancel = onCancel
)
val promptInfo = createPromptInfo("Сохранить пароль")
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
} catch (e: Exception) {
onError("Ошибка инициализации: ${e.message}")
}
}
/**
* Расшифровывает пароль с помощью биометрического ключа
* Расшифровывает пароль после подтверждения биометрией
*/
fun decryptPassword(
activity: FragmentActivity,
@@ -162,63 +99,42 @@ class BiometricAuthManager(private val context: Context) {
onError: (String) -> Unit,
onCancel: () -> Unit
) {
showBiometricPrompt(
activity = activity,
title = "Разблокировать",
subtitle = "Подтвердите для входа",
onSuccess = {
try {
// Разделяем IV и зашифрованные данные
val parts = encryptedData.split(IV_SEPARATOR)
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("Ошибка расшифровки")
}
val decrypted = decryptString(encryptedData)
onSuccess(decrypted)
} catch (e: Exception) {
onError("Ошибка при расшифровке: ${e.message}")
onError("Ошибка расшифровки: ${e.message}")
}
},
onError = onError,
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,
onSuccess: (BiometricPrompt.CryptoObject) -> Unit,
title: String,
subtitle: String,
onSuccess: () -> Unit,
onError: (String) -> Unit,
onCancel: () -> Unit
): BiometricPrompt {
) {
val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricPrompt.ERROR_CANCELED) {
onCancel()
} else {
onError(errString.toString())
@@ -227,51 +143,77 @@ class BiometricAuthManager(private val context: Context) {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
result.cryptoObject?.let { onSuccess(it) }
onSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
// Не вызываем onError здесь, так как пользователь может попробовать снова
// Не вызываем onError, пользователь может попробовать снова
}
}
return BiometricPrompt(activity, executor, callback)
}
val biometricPrompt = BiometricPrompt(activity, executor, callback)
/**
* Создает информацию для диалога биометрической аутентификации
*/
private fun createPromptInfo(title: String): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle("Используйте биометрию для аутентификации")
.setNegativeButtonText("Использовать пароль")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.BIOMETRIC_WEAK
)
.setSubtitle(subtitle)
.setNegativeButtonText("Отмена")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK)
.build()
biometricPrompt.authenticate(promptInfo)
}
/**
* Получает экземпляр Cipher для шифрования/расшифровки
* Простое AES шифрование строки
*/
private fun getCipher(): Cipher {
return Cipher.getInstance(TRANSFORMATION)
private fun encryptString(data: String): String {
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() {
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
keyStore.load(null)
keyStore.deleteEntry(KEY_NAME)
} catch (e: Exception) {
// Игнорируем ошибки при удалении
}
// Ничего не нужно удалять, так как мы не используем Keystore
}
}