BIT-2291: Bypass master password reprompt when user has no password (#1301)

This commit is contained in:
David Perez authored and Álison Fernandes committed 2024-06-20 17:08:07 +01:00
1 parent b730330196
commit 0a03df75cc
25 files changed
+373 -73

No files matched your search

@@ -65,7 +65,12 @@ data class UserState(
val organizations: List<Organization>,
val isBiometricsEnabled: Boolean,
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
)
) {
/**
* Indicates that the user does or does not have a master password.
*/
val hasMasterPassword: Boolean get() = trustedDevice?.hasMasterPassword != false
}
/**
* Models the data related to trusted device encryption (TDE).
@@ -97,6 +97,7 @@ class SearchViewModel @Inject constructor(
baseIconUrl = environmentRepo.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepo.isIconLoadingDisabled,
autofillSelectionData = autofillSelectionData,
hasMasterPassword = userState.activeAccount.hasMasterPassword,
)
},
) {
@@ -640,6 +641,7 @@ class SearchViewModel @Inject constructor(
)
.toViewState(
searchTerm = state.searchTerm,
hasMasterPassword = state.hasMasterPassword,
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isAutofill = state.isAutofill,
@@ -686,6 +688,7 @@ data class SearchState(
val isIconLoadingDisabled: Boolean,
// Internal
val autofillSelectionData: AutofillSelectionData? = null,
val hasMasterPassword: Boolean,
) : Parcelable {
/**
@@ -140,6 +140,7 @@ private fun CipherView.matchedSearch(searchTerm: String): SortPriority? {
fun List<CipherView>.toViewState(
searchTerm: String,
baseIconUrl: String,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
): SearchState.ViewState =
@@ -149,6 +150,7 @@ fun List<CipherView>.toViewState(
SearchState.ViewState.Content(
displayItems = toDisplayItemList(
baseIconUrl = baseIconUrl,
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = isAutofill,
),
@@ -164,12 +166,14 @@ fun List<CipherView>.toViewState(
private fun List<CipherView>.toDisplayItemList(
baseIconUrl: String,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
): List<SearchState.DisplayItem> =
this.map {
it.toDisplayItem(
baseIconUrl = baseIconUrl,
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = isAutofill,
)
@@ -177,6 +181,7 @@ private fun List<CipherView>.toDisplayItemList(
private fun CipherView.toDisplayItem(
baseIconUrl: String,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
): SearchState.DisplayItem =
@@ -191,7 +196,7 @@ private fun CipherView.toDisplayItem(
isIconLoadingDisabled = isIconLoadingDisabled,
),
extraIconList = toLabelIcons(),
overflowOptions = toOverflowActions(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
overflowTestTag = "CipherOptionsButton",
totpCode = login?.totp,
autofillSelectionOptions = AutofillSelectionOption
@@ -749,8 +749,7 @@ class VaultItemViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
viewState = vaultDataState.toViewStateOrError(
isPremiumUser = userState.activeAccount.isPremium,
totpCodeItemData = vaultDataState.data?.totpCodeItemData,
account = userState.activeAccount,
errorText = R.string.generic_error_message.asText(),
),
)
@@ -761,8 +760,7 @@ class VaultItemViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
viewState = vaultDataState.toViewStateOrError(
isPremiumUser = userState.activeAccount.isPremium,
totpCodeItemData = vaultDataState.data.totpCodeItemData,
account = userState.activeAccount,
errorText = R.string.generic_error_message.asText(),
),
)
@@ -779,8 +777,7 @@ class VaultItemViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
viewState = vaultDataState.toViewStateOrError(
isPremiumUser = userState.activeAccount.isPremium,
totpCodeItemData = vaultDataState.data?.totpCodeItemData,
account = userState.activeAccount,
errorText = R.string.internet_connection_required_title
.asText()
.concat(
@@ -796,8 +793,7 @@ class VaultItemViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
viewState = vaultDataState.toViewStateOrError(
isPremiumUser = userState.activeAccount.isPremium,
totpCodeItemData = vaultDataState.data.totpCodeItemData,
account = userState.activeAccount,
errorText = R.string.generic_error_message.asText(),
),
)
@@ -807,13 +803,16 @@ class VaultItemViewModel @Inject constructor(
}
private fun DataState<VaultItemStateData>.toViewStateOrError(
isPremiumUser: Boolean,
totpCodeItemData: TotpCodeItemData?,
account: UserState.Account,
errorText: Text,
): VaultItemState.ViewState = this
.data
?.cipher
?.toViewState(isPremiumUser = isPremiumUser, totpCodeItemData = totpCodeItemData)
?.toViewState(
isPremiumUser = account.isPremium,
hasMasterPassword = account.hasMasterPassword,
totpCodeItemData = this.data?.totpCodeItemData,
)
?: VaultItemState.ViewState.Error(message = errorText)
private fun handleValidatePasswordReceive(
@@ -33,9 +33,10 @@ private const val FIDO2_CREDENTIAL_CREATION_TIME_PATTERN: String = "h:mm a"
/**
* Transforms [VaultData] into [VaultState.ViewState].
*/
@Suppress("LongMethod")
@Suppress("CyclomaticComplexMethod", "LongMethod")
fun CipherView.toViewState(
isPremiumUser: Boolean,
hasMasterPassword: Boolean,
totpCodeItemData: TotpCodeItemData?,
clock: Clock = Clock.systemDefaultZone(),
): VaultItemState.ViewState =
@@ -43,7 +44,7 @@ fun CipherView.toViewState(
common = VaultItemState.ViewState.Content.Common(
currentCipher = this,
name = name,
requiresReprompt = reprompt == CipherRepromptType.PASSWORD,
requiresReprompt = reprompt == CipherRepromptType.PASSWORD && hasMasterPassword,
customFields = fields.orEmpty().map { it.toCustomField() },
lastUpdated = revisionDate.toFormattedPattern(
pattern = LAST_UPDATED_DATE_TIME_PATTERN,
@@ -99,6 +99,7 @@ class VaultItemListingViewModel @Inject constructor(
.any(),
autofillSelectionData = specialCircumstance?.autofillSelectionData,
shouldFinishOnComplete = specialCircumstance?.shouldFinishWhenComplete ?: false,
hasMasterPassword = userState.activeAccount.hasMasterPassword,
)
},
) {
@@ -686,6 +687,7 @@ class VaultItemListingViewModel @Inject constructor(
vaultData.toViewState(
vaultFilterType = state.vaultFilterType,
itemListingType = listingType,
hasMasterPassword = state.hasMasterPassword,
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
autofillSelectionData = state.autofillSelectionData,
@@ -754,6 +756,7 @@ data class VaultItemListingState(
private val isPullToRefreshSettingEnabled: Boolean,
val autofillSelectionData: AutofillSelectionData? = null,
val shouldFinishOnComplete: Boolean = false,
val hasMasterPassword: Boolean,
) {
/**
* Whether or not this represents a listing screen for autofill.
@@ -84,9 +84,11 @@ sealed class ListingItemOverflowAction : Parcelable {
* Click on the edit cipher overflow option.
*/
@Parcelize
data class EditClick(val cipherId: String) : VaultAction() {
data class EditClick(
val cipherId: String,
override val requiresPasswordReprompt: Boolean,
) : VaultAction() {
override val title: Text get() = R.string.edit.asText()
override val requiresPasswordReprompt: Boolean get() = true
}
/**
@@ -102,9 +104,11 @@ sealed class ListingItemOverflowAction : Parcelable {
* Click on the copy password overflow option.
*/
@Parcelize
data class CopyPasswordClick(val password: String) : VaultAction() {
data class CopyPasswordClick(
val password: String,
override val requiresPasswordReprompt: Boolean,
) : VaultAction() {
override val title: Text get() = R.string.copy_password.asText()
override val requiresPasswordReprompt: Boolean get() = true
}
/**
@@ -120,18 +124,22 @@ sealed class ListingItemOverflowAction : Parcelable {
* Click on the copy number overflow option.
*/
@Parcelize
data class CopyNumberClick(val number: String) : VaultAction() {
data class CopyNumberClick(
val number: String,
override val requiresPasswordReprompt: Boolean,
) : VaultAction() {
override val title: Text get() = R.string.copy_number.asText()
override val requiresPasswordReprompt: Boolean get() = true
}
/**
* Click on the copy security code overflow option.
*/
@Parcelize
data class CopySecurityCodeClick(val securityCode: String) : VaultAction() {
data class CopySecurityCodeClick(
val securityCode: String,
override val requiresPasswordReprompt: Boolean,
) : VaultAction() {
override val title: Text get() = R.string.copy_security_code.asText()
override val requiresPasswordReprompt: Boolean get() = true
}
/**
@@ -91,10 +91,11 @@ fun SendView.determineListingPredicate(
/**
* Transforms a list of [CipherView] into [VaultItemListingState.ViewState].
*/
@Suppress("CyclomaticComplexMethod", "LongMethod")
@Suppress("CyclomaticComplexMethod", "LongMethod", "LongParameterList")
fun VaultData.toViewState(
itemListingType: VaultItemListingState.ItemListingType.Vault,
vaultFilterType: VaultFilterType,
hasMasterPassword: Boolean,
baseIconUrl: String,
isIconLoadingDisabled: Boolean,
autofillSelectionData: AutofillSelectionData?,
@@ -122,6 +123,7 @@ fun VaultData.toViewState(
VaultItemListingState.ViewState.Content(
displayItemList = filteredCipherViewList.toDisplayItemList(
baseIconUrl = baseIconUrl,
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = autofillSelectionData != null,
),
@@ -246,12 +248,14 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
private fun List<CipherView>.toDisplayItemList(
baseIconUrl: String,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
): List<VaultItemListingState.DisplayItem> =
this.map {
it.toDisplayItem(
baseIconUrl = baseIconUrl,
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = isAutofill,
)
@@ -270,6 +274,7 @@ private fun List<SendView>.toDisplayItemList(
private fun CipherView.toDisplayItem(
baseIconUrl: String,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
): VaultItemListingState.DisplayItem =
@@ -285,7 +290,7 @@ private fun CipherView.toDisplayItem(
),
iconTestTag = toIconTestTag(),
extraIconList = toLabelIcons(),
overflowOptions = toOverflowActions(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
optionsTestTag = "CipherOptionsButton",
isAutofill = isAutofill,
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
@@ -9,28 +9,44 @@ import com.x8bit.bitwarden.ui.vault.model.VaultTrailingIcon
/**
* Creates the list of overflow actions to be displayed for a [CipherView].
*/
fun CipherView.toOverflowActions(): List<ListingItemOverflowAction.VaultAction> =
fun CipherView.toOverflowActions(
hasMasterPassword: Boolean,
): List<ListingItemOverflowAction.VaultAction> =
this
.id
?.let { cipherId ->
listOfNotNull(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = cipherId),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = cipherId)
ListingItemOverflowAction.VaultAction.EditClick(
cipherId = cipherId,
requiresPasswordReprompt = hasMasterPassword,
)
.takeUnless { this.deletedDate != null },
this.login?.username?.let {
ListingItemOverflowAction.VaultAction.CopyUsernameClick(username = it)
},
this.login?.password
?.let { ListingItemOverflowAction.VaultAction.CopyPasswordClick(password = it) }
?.let {
ListingItemOverflowAction.VaultAction.CopyPasswordClick(
password = it,
requiresPasswordReprompt = hasMasterPassword,
)
}
.takeIf { this.viewPassword },
this.login?.totp
?.let { ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode = it) }
.takeIf { this.type == CipherType.LOGIN },
this.card?.number?.let {
ListingItemOverflowAction.VaultAction.CopyNumberClick(number = it)
ListingItemOverflowAction.VaultAction.CopyNumberClick(
number = it,
requiresPasswordReprompt = hasMasterPassword,
)
},
this.card?.code?.let {
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(securityCode = it)
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(
securityCode = it,
requiresPasswordReprompt = hasMasterPassword,
)
},
this.notes
?.let { ListingItemOverflowAction.VaultAction.CopyNoteClick(notes = it) }
@@ -83,6 +83,7 @@ class VaultViewModel @Inject constructor(
isPremium = userState.activeAccount.isPremium,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
baseIconUrl = userState.activeAccount.environment.environmentUrlData.baseIconUrl,
hasMasterPassword = userState.activeAccount.hasMasterPassword,
)
},
) {
@@ -493,6 +494,7 @@ class VaultViewModel @Inject constructor(
vaultFilterType = vaultFilterTypeOrDefault,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isPremium = state.isPremium,
hasMasterPassword = state.hasMasterPassword,
errorTitle = R.string.an_error_has_occurred.asText(),
errorMessage = R.string.generic_error_message.asText(),
)
@@ -513,6 +515,7 @@ class VaultViewModel @Inject constructor(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isPremium = state.isPremium,
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
),
dialog = null,
@@ -533,6 +536,7 @@ class VaultViewModel @Inject constructor(
isPremium = state.isPremium,
errorTitle = R.string.internet_connection_required_title.asText(),
isIconLoadingDisabled = state.isIconLoadingDisabled,
hasMasterPassword = state.hasMasterPassword,
errorMessage = R.string.internet_connection_required_message.asText(),
)
sendEvent(VaultEvent.DismissPullToRefresh)
@@ -545,6 +549,7 @@ class VaultViewModel @Inject constructor(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isPremium = state.isPremium,
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
),
)
@@ -610,6 +615,7 @@ data class VaultState(
// Internal-use properties
val isSwitchingAccounts: Boolean = false,
val isPremium: Boolean,
val hasMasterPassword: Boolean,
private val isPullToRefreshSettingEnabled: Boolean,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
@@ -1163,6 +1169,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
vaultFilterType: VaultFilterType,
isIconLoadingDisabled: Boolean,
isPremium: Boolean,
hasMasterPassword: Boolean,
errorTitle: Text,
errorMessage: Text,
) {
@@ -1172,6 +1179,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
viewState = vaultData.toViewState(
baseIconUrl = baseIconUrl,
isPremium = isPremium,
hasMasterPassword = hasMasterPassword,
vaultFilterType = vaultFilterType,
isIconLoadingDisabled = isIconLoadingDisabled,
),
@@ -35,6 +35,7 @@ private const val NO_FOLDER_ITEM_THRESHOLD: Int = 100
@Suppress("LongMethod")
fun VaultData.toViewState(
isPremium: Boolean,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
vaultFilterType: VaultFilterType,
@@ -78,6 +79,7 @@ fun VaultData.toViewState(
.filter { it.favorite }
.mapNotNull {
it.toVaultItemOrNull(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
)
@@ -110,6 +112,7 @@ fun VaultData.toViewState(
noFolderItems = noFolderItems
.mapNotNull {
it.toVaultItemOrNull(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
)
@@ -183,6 +186,7 @@ fun List<LoginUriView>?.toLoginIconData(
*/
@Suppress("MagicNumber")
private fun CipherView.toVaultItemOrNull(
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
): VaultState.ViewState.VaultItem? {
@@ -196,7 +200,7 @@ private fun CipherView.toVaultItemOrNull(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
),
overflowOptions = toOverflowActions(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
@@ -204,7 +208,7 @@ private fun CipherView.toVaultItemOrNull(
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
id = id,
name = name.asText(),
overflowOptions = toOverflowActions(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
@@ -216,7 +220,7 @@ private fun CipherView.toVaultItemOrNull(
lastFourDigits = card?.number
?.takeLast(4)
?.asText(),
overflowOptions = toOverflowActions(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
@@ -230,7 +234,7 @@ private fun CipherView.toVaultItemOrNull(
else -> "${identity?.firstName} ${identity?.lastName}"
}
?.asText(),
overflowOptions = toOverflowActions(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)