Skip to content

[PM-32009] feat: Add infrastructure for new vault item types#6828

Draft
SaintPatrck wants to merge 2 commits intomainfrom
new-item-types/phase-01-04_infrastructure
Draft

[PM-32009] feat: Add infrastructure for new vault item types#6828
SaintPatrck wants to merge 2 commits intomainfrom
new-item-types/phase-01-04_infrastructure

Conversation

@SaintPatrck
Copy link
Copy Markdown
Contributor

@SaintPatrck SaintPatrck commented Apr 23, 2026

🎟️ Tracking

PM-32009

📔 Objective

First of three stacked PRs introducing Bank Account, Driver's License, and Passport cipher types. This PR lands the foundation only — network/model plumbing, enums, feature-flag registration, and exhaustive-when branches — so the UI and vault-integration PRs can be reviewed in isolation on top of it.

The entire effort ships behind the pm-32009-new-item-types feature flag so infrastructure can merge well ahead of user-visible surface area.

Why it looks the way it does

  • SDK decryption gracefully skips unsupported types. The Bitwarden SDK does not yet expose types for these ciphers, so sync could otherwise fail open on accounts that already hold server-side rows with the new CipherType values. The mapping layer defensively drops unknown types instead of throwing, keeping sync resilient while SDK variants are being added.
  • Exhaustive when branches are updated across every affected ViewModel. Adding the enum values forces compile-time coverage everywhere — reviewers can trust that no call site silently falls into an else.
  • LinkedIdTypeJson entries (600–812) mirror the server's contract so linked-field resolution works the moment data arrives, without a second round-trip PR.

Stacked on: main
Followed by: new-item-types/phase-05-07_cipher-type-ui (UI screens)

Register pm-32009-new-item-types feature flag and add foundational
support for Bank Account, Driver's License, and Passport cipher types
across the network model, SDK mapping, and UI layers.

Network: CipherTypeJson enum values (6-8), SyncResponseJson nested data
classes, CipherJsonRequest fields, LinkedIdTypeJson entries (600-812).
SDK: Defensive mapping that gracefully skips unsupported cipher types
during sync decryption (SDK types not yet available).
UI: VaultBankAccountType enum, VaultItemCipherType/CreateVaultItemType
extensions, ItemType state classes in VaultAddEditViewModel, string
resources, and exhaustive when-branch updates across ViewModels.
C1: Gate new item types behind pm-32009-new-item-types feature flag
in VaultViewModel and VaultItemListingViewModel excluded options.
I1: Add Timber.w logging when cipher conversion fails during sync.
I2: Add isSdkSupported property to ItemType and guard save path in
VaultAddEditViewModel to prevent crash on unsupported types.
@SaintPatrck SaintPatrck added the ai-review-vnext Request a Claude code review using the vNext workflow label Apr 23, 2026
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:feature Change Type - Feature Development labels Apr 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Reviewed the network/model/enum/feature-flag foundation for Bank Account, Driver's License, and Passport cipher types in :network, :core, :app, and :ui. The exhaustive-when coverage is thorough, feature-flag gating in VaultViewModel and VaultItemListingViewModel looks correct, and the save-path guard via isSdkSupported is a sensible safety net while SDK support is catching up. One issue undermines the PR description's "defensively drops unknown types" promise: the single-cipher mapping path still throws.

Code Review Details
  • ⚠️ : Single-cipher toSdkCipherType() throws; callers outside toEncryptedSdkCipherList are not defensive and can crash on new-type ciphers.
    • app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt:624

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 14.87603% with 103 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.32%. Comparing base (c01fc62) to head (5f659d4).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
.../ui/vault/feature/addedit/VaultAddEditViewModel.kt 1.72% 56 Missing and 1 partial ⚠️
...t/bitwarden/ui/vault/model/VaultBankAccountType.kt 0.00% 15 Missing ⚠️
...i/vault/feature/addedit/VaultAddEditItemContent.kt 0.00% 3 Missing and 3 partials ⚠️
...t/feature/itemlisting/VaultItemListingViewModel.kt 33.33% 6 Missing ⚠️
.../feature/vault/util/VaultAddItemStateExtensions.kt 0.00% 3 Missing and 1 partial ⚠️
.../vault/repository/util/VaultSdkCipherExtensions.kt 72.72% 1 Missing and 2 partials ⚠️
...ult/feature/addedit/util/VaultAddEditExtensions.kt 0.00% 3 Missing ⚠️
...warden/ui/vault/feature/item/VaultItemViewModel.kt 0.00% 3 Missing ⚠️
...bitwarden/ui/vault/feature/vault/VaultViewModel.kt 50.00% 3 Missing ⚠️
...it/bitwarden/ui/vault/model/VaultItemCipherType.kt 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6828      +/-   ##
==========================================
- Coverage   85.58%   85.32%   -0.27%     
==========================================
  Files         830      834       +4     
  Lines       61509    59179    -2330     
  Branches     8592     8611      +19     
