228 lines
9.0 KiB
Kotlin
228 lines
9.0 KiB
Kotlin
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()
|
||
}
|