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 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.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import android.util.Log
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 java.security.cert.Certificate
import java.util.Arrays
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec import javax.crypto.SecretKey
import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
/** /**
* Менеджер для работы с биометрической аутентификацией * Безопасный менеджер биометрической аутентификации
* Использует простую биометрию (без криптографии) для совместимости с Class 2 (Weak) биометрией *
* Использует Android Keystore для хранения ключей шифрования:
* - Ключи генерируются и хранятся в аппаратном модуле безопасности (TEE/StrongBox)
* - Ключи привязаны к биометрии через setUserAuthenticationRequired
* - Ключи инвалидируются при изменении биометрических данных
* - Используется AES-GCM для аутентифицированного шифрования
* - CryptoObject привязывает криптографические операции к биометрии
*/ */
class BiometricAuthManager(private val context: Context) { class BiometricAuthManager(private val context: Context) {
companion object { companion object {
private const val ENCRYPTION_KEY = "RosettaBiometricKey2024" private const val TAG = "BiometricAuthManager"
private const val SALT = "RosettaSalt" private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
private const val IV_SEPARATOR = "]" 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 { fun isBiometricAvailable(): BiometricAvailability {
val biometricManager = BiometricManager.from(context) val biometricManager = BiometricManager.from(context)
// Проверяем любую биометрию (включая слабую) return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
return when (biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK
)) {
BiometricManager.BIOMETRIC_SUCCESS -> BiometricManager.BIOMETRIC_SUCCESS ->
BiometricAvailability.Available BiometricAvailability.Available
@@ -60,8 +82,8 @@ class BiometricAuthManager(private val context: Context) {
} }
/** /**
* Шифрует пароль с помощью простого AES (не привязанного к биометрии) * Шифрует пароль с использованием ключа из Android Keystore
* Биометрия используется только для подтверждения личности * Шифрование привязано к биометрии через CryptoObject
*/ */
fun encryptPassword( fun encryptPassword(
activity: FragmentActivity, activity: FragmentActivity,
@@ -70,27 +92,57 @@ class BiometricAuthManager(private val context: Context) {
onError: (String) -> Unit, onError: (String) -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
// Сначала запрашиваем биометрию try {
showBiometricPrompt( // Генерируем или получаем ключ
activity = activity, val secretKey = getOrCreateSecretKey()
title = "Сохранить пароль",
subtitle = "Подтвердите для сохранения пароля", // Создаем Cipher для шифрования
onSuccess = { val cipher = Cipher.getInstance(TRANSFORMATION)
try { cipher.init(Cipher.ENCRYPT_MODE, secretKey)
// После успешной биометрии шифруем пароль
val encrypted = encryptString(password) // Показываем биометрический промпт с CryptoObject
onSuccess(encrypted) showBiometricPromptWithCrypto(
} catch (e: Exception) { activity = activity,
onError("Ошибка шифрования: ${e.message}") cipher = cipher,
} title = "Сохранить пароль",
}, subtitle = "Подтвердите биометрией для защиты пароля",
onError = onError, onSuccess = { authenticatedCipher ->
onCancel = onCancel 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( fun decryptPassword(
activity: FragmentActivity, activity: FragmentActivity,
@@ -99,31 +151,69 @@ class BiometricAuthManager(private val context: Context) {
onError: (String) -> Unit, onError: (String) -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
showBiometricPrompt( try {
activity = activity, val parts = encryptedData.split(IV_SEPARATOR)
title = "Разблокировать", if (parts.size != 2) {
subtitle = "Подтвердите для входа", onError("Неверный формат зашифрованных данных")
onSuccess = { return
try { }
val decrypted = decryptString(encryptedData)
onSuccess(decrypted) val iv = Base64.decode(parts[0], Base64.NO_WRAP)
} catch (e: Exception) { val encrypted = Base64.decode(parts[1], Base64.NO_WRAP)
onError("Ошибка расшифровки: ${e.message}")
} // Получаем ключ из Keystore
}, val secretKey = getSecretKey()
onError = onError, if (secretKey == null) {
onCancel = onCancel 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, activity: FragmentActivity,
cipher: Cipher,
title: String, title: String,
subtitle: String, subtitle: String,
onSuccess: () -> Unit, onSuccess: (Cipher) -> Unit,
onError: (String) -> Unit, onError: (String) -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
@@ -132,23 +222,36 @@ class BiometricAuthManager(private val context: 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 || Log.d(TAG, "Authentication error: $errorCode - $errString")
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricPrompt.ERROR_CANCELED) { when (errorCode) {
onCancel() BiometricPrompt.ERROR_USER_CANCELED,
} else { BiometricPrompt.ERROR_NEGATIVE_BUTTON,
onError(errString.toString()) BiometricPrompt.ERROR_CANCELED -> onCancel()
BiometricPrompt.ERROR_LOCKOUT,
BiometricPrompt.ERROR_LOCKOUT_PERMANENT ->
onError("Слишком много попыток. Попробуйте позже.")
else -> onError(errString.toString())
} }
} }
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result) 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() { override fun onAuthenticationFailed() {
super.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) .setTitle(title)
.setSubtitle(subtitle) .setSubtitle(subtitle)
.setNegativeButtonText("Отмена") .setNegativeButtonText("Отмена")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setConfirmationRequired(false)
.build() .build()
biometricPrompt.authenticate(promptInfo) // Аутентификация с CryptoObject - криптографическая операция привязана к биометрии
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
} }
/** /**
* Простое AES шифрование строки * Генерирует новый ключ или возвращает существующий из Android Keystore
*/ */
private fun encryptString(data: String): String { private fun getOrCreateSecretKey(): SecretKey {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") // Проверяем, есть ли уже ключ
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256) getSecretKey()?.let { return it }
val tmp = factory.generateSecret(spec)
val secretKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") // Генерируем новый ключ в Keystore
cipher.init(Cipher.ENCRYPT_MODE, secretKey) val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
KEYSTORE_PROVIDER
)
val iv = cipher.iv val builder = KeyGenParameterSpec.Builder(
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) 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"
}
/**
* Расшифровка строки
*/
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) // Для Android 11+ устанавливаем параметры аутентификации
val encrypted = Base64.decode(parts[1], Base64.NO_WRAP) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder.setUserAuthenticationParameters(
0, // timeout = 0 означает требование аутентификации для каждой операции
KeyProperties.AUTH_BIOMETRIC_STRONG
)
}
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") // Включаем Key Attestation для проверки TEE/StrongBox
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val tmp = factory.generateSecret(spec) try {
val secretKey = SecretKeySpec(tmp.encoded, "AES") builder.setAttestationChallenge(generateAttestationChallenge())
} catch (e: Exception) {
Log.w(TAG, "Key attestation not supported", e)
}
}
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") // Используем StrongBox если доступен (аппаратный модуль безопасности)
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv)) 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)
}
}
val decrypted = cipher.doFinal(encrypted) keyGenerator.init(builder.build())
return String(decrypted, Charsets.UTF_8) 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() { 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 package com.rosetta.messenger.biometric
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import android.content.SharedPreferences
import androidx.datastore.preferences.core.Preferences import android.util.Log
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.security.crypto.EncryptedSharedPreferences
import androidx.datastore.preferences.core.edit import androidx.security.crypto.MasterKey
import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.Dispatchers
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
private val Context.biometricDataStore: DataStore<Preferences> by preferencesDataStore(name = "biometric_prefs")
/** /**
* Управление настройками и данными биометрической аутентификации * Безопасное хранилище настроек биометрической аутентификации
* Использует EncryptedSharedPreferences с MasterKey из Android Keystore
*
* Уровни защиты:
* - AES256_GCM для шифрования значений
* - AES256_SIV для шифрования ключей
* - MasterKey хранится в Android Keystore (TEE/StrongBox)
*/ */
class BiometricPreferences(private val context: Context) { class BiometricPreferences(private val context: Context) {
companion object { companion object {
private val BIOMETRIC_ENABLED = booleanPreferencesKey("biometric_enabled") private const val TAG = "BiometricPreferences"
private val ENCRYPTED_PASSWORD_PREFIX = "encrypted_password_" 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 val isBiometricEnabled: Flow<Boolean> = _isBiometricEnabled.asStateFlow()
.map { preferences ->
preferences[BIOMETRIC_ENABLED] ?: false
}
/** /**
* Включить биометрическую аутентификацию * Включить биометрическую аутентификацию
*/ */
suspend fun enableBiometric() { suspend fun enableBiometric() = withContext(Dispatchers.IO) {
context.biometricDataStore.edit { preferences -> encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, true).apply()
preferences[BIOMETRIC_ENABLED] = true _isBiometricEnabled.value = true
}
} }
/** /**
* Отключить биометрическую аутентификацию * Отключить биометрическую аутентификацию
*/ */
suspend fun disableBiometric() { suspend fun disableBiometric() = withContext(Dispatchers.IO) {
context.biometricDataStore.edit { preferences -> encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, false).apply()
preferences[BIOMETRIC_ENABLED] = false _isBiometricEnabled.value = false
}
} }
/** /**
* Сохранить зашифрованный пароль для аккаунта * Сохранить зашифрованный пароль для аккаунта
* Пароль уже зашифрован BiometricAuthManager, здесь добавляется второй слой шифрования
*/ */
suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) { suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) = withContext(Dispatchers.IO) {
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey") val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
context.biometricDataStore.edit { preferences -> encryptedPrefs.edit().putString(key, encryptedPassword).apply()
preferences[key] = encryptedPassword Log.d(TAG, "Encrypted password saved for account")
}
} }
/** /**
* Получить зашифрованный пароль для аккаунта * Получить зашифрованный пароль для аккаунта
*/ */
suspend fun getEncryptedPassword(publicKey: String): String? { suspend fun getEncryptedPassword(publicKey: String): String? = withContext(Dispatchers.IO) {
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey") val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
return context.biometricDataStore.data.first()[key] encryptedPrefs.getString(key, null)
} }
/** /**
* Удалить зашифрованный пароль для аккаунта * Удалить зашифрованный пароль для аккаунта
*/ */
suspend fun removeEncryptedPassword(publicKey: String) { suspend fun removeEncryptedPassword(publicKey: String) = withContext(Dispatchers.IO) {
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey") val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
context.biometricDataStore.edit { preferences -> encryptedPrefs.edit().remove(key).apply()
preferences.remove(key) Log.d(TAG, "Encrypted password removed for account")
}
} }
/** /**
* Удалить все биометрические данные * Удалить все биометрические данные
*/ */
suspend fun clearAll() { suspend fun clearAll() = withContext(Dispatchers.IO) {
context.biometricDataStore.edit { preferences -> encryptedPrefs.edit().clear().apply()
preferences.clear() _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>() val messageAttachments = mutableListOf<MessageAttachment>()
var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом) 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()) { if (replyMsgsToSend.isNotEmpty()) {
// Формируем JSON массив с цитируемыми сообщениями (как в Desktop) // Формируем JSON массив с цитируемыми сообщениями (как в Desktop)