BIT-1321, BIT-1014: Implement Verify PIN screen (#635)

This commit is contained in:
Brian Yencho
2024-01-16 09:46:15 -06:00
committed by Álison Fernandes
parent ca517c88c4
commit 880bdc8826
16 changed files with 612 additions and 106 deletions

View File

@@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
@@ -113,6 +114,7 @@ class AuthRepositoryImpl constructor(
vaultState = vaultState,
userOrganizationsList = userOrganizationsList,
specialCircumstance = specialCircumstance,
vaultUnlockTypeProvider = ::getVaultUnlockType,
)
}
.stateIn(
@@ -124,6 +126,7 @@ class AuthRepositoryImpl constructor(
vaultState = vaultRepository.vaultStateFlow.value,
userOrganizationsList = authDiskSource.userOrganizationsList,
specialCircumstance = mutableSpecialCircumstanceStateFlow.value,
vaultUnlockTypeProvider = ::getVaultUnlockType,
),
)
@@ -401,4 +404,17 @@ class AuthRepositoryImpl constructor(
},
)
}
private fun getVaultUnlockType(
userId: String,
): VaultUnlockType =
when {
authDiskSource.getPinProtectedUserKey(userId = userId) != null -> {
VaultUnlockType.PIN
}
else -> {
VaultUnlockType.MASTER_PASSWORD
}
}
}

View File

@@ -46,6 +46,7 @@ data class UserState(
* authentication to view their vault.
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
* @property organizations List of [Organization]s the user is associated with, if any.
* @property vaultUnlockType The mechanism by which the user's vault may be unlocked.
*/
data class Account(
val userId: String,
@@ -57,6 +58,7 @@ data class UserState(
val isLoggedIn: Boolean,
val isVaultUnlocked: Boolean,
val organizations: List<Organization>,
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
)
/**

View File

@@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* The mechanism by which the user's vault may be unlocked.
*/
enum class VaultUnlockType {
/**
* The vault must be unlocked using a master password.
*/
MASTER_PASSWORD,
/**
* The vault must be unlocked using a PIN.
*/
PIN,
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
@@ -47,6 +48,7 @@ fun UserStateJson.toUserState(
vaultState: VaultState,
userOrganizationsList: List<UserOrganizations>,
specialCircumstance: UserState.SpecialCircumstance?,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
): UserState =
UserState(
activeUserId = this.activeUserId,
@@ -72,6 +74,7 @@ fun UserStateJson.toUserState(
.find { it.userId == userId }
?.organizations
.orEmpty(),
vaultUnlockType = vaultUnlockTypeProvider(userId),
)
},
specialCircumstance = specialCircumstance,

View File

@@ -300,6 +300,7 @@ class VaultRepositoryImpl(
.also {
if (it is VaultUnlockResult.Success) {
sync()
deriveTemporaryPinProtectedUserKeyIfNecessary(userId = userId)
}
}
}
@@ -517,6 +518,33 @@ class VaultRepositoryImpl(
)
}
/**
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
* master-password unlocking on app restart. This function may then be called after such an
* unlock to derive a pin-protected user key and store it in memory for use for any subsequent
* unlocks during this current app session.
*
* If the user's vault has not yet been unlocked, this call will do nothing.
*/
private suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(userId: String) {
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
val existingPinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
if (existingPinProtectedUserKey != null) return
vaultSdkSource
.derivePinProtectedUserKey(
userId = userId,
encryptedPin = encryptedPin,
)
.onSuccess { pinProtectedUserKey ->
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
inMemoryOnly = true,
)
}
}
private fun storeProfileData(
syncResponse: SyncResponseJson,
) {

View File

@@ -30,6 +30,10 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenInputLabel
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenKeyboardType
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenMessage
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenTitle
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
@@ -116,7 +120,7 @@ fun VaultUnlockScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.verify_master_password),
title = state.vaultUnlockType.unlockScreenTitle(),
scrollBehavior = scrollBehavior,
navigationIcon = null,
actions = {
@@ -145,18 +149,19 @@ fun VaultUnlockScreen(
.verticalScroll(rememberScrollState()),
) {
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,
label = state.vaultUnlockType.unlockScreenInputLabel(),
value = state.input,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(VaultUnlockAction.PasswordInputChanged(it)) }
{ viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) }
},
keyboardType = state.vaultUnlockType.unlockScreenKeyboardType,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(id = R.string.vault_locked_master_password),
text = state.vaultUnlockType.unlockScreenMessage(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
@@ -182,7 +187,7 @@ fun VaultUnlockScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
},
isEnabled = state.passwordInput.isNotEmpty(),
isEnabled = state.input.isNotEmpty(),
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),

View File

