- 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.
579 lines
23 KiB
Kotlin
579 lines
23 KiB
Kotlin
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.KeyGenerator
|
||
import javax.crypto.SecretKey
|
||
import javax.crypto.spec.GCMParameterSpec
|
||
|
||
/**
|
||
* Безопасный менеджер биометрической аутентификации
|
||
*
|
||
* Использует Android Keystore для хранения ключей шифрования:
|
||
* - Ключи генерируются и хранятся в аппаратном модуле безопасности (TEE/StrongBox)
|
||
* - Ключи привязаны к биометрии через setUserAuthenticationRequired
|
||
* - Ключи инвалидируются при изменении биометрических данных
|
||
* - Используется AES-GCM для аутентифицированного шифрования
|
||
* - CryptoObject привязывает криптографические операции к биометрии
|
||
*/
|
||
class BiometricAuthManager(private val context: Context) {
|
||
|
||
companion object {
|
||
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_STRONG)) {
|
||
BiometricManager.BIOMETRIC_SUCCESS ->
|
||
BiometricAvailability.Available
|
||
|
||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
|
||
BiometricAvailability.NotAvailable("Биометрическое оборудование недоступно")
|
||
|
||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
|
||
BiometricAvailability.NotAvailable("Биометрическое оборудование временно недоступно")
|
||
|
||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
|
||
BiometricAvailability.NotEnrolled("Биометрические данные не настроены")
|
||
|
||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED ->
|
||
BiometricAvailability.NotAvailable("Требуется обновление безопасности")
|
||
|
||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED ->
|
||
BiometricAvailability.NotAvailable("Биометрическая аутентификация не поддерживается")
|
||
|
||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN ->
|
||
BiometricAvailability.NotAvailable("Неизвестный статус")
|
||
|
||
else -> BiometricAvailability.NotAvailable("Неизвестная ошибка")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Шифрует пароль с использованием ключа из Android Keystore
|
||
* Шифрование привязано к биометрии через CryptoObject
|
||
*/
|
||
fun encryptPassword(
|
||
activity: FragmentActivity,
|
||
password: String,
|
||
onSuccess: (String) -> Unit,
|
||
onError: (String) -> Unit,
|
||
onCancel: () -> Unit
|
||
) {
|
||
try {
|
||
// Генерируем или получаем ключ
|
||
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) {
|
||
onError("Ошибка шифрования: ${e.message}")
|
||
}
|
||
},
|
||
onError = onError,
|
||
onCancel = onCancel
|
||
)
|
||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||
removeBiometricData()
|
||
onError("Биометрические данные изменились. Пожалуйста, настройте заново.")
|
||
} catch (e: Exception) {
|
||
onError("Ошибка инициализации: ${formatInitializationError(e)}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Расшифровывает пароль с использованием ключа из Android Keystore
|
||
* Расшифровка возможна только после успешной биометрической аутентификации
|
||
*/
|
||
fun decryptPassword(
|
||
activity: FragmentActivity,
|
||
encryptedData: String,
|
||
onSuccess: (String) -> Unit,
|
||
onError: (String) -> Unit,
|
||
onCancel: () -> Unit
|
||
) {
|
||
try {
|
||
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) {
|
||
onError("Ошибка расшифровки: ${e.message}")
|
||
} finally {
|
||
// Secure memory wipe - обнуляем расшифрованные данные
|
||
decrypted?.let { Arrays.fill(it, 0.toByte()) }
|
||
}
|
||
},
|
||
onError = onError,
|
||
onCancel = onCancel
|
||
)
|
||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||
removeBiometricData()
|
||
onError("Биометрические данные изменились. Пожалуйста, войдите с паролем и настройте биометрию заново.")
|
||
} catch (e: Exception) {
|
||
onError("Ошибка инициализации: ${formatInitializationError(e)}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Показывает биометрический промпт с криптографической привязкой (CryptoObject)
|
||
*/
|
||
private fun showBiometricPromptWithCrypto(
|
||
activity: FragmentActivity,
|
||
cipher: Cipher,
|
||
title: String,
|
||
subtitle: String,
|
||
onSuccess: (Cipher) -> Unit,
|
||
onError: (String) -> Unit,
|
||
onCancel: () -> Unit
|
||
) {
|
||
val executor = ContextCompat.getMainExecutor(context)
|
||
|
||
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||
super.onAuthenticationError(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)
|
||
// Получаем аутентифицированный Cipher из CryptoObject
|
||
val authenticatedCipher = result.cryptoObject?.cipher
|
||
if (authenticatedCipher != null) {
|
||
onSuccess(authenticatedCipher)
|
||
} else {
|
||
onError("Ошибка: криптографический объект не получен")
|
||
}
|
||
}
|
||
|
||
override fun onAuthenticationFailed() {
|
||
super.onAuthenticationFailed()
|
||
// Не вызываем onError - пользователь может попробовать снова
|
||
}
|
||
}
|
||
|
||
val biometricPrompt = BiometricPrompt(activity, executor, callback)
|
||
|
||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||
.setTitle(title)
|
||
.setSubtitle(subtitle)
|
||
.setNegativeButtonText("Отмена")
|
||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||
.setConfirmationRequired(false)
|
||
.build()
|
||
|
||
// Аутентификация с CryptoObject - криптографическая операция привязана к биометрии
|
||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||
}
|
||
|
||
/**
|
||
* Генерирует новый ключ или возвращает существующий из Android Keystore
|
||
*/
|
||
private fun getOrCreateSecretKey(): SecretKey {
|
||
// Проверяем, есть ли уже ключ
|
||
getSecretKey()?.let { return it }
|
||
|
||
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
|
||
)
|
||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||
.setKeySize(256)
|
||
.setUserAuthenticationRequired(true)
|
||
.setInvalidatedByBiometricEnrollment(true)
|
||
|
||
if (config.requireUnlockedDevice && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||
builder.setUnlockedDeviceRequired(true)
|
||
}
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||
builder.setUserAuthenticationParameters(
|
||
0,
|
||
KeyProperties.AUTH_BIOMETRIC_STRONG
|
||
)
|
||
}
|
||
|
||
if (config.useAttestation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||
builder.setAttestationChallenge(generateAttestationChallenge())
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получает существующий ключ из Keystore
|
||
*/
|
||
private fun getSecretKey(): SecretKey? {
|
||
return try {
|
||
keyStore.getKey(KEY_ALIAS, null) as? SecretKey
|
||
} catch (e: Exception) {
|
||
null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Удаляет ключ из Android Keystore
|
||
* Это делает все ранее зашифрованные данные недоступными
|
||
*/
|
||
fun removeBiometricData() {
|
||
try {
|
||
if (keyStore.containsAlias(KEY_ALIAS)) {
|
||
keyStore.deleteEntry(KEY_ALIAS)
|
||
}
|
||
} catch (e: Exception) {
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Проверяет, есть ли сохраненный ключ
|
||
*/
|
||
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) {
|
||
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) {
|
||
return KeyAttestationResult.Valid(isStrongBox = isKeyInStrongBox())
|
||
}
|
||
}
|
||
|
||
KeyAttestationResult.NotSupported
|
||
} catch (e: Exception) {
|
||
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) {
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
private data class KeyGenerationConfig(
|
||
val useStrongBox: Boolean,
|
||
val useAttestation: Boolean,
|
||
val requireUnlockedDevice: Boolean
|
||
)
|
||
|
||
/**
|
||
* Результат проверки 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()
|
||
}
|
||
|
||
/**
|
||
* Результат проверки доступности биометрии
|
||
*/
|
||
sealed class BiometricAvailability {
|
||
object Available : BiometricAvailability()
|
||
data class NotAvailable(val reason: String) : BiometricAvailability()
|
||
data class NotEnrolled(val reason: String) : BiometricAvailability()
|
||
}
|