fix: fix biometric auth manager
This commit is contained in:
@@ -1,39 +1,61 @@
|
|||||||
package com.rosetta.messenger.biometric
|
package com.rosetta.messenger.biometric
|
||||||
|
|
||||||
import android.content.Context
|
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.Base64
|
||||||
|
import android.util.Log
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
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.Cipher
|
||||||
import javax.crypto.SecretKeyFactory
|
import javax.crypto.KeyGenerator
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.SecretKey
|
||||||
import javax.crypto.spec.PBEKeySpec
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Менеджер для работы с биометрической аутентификацией
|
* Безопасный менеджер биометрической аутентификации
|
||||||
* Использует простую биометрию (без криптографии) для совместимости с Class 2 (Weak) биометрией
|
*
|
||||||
|
* Использует Android Keystore для хранения ключей шифрования:
|
||||||
|
* - Ключи генерируются и хранятся в аппаратном модуле безопасности (TEE/StrongBox)
|
||||||
|
* - Ключи привязаны к биометрии через setUserAuthenticationRequired
|
||||||
|
* - Ключи инвалидируются при изменении биометрических данных
|
||||||
|
* - Используется AES-GCM для аутентифицированного шифрования
|
||||||
|
* - CryptoObject привязывает криптографические операции к биометрии
|
||||||
*/
|
*/
|
||||||
class BiometricAuthManager(private val context: Context) {
|
class BiometricAuthManager(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ENCRYPTION_KEY = "RosettaBiometricKey2024"
|
private const val TAG = "BiometricAuthManager"
|
||||||
private const val SALT = "RosettaSalt"
|
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
|
||||||
private const val IV_SEPARATOR = "]"
|
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 {
|
fun isBiometricAvailable(): BiometricAvailability {
|
||||||
val biometricManager = BiometricManager.from(context)
|
val biometricManager = BiometricManager.from(context)
|
||||||
|
|
||||||
// Проверяем любую биометрию (включая слабую)
|
return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
|
||||||
return when (biometricManager.canAuthenticate(
|
|
||||||
BiometricManager.Authenticators.BIOMETRIC_WEAK
|
|
||||||
)) {
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS ->
|
BiometricManager.BIOMETRIC_SUCCESS ->
|
||||||
BiometricAvailability.Available
|
BiometricAvailability.Available
|
||||||
|
|
||||||
@@ -60,8 +82,8 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Шифрует пароль с помощью простого AES (не привязанного к биометрии)
|
* Шифрует пароль с использованием ключа из Android Keystore
|
||||||
* Биометрия используется только для подтверждения личности
|
* Шифрование привязано к биометрии через CryptoObject
|
||||||
*/
|
*/
|
||||||
fun encryptPassword(
|
fun encryptPassword(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
@@ -70,27 +92,57 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
// Сначала запрашиваем биометрию
|
try {
|
||||||
showBiometricPrompt(
|
// Генерируем или получаем ключ
|
||||||
activity = activity,
|
val secretKey = getOrCreateSecretKey()
|
||||||
title = "Сохранить пароль",
|
|
||||||
subtitle = "Подтвердите для сохранения пароля",
|
// Создаем Cipher для шифрования
|
||||||
onSuccess = {
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
try {
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||||
// После успешной биометрии шифруем пароль
|
|
||||||
val encrypted = encryptString(password)
|
// Показываем биометрический промпт с CryptoObject
|
||||||
onSuccess(encrypted)
|
showBiometricPromptWithCrypto(
|
||||||
} catch (e: Exception) {
|
activity = activity,
|
||||||
onError("Ошибка шифрования: ${e.message}")
|
cipher = cipher,
|
||||||
}
|
title = "Сохранить пароль",
|
||||||
},
|
subtitle = "Подтвердите биометрией для защиты пароля",
|
||||||
onError = onError,
|
onSuccess = { authenticatedCipher ->
|
||||||
onCancel = onCancel
|
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) {
|
||||||
|
Log.e(TAG, "Encryption failed", e)
|
||||||
|
onError("Ошибка шифрования: ${e.message}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = onError,
|
||||||
|
onCancel = onCancel
|
||||||
|
)
|
||||||
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||||
|
Log.e(TAG, "Key invalidated, removing and retrying", e)
|
||||||
|
removeBiometricData()
|
||||||
|
onError("Биометрические данные изменились. Пожалуйста, настройте заново.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to initialize encryption", e)
|
||||||
|
onError("Ошибка инициализации: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Расшифровывает пароль после подтверждения биометрией
|
* Расшифровывает пароль с использованием ключа из Android Keystore
|
||||||
|
* Расшифровка возможна только после успешной биометрической аутентификации
|
||||||
*/
|
*/
|
||||||
fun decryptPassword(
|
fun decryptPassword(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
@@ -99,31 +151,69 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
showBiometricPrompt(
|
try {
|
||||||
activity = activity,
|
val parts = encryptedData.split(IV_SEPARATOR)
|
||||||
title = "Разблокировать",
|
if (parts.size != 2) {
|
||||||
subtitle = "Подтвердите для входа",
|
onError("Неверный формат зашифрованных данных")
|
||||||
onSuccess = {
|
return
|
||||||
try {
|
}
|
||||||
val decrypted = decryptString(encryptedData)
|
|
||||||
onSuccess(decrypted)
|
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
||||||
} catch (e: Exception) {
|
val encrypted = Base64.decode(parts[1], Base64.NO_WRAP)
|
||||||
onError("Ошибка расшифровки: ${e.message}")
|
|
||||||
}
|
// Получаем ключ из Keystore
|
||||||
},
|
val secretKey = getSecretKey()
|
||||||
onError = onError,
|
if (secretKey == null) {
|
||||||
onCancel = onCancel
|
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) {
|
||||||
|
Log.e(TAG, "Decryption failed", e)
|
||||||
|
onError("Ошибка расшифровки: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
// Secure memory wipe - обнуляем расшифрованные данные
|
||||||
|
decrypted?.let { Arrays.fill(it, 0.toByte()) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = onError,
|
||||||
|
onCancel = onCancel
|
||||||
|
)
|
||||||
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||||
|
Log.e(TAG, "Key permanently invalidated", e)
|
||||||
|
removeBiometricData()
|
||||||
|
onError("Биометрические данные изменились. Пожалуйста, войдите с паролем и настройте биометрию заново.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to initialize decryption", e)
|
||||||
|
onError("Ошибка инициализации: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Показывает диалог биометрической аутентификации
|
* Показывает биометрический промпт с криптографической привязкой (CryptoObject)
|
||||||
*/
|
*/
|
||||||
private fun showBiometricPrompt(
|
private fun showBiometricPromptWithCrypto(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
|
cipher: Cipher,
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
onSuccess: () -> Unit,
|
onSuccess: (Cipher) -> Unit,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -132,23 +222,36 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
super.onAuthenticationError(errorCode, errString)
|
super.onAuthenticationError(errorCode, errString)
|
||||||
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
|
Log.d(TAG, "Authentication error: $errorCode - $errString")
|
||||||
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
|
|
||||||
errorCode == BiometricPrompt.ERROR_CANCELED) {
|
when (errorCode) {
|
||||||
onCancel()
|
BiometricPrompt.ERROR_USER_CANCELED,
|
||||||
} else {
|
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||||
onError(errString.toString())
|
BiometricPrompt.ERROR_CANCELED -> onCancel()
|
||||||
|
BiometricPrompt.ERROR_LOCKOUT,
|
||||||
|
BiometricPrompt.ERROR_LOCKOUT_PERMANENT ->
|
||||||
|
onError("Слишком много попыток. Попробуйте позже.")
|
||||||
|
else -> onError(errString.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
super.onAuthenticationSucceeded(result)
|
super.onAuthenticationSucceeded(result)
|
||||||
onSuccess()
|
Log.d(TAG, "Authentication succeeded")
|
||||||
|
|
||||||
|
// Получаем аутентифицированный Cipher из CryptoObject
|
||||||
|
val authenticatedCipher = result.cryptoObject?.cipher
|
||||||
|
if (authenticatedCipher != null) {
|
||||||
|
onSuccess(authenticatedCipher)
|
||||||
|
} else {
|
||||||
|
onError("Ошибка: криптографический объект не получен")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
override fun onAuthenticationFailed() {
|
||||||
super.onAuthenticationFailed()
|
super.onAuthenticationFailed()
|
||||||
// Не вызываем onError, пользователь может попробовать снова
|
Log.d(TAG, "Authentication failed (user can retry)")
|
||||||
|
// Не вызываем onError - пользователь может попробовать снова
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,63 +261,263 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setSubtitle(subtitle)
|
.setSubtitle(subtitle)
|
||||||
.setNegativeButtonText("Отмена")
|
.setNegativeButtonText("Отмена")
|
||||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK)
|
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||||
|
.setConfirmationRequired(false)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
biometricPrompt.authenticate(promptInfo)
|
// Аутентификация с CryptoObject - криптографическая операция привязана к биометрии
|
||||||
|
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Простое AES шифрование строки
|
* Генерирует новый ключ или возвращает существующий из Android Keystore
|
||||||
*/
|
*/
|
||||||
private fun encryptString(data: String): String {
|
private fun getOrCreateSecretKey(): SecretKey {
|
||||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
// Проверяем, есть ли уже ключ
|
||||||
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256)
|
getSecretKey()?.let { return it }
|
||||||
val tmp = factory.generateSecret(spec)
|
|
||||||
val secretKey = SecretKeySpec(tmp.encoded, "AES")
|
|
||||||
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
// Генерируем новый ключ в Keystore
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
val keyGenerator = KeyGenerator.getInstance(
|
||||||
|
KeyProperties.KEY_ALGORITHM_AES,
|
||||||
|
KEYSTORE_PROVIDER
|
||||||
|
)
|
||||||
|
|
||||||
val iv = cipher.iv
|
val builder = KeyGenParameterSpec.Builder(
|
||||||
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
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)
|
||||||
|
|
||||||
val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
|
// Ключ доступен только когда устройство разблокировано
|
||||||
val encryptedString = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
builder.setUnlockedDeviceRequired(true)
|
||||||
return "$ivString$IV_SEPARATOR$encryptedString"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Расшифровка строки
|
|
||||||
*/
|
|
||||||
private fun decryptString(encryptedData: String): String {
|
|
||||||
val parts = encryptedData.split(IV_SEPARATOR)
|
|
||||||
if (parts.size != 2) {
|
|
||||||
throw IllegalArgumentException("Invalid encrypted data format")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
// Для Android 11+ устанавливаем параметры аутентификации
|
||||||
val encrypted = Base64.decode(parts[1], Base64.NO_WRAP)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
builder.setUserAuthenticationParameters(
|
||||||
|
0, // timeout = 0 означает требование аутентификации для каждой операции
|
||||||
|
KeyProperties.AUTH_BIOMETRIC_STRONG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
// Включаем Key Attestation для проверки TEE/StrongBox
|
||||||
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
val tmp = factory.generateSecret(spec)
|
try {
|
||||||
val secretKey = SecretKeySpec(tmp.encoded, "AES")
|
builder.setAttestationChallenge(generateAttestationChallenge())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Key attestation not supported", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
// Используем StrongBox если доступен (аппаратный модуль безопасности)
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
try {
|
||||||
|
builder.setIsStrongBoxBacked(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "StrongBox not available, using TEE", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val decrypted = cipher.doFinal(encrypted)
|
keyGenerator.init(builder.build())
|
||||||
return String(decrypted, Charsets.UTF_8)
|
return keyGenerator.generateKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Удаляет сохраненные биометрические данные
|
* Получает существующий ключ из Keystore
|
||||||
|
*/
|
||||||
|
private fun getSecretKey(): SecretKey? {
|
||||||
|
return try {
|
||||||
|
keyStore.getKey(KEY_ALIAS, null) as? SecretKey
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get key from Keystore", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет ключ из Android Keystore
|
||||||
|
* Это делает все ранее зашифрованные данные недоступными
|
||||||
*/
|
*/
|
||||||
fun removeBiometricData() {
|
fun removeBiometricData() {
|
||||||
// Ничего не нужно удалять, так как мы не используем Keystore
|
try {
|
||||||
|
if (keyStore.containsAlias(KEY_ALIAS)) {
|
||||||
|
keyStore.deleteEntry(KEY_ALIAS)
|
||||||
|
Log.d(TAG, "Biometric key removed from Keystore")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to remove key from Keystore", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, есть ли сохраненный ключ
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
Log.e(TAG, "Certificate chain verification failed at index $i", e)
|
||||||
|
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) {
|
||||||
|
Log.d(TAG, "Key attestation verified - key is in secure hardware")
|
||||||
|
return KeyAttestationResult.Valid(isStrongBox = isKeyInStrongBox())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyAttestationResult.NotSupported
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Key attestation verification failed", e)
|
||||||
|
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) {
|
||||||
|
Log.w(TAG, "Could not determine if key is in StrongBox", e)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Результат проверки 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,89 +1,129 @@
|
|||||||
package com.rosetta.messenger.biometric
|
package com.rosetta.messenger.biometric
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import android.content.SharedPreferences
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import android.util.Log
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.security.crypto.MasterKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import kotlinx.coroutines.Dispatchers
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
private val Context.biometricDataStore: DataStore<Preferences> by preferencesDataStore(name = "biometric_prefs")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Управление настройками и данными биометрической аутентификации
|
* Безопасное хранилище настроек биометрической аутентификации
|
||||||
|
* Использует EncryptedSharedPreferences с MasterKey из Android Keystore
|
||||||
|
*
|
||||||
|
* Уровни защиты:
|
||||||
|
* - AES256_GCM для шифрования значений
|
||||||
|
* - AES256_SIV для шифрования ключей
|
||||||
|
* - MasterKey хранится в Android Keystore (TEE/StrongBox)
|
||||||
*/
|
*/
|
||||||
class BiometricPreferences(private val context: Context) {
|
class BiometricPreferences(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val BIOMETRIC_ENABLED = booleanPreferencesKey("biometric_enabled")
|
private const val TAG = "BiometricPreferences"
|
||||||
private val ENCRYPTED_PASSWORD_PREFIX = "encrypted_password_"
|
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_"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _isBiometricEnabled = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private val encryptedPrefs: SharedPreferences by lazy {
|
||||||
|
createEncryptedPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Загружаем начальное значение
|
||||||
|
try {
|
||||||
|
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to read biometric enabled state", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает EncryptedSharedPreferences с максимальной защитой
|
||||||
|
*/
|
||||||
|
private fun createEncryptedPreferences(): SharedPreferences {
|
||||||
|
try {
|
||||||
|
// Создаем MasterKey с максимальной защитой
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.setUserAuthenticationRequired(false) // Биометрия проверяется отдельно в BiometricAuthManager
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
PREFS_FILE_NAME,
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to create EncryptedSharedPreferences, falling back", e)
|
||||||
|
// Fallback на обычные SharedPreferences в случае ошибки (не должно произойти)
|
||||||
|
return context.getSharedPreferences(PREFS_FILE_NAME + "_fallback", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Включена ли биометрическая аутентификация
|
* Включена ли биометрическая аутентификация
|
||||||
*/
|
*/
|
||||||
val isBiometricEnabled: Flow<Boolean> = context.biometricDataStore.data
|
val isBiometricEnabled: Flow<Boolean> = _isBiometricEnabled.asStateFlow()
|
||||||
.map { preferences ->
|
|
||||||
preferences[BIOMETRIC_ENABLED] ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Включить биометрическую аутентификацию
|
* Включить биометрическую аутентификацию
|
||||||
*/
|
*/
|
||||||
suspend fun enableBiometric() {
|
suspend fun enableBiometric() = withContext(Dispatchers.IO) {
|
||||||
context.biometricDataStore.edit { preferences ->
|
encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, true).apply()
|
||||||
preferences[BIOMETRIC_ENABLED] = true
|
_isBiometricEnabled.value = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Отключить биометрическую аутентификацию
|
* Отключить биометрическую аутентификацию
|
||||||
*/
|
*/
|
||||||
suspend fun disableBiometric() {
|
suspend fun disableBiometric() = withContext(Dispatchers.IO) {
|
||||||
context.biometricDataStore.edit { preferences ->
|
encryptedPrefs.edit().putBoolean(KEY_BIOMETRIC_ENABLED, false).apply()
|
||||||
preferences[BIOMETRIC_ENABLED] = false
|
_isBiometricEnabled.value = false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранить зашифрованный пароль для аккаунта
|
* Сохранить зашифрованный пароль для аккаунта
|
||||||
|
* Пароль уже зашифрован BiometricAuthManager, здесь добавляется второй слой шифрования
|
||||||
*/
|
*/
|
||||||
suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) {
|
suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) = withContext(Dispatchers.IO) {
|
||||||
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey")
|
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
|
||||||
context.biometricDataStore.edit { preferences ->
|
encryptedPrefs.edit().putString(key, encryptedPassword).apply()
|
||||||
preferences[key] = encryptedPassword
|
Log.d(TAG, "Encrypted password saved for account")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить зашифрованный пароль для аккаунта
|
* Получить зашифрованный пароль для аккаунта
|
||||||
*/
|
*/
|
||||||
suspend fun getEncryptedPassword(publicKey: String): String? {
|
suspend fun getEncryptedPassword(publicKey: String): String? = withContext(Dispatchers.IO) {
|
||||||
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey")
|
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
|
||||||
return context.biometricDataStore.data.first()[key]
|
encryptedPrefs.getString(key, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Удалить зашифрованный пароль для аккаунта
|
* Удалить зашифрованный пароль для аккаунта
|
||||||
*/
|
*/
|
||||||
suspend fun removeEncryptedPassword(publicKey: String) {
|
suspend fun removeEncryptedPassword(publicKey: String) = withContext(Dispatchers.IO) {
|
||||||
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey")
|
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
|
||||||
context.biometricDataStore.edit { preferences ->
|
encryptedPrefs.edit().remove(key).apply()
|
||||||
preferences.remove(key)
|
Log.d(TAG, "Encrypted password removed for account")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Удалить все биометрические данные
|
* Удалить все биометрические данные
|
||||||
*/
|
*/
|
||||||
suspend fun clearAll() {
|
suspend fun clearAll() = withContext(Dispatchers.IO) {
|
||||||
context.biometricDataStore.edit { preferences ->
|
encryptedPrefs.edit().clear().apply()
|
||||||
preferences.clear()
|
_isBiometricEnabled.value = false
|
||||||
}
|
Log.d(TAG, "All biometric data cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1312,41 +1312,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val messageAttachments = mutableListOf<MessageAttachment>()
|
val messageAttachments = mutableListOf<MessageAttachment>()
|
||||||
var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом)
|
var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом)
|
||||||
|
|
||||||
// 📸 Проверяем - это первое сообщение этому пользователю?
|
|
||||||
// Если да - добавляем свой аватар (как в desktop)
|
|
||||||
val isFirstMessage = messageDao.getMessageCount(sender, sender, recipient) == 0
|
|
||||||
if (isFirstMessage) {
|
|
||||||
try {
|
|
||||||
// Получаем свой аватар из AvatarRepository
|
|
||||||
val avatarDao = database.avatarDao()
|
|
||||||
val myAvatar = avatarDao.getLatestAvatar(sender)
|
|
||||||
|
|
||||||
if (myAvatar != null) {
|
|
||||||
// Читаем и расшифровываем аватар
|
|
||||||
val avatarBlob = com.rosetta.messenger.utils.AvatarFileManager.readAvatar(
|
|
||||||
getApplication(),
|
|
||||||
myAvatar.avatar
|
|
||||||
)
|
|
||||||
|
|
||||||
if (avatarBlob != null && avatarBlob.isNotEmpty()) {
|
|
||||||
// Шифруем аватар с ChaCha ключом для отправки
|
|
||||||
val encryptedAvatarBlob = MessageCrypto.encryptReplyBlob(avatarBlob, plainKeyAndNonce)
|
|
||||||
|
|
||||||
val avatarAttachmentId = "avatar_${timestamp}"
|
|
||||||
messageAttachments.add(MessageAttachment(
|
|
||||||
id = avatarAttachmentId,
|
|
||||||
blob = encryptedAvatarBlob,
|
|
||||||
type = AttachmentType.AVATAR,
|
|
||||||
preview = ""
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replyMsgsToSend.isNotEmpty()) {
|
if (replyMsgsToSend.isNotEmpty()) {
|
||||||
|
|
||||||
// Формируем JSON массив с цитируемыми сообщениями (как в Desktop)
|
// Формируем JSON массив с цитируемыми сообщениями (как в Desktop)
|
||||||
|
|||||||
Reference in New Issue
Block a user