@@ -7,9 +7,11 @@ import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenErrorMessage
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
@@ -50,7 +52,8 @@ class VaultUnlockViewModel @Inject constructor(
email = activeAccountSummary.email,
dialog = null,
environmentUrl = environmentRepo.environment.label,
passwordInput = "",
input = "",
vaultUnlockType = userState.activeAccount.vaultUnlockType,
)
},
) {
@@ -79,7 +82,7 @@ class VaultUnlockViewModel @Inject constructor(
VaultUnlockAction.AddAccountClick -> handleAddAccountClick()
VaultUnlockAction.DismissDialog -> handleDismissDialog()
VaultUnlockAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
is VaultUnlockAction.PasswordInputChanged -> handlePasswordInputChanged(action)
is VaultUnlockAction.InputChanged -> handleInputChanged(action)
is VaultUnlockAction.LockAccountClick -> handleLockAccountClick(action)
is VaultUnlockAction.LogoutAccountClick -> handleLogoutAccountClick(action)
is VaultUnlockAction.SwitchAccountClick -> handleSwitchAccountClick(action)
@@ -106,9 +109,9 @@ class VaultUnlockViewModel @Inject constructor(
authRepository.logout()
}
private fun handlePasswordInputChanged(action: VaultUnlockAction.PasswordInputChanged) {
private fun handleInputChanged(action: VaultUnlockAction.InputChanged) {
mutableStateFlow.update {
it.copy(passwordInput = action.passwordInput)
it.copy(input = action.input)
}
}
@@ -127,9 +130,19 @@ class VaultUnlockViewModel @Inject constructor(
private fun handleUnlockClick() {
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
viewModelScope.launch {
val vaultUnlockResult = vaultRepo.unlockVaultWithMasterPasswordAndSync(
mutableStateFlow.value.passwordInput,
)
val vaultUnlockResult = when (state.vaultUnlockType) {
VaultUnlockType.MASTER_PASSWORD -> {
vaultRepo.unlockVaultWithMasterPasswordAndSync(
mutableStateFlow.value.input,
)
}
VaultUnlockType.PIN -> {
vaultRepo.unlockVaultWithPinAndSync(
mutableStateFlow.value.input,
)
}
}
sendAction(VaultUnlockAction.Internal.ReceiveVaultUnlockResult(vaultUnlockResult))
}
}
@@ -142,7 +155,7 @@ class VaultUnlockViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
dialog = VaultUnlockState.VaultUnlockDialog.Error(
R.string.invalid_master_password.asText(),
state.vaultUnlockType.unlockScreenErrorMessage,
),
)
}
@@ -185,6 +198,7 @@ class VaultUnlockViewModel @Inject constructor(
avatarColorString = activeAccountSummary.avatarColorHex,
accountSummaries = accountSummaries,
email = activeAccountSummary.email,
vaultUnlockType = userState.activeAccount.vaultUnlockType,
)
}
}
@@ -201,7 +215,8 @@ data class VaultUnlockState(
val email: String,
val environmentUrl: String,
val dialog: VaultUnlockDialog?,
val passwordInput: String,
val input: String,
val vaultUnlockType: VaultUnlockType,
) : Parcelable {
/**
@@ -261,10 +276,10 @@ sealed class VaultUnlockAction {
data object ConfirmLogoutClick : VaultUnlockAction()
/**
* The user has modified the password input.
* The user has modified the input.
*/
data class PasswordInputChanged(
val passwordInput: String,
data class InputChanged(
val input: String,
) : VaultUnlockAction()
/**

View File

@@ -0,0 +1,56 @@
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util
import androidx.compose.ui.text.input.KeyboardType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* A title to use on the Vault Unlock screen.
*/
val VaultUnlockType.unlockScreenTitle: Text
get() = when (this) {
VaultUnlockType.MASTER_PASSWORD -> R.string.verify_master_password
VaultUnlockType.PIN -> R.string.verify_pin
}
.asText()
/**
* A descriptive message to use on the Vault Unlock screen.
*/
val VaultUnlockType.unlockScreenMessage: Text
get() = when (this) {
VaultUnlockType.MASTER_PASSWORD -> R.string.vault_locked_master_password
VaultUnlockType.PIN -> R.string.vault_locked_pin
}
.asText()
/**
* The label for the main text input to use on the Vault Unlock screen.
*/
val VaultUnlockType.unlockScreenInputLabel: Text
get() = when (this) {
VaultUnlockType.MASTER_PASSWORD -> R.string.master_password
VaultUnlockType.PIN -> R.string.pin
}
.asText()
/**
* The error message to use for a failed unlock on the Vault Unlock screen.
*/
val VaultUnlockType.unlockScreenErrorMessage: Text
get() = when (this) {
VaultUnlockType.MASTER_PASSWORD -> R.string.invalid_master_password
VaultUnlockType.PIN -> R.string.invalid_pin
}
.asText()
/**
* The [KeyboardType] to use on the input on the Vault Unlock screen.
*/
val VaultUnlockType.unlockScreenKeyboardType: KeyboardType
get() = when (this) {
VaultUnlockType.MASTER_PASSWORD -> KeyboardType.Password
VaultUnlockType.PIN -> KeyboardType.Number
}

View File

@@ -191,12 +191,10 @@ class AccountSecurityViewModel @Inject constructor(
it.copy(isUnlockWithPinEnabled = action.isUnlockWithPinEnabled)
}
// TODO: Complete implementation (BIT-465)
when (action) {
AccountSecurityAction.UnlockWithPinToggle.PendingEnabled -> Unit
AccountSecurityAction.UnlockWithPinToggle.Disabled -> {
settingsRepository.clearUnlockPin()
sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()))
}
is AccountSecurityAction.UnlockWithPinToggle.Enabled -> {
@@ -205,7 +203,6 @@ class AccountSecurityViewModel @Inject constructor(
shouldRequireMasterPasswordOnRestart =
action.shouldRequireMasterPasswordOnRestart,
)
sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()))
}
}
}