From 6ba3e294eca1bf9f39d1d02d954066964e5c71cf Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:01:49 +0100 Subject: [PATCH] [PM-27233] Support v2 encryption for JIT Password signups (#18222) * Support v2 encryption for JIT Password signups * TDE set master password split * update sdk-internal dependency * moved encryption v2 to InitializeJitPasswordUserService * remove account cryptographic state legacy states from #18164 * legacy state comments * sdk update * unit test coverage * consolidate do SetInitialPasswordService * replace legacy master key with setLegacyMasterKeyFromUnlockData * typo * web and desktop overrides with unit tests * early return * compact validation * simplify super prototype --- .../src/app/services/services.module.ts | 2 + ...sktop-set-initial-password.service.spec.ts | 43 ++- .../desktop-set-initial-password.service.ts | 13 + .../web-set-initial-password.service.spec.ts | 40 ++- .../web-set-initial-password.service.ts | 15 + apps/web/src/app/core/core.module.ts | 24 +- ...initial-password.service.implementation.ts | 215 +++++++++++--- ...fault-set-initial-password.service.spec.ts | 273 +++++++++++++++++- .../set-initial-password.component.ts | 113 ++++++-- ...et-initial-password.service.abstraction.ts | 30 +- .../src/services/jslib-services.module.ts | 13 +- libs/common/src/enums/feature-flag.enum.ts | 2 + package-lock.json | 16 +- package.json | 4 +- 14 files changed, 693 insertions(+), 110 deletions(-) diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index a5a91c52e7e..66613efd115 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -89,6 +89,7 @@ import { PlatformUtilsService, PlatformUtilsService as PlatformUtilsServiceAbstraction, } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -432,6 +433,7 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, MessagingServiceAbstraction, AccountCryptographicStateService, + RegisterSdkService, ], }), safeProvider({ diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts index 6b29a464e2c..9bb7d5077cf 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts @@ -1,8 +1,10 @@ -import { MockProxy, mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation"; import { + InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordUserType, @@ -19,12 +21,14 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme 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 { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; @@ -45,6 +49,7 @@ describe("DesktopSetInitialPasswordService", () => { let userDecryptionOptionsService: MockProxy; let messagingService: MockProxy; let accountCryptographicStateService: MockProxy; + let registerSdkService: MockProxy; beforeEach(() => { apiService = mock(); @@ -59,6 +64,7 @@ describe("DesktopSetInitialPasswordService", () => { userDecryptionOptionsService = mock(); messagingService = mock(); accountCryptographicStateService = mock(); + registerSdkService = mock(); sut = new DesktopSetInitialPasswordService( apiService, @@ -73,6 +79,7 @@ describe("DesktopSetInitialPasswordService", () => { userDecryptionOptionsService, messagingService, accountCryptographicStateService, + registerSdkService, ); }); @@ -179,4 +186,36 @@ describe("DesktopSetInitialPasswordService", () => { }); }); }); + + describe("initializePasswordJitPasswordUserV2Encryption(...)", () => { + it("should send a 'redrawMenu' message", async () => { + // Arrange + const credentials: InitializeJitPasswordCredentials = { + newPasswordHint: "newPasswordHint", + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId" as OrganizationId, + resetPasswordAutoEnroll: false, + newPassword: "newPassword123!", + salt: "user@example.com" as MasterPasswordSalt, + }; + const userId = "userId" as UserId; + + const superSpy = jest + .spyOn( + DefaultSetInitialPasswordService.prototype, + "initializePasswordJitPasswordUserV2Encryption", + ) + .mockResolvedValue(undefined); + + // Act + await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + // Assert + expect(superSpy).toHaveBeenCalledWith(credentials, userId); + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); + + superSpy.mockRestore(); + }); + }); }); diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts index cedfa3fe589..f9fb8361056 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts @@ -1,6 +1,7 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation"; import { + InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordUserType, @@ -14,6 +15,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; import { UserId } from "@bitwarden/common/types/guid"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; @@ -34,6 +36,7 @@ export class DesktopSetInitialPasswordService protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private messagingService: MessagingService, protected accountCryptographicStateService: AccountCryptographicStateService, + protected registerSdkService: RegisterSdkService, ) { super( apiService, @@ -47,6 +50,7 @@ export class DesktopSetInitialPasswordService organizationUserApiService, userDecryptionOptionsService, accountCryptographicStateService, + registerSdkService, ); } @@ -59,4 +63,13 @@ export class DesktopSetInitialPasswordService this.messagingService.send("redrawMenu"); } + + override async initializePasswordJitPasswordUserV2Encryption( + credentials: InitializeJitPasswordCredentials, + userId: UserId, + ): Promise { + await super.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + this.messagingService.send("redrawMenu"); + } } diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts index 1bbaa0ec236..b09b5f0bc9a 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts @@ -3,6 +3,7 @@ import { BehaviorSubject, of } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { + InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordUserType, @@ -20,11 +21,13 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme 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 { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { RouterService } from "@bitwarden/web-vault/app/core"; @@ -47,6 +50,7 @@ describe("WebSetInitialPasswordService", () => { let organizationInviteService: MockProxy; let routerService: MockProxy; let accountCryptographicStateService: MockProxy; + let registerSdkService: MockProxy; beforeEach(() => { apiService = mock(); @@ -62,6 +66,7 @@ describe("WebSetInitialPasswordService", () => { organizationInviteService = mock(); routerService = mock(); accountCryptographicStateService = mock(); + registerSdkService = mock(); sut = new WebSetInitialPasswordService( apiService, @@ -77,6 +82,7 @@ describe("WebSetInitialPasswordService", () => { organizationInviteService, routerService, accountCryptographicStateService, + registerSdkService, ); }); @@ -208,4 +214,36 @@ describe("WebSetInitialPasswordService", () => { }); }); }); + + describe("initializePasswordJitPasswordUserV2Encryption(...)", () => { + it("should call routerService.getAndClearLoginRedirectUrl() and organizationInviteService.clearOrganizationInvitation()", async () => { + // Arrange + const credentials: InitializeJitPasswordCredentials = { + newPasswordHint: "newPasswordHint", + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId" as OrganizationId, + resetPasswordAutoEnroll: false, + newPassword: "newPassword123!", + salt: "user@example.com" as MasterPasswordSalt, + }; + const userId = "userId" as UserId; + + const superSpy = jest + .spyOn( + Object.getPrototypeOf(Object.getPrototypeOf(sut)), + "initializePasswordJitPasswordUserV2Encryption", + ) + .mockResolvedValue(undefined); + + // Act + await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + // Assert + expect(superSpy).toHaveBeenCalledWith(credentials, userId); + expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1); + expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1); + + superSpy.mockRestore(); + }); + }); }); diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts index 303b9148e8e..0b8dba6c40e 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts @@ -1,6 +1,7 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation"; import { + InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordUserType, @@ -14,6 +15,7 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; import { UserId } from "@bitwarden/common/types/guid"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { RouterService } from "@bitwarden/web-vault/app/core"; @@ -36,6 +38,7 @@ export class WebSetInitialPasswordService private organizationInviteService: OrganizationInviteService, private routerService: RouterService, protected accountCryptographicStateService: AccountCryptographicStateService, + protected registerSdkService: RegisterSdkService, ) { super( apiService, @@ -49,6 +52,7 @@ export class WebSetInitialPasswordService organizationUserApiService, userDecryptionOptionsService, accountCryptographicStateService, + registerSdkService, ); } @@ -83,4 +87,15 @@ export class WebSetInitialPasswordService await this.routerService.getAndClearLoginRedirectUrl(); await this.organizationInviteService.clearOrganizationInvitation(); } + + override async initializePasswordJitPasswordUserV2Encryption( + credentials: InitializeJitPasswordCredentials, + userId: UserId, + ): Promise { + await super.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + // TODO: Investigate refactoring the following logic in https://bitwarden.atlassian.net/browse/PM-22615 + await this.routerService.getAndClearLoginRedirectUrl(); + await this.organizationInviteService.clearOrganizationInvitation(); + } } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 661d14502fe..7b248eee8a3 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -6,11 +6,11 @@ import { Router } from "@angular/router"; import { CollectionAdminService, - DefaultCollectionAdminService, - OrganizationUserApiService, CollectionService, - OrganizationUserService, + DefaultCollectionAdminService, DefaultOrganizationUserService, + OrganizationUserApiService, + OrganizationUserService, } from "@bitwarden/admin-console/common"; import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth/device-management/default-device-management-component.service"; import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction"; @@ -27,17 +27,17 @@ import { OBSERVABLE_DISK_LOCAL_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, + SafeInjectionToken, SECURE_STORAGE, SYSTEM_LANGUAGE, - SafeInjectionToken, WINDOW, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { - RegistrationFinishService as RegistrationFinishServiceAbstraction, LoginComponentService, - SsoComponentService, LoginDecryptionOptionsService, + RegistrationFinishService as RegistrationFinishServiceAbstraction, + SsoComponentService, TwoFactorAuthDuoComponentService, } from "@bitwarden/auth/angular"; import { @@ -90,6 +90,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -120,9 +121,9 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { GeneratorServicesModule } from "@bitwarden/generator-components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { + BiometricsService, KdfConfigService, KeyService as KeyServiceAbstraction, - BiometricsService, } from "@bitwarden/key-management"; import { LockComponentService, @@ -135,17 +136,17 @@ import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/va import { flagEnabled } from "../../utils/flags"; import { - POLICY_EDIT_REGISTER, ossPolicyEditRegister, + POLICY_EDIT_REGISTER, } from "../admin-console/organizations/policies"; import { + LinkSsoService, WebChangePasswordService, - WebRegistrationFinishService, WebLoginComponentService, WebLoginDecryptionOptionsService, - WebTwoFactorAuthDuoComponentService, - LinkSsoService, + WebRegistrationFinishService, WebSetInitialPasswordService, + WebTwoFactorAuthDuoComponentService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; import { WebPremiumInterestStateService } from "../billing/services/premium-interest/web-premium-interest-state.service"; @@ -320,6 +321,7 @@ const safeProviders: SafeProvider[] = [ OrganizationInviteService, RouterService, AccountCryptographicStateService, + RegisterSdkService, ], }), safeProvider({ diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts index 2f5c43e2db9..3f6023c1205 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { concatMap, firstValueFrom } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -19,19 +19,32 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme 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 { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types"; +import { + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; +import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management"; +import { + fromSdkKdfConfig, + KdfConfig, + KdfConfigService, + KeyService, +} from "@bitwarden/key-management"; +import { OrganizationId as SdkOrganizationId, UserId as SdkUserId } from "@bitwarden/sdk-internal"; import { - SetInitialPasswordService, + InitializeJitPasswordCredentials, SetInitialPasswordCredentials, - SetInitialPasswordUserType, + SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, + SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; export class DefaultSetInitialPasswordService implements SetInitialPasswordService { @@ -47,6 +60,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi protected organizationUserApiService: OrganizationUserApiService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected accountCryptographicStateService: AccountCryptographicStateService, + protected registerSdkService: RegisterSdkService, ) {} async setInitialPassword( @@ -199,6 +213,126 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi } } + async setInitialPasswordTdeOffboarding( + credentials: SetInitialPasswordTdeOffboardingCredentials, + userId: UserId, + ) { + const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials; + for (const [key, value] of Object.entries(credentials)) { + if (value == null) { + throw new Error(`${key} not found. Could not set password.`); + } + } + + if (userId == null) { + throw new Error("userId not found. Could not set password."); + } + + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + if (userKey == null) { + throw new Error("userKey not found. Could not set password."); + } + + const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey( + newMasterKey, + userKey, + ); + + if (!newMasterKeyEncryptedUserKey[1].encryptedString) { + throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password."); + } + + const request = new UpdateTdeOffboardingPasswordRequest(); + request.key = newMasterKeyEncryptedUserKey[1].encryptedString; + request.newMasterPasswordHash = newServerMasterKeyHash; + request.masterPasswordHint = newPasswordHint; + + await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request); + + // Clear force set password reason to allow navigation back to vault. + await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + } + + async initializePasswordJitPasswordUserV2Encryption( + credentials: InitializeJitPasswordCredentials, + userId: UserId, + ): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + + for (const [key, value] of Object.entries(credentials)) { + if (value == null) { + throw new Error(`${key} is required.`); + } + } + + const { newPasswordHint, orgSsoIdentifier, orgId, resetPasswordAutoEnroll, newPassword, salt } = + credentials; + + const organizationKeys = await this.organizationApiService.getKeys(orgId); + if (organizationKeys == null) { + throw new Error("Organization keys response is null."); + } + + const registerResult = await firstValueFrom( + this.registerSdkService.registerClient$(userId).pipe( + concatMap(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + return await ref.value + .auth() + .registration() + .post_keys_for_jit_password_registration({ + org_id: asUuid(orgId), + org_public_key: organizationKeys.publicKey, + master_password: newPassword, + master_password_hint: newPasswordHint, + salt: salt, + organization_sso_identifier: orgSsoIdentifier, + user_id: asUuid(userId), + reset_password_enroll: resetPasswordAutoEnroll, + }); + }), + ), + ); + + if (!("V2" in registerResult.account_cryptographic_state)) { + throw new Error("Unexpected V2 account cryptographic state"); + } + + // Note: When SDK state management matures, these should be moved into post_keys_for_tde_registration + // Set account cryptography state + await this.accountCryptographicStateService.setAccountCryptographicState( + registerResult.account_cryptographic_state, + userId, + ); + + // Clear force set password reason to allow navigation back to vault. + await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + + const masterPasswordUnlockData = MasterPasswordUnlockData.fromSdk( + registerResult.master_password_unlock, + ); + await this.masterPasswordService.setMasterPasswordUnlockData(masterPasswordUnlockData, userId); + + await this.keyService.setUserKey( + SymmetricCryptoKey.fromString(registerResult.user_key) as UserKey, + userId, + ); + + await this.updateLegacyState( + newPassword, + fromSdkKdfConfig(registerResult.master_password_unlock.kdf), + new EncString(registerResult.master_password_unlock.masterKeyWrappedUserKey), + userId, + masterPasswordUnlockData, + ); + } + private async makeMasterKeyEncryptedUserKey( masterKey: MasterKey, userId: UserId, @@ -244,6 +378,37 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId); } + // Deprecated legacy support - to be removed in future + private async updateLegacyState( + newPassword: string, + kdfConfig: KdfConfig, + masterKeyWrappedUserKey: EncString, + userId: UserId, + masterPasswordUnlockData: MasterPasswordUnlockData, + ) { + // TODO Remove HasMasterPassword from UserDecryptionOptions https://bitwarden.atlassian.net/browse/PM-23475 + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + ); + userDecryptionOpts.hasMasterPassword = true; + await this.userDecryptionOptionsService.setUserDecryptionOptionsById( + userId, + userDecryptionOpts, + ); + + // TODO Remove KDF state https://bitwarden.atlassian.net/browse/PM-30661 + await this.kdfConfigService.setKdfConfig(userId, kdfConfig); + // TODO Remove master key memory state https://bitwarden.atlassian.net/browse/PM-23477 + await this.masterPasswordService.setMasterKeyEncryptedUserKey(masterKeyWrappedUserKey, userId); + + // TODO Removed with https://bitwarden.atlassian.net/browse/PM-30676 + await this.masterPasswordService.setLegacyMasterKeyFromUnlockData( + newPassword, + masterPasswordUnlockData, + userId, + ); + } + /** * As part of [PM-28494], adding this setting path to accommodate the changes that are * emerging with pm-23246-unlock-with-master-password-unlock-data. @@ -310,44 +475,4 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi enrollmentRequest, ); } - - async setInitialPasswordTdeOffboarding( - credentials: SetInitialPasswordTdeOffboardingCredentials, - userId: UserId, - ) { - const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials; - for (const [key, value] of Object.entries(credentials)) { - if (value == null) { - throw new Error(`${key} not found. Could not set password.`); - } - } - - if (userId == null) { - throw new Error("userId not found. Could not set password."); - } - - const userKey = await firstValueFrom(this.keyService.userKey$(userId)); - if (userKey == null) { - throw new Error("userKey not found. Could not set password."); - } - - const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey( - newMasterKey, - userKey, - ); - - if (!newMasterKeyEncryptedUserKey[1].encryptedString) { - throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password."); - } - - const request = new UpdateTdeOffboardingPasswordRequest(); - request.key = newMasterKeyEncryptedUserKey[1].encryptedString; - request.newMasterPasswordHash = newServerMasterKeyHash; - request.masterPasswordHint = newPasswordHint; - - await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request); - - // Clear force set password reason to allow navigation back to vault. - await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); - } } diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts index af4505371d3..6b3981a5231 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -1,5 +1,8 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +// Polyfill for Symbol.dispose required by the service's use of `using` keyword +import "core-js/proposals/explicit-resource-management"; + +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, Observable, of } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -27,17 +30,35 @@ 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 { + MasterPasswordSalt, + MasterPasswordUnlockData, +} from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service"; +import { Rc } from "@bitwarden/common/platform/misc/reference-counting/rc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { + DEFAULT_KDF_CONFIG, + fromSdkKdfConfig, + KdfConfigService, + KeyService, +} from "@bitwarden/key-management"; +import { + AuthClient, + BitwardenClient, + WrappedAccountCryptographicState, +} from "@bitwarden/sdk-internal"; import { DefaultSetInitialPasswordService } from "./default-set-initial-password.service.implementation"; import { + InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, @@ -58,6 +79,7 @@ describe("DefaultSetInitialPasswordService", () => { let organizationUserApiService: MockProxy; let userDecryptionOptionsService: MockProxy; let accountCryptographicStateService: MockProxy; + const registerSdkService = mock(); let userId: UserId; let userKey: UserKey; @@ -94,6 +116,7 @@ describe("DefaultSetInitialPasswordService", () => { organizationUserApiService, userDecryptionOptionsService, accountCryptographicStateService, + registerSdkService, ); }); @@ -834,4 +857,246 @@ describe("DefaultSetInitialPasswordService", () => { }); }); }); + + describe("initializePasswordJitPasswordUserV2Encryption()", () => { + let mockSdkRef: { + value: MockProxy; + [Symbol.dispose]: jest.Mock; + }; + let mockSdk: { + take: jest.Mock; + }; + let mockRegistration: jest.Mock; + + const userId = "d4e2e3a1-1b5e-4c3b-8d7a-9f8e7d6c5b4a" as UserId; + const orgId = "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d" as OrganizationId; + + const credentials: InitializeJitPasswordCredentials = { + newPasswordHint: "test-hint", + orgSsoIdentifier: "org-sso-id", + orgId: orgId, + resetPasswordAutoEnroll: false, + newPassword: "Test@Password123!", + salt: "user@example.com" as unknown as MasterPasswordSalt, + }; + + const orgKeys: OrganizationKeysResponse = { + publicKey: "org-public-key-base64", + privateKey: "org-private-key-encrypted", + } as OrganizationKeysResponse; + + const sdkRegistrationResult = { + account_cryptographic_state: { + V2: { + private_key: makeEncString().encryptedString!, + signed_public_key: "test-signed-public-key", + signing_key: makeEncString().encryptedString!, + security_state: "test-security-state", + }, + }, + master_password_unlock: { + kdf: { + pBKDF2: { + iterations: 600000, + }, + }, + masterKeyWrappedUserKey: makeEncString().encryptedString!, + salt: "user@example.com" as unknown as MasterPasswordSalt, + }, + user_key: makeSymmetricCryptoKey(64).keyB64, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockSdkRef = { + value: mock(), + [Symbol.dispose]: jest.fn(), + }; + + mockSdkRef.value.auth.mockReturnValue({ + registration: jest.fn().mockReturnValue({ + post_keys_for_jit_password_registration: jest.fn(), + }), + } as unknown as AuthClient); + + mockSdk = { + take: jest.fn().mockReturnValue(mockSdkRef), + }; + + registerSdkService.registerClient$.mockReturnValue( + of(mockSdk) as unknown as Observable>, + ); + + organizationApiService.getKeys.mockResolvedValue(orgKeys); + + mockRegistration = mockSdkRef.value.auth().registration() + .post_keys_for_jit_password_registration as unknown as jest.Mock; + mockRegistration.mockResolvedValue(sdkRegistrationResult); + + const mockUserDecryptionOpts = new UserDecryptionOptions({ hasMasterPassword: false }); + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(mockUserDecryptionOpts), + ); + }); + + it("should successfully initialize JIT password user", async () => { + await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + expect(organizationApiService.getKeys).toHaveBeenCalledWith(credentials.orgId); + + expect(registerSdkService.registerClient$).toHaveBeenCalledWith(userId); + expect(mockRegistration).toHaveBeenCalledWith( + expect.objectContaining({ + org_id: credentials.orgId, + org_public_key: orgKeys.publicKey, + master_password: credentials.newPassword, + master_password_hint: credentials.newPasswordHint, + salt: credentials.salt, + organization_sso_identifier: credentials.orgSsoIdentifier, + user_id: userId, + reset_password_enroll: credentials.resetPasswordAutoEnroll, + }), + ); + + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( + sdkRegistrationResult.account_cryptographic_state, + userId, + ); + + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + + expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith( + MasterPasswordUnlockData.fromSdk(sdkRegistrationResult.master_password_unlock), + userId, + ); + + expect(keyService.setUserKey).toHaveBeenCalledWith( + SymmetricCryptoKey.fromString(sdkRegistrationResult.user_key) as UserKey, + userId, + ); + + // Verify legacy state updates below + expect(userDecryptionOptionsService.userDecryptionOptionsById$).toHaveBeenCalledWith(userId); + expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith( + userId, + expect.objectContaining({ hasMasterPassword: true }), + ); + + expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith( + userId, + fromSdkKdfConfig(sdkRegistrationResult.master_password_unlock.kdf), + ); + + expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + new EncString(sdkRegistrationResult.master_password_unlock.masterKeyWrappedUserKey), + userId, + ); + + expect(masterPasswordService.setLegacyMasterKeyFromUnlockData).toHaveBeenCalledWith( + credentials.newPassword, + MasterPasswordUnlockData.fromSdk(sdkRegistrationResult.master_password_unlock), + userId, + ); + }); + + describe("input validation", () => { + it.each([ + "newPasswordHint", + "orgSsoIdentifier", + "orgId", + "resetPasswordAutoEnroll", + "newPassword", + "salt", + ])("should throw error when %s is null", async (field) => { + const invalidCredentials = { + ...credentials, + [field]: null, + } as unknown as InitializeJitPasswordCredentials; + + const promise = sut.initializePasswordJitPasswordUserV2Encryption( + invalidCredentials, + userId, + ); + + await expect(promise).rejects.toThrow(`${field} is required.`); + + expect(organizationApiService.getKeys).not.toHaveBeenCalled(); + expect(registerSdkService.registerClient$).not.toHaveBeenCalled(); + }); + + it("should throw error when userId is null", async () => { + const nullUserId = null as unknown as UserId; + + const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, nullUserId); + + await expect(promise).rejects.toThrow("User ID is required."); + expect(organizationApiService.getKeys).not.toHaveBeenCalled(); + }); + }); + + describe("organization API error handling", () => { + it("should throw when organizationApiService.getKeys returns null", async () => { + organizationApiService.getKeys.mockResolvedValue( + null as unknown as OrganizationKeysResponse, + ); + + const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + await expect(promise).rejects.toThrow("Organization keys response is null."); + expect(organizationApiService.getKeys).toHaveBeenCalledWith(credentials.orgId); + expect(registerSdkService.registerClient$).not.toHaveBeenCalled(); + }); + + it("should throw when organizationApiService.getKeys rejects", async () => { + const apiError = new Error("API network error"); + organizationApiService.getKeys.mockRejectedValue(apiError); + + const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + await expect(promise).rejects.toThrow("API network error"); + expect(registerSdkService.registerClient$).not.toHaveBeenCalled(); + }); + }); + + describe("SDK error handling", () => { + it("should throw when SDK is not available", async () => { + organizationApiService.getKeys.mockResolvedValue(orgKeys); + registerSdkService.registerClient$.mockReturnValue( + of(null) as unknown as Observable>, + ); + + const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + await expect(promise).rejects.toThrow("SDK not available"); + }); + + it("should throw when SDK registration fails", async () => { + const sdkError = new Error("SDK crypto operation failed"); + + organizationApiService.getKeys.mockResolvedValue(orgKeys); + mockRegistration.mockRejectedValue(sdkError); + + const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + await expect(promise).rejects.toThrow("SDK crypto operation failed"); + }); + }); + + it("should throw when account_cryptographic_state is not V2", async () => { + const invalidResult = { + ...sdkRegistrationResult, + account_cryptographic_state: { V1: {} } as unknown as WrappedAccountCryptographicState, + }; + + mockRegistration.mockResolvedValue(invalidResult); + + const promise = sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId); + + await expect(promise).rejects.toThrow("Unexpected V2 account cryptographic state"); + }); + }); }); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts index 0e0bae62b9a..4ab26ecd09e 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -21,14 +21,16 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { assertTruthy, assertNonNullish } from "@bitwarden/common/auth/utils"; +import { assertNonNullish, assertTruthy } from "@bitwarden/common/auth/utils"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { AnonLayoutWrapperDataService, ButtonModule, @@ -39,6 +41,7 @@ import { import { I18nPipe } from "@bitwarden/ui-common"; import { + InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, @@ -86,6 +89,7 @@ export class SetInitialPasswordComponent implements OnInit { private syncService: SyncService, private toastService: ToastService, private validationService: ValidationService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -101,6 +105,51 @@ export class SetInitialPasswordComponent implements OnInit { this.initializing = false; } + protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { + this.submitting = true; + + switch (this.userType) { + case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER: { + const accountEncryptionV2 = await this.configService.getFeatureFlag( + FeatureFlag.EnableAccountEncryptionV2JitPasswordRegistration, + ); + + if (accountEncryptionV2) { + await this.setInitialPasswordJitMPUserV2Encryption(passwordInputResult); + return; + } + + await this.setInitialPassword(passwordInputResult); + + break; + } + case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: + await this.setInitialPassword(passwordInputResult); + break; + case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER: + await this.setInitialPasswordTdeOffboarding(passwordInputResult); + break; + default: + this.logService.error( + `Unexpected user type: ${this.userType}. Could not set initial password.`, + ); + this.validationService.showError("Unexpected user type. Could not set initial password."); + } + } + + protected async logout() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + if (confirmed) { + this.messagingService.send("logout"); + } + } + private async establishUserType() { if (!this.userId) { throw new Error("userId not found. Could not determine user type."); @@ -189,22 +238,39 @@ export class SetInitialPasswordComponent implements OnInit { } } - protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { - this.submitting = true; + private async setInitialPasswordJitMPUserV2Encryption(passwordInputResult: PasswordInputResult) { + const ctx = "Could not set initial password for SSO JIT master password encryption user."; + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertTruthy(passwordInputResult.salt, "salt", ctx); + assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx); + assertTruthy(this.orgId, "orgId", ctx); + assertTruthy(this.userId, "userId", ctx); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + assertNonNullish(this.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish - switch (this.userType) { - case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER: - case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: - await this.setInitialPassword(passwordInputResult); - break; - case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER: - await this.setInitialPasswordTdeOffboarding(passwordInputResult); - break; - default: - this.logService.error( - `Unexpected user type: ${this.userType}. Could not set initial password.`, - ); - this.validationService.showError("Unexpected user type. Could not set initial password."); + try { + const credentials: InitializeJitPasswordCredentials = { + newPasswordHint: passwordInputResult.newPasswordHint, + orgSsoIdentifier: this.orgSsoIdentifier, + orgId: this.orgId as OrganizationId, + resetPasswordAutoEnroll: this.resetPasswordAutoEnroll, + newPassword: passwordInputResult.newPassword, + salt: passwordInputResult.salt, + }; + + await this.setInitialPasswordService.initializePasswordJitPasswordUserV2Encryption( + credentials, + this.userId, + ); + + this.showSuccessToastByUserType(); + + this.submitting = false; + await this.router.navigate(["vault"]); + } catch (e) { + this.logService.error("Error setting initial password", e); + this.validationService.showError(e); + this.submitting = false; } } @@ -307,17 +373,4 @@ export class SetInitialPasswordComponent implements OnInit { }); } } - - protected async logout() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "logOut" }, - content: { key: "logOutConfirmation" }, - acceptButtonText: { key: "logOut" }, - type: "warning", - }); - - if (confirmed) { - this.messagingService.send("logout"); - } - } } diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts index 5620194e1bb..2667040c707 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts @@ -1,5 +1,5 @@ import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { MasterKey } from "@bitwarden/common/types/key"; import { KdfConfig } from "@bitwarden/key-management"; @@ -61,6 +61,24 @@ export interface SetInitialPasswordTdeOffboardingCredentials { newPasswordHint: string; } +/** + * Credentials required to initialize a just-in-time (JIT) provisioned user with a master password. + */ +export interface InitializeJitPasswordCredentials { + /** Hint for the new master password */ + newPasswordHint: string; + /** SSO identifier for the organization */ + orgSsoIdentifier: string; + /** Organization ID */ + orgId: OrganizationId; + /** Whether to auto-enroll the user in account recovery (reset password) */ + resetPasswordAutoEnroll: boolean; + /** The new master password */ + newPassword: string; + /** Master password salt (typically the user's email) */ + salt: MasterPasswordSalt; +} + /** * Handles setting an initial password for an existing authed user. * @@ -95,4 +113,14 @@ export abstract class SetInitialPasswordService { credentials: SetInitialPasswordTdeOffboardingCredentials, userId: UserId, ) => Promise; + + /** + * Initializes a JIT-provisioned user's cryptographic state and enrolls them in master password unlock. + * @param credentials The credentials needed to initialize the JIT password user + * @param userId The account userId + */ + abstract initializePasswordJitPasswordUserV2Encryption( + credentials: InitializeJitPasswordCredentials, + userId: UserId, + ): Promise; } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 5eaac4033eb..cf41b28baca 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -108,7 +108,7 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@ import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; -import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/auth/send-access"; +import { DefaultSendTokenService, SendTokenService } from "@bitwarden/common/auth/send-access"; import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service"; @@ -131,10 +131,10 @@ import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauth import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service"; import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service"; import { - TwoFactorApiService, DefaultTwoFactorApiService, - TwoFactorService, DefaultTwoFactorService, + TwoFactorApiService, + TwoFactorService, } from "@bitwarden/common/auth/two-factor"; import { AutofillSettingsService, @@ -208,8 +208,8 @@ import { PinService } from "@bitwarden/common/key-management/pin/pin.service.imp import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service"; import { - SendPasswordService, DefaultSendPasswordService, + SendPasswordService, } from "@bitwarden/common/key-management/sends"; import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout"; import { @@ -387,12 +387,12 @@ import { SafeInjectionToken } from "@bitwarden/ui-common"; // eslint-disable-next-line no-restricted-imports import { PasswordRepromptService } from "@bitwarden/vault"; import { + DefaultVaultExportApiService, IndividualVaultExportService, IndividualVaultExportServiceAbstraction, - DefaultVaultExportApiService, - VaultExportApiService, OrganizationVaultExportService, OrganizationVaultExportServiceAbstraction, + VaultExportApiService, VaultExportService, VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; @@ -1583,6 +1583,7 @@ const safeProviders: SafeProvider[] = [ OrganizationUserApiService, InternalUserDecryptionOptionsServiceAbstraction, AccountCryptographicStateService, + RegisterSdkService, ], }), safeProvider({ diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f82c095d45f..811f4e524ac 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -47,6 +47,7 @@ export enum FeatureFlag { ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component", PM27279_V2RegistrationTdeJit = "pm-27279-v2-registration-tde-jit", EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration", + EnableAccountEncryptionV2JitPasswordRegistration = "enable-account-encryption-v2-jit-password-registration", /* Tools */ UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators", @@ -156,6 +157,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE, [FeatureFlag.PM27279_V2RegistrationTdeJit]: FALSE, [FeatureFlag.EnableAccountEncryptionV2KeyConnectorRegistration]: FALSE, + [FeatureFlag.EnableAccountEncryptionV2JitPasswordRegistration]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, diff --git a/package-lock.json b/package-lock.json index 95842c6b409..ff632dc2807 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.15", "@angular/platform-browser-dynamic": "20.3.15", "@angular/router": "20.3.15", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.450", - "@bitwarden/sdk-internal": "0.2.0-main.450", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.470", + "@bitwarden/sdk-internal": "0.2.0-main.470", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4982,9 +4982,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.450", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.450.tgz", - "integrity": "sha512-WCihR6ykpIfaqJBHl4Wou4xDB8mp+5UPi94eEKYUdkx/9/19YyX33SX9H56zEriOuOMCD8l2fymhzAFjAAB++g==", + "version": "0.2.0-main.470", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.470.tgz", + "integrity": "sha512-QYhxv5eX6ouFJv94gMtBW7MjuK6t2KAN9FLz+/w1wnq8dScnA9Iky25phNPw+iHMgWwhq/dzZq45asKUFF//oA==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -5087,9 +5087,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.450", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.450.tgz", - "integrity": "sha512-XRhrBN0uoo66ONx7dYo9glhe9N451+VhwtC/oh3wo3j3qYxbPwf9yE98szlQ52u3iUExLisiYJY7sQNzhZrbZw==", + "version": "0.2.0-main.470", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.470.tgz", + "integrity": "sha512-XKvcUtoU6NnxeEzl3WK7bATiCh2RNxRmuX6JYNgcQHUtHUH+x3ckToR6II1qM3nha0VH0u1ijy3+07UdNQM+JQ==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 01d11df89f8..829dc91370a 100644 --- a/package.json +++ b/package.json @@ -162,8 +162,8 @@ "@angular/platform-browser": "20.3.15", "@angular/platform-browser-dynamic": "20.3.15", "@angular/router": "20.3.15", - "@bitwarden/sdk-internal": "0.2.0-main.450", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.450", + "@bitwarden/sdk-internal": "0.2.0-main.470", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.470", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0",