fix: fix biometric auth manager

This commit is contained in:
k1ngsterr1
2026-02-02 17:51:50 +05:00
parent 6cb4a80666
commit dc23ba9d36
3 changed files with 486 additions and 178 deletions

View File

@@ -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)
// Генерируем или получаем ключ
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)
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")
private fun getOrCreateSecretKey(): SecretKey {
// Проверяем, есть ли уже ключ
getSecretKey()?.let { return it }
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
// Генерируем новый ключ в Keystore
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
KEYSTORE_PROVIDER
)
val iv = cipher.iv
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
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)
val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
val encryptedString = Base64.encodeToString(encrypted, Base64.NO_WRAP)
// Ключ доступен только когда устройство разблокировано
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUnlockedDeviceRequired(true)
}
return "$ivString$IV_SEPARATOR$encryptedString"
// Для 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 decryptString(encryptedData: String): String {
val parts = encryptedData.split(IV_SEPARATOR)
if (parts.size != 2) {
throw IllegalArgumentException("Invalid encrypted data format")
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
}
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 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<String>()
// Проверка на 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<String>) : DeviceIntegrityResult()
}
/**

View File

@@ -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<Preferences> 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<Boolean> = context.biometricDataStore.data
.map { preferences ->
preferences[BIOMETRIC_ENABLED] ?: false
}
val isBiometricEnabled: Flow<Boolean> = _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")
}
/**

View File

@@ -1312,41 +1312,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val messageAttachments = mutableListOf<MessageAttachment>()
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)