Онбординг: отдельный экран биометрии, новый UI пароля (Telegram-style), Skip на всех шагах. Биометрия per-account. Навбар плавно анимируется при смене темы. Поиск: аватарки в результатах. Профиль: клавиатура прячется при скролле. Фокус сбрасывается при навигации.

This commit is contained in:
2026-04-08 02:56:53 +05:00
parent 14d7fc6eb1
commit 299c84cb89
11 changed files with 649 additions and 651 deletions

View File

@@ -14,50 +14,36 @@ import kotlinx.coroutines.withContext
/**
* Безопасное хранилище настроек биометрической аутентификации
* Использует EncryptedSharedPreferences с MasterKey из Android Keystore
*
* Уровни защиты:
* - AES256_GCM для шифрования значений
* - AES256_SIV для шифрования ключей
* - MasterKey хранится в Android Keystore (TEE/StrongBox)
*
* Биометрия привязана к конкретному аккаунту (per-account), не глобальная.
*/
class BiometricPreferences(private val context: Context) {
companion object {
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 KEY_BIOMETRIC_ENABLED_PREFIX = "biometric_enabled_"
private const val ENCRYPTED_PASSWORD_PREFIX = "encrypted_password_"
// Shared between all BiometricPreferences instances so UI in different screens
// receives updates immediately (ProfileScreen <-> BiometricEnableScreen).
// Legacy key (global) — for migration
private const val KEY_BIOMETRIC_ENABLED_LEGACY = "biometric_enabled"
// Shared state for reactive UI updates
private val biometricEnabledState = MutableStateFlow(false)
}
private val appContext = context.applicationContext
private val _isBiometricEnabled = biometricEnabledState
private val encryptedPrefs: SharedPreferences by lazy {
createEncryptedPreferences()
}
init {
// Загружаем начальное значение
try {
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
} catch (e: Exception) {
}
}
/**
* Создает EncryptedSharedPreferences с максимальной защитой
*/
private fun createEncryptedPreferences(): SharedPreferences {
try {
// Создаем MasterKey с максимальной защитой
val masterKey = MasterKey.Builder(appContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setUserAuthenticationRequired(false) // Биометрия проверяется отдельно в BiometricAuthManager
.setUserAuthenticationRequired(false)
.build()
return EncryptedSharedPreferences.create(
appContext,
PREFS_FILE_NAME,
@@ -66,77 +52,93 @@ class BiometricPreferences(private val context: Context) {
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (e: Exception) {
// Fallback на обычные SharedPreferences в случае ошибки (не должно произойти)
return appContext.getSharedPreferences(PREFS_FILE_NAME + "_fallback", Context.MODE_PRIVATE)
}
}
/**
* Включена ли биометрическая аутентификация
*/
val isBiometricEnabled: Flow<Boolean> = _isBiometricEnabled.asStateFlow()
/**
* Включить биометрическую аутентификацию
* Загрузить состояние биометрии для конкретного аккаунта
*/
fun loadForAccount(publicKey: String) {
try {
val key = "$KEY_BIOMETRIC_ENABLED_PREFIX$publicKey"
val perAccount = encryptedPrefs.getBoolean(key, false)
// Migration: если per-account нет, проверяем legacy глобальный ключ
if (!perAccount && encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED_LEGACY, false)) {
// Мигрируем: копируем глобальное значение в per-account
encryptedPrefs.edit().putBoolean(key, true).apply()
_isBiometricEnabled.value = true
} else {
_isBiometricEnabled.value = perAccount
}
} catch (e: Exception) {
_isBiometricEnabled.value = false
}
}
/**
* Включить биометрическую аутентификацию для аккаунта
*/
suspend fun enableBiometric(publicKey: String) = withContext(Dispatchers.IO) {
val key = "$KEY_BIOMETRIC_ENABLED_PREFIX$publicKey"
encryptedPrefs.edit().putBoolean(key, true).commit()
_isBiometricEnabled.value = true
}
/**
* Отключить биометрическую аутентификацию для аккаунта
*/
suspend fun disableBiometric(publicKey: String) = withContext(Dispatchers.IO) {
val key = "$KEY_BIOMETRIC_ENABLED_PREFIX$publicKey"
encryptedPrefs.edit().putBoolean(key, false).commit()
_isBiometricEnabled.value = false
}
/**
* Проверить включена ли биометрия для аккаунта (синхронно)
*/
fun isBiometricEnabledForAccount(publicKey: String): Boolean {
return try {
encryptedPrefs.getBoolean("$KEY_BIOMETRIC_ENABLED_PREFIX$publicKey", false)
} catch (_: Exception) { false }
}
// --- Legacy compat: old callers without publicKey ---
@Deprecated("Use enableBiometric(publicKey) instead")
suspend fun enableBiometric() = withContext(Dispatchers.IO) {
val success = encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, true).commit()
if (!success) {
Log.w(TAG, "Failed to persist biometric enabled state")
}
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED_LEGACY, true).commit()
_isBiometricEnabled.value = true
}
/**
* Отключить биометрическую аутентификацию
*/
@Deprecated("Use disableBiometric(publicKey) instead")
suspend fun disableBiometric() = withContext(Dispatchers.IO) {
val success = encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, false).commit()
if (!success) {
Log.w(TAG, "Failed to persist biometric disabled state")
}
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED_LEGACY, false).commit()
_isBiometricEnabled.value = false
}
/**
* Сохранить зашифрованный пароль для аккаунта
* Пароль уже зашифрован BiometricAuthManager, здесь добавляется второй слой шифрования
*/
suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) = withContext(Dispatchers.IO) {
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
encryptedPrefs.edit().putString(key, encryptedPassword).apply()
}
/**
* Получить зашифрованный пароль для аккаунта
*/
suspend fun getEncryptedPassword(publicKey: String): String? = withContext(Dispatchers.IO) {
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
encryptedPrefs.getString(key, null)
}
/**
* Удалить зашифрованный пароль для аккаунта
*/
suspend fun removeEncryptedPassword(publicKey: String) = withContext(Dispatchers.IO) {
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
encryptedPrefs.edit().remove(key).apply()
}
/**
* Удалить все биометрические данные
*/
suspend fun clearAll() = withContext(Dispatchers.IO) {
val success = encryptedPrefs.edit().clear().commit()
if (!success) {
Log.w(TAG, "Failed to clear biometric preferences")
}
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
encryptedPrefs.edit().clear().commit()
_isBiometricEnabled.value = false
}
/**
* Проверить, есть ли сохраненный зашифрованный пароль для аккаунта
*/
suspend fun hasEncryptedPassword(publicKey: String): Boolean {
return getEncryptedPassword(publicKey) != null
}