Skip to content

Conversation

@jaasen-livefront
Copy link
Collaborator

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-25685

📔 Objective

This PR migrates the existing folder models to enforce strict type checking. Please see ticket description for additional details.

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Oct 28, 2025

Logo
Checkmarx One – Scan Summary & Detailsbc64014d-6779-41be-9bbf-f6935ed5c80a

New Issues (2)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
HIGH CVE-2025-12726 Npm-electron-37.7.0
detailsDescription: Inappropriate implementation in Views in Google Chrome on Windows prior to 142.0.7444.137 allowed a remote attacker who had compromised the rendere...
Attack Vector: NETWORK
Attack Complexity: HIGH

ID: iw1vhEld1Lva5auUkh63NyNNyZrajubcRUw%2FtutTO2E%3D
Vulnerable Package
MEDIUM CVE-2025-8129 Npm-koa-2.16.1
detailsRecommended version: 2.16.3
Description: A vulnerability, which was classified as problematic, was found in KoaJS Koa versions through 2.16.1 and versions 3.0.0-alpha0 through 3.0.0. Affec...
Attack Vector: NETWORK
Attack Complexity: LOW

ID: k6rEdM25w0SLPI%2FMkzXHwzkaTw65dve1j%2B0g%2FVf683E%3D
Vulnerable Package

@codecov
Copy link

codecov bot commented Oct 31, 2025

Codecov Report

❌ Patch coverage is 54.05405% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.25%. Comparing base (e3acd27) to head (1d5da94).
⚠️ Report is 118 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
libs/common/src/vault/models/domain/folder.ts 60.00% 2 Missing and 2 partials ⚠️
libs/common/src/vault/models/view/folder.view.ts 60.00% 0 Missing and 4 partials ⚠️
libs/common/src/models/export/folder.export.ts 0.00% 3 Missing ⚠️
libs/common/src/vault/models/data/folder.data.ts 66.66% 2 Missing ⚠️
...on/src/vault/services/folder/folder-api.service.ts 0.00% 2 Missing ⚠️
.../src/vault/components/folder-add-edit.component.ts 0.00% 1 Missing ⚠️
...src/vault/models/request/folder-with-id.request.ts 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #17077      +/-   ##
==========================================
+ Coverage   40.63%   41.25%   +0.62%     
==========================================
  Files        3531     3546      +15     
  Lines      101119   102050     +931     
  Branches    15150    15316     +166     
==========================================
+ Hits        41087    42098    +1011     
+ Misses      58293    58185     -108     
- Partials     1739     1767      +28     

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

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jaasen-livefront jaasen-livefront marked this pull request as ready for review October 31, 2025 21:16
@jaasen-livefront jaasen-livefront requested review from a team as code owners October 31, 2025 21:16
@claude
Copy link

claude bot commented Oct 31, 2025

Claude finished @jaasen-livefront's task —— View job


Code Review - Recent Changes

I've reviewed the recent commits addressing shane-melton's feedback. Here's my assessment:


Summary of Changes Since Last Review

The author made 5 commits addressing previous review feedback:

  • a4fec9e - Fix folder (most recent)
  • e09cab2 - Remove optional param to folder constructor
  • cfb3218 - Initialize id in folder constructor, fix failing tests
  • ab47d10 - Fixes to folder
  • 1d5da94 - Check for empty name in folder toDomain

Status of Previous Review Items:

RESOLVED: Optional fields made consistent (id and name now have defaults in Folder)
RESOLVED: Desktop null checks verified (no explicit null checks found)
⚠️ PARTIALLY RESOLVED: FolderView.fromJSON revisionDate handling (conditional but could be improved)
NOT RESOLVED: Several issues remain (see findings below)


Critical Issues Found

Finding 1: FolderView constructor doesn't validate folder state before accessing name.toString() (folder.view.ts:19)

When a Folder is constructed via new Folder() and immediately passed to new FolderView(f) without encryption, line 19 calls .toString() on an EncString with empty encrypted string. While this may not crash, it's semantically incorrect.

Recommendation: Consider validating folder has encrypted data before constructing view, or document that FolderView constructor expects properly encrypted folders.


Finding 2: FolderView.fromJSON doesn't initialize revisionDate when null/undefined, violating type contract (folder.view.ts:27-29)

The conditional assignment means revisionDate can remain undefined despite being typed as non-optional Date (line 10).

Current code:

if (obj.revisionDate != null) {
  folderView.revisionDate = new Date(obj.revisionDate);
}

Should be (matching CipherView pattern at cipher.view.ts:225-227):

if (obj.revisionDate != null) {
  folderView.revisionDate = new Date(obj.revisionDate);
} else {
  folderView.revisionDate = new Date();
}

