Skip to content

Commit

Permalink
This PR adds the TOTP matching flow to the app (#4042)
Browse files Browse the repository at this point in the history
  • Loading branch information
david-livefront authored Oct 8, 2024
1 parent 641a48f commit e745017
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 69 deletions.
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="otpauth" />
<data android:host="totp" />
</intent-filter>
</activity>

<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toTotpDataOrNull
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
Expand Down Expand Up @@ -53,6 +54,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
Expand Down Expand Up @@ -108,47 +110,44 @@ class VaultAddEditViewModel @Inject constructor(
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
.any()

val specialCircumstance = specialCircumstanceManager.specialCircumstance
// Check for autofill data to pre-populate
val autofillSaveItem = specialCircumstanceManager
.specialCircumstance
?.toAutofillSaveItemOrNull()
val autofillSelectionData = specialCircumstanceManager
.specialCircumstance
?.toAutofillSelectionDataOrNull()

val fido2CreationRequest = specialCircumstanceManager
.specialCircumstance
?.toFido2RequestOrNull()

val fido2AttestationOptions = fido2CreationRequest
?.let { request ->
fido2CredentialManager
.getPasskeyAttestationOptionsOrNull(request.requestJson)
}

val dialogState =
if (!settingsRepository.initialAutofillDialogShown &&
vaultAddEditType is VaultAddEditType.AddItem &&
autofillSelectionData == null
) {
VaultAddEditState.DialogState.InitialAutofillPrompt
} else {
null
}
val autofillSaveItem = specialCircumstance?.toAutofillSaveItemOrNull()
val autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull()
// Check for totp data to pre-populate
val totpData = specialCircumstance?.toTotpDataOrNull()
// Check for Fido2 data to pre-populate
val fido2CreationRequest = specialCircumstance?.toFido2RequestOrNull()
val fido2AttestationOptions = fido2CreationRequest?.let { request ->
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(request.requestJson)
}

// Exit on save if handling an autofill, Fido2 Attestation, or TOTP link
val shouldExitOnSave = autofillSaveItem != null ||
fido2AttestationOptions != null ||
totpData != null

val dialogState = if (!settingsRepository.initialAutofillDialogShown &&
vaultAddEditType is VaultAddEditType.AddItem &&
autofillSelectionData == null
) {
VaultAddEditState.DialogState.InitialAutofillPrompt
} else {
null
}

VaultAddEditState(
vaultAddEditType = vaultAddEditType,
viewState = when (vaultAddEditType) {
is VaultAddEditType.AddItem -> {
autofillSelectionData
?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: autofillSaveItem
?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: fido2CreationRequest
?.toDefaultAddTypeContent(
attestationOptions = fido2AttestationOptions,
isIndividualVaultDisabled = isIndividualVaultDisabled,
)
?: autofillSaveItem?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: fido2CreationRequest?.toDefaultAddTypeContent(
attestationOptions = fido2AttestationOptions,
isIndividualVaultDisabled = isIndividualVaultDisabled,
)
?: totpData?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
isIndividualVaultDisabled = isIndividualVaultDisabled,
Expand All @@ -160,9 +159,10 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditType.CloneItem -> VaultAddEditState.ViewState.Loading
},
dialog = dialogState,
totpData = totpData,
// Set special conditions for autofill and fido2 save
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
shouldExitOnSave = autofillSaveItem != null || fido2AttestationOptions != null,
shouldExitOnSave = shouldExitOnSave,
)
},
) {
Expand Down Expand Up @@ -1412,18 +1412,14 @@ class VaultAddEditViewModel @Inject constructor(
}

is CreateCipherResult.Success -> {
specialCircumstanceManager.specialCircumstance = null
if (state.shouldExitOnSave) {
specialCircumstanceManager.specialCircumstance = null
sendEvent(
event = VaultAddEditEvent.ExitApp,
)
sendEvent(event = VaultAddEditEvent.ExitApp)
} else {
sendEvent(
event = VaultAddEditEvent.ShowToast(R.string.new_item_created.asText()),
)
sendEvent(
event = VaultAddEditEvent.NavigateBack,
)
sendEvent(event = VaultAddEditEvent.NavigateBack)
}
}
}
Expand All @@ -1444,10 +1440,13 @@ class VaultAddEditViewModel @Inject constructor(
}

is UpdateCipherResult.Success -> {
sendEvent(
event = VaultAddEditEvent.ShowToast(R.string.item_updated.asText()),
)
sendEvent(VaultAddEditEvent.NavigateBack)
specialCircumstanceManager.specialCircumstance = null
if (state.shouldExitOnSave) {
sendEvent(event = VaultAddEditEvent.ExitApp)
} else {
sendEvent(event = VaultAddEditEvent.ShowToast(R.string.item_updated.asText()))
sendEvent(event = VaultAddEditEvent.NavigateBack)
}
}
}
}
Expand Down Expand Up @@ -1544,15 +1543,19 @@ class VaultAddEditViewModel @Inject constructor(
) { currentAccount, cipherView ->
// Derive the view state from the current Cipher for Edit mode
// or use the current state for Add
(cipherView?.toViewState(
isClone = isCloneMode,
isIndividualVaultDisabled = isIndividualVaultDisabled,
resourceManager = resourceManager,
clock = clock,
) ?: viewState)
(cipherView
?.toViewState(
isClone = isCloneMode,
isIndividualVaultDisabled = isIndividualVaultDisabled,
totpData = totpData,
resourceManager = resourceManager,
clock = clock,
)
?: viewState)
.appendFolderAndOwnerData(
folderViewList = vaultData.folderViewList,
collectionViewList = vaultData.collectionViewList
collectionViewList = vaultData
.collectionViewList
.filter { !it.readOnly },
activeAccount = currentAccount,
isIndividualVaultDisabled = isIndividualVaultDisabled,
Expand Down Expand Up @@ -1911,6 +1914,7 @@ data class VaultAddEditState(
val shouldShowCloseButton: Boolean = true,
// Internal
val shouldExitOnSave: Boolean = false,
val totpData: TotpData? = null,
) : Parcelable {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
Expand All @@ -37,6 +38,7 @@ private const val PASSKEY_CREATION_TIME_PATTERN: String = "hh:mm a"
fun CipherView.toViewState(
isClone: Boolean,
isIndividualVaultDisabled: Boolean,
totpData: TotpData?,
resourceManager: ResourceManager,
clock: Clock,
): VaultAddEditState.ViewState =
Expand All @@ -46,7 +48,7 @@ fun CipherView.toViewState(
VaultAddEditState.ViewState.Content.ItemType.Login(
username = login?.username.orEmpty(),
password = login?.password.orEmpty(),
totp = login?.totp,
totp = totpData?.uri ?: login?.totp,
canViewPassword = this.viewPassword,
canEditItem = this.edit,
uriList = login?.uris.toUriItems(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util

import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.model.TotpData

/**
* Returns pre-filled content that may be used for an "add" type
* [VaultAddEditState.ViewState.Content] during a TOTP creation event.
*/
fun TotpData.toDefaultAddTypeContent(
isIndividualVaultDisabled: Boolean,
): VaultAddEditState.ViewState.Content = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
name = (this.issuer ?: this.accountName).orEmpty(),
),
isIndividualVaultDisabled = isIndividualVaultDisabled,
type = VaultAddEditState.ViewState.Content.ItemType.Login(totp = this.uri),
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
Expand All @@ -27,6 +28,7 @@ import com.x8bit.bitwarden.ui.platform.components.listitem.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.toIconResources
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenPolicyWarningText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import kotlinx.collections.immutable.toPersistentList

Expand All @@ -38,6 +40,7 @@ import kotlinx.collections.immutable.toPersistentList
fun VaultItemListingContent(
state: VaultItemListingState.ViewState.Content,
policyDisablesSend: Boolean,
showAddTotpBanner: Boolean,
collectionClick: (id: String) -> Unit,
folderClick: (id: String) -> Unit,
vaultItemClick: (id: String) -> Unit,
Expand Down Expand Up @@ -100,8 +103,23 @@ fun VaultItemListingContent(
LazyColumn(
modifier = modifier,
) {
item {
if (showAddTotpBanner) {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenPolicyWarningText(
text = stringResource(id = R.string.add_this_authenticator_key_to_a_login),
style = BitwardenTheme.typography.bodyMedium,
textAlign = TextAlign.Start,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
}

item {
if (policyDisablesSend) {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenPolicyWarningText(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting

import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
Expand Down Expand Up @@ -45,11 +46,13 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInputDialog
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingUserVerificationHandlers
Expand All @@ -74,6 +77,7 @@ fun VaultItemListingScreen(
onNavigateToEditSendItem: (sendId: String) -> Unit,
onNavigateToSearch: (searchType: SearchType) -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
exitManager: ExitManager = LocalExitManager.current,
fido2CompletionManager: Fido2CompletionManager = LocalFido2CompletionManager.current,
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
viewModel: VaultItemListingViewModel = hiltViewModel(),
Expand Down Expand Up @@ -168,6 +172,8 @@ fun VaultItemListingScreen(
is VaultItemListingEvent.CompleteFido2GetCredentialsRequest -> {
fido2CompletionManager.completeFido2GetCredentialRequest(event.result)
}

VaultItemListingEvent.ExitApp -> exitManager.exitApplication()
}
}

Expand Down Expand Up @@ -252,12 +258,15 @@ fun VaultItemListingScreen(
},
)

val vaultItemListingHandlers = remember(viewModel) {
VaultItemListingHandlers.create(viewModel)
}

BackHandler(onBack = vaultItemListingHandlers.backClick)
VaultItemListingScaffold(
state = state,
pullToRefreshState = pullToRefreshState,
vaultItemListingHandlers = remember(viewModel) {
VaultItemListingHandlers.create(viewModel)
},
vaultItemListingHandlers = vaultItemListingHandlers,
)
}

Expand Down Expand Up @@ -451,6 +460,7 @@ private fun VaultItemListingScaffold(
is VaultItemListingState.ViewState.Content -> {
VaultItemListingContent(
state = state.viewState,
showAddTotpBanner = state.isTotp,
policyDisablesSend = state.policyDisablesSend &&
state.itemListingType is VaultItemListingState.ItemListingType.Send,
vaultItemClick = vaultItemListingHandlers.itemClick,
Expand Down
Loading

0 comments on commit e745017

Please sign in to comment.