From 75d16df6afc44f23fdf945955c7f1f30e5a46c2d Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:17:05 -0800 Subject: [PATCH] [PM-27086] create setInitialPasswordV2() --- ...initial-password.service.implementation.ts | 170 +++++++++++++++++- .../set-initial-password.component.ts | 59 +++++- ...et-initial-password.service.abstraction.ts | 32 +++- 3 files changed, 253 insertions(+), 8 deletions(-) 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..1ab4b12a66e 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 @@ -19,13 +19,20 @@ 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, + MasterPasswordAuthenticationData, + MasterPasswordAuthenticationHash, + 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 { 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 { PureCrypto } from "@bitwarden/sdk-internal"; import { SetInitialPasswordService, @@ -49,6 +56,9 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi protected accountCryptographicStateService: AccountCryptographicStateService, ) {} + /** + * @deprecated To be removed in PM-28143 + */ async setInitialPassword( credentials: SetInitialPasswordCredentials, userType: SetInitialPasswordUserType, @@ -199,6 +209,145 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi } } + async setInitialPasswordV2( + credentials: SetInitialPasswordCredentials, + userType: SetInitialPasswordUserType, + userId: UserId, + ): Promise { + const { + newPassword, + newPasswordHint, + kdfConfig, + salt, + orgSsoIdentifier, + orgId, + resetPasswordAutoEnroll, + } = 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."); + } + if (userType == null) { + throw new Error("userType not found. Could not set password."); + } + + let userKey = await firstValueFrom(this.keyService.userKey$(userId)); + + if (userKey == null) { + userKey = new SymmetricCryptoKey(PureCrypto.make_user_key_aes256_cbc_hmac()) as UserKey; + } + + const authenticationData: MasterPasswordAuthenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + newPassword, + kdfConfig, + salt, + ); + + const unlockData: MasterPasswordUnlockData = + await this.masterPasswordService.makeMasterPasswordUnlockData( + newPassword, + kdfConfig, + salt, + userKey, + ); + + if (unlockData.masterKeyWrappedUserKey == null) { + throw new Error("masterKeyEncryptedUserKey not found. Could not set password."); + } + + let keyPair: [string, EncString] | null = null; + let keysRequest: KeysRequest | null = null; + + if (userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + /** + * A user being JIT provisioned into a MP encryption org does not yet have a user + * asymmetric key pair, so we create it for them here. + * + * Sidenote: + * In the case of a TDE user whose permissions require that they have a MP - that user + * will already have a user asymmetric key pair by this point, so we skip this if-block + * so that we don't create a new key pair for them. + */ + + // Extra safety check (see description on https://github.com/bitwarden/clients/pull/10180): + // In case we have have a local private key and are not sure whether it has been posted to the server, + // we post the local private key instead of generating a new one + const existingUserPrivateKey = (await firstValueFrom( + this.keyService.userPrivateKey$(userId), + )) as Uint8Array; + + const existingUserPublicKey = await firstValueFrom(this.keyService.userPublicKey$(userId)); + + if (existingUserPrivateKey != null && existingUserPublicKey != null) { + const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); + + // Existing key pair + keyPair = [ + existingUserPublicKeyB64, + await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey), + ]; + } else { + // New key pair + keyPair = await this.keyService.makeKeyPair(userKey); + } + + if (keyPair == null) { + throw new Error("keyPair not found. Could not set password."); + } + if (!keyPair[1].encryptedString) { + throw new Error("encrypted private key not found. Could not set password."); + } + + keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); + } + + const request = SetPasswordRequest.newConstructor( + authenticationData, + unlockData, + newPasswordHint, + orgSsoIdentifier, + keysRequest, + ); + + await this.masterPasswordApiService.setPassword(request); + + // Clear force set password reason to allow navigation back to vault. + await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + + // User now has a password so update account decryption options in state + await this.updateAccountDecryptionPropertiesV2(unlockData, userId); + + /** + * Set the private key only for new JIT provisioned users in MP encryption orgs. + * (Existing TDE users will have their private key set on sync or on login.) + */ + if (keyPair != null && userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + if (!keyPair[1].encryptedString) { + throw new Error("encrypted private key not found. Could not set private key in state."); + } + await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId); + } + + // await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId); // TODO-rr-bw: how to handle local key hash? + + if (resetPasswordAutoEnroll) { + await this.handleResetPasswordAutoEnroll( + authenticationData.masterPasswordAuthenticationHash, + orgId, + userId, + ); + } + } + + /** + * @deprecated To be removed in PM-28143 + */ private async makeMasterKeyEncryptedUserKey( masterKey: MasterKey, userId: UserId, @@ -244,6 +393,23 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId); } + private async updateAccountDecryptionPropertiesV2( + unlockData: MasterPasswordUnlockData, + userId: UserId, + ) { + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + ); + userDecryptionOpts.hasMasterPassword = true; + await this.userDecryptionOptionsService.setUserDecryptionOptionsById( + userId, + userDecryptionOpts, + ); + + await this.masterPasswordService.setMasterPasswordUnlockData(unlockData, userId); + // await this.masterPasswordService.setMasterKey(masterKey, userId); // TODO-rr-bw: how to handle this? remove this? + } + /** * 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. @@ -269,7 +435,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi } private async handleResetPasswordAutoEnroll( - masterKeyHash: string, + masterKeyHash: MasterPasswordAuthenticationHash | string, // In PM-28143, remove `| string`; should only accept MasterPasswordAuthenticationHash. orgId: string, userId: UserId, ) { 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..10bd26d8db1 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 @@ -22,7 +22,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv 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 { 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"; @@ -71,6 +73,7 @@ export class SetInitialPasswordComponent implements OnInit { private accountService: AccountService, private activatedRoute: ActivatedRoute, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private configService: ConfigService, private dialogService: DialogService, private i18nService: I18nService, private logoutService: LogoutService, @@ -194,9 +197,19 @@ export class SetInitialPasswordComponent implements OnInit { switch (this.userType) { case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER: - case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: + case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: { + const newApisFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword, + ); + + if (newApisFlagEnabled) { + await this.setInitialPasswordV2(passwordInputResult); + return; + } + await this.setInitialPassword(passwordInputResult); break; + } case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER: await this.setInitialPasswordTdeOffboarding(passwordInputResult); break; @@ -208,6 +221,9 @@ export class SetInitialPasswordComponent implements OnInit { } } + /** + * @deprecated To be removed in PM-28143 + */ private async setInitialPassword(passwordInputResult: PasswordInputResult) { const ctx = "Could not set initial password."; assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx); @@ -254,6 +270,47 @@ export class SetInitialPasswordComponent implements OnInit { } } + private async setInitialPasswordV2(passwordInputResult: PasswordInputResult) { + const ctx = "Could not set initial password."; + + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertTruthy(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertTruthy(passwordInputResult.salt, "salt", ctx); + assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx); + assertTruthy(this.orgId, "orgId", ctx); + assertTruthy(this.userType, "userType", 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 + + try { + const credentials: SetInitialPasswordCredentials = { + newPassword: passwordInputResult.newPassword, + newPasswordHint: passwordInputResult.newPasswordHint, + kdfConfig: passwordInputResult.kdfConfig, + salt: passwordInputResult.salt, + orgSsoIdentifier: this.orgSsoIdentifier, + orgId: this.orgId, + resetPasswordAutoEnroll: this.resetPasswordAutoEnroll, + }; + + await this.setInitialPasswordService.setInitialPasswordV2( + credentials, + this.userType, + 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; + } + } + private async setInitialPasswordTdeOffboarding(passwordInputResult: PasswordInputResult) { const ctx = "Could not set initial password."; assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx); 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..8f01f87e5d5 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 @@ -43,16 +43,21 @@ export const SetInitialPasswordUserType: Readonly<{ }> = Object.freeze(_SetInitialPasswordUserType); export interface SetInitialPasswordCredentials { - newMasterKey: MasterKey; - newServerMasterKeyHash: string; - newLocalMasterKeyHash: string; + newPassword?: string; // Make required in PM-28143 (remove `?`) newPasswordHint: string; kdfConfig: KdfConfig; + salt?: MasterPasswordSalt; // Make required in PM-28143 (remove `?`) orgSsoIdentifier: string; orgId: string; resetPasswordAutoEnroll: boolean; - newPassword: string; - salt: MasterPasswordSalt; + + // The deprecated properties below will be removed in PM-28143 + /** @deprecated */ + newMasterKey?: MasterKey; + /** @deprecated */ + newServerMasterKeyHash?: string; + /** @deprecated */ + newLocalMasterKeyHash?: string; } export interface SetInitialPasswordTdeOffboardingCredentials { @@ -69,6 +74,8 @@ export interface SetInitialPasswordTdeOffboardingCredentials { */ export abstract class SetInitialPasswordService { /** + * @deprecated To be removed in PM-28143 + * * Sets an initial password for an existing authed user who is either: * - {@link SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER} * - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP} @@ -95,4 +102,19 @@ export abstract class SetInitialPasswordService { credentials: SetInitialPasswordTdeOffboardingCredentials, userId: UserId, ) => Promise; + + /** + * Sets an initial password for an existing authed user who is either: + * - {@link SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER} + * - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP} + * + * @param credentials An object of the credentials needed to set the initial password + * @throws If any property on the `credentials` object is null or undefined, or if a + * masterKeyEncryptedUserKey or newKeyPair could not be created. + */ + abstract setInitialPasswordV2: ( + credentials: SetInitialPasswordCredentials, + userType: SetInitialPasswordUserType, + userId: UserId, + ) => Promise; }