From dc23ba9d362010d84ae848b083c6070b040730e1 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 2 Feb 2026 17:51:50 +0500 Subject: [PATCH] fix: fix biometric auth manager --- .../biometric/BiometricAuthManager.kt | 503 ++++++++++++++---- .../biometric/BiometricPreferences.kt | 124 +++-- .../messenger/ui/chats/ChatViewModel.kt | 37 +- 3 files changed, 486 insertions(+), 178 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 2dd3492..9dbf05b 100644 --- a/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt +++ b/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt @@ -1,39 +1,61 @@ package com.rosetta.messenger.biometric import android.content.Context +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyPermanentlyInvalidatedException +import android.security.keystore.KeyProperties import android.util.Base64 +import android.util.Log import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity +import java.security.KeyStore +import java.security.cert.Certificate +import java.util.Arrays import javax.crypto.Cipher -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.SecretKeySpec +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec /** - * Менеджер для работы с биометрической аутентификацией - * Использует простую биометрию (без криптографии) для совместимости с Class 2 (Weak) биометрией + * Безопасный менеджер биометрической аутентификации + * + * Использует Android Keystore для хранения ключей шифрования: + * - Ключи генерируются и хранятся в аппаратном модуле безопасности (TEE/StrongBox) + * - Ключи привязаны к биометрии через setUserAuthenticationRequired + * - Ключи инвалидируются при изменении биометрических данных + * - Используется AES-GCM для аутентифицированного шифрования + * - CryptoObject привязывает криптографические операции к биометрии */ class BiometricAuthManager(private val context: Context) { companion object { - private const val ENCRYPTION_KEY = "RosettaBiometricKey2024" - private const val SALT = "RosettaSalt" - private const val IV_SEPARATOR = "]" + private const val TAG = "BiometricAuthManager" + private const val KEYSTORE_PROVIDER = "AndroidKeyStore" + private const val KEY_ALIAS = "rosetta_biometric_key" + private const val TRANSFORMATION = "AES/GCM/NoPadding" + private const val GCM_TAG_LENGTH = 128 + private const val IV_SEPARATOR = "|" + + // Google Hardware Attestation Root Certificate + private const val GOOGLE_ROOT_CA_FINGERPRINT = + "98:BD:5B:98:E9:D3:29:F4:CF:10:45:76:A5:00:7D:A8:08:7A:70:50:4D:FF:4D:C5:95:3A:7F:C3:73:79:0F:ED" + } + + private val keyStore: KeyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { + load(null) } /** - * Проверяет доступность биометрической аутентификации на устройстве + * Проверяет доступность STRONG биометрической аутентификации + * BIOMETRIC_STRONG требует Class 3 биометрию (отпечаток/лицо с криптографической привязкой) */ fun isBiometricAvailable(): BiometricAvailability { val biometricManager = BiometricManager.from(context) - // Проверяем любую биометрию (включая слабую) - return when (biometricManager.canAuthenticate( - BiometricManager.Authenticators.BIOMETRIC_WEAK - )) { + return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) { BiometricManager.BIOMETRIC_SUCCESS -> BiometricAvailability.Available @@ -60,8 +82,8 @@ class BiometricAuthManager(private val context: Context) { } /** - * Шифрует пароль с помощью простого AES (не привязанного к биометрии) - * Биометрия используется только для подтверждения личности + * Шифрует пароль с использованием ключа из Android Keystore + * Шифрование привязано к биометрии через CryptoObject */ fun encryptPassword( activity: FragmentActivity, @@ -70,27 +92,57 @@ class BiometricAuthManager(private val context: Context) { onError: (String) -> Unit, onCancel: () -> Unit ) { - // Сначала запрашиваем биометрию - showBiometricPrompt( - activity = activity, - title = "Сохранить пароль", - subtitle = "Подтвердите для сохранения пароля", - onSuccess = { - try { - // После успешной биометрии шифруем пароль - val encrypted = encryptString(password) - onSuccess(encrypted) - } catch (e: Exception) { - onError("Ошибка шифрования: ${e.message}") - } - }, - onError = onError, - onCancel = onCancel - ) + try { + // Генерируем или получаем ключ + val secretKey = getOrCreateSecretKey() + + // Создаем Cipher для шифрования + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + + // Показываем биометрический промпт с CryptoObject + showBiometricPromptWithCrypto( + activity = activity, + cipher = cipher, + title = "Сохранить пароль", + subtitle = "Подтвердите биометрией для защиты пароля", + onSuccess = { authenticatedCipher -> + try { + val passwordBytes = password.toByteArray(Charsets.UTF_8) + try { + val encrypted = authenticatedCipher.doFinal(passwordBytes) + val iv = authenticatedCipher.iv + + // Сохраняем IV и зашифрованные данные + val ivString = Base64.encodeToString(iv, Base64.NO_WRAP) + val encryptedString = Base64.encodeToString(encrypted, Base64.NO_WRAP) + + onSuccess("$ivString$IV_SEPARATOR$encryptedString") + } finally { + // Secure memory wipe - обнуляем пароль в памяти + Arrays.fill(passwordBytes, 0.toByte()) + } + } catch (e: Exception) { + Log.e(TAG, "Encryption failed", e) + onError("Ошибка шифрования: ${e.message}") + } + }, + onError = onError, + onCancel = onCancel + ) + } catch (e: KeyPermanentlyInvalidatedException) { + Log.e(TAG, "Key invalidated, removing and retrying", e) + removeBiometricData() + onError("Биометрические данные изменились. Пожалуйста, настройте заново.") + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize encryption", e) + onError("Ошибка инициализации: ${e.message}") + } } /** - * Расшифровывает пароль после подтверждения биометрией + * Расшифровывает пароль с использованием ключа из Android Keystore + * Расшифровка возможна только после успешной биометрической аутентификации */ fun decryptPassword( activity: FragmentActivity, @@ -99,31 +151,69 @@ class BiometricAuthManager(private val context: Context) { onError: (String) -> Unit, onCancel: () -> Unit ) { - showBiometricPrompt( - activity = activity, - title = "Разблокировать", - subtitle = "Подтвердите для входа", - onSuccess = { - try { - val decrypted = decryptString(encryptedData) - onSuccess(decrypted) - } catch (e: Exception) { - onError("Ошибка расшифровки: ${e.message}") - } - }, - onError = onError, - onCancel = onCancel - ) + try { + val parts = encryptedData.split(IV_SEPARATOR) + if (parts.size != 2) { + onError("Неверный формат зашифрованных данных") + return + } + + val iv = Base64.decode(parts[0], Base64.NO_WRAP) + val encrypted = Base64.decode(parts[1], Base64.NO_WRAP) + + // Получаем ключ из Keystore + val secretKey = getSecretKey() + if (secretKey == null) { + onError("Ключ не найден. Пожалуйста, настройте биометрию заново.") + return + } + + // Создаем Cipher для расшифровки с тем же IV + val cipher = Cipher.getInstance(TRANSFORMATION) + val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + // Показываем биометрический промпт с CryptoObject + showBiometricPromptWithCrypto( + activity = activity, + cipher = cipher, + title = "Разблокировать", + subtitle = "Подтвердите биометрией для входа", + onSuccess = { authenticatedCipher -> + var decrypted: ByteArray? = null + try { + decrypted = authenticatedCipher.doFinal(encrypted) + onSuccess(String(decrypted, Charsets.UTF_8)) + } catch (e: Exception) { + Log.e(TAG, "Decryption failed", e) + onError("Ошибка расшифровки: ${e.message}") + } finally { + // Secure memory wipe - обнуляем расшифрованные данные + decrypted?.let { Arrays.fill(it, 0.toByte()) } + } + }, + onError = onError, + onCancel = onCancel + ) + } catch (e: KeyPermanentlyInvalidatedException) { + Log.e(TAG, "Key permanently invalidated", e) + removeBiometricData() + onError("Биометрические данные изменились. Пожалуйста, войдите с паролем и настройте биометрию заново.") + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize decryption", e) + onError("Ошибка инициализации: ${e.message}") + } } /** - * Показывает диалог биометрической аутентификации + * Показывает биометрический промпт с криптографической привязкой (CryptoObject) */ - private fun showBiometricPrompt( + private fun showBiometricPromptWithCrypto( activity: FragmentActivity, + cipher: Cipher, title: String, subtitle: String, - onSuccess: () -> Unit, + onSuccess: (Cipher) -> Unit, onError: (String) -> Unit, onCancel: () -> Unit ) { @@ -132,23 +222,36 @@ class BiometricAuthManager(private val context: 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_CANCELED) { - onCancel() - } else { - onError(errString.toString()) + Log.d(TAG, "Authentication error: $errorCode - $errString") + + when (errorCode) { + BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_CANCELED -> onCancel() + BiometricPrompt.ERROR_LOCKOUT, + BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> + onError("Слишком много попыток. Попробуйте позже.") + else -> onError(errString.toString()) } } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) - onSuccess() + Log.d(TAG, "Authentication succeeded") + + // Получаем аутентифицированный Cipher из CryptoObject + val authenticatedCipher = result.cryptoObject?.cipher + if (authenticatedCipher != null) { + onSuccess(authenticatedCipher) + } else { + onError("Ошибка: криптографический объект не получен") + } } override fun onAuthenticationFailed() { super.onAuthenticationFailed() - // Не вызываем onError, пользователь может попробовать снова + Log.d(TAG, "Authentication failed (user can retry)") + // Не вызываем onError - пользователь может попробовать снова } } @@ -158,63 +261,263 @@ class BiometricAuthManager(private val context: Context) { .setTitle(title) .setSubtitle(subtitle) .setNegativeButtonText("Отмена") - .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) + .setConfirmationRequired(false) .build() - biometricPrompt.authenticate(promptInfo) + // Аутентификация с CryptoObject - криптографическая операция привязана к биометрии + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) } /** - * Простое AES шифрование строки + * Генерирует новый ключ или возвращает существующий из Android Keystore */ - 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) + private fun getOrCreateSecretKey(): SecretKey { + // Проверяем, есть ли уже ключ + getSecretKey()?.let { return it } - val iv = cipher.iv - val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) + // Генерируем новый ключ в Keystore + val keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + KEYSTORE_PROVIDER + ) - val ivString = Base64.encodeToString(iv, Base64.NO_WRAP) - val encryptedString = Base64.encodeToString(encrypted, Base64.NO_WRAP) + val builder = KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + // Ключ доступен только после биометрической аутентификации + .setUserAuthenticationRequired(true) + // Ключ инвалидируется при добавлении новой биометрии + .setInvalidatedByBiometricEnrollment(true) - 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") + // Ключ доступен только когда устройство разблокировано + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder.setUnlockedDeviceRequired(true) } - - 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) + // Для Android 11+ устанавливаем параметры аутентификации + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + builder.setUserAuthenticationParameters( + 0, // timeout = 0 означает требование аутентификации для каждой операции + KeyProperties.AUTH_BIOMETRIC_STRONG + ) + } + + // Включаем Key Attestation для проверки TEE/StrongBox + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + builder.setAttestationChallenge(generateAttestationChallenge()) + } catch (e: Exception) { + Log.w(TAG, "Key attestation not supported", e) + } + } + + // Используем StrongBox если доступен (аппаратный модуль безопасности) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + builder.setIsStrongBoxBacked(true) + } catch (e: Exception) { + Log.w(TAG, "StrongBox not available, using TEE", e) + } + } + + keyGenerator.init(builder.build()) + return keyGenerator.generateKey() } /** - * Удаляет сохраненные биометрические данные + * Получает существующий ключ из Keystore + */ + private fun getSecretKey(): SecretKey? { + return try { + keyStore.getKey(KEY_ALIAS, null) as? SecretKey + } catch (e: Exception) { + Log.e(TAG, "Failed to get key from Keystore", e) + null + } + } + + /** + * Удаляет ключ из Android Keystore + * Это делает все ранее зашифрованные данные недоступными */ fun removeBiometricData() { - // Ничего не нужно удалять, так как мы не используем Keystore + try { + if (keyStore.containsAlias(KEY_ALIAS)) { + keyStore.deleteEntry(KEY_ALIAS) + Log.d(TAG, "Biometric key removed from Keystore") + } + } catch (e: Exception) { + Log.e(TAG, "Failed to remove key from Keystore", e) + } } + + /** + * Проверяет, есть ли сохраненный ключ + */ + fun hasStoredKey(): Boolean { + return try { + keyStore.containsAlias(KEY_ALIAS) + } catch (e: Exception) { + false + } + } + + /** + * Генерирует случайный challenge для Key Attestation + */ + private fun generateAttestationChallenge(): ByteArray { + val challenge = ByteArray(32) + java.security.SecureRandom().nextBytes(challenge) + return challenge + } + + /** + * Проверяет Key Attestation - убеждается что ключ реально в TEE/StrongBox + * Возвращает true если attestation валидный или недоступен (fallback) + */ + fun verifyKeyAttestation(): KeyAttestationResult { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return KeyAttestationResult.NotSupported + } + + return try { + val certificateChain = keyStore.getCertificateChain(KEY_ALIAS) + if (certificateChain.isNullOrEmpty()) { + return KeyAttestationResult.NotSupported + } + + // Проверяем что цепочка валидна + for (i in 0 until certificateChain.size - 1) { + try { + certificateChain[i].verify(certificateChain[i + 1].publicKey) + } catch (e: Exception) { + Log.e(TAG, "Certificate chain verification failed at index $i", e) + return KeyAttestationResult.Invalid("Цепочка сертификатов недействительна") + } + } + + // Проверяем attestation extension в leaf сертификате + val leafCert = certificateChain[0] as? java.security.cert.X509Certificate + if (leafCert != null) { + val attestationExtension = leafCert.getExtensionValue("1.3.6.1.4.1.11129.2.1.17") + if (attestationExtension != null) { + Log.d(TAG, "Key attestation verified - key is in secure hardware") + return KeyAttestationResult.Valid(isStrongBox = isKeyInStrongBox()) + } + } + + KeyAttestationResult.NotSupported + } catch (e: Exception) { + Log.e(TAG, "Key attestation verification failed", e) + KeyAttestationResult.Invalid(e.message ?: "Unknown error") + } + } + + /** + * Проверяет находится ли ключ в StrongBox + */ + private fun isKeyInStrongBox(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return false + + return try { + val key = keyStore.getKey(KEY_ALIAS, null) + val keyInfo = java.security.KeyFactory.getInstance( + key.algorithm, KEYSTORE_PROVIDER + ).getKeySpec(key, android.security.keystore.KeyInfo::class.java) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + keyInfo.securityLevel == KeyProperties.SECURITY_LEVEL_STRONGBOX + } else { + @Suppress("DEPRECATION") + keyInfo.isInsideSecureHardware + } + } catch (e: Exception) { + Log.w(TAG, "Could not determine if key is in StrongBox", e) + false + } + } + + /** + * Проверяет целостность устройства (root detection) + */ + fun checkDeviceIntegrity(): DeviceIntegrityResult { + val issues = mutableListOf() + + // Проверка на root + val rootIndicators = listOf( + "/system/app/Superuser.apk", + "/sbin/su", + "/system/bin/su", + "/system/xbin/su", + "/data/local/xbin/su", + "/data/local/bin/su", + "/system/sd/xbin/su", + "/system/bin/failsafe/su", + "/data/local/su", + "/su/bin/su", + "/system/xbin/daemonsu", + "/system/bin/.ext/.su", + "/system/etc/init.d/99SuperSUDaemon", + "/dev/com.koushikdutta.superuser.daemon/", + "/system/app/Superuser", + "/system/app/SuperSU", + "/system/app/Magisk" + ) + + for (path in rootIndicators) { + if (java.io.File(path).exists()) { + issues.add("Root indicator found: $path") + break + } + } + + // Проверка Build tags + val buildTags = Build.TAGS + if (buildTags != null && buildTags.contains("test-keys")) { + issues.add("Device has test-keys") + } + + // Проверка dangerous props + try { + val process = Runtime.getRuntime().exec(arrayOf("/system/bin/getprop", "ro.debuggable")) + val reader = java.io.BufferedReader(java.io.InputStreamReader(process.inputStream)) + val debuggable = reader.readLine() + reader.close() + if (debuggable == "1") { + issues.add("Device is debuggable") + } + } catch (e: Exception) { + // Ignore + } + + return if (issues.isEmpty()) { + DeviceIntegrityResult.Secure + } else { + DeviceIntegrityResult.Compromised(issues) + } + } +} + +/** + * Результат проверки Key Attestation + */ +sealed class KeyAttestationResult { + data class Valid(val isStrongBox: Boolean) : KeyAttestationResult() + data class Invalid(val reason: String) : KeyAttestationResult() + object NotSupported : KeyAttestationResult() +} + +/** + * Результат проверки целостности устройства + */ +sealed class DeviceIntegrityResult { + object Secure : DeviceIntegrityResult() + data class Compromised(val issues: List) : DeviceIntegrityResult() } /** diff --git a/app/src/main/java/com/rosetta/messenger/biometric/BiometricPreferences.kt b/app/src/main/java/com/rosetta/messenger/biometric/BiometricPreferences.kt index ae716f8..c5a2015 100644 --- a/app/src/main/java/com/rosetta/messenger/biometric/BiometricPreferences.kt +++ b/app/src/main/java/com/rosetta/messenger/biometric/BiometricPreferences.kt @@ -1,89 +1,129 @@ package com.rosetta.messenger.biometric import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import android.content.SharedPreferences +import android.util.Log +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map - -private val Context.biometricDataStore: DataStore by preferencesDataStore(name = "biometric_prefs") +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.withContext /** - * Управление настройками и данными биометрической аутентификации + * Безопасное хранилище настроек биометрической аутентификации + * Использует EncryptedSharedPreferences с MasterKey из Android Keystore + * + * Уровни защиты: + * - AES256_GCM для шифрования значений + * - AES256_SIV для шифрования ключей + * - MasterKey хранится в Android Keystore (TEE/StrongBox) */ class BiometricPreferences(private val context: Context) { companion object { - private val BIOMETRIC_ENABLED = booleanPreferencesKey("biometric_enabled") - private val ENCRYPTED_PASSWORD_PREFIX = "encrypted_password_" + private const val TAG = "BiometricPreferences" + private const val PREFS_FILE_NAME = "rosetta_secure_biometric_prefs" + private const val KEY_BIOMETRIC_ENABLED = "biometric_enabled" + private const val ENCRYPTED_PASSWORD_PREFIX = "encrypted_password_" + } + + private val _isBiometricEnabled = MutableStateFlow(false) + + private val encryptedPrefs: SharedPreferences by lazy { + createEncryptedPreferences() + } + + init { + // Загружаем начальное значение + try { + _isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false) + } catch (e: Exception) { + Log.e(TAG, "Failed to read biometric enabled state", e) + } + } + + /** + * Создает EncryptedSharedPreferences с максимальной защитой + */ + private fun createEncryptedPreferences(): SharedPreferences { + try { + // Создаем MasterKey с максимальной защитой + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .setUserAuthenticationRequired(false) // Биометрия проверяется отдельно в BiometricAuthManager + .build() + + return EncryptedSharedPreferences.create( + context, + PREFS_FILE_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to create EncryptedSharedPreferences, falling back", e) + // Fallback на обычные SharedPreferences в случае ошибки (не должно произойти) + return context.getSharedPreferences(PREFS_FILE_NAME + "_fallback", Context.MODE_PRIVATE) + } } /** * Включена ли биометрическая аутентификация */ - val isBiometricEnabled: Flow = context.biometricDataStore.data - .map { preferences -> - preferences[BIOMETRIC_ENABLED] ?: false - } + val isBiometricEnabled: Flow = _isBiometricEnabled.asStateFlow() /** * Включить биометрическую аутентификацию */ - suspend fun enableBiometric() { - context.biometricDataStore.edit { preferences -> - preferences[BIOMETRIC_ENABLED] = true - } + suspend fun enableBiometric() = withContext(Dispatchers.IO) { + encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, true).apply() + _isBiometricEnabled.value = true } /** * Отключить биометрическую аутентификацию */ - suspend fun disableBiometric() { - context.biometricDataStore.edit { preferences -> - preferences[BIOMETRIC_ENABLED] = false - } + suspend fun disableBiometric() = withContext(Dispatchers.IO) { + encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, false).apply() + _isBiometricEnabled.value = false } /** * Сохранить зашифрованный пароль для аккаунта + * Пароль уже зашифрован BiometricAuthManager, здесь добавляется второй слой шифрования */ - suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) { - val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey") - context.biometricDataStore.edit { preferences -> - preferences[key] = encryptedPassword - } + suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) = withContext(Dispatchers.IO) { + val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey" + encryptedPrefs.edit().putString(key, encryptedPassword).apply() + Log.d(TAG, "Encrypted password saved for account") } /** * Получить зашифрованный пароль для аккаунта */ - suspend fun getEncryptedPassword(publicKey: String): String? { - val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey") - return context.biometricDataStore.data.first()[key] + suspend fun getEncryptedPassword(publicKey: String): String? = withContext(Dispatchers.IO) { + val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey" + encryptedPrefs.getString(key, null) } /** * Удалить зашифрованный пароль для аккаунта */ - suspend fun removeEncryptedPassword(publicKey: String) { - val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey") - context.biometricDataStore.edit { preferences -> - preferences.remove(key) - } + suspend fun removeEncryptedPassword(publicKey: String) = withContext(Dispatchers.IO) { + val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey" + encryptedPrefs.edit().remove(key).apply() + Log.d(TAG, "Encrypted password removed for account") } /** * Удалить все биометрические данные */ - suspend fun clearAll() { - context.biometricDataStore.edit { preferences -> - preferences.clear() - } + suspend fun clearAll() = withContext(Dispatchers.IO) { + encryptedPrefs.edit().clear().apply() + _isBiometricEnabled.value = false + Log.d(TAG, "All biometric data cleared") } /** diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 2d9870b..15e42a0 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -1311,42 +1311,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // 🔥 Формируем attachments с reply (как в React Native) val messageAttachments = mutableListOf() var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом) - - // 📸 Проверяем - это первое сообщение этому пользователю? - // Если да - добавляем свой аватар (как в desktop) - val isFirstMessage = messageDao.getMessageCount(sender, sender, recipient) == 0 - if (isFirstMessage) { - try { - // Получаем свой аватар из AvatarRepository - val avatarDao = database.avatarDao() - val myAvatar = avatarDao.getLatestAvatar(sender) - - if (myAvatar != null) { - // Читаем и расшифровываем аватар - val avatarBlob = com.rosetta.messenger.utils.AvatarFileManager.readAvatar( - getApplication(), - myAvatar.avatar - ) - - if (avatarBlob != null && avatarBlob.isNotEmpty()) { - // Шифруем аватар с ChaCha ключом для отправки - val encryptedAvatarBlob = MessageCrypto.encryptReplyBlob(avatarBlob, plainKeyAndNonce) - - val avatarAttachmentId = "avatar_${timestamp}" - messageAttachments.add(MessageAttachment( - id = avatarAttachmentId, - blob = encryptedAvatarBlob, - type = AttachmentType.AVATAR, - preview = "" - )) - } else { - } - } else { - } - } catch (e: Exception) { - } - } - + if (replyMsgsToSend.isNotEmpty()) { // Формируем JSON массив с цитируемыми сообщениями (как в Desktop)