From ea04b0562f8455aa90ec637634d90681497c3630 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Feb 2026 21:19:38 +0100 Subject: [PATCH 01/13] Prevent SDK from disposing withit debounce period (#18775) --- .../services/sdk/default-sdk.service.spec.ts | 35 +++++++++++++++++-- .../services/sdk/default-sdk.service.ts | 8 ++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index fb9c1fae77e..2a1a3497887 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -143,17 +143,44 @@ describe("DefaultSdkService", () => { }); it("destroys the internal SDK client when all subscriptions are closed", async () => { + jest.useFakeTimers(); const subject_1 = new BehaviorSubject | undefined>(undefined); const subject_2 = new BehaviorSubject | undefined>(undefined); const subscription_1 = service.userClient$(userId).subscribe(subject_1); const subscription_2 = service.userClient$(userId).subscribe(subject_2); - await new Promise(process.nextTick); + await jest.advanceTimersByTimeAsync(0); subscription_1.unsubscribe(); subscription_2.unsubscribe(); - await new Promise(process.nextTick); + await jest.advanceTimersByTimeAsync(0); + expect(mockClient.free).not.toHaveBeenCalled(); + + await jest.advanceTimersByTimeAsync(1000); expect(mockClient.free).toHaveBeenCalledTimes(1); + jest.useRealTimers(); + }); + + it("does not destroy the internal SDK client if resubscribed within 1 second", async () => { + jest.useFakeTimers(); + const subject_1 = new BehaviorSubject | undefined>(undefined); + const subscription_1 = service.userClient$(userId).subscribe(subject_1); + await jest.advanceTimersByTimeAsync(0); + + subscription_1.unsubscribe(); + await jest.advanceTimersByTimeAsync(500); + expect(mockClient.free).not.toHaveBeenCalled(); + + // Resubscribe before the 1 second delay + const subject_2 = new BehaviorSubject | undefined>(undefined); + const subscription_2 = service.userClient$(userId).subscribe(subject_2); + await jest.advanceTimersByTimeAsync(1000); + + // Client should not be freed since we resubscribed + expect(mockClient.free).not.toHaveBeenCalled(); + expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1); + subscription_2.unsubscribe(); + jest.useRealTimers(); }); it("destroys the internal SDK client when the userKey is unset (i.e. lock or logout)", async () => { @@ -218,6 +245,7 @@ describe("DefaultSdkService", () => { }); it("destroys the internal client when an override is set", async () => { + jest.useFakeTimers(); const mockInternalClient = createMockClient(); const mockOverrideClient = createMockClient(); sdkClientFactory.createSdkClient.mockResolvedValue(mockInternalClient); @@ -227,7 +255,10 @@ describe("DefaultSdkService", () => { service.setClient(userId, mockOverrideClient); await userClientTracker.pauseUntilReceived(2); + expect(mockInternalClient.free).not.toHaveBeenCalled(); + await jest.advanceTimersByTimeAsync(1000); expect(mockInternalClient.free).toHaveBeenCalled(); + jest.useRealTimers(); }); it("destroys the override client when explicitly setting the client to undefined", async () => { diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index e2c9c77e204..e5104f3c68d 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -2,7 +2,10 @@ import { combineLatest, concatMap, Observable, + share, shareReplay, + ReplaySubject, + timer, map, distinctUntilChanged, tap, @@ -263,7 +266,10 @@ export class DefaultSdkService implements SdkService { }, ), tap({ finalize: () => this.sdkClientCache.delete(userId) }), - shareReplay({ refCount: true, bufferSize: 1 }), + share({ + connector: () => new ReplaySubject(1), + resetOnRefCountZero: () => timer(1000), + }), ); this.sdkClientCache.set(userId, client$); From 48d18df2854c34f21ab4a4c2cb167ee7ae9baa47 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Mon, 9 Feb 2026 15:32:49 -0500 Subject: [PATCH 02/13] =?UTF-8?q?Revert=20"[PM-31668]=20Race=20condition?= =?UTF-8?q?=20in=20cipher=20cache=20clearing=20causes=20stale=20faile?= =?UTF-8?q?=E2=80=A6"=20(#18846)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bf13194b9cde3dbad47866cfe4fc752935e3e52a. --- .../src/platform/sync/default-sync.service.ts | 2 - .../src/vault/abstractions/search.service.ts | 3 +- .../src/vault/services/cipher.service.ts | 16 +- .../src/vault/services/search.service.ts | 120 +++++------ .../utils/cipher-view-like-utils.spec.ts | 194 ------------------ .../src/vault/utils/cipher-view-like-utils.ts | 66 ------ 6 files changed, 61 insertions(+), 340 deletions(-) diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index a25b1b3c210..68c03503e8d 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -183,8 +183,6 @@ export class DefaultSyncService extends CoreSyncService { const response = await this.inFlightApiCalls.sync; - await this.cipherService.clear(response.profile.id); - await this.syncUserDecryption(response.profile.id, response.userDecryption); await this.syncProfile(response.profile); await this.syncFolders(response.folders, response.profile.id); diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts index b4dfc015efe..29575ec3af9 100644 --- a/libs/common/src/vault/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -2,6 +2,7 @@ import { Observable } from "rxjs"; import { SendView } from "../../tools/send/models/view/send.view"; import { IndexedEntityId, UserId } from "../../types/guid"; +import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike } from "../utils/cipher-view-like-utils"; export abstract class SearchService { @@ -19,7 +20,7 @@ export abstract class SearchService { abstract isSearchable(userId: UserId, query: string | null): Promise; abstract indexCiphers( userId: UserId, - ciphersToIndex: CipherViewLike[], + ciphersToIndex: CipherView[], indexedEntityGuid?: string, ): Promise; abstract searchCiphers( diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 696ef49065c..6373a511724 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -173,14 +173,13 @@ export class CipherService implements CipherServiceAbstraction { decryptStartTime = performance.now(); }), switchMap(async (ciphers) => { - return await this.decryptCiphersWithSdk(ciphers, userId, false); + const [decrypted, failures] = await this.decryptCiphersWithSdk(ciphers, userId, false); + void this.setFailedDecryptedCiphers(failures, userId); + // Trigger full decryption and indexing in background + void this.getAllDecrypted(userId); + return decrypted; }), - tap(([decrypted, failures]) => { - void Promise.all([ - this.setFailedDecryptedCiphers(failures, userId), - this.searchService.indexCiphers(userId, decrypted), - ]); - + tap((decrypted) => { this.logService.measure( decryptStartTime, "Vault", @@ -189,11 +188,10 @@ export class CipherService implements CipherServiceAbstraction { [["Items", decrypted.length]], ); }), - map(([decrypted]) => decrypted), ); }), ); - }, this.clearCipherViewsForUser$); + }); /** * Observable that emits an array of decrypted ciphers for the active user. diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts index e14a66aad6f..feb6a7494b5 100644 --- a/libs/common/src/vault/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -21,6 +21,7 @@ import { IndexedEntityId, UserId } from "../../types/guid"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { FieldType } from "../enums"; import { CipherType } from "../enums/cipher-type"; +import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike, CipherViewLikeUtils } from "../utils/cipher-view-like-utils"; // Time to wait before performing a search after the user stops typing. @@ -168,7 +169,7 @@ export class SearchService implements SearchServiceAbstraction { async indexCiphers( userId: UserId, - ciphers: CipherViewLike[], + ciphers: CipherView[], indexedEntityId?: string, ): Promise { if (await this.getIsIndexing(userId)) { @@ -181,47 +182,34 @@ export class SearchService implements SearchServiceAbstraction { const builder = new lunr.Builder(); builder.pipeline.add(this.normalizeAccentsPipelineFunction); builder.ref("id"); - builder.field("shortid", { - boost: 100, - extractor: (c: CipherViewLike) => uuidAsString(c.id).substr(0, 8), - }); + builder.field("shortid", { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); builder.field("name", { boost: 10, }); builder.field("subtitle", { boost: 5, - extractor: (c: CipherViewLike) => { - const subtitle = CipherViewLikeUtils.subtitle(c); - if (subtitle != null && CipherViewLikeUtils.getType(c) === CipherType.Card) { - return subtitle.replace(/\*/g, ""); + extractor: (c: CipherView) => { + if (c.subTitle != null && c.type === CipherType.Card) { + return c.subTitle.replace(/\*/g, ""); } - return subtitle; + return c.subTitle; }, }); - builder.field("notes", { extractor: (c: CipherViewLike) => CipherViewLikeUtils.getNotes(c) }); + builder.field("notes"); builder.field("login.username", { - extractor: (c: CipherViewLike) => { - const login = CipherViewLikeUtils.getLogin(c); - return login?.username ?? null; - }, - }); - builder.field("login.uris", { - boost: 2, - extractor: (c: CipherViewLike) => this.uriExtractor(c), - }); - builder.field("fields", { - extractor: (c: CipherViewLike) => this.fieldExtractor(c, false), - }); - builder.field("fields_joined", { - extractor: (c: CipherViewLike) => this.fieldExtractor(c, true), + extractor: (c: CipherView) => + c.type === CipherType.Login && c.login != null ? c.login.username : null, }); + builder.field("login.uris", { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) }); + builder.field("fields", { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); + builder.field("fields_joined", { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); builder.field("attachments", { - extractor: (c: CipherViewLike) => this.attachmentExtractor(c, false), + extractor: (c: CipherView) => this.attachmentExtractor(c, false), }); builder.field("attachments_joined", { - extractor: (c: CipherViewLike) => this.attachmentExtractor(c, true), + extractor: (c: CipherView) => this.attachmentExtractor(c, true), }); - builder.field("organizationid", { extractor: (c: CipherViewLike) => c.organizationId }); + builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId }); ciphers = ciphers || []; ciphers.forEach((c) => builder.add(c)); const index = builder.build(); @@ -412,44 +400,37 @@ export class SearchService implements SearchServiceAbstraction { return await firstValueFrom(this.searchIsIndexing$(userId)); } - private fieldExtractor(c: CipherViewLike, joined: boolean) { - const fields = CipherViewLikeUtils.getFields(c); - if (!fields || fields.length === 0) { + private fieldExtractor(c: CipherView, joined: boolean) { + if (!c.hasFields) { return null; } - let fieldStrings: string[] = []; - fields.forEach((f) => { + let fields: string[] = []; + c.fields.forEach((f) => { if (f.name != null) { - fieldStrings.push(f.name); + fields.push(f.name); } - // For CipherListView, value is only populated for Text fields - // For CipherView, we check the type explicitly - if (f.value != null) { - const fieldType = (f as { type?: FieldType }).type; - if (fieldType === undefined || fieldType === FieldType.Text) { - fieldStrings.push(f.value); - } + if (f.type === FieldType.Text && f.value != null) { + fields.push(f.value); } }); - fieldStrings = fieldStrings.filter((f) => f.trim() !== ""); - if (fieldStrings.length === 0) { + fields = fields.filter((f) => f.trim() !== ""); + if (fields.length === 0) { return null; } - return joined ? fieldStrings.join(" ") : fieldStrings; + return joined ? fields.join(" ") : fields; } - private attachmentExtractor(c: CipherViewLike, joined: boolean) { - const attachmentNames = CipherViewLikeUtils.getAttachmentNames(c); - if (!attachmentNames || attachmentNames.length === 0) { + private attachmentExtractor(c: CipherView, joined: boolean) { + if (!c.hasAttachments) { return null; } let attachments: string[] = []; - attachmentNames.forEach((fileName) => { - if (fileName != null) { - if (joined && fileName.indexOf(".") > -1) { - attachments.push(fileName.substring(0, fileName.lastIndexOf("."))); + c.attachments.forEach((a) => { + if (a != null && a.fileName != null) { + if (joined && a.fileName.indexOf(".") > -1) { + attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf("."))); } else { - attachments.push(fileName); + attachments.push(a.fileName); } } }); @@ -460,39 +441,43 @@ export class SearchService implements SearchServiceAbstraction { return joined ? attachments.join(" ") : attachments; } - private uriExtractor(c: CipherViewLike) { - if (CipherViewLikeUtils.getType(c) !== CipherType.Login) { - return null; - } - const login = CipherViewLikeUtils.getLogin(c); - if (!login?.uris?.length) { + private uriExtractor(c: CipherView) { + if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) { return null; } const uris: string[] = []; - login.uris.forEach((u) => { + c.login.uris.forEach((u) => { if (u.uri == null || u.uri === "") { return; } - // Extract port from URI + // Match ports const portMatch = u.uri.match(/:(\d+)(?:[/?#]|$)/); const port = portMatch?.[1]; - const hostname = CipherViewLikeUtils.getUriHostname(u); - if (hostname !== undefined) { - uris.push(hostname); + let uri = u.uri; + + if (u.hostname !== null) { + uris.push(u.hostname); if (port) { - uris.push(`${hostname}:${port}`); + uris.push(`${u.hostname}:${port}`); + uris.push(port); + } + return; + } else { + const slash = uri.indexOf("/"); + const hostPart = slash > -1 ? uri.substring(0, slash) : uri; + uris.push(hostPart); + if (port) { + uris.push(`${hostPart}`); uris.push(port); } } - // Add processed URI (strip protocol and query params for non-regex matches) - let uri = u.uri; if (u.match !== UriMatchStrategy.RegularExpression) { const protocolIndex = uri.indexOf("://"); if (protocolIndex > -1) { - uri = uri.substring(protocolIndex + 3); + uri = uri.substr(protocolIndex + 3); } const queryIndex = uri.search(/\?|&|#/); if (queryIndex > -1) { @@ -501,7 +486,6 @@ export class SearchService implements SearchServiceAbstraction { } uris.push(uri); }); - return uris.length > 0 ? uris : null; } diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts index 2a7bfac2970..56b94fcf3ce 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts @@ -651,198 +651,4 @@ describe("CipherViewLikeUtils", () => { expect(CipherViewLikeUtils.decryptionFailure(cipherListView)).toBe(false); }); }); - - describe("getNotes", () => { - describe("CipherView", () => { - it("returns notes when present", () => { - const cipherView = createCipherView(); - cipherView.notes = "This is a test note"; - - expect(CipherViewLikeUtils.getNotes(cipherView)).toBe("This is a test note"); - }); - - it("returns undefined when notes are not present", () => { - const cipherView = createCipherView(); - cipherView.notes = undefined; - - expect(CipherViewLikeUtils.getNotes(cipherView)).toBeUndefined(); - }); - }); - - describe("CipherListView", () => { - it("returns notes when present", () => { - const cipherListView = { - type: "secureNote", - notes: "List view notes", - } as CipherListView; - - expect(CipherViewLikeUtils.getNotes(cipherListView)).toBe("List view notes"); - }); - - it("returns undefined when notes are not present", () => { - const cipherListView = { - type: "secureNote", - } as CipherListView; - - expect(CipherViewLikeUtils.getNotes(cipherListView)).toBeUndefined(); - }); - }); - }); - - describe("getFields", () => { - describe("CipherView", () => { - it("returns fields when present", () => { - const cipherView = createCipherView(); - cipherView.fields = [ - { name: "Field1", value: "Value1" } as any, - { name: "Field2", value: "Value2" } as any, - ]; - - const fields = CipherViewLikeUtils.getFields(cipherView); - - expect(fields).toHaveLength(2); - expect(fields?.[0].name).toBe("Field1"); - expect(fields?.[0].value).toBe("Value1"); - expect(fields?.[1].name).toBe("Field2"); - expect(fields?.[1].value).toBe("Value2"); - }); - - it("returns empty array when fields array is empty", () => { - const cipherView = createCipherView(); - cipherView.fields = []; - - expect(CipherViewLikeUtils.getFields(cipherView)).toEqual([]); - }); - }); - - describe("CipherListView", () => { - it("returns fields when present", () => { - const cipherListView = { - type: { login: {} }, - fields: [ - { name: "Username", value: "user@example.com" }, - { name: "API Key", value: "abc123" }, - ], - } as CipherListView; - - const fields = CipherViewLikeUtils.getFields(cipherListView); - - expect(fields).toHaveLength(2); - expect(fields?.[0].name).toBe("Username"); - expect(fields?.[0].value).toBe("user@example.com"); - expect(fields?.[1].name).toBe("API Key"); - expect(fields?.[1].value).toBe("abc123"); - }); - - it("returns empty array when fields array is empty", () => { - const cipherListView = { - type: "secureNote", - fields: [], - } as unknown as CipherListView; - - expect(CipherViewLikeUtils.getFields(cipherListView)).toEqual([]); - }); - - it("returns undefined when fields are not present", () => { - const cipherListView = { - type: "secureNote", - } as CipherListView; - - expect(CipherViewLikeUtils.getFields(cipherListView)).toBeUndefined(); - }); - }); - }); - - describe("getAttachmentNames", () => { - describe("CipherView", () => { - it("returns attachment filenames when present", () => { - const cipherView = createCipherView(); - const attachment1 = new AttachmentView(); - attachment1.id = "1"; - attachment1.fileName = "document.pdf"; - const attachment2 = new AttachmentView(); - attachment2.id = "2"; - attachment2.fileName = "image.png"; - const attachment3 = new AttachmentView(); - attachment3.id = "3"; - attachment3.fileName = "spreadsheet.xlsx"; - cipherView.attachments = [attachment1, attachment2, attachment3]; - - const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); - - expect(attachmentNames).toEqual(["document.pdf", "image.png", "spreadsheet.xlsx"]); - }); - - it("filters out null and undefined filenames", () => { - const cipherView = createCipherView(); - const attachment1 = new AttachmentView(); - attachment1.id = "1"; - attachment1.fileName = "valid.pdf"; - const attachment2 = new AttachmentView(); - attachment2.id = "2"; - attachment2.fileName = null as any; - const attachment3 = new AttachmentView(); - attachment3.id = "3"; - attachment3.fileName = undefined; - const attachment4 = new AttachmentView(); - attachment4.id = "4"; - attachment4.fileName = "another.txt"; - cipherView.attachments = [attachment1, attachment2, attachment3, attachment4]; - - const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); - - expect(attachmentNames).toEqual(["valid.pdf", "another.txt"]); - }); - - it("returns empty array when attachments have no filenames", () => { - const cipherView = createCipherView(); - const attachment1 = new AttachmentView(); - attachment1.id = "1"; - const attachment2 = new AttachmentView(); - attachment2.id = "2"; - cipherView.attachments = [attachment1, attachment2]; - - const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); - - expect(attachmentNames).toEqual([]); - }); - - it("returns empty array for empty attachments array", () => { - const cipherView = createCipherView(); - cipherView.attachments = []; - - expect(CipherViewLikeUtils.getAttachmentNames(cipherView)).toEqual([]); - }); - }); - - describe("CipherListView", () => { - it("returns attachment names when present", () => { - const cipherListView = { - type: "secureNote", - attachmentNames: ["report.pdf", "photo.jpg", "data.csv"], - } as CipherListView; - - const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherListView); - - expect(attachmentNames).toEqual(["report.pdf", "photo.jpg", "data.csv"]); - }); - - it("returns empty array when attachmentNames is empty", () => { - const cipherListView = { - type: "secureNote", - attachmentNames: [], - } as unknown as CipherListView; - - expect(CipherViewLikeUtils.getAttachmentNames(cipherListView)).toEqual([]); - }); - - it("returns undefined when attachmentNames is not present", () => { - const cipherListView = { - type: "secureNote", - } as CipherListView; - - expect(CipherViewLikeUtils.getAttachmentNames(cipherListView)).toBeUndefined(); - }); - }); - }); }); diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.ts b/libs/common/src/vault/utils/cipher-view-like-utils.ts index 5359bfb958f..04adb8d4832 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.ts @@ -10,7 +10,6 @@ import { LoginUriView as LoginListUriView, } from "@bitwarden/sdk-internal"; -import { Utils } from "../../platform/misc/utils"; import { CipherType } from "../enums"; import { Cipher } from "../models/domain/cipher"; import { CardView } from "../models/view/card.view"; @@ -291,71 +290,6 @@ export class CipherViewLikeUtils { static decryptionFailure = (cipher: CipherViewLike): boolean => { return "decryptionFailure" in cipher ? cipher.decryptionFailure : false; }; - - /** - * Returns the notes from the cipher. - * - * @param cipher - The cipher to extract notes from (either `CipherView` or `CipherListView`) - * @returns The notes string if present, or `undefined` if not set - */ - static getNotes = (cipher: CipherViewLike): string | undefined => { - return cipher.notes; - }; - - /** - * Returns the fields from the cipher. - * - * @param cipher - The cipher to extract fields from (either `CipherView` or `CipherListView`) - * @returns Array of field objects with `name` and `value` properties, `undefined` if not set - */ - static getFields = ( - cipher: CipherViewLike, - ): { name?: string | null; value?: string | undefined }[] | undefined => { - if (this.isCipherListView(cipher)) { - return cipher.fields; - } - return cipher.fields; - }; - - /** - * Returns attachment filenames from the cipher. - * - * @param cipher - The cipher to extract attachment names from (either `CipherView` or `CipherListView`) - * @returns Array of attachment filenames, `undefined` if attachments are not present - */ - static getAttachmentNames = (cipher: CipherViewLike): string[] | undefined => { - if (this.isCipherListView(cipher)) { - return cipher.attachmentNames; - } - - return cipher.attachments - ?.map((a) => a.fileName) - .filter((name): name is string => name != null); - }; - - /** - * Extracts hostname from a login URI. - * - * @param uri - The URI object (either `LoginUriView` class or `LoginListUriView`) - * @returns The hostname if available, `undefined` otherwise - * - * @remarks - * - For `LoginUriView` (CipherView): Uses the built-in `hostname` getter - * - For `LoginListUriView` (CipherListView): Computes hostname using `Utils.getHostname()` - * - Returns `undefined` for RegularExpression match types or when hostname cannot be extracted - */ - static getUriHostname = (uri: LoginListUriView | LoginUriView): string | undefined => { - if ("hostname" in uri && typeof uri.hostname !== "undefined") { - return uri.hostname ?? undefined; - } - - if (uri.match !== UriMatchStrategy.RegularExpression && uri.uri) { - const hostname = Utils.getHostname(uri.uri); - return hostname === "" ? undefined : hostname; - } - - return undefined; - }; } /** From e485623ed82c49f3408aff87dbc0add803643d17 Mon Sep 17 00:00:00 2001 From: Alex Dragovich <46065570+itsadrago@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:59:17 -0800 Subject: [PATCH 03/13] [PM-31685] Removing email hashes (#18744) * [PM-31685] Removing email hashes * [PM-31685] fixing tests, which are now passing * [PM-31685] removing anon access emails field and reusing emails field * [PM-31685] fixing missed tests * [PM-31685] fixing missed tests * [PM-31685] code review changes * [PM-31685] do not encrypt emails by use of domain functionality * [PM-31685] test fixes --- .../browser/src/background/main.background.ts | 1 - .../service-container/service-container.ts | 1 - .../src/services/jslib-services.module.ts | 1 - .../src/tools/send/models/data/send.data.ts | 2 - .../src/tools/send/models/domain/send.spec.ts | 115 ++---------------- .../src/tools/send/models/domain/send.ts | 11 +- .../send/models/request/send.request.spec.ts | 110 ----------------- .../tools/send/models/request/send.request.ts | 4 +- .../tools/send/services/send.service.spec.ts | 87 ++----------- .../src/tools/send/services/send.service.ts | 25 +--- .../services/test-data/send-tests.data.ts | 5 +- 11 files changed, 29 insertions(+), 333 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 65eb88156ae..585942d7537 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1008,7 +1008,6 @@ export default class MainBackground { this.keyGenerationService, this.sendStateProvider, this.encryptService, - this.cryptoFunctionService, this.configService, ); this.sendApiService = new SendApiService( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 848a0e08b29..2033a2dd064 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -615,7 +615,6 @@ export class ServiceContainer { this.keyGenerationService, this.sendStateProvider, this.encryptService, - this.cryptoFunctionService, this.configService, ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 429e54ced28..0f857e67247 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -859,7 +859,6 @@ const safeProviders: SafeProvider[] = [ KeyGenerationService, SendStateProviderAbstraction, EncryptService, - CryptoFunctionServiceAbstraction, ConfigService, ], }), diff --git a/libs/common/src/tools/send/models/data/send.data.ts b/libs/common/src/tools/send/models/data/send.data.ts index 4081eba2878..b4317c48959 100644 --- a/libs/common/src/tools/send/models/data/send.data.ts +++ b/libs/common/src/tools/send/models/data/send.data.ts @@ -23,7 +23,6 @@ export class SendData { deletionDate: string; password: string; emails: string; - emailHashes: string; disabled: boolean; hideEmail: boolean; authType: AuthType; @@ -47,7 +46,6 @@ export class SendData { this.deletionDate = response.deletionDate; this.password = response.password; this.emails = response.emails; - this.emailHashes = ""; this.disabled = response.disable; this.hideEmail = response.hideEmail; this.authType = response.authType; diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index f660333c917..b3fc3c4c3ef 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -41,7 +41,6 @@ describe("Send", () => { deletionDate: "2022-01-31T12:00:00.000Z", password: "password", emails: "", - emailHashes: "", disabled: false, hideEmail: true, authType: AuthType.None, @@ -70,8 +69,7 @@ describe("Send", () => { expirationDate: null, deletionDate: null, password: undefined, - emails: null, - emailHashes: undefined, + emails: undefined, disabled: undefined, hideEmail: undefined, }); @@ -97,8 +95,7 @@ describe("Send", () => { expirationDate: new Date("2022-01-31T12:00:00.000Z"), deletionDate: new Date("2022-01-31T12:00:00.000Z"), password: "password", - emails: null, - emailHashes: "", + emails: "", disabled: false, hideEmail: true, authType: AuthType.None, @@ -173,7 +170,7 @@ describe("Send", () => { }); }); - describe("Email decryption", () => { + describe("Email parsing", () => { let encryptService: jest.Mocked; let keyService: jest.Mocked; const userKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; @@ -188,91 +185,45 @@ describe("Send", () => { (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); }); - it("should decrypt and parse single email", async () => { + it("should parse single email", async () => { const send = new Send(); send.id = "id"; send.type = SendType.Text; send.name = mockEnc("name"); send.notes = mockEnc("notes"); send.key = mockEnc("key"); - send.emails = mockEnc("test@example.com"); + send.emails = "test@example.com"; send.text = mock(); send.text.decrypt = jest.fn().mockResolvedValue("textView" as any); - - encryptService.decryptString.mockImplementation((encString, key) => { - if (encString === send.emails) { - return Promise.resolve("test@example.com"); - } - if (encString === send.name) { - return Promise.resolve("name"); - } - if (encString === send.notes) { - return Promise.resolve("notes"); - } - return Promise.resolve(""); - }); - const view = await send.decrypt(userId); - - expect(encryptService.decryptString).toHaveBeenCalledWith(send.emails, "cryptoKey"); expect(view.emails).toEqual(["test@example.com"]); }); - it("should decrypt and parse multiple emails", async () => { + it("should parse multiple emails", async () => { const send = new Send(); send.id = "id"; send.type = SendType.Text; send.name = mockEnc("name"); send.notes = mockEnc("notes"); send.key = mockEnc("key"); - send.emails = mockEnc("test@example.com,user@test.com,admin@domain.com"); + send.emails = "test@example.com,user@test.com,admin@domain.com"; send.text = mock(); send.text.decrypt = jest.fn().mockResolvedValue("textView" as any); - - encryptService.decryptString.mockImplementation((encString, key) => { - if (encString === send.emails) { - return Promise.resolve("test@example.com,user@test.com,admin@domain.com"); - } - if (encString === send.name) { - return Promise.resolve("name"); - } - if (encString === send.notes) { - return Promise.resolve("notes"); - } - return Promise.resolve(""); - }); - const view = await send.decrypt(userId); - expect(view.emails).toEqual(["test@example.com", "user@test.com", "admin@domain.com"]); }); - it("should trim whitespace from decrypted emails", async () => { + it("should trim whitespace from emails", async () => { const send = new Send(); send.id = "id"; send.type = SendType.Text; send.name = mockEnc("name"); send.notes = mockEnc("notes"); send.key = mockEnc("key"); - send.emails = mockEnc(" test@example.com , user@test.com "); + send.emails = " test@example.com , user@test.com "; send.text = mock(); send.text.decrypt = jest.fn().mockResolvedValue("textView" as any); - - encryptService.decryptString.mockImplementation((encString, key) => { - if (encString === send.emails) { - return Promise.resolve(" test@example.com , user@test.com "); - } - if (encString === send.name) { - return Promise.resolve("name"); - } - if (encString === send.notes) { - return Promise.resolve("notes"); - } - return Promise.resolve(""); - }); - const view = await send.decrypt(userId); - expect(view.emails).toEqual(["test@example.com", "user@test.com"]); }); @@ -293,61 +244,17 @@ describe("Send", () => { expect(encryptService.decryptString).not.toHaveBeenCalledWith(expect.anything(), "cryptoKey"); }); - it("should return empty array when decrypted emails is empty string", async () => { + it("should return empty array when emails is empty string", async () => { const send = new Send(); send.id = "id"; send.type = SendType.Text; send.name = mockEnc("name"); send.notes = mockEnc("notes"); send.key = mockEnc("key"); - send.emails = mockEnc(""); + send.emails = ""; send.text = mock(); send.text.decrypt = jest.fn().mockResolvedValue("textView" as any); - - encryptService.decryptString.mockImplementation((encString, key) => { - if (encString === send.emails) { - return Promise.resolve(""); - } - if (encString === send.name) { - return Promise.resolve("name"); - } - if (encString === send.notes) { - return Promise.resolve("notes"); - } - return Promise.resolve(""); - }); - const view = await send.decrypt(userId); - - expect(view.emails).toEqual([]); - }); - - it("should return empty array when decrypted emails is null", async () => { - const send = new Send(); - send.id = "id"; - send.type = SendType.Text; - send.name = mockEnc("name"); - send.notes = mockEnc("notes"); - send.key = mockEnc("key"); - send.emails = mockEnc("something"); - send.text = mock(); - send.text.decrypt = jest.fn().mockResolvedValue("textView" as any); - - encryptService.decryptString.mockImplementation((encString, key) => { - if (encString === send.emails) { - return Promise.resolve(null); - } - if (encString === send.name) { - return Promise.resolve("name"); - } - if (encString === send.notes) { - return Promise.resolve("notes"); - } - return Promise.resolve(""); - }); - - const view = await send.decrypt(userId); - expect(view.emails).toEqual([]); }); }); diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index 5247d35c655..6e35bde8bfc 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -31,8 +31,7 @@ export class Send extends Domain { expirationDate: Date; deletionDate: Date; password: string; - emails: EncString; - emailHashes: string; + emails: string; disabled: boolean; hideEmail: boolean; authType: AuthType; @@ -52,7 +51,6 @@ export class Send extends Domain { name: null, notes: null, key: null, - emails: null, }, ["id", "accessId"], ); @@ -62,13 +60,13 @@ export class Send extends Domain { this.maxAccessCount = obj.maxAccessCount; this.accessCount = obj.accessCount; this.password = obj.password; - this.emailHashes = obj.emailHashes; this.disabled = obj.disabled; this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.deletionDate = obj.deletionDate != null ? new Date(obj.deletionDate) : null; this.expirationDate = obj.expirationDate != null ? new Date(obj.expirationDate) : null; this.hideEmail = obj.hideEmail; this.authType = obj.authType; + this.emails = obj.emails; switch (this.type) { case SendType.Text: @@ -100,8 +98,7 @@ export class Send extends Domain { this.notes != null ? await encryptService.decryptString(this.notes, model.cryptoKey) : null; if (this.emails != null) { - const decryptedEmails = await encryptService.decryptString(this.emails, model.cryptoKey); - model.emails = decryptedEmails ? decryptedEmails.split(",").map((e) => e.trim()) : []; + model.emails = this.emails ? this.emails.split(",").map((e) => e.trim()) : []; } else { model.emails = []; } @@ -133,7 +130,7 @@ export class Send extends Domain { key: EncString.fromJSON(obj.key), name: EncString.fromJSON(obj.name), notes: EncString.fromJSON(obj.notes), - emails: EncString.fromJSON(obj.emails), + emails: obj.emails, text: SendText.fromJSON(obj.text), file: SendFile.fromJSON(obj.file), revisionDate, diff --git a/libs/common/src/tools/send/models/request/send.request.spec.ts b/libs/common/src/tools/send/models/request/send.request.spec.ts index 1daee1d01ff..48119e372c1 100644 --- a/libs/common/src/tools/send/models/request/send.request.spec.ts +++ b/libs/common/src/tools/send/models/request/send.request.spec.ts @@ -8,44 +8,6 @@ import { SendRequest } from "./send.request"; describe("SendRequest", () => { describe("constructor", () => { - it("should populate emails with encrypted string from Send.emails", () => { - const send = new Send(); - send.type = SendType.Text; - send.name = new EncString("encryptedName"); - send.notes = new EncString("encryptedNotes"); - send.key = new EncString("encryptedKey"); - send.emails = new EncString("encryptedEmailList"); - send.emailHashes = "HASH1,HASH2,HASH3"; - send.disabled = false; - send.hideEmail = false; - send.text = new SendText(); - send.text.text = new EncString("text"); - send.text.hidden = false; - - const request = new SendRequest(send); - - expect(request.emails).toBe("encryptedEmailList"); - }); - - it("should populate emailHashes from Send.emailHashes", () => { - const send = new Send(); - send.type = SendType.Text; - send.name = new EncString("encryptedName"); - send.notes = new EncString("encryptedNotes"); - send.key = new EncString("encryptedKey"); - send.emails = new EncString("encryptedEmailList"); - send.emailHashes = "HASH1,HASH2,HASH3"; - send.disabled = false; - send.hideEmail = false; - send.text = new SendText(); - send.text.text = new EncString("text"); - send.text.hidden = false; - - const request = new SendRequest(send); - - expect(request.emailHashes).toBe("HASH1,HASH2,HASH3"); - }); - it("should set emails to null when Send.emails is null", () => { const send = new Send(); send.type = SendType.Text; @@ -53,7 +15,6 @@ describe("SendRequest", () => { send.notes = new EncString("encryptedNotes"); send.key = new EncString("encryptedKey"); send.emails = null; - send.emailHashes = ""; send.disabled = false; send.hideEmail = false; send.text = new SendText(); @@ -63,45 +24,6 @@ describe("SendRequest", () => { const request = new SendRequest(send); expect(request.emails).toBeNull(); - expect(request.emailHashes).toBe(""); - }); - - it("should handle empty emailHashes", () => { - const send = new Send(); - send.type = SendType.Text; - send.name = new EncString("encryptedName"); - send.key = new EncString("encryptedKey"); - send.emails = null; - send.emailHashes = ""; - send.disabled = false; - send.hideEmail = false; - send.text = new SendText(); - send.text.text = new EncString("text"); - send.text.hidden = false; - - const request = new SendRequest(send); - - expect(request.emailHashes).toBe(""); - }); - - it("should not expose plaintext emails", () => { - const send = new Send(); - send.type = SendType.Text; - send.name = new EncString("encryptedName"); - send.key = new EncString("encryptedKey"); - send.emails = new EncString("2.encrypted|emaildata|here"); - send.emailHashes = "ABC123,DEF456"; - send.disabled = false; - send.hideEmail = false; - send.text = new SendText(); - send.text.text = new EncString("text"); - send.text.hidden = false; - - const request = new SendRequest(send); - - // Ensure the request contains the encrypted string format, not plaintext - expect(request.emails).toBe("2.encrypted|emaildata|here"); - expect(request.emails).not.toContain("@"); }); it("should handle name being null", () => { @@ -111,7 +33,6 @@ describe("SendRequest", () => { send.notes = new EncString("encryptedNotes"); send.key = new EncString("encryptedKey"); send.emails = null; - send.emailHashes = ""; send.disabled = false; send.hideEmail = false; send.text = new SendText(); @@ -130,7 +51,6 @@ describe("SendRequest", () => { send.notes = null; send.key = new EncString("encryptedKey"); send.emails = null; - send.emailHashes = ""; send.disabled = false; send.hideEmail = false; send.text = new SendText(); @@ -148,7 +68,6 @@ describe("SendRequest", () => { send.name = new EncString("encryptedName"); send.key = new EncString("encryptedKey"); send.emails = null; - send.emailHashes = ""; send.disabled = false; send.hideEmail = false; send.text = new SendText(); @@ -160,33 +79,4 @@ describe("SendRequest", () => { expect(request.fileLength).toBe(1024); }); }); - - describe("Email auth requirements", () => { - it("should create request with encrypted emails and plaintext emailHashes", () => { - // Setup: A Send with encrypted emails and computed hashes - const send = new Send(); - send.type = SendType.Text; - send.name = new EncString("encryptedName"); - send.key = new EncString("encryptedKey"); - send.emails = new EncString("2.encryptedEmailString|data"); - send.emailHashes = "A1B2C3D4,E5F6G7H8"; // Plaintext hashes - send.disabled = false; - send.hideEmail = false; - send.text = new SendText(); - send.text.text = new EncString("text"); - send.text.hidden = false; - - // Act: Create the request - const request = new SendRequest(send); - - // emails field contains encrypted value - expect(request.emails).toBe("2.encryptedEmailString|data"); - expect(request.emails).toContain("encrypted"); - - //emailHashes field contains plaintext comma-separated hashes - expect(request.emailHashes).toBe("A1B2C3D4,E5F6G7H8"); - expect(request.emailHashes).not.toContain("encrypted"); - expect(request.emailHashes.split(",")).toHaveLength(2); - }); - }); }); diff --git a/libs/common/src/tools/send/models/request/send.request.ts b/libs/common/src/tools/send/models/request/send.request.ts index 37590e40108..902ca0a2c54 100644 --- a/libs/common/src/tools/send/models/request/send.request.ts +++ b/libs/common/src/tools/send/models/request/send.request.ts @@ -18,7 +18,6 @@ export class SendRequest { file: SendFileApi; password: string; emails: string; - emailHashes: string; disabled: boolean; hideEmail: boolean; @@ -32,8 +31,7 @@ export class SendRequest { this.deletionDate = send.deletionDate != null ? send.deletionDate.toISOString() : null; this.key = send.key != null ? send.key.encryptedString : null; this.password = send.password; - this.emails = send.emails ? send.emails.encryptedString : null; - this.emailHashes = send.emailHashes; + this.emails = send.emails; this.disabled = send.disabled; this.hideEmail = send.hideEmail; diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 1c587327098..d29dc81389f 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -1,7 +1,6 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; @@ -51,7 +50,6 @@ describe("SendService", () => { const keyGenerationService = mock(); const encryptService = mock(); const environmentService = mock(); - const cryptoFunctionService = mock(); const configService = mock(); let sendStateProvider: SendStateProvider; let sendService: SendService; @@ -98,7 +96,6 @@ describe("SendService", () => { keyGenerationService, sendStateProvider, encryptService, - cryptoFunctionService, configService, ); }); @@ -612,111 +609,50 @@ describe("SendService", () => { describe("when SendEmailOTP feature flag is ON", () => { beforeEach(() => { configService.getFeatureFlag.mockResolvedValue(true); - cryptoFunctionService.hash.mockClear(); }); - describe("email encryption", () => { - it("should encrypt emails when email list is provided", async () => { + describe("email processing", () => { + it("should create a comma separated string when an email list is provided", async () => { sendView.emails = ["test@example.com", "user@test.com"]; - cryptoFunctionService.hash.mockResolvedValue(new Uint8Array([0xab, 0xcd])); - const [send] = await sendService.encrypt(sendView, null, null); - - expect(encryptService.encryptString).toHaveBeenCalledWith( - "test@example.com,user@test.com", - mockCryptoKey, - ); - expect(send.emails).toEqual({ encryptedString: "encrypted" }); + expect(send.emails).toEqual("test@example.com,user@test.com"); expect(send.password).toBeNull(); }); it("should set emails to null when email list is empty", async () => { sendView.emails = []; - const [send] = await sendService.encrypt(sendView, null, null); - expect(send.emails).toBeNull(); - expect(send.emailHashes).toBe(""); }); it("should set emails to null when email list is null", async () => { sendView.emails = null; - const [send] = await sendService.encrypt(sendView, null, null); - expect(send.emails).toBeNull(); - expect(send.emailHashes).toBe(""); }); it("should set emails to null when email list is undefined", async () => { sendView.emails = undefined; - const [send] = await sendService.encrypt(sendView, null, null); - expect(send.emails).toBeNull(); - expect(send.emailHashes).toBe(""); - }); - }); - - describe("email hashing", () => { - it("should hash emails using SHA-256 and return uppercase hex", async () => { - sendView.emails = ["test@example.com"]; - const mockHash = new Uint8Array([0xab, 0xcd, 0xef]); - - cryptoFunctionService.hash.mockResolvedValue(mockHash); - - const [send] = await sendService.encrypt(sendView, null, null); - - expect(cryptoFunctionService.hash).toHaveBeenCalledWith("test@example.com", "sha256"); - expect(send.emailHashes).toBe("ABCDEF"); }); - it("should hash multiple emails and return comma-separated hashes", async () => { + it("should process multiple emails and return comma-separated string", async () => { sendView.emails = ["test@example.com", "user@test.com"]; - const mockHash1 = new Uint8Array([0xab, 0xcd]); - const mockHash2 = new Uint8Array([0x12, 0x34]); - - cryptoFunctionService.hash - .mockResolvedValueOnce(mockHash1) - .mockResolvedValueOnce(mockHash2); - const [send] = await sendService.encrypt(sendView, null, null); - - expect(cryptoFunctionService.hash).toHaveBeenCalledWith("test@example.com", "sha256"); - expect(cryptoFunctionService.hash).toHaveBeenCalledWith("user@test.com", "sha256"); - expect(send.emailHashes).toBe("ABCD,1234"); + expect(send.emails).toBe("test@example.com,user@test.com"); }); - it("should trim and lowercase emails before hashing", async () => { + it("should trim and lowercase emails", async () => { sendView.emails = [" Test@Example.COM ", "USER@test.com"]; - const mockHash = new Uint8Array([0xff]); - - cryptoFunctionService.hash.mockResolvedValue(mockHash); - - await sendService.encrypt(sendView, null, null); - - expect(cryptoFunctionService.hash).toHaveBeenCalledWith("test@example.com", "sha256"); - expect(cryptoFunctionService.hash).toHaveBeenCalledWith("user@test.com", "sha256"); - }); - - it("should set emailHashes to empty string when no emails", async () => { - sendView.emails = []; - const [send] = await sendService.encrypt(sendView, null, null); - - expect(send.emailHashes).toBe(""); - expect(cryptoFunctionService.hash).not.toHaveBeenCalled(); + expect(send.emails).toBe("test@example.com,user@test.com"); }); it("should handle single email correctly", async () => { sendView.emails = ["single@test.com"]; - const mockHash = new Uint8Array([0xa1, 0xb2, 0xc3]); - - cryptoFunctionService.hash.mockResolvedValue(mockHash); - const [send] = await sendService.encrypt(sendView, null, null); - - expect(send.emailHashes).toBe("A1B2C3"); + expect(send.emails).toBe("single@test.com"); }); }); @@ -747,7 +683,6 @@ describe("SendService", () => { describe("when SendEmailOTP feature flag is OFF", () => { beforeEach(() => { configService.getFeatureFlag.mockResolvedValue(false); - cryptoFunctionService.hash.mockClear(); }); it("should NOT encrypt emails even when provided", async () => { @@ -756,8 +691,6 @@ describe("SendService", () => { const [send] = await sendService.encrypt(sendView, null, null); expect(send.emails).toBeNull(); - expect(send.emailHashes).toBe(""); - expect(cryptoFunctionService.hash).not.toHaveBeenCalled(); }); it("should use password when provided and flag is OFF", async () => { @@ -769,7 +702,6 @@ describe("SendService", () => { const [send] = await sendService.encrypt(sendView, null, "password123"); expect(send.emails).toBeNull(); - expect(send.emailHashes).toBe(""); expect(send.password).toBe("hashedPassword"); }); @@ -782,9 +714,7 @@ describe("SendService", () => { const [send] = await sendService.encrypt(sendView, null, "password123"); expect(send.emails).toBeNull(); - expect(send.emailHashes).toBe(""); expect(send.password).toBe("hashedPassword"); - expect(cryptoFunctionService.hash).not.toHaveBeenCalled(); }); it("should set emails and password to null when neither provided", async () => { @@ -793,7 +723,6 @@ describe("SendService", () => { const [send] = await sendService.encrypt(sendView, null, null); expect(send.emails).toBeNull(); - expect(send.emailHashes).toBe(""); expect(send.password).toBeUndefined(); }); }); diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 078e94b2563..93031b61c9f 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -9,7 +9,6 @@ import { PBKDF2KdfConfig, KeyService } from "@bitwarden/key-management"; import { FeatureFlag } from "../../../enums/feature-flag.enum"; import { KeyGenerationService } from "../../../key-management/crypto"; -import { CryptoFunctionService } from "../../../key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import { ConfigService } from "../../../platform/abstractions/config/config.service"; @@ -54,7 +53,6 @@ export class SendService implements InternalSendServiceAbstraction { private keyGenerationService: KeyGenerationService, private stateProvider: SendStateProvider, private encryptService: EncryptService, - private cryptoFunctionService: CryptoFunctionService, private configService: ConfigService, ) {} @@ -91,13 +89,13 @@ export class SendService implements InternalSendServiceAbstraction { const hasEmails = (model.emails?.length ?? 0) > 0; if (sendEmailOTPEnabled && hasEmails) { - const plaintextEmails = model.emails.join(","); - send.emails = await this.encryptService.encryptString(plaintextEmails, model.cryptoKey); - send.emailHashes = await this.hashEmails(plaintextEmails); + send.emails = model.emails + .map((e) => e.trim()) + .join(",") + .toLocaleLowerCase(); send.password = null; } else { send.emails = null; - send.emailHashes = ""; if (password != null) { // Note: Despite being called key, the passwordKey is not used for encryption. @@ -393,19 +391,4 @@ export class SendService implements InternalSendServiceAbstraction { decryptedSends.sort(Utils.getSortFunction(this.i18nService, "name")); return decryptedSends; } - - private async hashEmails(emails: string): Promise { - if (!emails) { - return ""; - } - - const emailArray = emails.split(",").map((e) => e.trim().toLowerCase()); - const hashPromises = emailArray.map(async (email) => { - const hash: Uint8Array = await this.cryptoFunctionService.hash(email, "sha256"); - return Utils.fromBufferToHex(hash).toUpperCase(); - }); - - const hashes = await Promise.all(hashPromises); - return hashes.join(","); - } } diff --git a/libs/common/src/tools/send/services/test-data/send-tests.data.ts b/libs/common/src/tools/send/services/test-data/send-tests.data.ts index 9c4e121edc0..6c901be8fd4 100644 --- a/libs/common/src/tools/send/services/test-data/send-tests.data.ts +++ b/libs/common/src/tools/send/services/test-data/send-tests.data.ts @@ -41,7 +41,6 @@ export function createSendData(value: Partial = {}) { deletionDate: "2024-09-04", password: "password", emails: "", - emailHashes: "", disabled: false, hideEmail: false, }; @@ -66,7 +65,6 @@ export function testSendData(id: string, name: string) { data.notes = "Notes!!"; data.key = null; data.emails = ""; - data.emailHashes = ""; return data; } @@ -82,7 +80,6 @@ export function testSend(id: string, name: string) { data.deletionDate = null; data.notes = new EncString("Notes!!"); data.key = null; - data.emails = null; - data.emailHashes = ""; + data.emails = ""; return data; } From e92817011b859012e61f31f58fc90dc2c2f6b072 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 9 Feb 2026 16:05:19 -0500 Subject: [PATCH 04/13] [PM 29531]Remove ts strict ignore in list autofill inline menu list ts (#18738) * fix(autofill): type throttle to preserve handler this/args and return void * fix(autofill): strict TS and defaults for inline menu list, throttle typing, TOTP interval * update snapshots * swap mouse event for event * prevent default does nothing on event --- .../autofill-inline-menu-list.spec.ts.snap | 12 +- .../list/autofill-inline-menu-list.spec.ts | 4 + .../pages/list/autofill-inline-menu-list.ts | 112 +++++++++++------- apps/browser/src/autofill/utils/index.ts | 19 +-- 4 files changed, 90 insertions(+), 57 deletions(-) diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap index 22e3a765666..53a055075fe 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap @@ -716,7 +716,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f stroke-dasharray="78.5" stroke-dashoffset="78.5" stroke-width="3" - style="stroke-dashoffset: NaN;" + style="stroke-dashoffset: 34.033920413889426;" transform="rotate(-90 14.5 14.5)" /> @@ -737,7 +737,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f bittypography="helper" class="totp-sec-span" > - NaN + 17 @@ -2115,7 +2115,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f stroke-dasharray="78.5" stroke-dashoffset="78.5" stroke-width="3" - style="stroke-dashoffset: NaN;" + style="stroke-dashoffset: 34.033920413889426;" transform="rotate(-90 14.5 14.5)" /> @@ -2136,7 +2136,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f bittypography="helper" class="totp-sec-span" > - NaN + 17 @@ -2227,7 +2227,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f stroke-dasharray="78.5" stroke-dashoffset="78.5" stroke-width="3" - style="stroke-dashoffset: NaN;" + style="stroke-dashoffset: 34.033920413889426;" transform="rotate(-90 14.5 14.5)" /> @@ -2248,7 +2248,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f bittypography="helper" class="totp-sec-span" > - NaN + 17 diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index 81bf7240230..1e99ac9df90 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -157,6 +157,8 @@ describe("AutofillInlineMenuList", () => { }); it("creates the view for a totp field", async () => { + jest.spyOn(Date, "now").mockReturnValue(13000); + postWindowMessage( createInitAutofillInlineMenuListMessageMock({ inlineMenuFillType: CipherType.Login, @@ -184,6 +186,8 @@ describe("AutofillInlineMenuList", () => { }); it("renders correctly when there are multiple TOTP elements with username displayed", async () => { + jest.spyOn(Date, "now").mockReturnValue(13000); + const totpCipher1 = createAutofillOverlayCipherDataMock(1, { type: CipherType.Login, login: { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index c680fe4745c..c13c523e30a 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import "@webcomponents/custom-elements"; import "lit/polyfill-support.js"; @@ -33,27 +31,36 @@ import { import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element"; export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { - private inlineMenuListContainer: HTMLDivElement; - private passwordGeneratorContainer: HTMLDivElement; + /** Non-null asserted. Set in initAutofillInlineMenuList before any read. */ + private inlineMenuListContainer!: HTMLDivElement; + /** Non-null asserted. Set in initAutofillInlineMenuList before any read. */ + private passwordGeneratorContainer!: HTMLDivElement; private resizeObserver: ResizeObserver; private eventHandlersMemo: { [key: string]: EventListener } = {}; private ciphers: InlineMenuCipherData[] = []; - private ciphersList: HTMLUListElement; + /** Non-null asserted. Set in buildInlineMenuList before any read. */ + private ciphersList!: HTMLUListElement; private cipherListScrollIsDebounced = false; - private cipherListScrollDebounceTimeout: number | NodeJS.Timeout; + private cipherListScrollDebounceTimeout: number | ReturnType = 0; private currentCipherIndex = 0; - private inlineMenuFillType: InlineMenuFillType; - private showInlineMenuAccountCreation: boolean; - private showPasskeysLabels: boolean; - private newItemButtonElement: HTMLButtonElement; - private passkeysHeadingElement: HTMLLIElement; - private loginHeadingElement: HTMLLIElement; - private lastPasskeysListItem: HTMLLIElement; - private passkeysHeadingHeight: number; - private lastPasskeysListItemHeight: number; - private ciphersListHeight: number; + /** Non-null asserted. Set in initAutofillInlineMenuList from message. */ + private inlineMenuFillType!: InlineMenuFillType; + private showInlineMenuAccountCreation = false; + private showPasskeysLabels = false; + /** Non-null asserted. Set in buildNewItemButton before any read. */ + private newItemButtonElement!: HTMLButtonElement; + /** Conditionally set in buildPasskeysHeadingElements, may be undefined when no passkeys. */ + private passkeysHeadingElement?: HTMLLIElement; + /** Conditionally set in buildPasskeysHeadingElements, may be undefined when no login heading. */ + private loginHeadingElement?: HTMLLIElement; + /** Conditionally set in buildInlineMenuListActionsItem when showPasskeysLabels and passkey cipher. */ + private lastPasskeysListItem?: HTMLLIElement; + private passkeysHeadingHeight = 0; + private lastPasskeysListItemHeight = 0; + private ciphersListHeight = 0; private isPasskeyAuthInProgress = false; - private authStatus: AuthenticationStatus; + private authStatus: AuthenticationStatus = AuthenticationStatus.Locked; + private isInitialized = false; private readonly showCiphersPerPage = 6; private readonly headingBorderClass = "inline-menu-list-heading--bordered"; private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers = @@ -70,6 +77,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { constructor() { super(); + this.resizeObserver = new ResizeObserver(this.handleResizeObserver); this.setupInlineMenuListGlobalListeners(); } @@ -85,11 +93,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { styleSheetUrl, theme, authStatus, - ciphers, + ciphers = [], portKey, - inlineMenuFillType, - showInlineMenuAccountCreation, - showPasskeysLabels, + inlineMenuFillType = CipherType.Login, + showInlineMenuAccountCreation = false, + showPasskeysLabels = false, generatedPassword, showSaveLoginMenu, } = message; @@ -112,6 +120,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { this.resizeObserver.observe(this.inlineMenuListContainer); this.shadowDom.append(linkElement, this.inlineMenuListContainer); + this.isInitialized = true; if (authStatus !== AuthenticationStatus.Unlocked) { this.buildLockedInlineMenu(); @@ -368,7 +377,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { event.target.nextElementSibling ) { (event.target.nextElementSibling as HTMLElement).focus(); - event.target.parentElement.classList.add("remove-outline"); + event.target.parentElement?.classList.add("remove-outline"); return; } }; @@ -409,7 +418,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { event.target.previousElementSibling ) { (event.target.previousElementSibling as HTMLElement).focus(); - event.target.parentElement.classList.remove("remove-outline"); + event.target.parentElement?.classList.remove("remove-outline"); return; } }; @@ -473,8 +482,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields. */ private updateListItems({ - ciphers, - showInlineMenuAccountCreation, + ciphers = [], + showInlineMenuAccountCreation = false, }: UpdateAutofillInlineMenuListCiphersParams) { if (this.isPasskeyAuthInProgress) { return; @@ -655,7 +664,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * scroll listeners that reposition the passkeys and login headings when the user scrolls. */ private setupCipherListScrollListeners() { - const options = { passive: true }; + const options: AddEventListenerOptions = { passive: true }; this.ciphersList.addEventListener(EVENTS.SCROLL, this.updateCiphersListOnScroll, options); if (this.showPasskeysLabels) { this.ciphersList.addEventListener( @@ -673,8 +682,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles updating the list of ciphers when the * user scrolls to the bottom of the list. */ - private updateCiphersListOnScroll = (event: MouseEvent) => { - event.preventDefault(); + private updateCiphersListOnScroll = (event: Event) => { event.stopPropagation(); if (this.cipherListScrollIsDebounced) { @@ -721,8 +729,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * * @param event - The scroll event. */ - private handleThrottledOnScrollEvent = (event: MouseEvent) => { - event.preventDefault(); + private handleThrottledOnScrollEvent = (event: Event) => { event.stopPropagation(); this.updatePasskeysHeadingsOnScroll(this.ciphersList.scrollTop); @@ -754,6 +761,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipherListScrollTop - The current scroll top position of the ciphers list. */ private togglePasskeysHeadingAnchored(cipherListScrollTop: number) { + if (!this.passkeysHeadingElement || !this.lastPasskeysListItem) { + return; + } if (!this.passkeysHeadingHeight) { this.passkeysHeadingHeight = this.passkeysHeadingElement.offsetHeight; } @@ -776,6 +786,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipherListScrollTop - The current scroll top position of the ciphers list. */ private togglePasskeysHeadingBorder(cipherListScrollTop: number) { + if (!this.passkeysHeadingElement) { + return; + } if (cipherListScrollTop < 1) { this.passkeysHeadingElement.classList.remove(this.headingBorderClass); return; @@ -791,6 +804,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipherListScrollTop - The current scroll top position of the ciphers list. */ private toggleLoginHeadingBorder(cipherListScrollTop: number) { + if (!this.loginHeadingElement || !this.lastPasskeysListItem) { + return; + } if (!this.lastPasskeysListItemHeight) { this.lastPasskeysListItemHeight = this.lastPasskeysListItem.offsetHeight; } @@ -884,7 +900,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { ); this.addFillCipherElementAriaDescription(fillCipherElement, cipher); - fillCipherElement.append(cipherIcon, cipherDetailsElement); + fillCipherElement.append(cipherIcon, ...(cipherDetailsElement ? [cipherDetailsElement] : [])); fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher)); fillCipherElement.addEventListener(EVENTS.KEYUP, this.handleFillCipherKeyUpEvent); @@ -1126,7 +1142,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { cipherIcon.appendChild(totpContainer); - const intervalSeconds = cipher.login.totpCodeTimeInterval; + const intervalSeconds = cipher.login.totpCodeTimeInterval ?? 30; const updateCountdown = () => { const epoch = Math.round(Date.now() / 1000); @@ -1266,7 +1282,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { if (this.multipleTotpElements() && username) { const usernameSubtitle = this.buildCipherSubtitleElement(username); - containerElement.appendChild(usernameSubtitle); + if (usernameSubtitle) { + containerElement.appendChild(usernameSubtitle); + } } const totpCodeSpan = document.createElement("span"); @@ -1326,19 +1344,25 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { cipher: InlineMenuCipherData, cipherDetailsElement: HTMLSpanElement, ): HTMLSpanElement { - let rpNameSubtitle: HTMLSpanElement; + const login = cipher.login; + const passkey = login?.passkey; + if (!login || !passkey) { + return cipherDetailsElement; + } - if (cipher.name !== cipher.login.passkey.rpName) { - rpNameSubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.rpName); - if (rpNameSubtitle) { + let rpNameSubtitle: HTMLSpanElement | undefined; + if (cipher.name !== passkey.rpName) { + const element = this.buildCipherSubtitleElement(passkey.rpName); + if (element) { + rpNameSubtitle = element; rpNameSubtitle.prepend(buildSvgDomElement(passkeyIcon)); rpNameSubtitle.classList.add("cipher-subtitle--passkey"); cipherDetailsElement.appendChild(rpNameSubtitle); } } - if (cipher.login.username) { - const usernameSubtitle = this.buildCipherSubtitleElement(cipher.login.username); + if (login.username) { + const usernameSubtitle = this.buildCipherSubtitleElement(login.username); if (usernameSubtitle) { if (!rpNameSubtitle) { usernameSubtitle.prepend(buildSvgDomElement(passkeyIcon)); @@ -1350,7 +1374,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { return cipherDetailsElement; } - const passkeySubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.userName); + const passkeySubtitle = this.buildCipherSubtitleElement(passkey.userName); if (passkeySubtitle) { if (!rpNameSubtitle) { passkeySubtitle.prepend(buildSvgDomElement(passkeyIcon)); @@ -1412,6 +1436,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * If not focused, will check if the button element is focused. */ private checkInlineMenuListFocused() { + if (!this.isInitialized) { + return; + } if (globalThis.document.hasFocus()) { return; } @@ -1450,6 +1477,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * the first cipher button. */ private focusInlineMenuList() { + if (!this.isInitialized) { + return; + } this.inlineMenuListContainer.setAttribute("role", "dialog"); this.inlineMenuListContainer.setAttribute("aria-modal", "true"); @@ -1472,8 +1502,6 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { */ private setupInlineMenuListGlobalListeners() { this.setupGlobalListeners(this.inlineMenuListWindowMessageHandlers); - - this.resizeObserver = new ResizeObserver(this.handleResizeObserver); } /** diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index dc07ca1e258..fa47ddd943b 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -368,20 +368,21 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri /** * Throttles a callback function to run at most once every `limit` milliseconds. * - * @param callback - The callback function to throttle. + * @param callback - The callback function to throttle (must return void). * @param limit - The time in milliseconds to throttle the callback. */ -export function throttle unknown>( - callback: FunctionType, +export function throttle( + callback: (this: TypeContext, ...args: Args) => void, limit: number, -): (this: ThisParameterType, ...args: Parameters) => void { +): (this: TypeContext, ...args: Args) => void { let waitingDelay = false; - return function (this: ThisParameterType, ...args: Parameters) { - if (!waitingDelay) { - callback.apply(this, args); - waitingDelay = true; - globalThis.setTimeout(() => (waitingDelay = false), limit); + return function (this: TypeContext, ...args: Args) { + if (waitingDelay) { + return; } + callback.apply(this, args); + waitingDelay = true; + globalThis.setTimeout(() => (waitingDelay = false), limit); }; } From 322ff6b70b0732ad8bd85bfea75818658b08c975 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Mon, 9 Feb 2026 16:17:46 -0500 Subject: [PATCH 05/13] [PM-31675] remove archive from web edit (#18764) * refactor default cipher archive service, update archive/unarchive in vault-item-dialog, remove archive/unarchive items in edit form --- .../add-edit/add-edit-v2.component.html | 2 +- .../vault-v2/view-v2/view-v2.component.html | 2 +- .../vault-item-dialog.component.html | 8 ++-- .../vault-item-dialog.component.spec.ts | 31 +++++++++--- .../vault-item-dialog.component.ts | 47 +++++++++++-------- .../default-cipher-archive.service.ts | 36 ++++---------- 6 files changed, 66 insertions(+), 60 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index 8b4d2d21b8b..f8238a188e0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -7,7 +7,7 @@ [backAction]="handleBackButton" showBackButton > - @if (config?.originalCipher?.archivedDate) { + @if (config?.originalCipher?.archivedDate && (archiveFlagEnabled$ | async)) { {{ "archived" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html index 8ac6de75997..a3d65522022 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html @@ -1,7 +1,7 @@ - @if (cipher?.isArchived) { + @if (cipher?.isArchived && (archiveFlagEnabled$ | async)) { {{ "archived" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index ec06c740f24..73670339ca8 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -3,7 +3,7 @@ {{ title }} - @if (isCipherArchived && !params.isAdminConsoleAction) { + @if (isCipherArchived && !params.isAdminConsoleAction && (archiveFlagEnabled$ | async)) { {{ "archived" | i18n }} } @@ -86,8 +86,8 @@ @if (showActionButtons) {
- @if ((userCanArchive$ | async) && !params.isAdminConsoleAction) { - @if (isCipherArchived && !cipher?.isDeleted) { + @if (showArchiveOptions) { + @if (showUnarchiveBtn) { } - @if (cipher?.canBeArchived) { + @if (showArchiveBtn) { - + {{ "howToManageMyVault" | i18n }} - + diff --git a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts index af106376a79..44788a8234a 100644 --- a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts +++ b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.ts @@ -10,6 +10,7 @@ import { DialogService, ButtonModule, DialogModule, + IconModule, LinkModule, TypographyModule, CenterPositionStrategy, @@ -35,7 +36,7 @@ export type LeaveConfirmationDialogResultType = UnionOfValues(DIALOG_DATA); diff --git a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html index 3cf626baaf7..5d1c3ba9aed 100644 --- a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html +++ b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.html @@ -14,9 +14,9 @@ {{ "declineAndLeave" | i18n }} - + {{ "whyAmISeeingThis" | i18n }} - + diff --git a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts index 619181f37fc..45f6305b5b3 100644 --- a/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts +++ b/libs/vault/src/components/vault-items-transfer/transfer-items-dialog.component.ts @@ -10,6 +10,7 @@ import { DialogService, ButtonModule, DialogModule, + IconModule, LinkModule, TypographyModule, CenterPositionStrategy, @@ -35,7 +36,7 @@ export type TransferItemsDialogResultType = UnionOfValues(DIALOG_DATA); From 341de2c378a548aa15acf8d4cc46ddd54f1e3b82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:40:21 -0500 Subject: [PATCH 12/13] [deps]: Update Minor github-actions updates (#18714) * [deps]: Update Minor github-actions updates * Revert bump of create-github-app-token for test-browser-interactions.yml --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith --- .github/workflows/build-browser.yml | 2 +- .github/workflows/build-desktop.yml | 8 ++++---- .github/workflows/build-web.yml | 4 ++-- .github/workflows/lint-crowdin-config.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/publish-desktop.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index ef2c91f0a7d..6a334e31a18 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -565,7 +565,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Upload Sources - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 6818064a808..c500e59d536 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1007,7 +1007,7 @@ jobs: node-version: ${{ env._NODE_VERSION }} - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14.2' @@ -1247,7 +1247,7 @@ jobs: node-version: ${{ env._NODE_VERSION }} - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14.2' @@ -1522,7 +1522,7 @@ jobs: node-version: ${{ env._NODE_VERSION }} - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14.2' @@ -1873,7 +1873,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Upload Sources - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 71a2c62ec1a..688bd30bfe5 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -352,7 +352,7 @@ jobs: - name: Scan Docker image if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: container-scan - uses: anchore/scan-action@62b74fb7bb810d2c45b1865f47a77655621862a5 # v7.2.3 + uses: anchore/scan-action@0d444ed77d83ee2ba7f5ced0d90d640a1281d762 # v7.3.0 with: image: ${{ steps.image-name.outputs.name }} fail-build: false @@ -408,7 +408,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Upload Sources - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/lint-crowdin-config.yml b/.github/workflows/lint-crowdin-config.yml index 61e2b3631e6..e1e620c864d 100644 --- a/.github/workflows/lint-crowdin-config.yml +++ b/.github/workflows/lint-crowdin-config.yml @@ -45,7 +45,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Lint ${{ matrix.app.name }} config - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_PROJECT_ID: ${{ matrix.app.project_id }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7862c14c186..efc8c25fc5e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -142,7 +142,7 @@ jobs: run: cargo +nightly udeps --workspace --all-features --all-targets - name: Install cargo-deny - uses: taiki-e/install-action@542cebaaed782771e619bd5609d97659d109c492 # v2.66.7 + uses: taiki-e/install-action@887bc4e03483810873d617344dd5189cd82e7b8b # v2.67.11 with: tool: cargo-deny@0.18.6 diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index c5db7ea9295..45665f459e8 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -331,7 +331,7 @@ jobs: run: wget "https://github.com/bitwarden/clients/releases/download/${_RELEASE_TAG}/macos-build-number.json" - name: Setup Ruby and Install Fastlane - uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0 + uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0 with: ruby-version: '3.4.7' bundler-cache: false From 6f1a6187147b88bc3bfdf0570b65dd1032b0b0e6 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Tue, 10 Feb 2026 11:46:03 -0800 Subject: [PATCH 13/13] [PM-31732] Fix issue with user flow from vault-item-dialog --- .../vault-item-dialog/vault-item-dialog.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 4a24bbcf3fd..d9eb03ea1ca 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -524,11 +524,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { const dialogRef = this.dialogService.open< AttachmentDialogCloseResult, - { cipherId: CipherId; organizationId?: OrganizationId } + { cipherId: CipherId; organizationId?: OrganizationId; canEditCipher?: boolean } >(AttachmentsV2Component, { data: { cipherId: this.formConfig.originalCipher?.id as CipherId, organizationId: this.formConfig.originalCipher?.organizationId as OrganizationId, + canEditCipher: this.formConfig.originalCipher?.edit, }, });