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 1/8] 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 2/8] [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 3/8] - 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 4/8] [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 5/8] [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 6/8] [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 7/8] [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" > -
- -
-
-
- - - - - - - + + + + + } +
- -