diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 1d95a498694..15d106057ba 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -12,16 +12,10 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { HashPurpose } from "@bitwarden/common/platform/enums"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -47,7 +41,6 @@ export class ChangePasswordComponent masterPasswordHint: string; checkForBreaches = true; characterMinimumMessage = ""; - userkeyRotationV2 = false; constructor( i18nService: I18nService, @@ -67,7 +60,6 @@ export class ChangePasswordComponent protected masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, toastService: ToastService, - private configService: ConfigService, ) { super( i18nService, @@ -84,8 +76,6 @@ export class ChangePasswordComponent } async ngOnInit() { - this.userkeyRotationV2 = await this.configService.getFeatureFlag(FeatureFlag.UserKeyRotationV2); - if (!(await this.userVerificationService.hasMasterPassword())) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -148,22 +138,14 @@ export class ChangePasswordComponent } async submit() { - if (this.userkeyRotationV2) { - this.loading = true; - await this.submitNew(); - this.loading = false; - } else { - await this.submitOld(); - } - } - - async submitNew() { + this.loading = true; if (this.currentMasterPassword == null || this.currentMasterPassword === "") { this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), message: this.i18nService.t("masterPasswordRequired"), }); + this.loading = false; return; } @@ -176,6 +158,7 @@ export class ChangePasswordComponent title: this.i18nService.t("errorOccurred"), message: this.i18nService.t("hintEqualsPassword"), }); + this.loading = false; return; } @@ -185,6 +168,7 @@ export class ChangePasswordComponent } if (!(await this.strongPassword())) { + this.loading = false; return; } @@ -207,6 +191,8 @@ export class ChangePasswordComponent title: this.i18nService.t("errorOccurred"), message: e.message, }); + } finally { + this.loading = false; } } @@ -270,116 +256,4 @@ export class ChangePasswordComponent }); } } - - async submitOld() { - if ( - this.masterPasswordHint != null && - this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase() - ) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("hintEqualsPassword"), - }); - return; - } - - this.leakedPassword = false; - if (this.checkForBreaches) { - this.leakedPassword = (await this.auditService.passwordLeaked(this.masterPassword)) > 0; - } - - await super.submit(); - } - - async setupSubmitActions() { - if (this.currentMasterPassword == null || this.currentMasterPassword === "") { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordRequired"), - }); - return false; - } - - if (this.rotateUserKey) { - await this.syncService.fullSync(true); - } - - return super.setupSubmitActions(); - } - - async performSubmitActions( - newMasterPasswordHash: string, - newMasterKey: MasterKey, - newUserKey: [UserKey, EncString], - ) { - const [userId, email] = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), - ); - - const masterKey = await this.keyService.makeMasterKey( - this.currentMasterPassword, - email, - await this.kdfConfigService.getKdfConfig(userId), - ); - - const newLocalKeyHash = await this.keyService.hashMasterKey( - this.masterPassword, - newMasterKey, - HashPurpose.LocalAuthorization, - ); - - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId); - if (userKey == null) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("invalidMasterPassword"), - }); - return; - } - - const request = new PasswordRequest(); - request.masterPasswordHash = await this.keyService.hashMasterKey( - this.currentMasterPassword, - masterKey, - ); - request.masterPasswordHint = this.masterPasswordHint; - request.newMasterPasswordHash = newMasterPasswordHash; - request.key = newUserKey[1].encryptedString; - - try { - if (this.rotateUserKey) { - this.formPromise = this.masterPasswordApiService.postPassword(request).then(async () => { - // we need to save this for local masterkey verification during rotation - await this.masterPasswordService.setMasterKeyHash(newLocalKeyHash, userId as UserId); - await this.masterPasswordService.setMasterKey(newMasterKey, userId as UserId); - return this.updateKey(); - }); - } else { - this.formPromise = this.masterPasswordApiService.postPassword(request); - } - - await this.formPromise; - - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("masterPasswordChanged"), - message: this.i18nService.t("logBackIn"), - }); - this.messagingService.send("logout"); - } catch { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("errorOccurred"), - }); - } - } - - private async updateKey() { - const user = await firstValueFrom(this.accountService.activeAccount$); - await this.keyRotationService.rotateUserKeyAndEncryptedDataLegacy(this.masterPassword, user); - } } diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index c65c4ac3ea4..4dc5a206a63 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -244,21 +244,6 @@ describe("KeyRotationService", () => { mockWebauthnLoginAdminService.getRotatedData.mockResolvedValue(webauthn); }); - it("rotates the user key and encrypted data legacy", async () => { - await keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser); - - expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled(); - const arg = mockApiService.postUserKeyUpdate.mock.calls[0][0]; - expect(arg.key).toBe("mockNewUserKey"); - expect(arg.privateKey).toBe("mockEncryptedData"); - expect(arg.ciphers.length).toBe(2); - expect(arg.folders.length).toBe(2); - expect(arg.sends.length).toBe(2); - expect(arg.emergencyAccessKeys.length).toBe(1); - expect(arg.resetPasswordKeys.length).toBe(1); - expect(arg.webauthnKeys.length).toBe(2); - }); - it("rotates the userkey and encrypted data and changes master password", async () => { KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; @@ -383,34 +368,12 @@ describe("KeyRotationService", () => { expect(mockApiService.postUserKeyUpdateV2).not.toHaveBeenCalled(); }); - it("legacy throws if master password provided is falsey", async () => { - await expect( - keyRotationService.rotateUserKeyAndEncryptedDataLegacy("", mockUser), - ).rejects.toThrow(); - }); - it("throws if master password provided is falsey", async () => { await expect( keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData("", "", mockUser), ).rejects.toThrow(); }); - it("legacy throws if user key creation fails", async () => { - mockKeyService.makeUserKey.mockResolvedValueOnce([null, null]); - - await expect( - keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser), - ).rejects.toThrow(); - }); - - it("legacy throws if no private key is found", async () => { - privateKey.next(null); - - await expect( - keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser), - ).rejects.toThrow(); - }); - it("throws if no private key is found", async () => { keyPair.next(null); @@ -423,16 +386,6 @@ describe("KeyRotationService", () => { ).rejects.toThrow(); }); - it("legacy throws if master password is incorrect", async () => { - mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce( - new Error("Invalid master password"), - ); - - await expect( - keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser), - ).rejects.toThrow(); - }); - it("throws if master password is incorrect", async () => { mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce( new Error("Invalid master password"), @@ -447,14 +400,6 @@ describe("KeyRotationService", () => { ).rejects.toThrow(); }); - it("legacy throws if server rotation fails", async () => { - mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError")); - - await expect( - keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser), - ).rejects.toThrow(); - }); - it("throws if server rotation fails", async () => { KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 129d643f677..fc4ad0c869b 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -4,7 +4,6 @@ import { firstValueFrom } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; @@ -14,10 +13,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -39,7 +37,6 @@ import { AccountKeysRequest } from "./request/account-keys.request"; import { MasterPasswordUnlockDataRequest } from "./request/master-password-unlock-data.request"; import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request"; import { UnlockDataRequest } from "./request/unlock-data.request"; -import { UpdateKeyRequest } from "./request/update-key.request"; import { UserDataRequest } from "./request/userdata.request"; import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; @@ -302,152 +299,4 @@ export class UserKeyRotationService { // temporary until userkey can be better verified await this.vaultTimeoutService.logOut(); } - - /** - * Creates a new user key and re-encrypts all required data with the it. - * @param masterPassword current master password (used for validation) - * @deprecated - */ - async rotateUserKeyAndEncryptedDataLegacy(masterPassword: string, user: Account): Promise { - this.logService.info("[Userkey rotation] Starting legacy user key rotation..."); - if (!masterPassword) { - this.logService.info("[Userkey rotation] Invalid master password provided. Aborting!"); - throw new Error("Invalid master password"); - } - - if ((await this.syncService.getLastSync()) === null) { - this.logService.info("[Userkey rotation] Client was never synced. Aborting!"); - throw new Error( - "The local vault is de-synced and the keys cannot be rotated. Please log out and log back in to resolve this issue.", - ); - } - - const emergencyAccessGrantees = await this.emergencyAccessService.getPublicKeys(); - const orgs = await this.resetPasswordService.getPublicKeys(user.id); - - // Verify master password - // UV service sets master key on success since it is stored in memory and can be lost on refresh - const verification = { - type: VerificationType.MasterPassword, - secret: masterPassword, - } as MasterPasswordVerification; - - const { masterKey } = await this.userVerificationService.verifyUserByMasterPassword( - verification, - user.id, - user.email, - ); - - const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(masterKey); - - if (newUserKey == null || newEncUserKey == null || newEncUserKey.encryptedString == null) { - this.logService.info("[Userkey rotation] User key could not be created. Aborting!"); - throw new Error("User key could not be created"); - } - - // New user key - const key = newEncUserKey.encryptedString; - - // Add master key hash - const masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey); - - // Get original user key - // Note: We distribute the legacy key, but not all domains actually use it. If any of those - // domains break their legacy support it will break the migration process for legacy users. - const originalUserKey = await this.keyService.getUserKeyWithLegacySupport(user.id); - const isMasterKey = - (await firstValueFrom(this.keyService.userKey$(user.id))) != originalUserKey; - this.logService.info("[Userkey rotation] Is legacy user: " + isMasterKey); - - // Add re-encrypted data - const privateKey = await this.encryptPrivateKey(newUserKey, user.id); - if (privateKey == null) { - this.logService.info("[Userkey rotation] Private key could not be encrypted. Aborting!"); - throw new Error("Private key could not be encrypted"); - } - - // Create new request - const request = new UpdateKeyRequest(masterPasswordHash, key, privateKey); - - const rotatedCiphers = await this.cipherService.getRotatedData( - originalUserKey, - newUserKey, - user.id, - ); - if (rotatedCiphers != null) { - request.ciphers = rotatedCiphers; - } - - const rotatedFolders = await this.folderService.getRotatedData( - originalUserKey, - newUserKey, - user.id, - ); - if (rotatedFolders != null) { - request.folders = rotatedFolders; - } - - const rotatedSends = await this.sendService.getRotatedData( - originalUserKey, - newUserKey, - user.id, - ); - if (rotatedSends != null) { - request.sends = rotatedSends; - } - - const trustedUserPublicKeys = emergencyAccessGrantees.map((d) => d.publicKey); - const rotatedEmergencyAccessKeys = await this.emergencyAccessService.getRotatedData( - newUserKey, - trustedUserPublicKeys, - user.id, - ); - if (rotatedEmergencyAccessKeys != null) { - request.emergencyAccessKeys = rotatedEmergencyAccessKeys; - } - - const trustedOrgPublicKeys = orgs.map((d) => d.publicKey); - // Note: Reset password keys request model has user verification - // properties, but the rotation endpoint uses its own MP hash. - const rotatedResetPasswordKeys = await this.resetPasswordService.getRotatedData( - originalUserKey, - trustedOrgPublicKeys, - user.id, - ); - if (rotatedResetPasswordKeys != null) { - request.resetPasswordKeys = rotatedResetPasswordKeys; - } - - const rotatedWebauthnKeys = await this.webauthnLoginAdminService.getRotatedData( - originalUserKey, - newUserKey, - user.id, - ); - if (rotatedWebauthnKeys != null) { - request.webauthnKeys = rotatedWebauthnKeys; - } - - this.logService.info("[Userkey rotation] Posting user key rotation request to server"); - await this.apiService.postUserKeyUpdate(request); - this.logService.info("[Userkey rotation] Userkey rotation request posted to server"); - - // TODO PM-2199: Add device trust rotation support to the user key rotation endpoint - this.logService.info("[Userkey rotation] Rotating device trust..."); - await this.deviceTrustService.rotateDevicesTrust(user.id, newUserKey, masterPasswordHash); - this.logService.info("[Userkey rotation] Device trust rotation completed"); - await this.vaultTimeoutService.logOut(); - } - - private async encryptPrivateKey( - newUserKey: UserKey, - userId: UserId, - ): Promise { - const privateKey = await firstValueFrom( - this.keyService.userPrivateKeyWithLegacySupport$(userId), - ); - if (privateKey == null) { - throw new Error("No private key found for user key rotation"); - } - return (await this.encryptService.wrapDecapsulationKey(privateKey, newUserKey)).encryptedString; - } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index b78f5a1deec..f64590b9e66 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -38,7 +38,6 @@ export enum FeatureFlag { /* Key Management */ PrivateKeyRegeneration = "pm-12241-private-key-regeneration", - UserKeyRotationV2 = "userkey-rotation-v2", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UseSDKForDecryption = "use-sdk-for-decryption", PM17987_BlockType0 = "pm-17987-block-type-0", @@ -116,7 +115,6 @@ export const DefaultFeatureFlagValue = { /* Key Management */ [FeatureFlag.PrivateKeyRegeneration]: FALSE, - [FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UseSDKForDecryption]: FALSE, [FeatureFlag.PM17987_BlockType0]: FALSE,