From 11e6b434e30043f088983da18dbc8223b2c7e846 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:59:55 -0800 Subject: [PATCH 01/13] Fix bytes crate vuln RUSTSEC-2026-0007 (#18737) --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 6dab7721f6d..e5c197ef51c 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -512,9 +512,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "camino" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index f63b09de7ff..b3fac851026 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -27,7 +27,7 @@ ashpd = "=0.12.0" base64 = "=0.22.1" bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" } byteorder = "=1.5.0" -bytes = "=1.11.0" +bytes = "=1.11.1" cbc = "=0.1.2" chacha20poly1305 = "=0.10.1" core-foundation = "=0.10.1" From 2d85b62bebe213d8f168a6b097867f8056cf7877 Mon Sep 17 00:00:00 2001 From: bmbitwarden Date: Tue, 3 Feb 2026 12:18:10 -0500 Subject: [PATCH 02/13] PM-31247 interchanged error message (#18644) --- .../send-form/components/send-details/send-details.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts index 463f3195645..46eded5e86d 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts @@ -312,7 +312,7 @@ export class SendDetailsComponent implements OnInit { const emails = control.value.split(",").map((e: string) => e.trim()); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const invalidEmails = emails.filter((e: string) => e.length > 0 && !emailRegex.test(e)); - return invalidEmails.length > 0 ? { email: true } : null; + return invalidEmails.length > 0 ? { multipleEmails: true } : null; }; } From 38465c059c297f8d52dffdf73d6990cfd4b10ce6 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Tue, 3 Feb 2026 12:47:58 -0500 Subject: [PATCH 03/13] [PM-29602] Update Cart Summary for Upgrade Flow (#18605) * feat(billing): update cart-summary logic Add functionality to hide breakdown and allow translation params * tests(cart-summary): update tests and stories * feat(pricing): Add quantity support to discount labels * feat(pricing): discount quantity story * Revert "feat(pricing): discount quantity story" This reverts commit 2c00891f1fbb654954d58483d4dfdb720b5d9348. * Revert "feat(pricing): Add quantity support to discount labels" This reverts commit 8350fdd90f0de7f0d7675cd1be5a22cba34ed3fe. * fix(cart-summary): Adjust discount text styling * feat(pricing): adds support for hidden discount amounts Allows hiding the formatted amount for discounts in the cart summary. This is useful for scenarios where the discount amount is displayed elsewhere or is not relevant to the user. Updates the storybook to include a story demonstrating this feature. * feat(pricing): conditionally format currency amounts to show or hide decimals * Revert "feat(pricing): adds support for hidden discount amounts" This reverts commit 076724276c05a4463f05aa50fc119f5058dc2324. * Revert "fix(cart-summary): Adjust discount text styling" This reverts commit d02c12fc2a11b3e050bf59ba85525d8f066bd446. * Revert "discount translation" * feat(pricing): add credit type to cart summary * feat(pricing-card): Add i18n and icon component infrastructure * feat(pricing-card): Apply i18n pipe to pricing card template * refactor(pricing-card): Replace `` tags with `` in template * test(pricing-card): Update tests for i18n and icon component changes * docs(pricing-card): Enhance Storybook and documentation for new features * feat(pricing-card): Adds "per user" translation key * refactor(pricing-card): use property binding for bit-icon name * docs(pricing-card): expand price cadence options in MDX * fix(icon): update exports for icon types * feat(billing): Use strongly typed BitwardenIcon for pricing card buttons * refactor(pricing): Remove unused I18nService from PricingCardComponent * fix(pricing): Improve pricing card button icon template null-safety * fix(pricing-card): format update Clarifies the description of the `price` property within the PricingCard component documentation. No functional code changes are included. * refactor: Update discount label typography in cart summary * refactor(stories): Rename account credit translation key to premium subscription credit * feat(pricing-card): update spacing for card without button --- apps/web/src/locales/en/messages.json | 3 + .../subscription-pricing-card-details.ts | 8 +- libs/components/src/index.ts | 1 + .../cart-summary/cart-summary.component.html | 122 +++++++--- .../cart-summary/cart-summary.component.mdx | 78 ++++++- .../cart-summary.component.spec.ts | 212 ++++++++++++++++++ .../cart-summary.component.stories.ts | 91 ++++++++ .../cart-summary/cart-summary.component.ts | 23 +- .../pricing-card/pricing-card.component.html | 32 +-- .../pricing-card/pricing-card.component.mdx | 54 ++++- .../pricing-card.component.spec.ts | 26 ++- .../pricing-card.component.stories.ts | 43 +++- .../pricing-card/pricing-card.component.ts | 18 +- libs/pricing/src/types/cart.ts | 5 + libs/pricing/src/types/credit.ts | 5 + 15 files changed, 662 insertions(+), 59 deletions(-) create mode 100644 libs/pricing/src/types/credit.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a894b328d56..04566a666d4 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12782,5 +12782,8 @@ }, "invalidSendPassword": { "message": "Invalid Send password" + }, + "perUser": { + "message": "per user" } } diff --git a/libs/angular/src/billing/types/subscription-pricing-card-details.ts b/libs/angular/src/billing/types/subscription-pricing-card-details.ts index 9000b10a729..5f37f91c4f0 100644 --- a/libs/angular/src/billing/types/subscription-pricing-card-details.ts +++ b/libs/angular/src/billing/types/subscription-pricing-card-details.ts @@ -1,10 +1,14 @@ import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier"; -import { ButtonType } from "@bitwarden/components"; +import { BitwardenIcon, ButtonType } from "@bitwarden/components"; export type SubscriptionPricingCardDetails = { title: string; tagline: string; price?: { amount: number; cadence: SubscriptionCadence }; - button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } }; + button: { + text: string; + type: ButtonType; + icon?: { type: BitwardenIcon; position: "before" | "after" }; + }; features: string[]; }; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 7395b87b2ab..d92e0770e49 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,4 +1,5 @@ export { ButtonType, ButtonLikeAbstraction } from "./shared/button-like.abstraction"; +export { BitwardenIcon } from "./shared/icon"; export * from "./a11y"; export * from "./anon-layout"; export * from "./async-actions"; diff --git a/libs/pricing/src/components/cart-summary/cart-summary.component.html b/libs/pricing/src/components/cart-summary/cart-summary.component.html index e916de3995d..d3a0ad25e6c 100644 --- a/libs/pricing/src/components/cart-summary/cart-summary.component.html +++ b/libs/pricing/src/components/cart-summary/cart-summary.component.html @@ -16,7 +16,7 @@ {{ "total" | i18n }}: {{ total() | currency: "USD" : "symbol" }} USD   - / {{ term }} + / {{ term }} } - } - + + }
@@ -67,10 +69,12 @@
    @for (feature of featureList; track feature) {
  • - + > + {{ feature }} diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.mdx b/libs/pricing/src/components/pricing-card/pricing-card.component.mdx index 905b8e6981f..1cbac94d8ee 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.mdx +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.mdx @@ -39,7 +39,7 @@ import { PricingCardComponent } from "@bitwarden/pricing"; | Input | Type | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tagline` | `string` | **Required.** Descriptive text below title (max 2 lines) | -| `price` | `{ amount: number; cadence: "monthly" \| "annually"; showPerUser?: boolean }` | **Optional.** Price information. If omitted, no price is shown | +| `price` | `{ amount: number; cadence: "month" \| "monthly" \| "year" \| "annually"; showPerUser?: boolean }` | **Optional.** Price information. If omitted, no price is shown | | `button` | `{ type: ButtonType; text: string; disabled?: boolean; icon?: { type: string; position: "before" \| "after" } }` | **Optional.** Button configuration with optional icon. If omitted, no button is shown. Icon uses `bwi-*` classes, position defaults to "after" | | `features` | `string[]` | **Optional.** List of features with checkmarks | | `activeBadge` | `{ text: string; variant?: BadgeVariant }` | **Optional.** Active plan badge using proper Badge component, positioned on the same line as title, aligned to the right. If omitted, no badge is shown | @@ -182,6 +182,58 @@ For coming soon or unavailable plans: ``` +### With Button Icons + +Add icons to buttons for enhanced visual communication: + + + +```html + + + + + + + +``` + +### Active Plan Badge + +Show which plan is currently active: + + + +```html + + +``` + ### Pricing Grid Layout Multiple cards displayed together: diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts index 669b54c5b57..fc8a9541952 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { BadgeVariant, ButtonType, SvgModule, TypographyModule } from "@bitwarden/components"; import { PricingCardComponent } from "@bitwarden/pricing"; @@ -69,6 +70,29 @@ describe("PricingCardComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PricingCardComponent, TestHostComponent, SvgModule, TypographyModule, CommonModule], + providers: [ + { + provide: I18nService, + useValue: { + t: (key: string) => { + switch (key) { + case "month": + return "month"; + case "monthly": + return "monthly"; + case "year": + return "year"; + case "annually": + return "annually"; + case "perUser": + return "per user"; + default: + return key; + } + }, + }, + }, + ], }).compileComponents(); // For signal inputs, we need to set required inputs through the host component @@ -151,7 +175,7 @@ describe("PricingCardComponent", () => { it("should display bwi-check icons for features", () => { hostFixture.detectChanges(); const compiled = hostFixture.nativeElement; - const icons = compiled.querySelectorAll("i.bwi-check"); + const icons = compiled.querySelectorAll("bit-icon[name='bwi-check']"); expect(icons.length).toBe(3); // One for each feature }); diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts index 832345de357..63946cbf19a 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts @@ -1,15 +1,42 @@ -import { Meta, StoryObj } from "@storybook/angular"; +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { TypographyModule } from "@bitwarden/components"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SvgModule, TypographyModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PricingCardComponent } from "./pricing-card.component"; export default { title: "Billing/Pricing Card", component: PricingCardComponent, - moduleMetadata: { - imports: [TypographyModule], - }, + decorators: [ + moduleMetadata({ + imports: [PricingCardComponent, SvgModule, TypographyModule, I18nPipe], + providers: [ + { + provide: I18nService, + useValue: { + t: (key: string) => { + switch (key) { + case "month": + return "month"; + case "monthly": + return "monthly"; + case "year": + return "year"; + case "annually": + return "annually"; + case "perUser": + return "per user"; + default: + return key; + } + }, + }, + }, + ], + }), + ], args: { tagline: "Everything you need for secure password management across all your devices", }, @@ -83,7 +110,7 @@ export const WithoutFeatures: Story = { }), args: { tagline: "Advanced security and management for your organization", - price: { amount: 3, cadence: "monthly" }, + price: { amount: 3, cadence: "month" }, button: { text: "Contact Sales", type: "primary" }, }, }; @@ -150,7 +177,7 @@ export const LongTagline: Story = { args: { tagline: "Comprehensive password management solution for teams and organizations that need advanced security features, detailed reporting, and enterprise-grade administration tools that scale with your business", - price: { amount: 5, cadence: "monthly", showPerUser: true }, + price: { amount: 5, cadence: "month", showPerUser: true }, button: { text: "Start Business Trial", type: "primary" }, features: [ "Everything in Premium", @@ -274,7 +301,7 @@ export const WithoutButton: Story = { }), args: { tagline: "This plan will be available soon with exciting new features", - price: { amount: 15, cadence: "monthly" }, + price: { amount: 15, cadence: "month" }, features: ["Advanced security features", "Enhanced collaboration tools", "Premium support"], }, }; diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.ts index 4b9241fc9dd..23eda0fa99b 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.ts @@ -4,12 +4,15 @@ import { ChangeDetectionStrategy, Component, input, output } from "@angular/core import { BadgeModule, BadgeVariant, + BitwardenIcon, ButtonModule, ButtonType, CardComponent, + IconModule, SvgModule, TypographyModule, } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; /** * A reusable UI-only component that displays pricing information in a card format. @@ -20,20 +23,29 @@ import { selector: "billing-pricing-card", templateUrl: "./pricing-card.component.html", changeDetection: ChangeDetectionStrategy.OnPush, - imports: [BadgeModule, ButtonModule, SvgModule, TypographyModule, CurrencyPipe, CardComponent], + imports: [ + BadgeModule, + ButtonModule, + SvgModule, + IconModule, + TypographyModule, + CurrencyPipe, + CardComponent, + I18nPipe, + ], }) export class PricingCardComponent { readonly tagline = input.required(); readonly price = input<{ amount: number; - cadence: "monthly" | "annually"; + cadence: "month" | "monthly" | "year" | "annually"; showPerUser?: boolean; }>(); readonly button = input<{ type: ButtonType; text: string; disabled?: boolean; - icon?: { type: string; position: "before" | "after" }; + icon?: { type: BitwardenIcon; position: "before" | "after" }; }>(); readonly features = input(); readonly activeBadge = input<{ text: string; variant?: BadgeVariant }>(); diff --git a/libs/pricing/src/types/cart.ts b/libs/pricing/src/types/cart.ts index ed5108edee8..aeec6b269af 100644 --- a/libs/pricing/src/types/cart.ts +++ b/libs/pricing/src/types/cart.ts @@ -1,10 +1,14 @@ import { Discount } from "@bitwarden/pricing"; +import { Credit } from "./credit"; + export type CartItem = { translationKey: string; + translationParams?: Array; quantity: number; cost: number; discount?: Discount; + hideBreakdown?: boolean; }; export type Cart = { @@ -18,5 +22,6 @@ export type Cart = { }; cadence: "annually" | "monthly"; discount?: Discount; + credit?: Credit; estimatedTax: number; }; diff --git a/libs/pricing/src/types/credit.ts b/libs/pricing/src/types/credit.ts new file mode 100644 index 00000000000..bb7e42bcb62 --- /dev/null +++ b/libs/pricing/src/types/credit.ts @@ -0,0 +1,5 @@ +export type Credit = { + translationKey: string; + translationParams?: Array; + value: number; +}; From 489308fd75e719616349f973fab26250e0b709d3 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:41:25 -0800 Subject: [PATCH 04/13] refactor(input-password-flows): [Auth/PM-27086] Use new KM Data Types in InputPasswordComponent flows - Emergency Access (#18425) Update the Emergency Access Takeover flow to use new KM data types from `master-password.types.ts` / `MasterPasswordService`: - `MasterPasswordAuthenticationData` - `MasterPasswordUnlockData` This allows us to move away from the deprecated `makeMasterKey()` method (which takes email as salt) as we seek to eventually separate the email from the salt. Changes are behind feature flag: `pm-27086-update-authentication-apis-for-input-password` --- .../emergency-access-password.request.ts | 19 ++ .../services/emergency-access.service.spec.ts | 209 +++++++++++++++++- .../services/emergency-access.service.ts | 42 +++- 3 files changed, 267 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/auth/emergency-access/request/emergency-access-password.request.ts b/apps/web/src/app/auth/emergency-access/request/emergency-access-password.request.ts index ba9f1d1bc5a..68b6f4146d8 100644 --- a/apps/web/src/app/auth/emergency-access/request/emergency-access-password.request.ts +++ b/apps/web/src/app/auth/emergency-access/request/emergency-access-password.request.ts @@ -1,6 +1,25 @@ // FIXME: Update this file to be type safe and remove this and next line + +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; + // @ts-strict-ignore export class EmergencyAccessPasswordRequest { newMasterPasswordHash: string; key: string; + + // This will eventually be changed to be an actual constructor, once all callers are updated. + // The body of this request will be changed to carry the authentication data and unlock data. + // https://bitwarden.atlassian.net/browse/PM-23234 + static newConstructor( + authenticationData: MasterPasswordAuthenticationData, + unlockData: MasterPasswordUnlockData, + ): EmergencyAccessPasswordRequest { + const request = new EmergencyAccessPasswordRequest(); + request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash; + request.key = unlockData.masterKeyWrappedUserKey; + return request; + } } diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 05d6094745c..717e21e246c 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -7,8 +7,17 @@ import { of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { + MasterKeyWrappedUserKey, + MasterPasswordAuthenticationData, + MasterPasswordAuthenticationHash, + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -18,7 +27,13 @@ import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, UserPrivateKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { newGuid } from "@bitwarden/guid"; -import { Argon2KdfConfig, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; +import { + Argon2KdfConfig, + DEFAULT_KDF_CONFIG, + KdfType, + KeyService, + PBKDF2KdfConfig, +} from "@bitwarden/key-management"; import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; import { EmergencyAccessType } from "../enums/emergency-access-type"; @@ -42,6 +57,8 @@ describe("EmergencyAccessService", () => { let cipherService: MockProxy; let logService: MockProxy; let emergencyAccessService: EmergencyAccessService; + let masterPasswordService: MockProxy; + let configService: MockProxy; const mockNewUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("trustedPublicKey")]; @@ -54,6 +71,8 @@ describe("EmergencyAccessService", () => { encryptService = mock(); cipherService = mock(); logService = mock(); + masterPasswordService = mock(); + configService = mock(); emergencyAccessService = new EmergencyAccessService( emergencyAccessApiService, @@ -62,6 +81,8 @@ describe("EmergencyAccessService", () => { encryptService, cipherService, logService, + masterPasswordService, + configService, ); }); @@ -215,7 +236,13 @@ describe("EmergencyAccessService", () => { }); }); - describe("takeover", () => { + /** + * @deprecated This 'describe' to be removed in PM-28143. When you remove this, check also if there are any imports/properties + * in the test setup above that are now un-used and can also be removed. + */ + describe("takeover [PM27086_UpdateAuthenticationApisForInputPassword flag DISABLED]", () => { + const PM27086_UpdateAuthenticationApisForInputPasswordEnabled = false; + const params = { id: "emergencyAccessId", masterPassword: "mockPassword", @@ -242,6 +269,10 @@ describe("EmergencyAccessService", () => { ); beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue( + PM27086_UpdateAuthenticationApisForInputPasswordEnabled, + ); + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce(takeoverResponse); keyService.userPrivateKey$.mockReturnValue(of(userPrivateKey)); @@ -450,6 +481,180 @@ describe("EmergencyAccessService", () => { }); }); + describe("takeover [PM27086_UpdateAuthenticationApisForInputPassword flag ENABLED]", () => { + // Mock feature flag value + const PM27086_UpdateAuthenticationApisForInputPasswordEnabled = true; + + // Mock sut method params + const id = "emergency-access-id"; + const masterPassword = "mockPassword"; + const email = "user@example.com"; + const activeUserId = newGuid() as UserId; + + // Mock method data + const kdfConfig = DEFAULT_KDF_CONFIG; + + const takeoverResponse = { + keyEncrypted: "EncryptedKey", + kdf: kdfConfig.kdfType, + kdfIterations: kdfConfig.iterations, + } as EmergencyAccessTakeoverResponse; + + const activeUserPrivateKey = new Uint8Array(64) as UserPrivateKey; + let mockGrantorUserKey: UserKey; + let salt: MasterPasswordSalt; + let authenticationData: MasterPasswordAuthenticationData; + let unlockData: MasterPasswordUnlockData; + + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue( + PM27086_UpdateAuthenticationApisForInputPasswordEnabled, + ); + + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValue(takeoverResponse); + keyService.userPrivateKey$.mockReturnValue(of(activeUserPrivateKey)); + + const mockDecryptedGrantorUserKey = new SymmetricCryptoKey(new Uint8Array(64)); + encryptService.decapsulateKeyUnsigned.mockResolvedValue(mockDecryptedGrantorUserKey); + mockGrantorUserKey = mockDecryptedGrantorUserKey as UserKey; + + salt = email as MasterPasswordSalt; + masterPasswordService.emailToSalt.mockReturnValue(salt); + + authenticationData = { + salt, + kdf: kdfConfig, + masterPasswordAuthenticationHash: + "masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, + }; + + unlockData = { + salt, + kdf: kdfConfig, + masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, + } as MasterPasswordUnlockData; + + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + authenticationData, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(unlockData); + }); + + it("should throw if active user private key is not found", async () => { + // Arrange + keyService.userPrivateKey$.mockReturnValue(of(null)); + + // Act + const promise = emergencyAccessService.takeover(id, masterPassword, email, activeUserId); + + // Assert + await expect(promise).rejects.toThrow( + "Active user does not have a private key, cannot complete a takeover.", + ); + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); + + it("should throw if the grantor user key cannot be decrypted via the active user private key", async () => { + // Arrange + encryptService.decapsulateKeyUnsigned.mockResolvedValue(null); + + // Act + const promise = emergencyAccessService.takeover(id, masterPassword, email, activeUserId); + + // Assert + await expect(promise).rejects.toThrow("Failed to decrypt grantor key"); + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); + + it("should use PBKDF2 if takeover response contains KdfType.PBKDF2_SHA256", async () => { + // Act + await emergencyAccessService.takeover(id, masterPassword, email, activeUserId); + + // Assert + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + masterPassword, + kdfConfig, // default config (PBKDF2) + salt, + ); + }); + + it("should use Argon2 if takeover response contains KdfType.Argon2id", async () => { + // Arrange + const argon2TakeoverResponse = { + keyEncrypted: "EncryptedKey", + kdf: KdfType.Argon2id, + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 4, + } as EmergencyAccessTakeoverResponse; + + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValue( + argon2TakeoverResponse, + ); + + const expectedKdfConfig = new Argon2KdfConfig( + argon2TakeoverResponse.kdfIterations, + argon2TakeoverResponse.kdfMemory, + argon2TakeoverResponse.kdfParallelism, + ); + + // Act + await emergencyAccessService.takeover(id, masterPassword, email, activeUserId); + + // Assert + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + masterPassword, + expectedKdfConfig, + salt, + ); + expect(masterPasswordService.makeMasterPasswordAuthenticationData).not.toHaveBeenCalledWith( + masterPassword, + kdfConfig, // default config (PBKDF2) + salt, + ); + }); + + it("should call makeMasterPasswordAuthenticationData and makeMasterPasswordUnlockData with the correct parameters", async () => { + // Act + await emergencyAccessService.takeover(id, masterPassword, email, activeUserId); + + // Assert + const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData); + + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + masterPassword, + kdfConfig, + salt, + ); + + expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith( + masterPassword, + kdfConfig, + salt, + mockGrantorUserKey, + ); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith( + id, + request, + ); + }); + + it("should call the API method to change the grantor's master password", async () => { + // Act + await emergencyAccessService.takeover(id, masterPassword, email, activeUserId); + + // Assert + const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledTimes(1); + expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith( + id, + request, + ); + }); + }); + describe("getRotatedData", () => { const allowedStatuses = [ EmergencyAccessStatusType.Confirmed, diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 80b1b27116b..81e7275af23 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -4,11 +4,19 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { + MasterPasswordAuthenticationData, + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; @@ -56,6 +64,8 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide private encryptService: EncryptService, private cipherService: CipherService, private logService: LogService, + private masterPasswordService: MasterPasswordServiceAbstraction, + private configService: ConfigService, ) {} /** @@ -270,7 +280,7 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide * Intended for grantee. * @param id emergency access id * @param masterPassword new master password - * @param email email address of grantee (must be consistent or login will fail) + * @param email email address of grantor (must be consistent or login will fail) * @param activeUserId the user id of the active user */ async takeover(id: string, masterPassword: string, email: string, activeUserId: UserId) { @@ -309,6 +319,36 @@ export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvide break; } + // When you unwind the flag in PM-28143, also remove the ConfigService if it is un-used. + const newApisWithInputPasswordFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword, + ); + + if (newApisWithInputPasswordFlagEnabled) { + const salt: MasterPasswordSalt = this.masterPasswordService.emailToSalt(email); + + const authenticationData: MasterPasswordAuthenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + masterPassword, + config, + salt, + ); + + const unlockData: MasterPasswordUnlockData = + await this.masterPasswordService.makeMasterPasswordUnlockData( + masterPassword, + config, + salt, + grantorUserKey, + ); + + const request = EmergencyAccessPasswordRequest.newConstructor(authenticationData, unlockData); + + await this.emergencyAccessApiService.postEmergencyAccessPassword(id, request); + + return; // EARLY RETURN for flagged logic + } + const masterKey = await this.keyService.makeMasterKey(masterPassword, email, config); const masterKeyHash = await this.keyService.hashMasterKey(masterPassword, masterKey); From 50063c7f71124283027823044d365626bd4b956b Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 3 Feb 2026 14:21:40 -0500 Subject: [PATCH 05/13] [PM-31477] Align Desktop V3 with Archive Premium Banner (#18696) * adding showPremiumCallout to vault-v3 for non premium banner --- .../src/vault/app/vault-v3/vault.component.html | 1 + .../desktop/src/vault/app/vault-v3/vault.component.ts | 11 ++++++++++- .../src/vault/app/vault/vault-items-v2.component.ts | 4 +--- .../src/vault/app/vault/vault-v2.component.html | 1 - 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.html b/apps/desktop/src/vault/app/vault-v3/vault.component.html index a9a25f57994..d81df3eba74 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.html @@ -6,6 +6,7 @@ (onCipherClicked)="viewCipher($event)" (onCipherRightClicked)="viewCipherMenu($event)" (onAddCipher)="addCipher($event)" + [showPremiumCallout]="showPremiumCallout$ | async" >
    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 e3b4493ec7d..1f9138426ce 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts @@ -154,7 +154,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { type: CipherType | null = null; folderId: string | null | undefined = null; collectionId: string | null = null; - organizationId: string | null = null; + organizationId: OrganizationId | null = null; myVaultOnly = false; addType: CipherType | undefined = undefined; addOrganizationId: string | null = null; @@ -168,6 +168,15 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { cipher: CipherView | null = new CipherView(); collections: CollectionView[] | null = null; config: CipherFormConfig | null = null; + private userId$ = this.accountService.activeAccount$.pipe(getUserId); + showPremiumCallout$: Observable = this.userId$.pipe( + switchMap((userId) => + combineLatest([ + this.routedVaultFilterBridgeService.activeFilter$, + this.cipherArchiveService.showSubscriptionEndedMessaging$(userId), + ]).pipe(map(([activeFilter, showMessaging]) => activeFilter.isArchived && showMessaging)), + ), + ); /** Tracks the disabled status of the edit cipher form */ protected formDisabled: boolean = false; diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts index 1ec0bb0b22e..a6582f6de58 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -9,7 +9,6 @@ import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angul import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; -import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; @@ -32,7 +31,6 @@ import { SearchBarService } from "../../../app/layout/search/search-bar.service" }) export class VaultItemsV2Component extends BaseVaultItemsComponent { readonly showPremiumCallout = input(false); - readonly organizationId = input(undefined); protected CipherViewLikeUtils = CipherViewLikeUtils; @@ -55,7 +53,7 @@ export class VaultItemsV2Component extends BaseVaultIt } async navigateToGetPremium() { - await this.premiumUpgradePromptService.promptForPremium(this.organizationId()); + await this.premiumUpgradePromptService.promptForPremium(); } trackByFn(index: number, c: C): string { diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html index 61b7c0ee355..129db673b39 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -7,7 +7,6 @@ (onCipherRightClicked)="viewCipherMenu($event)" (onAddCipher)="addCipher($event)" [showPremiumCallout]="showPremiumCallout$ | async" - [organizationId]="organizationId" > @if (!!action) { From 557d417ed14aa8d59f16149e91c0f4112bb3cbcc Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 3 Feb 2026 15:10:05 -0500 Subject: [PATCH 06/13] - Clear pending auth requests for both HTTP and HTTPS (#18661) - Add null-safe checks before returning auth credentials - Align callback typing and optional arguments --- .../background/web-request.background.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index 5c02f2df34d..5bab219d0b1 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -28,7 +26,7 @@ export default class WebRequestBackground { this.webRequest.onAuthRequired.addListener( (async ( details: chrome.webRequest.OnAuthRequiredDetails, - callback: (response: chrome.webRequest.BlockingResponse) => void, + callback: (response: chrome.webRequest.BlockingResponse | null) => void, ) => { if (!details.url || this.pendingAuthRequests.has(details.requestId)) { if (callback) { @@ -51,16 +49,16 @@ export default class WebRequestBackground { ); this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), { - urls: ["http://*/*"], + urls: ["http://*/*", "https://*/*"], }); this.webRequest.onErrorOccurred.addListener((details) => this.completeAuthRequest(details), { - urls: ["http://*/*"], + urls: ["http://*/*", "https://*/*"], }); } private async resolveAuthCredentials( domain: string, - success: (response: chrome.webRequest.BlockingResponse) => void, + success: (response: chrome.webRequest.BlockingResponse | null) => void, // eslint-disable-next-line error: Function, ) { @@ -82,7 +80,7 @@ export default class WebRequestBackground { const ciphers = await this.cipherService.getAllDecryptedForUrl( domain, activeUserId, - null, + undefined, UriMatchStrategy.Host, ); if (ciphers == null || ciphers.length !== 1) { @@ -90,10 +88,17 @@ export default class WebRequestBackground { return; } + const username = ciphers[0].login?.username; + const password = ciphers[0].login?.password; + if (username == null || password == null) { + error(); + return; + } + success({ authCredentials: { - username: ciphers[0].login.username, - password: ciphers[0].login.password, + username, + password, }, }); } catch { From 51a99fecd832607d5cfd13f831d5f0efc7184a08 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Tue, 3 Feb 2026 15:18:23 -0500 Subject: [PATCH 07/13] [PM-31429] Add missing helper text for password protected Sends, remove unused one (#18694) * [PM-31429] Add missing helper text for password protected Sends, remove unused one * Put one UI change behind feature flag, add back required translations * Reorder translation * Add spaces * Come full circle, remove last couple of committed changes --- apps/browser/src/_locales/en/messages.json | 8 ++++---- apps/desktop/src/locales/en/messages.json | 8 ++++---- apps/web/src/locales/en/messages.json | 8 ++++---- .../components/send-details/send-details.component.html | 4 +++- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4c36a852f6a..9f15bfd840f 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3035,10 +3035,6 @@ "custom": { "message": "Custom" }, - "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6144,5 +6140,9 @@ }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "sendPasswordHelperText": { + "message": "Individuals will need to enter the password to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } } \ No newline at end of file diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 0ce98b8c62b..f04ab8756d0 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -137,10 +137,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeTextToShare": { "message": "Text to share" }, @@ -4587,5 +4583,9 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "sendPasswordHelperText": { + "message": "Individuals will need to enter the password to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 04566a666d4..160ad4e867a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5645,10 +5645,6 @@ "sendTypeText": { "message": "Text" }, - "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12783,6 +12779,10 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "sendPasswordHelperText": { + "message": "Individuals will need to enter the password to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "perUser": { "message": "per user" } diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html index 581ee20caf7..dc1894b0935 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html @@ -61,6 +61,9 @@ @if (sendDetailsForm.get("authType").value === AuthType.Email) { {{ "emailVerificationDesc" | i18n }} } + @if (sendDetailsForm.get("authType").value === AuthType.Password) { + {{ "sendPasswordHelperText" | i18n }} + } @if (sendDetailsForm.get("authType").value === AuthType.Password) { @@ -108,7 +111,6 @@ > }
    - {{ "sendPasswordDescV3" | i18n }} } From eaa7e5ab2a9e34e68b381bec0d59fbd1c498e669 Mon Sep 17 00:00:00 2001 From: Sola Date: Wed, 4 Feb 2026 04:18:34 +0800 Subject: [PATCH 08/13] [PM-30894] Support importing SSH keys from 1pux (#18391) * Support importing SSH keys from 1pux Co-authored-by: Bernd Schoolmann Co-authored-by: Daniel James Smith * Propagate SSH key import error --------- Co-authored-by: Bernd Schoolmann Co-authored-by: Daniel James Smith --- .../onepassword-1pux-importer.spec.ts | 35 ++++++++ .../onepassword/onepassword-1pux-importer.ts | 19 +++++ .../types/onepassword-1pux-importer-types.ts | 15 ++++ .../spec-data/onepassword-1pux/ssh-key.ts | 83 +++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 libs/importer/src/importers/spec-data/onepassword-1pux/ssh-key.ts diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts index 4ec20ba2a87..8dbcf29fd2f 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts @@ -2,6 +2,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import * as sdkInternal from "@bitwarden/sdk-internal"; import { APICredentialsData } from "../spec-data/onepassword-1pux/api-credentials"; import { BankAccountData } from "../spec-data/onepassword-1pux/bank-account"; @@ -25,11 +26,14 @@ import { SanitizedExport } from "../spec-data/onepassword-1pux/sanitized-export" import { SecureNoteData } from "../spec-data/onepassword-1pux/secure-note"; import { ServerData } from "../spec-data/onepassword-1pux/server"; import { SoftwareLicenseData } from "../spec-data/onepassword-1pux/software-license"; +import { SSH_KeyData } from "../spec-data/onepassword-1pux/ssh-key"; import { SSNData } from "../spec-data/onepassword-1pux/ssn"; import { WirelessRouterData } from "../spec-data/onepassword-1pux/wireless-router"; import { OnePassword1PuxImporter } from "./onepassword-1pux-importer"; +jest.mock("@bitwarden/sdk-internal"); + function validateCustomField(fields: FieldView[], fieldName: string, expectedValue: any) { expect(fields).toBeDefined(); const customField = fields.find((f) => f.name === fieldName); @@ -669,6 +673,37 @@ describe("1Password 1Pux Importer", () => { validateCustomField(cipher.fields, "medication notes", "multiple times a day"); }); + it("should parse category 114 - SSH Key", async () => { + // Mock the SDK import_ssh_key function to return converted OpenSSH format + const mockConvertedKey = { + privateKey: + "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACCWsp3FFVVCMGZ23hscRkDPfGzKZ8z1V/ZB9nzbdDFRswAAAJh8F3bYfBd2\n2AAAAAtzc2gtZWQyNTUxOQAAACCWsp3FFVVCMGZ23hscRkDPfGzKZ8z1V/ZB9nzbdDFRsw\nAAAEA59QYE22f+VFHhiyH1Vfqiwz7xLEt1zCuk8M8Ng5LpKpayncUVVUKwZ3beGxxGQM98\nbMpnzPVX9kH2fNt0MVGzAAAAE3Rlc3RAZXhhbXBsZS5jb20BAgMEBQ==\n-----END OPENSSH PRIVATE KEY-----\n", + publicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJayncUVVUKwZ3beGxxGQM98bMpnzPVX9kH2fNt0MVGz", + fingerprint: "SHA256:/9qSxXuic8kaVBhwv3c8PuetiEpaOgIp7xHNCbcSuN8", + } as sdkInternal.SshKeyView; + + jest.spyOn(sdkInternal, "import_ssh_key").mockReturnValue(mockConvertedKey); + + const importer = new OnePassword1PuxImporter(); + const jsonString = JSON.stringify(SSH_KeyData); + const result = await importer.parse(jsonString); + expect(result != null).toBe(true); + const cipher = result.ciphers.shift(); + expect(cipher.type).toEqual(CipherType.SshKey); + expect(cipher.name).toEqual("Some SSH Key"); + expect(cipher.notes).toEqual("SSH Key Note"); + + // Verify that import_ssh_key was called with the PKCS#8 key from 1Password + expect(sdkInternal.import_ssh_key).toHaveBeenCalledWith( + "-----BEGIN PRIVATE KEY-----\nMFECAQEwBQYDK2VwBCIEIDn1BgTbZ/5UUeGLIfVV+qLBOvEsS3XMK6Twzw2Dkukq\ngSEAlrKdxRVVQrBndt4bHEZAz3xsymfM9Vf2QfZ823QxUbM=\n-----END PRIVATE KEY-----\n", + ); + + // Verify the key was converted to OpenSSH format + expect(cipher.sshKey.privateKey).toEqual(mockConvertedKey.privateKey); + expect(cipher.sshKey.publicKey).toEqual(mockConvertedKey.publicKey); + expect(cipher.sshKey.keyFingerprint).toEqual(mockConvertedKey.fingerprint); + }); + it("should create folders", async () => { const importer = new OnePassword1PuxImporter(); const result = await importer.parse(SanitizedExportJson); diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts index 4571a6957c4..48de18bc54b 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts @@ -8,6 +8,8 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view" import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; +import { import_ssh_key } from "@bitwarden/sdk-internal"; import { ImportResult } from "../../models/import-result"; import { BaseImporter } from "../base-importer"; @@ -80,6 +82,10 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer { cipher.type = CipherType.Identity; cipher.identity = new IdentityView(); break; + case Category.SSH_Key: + cipher.type = CipherType.SshKey; + cipher.sshKey = new SshKeyView(); + break; default: break; } @@ -316,6 +322,19 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer { default: break; } + } else if (cipher.type === CipherType.SshKey) { + if (valueKey === "sshKey") { + // Use sshKey.metadata.privateKey instead of the sshKey.privateKey field. + // The sshKey.privateKey field doesn't have a consistent format for every item. + const { privateKey } = field.value.sshKey.metadata; + // Convert SSH key from PKCS#8 (1Password format) to OpenSSH format using SDK + // Note: 1Password does not store password-protected SSH keys, so no password handling needed for now + const parsedKey = import_ssh_key(privateKey); + cipher.sshKey.privateKey = parsedKey.privateKey; + cipher.sshKey.publicKey = parsedKey.publicKey; + cipher.sshKey.keyFingerprint = parsedKey.fingerprint; + return; + } } if (valueKey === "email") { diff --git a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts index 43f3bc4f7d6..a24c6489c24 100644 --- a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts +++ b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts @@ -49,6 +49,7 @@ export const Category = Object.freeze({ EmailAccount: "111", API_Credential: "112", MedicalRecord: "113", + SSH_Key: "114", } as const); /** @@ -133,6 +134,7 @@ export interface Value { creditCardType?: string | null; creditCardNumber?: string | null; reference?: string | null; + sshKey?: SSHKey | null; } export interface Email { @@ -147,6 +149,19 @@ export interface Address { zip: string; state: string; } + +export interface SSHKey { + privateKey: string; + metadata: SSHKeyMetadata; +} + +export interface SSHKeyMetadata { + privateKey: string; + publicKey: string; + fingerprint: string; + keyType: string; +} + export interface InputTraits { keyboard: string; correction: string; diff --git a/libs/importer/src/importers/spec-data/onepassword-1pux/ssh-key.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/ssh-key.ts new file mode 100644 index 00000000000..3e9cde46271 --- /dev/null +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/ssh-key.ts @@ -0,0 +1,83 @@ +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; + +export const SSH_KeyData: ExportData = { + accounts: [ + { + attrs: { + accountName: "1Password Customer", + name: "1Password Customer", + avatar: "", + email: "username123123123@gmail.com", + uuid: "TRIZ3XV4JJFRXJ3BARILLTUA6E", + domain: "https://my.1password.com/", + }, + vaults: [ + { + attrs: { + uuid: "pqcgbqjxr4tng2hsqt5ffrgwju", + desc: "Just test entries", + avatar: "ke7i5rxnjrh3tj6uesstcosspu.png", + name: "T's Test Vault", + type: "U", + }, + items: [ + { + uuid: "kf7wevmfiqmbgyao42plvgrasy", + favIndex: 0, + createdAt: 1724868152, + updatedAt: 1724868152, + state: "active", + categoryUuid: "114", + details: { + loginFields: [], + notesPlain: "SSH Key Note", + sections: [ + { + title: "SSH Key Section", + fields: [ + { + title: "private key", + id: "private_key", + value: { + sshKey: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMFECAQEwBQYDK2VwBCIEIDn1BgTbZ/5UUeGLIfVV+qLBOvEsS3XMK6Twzw2Dkukq\ngSEAlrKdxRVVQrBndt4bHEZAz3xsymfM9Vf2QfZ823QxUbM=\n-----END PRIVATE KEY-----\n", + metadata: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMFECAQEwBQYDK2VwBCIEIDn1BgTbZ/5UUeGLIfVV+qLBOvEsS3XMK6Twzw2Dkukq\ngSEAlrKdxRVVQrBndt4bHEZAz3xsymfM9Vf2QfZ823QxUbM=\n-----END PRIVATE KEY-----\n", + publicKey: + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJayncUVVUKwZ3beGxxGQM98bMpnzPVX9kH2fNt0MVGz", + fingerprint: "SHA256:/9qSxXuic8kaVBhwv3c8PuetiEpaOgIp7xHNCbcSuN8", + keyType: "ed25519", + }, + }, + }, + guarded: true, + multiline: false, + dontGenerate: false, + inputTraits: { + keyboard: "default", + correction: "default", + capitalization: "default", + }, + }, + ], + hideAddAnotherField: true, + }, + ], + passwordHistory: [], + }, + overview: { + subtitle: "SHA256:/9qSxXuic8kaVBhwv3c8PuetiEpaOgIp7xHNCbcSuN8", + icons: null, + title: "Some SSH Key", + url: "", + watchtowerExclusions: null, + }, + }, + ], + }, + ], + }, + ], +}; From 9e61f1b16d7a5c3a142cd0e2588a8c98c3d3983d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:23:04 -0500 Subject: [PATCH 09/13] [deps] Autofill: Update prettier to v3.8.1 (#18710) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index da9b3e7dcbe..55873bdb40c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -160,7 +160,7 @@ "path-browserify": "1.0.1", "postcss": "8.5.6", "postcss-loader": "8.2.0", - "prettier": "3.7.3", + "prettier": "3.8.1", "prettier-plugin-tailwindcss": "0.7.1", "process": "0.11.10", "remark-gfm": "4.0.1", @@ -36683,9 +36683,9 @@ } }, "node_modules/prettier": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz", - "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 20ca9b20f8e..1a72c49d263 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "path-browserify": "1.0.1", "postcss": "8.5.6", "postcss-loader": "8.2.0", - "prettier": "3.7.3", + "prettier": "3.8.1", "prettier-plugin-tailwindcss": "0.7.1", "process": "0.11.10", "remark-gfm": "4.0.1", From f3686c657bc4f344320bd2cef02a6c9ec37a3193 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 3 Feb 2026 15:29:11 -0500 Subject: [PATCH 10/13] [PM-31476] Desktop Archive Empty State Vault-V3 (#18695) * add empty state for archive desktop --- .../vault/app/vault-v3/vault.component.html | 117 ++++++++++-------- .../src/vault/app/vault-v3/vault.component.ts | 11 +- 2 files changed, 72 insertions(+), 56 deletions(-) diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.html b/apps/desktop/src/vault/app/vault-v3/vault.component.html index d81df3eba74..42151500964 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.html @@ -9,63 +9,76 @@ [showPremiumCallout]="showPremiumCallout$ | async" > -
    - -
    -
    -
    - - - - - - - + + + + + } +
    -
-