Or make revisionDate optional in the type definition.


Finding 3: Redundant double nullish coalescing in FolderExport.build() (folder.export.ts:28)

this.name = safeGetString(o.name ?? "") ?? "";

The second ?? "" is unreachable because:

  1. When o.name is nullish, first ?? "" converts to ""
  2. safeGetString("") returns "" (not null)
  3. Second ?? "" never executes

Simplify to:

this.name = safeGetString(o.name ?? "");

⚠️ Finding 4: Folder.fromJSON inconsistent with codebase pattern (folder.ts:48-58)

shane-melton correctly identified this. The pattern should match Cipher.fromJSON approach:

  • Create empty instance
  • Explicitly assign each property
  • Use EncString.fromJSON() for proper deserialization

Current implementation:

static fromJSON(obj: Jsonify<Folder>) {
  if (obj == null) {
    return null;
  }

  const folder = new Folder();
  folder.id = obj.id;
  folder.name = EncString.fromJSON(obj.name);
  folder.revisionDate = new Date(obj.revisionDate);
  return folder;
}

Issues:

  • ✅ Good: Now uses EncString.fromJSON() (preserves encryption metadata)
  • ❌ Problem: Doesn't handle null/undefined cases for individual properties
  • ❌ Problem: obj.id could be undefined, violating non-nullable type

Recommended pattern (matching Cipher):

static fromJSON(obj: Jsonify<Folder>) {
  if (obj == null) {
    return null;
  }

  const folder = new Folder();
  folder.id = obj.id ?? "";
  folder.name = EncString.fromJSON(obj.name) ?? new EncString("");
  folder.revisionDate = obj.revisionDate ? new Date(obj.revisionDate) : new Date();
  return folder;
}

⚠️ Finding 5: FolderExport.toDomain creates EncString unconditionally (folder.export.ts:22)

static toDomain(req: FolderExport, domain = new FolderDomain()) {
  domain.name = new EncString(req.name);
  return domain;
}

Since FolderExport.name defaults to "", this will create an EncString from empty string when name is missing. This matches the original behavior but consider if this is semantically correct - should empty folders have an EncString("") or undefined name?


Good Practices Observed

  • Default values provide clear contracts (id: "", name: new EncString(""), revisionDate: new Date())
  • Folder.fromJSON now properly uses EncString.fromJSON()
  • Browser/Web null check updates are correct and consistent

Action Items

Required:

  1. Fix Finding 2: Add fallback for revisionDate in FolderView.fromJSON
  2. Fix Finding 3: Remove redundant nullish coalescing

Recommended:
3. Address Finding 4: Add null handling in Folder.fromJSON for robustness
4. Consider Finding 1: Document or validate FolderView constructor expectations
5. Review Finding 5: Confirm empty string EncString behavior is intentional


Response to Author's Question

@Hinton I didn't see any explicit checks for a null id in the desktop client. Can you think of anywhere we may be doing that?

Confirmed: I searched the Desktop client codebase and found no explicit null checks for folder.id (patterns like folder.id === null, folder.id == null, etc.). The Desktop client appears safe from this breaking change.

The Browser and Web clients were correctly updated to use truthy checks (!folder.id) instead of explicit null checks.


Jingo88
Jingo88 previously approved these changes Nov 6, 2025
Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

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

This seems to change the behaviour of the FolderExport model.


