mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
438 lines
16 KiB
Kotlin
438 lines
16 KiB
Kotlin
package com.kunzisoft.keepass.viewmodels
|
|
|
|
import android.app.Application
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import androidx.activity.result.ActivityResult
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.biometric.BiometricManager
|
|
import androidx.biometric.BiometricPrompt
|
|
import androidx.lifecycle.AndroidViewModel
|
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPrompt
|
|
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPromptType
|
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
|
import com.kunzisoft.keepass.biometric.DeviceUnlockMode
|
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
|
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
|
import com.kunzisoft.keepass.model.CredentialStorage
|
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.flow.update
|
|
import javax.crypto.Cipher
|
|
|
|
class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) {
|
|
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
|
|
|
private var isConditionToStoreCredentialVerified: Boolean = false
|
|
|
|
private var deviceUnlockManager: DeviceUnlockManager? = null
|
|
private var databaseUri: Uri? = null
|
|
|
|
private var deviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
|
var cryptoPrompt: DeviceUnlockCryptoPrompt? = null
|
|
|
|
// TODO Retrieve credential storage from app database
|
|
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
|
|
|
val cipherDatabaseAction = CipherDatabaseAction.getInstance(getApplication())
|
|
|
|
private val _uiState = MutableStateFlow(DeviceUnlockState())
|
|
val uiState: StateFlow<DeviceUnlockState> = _uiState
|
|
|
|
fun checkConditionToStoreCredential(condition: Boolean, databaseFileUri: Uri?) {
|
|
isConditionToStoreCredentialVerified = condition
|
|
checkUnlockAvailability(databaseFileUri)
|
|
}
|
|
|
|
/**
|
|
* Check unlock availability by verifying device settings and database mode
|
|
*/
|
|
fun checkUnlockAvailability() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipherDatabase ->
|
|
if (PreferencesUtil.isBiometricUnlockEnable(getApplication())) {
|
|
// biometric not supported (by API level or hardware) so keep option hidden
|
|
// or manually disable
|
|
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(getApplication())
|
|
if (!PreferencesUtil.isAdvancedUnlockEnable(getApplication())
|
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
|
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
|
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
|
changeMode(DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
|
} else {
|
|
// biometric is available but not configured, show icon but in disabled state with some information
|
|
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
|
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
|
} else {
|
|
selectMode(containsCipherDatabase)
|
|
}
|
|
}
|
|
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(getApplication())) {
|
|
if (DeviceUnlockManager.isDeviceSecure(getApplication())) {
|
|
selectMode(containsCipherDatabase)
|
|
} else {
|
|
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check unlock availability and change the current mode depending of device's state
|
|
*/
|
|
fun checkUnlockAvailability(databaseFileUri: Uri?) {
|
|
databaseUri = databaseFileUri
|
|
checkUnlockAvailability()
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
fun selectMode(containsCipherDatabase: Boolean) {
|
|
try {
|
|
if (isConditionToStoreCredentialVerified) {
|
|
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
|
// listen for encryption
|
|
changeMode(DeviceUnlockMode.STORE_CREDENTIAL)
|
|
initEncryptData()
|
|
} else if (containsCipherDatabase) {
|
|
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
|
// biometric available but no stored password found yet for this DB
|
|
// listen for decryption
|
|
changeMode(DeviceUnlockMode.EXTRACT_CREDENTIAL)
|
|
initDecryptData()
|
|
} else {
|
|
// wait for typing
|
|
changeMode(DeviceUnlockMode.WAIT_CREDENTIAL)
|
|
}
|
|
} catch (e: Exception) {
|
|
changeMode(DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE)
|
|
setException(e)
|
|
}
|
|
}
|
|
|
|
fun connect(databaseUri: Uri) {
|
|
this.databaseUri = databaseUri
|
|
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
|
override fun onCipherDatabaseCleared() {
|
|
closeBiometricPrompt()
|
|
checkUnlockAvailability(databaseUri)
|
|
}
|
|
}
|
|
cipherDatabaseAction.apply {
|
|
reloadPreferences()
|
|
cipherDatabaseListener?.let {
|
|
registerDatabaseListener(it)
|
|
}
|
|
}
|
|
checkUnlockAvailability(databaseUri)
|
|
}
|
|
|
|
fun disconnect() {
|
|
this.databaseUri = null
|
|
closeBiometricPrompt()
|
|
cipherDatabaseListener?.let {
|
|
cipherDatabaseAction.unregisterDatabaseListener(it)
|
|
}
|
|
reset()
|
|
}
|
|
|
|
fun databaseFileLoaded(databaseUri: Uri?) {
|
|
// To get device credential unlock result, only if same database uri
|
|
if (databaseUri != null
|
|
&& PreferencesUtil.isAdvancedUnlockEnable(getApplication())) {
|
|
if (databaseUri != this.databaseUri) {
|
|
connect(databaseUri)
|
|
}
|
|
} else {
|
|
disconnect()
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
fun onAuthenticationSucceeded() {
|
|
cryptoPrompt?.let { prompt ->
|
|
when (prompt.type) {
|
|
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
|
retrieveCredentialForEncryption( prompt.cipher)
|
|
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
|
decryptCredential( prompt.cipher)
|
|
}
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
fun onAuthenticationSucceeded(
|
|
result: BiometricPrompt.AuthenticationResult
|
|
) {
|
|
cryptoPrompt?.type?.let { type ->
|
|
when (type) {
|
|
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
|
retrieveCredentialForEncryption(result.cryptoObject?.cipher)
|
|
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
|
decryptCredential(result.cryptoObject?.cipher)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun retrieveCredentialForEncryption(cipher: Cipher?) {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
credentialRequiredCipher = cipher
|
|
)
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
fun encryptCredential(
|
|
credential: ByteArray,
|
|
cipher: Cipher?
|
|
) {
|
|
try {
|
|
deviceUnlockManager?.encryptData(
|
|
value = credential,
|
|
cipher = cipher,
|
|
handleEncryptedResult = { encryptedValue, ivSpec ->
|
|
databaseUri?.let { databaseUri ->
|
|
onCredentialEncrypted(
|
|
CipherEncryptDatabase().apply {
|
|
this.databaseUri = databaseUri
|
|
this.credentialStorage = credentialDatabaseStorage
|
|
this.encryptedValue = encryptedValue
|
|
this.specParameters = ivSpec
|
|
}
|
|
)
|
|
}
|
|
}
|
|
)
|
|
} catch (e: Exception) {
|
|
setException(e)
|
|
} finally {
|
|
// Reinit credential storage request
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
credentialRequiredCipher = null
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
fun decryptCredential(cipher: Cipher?) {
|
|
// retrieve the encrypted value from preferences
|
|
databaseUri?.let { databaseUri ->
|
|
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
|
cipherDatabase?.encryptedValue?.let { encryptedCredential ->
|
|
try {
|
|
deviceUnlockManager?.decryptData(
|
|
encryptedValue = encryptedCredential,
|
|
cipher = cipher,
|
|
handleDecryptedResult = { decryptedValue ->
|
|
// Load database directly with password retrieve
|
|
onCredentialDecrypted(
|
|
CipherDecryptDatabase().apply {
|
|
this.databaseUri = databaseUri
|
|
this.credentialStorage = credentialDatabaseStorage
|
|
this.decryptedValue = decryptedValue
|
|
}
|
|
)
|
|
cipherDatabaseAction.resetCipherParameters(databaseUri)
|
|
}
|
|
)
|
|
} catch (e: Exception) {
|
|
setException(e)
|
|
}
|
|
} ?: deleteEncryptedDatabaseKey()
|
|
}
|
|
} ?: run {
|
|
setException(UnknownDatabaseLocationException())
|
|
}
|
|
}
|
|
|
|
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cipherEncryptDatabase = cipherEncryptDatabase
|
|
)
|
|
}
|
|
}
|
|
|
|
fun consumeCredentialEncrypted() {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cipherEncryptDatabase = null
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cipherDecryptDatabase = cipherDecryptDatabase
|
|
)
|
|
}
|
|
}
|
|
|
|
fun consumeCredentialDecrypted() {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cipherDecryptDatabase = null
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onPromptRequested(
|
|
cryptoPrompt: DeviceUnlockCryptoPrompt,
|
|
autoOpen: Boolean = false
|
|
) {
|
|
this@DeviceUnlockViewModel.cryptoPrompt = cryptoPrompt
|
|
if (autoOpen && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication()))
|
|
showPrompt()
|
|
}
|
|
|
|
fun showPrompt() {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cryptoPromptState = DeviceUnlockPromptMode.SHOW
|
|
)
|
|
}
|
|
}
|
|
|
|
fun promptShown() {
|
|
isAutoOpenBiometricPromptAllowed = false
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cryptoPromptState = DeviceUnlockPromptMode.IDLE
|
|
)
|
|
}
|
|
}
|
|
|
|
fun setException(value: Exception?) {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
exception = value
|
|
)
|
|
}
|
|
}
|
|
|
|
fun exceptionShown() {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
exception = null
|
|
)
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
private fun initEncryptData() {
|
|
try {
|
|
deviceUnlockManager?.initEncryptData { cryptoPrompt ->
|
|
onPromptRequested(cryptoPrompt)
|
|
} ?: setException(Exception("AdvancedUnlockManager not initialized"))
|
|
} catch (e: Exception) {
|
|
setException(e)
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
private fun initDecryptData() {
|
|
databaseUri?.let { databaseUri ->
|
|
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
|
cipherDatabase?.let {
|
|
try {
|
|
deviceUnlockManager?.initDecryptData(cipherDatabase.specParameters) { cryptoPrompt ->
|
|
onPromptRequested(
|
|
cryptoPrompt,
|
|
autoOpen = isAutoOpenBiometricPromptAllowed
|
|
)
|
|
} ?: setException(Exception("AdvancedUnlockManager not initialized"))
|
|
} catch (e: Exception) {
|
|
setException(e)
|
|
}
|
|
} ?: deleteEncryptedDatabaseKey()
|
|
}
|
|
} ?: setException(UnknownDatabaseLocationException())
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.M)
|
|
private fun changeMode(deviceUnlockMode: DeviceUnlockMode) {
|
|
this.deviceUnlockMode = deviceUnlockMode
|
|
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
newDeviceUnlockMode = deviceUnlockMode,
|
|
allowAdvancedUnlockMenu = containsCipher
|
|
&& deviceUnlockMode != DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
|
&& deviceUnlockMode != DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun deleteEncryptedDatabaseKey() {
|
|
closeBiometricPrompt()
|
|
databaseUri?.let { databaseUri ->
|
|
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
|
checkUnlockAvailability(databaseUri)
|
|
}
|
|
} ?: checkUnlockAvailability(null)
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
allowAdvancedUnlockMenu = false
|
|
)
|
|
}
|
|
}
|
|
|
|
fun closeBiometricPrompt() {
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cryptoPromptState = DeviceUnlockPromptMode.CLOSE
|
|
)
|
|
}
|
|
}
|
|
|
|
fun biometricPromptClosed() {
|
|
cryptoPrompt = null
|
|
_uiState.update { currentState ->
|
|
currentState.copy(
|
|
cryptoPromptState = DeviceUnlockPromptMode.IDLE
|
|
)
|
|
}
|
|
}
|
|
|
|
fun reset() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
|
}
|
|
}
|
|
|
|
override fun onCleared() {
|
|
super.onCleared()
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
deviceUnlockManager = null
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
var isAutoOpenBiometricPromptAllowed = true
|
|
}
|
|
}
|
|
|
|
enum class DeviceUnlockPromptMode {
|
|
IDLE, SHOW, CLOSE
|
|
}
|
|
|
|
data class DeviceUnlockState(
|
|
val newDeviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE,
|
|
val allowAdvancedUnlockMenu: Boolean = false,
|
|
val credentialRequiredCipher: Cipher? = null,
|
|
val cipherEncryptDatabase: CipherEncryptDatabase? = null,
|
|
val cipherDecryptDatabase: CipherDecryptDatabase? = null,
|
|
val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE,
|
|
val autoOpenPrompt: Boolean = false,
|
|
val exception: Exception? = null
|
|
) |