==========================================
- Hits        52642    50492    -2150     
+ Misses       5900     5708     -192     
- Partials     2967     2979      +12     
Flag Coverage Δ
app-data 17.40% <7.76%> (+0.04%) ⬆️
app-ui-auth-tools 20.25% <0.00%> (-0.07%) ⬇️
app-ui-platform 15.45% <0.00%> (-0.06%) ⬇️
app-ui-vault 25.83% <9.70%> (-0.06%) ⬇️
authenticator 6.67% <0.00%> (+<0.01%) ⬆️
lib-core-network-bridge 4.25% <0.00%> (+0.03%) ⬆️
lib-data-ui 1.02% <0.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Copy Markdown
Contributor

Logo
Checkmarx One – Scan Summary & Detailsa1e43a9e-c96d-45c1-9f61-ad0d32623c04

Great job! No new security vulnerabilities introduced in this pull request

Comment on lines +621 to +624
CipherTypeJson.BANK_ACCOUNT,
CipherTypeJson.DRIVERS_LICENSE,
CipherTypeJson.PASSPORT,
-> throw IllegalArgumentException("SDK mapping not yet available for $this")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Single-cipher toSdkCipherType() throws; callers outside toEncryptedSdkCipherList are not defensive and can crash on new-type ciphers.

Details and fix

The PR description states "the mapping layer defensively drops unknown types instead of throwing, keeping sync resilient." That promise only holds for toEncryptedSdkCipherList (line 389–394). The single-item path still throws via toSdkCipherType(), and several call sites invoke toEncryptedSdkCipher() directly without a runCatching wrapper:

  • SdkCipherRepository.list()map { it.toEncryptedSdkCipher() } (line 27). The SDK invokes this during its own crypto operations.
  • SdkCipherRepository.get(id)?.toEncryptedSdkCipher() (line 20).
  • VaultRepositoryImpl.exportVaultDataToString.map { it.toEncryptedSdkCipher() } (line 457).
  • VaultRepositoryImpl.exportVaultDataToCxf.map { it.toEncryptedSdkCipher() } (line 518).
  • CipherManagerImpl.getCiphersyncResponseCipher.toEncryptedSdkCipher() (line 335).

Trigger path: A web vault with the server-side flag enabled stores a bank account cipher. Mobile syncs (storing the CipherTypeJson.BANK_ACCOUNT row in vaultDiskSource regardless of the mobile feature flag). User then exports their vault or the SDK enumerates ciphers for a crypto op → IllegalArgumentException propagates.

Suggested fix: either (a) filter new types out of the disk source before they reach these call sites, or (b) make SdkCipherRepository.list()/get() and the export paths use the same mapNotNull { runCatching { ... }.getOrNull() } pattern, or (c) relocate the exhaustive branch to a non-throwing toSdkCipherTypeOrNull() and adjust callers to skip nulls. This aligns the runtime behavior with the resilience claim in the PR description.

Comment on lines +621 to +624
CipherTypeJson.BANK_ACCOUNT,
CipherTypeJson.DRIVERS_LICENSE,
CipherTypeJson.PASSPORT,
-> throw IllegalArgumentException("SDK mapping not yet available for $this")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Single-cipher toSdkCipherType() throws; callers outside toEncryptedSdkCipherList are not defensive and can crash on new-type ciphers.

Details and fix

