Files
mobile-android/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt
k1ngsterr1 88e2084f8b 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.
2026-02-20 02:45:00 +05:00

579 lines
23 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}