refactor: Simplify BiometricAuthManager by removing Android Keystore integration and using AES for encryption/decryption
This commit is contained in:
@@ -1,30 +1,26 @@
|
|||||||
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.KeyProperties
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
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 javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.KeyGenerator
|
import javax.crypto.SecretKeyFactory
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.GCMParameterSpec
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Менеджер для работы с биометрической аутентификацией
|
* Менеджер для работы с биометрической аутентификацией
|
||||||
* Поддерживает отпечаток пальца, Face ID и другие биометрические методы
|
* Использует простую биометрию (без криптографии) для совместимости с Class 2 (Weak) биометрией
|
||||||
*/
|
*/
|
||||||
class BiometricAuthManager(private val context: Context) {
|
class BiometricAuthManager(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_NAME = "rosetta_biometric_key"
|
private const val ENCRYPTION_KEY = "RosettaBiometricKey2024"
|
||||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
private const val SALT = "RosettaSalt"
|
||||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
|
||||||
private const val IV_SEPARATOR = "]"
|
private const val IV_SEPARATOR = "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +30,8 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
fun isBiometricAvailable(): BiometricAvailability {
|
fun isBiometricAvailable(): BiometricAvailability {
|
||||||
val biometricManager = BiometricManager.from(context)
|
val biometricManager = BiometricManager.from(context)
|
||||||
|
|
||||||
|
// Проверяем любую биометрию (включая слабую)
|
||||||
return when (biometricManager.canAuthenticate(
|
return when (biometricManager.canAuthenticate(
|
||||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
|
||||||
BiometricManager.Authenticators.BIOMETRIC_WEAK
|
BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
)) {
|
)) {
|
||||||
BiometricManager.BIOMETRIC_SUCCESS ->
|
BiometricManager.BIOMETRIC_SUCCESS ->
|
||||||
@@ -64,51 +60,8 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Генерирует или получает ключ из Android Keystore
|
* Шифрует пароль с помощью простого AES (не привязанного к биометрии)
|
||||||
*/
|
* Биометрия используется только для подтверждения личности
|
||||||
private fun getSecretKey(): SecretKey {
|
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
||||||
keyStore.load(null)
|
|
||||||
|
|
||||||
// Проверяем существование ключа
|
|
||||||
if (!keyStore.containsAlias(KEY_NAME)) {
|
|
||||||
generateSecretKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyStore.getKey(KEY_NAME, null) as SecretKey
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Генерирует новый ключ в Android Keystore
|
|
||||||
*/
|
|
||||||
private fun generateSecretKey() {
|
|
||||||
val keyGenerator = KeyGenerator.getInstance(
|
|
||||||
KeyProperties.KEY_ALGORITHM_AES,
|
|
||||||
ANDROID_KEYSTORE
|
|
||||||
)
|
|
||||||
|
|
||||||
val purposes = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
|
||||||
|
|
||||||
val builder = KeyGenParameterSpec.Builder(KEY_NAME, purposes)
|
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
||||||
.setUserAuthenticationRequired(true)
|
|
||||||
.setInvalidatedByBiometricEnrollment(true)
|
|
||||||
|
|
||||||
// Для Android 11+ можем указать тайм-аут аутентификации
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
builder.setUserAuthenticationParameters(
|
|
||||||
0, // 0 означает, что требуется аутентификация для каждого использования
|
|
||||||
KeyProperties.AUTH_BIOMETRIC_STRONG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyGenerator.init(builder.build())
|
|
||||||
keyGenerator.generateKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Шифрует пароль с помощью биометрического ключа
|
|
||||||
*/
|
*/
|
||||||
fun encryptPassword(
|
fun encryptPassword(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
@@ -117,43 +70,27 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
try {
|
// Сначала запрашиваем биометрию
|
||||||
val cipher = getCipher()
|
showBiometricPrompt(
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
|
activity = activity,
|
||||||
|
title = "Сохранить пароль",
|
||||||
val biometricPrompt = createBiometricPrompt(
|
subtitle = "Подтвердите для сохранения пароля",
|
||||||
activity,
|
onSuccess = {
|
||||||
onSuccess = { cryptoObject ->
|
try {
|
||||||
try {
|
// После успешной биометрии шифруем пароль
|
||||||
val encryptedPassword = cryptoObject.cipher?.doFinal(password.toByteArray())
|
val encrypted = encryptString(password)
|
||||||
val iv = cryptoObject.cipher?.iv
|
onSuccess(encrypted)
|
||||||
|
} catch (e: Exception) {
|
||||||
if (encryptedPassword != null && iv != null) {
|
onError("Ошибка шифрования: ${e.message}")
|
||||||
// Сохраняем IV вместе с зашифрованными данными
|
}
|
||||||
val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
|
},
|
||||||
val encryptedString = Base64.encodeToString(encryptedPassword, Base64.NO_WRAP)
|
onError = onError,
|
||||||
onSuccess("$ivString$IV_SEPARATOR$encryptedString")
|
onCancel = onCancel
|
||||||
} else {
|
)
|
||||||
onError("Ошибка шифрования")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onError("Ошибка при шифровании: ${e.message}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError = onError,
|
|
||||||
onCancel = onCancel
|
|
||||||
)
|
|
||||||
|
|
||||||
val promptInfo = createPromptInfo("Сохранить пароль")
|
|
||||||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onError("Ошибка инициализации: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Расшифровывает пароль с помощью биометрического ключа
|
* Расшифровывает пароль после подтверждения биометрией
|
||||||
*/
|
*/
|
||||||
fun decryptPassword(
|
fun decryptPassword(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
@@ -162,63 +99,42 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
try {
|
showBiometricPrompt(
|
||||||
// Разделяем IV и зашифрованные данные
|
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 encryptedPassword = Base64.decode(parts[1], Base64.NO_WRAP)
|
onError("Ошибка расшифровки: ${e.message}")
|
||||||
|
}
|
||||||
val cipher = getCipher()
|
},
|
||||||
val spec = GCMParameterSpec(128, iv)
|
onError = onError,
|
||||||
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
|
onCancel = onCancel
|
||||||
|
)
|
||||||
val biometricPrompt = createBiometricPrompt(
|
|
||||||
activity,
|
|
||||||
onSuccess = { cryptoObject ->
|
|
||||||
try {
|
|
||||||
val decryptedPassword = cryptoObject.cipher?.doFinal(encryptedPassword)
|
|
||||||
if (decryptedPassword != null) {
|
|
||||||
onSuccess(String(decryptedPassword))
|
|
||||||
} else {
|
|
||||||
onError("Ошибка расшифровки")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onError("Ошибка при расшифровке: ${e.message}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError = onError,
|
|
||||||
onCancel = onCancel
|
|
||||||
)
|
|
||||||
|
|
||||||
val promptInfo = createPromptInfo("Разблокировать приложение")
|
|
||||||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onError("Ошибка инициализации: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создает BiometricPrompt с обработчиками
|
* Показывает диалог биометрической аутентификации
|
||||||
*/
|
*/
|
||||||
private fun createBiometricPrompt(
|
private fun showBiometricPrompt(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
onSuccess: (BiometricPrompt.CryptoObject) -> Unit,
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
): BiometricPrompt {
|
) {
|
||||||
val executor = ContextCompat.getMainExecutor(context)
|
val executor = ContextCompat.getMainExecutor(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 ||
|
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
|
||||||
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
|
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
|
||||||
|
errorCode == BiometricPrompt.ERROR_CANCELED) {
|
||||||
onCancel()
|
onCancel()
|
||||||
} else {
|
} else {
|
||||||
onError(errString.toString())
|
onError(errString.toString())
|
||||||
@@ -227,51 +143,77 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
super.onAuthenticationSucceeded(result)
|
super.onAuthenticationSucceeded(result)
|
||||||
result.cryptoObject?.let { onSuccess(it) }
|
onSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
override fun onAuthenticationFailed() {
|
||||||
super.onAuthenticationFailed()
|
super.onAuthenticationFailed()
|
||||||
// Не вызываем onError здесь, так как пользователь может попробовать снова
|
// Не вызываем onError, пользователь может попробовать снова
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return BiometricPrompt(activity, executor, callback)
|
val biometricPrompt = BiometricPrompt(activity, executor, callback)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||||
* Создает информацию для диалога биометрической аутентификации
|
|
||||||
*/
|
|
||||||
private fun createPromptInfo(title: String): BiometricPrompt.PromptInfo {
|
|
||||||
return BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setSubtitle("Используйте биометрию для аутентификации")
|
.setSubtitle(subtitle)
|
||||||
.setNegativeButtonText("Использовать пароль")
|
.setNegativeButtonText("Отмена")
|
||||||
.setAllowedAuthenticators(
|
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK)
|
||||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
|
||||||
BiometricManager.Authenticators.BIOMETRIC_WEAK
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
biometricPrompt.authenticate(promptInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает экземпляр Cipher для шифрования/расшифровки
|
* Простое AES шифрование строки
|
||||||
*/
|
*/
|
||||||
private fun getCipher(): Cipher {
|
private fun encryptString(data: String): String {
|
||||||
return Cipher.getInstance(TRANSFORMATION)
|
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() {
|
fun removeBiometricData() {
|
||||||
try {
|
// Ничего не нужно удалять, так как мы не используем Keystore
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
||||||
keyStore.load(null)
|
|
||||||
keyStore.deleteEntry(KEY_NAME)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Игнорируем ошибки при удалении
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user