From 1f6fc01a548294089983fcdc36f762e6880f89ae Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Thu, 22 Jan 2026 14:54:40 +0500 Subject: [PATCH] refactor: Simplify BiometricAuthManager by removing Android Keystore integration and using AES for encryption/decryption --- .../biometric/BiometricAuthManager.kt | 262 +++++++----------- 1 file changed, 102 insertions(+), 160 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt b/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt index c2636fa..2dd3492 100644 --- a/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt +++ b/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt @@ -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 ) { - 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("Ошибка шифрования") - } - } catch (e: Exception) { - onError("Ошибка при шифровании: ${e.message}") - } - }, - onError = onError, - onCancel = onCancel - ) - - val promptInfo = createPromptInfo("Сохранить пароль") - biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) - - } catch (e: Exception) { - onError("Ошибка инициализации: ${e.message}") - } + // Сначала запрашиваем биометрию + showBiometricPrompt( + activity = activity, + title = "Сохранить пароль", + subtitle = "Подтвердите для сохранения пароля", + onSuccess = { + try { + // После успешной биометрии шифруем пароль + val encrypted = encryptString(password) + onSuccess(encrypted) + } catch (e: Exception) { + onError("Ошибка шифрования: ${e.message}") + } + }, + onError = onError, + onCancel = onCancel + ) } /** - * Расшифровывает пароль с помощью биометрического ключа + * Расшифровывает пароль после подтверждения биометрией */ fun decryptPassword( activity: FragmentActivity, @@ -162,63 +99,42 @@ class BiometricAuthManager(private val context: Context) { onError: (String) -> Unit, onCancel: () -> Unit ) { - 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("Ошибка расшифровки") - } - } catch (e: Exception) { - onError("Ошибка при расшифровке: ${e.message}") - } - }, - onError = onError, - onCancel = onCancel - ) - - val promptInfo = createPromptInfo("Разблокировать приложение") - biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) - - } catch (e: Exception) { - onError("Ошибка инициализации: ${e.message}") - } + showBiometricPrompt( + activity = activity, + title = "Разблокировать", + subtitle = "Подтвердите для входа", + onSuccess = { + try { + val decrypted = decryptString(encryptedData) + onSuccess(decrypted) + } catch (e: Exception) { + onError("Ошибка расшифровки: ${e.message}") + } + }, + onError = onError, + onCancel = onCancel + ) } /** - * Создает 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 } }