feat: Implement BiometricAuthManager for biometric authentication and password encryption/decryption
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
package com.rosetta.messenger.biometric
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
/**
|
||||
* Менеджер для работы с биометрической аутентификацией
|
||||
* Поддерживает отпечаток пальца, Face ID и другие биометрические методы
|
||||
*/
|
||||
class BiometricAuthManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val KEY_NAME = "rosetta_biometric_key"
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
private const val IV_SEPARATOR = "]"
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет доступность биометрической аутентификации на устройстве
|
||||
*/
|
||||
fun isBiometricAvailable(): BiometricAvailability {
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
|
||||
return when (biometricManager.canAuthenticate(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
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("Неизвестная ошибка")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует или получает ключ из Android Keystore
|
||||
*/
|
||||
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(
|
||||
activity: FragmentActivity,
|
||||
password: String,
|
||||
onSuccess: (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
try {
|
||||
val cipher = getCipher()
|
||||
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
|
||||
|
||||
val biometricPrompt = createBiometricPrompt(
|
||||
activity,
|
||||
onSuccess = { cryptoObject ->
|
||||
try {
|
||||
val encryptedPassword = cryptoObject.cipher?.doFinal(password.toByteArray())
|
||||
val iv = cryptoObject.cipher?.iv
|
||||
|
||||
if (encryptedPassword != null && iv != null) {
|
||||
// Сохраняем IV вместе с зашифрованными данными
|
||||
val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
|
||||
val encryptedString = Base64.encodeToString(encryptedPassword, Base64.NO_WRAP)
|
||||
onSuccess("$ivString$IV_SEPARATOR$encryptedString")
|
||||
} 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(
|
||||
activity: FragmentActivity,
|
||||
encryptedData: String,
|
||||
onSuccess: (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
try {
|
||||
// Разделяем IV и зашифрованные данные
|
||||
val parts = encryptedData.split(IV_SEPARATOR)
|
||||
if (parts.size != 2) {
|
||||
onError("Неверный формат данных")
|
||||
return
|
||||
}
|
||||
|
||||
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
||||
val encryptedPassword = Base64.decode(parts[1], Base64.NO_WRAP)
|
||||
|
||||
val cipher = getCipher()
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
|
||||
|
||||
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(
|
||||
activity: FragmentActivity,
|
||||
onSuccess: (BiometricPrompt.CryptoObject) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onCancel: () -> Unit
|
||||
): BiometricPrompt {
|
||||
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) {
|
||||
onCancel()
|
||||
} else {
|
||||
onError(errString.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
result.cryptoObject?.let { onSuccess(it) }
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
// Не вызываем onError здесь, так как пользователь может попробовать снова
|
||||
}
|
||||
}
|
||||
|
||||
return BiometricPrompt(activity, executor, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает информацию для диалога биометрической аутентификации
|
||||
*/
|
||||
private fun createPromptInfo(title: String): BiometricPrompt.PromptInfo {
|
||||
return BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(title)
|
||||
.setSubtitle("Используйте биометрию для аутентификации")
|
||||
.setNegativeButtonText("Использовать пароль")
|
||||
.setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает экземпляр Cipher для шифрования/расшифровки
|
||||
*/
|
||||
private fun getCipher(): Cipher {
|
||||
return Cipher.getInstance(TRANSFORMATION)
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет сохраненные биометрические данные
|
||||
*/
|
||||
fun removeBiometricData() {
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
||||
keyStore.load(null)
|
||||
keyStore.deleteEntry(KEY_NAME)
|
||||
} catch (e: Exception) {
|
||||
// Игнорируем ошибки при удалении
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Результат проверки доступности биометрии
|
||||
*/
|
||||
sealed class BiometricAvailability {
|
||||
object Available : BiometricAvailability()
|
||||
data class NotAvailable(val reason: String) : BiometricAvailability()
|
||||
data class NotEnrolled(val reason: String) : BiometricAvailability()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.rosetta.messenger.biometric
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private val Context.biometricDataStore: DataStore<Preferences> by preferencesDataStore(name = "biometric_prefs")
|
||||
|
||||
/**
|
||||
* Управление настройками и данными биометрической аутентификации
|
||||
*/
|
||||
class BiometricPreferences(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private val BIOMETRIC_ENABLED = booleanPreferencesKey("biometric_enabled")
|
||||
private val ENCRYPTED_PASSWORD_PREFIX = "encrypted_password_"
|
||||
}
|
||||
|
||||
/**
|
||||
* Включена ли биометрическая аутентификация
|
||||
*/
|
||||
val isBiometricEnabled: Flow<Boolean> = context.biometricDataStore.data
|
||||
.map { preferences ->
|
||||
preferences[BIOMETRIC_ENABLED] ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Включить биометрическую аутентификацию
|
||||
*/
|
||||
suspend fun enableBiometric() {
|
||||
context.biometricDataStore.edit { preferences ->
|
||||
preferences[BIOMETRIC_ENABLED] = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключить биометрическую аутентификацию
|
||||
*/
|
||||
suspend fun disableBiometric() {
|
||||
context.biometricDataStore.edit { preferences ->
|
||||
preferences[BIOMETRIC_ENABLED] = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить зашифрованный пароль для аккаунта
|
||||
*/
|
||||
suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) {
|
||||
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey")
|
||||
context.biometricDataStore.edit { preferences ->
|
||||
preferences[key] = encryptedPassword
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить зашифрованный пароль для аккаунта
|
||||
*/
|
||||
suspend fun getEncryptedPassword(publicKey: String): String? {
|
||||
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey")
|
||||
return context.biometricDataStore.data.first()[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить зашифрованный пароль для аккаунта
|
||||
*/
|
||||
suspend fun removeEncryptedPassword(publicKey: String) {
|
||||
val key = stringPreferencesKey("$ENCRYPTED_PASSWORD_PREFIX$publicKey")
|
||||
context.biometricDataStore.edit { preferences ->
|
||||
preferences.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить все биометрические данные
|
||||
*/
|
||||
suspend fun clearAll() {
|
||||
context.biometricDataStore.edit { preferences ->
|
||||
preferences.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, есть ли сохраненный зашифрованный пароль для аккаунта
|
||||
*/
|
||||
suspend fun hasEncryptedPassword(publicKey: String): Boolean {
|
||||
return getEncryptedPassword(publicKey) != null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user