The PR description states "the mapping layer defensively drops unknown types instead of throwing, keeping sync resilient." That promise only holds for toEncryptedSdkCipherList (line 389–394). The single-item path still throws via toSdkCipherType(), and several call sites invoke toEncryptedSdkCipher() directly without a runCatching wrapper:

  • SdkCipherRepository.list()map { it.toEncryptedSdkCipher() } (line 27). The SDK invokes this during its own crypto operations.
  • SdkCipherRepository.get(id)?.toEncryptedSdkCipher() (line 20).
  • VaultRepositoryImpl.exportVaultDataToString.map { it.toEncryptedSdkCipher() } (line 457).
  • VaultRepositoryImpl.exportVaultDataToCxf.map { it.toEncryptedSdkCipher() } (line 518).
  • CipherManagerImpl.getCiphersyncResponseCipher.toEncryptedSdkCipher() (line 335).

Trigger path: A web vault with the server-side flag enabled stores a bank account cipher. Mobile syncs (storing the CipherTypeJson.BANK_ACCOUNT row in vaultDiskSource regardless of the mobile feature flag). User then exports their vault or the SDK enumerates ciphers for a crypto op → IllegalArgumentException propagates.

Suggested fix: either (a) filter new types out of the disk source before they reach these call sites, or (b) make SdkCipherRepository.list()/get() and the export paths use the same mapNotNull { runCatching { ... }.getOrNull() } pattern, or (c) relocate the exhaustive branch to a non-throwing toSdkCipherTypeOrNull() and adjust callers to skip nulls. This aligns the runtime behavior with the resilience claim in the PR description.

fun List<SyncResponseJson.Cipher>.toEncryptedSdkCipherList(): List<Cipher> =
map { it.toEncryptedSdkCipher() }
mapNotNull {
runCatching { it.toEncryptedSdkCipher() }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this need to be wrapped in runCatching

VaultItemCipherType.DRIVERS_LICENSE ->
VaultAddEditState.ViewState.Content.ItemType.DriversLicense()
VaultItemCipherType.PASSPORT ->
VaultAddEditState.ViewState.Content.ItemType.Passport()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ding on formatting

.entries
.find { vaultBankAccountType ->
vaultBankAccountType.name.lowercaseWithoutSpacesOrUnderscores ==
this.lowercaseWithoutSpacesOrUnderscores
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just give these values?

enum class VaultBankAccountType(val value: String) {
    SELECT(value = "select"),
    ...

    companion object {
        fun parse(value: String?): VaultBankAccountType =
            VaultBankAccountType
                .entries
                .firstOrNull { it.value.equals(other = value, ignoreCase = true) }
                ?: VaultBankAccountType.OTHER
    }
}

val nameOnAccount: String?,

@SerialName("accountType")
val accountType: String?,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be enumerated?

sshKey: SyncResponseJson.Cipher.SshKey? = createMockSshKey(number = number),
bankAccount: SyncResponseJson.Cipher.BankAccount? = null,
driversLicense: SyncResponseJson.Cipher.DriversLicense? = null,
passport: SyncResponseJson.Cipher.Passport? = null,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the helper functions?

fun createMockBankAccount(
    number: Int,
    bankName: String? = "mockBankName-$number",
    nameOnAccount: String? = "mockNameOnAccount-$number",
    accountType: String? = "mockAccountType-$number",
    accountNumber: String? = "mockAccountNumber-$number",
    routingNumber: String? = "mockRoutingNumber-$number",
    branchNumber: String? = "mockBranchNumber-$number",
    pin: String? = "mockPin-$number",
    swiftCode: String? = "mokSwiftCode-$number",
    iban: String? = "mockIban-$number",
    bankContactPhone: String? = "mockBankContractPhone-$number",
): SyncResponseJson.Cipher.BankAccount =
    SyncResponseJson.Cipher.BankAccount(
        bankName = bankName,
        nameOnAccount = nameOnAccount,
        accountType = accountType,
        accountNumber = accountNumber,
        routingNumber = routingNumber,
        branchNumber = branchNumber,
        pin = pin,
        swiftCode = swiftCode,
        iban = iban,
        bankContactPhone = bankContactPhone,
    )

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, there is one literally a few lines down.

val issuingCountry: String = "",
val issuingState: String = "",
val expirationMonth: String = "",
val expirationYear: String = "",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this still should also be made for VaultItemState.ViewState.Content.ItemType., right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review-vnext Request a Claude code review using the vNext workflow app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants