Refactor image handling and decoding logic

- Introduced a maximum bitmap decode dimension to prevent excessive memory usage.
- Enhanced base64 to bitmap conversion by extracting payload and applying EXIF orientation.
- Improved error handling for image downloads and decoding processes.
- Simplified media picker and chat input components to manage keyboard visibility more effectively.
- Updated color selection grid to adaptively adjust based on available width.
- Added safety checks for notifications and call actions in profile screens.
- Optimized bitmap decoding in uriToBase64Image to handle large images more efficiently.
This commit is contained in:
2026-02-20 02:45:00 +05:00
parent 5cf8b2866f
commit 88e2084f8b
26 changed files with 943 additions and 464 deletions

View File

@@ -133,7 +133,7 @@ class BiometricAuthManager(private val context: Context) {
removeBiometricData()
onError("Биометрические данные изменились. Пожалуйста, настройте заново.")
} catch (e: Exception) {
onError("Ошибка инициализации: ${e.message}")
onError("Ошибка инициализации: ${formatInitializationError(e)}")
}
}
@@ -195,7 +195,7 @@ class BiometricAuthManager(private val context: Context) {
removeBiometricData()
onError("Биометрические данные изменились. Пожалуйста, войдите с паролем и настройте биометрию заново.")
} catch (e: Exception) {
onError("Ошибка инициализации: ${e.message}")
onError("Ошибка инициализации: ${formatInitializationError(e)}")
}
}
@@ -264,13 +264,68 @@ class BiometricAuthManager(private val context: Context) {
private fun getOrCreateSecretKey(): SecretKey {
// Проверяем, есть ли уже ключ
getSecretKey()?.let { return it }
// Генерируем новый ключ в Keystore
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
KEYSTORE_PROVIDER
val attempts = buildKeyGenerationAttempts()
var lastError: Exception? = null
for ((index, config) in attempts.withIndex()) {
try {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
KEYSTORE_PROVIDER
)
val spec = buildKeyGenSpec(config)
keyGenerator.init(spec)
return keyGenerator.generateKey()
} catch (e: Exception) {
lastError = e
Log.w(
TAG,
"Key generation attempt ${index + 1}/${attempts.size} failed " +
"(strongBox=${config.useStrongBox}, attestation=${config.useAttestation}, " +
"unlockedRequired=${config.requireUnlockedDevice})",
e
)
removeKeyAliasSilently()
}
}
throw IllegalStateException("Failed to generate key", lastError)
}
private fun buildKeyGenerationAttempts(): List<KeyGenerationConfig> {
val attempts = mutableListOf<KeyGenerationConfig>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attempts += KeyGenerationConfig(
useStrongBox = true,
useAttestation = true,
requireUnlockedDevice = true
)
}
attempts += KeyGenerationConfig(
useStrongBox = false,
useAttestation = true,
requireUnlockedDevice = true
)
attempts += KeyGenerationConfig(
useStrongBox = false,
useAttestation = false,
requireUnlockedDevice = true
)
attempts += KeyGenerationConfig(
useStrongBox = false,
useAttestation = false,
requireUnlockedDevice = false
)
return attempts
}
private fun buildKeyGenSpec(config: KeyGenerationConfig): KeyGenParameterSpec {
val builder = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
@@ -278,42 +333,49 @@ class BiometricAuthManager(private val context: Context) {
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
// Ключ доступен только после биометрической аутентификации
.setUserAuthenticationRequired(true)
// Ключ инвалидируется при добавлении новой биометрии
.setInvalidatedByBiometricEnrollment(true)
// Ключ доступен только когда устройство разблокировано
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (config.requireUnlockedDevice && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUnlockedDeviceRequired(true)
}
// Для Android 11+ устанавливаем параметры аутентификации
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder.setUserAuthenticationParameters(
0, // timeout = 0 означает требование аутентификации для каждой операции
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) {
}
if (config.useAttestation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setAttestationChallenge(generateAttestationChallenge())
}
// Используем StrongBox если доступен (аппаратный модуль безопасности)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
builder.setIsStrongBoxBacked(true)
} catch (e: Exception) {
}
if (config.useStrongBox && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setIsStrongBoxBacked(true)
}
return builder.build()
}
private fun removeKeyAliasSilently() {
try {
if (keyStore.containsAlias(KEY_ALIAS)) {
keyStore.deleteEntry(KEY_ALIAS)
}
} catch (_: Exception) {
}
}
private fun formatInitializationError(error: Throwable): String {
val rootCause = generateSequence(error) { it.cause }.lastOrNull() ?: error
val message = rootCause.message ?: error.message ?: "Unknown error"
return if (message.contains("Failed to generate key", ignoreCase = true)) {
"Не удалось создать ключ. Проверьте, что на устройстве включена блокировка экрана и настроена биометрия."
} else {
message
}
keyGenerator.init(builder.build())
return keyGenerator.generateKey()
}
/**
@@ -483,6 +545,12 @@ class BiometricAuthManager(private val context: Context) {
}
}
private data class KeyGenerationConfig(
val useStrongBox: Boolean,
val useAttestation: Boolean,
val requireUnlockedDevice: Boolean
)
/**
* Результат проверки Key Attestation
*/

View File

@@ -27,9 +27,13 @@ class BiometricPreferences(private val context: Context) {
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_"
// Shared between all BiometricPreferences instances so UI in different screens
// receives updates immediately (ProfileScreen <-> BiometricEnableScreen).
private val biometricEnabledState = MutableStateFlow(false)
}
private val _isBiometricEnabled = MutableStateFlow(false)
private val appContext = context.applicationContext
private val _isBiometricEnabled = biometricEnabledState
private val encryptedPrefs: SharedPreferences by lazy {
createEncryptedPreferences()
@@ -49,13 +53,13 @@ class BiometricPreferences(private val context: Context) {
private fun createEncryptedPreferences(): SharedPreferences {
try {
// Создаем MasterKey с максимальной защитой
val masterKey = MasterKey.Builder(context)
val masterKey = MasterKey.Builder(appContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setUserAuthenticationRequired(false) // Биометрия проверяется отдельно в BiometricAuthManager
.build()
return EncryptedSharedPreferences.create(
context,
appContext,
PREFS_FILE_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
@@ -63,7 +67,7 @@ class BiometricPreferences(private val context: Context) {
)
} catch (e: Exception) {
// Fallback на обычные SharedPreferences в случае ошибки (не должно произойти)
return context.getSharedPreferences(PREFS_FILE_NAME + "_fallback", Context.MODE_PRIVATE)
return appContext.getSharedPreferences(PREFS_FILE_NAME + "_fallback", Context.MODE_PRIVATE)
}
}
@@ -76,16 +80,22 @@ class BiometricPreferences(private val context: Context) {
* Включить биометрическую аутентификацию
*/
suspend fun enableBiometric() = withContext(Dispatchers.IO) {
encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, true).apply()
_isBiometricEnabled.value = true
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)
}
/**
* Отключить биометрическую аутентификацию
*/
suspend fun disableBiometric() = withContext(Dispatchers.IO) {
encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, false).apply()
_isBiometricEnabled.value = false
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)
}
/**
@@ -117,8 +127,11 @@ class BiometricPreferences(private val context: Context) {
* Удалить все биометрические данные
*/
suspend fun clearAll() = withContext(Dispatchers.IO) {
encryptedPrefs.edit().clear().apply()
_isBiometricEnabled.value = false
val success = encryptedPrefs.edit().clear().commit()
if (!success) {
Log.w(TAG, "Failed to clear biometric preferences")
}
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
}
/**