1package com.peachpasswords.ui.lock23import android.content.Context4import androidx.biometric.BiometricPrompt5import androidx.fragment.app.FragmentActivity6import androidx.lifecycle.ViewModel7import androidx.lifecycle.viewModelScope8import com.peachpasswords.sync.ForegroundSyncCoordinator9import com.peachpasswords.ui.PeachNav10import com.peachpasswords.ui.vault.entryTapDestination11import com.peachpasswords.vault.CreateVaultResult12import com.peachpasswords.ui.security.scorePassword13import com.peachpasswords.vault.DuressAction14import com.peachpasswords.vault.DuressConfigStore15import com.peachpasswords.vault.LastViewedEntryStore16import com.peachpasswords.vault.UnlockResult17import com.peachpasswords.vault.VaultCrypto18import com.peachpasswords.vault.VaultEntry19import com.peachpasswords.vault.VaultLockTimer20import com.peachpasswords.vault.VaultRepository21import dagger.hilt.android.lifecycle.HiltViewModel22import dagger.hilt.android.qualifiers.ApplicationContext23import kotlinx.coroutines.Dispatchers24import kotlinx.coroutines.async25import kotlinx.coroutines.flow.MutableStateFlow26import kotlinx.coroutines.flow.StateFlow27import kotlinx.coroutines.flow.asStateFlow28import kotlinx.coroutines.flow.update29import kotlinx.coroutines.launch30import kotlinx.coroutines.withContext31import javax.inject.Inject3233data class LockUiState(34 val hasVault: Boolean = false,35 val loading: Boolean = false,36 val error: String? = null,37 val unlocked: Boolean = false,38 39 val created: Boolean = false,40 41 val shakeNonce: Int = 0,42 43 val duressDestroyed: Boolean = false,44)4546const val MIN_PASSWORD_LENGTH = 12474849const val MIN_CREATE_STRENGTH = 250515253545556internal fun createValidationError(password: String, confirm: String, score: Int): String? = when {57 password.length < MIN_PASSWORD_LENGTH -> "password must be at least $MIN_PASSWORD_LENGTH characters"58 score < MIN_CREATE_STRENGTH -> "Password too weak — try a longer or more varied password"59 password != confirm -> "passwords do not match"60 else -> null61}626364internal fun canSubmitCreateVault(password: String, confirm: String, score: Int): Boolean =65 createValidationError(password, confirm, score) == null666768697071727374internal fun postUnlockDestination(75 lastViewedId: String?,76 resolve: (String) -> VaultEntry?,77 firstActive: VaultEntry?,78): PeachNav {79 val target = lastViewedId?.let(resolve) ?: firstActive ?: return PeachNav.VaultList80 return entryTapDestination(target)81}8283@HiltViewModel84class LockViewModel @Inject constructor(85 private val vault: VaultRepository,86 private val duress: DuressConfigStore,87 private val lockTimer: VaultLockTimer,88 private val lastViewed: LastViewedEntryStore,89 private val foregroundSync: ForegroundSyncCoordinator,90 @ApplicationContext private val appContext: Context,91) : ViewModel() {92 private val _state = MutableStateFlow(LockUiState(hasVault = vault.hasVault()))93 val state: StateFlow<LockUiState> = _state.asStateFlow()9495 96 fun postUnlockDestination(): PeachNav =97 postUnlockDestination(lastViewed.get(), vault::getEntry, vault.getActiveEntries().firstOrNull())9899 fun unlock(password: String) {100 if (password.isBlank() || _state.value.loading) return101 _state.update { it.copy(loading = true, error = null) }102 viewModelScope.launch {103 104 105 106 107 108 109 val (duressUnlock, slotA) = withContext(Dispatchers.Default) {110 val duressDeferred = async { if (duress.isConfigured()) vault.tryUnlockDuress(password) else null }111 val slotADeferred = async { vault.tryUnlockSlotA(password) }112 duressDeferred.await() to slotADeferred.await()113 }114 when {115 116 duressUnlock != null -> {117 slotA?.let(vault::discardSlotAUnlock)118 handleDuress(duressUnlock)119 }120 slotA != null -> {121 vault.commitSlotAUnlock(slotA)122 lockTimer.start()123 foregroundSync.triggerForegroundSync()124 _state.update { it.copy(loading = false, unlocked = true) }125 }126 else -> _state.update {127 it.copy(loading = false, error = "incorrect password", shakeNonce = it.shakeNonce + 1)128 }129 }130 }131 }132133 134135136137138 private suspend fun handleDuress(duressUnlock: VaultRepository.DuressUnlock) {139 when (duress.action()) {140 DuressAction.DECOY -> when (vault.commitDuressUnlock(duressUnlock)) {141 is UnlockResult.Success -> { lockTimer.start(); _state.update { it.copy(loading = false, unlocked = true) } }142 else -> _state.update {143 it.copy(loading = false, error = "incorrect password", shakeNonce = it.shakeNonce + 1)144 }145 }146 DuressAction.DESTROY -> {147 vault.discardDuressUnlock(duressUnlock)148 vault.destroyVault()149 duress.clear()150 _state.update { it.copy(loading = false, hasVault = false, duressDestroyed = true) }151 }152 }153 }154155 fun create(password: String, confirm: String) {156 if (_state.value.loading) return157 createValidationError(password, confirm, scorePassword(password))?.let { msg ->158 return _state.update { it.copy(error = msg) }159 }160 _state.update { it.copy(loading = true, error = null) }161 viewModelScope.launch {162 when (val r = vault.createVault(password)) {163 is CreateVaultResult.Success -> { lockTimer.start(); _state.update { it.copy(loading = false, unlocked = true, created = true) } }164 is CreateVaultResult.Error -> _state.update { it.copy(loading = false, error = r.reason) }165 }166 }167 }168169 fun clearError() = _state.update { it.copy(error = null) }170171 172173174175176 fun canTryBiometric(): Boolean =177 vault.hasVault() &&178 Biometrics.strongAvailable(appContext) &&179 BiometricKeyStore.hasWrappedKey(appContext)180181 182183184185186 fun biometricUnlock(activity: FragmentActivity) {187 if (_state.value.loading) return188 val cipher = BiometricKeyStore.decryptCipher(appContext) ?: return189 Biometrics.authenticateCrypto(190 activity = activity,191 title = "Unlock Peach",192 subtitle = "Use your biometric to unlock your vault",193 crypto = BiometricPrompt.CryptoObject(cipher),194 onSuccess = { result ->195 val used = result.cryptoObject?.cipher ?: return@authenticateCrypto196 val key = BiometricKeyStore.unwrap(appContext, used) ?: return@authenticateCrypto197 viewModelScope.launch {198 val r = vault.unlockWithKey(key)199 VaultCrypto.zero(key)200 when (r) {201 is UnlockResult.Success -> {202 lockTimer.start()203 foregroundSync.triggerForegroundSync()204 _state.update { it.copy(unlocked = true) }205 }206 else -> _state.update { it.copy(error = "biometric unlock failed", shakeNonce = it.shakeNonce + 1) }207 }208 }209 },210 )211 }212}