static toDomain(req: FolderExport, domain = new FolderDomain()) {
domain.name = req.name != null ? new EncString(req.name) : null;
domain.name = new EncString(req.name);
Copy link
Member

Choose a reason for hiding this comment

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

issue: This is not functionally equivalent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Hinton Indeed, but since the folder model requires name to be a type of EncString we can no longer return null.

Copy link
Collaborator Author

@jaasen-livefront jaasen-livefront Nov 7, 2025

Choose a reason for hiding this comment

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

Fixed 939ec1b. name is now optional so this should now return apply the expected types (string | undefined)

Copy link
Member

Choose a reason for hiding this comment

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

This is still a behavioural change. If name is now optional all changes to this file should be reverted.

Copy link
Collaborator Author

@jaasen-livefront jaasen-livefront Nov 20, 2025

Choose a reason for hiding this comment

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

@Hinton Sorry about the confusion. Since FolderExport requires name we can just check for presence now. I've updated the code to reflect this.

// Use build method instead of ctor so that we can control order of JSON stringify for pretty print
build(o: FolderView | FolderDomain) {
this.name = safeGetString(o.name);
this.name = safeGetString(o.name) ?? "";
Copy link
Member

Choose a reason for hiding this comment

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

issue: This is not functionally equivalent.

Copy link
Collaborator Author

@jaasen-livefront jaasen-livefront Nov 7, 2025

Choose a reason for hiding this comment

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

@Hinton We're modelling FolderExport to match the Folder domain so we now requires name to be of type string. Since safeGetString can return undefined we must fallback to an empty string. Do you suggest we have name be optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed 939ec1b

export class Folder extends Domain {
id: string;
name: EncString;
id: string = "";
Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure id can be optional, when you create a folder ID should be null/undefined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed 939ec1b

@jaasen-livefront
Copy link
Collaborator Author

@Hinton @Jingo88 Ready for another look when you have a chance.

Jingo88
Jingo88 previously approved these changes Nov 13, 2025
Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

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

There are still various behavioral changes across the PR. See my DM for testing scope if these are desired.

I also think some of the claude comments are relevant.


static toDomain(req: FolderExport, domain = new FolderDomain()) {
domain.name = req.name != null ? new EncString(req.name) : null;
domain.name = new EncString(req.name);
Copy link
Member

Choose a reason for hiding this comment

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

This is still a behavioural change. If name is now optional all changes to this file should be reverted.

Copy link
Member

Choose a reason for hiding this comment

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

Why are we changing this test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Hinton I've reverted it back to close to the original. We're now checking for object equality instead of string equality which makes the tests less brittle.

// Use build method instead of ctor so that we can control order of JSON stringify for pretty print
build(o: FolderView | FolderDomain) {
this.name = safeGetString(o.name);
this.name = safeGetString(o.name ?? "") ?? "";
Copy link

Choose a reason for hiding this comment

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

Finding 1: Behavioral change - this now converts nullish folder names to empty strings in exports.

Original behavior:

this.name = safeGetString(o.name); // Returns null for nullish values

New behavior:

this.name = safeGetString(o.name ?? "") ?? ""; // Always returns string

When o.name is undefined, the original would pass it to safeGetString, which returns null. The new code converts to "" first, changing the export format.

Also: The double ?? "" is redundant. Simplify to:

this.name = safeGetString(o.name ?? "");

If folders can have no name, should this distinction be preserved in exports?


static toDomain(req: FolderExport, domain = new FolderDomain()) {
domain.name = req.name != null ? new EncString(req.name) : null;
domain.name = req.name ? new EncString(req.name) : undefined;
Copy link

Choose a reason for hiding this comment

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

Finding 2: Changed from null to undefined and from != null to truthy check.

Original:

domain.name = req.name != null ? new EncString(req.name) : null;

New:

domain.name = req.name ? new EncString(req.name) : undefined;

This changes behavior for empty strings: toDomain({ name: "" }) now returns undefined instead of new EncString("") because "" is falsy.

return null;
}
return new Folder({
name: obj.name ?? "",
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 3: Potential loss of encryption metadata.

The original implementation used EncString.fromJSON(obj.name) which properly deserializes encryption type information. The new approach passes the raw string to FolderData, which gets wrapped in new EncString(obj.name) in the constructor.

Original:

const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
return Object.assign(new Folder(), obj, { 
  name: EncString.fromJSON(obj.name),  // Preserves encryption metadata
  revisionDate 
});

While the new approach may work for simple cases, EncString.fromJSON() is designed to properly deserialize the encryption type and IV from the JSON format. Consider whether this simplified approach is safe for all encryption types used in production.

this.name = response?.name;
this.id = response?.id;
this.revisionDate = response?.revisionDate;
this.name = response.name ?? "";
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 5: Data migration concern - nullish values now coerced to empty strings.

Before: Properties could be undefined/null
After: Properties always have default values ("" or current date)

This changes the data layer contract from nullable to non-nullable. If any persisted state contains null values for these properties, they'll silently convert to "" on read.

Consider whether:

  1. This is acceptable behavior (silent coercion vs. explicit migration)
  2. Existing user data needs migration
  3. The distinction between "no folder name" (null) vs. "empty folder name" ("") should be preserved

return null;
}
return new Folder({
name: obj.name ?? "",
Copy link

Choose a reason for hiding this comment

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

Critical: Loss of encryption metadata in deserialization

This implementation passes a plain string to the FolderData constructor, which then wraps it in new EncString(obj.name) at line 31. This only creates a basic AesCbc256_B64 EncString.

The original implementation used EncString.fromJSON(obj.name) which properly deserializes encryption type information (AES-CBC vs AES-GCM, IV, MAC, etc.) from the JSON representation.

Impact: If the JSON contains encrypted data with specific encryption metadata, this will be lost during deserialization, potentially causing decryption failures or security issues.

Original implementation:

return Object.assign(new Folder(), obj, { 
  name: EncString.fromJSON(obj.name),  // Preserves encryption metadata
  revisionDate 
});

Recommendation: Either revert to using EncString.fromJSON() or document this as an intentional breaking change if encryption metadata is never present in this deserialization path.

async submit(): Promise<boolean> {
this.folder.name = this.formGroup.controls.name.value;
if (this.folder.name == null || this.folder.name === "") {
if (this.folder.name === "") {
Copy link
Member

Choose a reason for hiding this comment

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

🎨 We shouldn't remove this null check yet.

Comment on lines 11 to 12
id?: string;
name?: EncString;
Copy link
Member

Choose a reason for hiding this comment

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

❓ Why are these marked as optional in Folder but required in the other models? I would expect them to be consistent across models.

return;
}

this.buildDomainModel(
Copy link
Member

Choose a reason for hiding this comment

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

🎨 If we're setting the name and revision date manually then we no longer need to call this buildDomainModel() method.

constructor(obj?: FolderData) {
super();
if (obj == null) {
this.revisionDate = new Date();
Copy link
Member

Choose a reason for hiding this comment

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

💭 This is inconsistent with name. Why are we setting revisionDate to a default value and not name?

folderView.revisionDate = this.revisionDate;
try {
folderView.name = await encryptService.decryptString(this.name, key);
folderView.name = await encryptService.decryptString(this.name ?? new EncString(""), key);
Copy link
Member

Choose a reason for hiding this comment

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

⚠️ Passing an EncString("") to decryptString doesn't really mean anything or serve a purpose here. If the encrypted name is missing, we should just skip decryption.

return null;
}
return new Folder({
name: obj.name ?? "",
Copy link
Member

Choose a reason for hiding this comment

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

🎨 Claude makes some potentially good points here, it seems we're changing/obsfucating this implementation here when we shouldn't need to.

We should be able to call the empty Folder() constructor and explicitly assign the the name and revisionDate properties. We should also continue to use the fromJson() helpers for EncString for consistency. See the Cipher.fromJSON method for an example.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed ab47d10

revisionDate: Date;

constructor(f?: Folder | DecryptedObject<Folder, "name">) {
constructor(f?: Folder | DecryptedObject<Folder, undefined>) {
Copy link
Member

Choose a reason for hiding this comment

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

undefined doesn't seem right here?

On second look, I don't see any usage of the FolderView constructor where we pass in a decrypted Folder object. I think its safe to just remove this type.

Copy link
Collaborator Author

@jaasen-livefront jaasen-livefront Nov 21, 2025

Choose a reason for hiding this comment

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

fixed e09cab2


this.id = f.id;
this.id = f.id ?? "";
this.name = f.name?.decryptedValue ?? "";
Copy link
Member

Choose a reason for hiding this comment

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

🎨 Since we never pass in a decrypted folder, I don't think we need to add this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed ab47d10

const folderView = new FolderView();
folderView.id = obj.id ?? "";
folderView.name = obj.name ?? "";
folderView.revisionDate = new Date(obj.revisionDate);
Copy link
Member

Choose a reason for hiding this comment

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

⚠️ I'd expect this to be a type error as you can't pass a | undefined into new Date(). We need to keep the check and only assign if the obj.revisionDate has a value.

See here in CipherView:

if (obj.revisionDate) {
view.revisionDate = new Date(obj.revisionDate);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed ab47d10

id: string = null;
name: string = null;
revisionDate: Date = null;
id: string = "";
Copy link
Member

Choose a reason for hiding this comment

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

ℹ️ This is going to break our checks for the "No Folder" folder that assumes a null id. We'll need to do a thorough search of the clients to be sure we catch them all. I see you touched the browser and web services, but nothing for Desktop.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@shane-melton I didn't see any explicit checks for a null id in the desktop client. Can you think of anywhere we many be doing that?

folderView.name = obj.name ?? "";
if (obj.revisionDate != null) {
folderView.revisionDate = new Date(obj.revisionDate);
}
Copy link

Choose a reason for hiding this comment

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

Critical: revisionDate may remain uninitialized

When obj.revisionDate is null or undefined, this property is never set, leaving it undefined despite being typed as non-optional Date (line 10). This violates the type contract.

Fix:

folderView.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : new Date();

This matches the pattern used in the FolderView constructor (line 14) which initializes to current date when no folder is provided.

// Use build method instead of ctor so that we can control order of JSON stringify for pretty print
build(o: FolderView | FolderDomain) {
this.name = safeGetString(o.name);
this.name = safeGetString(o.name ?? "") ?? "";
Copy link

Choose a reason for hiding this comment

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

Redundant double nullish coalescing

The second ?? "" is unreachable:

  • When o.name is nullish, first ?? "" converts to ""
  • safeGetString("") returns "" (not null)
  • Second ?? "" never executes

Simplify to:

this.name = safeGetString(o.name ?? "");

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants