Files
mobile-android/app/src/main/java/com/rosetta/messenger/biometric/BiometricAuthManager.kt

228 lines
9.0 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.util.Base64
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
/**
* Менеджер для работы с биометрической аутентификацией
* Использует простую биометрию (без криптографии) для совместимости с Class 2 (Weak) биометрией
*/
class BiometricAuthManager(private val context: Context) {
companion object {
private const val ENCRYPTION_KEY = "RosettaBiometricKey2024"
private const val SALT = "RosettaSalt"
private const val IV_SEPARATOR = "]"
}
/**
* Проверяет доступность биометрической аутентификации на устройстве
*/
fun isBiometricAvailable(): BiometricAvailability {
val biometricManager = BiometricManager.from(context)
// Проверяем любую биометрию (включая слабую)
return when (biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK
)) {
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("Неизвестная ошибка")
}
}
/**
* Шифрует пароль с помощью простого AES (не привязанного к биометрии)
* Биометрия используется только для подтверждения личности
*/
fun encryptPassword(
activity: FragmentActivity,
password: String,
onSuccess: (String) -> Unit,
onError: (String) -> Unit,
onCancel: () -> Unit
) {
// Сначала запрашиваем биометрию
showBiometricPrompt(
activity = activity,
title = "Сохранить пароль",
subtitle = "Подтвердите для сохранения пароля",
onSuccess = {
try {
// После успешной биометрии шифруем пароль
val encrypted = encryptString(password)
onSuccess(encrypted)
} catch (e: Exception) {
onError("Ошибка шифрования: ${e.message}")
}
},
onError = onError,
onCancel = onCancel
)
}
/**
* Расшифровывает пароль после подтверждения биометрией
*/
fun decryptPassword(
activity: FragmentActivity,
encryptedData: String,
onSuccess: (String) -> Unit,
onError: (String) -> Unit,
onCancel: () -> Unit
) {
showBiometricPrompt(
activity = activity,
title = "Разблокировать",
subtitle = "Подтвердите для входа",
onSuccess = {
try {
val decrypted = decryptString(encryptedData)
onSuccess(decrypted)
} catch (e: Exception) {
onError("Ошибка расшифровки: ${e.message}")
}
},
onError = onError,
onCancel = onCancel
)
}
/**
* Показывает диалог биометрической аутентификации
*/
private fun showBiometricPrompt(
activity: FragmentActivity,
title: String,
subtitle: String,
onSuccess: () -> 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)
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricPrompt.ERROR_CANCELED) {
onCancel()
} else {
onError(errString.toString())
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess()
}
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_WEAK)
.build()
biometricPrompt.authenticate(promptInfo)
}
/**
* Простое AES шифрование строки
*/
private fun encryptString(data: String): String {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256)
val tmp = factory.generateSecret(spec)
val secretKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
val encryptedString = Base64.encodeToString(encrypted, Base64.NO_WRAP)
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)
val encrypted = Base64.decode(parts[1], Base64.NO_WRAP)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(ENCRYPTION_KEY.toCharArray(), SALT.toByteArray(), 65536, 256)
val tmp = factory.generateSecret(spec)
val secretKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
val decrypted = cipher.doFinal(encrypted)
return String(decrypted, Charsets.UTF_8)
}
/**
* Удаляет сохраненные биометрические данные
*/
fun removeBiometricData() {
// Ничего не нужно удалять, так как мы не используем Keystore
}
}
/**
* Результат проверки доступности биометрии
*/
sealed class BiometricAvailability {
object Available : BiometricAvailability()
data class NotAvailable(val reason: String) : BiometricAvailability()
data class NotEnrolled(val reason: String) : BiometricAvailability()
}