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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user