From 0dd4ed702624249c72c90dccf055b049a0d2f443 Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:54:39 -0600 Subject: [PATCH 1/6] Use official latest phishing sources from phish.co.za (#18271) --- .../src/dirt/phishing-detection/phishing-resources.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/dirt/phishing-detection/phishing-resources.ts b/apps/browser/src/dirt/phishing-detection/phishing-resources.ts index 262d6cf833b..4cd155c8ae3 100644 --- a/apps/browser/src/dirt/phishing-detection/phishing-resources.ts +++ b/apps/browser/src/dirt/phishing-detection/phishing-resources.ts @@ -18,8 +18,7 @@ export const PHISHING_RESOURCES: Record Date: Thu, 8 Jan 2026 15:01:03 -0500 Subject: [PATCH 2/6] Added batch encrypt many method and used that in imports (#18266) --- .../abstractions/cipher-encryption.service.ts | 10 +++ .../src/vault/abstractions/cipher.service.ts | 9 +++ .../src/vault/services/cipher.service.ts | 18 ++++++ .../default-cipher-encryption.service.spec.ts | 62 +++++++++++++++++++ .../default-cipher-encryption.service.ts | 38 ++++++++++++ libs/importer/src/services/import.service.ts | 24 ++++--- 6 files changed, 154 insertions(+), 7 deletions(-) diff --git a/libs/common/src/vault/abstractions/cipher-encryption.service.ts b/libs/common/src/vault/abstractions/cipher-encryption.service.ts index fdd42c0acf2..a3b824fd46e 100644 --- a/libs/common/src/vault/abstractions/cipher-encryption.service.ts +++ b/libs/common/src/vault/abstractions/cipher-encryption.service.ts @@ -20,6 +20,16 @@ export abstract class CipherEncryptionService { */ abstract encrypt(model: CipherView, userId: UserId): Promise; + /** + * Encrypts multiple ciphers using the SDK for the given userId. + * + * @param models The cipher views to encrypt + * @param userId The user ID to initialize the SDK client with + * + * @returns A promise that resolves to an array of encryption contexts + */ + abstract encryptMany(models: CipherView[], userId: UserId): Promise; + /** * Move the cipher to the specified organization by re-encrypting its keys with the organization's key. * The cipher.organizationId will be updated to the new organizationId. diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 0d3a0b99fcb..203984075f7 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -50,6 +50,15 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + /** + * Encrypts multiple ciphers for the given user. + * + * @param models The cipher views to encrypt + * @param userId The user ID to encrypt for + * + * @returns A promise that resolves to an array of encryption contexts + */ + abstract encryptMany(models: CipherView[], userId: UserId): Promise; abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise; abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise; abstract get(id: string, userId: UserId): Promise; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index d25aa62ea3a..2e0adc892e3 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -340,6 +340,24 @@ export class CipherService implements CipherServiceAbstraction { } } + async encryptMany(models: CipherView[], userId: UserId): Promise { + const sdkEncryptionEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM22136_SdkCipherEncryption, + ); + + if (sdkEncryptionEnabled) { + return await this.cipherEncryptionService.encryptMany(models, userId); + } + + // Fallback to sequential encryption if SDK disabled + const results: EncryptionContext[] = []; + for (const model of models) { + const result = await this.encrypt(model, userId); + results.push(result); + } + return results; + } + async encryptAttachments( attachmentsModel: AttachmentView[], key: SymmetricCryptoKey, diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts index f54dfa17a38..a0ca4833b92 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -253,6 +253,68 @@ describe("DefaultCipherEncryptionService", () => { }); }); + describe("encryptMany", () => { + it("should encrypt multiple ciphers", async () => { + const cipherView2 = new CipherView(cipherObj); + cipherView2.name = "test-name-2"; + const cipherView3 = new CipherView(cipherObj); + cipherView3.name = "test-name-3"; + + const ciphers = [cipherViewObj, cipherView2, cipherView3]; + + const expectedCipher1: Cipher = { + id: cipherId as string, + type: CipherType.Login, + name: "encrypted-name-1", + } as unknown as Cipher; + + const expectedCipher2: Cipher = { + id: cipherId as string, + type: CipherType.Login, + name: "encrypted-name-2", + } as unknown as Cipher; + + const expectedCipher3: Cipher = { + id: cipherId as string, + type: CipherType.Login, + name: "encrypted-name-3", + } as unknown as Cipher; + + mockSdkClient.vault().ciphers().encrypt.mockReturnValue({ + cipher: sdkCipher, + encryptedFor: userId, + }); + + jest + .spyOn(Cipher, "fromSdkCipher") + .mockReturnValueOnce(expectedCipher1) + .mockReturnValueOnce(expectedCipher2) + .mockReturnValueOnce(expectedCipher3); + + const results = await cipherEncryptionService.encryptMany(ciphers, userId); + + expect(results).toBeDefined(); + expect(results.length).toBe(3); + expect(results[0].cipher).toEqual(expectedCipher1); + expect(results[1].cipher).toEqual(expectedCipher2); + expect(results[2].cipher).toEqual(expectedCipher3); + + expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledTimes(3); + + expect(results[0].encryptedFor).toBe(userId); + expect(results[1].encryptedFor).toBe(userId); + expect(results[2].encryptedFor).toBe(userId); + }); + + it("should handle empty array", async () => { + const results = await cipherEncryptionService.encryptMany([], userId); + + expect(results).toBeDefined(); + expect(results.length).toBe(0); + expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled(); + }); + }); + describe("encryptCipherForRotation", () => { it("should call the sdk method to encrypt the cipher with a new key for rotation", async () => { mockSdkClient.vault().ciphers().encrypt_cipher_for_rotation.mockReturnValue({ diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index f1b737ed50f..588265846e0 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -51,6 +51,44 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { ); } + async encryptMany(models: CipherView[], userId: UserId): Promise { + if (!models || models.length === 0) { + return []; + } + + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + + const results: EncryptionContext[] = []; + + // TODO: https://bitwarden.atlassian.net/browse/PM-30580 + // Replace this loop with a native SDK encryptMany method for better performance. + for (const model of models) { + const sdkCipherView = this.toSdkCipherView(model, ref.value); + const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView); + + results.push({ + cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, + encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId, + }); + } + + return results; + }), + catchError((error: unknown) => { + this.logService.error(`Failed to encrypt ciphers in batch: ${error}`); + return EMPTY; + }), + ), + ); + } + async moveToOrganization( model: CipherView, organizationId: OrganizationId, diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 400beae5179..829bd04e994 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -374,10 +374,13 @@ export class ImportService implements ImportServiceAbstraction { private async handleIndividualImport(importResult: ImportResult, userId: UserId) { const request = new ImportCiphersRequest(); - for (let i = 0; i < importResult.ciphers.length; i++) { - const c = await this.cipherService.encrypt(importResult.ciphers[i], userId); - request.ciphers.push(new CipherRequest(c)); + + const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId); + + for (const encryptedCipher of encryptedCiphers) { + request.ciphers.push(new CipherRequest(encryptedCipher)); } + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); if (importResult.folders != null) { @@ -400,11 +403,18 @@ export class ImportService implements ImportServiceAbstraction { userId: UserId, ) { const request = new ImportOrganizationCiphersRequest(); - for (let i = 0; i < importResult.ciphers.length; i++) { - importResult.ciphers[i].organizationId = organizationId; - const c = await this.cipherService.encrypt(importResult.ciphers[i], userId); - request.ciphers.push(new CipherRequest(c)); + + // Set organization ID on all ciphers before batch encryption + importResult.ciphers.forEach((cipher) => { + cipher.organizationId = organizationId; + }); + + const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId); + + for (const encryptedCipher of encryptedCiphers) { + request.ciphers.push(new CipherRequest(encryptedCipher)); } + if (importResult.collections != null) { for (let i = 0; i < importResult.collections.length; i++) { importResult.collections[i].organizationId = organizationId; From 6579e31374cc96fc0b0d4debea8c7e3a429c0bf6 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 8 Jan 2026 16:16:08 -0500 Subject: [PATCH 3/6] [PM-30537] add tab nav to restart premium link (#18269) --- apps/web/src/app/vault/individual-vault/vault.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index df1b727154f..cb5332d07d8 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -43,7 +43,9 @@
{{ "premiumSubscriptionEndedDesc" | i18n }}
- {{ "restartPremium" | i18n }} + + {{ "restartPremium" | i18n }} + } From eedc36cf3959f8c8da1d350c45697960e16e1e65 Mon Sep 17 00:00:00 2001 From: blackwood Date: Thu, 8 Jan 2026 16:46:13 -0500 Subject: [PATCH 4/6] Revert "Display autofill overlay for zoom.us signin password (#16900)" (#18261) --- .../services/autofill-overlay-content.service.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 2087b0640fb..7ea89e114ab 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -1086,15 +1086,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ pageDetails, ) ) { - const hasUsernameField = [...this.formFieldElements.values()].some((field) => - this.inlineMenuFieldQualificationService.isUsernameField(field), - ); - - if (hasUsernameField) { - void this.setQualifiedLoginFillType(autofillFieldData); - } else { - this.setQualifiedAccountCreationFillType(autofillFieldData); - } + this.setQualifiedAccountCreationFillType(autofillFieldData); return false; } From 4aa69a769be8228c4001225c2b448d2a092b6927 Mon Sep 17 00:00:00 2001 From: Zhaolin Liang Date: Fri, 9 Jan 2026 07:19:56 +0800 Subject: [PATCH 5/6] [PM-25402] auto-assign new logins to current folder/collection (#16268) * auto-assign selected collection for new vault items * Ensure a selected collectionId in the vault filter is passed on to a newly created cipher. Fixes #15485 * Assign selected folder and collection when creating a new cipher Added here to prevent a regression whenever we switch over to this componet and deprecate vault-v2.component * account for null folderIds --------- Co-authored-by: Daniel James Smith Co-authored-by: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Co-authored-by: jaasen-livefront --- apps/desktop/src/vault/app/vault-v3/vault.component.ts | 10 +++++----- apps/desktop/src/vault/app/vault/vault-v2.component.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.ts index 21ba7547f8b..a16ef93e230 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts @@ -34,7 +34,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getByIds } from "@bitwarden/common/platform/misc"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -158,7 +158,7 @@ export class VaultComponent cipherId: string | null = null; favorites = false; type: CipherType | null = null; - folderId: string | null = null; + folderId: string | null | undefined = null; collectionId: string | null = null; organizationId: string | null = null; myVaultOnly = false; @@ -980,9 +980,7 @@ export class VaultComponent // clear out organizationId when the user switches to a personal vault filter this.addOrganizationId = null; } - if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { - this.folderId = this.activeFilter.selectedFolderId; - } + this.folderId = this.activeFilter.selectedFolderId; if (this.config == null) { return; @@ -990,7 +988,9 @@ export class VaultComponent this.config.initialValues = { ...this.config.initialValues, + folderId: this.folderId, organizationId: this.addOrganizationId as OrganizationId, + collectionIds: this.addCollectionIds as CollectionId[], }; } diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index ade4af928fc..eedcb4dde83 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -45,7 +45,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getByIds } from "@bitwarden/common/platform/misc"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -172,7 +172,7 @@ export class VaultV2Component cipherId: string | null = null; favorites = false; type: CipherType | null = null; - folderId: string | null = null; + folderId: string | null | undefined = null; collectionId: string | null = null; organizationId: OrganizationId | null = null; myVaultOnly = false; @@ -1016,9 +1016,7 @@ export class VaultV2Component // clear out organizationId when the user switches to a personal vault filter this.addOrganizationId = null; } - if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { - this.folderId = this.activeFilter.selectedFolderId; - } + this.folderId = this.activeFilter.selectedFolderId; if (this.config == null) { return; @@ -1027,6 +1025,8 @@ export class VaultV2Component this.config.initialValues = { ...this.config.initialValues, organizationId: this.addOrganizationId as OrganizationId, + folderId: this.folderId, + collectionIds: this.addCollectionIds as CollectionId[], }; } From 1022d21654fd54223eb460d03e2773aa14e52d8f Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:21:32 -0800 Subject: [PATCH 6/6] use custom auth wrapper for at-risk-passwords (#18055) --- apps/browser/src/popup/app-routing.module.ts | 3 ++- .../popup/guards/at-risk-passwords.guard.ts | 26 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 6838d4940ab..1f1d4d25b40 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -86,6 +86,7 @@ import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/v import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { + atRiskPasswordAuthGuard, canAccessAtRiskPasswords, hasAtRiskPasswords, } from "../vault/popup/guards/at-risk-passwords.guard"; @@ -723,7 +724,7 @@ const routes: Routes = [ { path: "at-risk-passwords", component: AtRiskPasswordsComponent, - canActivate: [authGuard, canAccessAtRiskPasswords, hasAtRiskPasswords], + canActivate: [atRiskPasswordAuthGuard, canAccessAtRiskPasswords, hasAtRiskPasswords], }, { path: AuthExtensionRoute.AccountSwitcher, diff --git a/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts b/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts index 03111859165..1b279e1078d 100644 --- a/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts +++ b/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts @@ -1,7 +1,13 @@ import { inject } from "@angular/core"; -import { CanActivateFn, Router } from "@angular/router"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from "@angular/router"; import { combineLatest, map, switchMap } from "rxjs"; +import { authGuard } from "@bitwarden/angular/auth/guards"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -9,6 +15,24 @@ import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { ToastService } from "@bitwarden/components"; +/** + * Wrapper around the main auth guard to redirect to login if not authenticated. + * This is necessary because the main auth guard returns false when not authenticated, + * which in a browser context may result in a blank extension page rather than a redirect. + */ +export const atRiskPasswordAuthGuard: CanActivateFn = async ( + route: ActivatedRouteSnapshot, + routerState: RouterStateSnapshot, +) => { + const router = inject(Router); + + const authGuardResponse = await authGuard(route, routerState); + if (authGuardResponse === true) { + return authGuardResponse; + } + return router.createUrlTree(["/login"]); +}; + export const canAccessAtRiskPasswords: CanActivateFn = () => { const accountService = inject(AccountService); const taskService = inject(TaskService);