diff --git a/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts b/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts index 51bf6c3a238..e97ad9f15f2 100644 --- a/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts +++ b/apps/web/src/app/auth/core/services/change-password/web-change-password.service.ts @@ -23,4 +23,11 @@ export class WebChangePasswordService hint, ); } + + override async rotateUserKeyAndEncryptedDataLegacy( + newPassword: string, + user: Account, + ): Promise { + await this.userKeyRotationService.rotateUserKeyAndEncryptedDataLegacy(newPassword, user); + } } diff --git a/libs/auth/src/angular/change-existing-password/change-existing-password.component.ts b/libs/auth/src/angular/change-existing-password/change-existing-password.component.ts index f5ec816a9d8..2377f273531 100644 --- a/libs/auth/src/angular/change-existing-password/change-existing-password.component.ts +++ b/libs/auth/src/angular/change-existing-password/change-existing-password.component.ts @@ -7,12 +7,15 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.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 { HashPurpose } from "@bitwarden/common/platform/enums"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; @@ -34,6 +37,7 @@ export class ChangeExistingPasswordComponent implements OnInit { email?: string; masterPasswordPolicyOptions?: MasterPasswordPolicyOptions; userkeyRotationV2 = false; + formPromise: Promise; constructor( private accountService: AccountService, @@ -100,8 +104,77 @@ export class ChangeExistingPasswordComponent implements OnInit { } } - async submitOld(passwordInputResult: PasswordInputResult) {} + async submitOld(passwordInputResult: PasswordInputResult) { + if (passwordInputResult.rotateUserKey) { + await this.syncService.fullSync(true); + } + const masterKey = await this.keyService.makeMasterKey( + passwordInputResult.currentPassword, + await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email))), + await this.kdfConfigService.getKdfConfig(), + ); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const newLocalKeyHash = await this.keyService.hashMasterKey( + passwordInputResult.newPassword, + passwordInputResult.masterKey, + 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( + passwordInputResult.currentPassword, + masterKey, + ); + request.masterPasswordHint = passwordInputResult.hint; + request.newMasterPasswordHash = passwordInputResult.serverMasterKeyHash; + // request.key = newUserKey[1].encryptedString; + + try { + if (passwordInputResult.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( + passwordInputResult.masterKey, + userId as UserId, + ); + return this.updateKey(passwordInputResult.newPassword); + }); + } 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"), + }); + } + } + + // todo: move this to a service + // https://bitwarden.atlassian.net/browse/PM-17108 async updatePassword(currentPassword: string, newPassword: string, hint: string) { const { userId, email } = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => ({ userId: a?.id, email: a?.email }))), @@ -155,4 +228,9 @@ export class ChangeExistingPasswordComponent implements OnInit { }); } } + + private async updateKey(newPassword: string) { + const user = await firstValueFrom(this.accountService.activeAccount$); + await this.changePasswordService.rotateUserKeyAndEncryptedDataLegacy(newPassword, user); + } } diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index c890682ff5b..00c4c8bf429 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -14,8 +14,10 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { AsyncActionsModule, ButtonModule, @@ -158,12 +160,14 @@ export class InputPasswordComponent implements OnInit { constructor( private accountService: AccountService, private auditService: AuditService, + private cipherService: CipherService, private dialogService: DialogService, private formBuilder: FormBuilder, private i18nService: I18nService, private kdfConfigService: KdfConfigService, private keyService: KeyService, private masterPasswordService: MasterPasswordServiceAbstraction, + private platformUtilsService: PlatformUtilsService, private policyService: PolicyService, private toastService: ToastService, ) {} @@ -385,4 +389,55 @@ export class InputPasswordComponent implements OnInit { return true; } + + async rotateUserKeyClicked() { + if (this.rotateUserKey) { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const ciphers = await this.cipherService.getAllDecrypted(activeUserId); + let hasOldAttachments = false; + if (ciphers != null) { + for (let i = 0; i < ciphers.length; i++) { + if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) { + hasOldAttachments = true; + break; + } + } + } + + if (hasOldAttachments) { + const learnMore = await this.dialogService.openSimpleDialog({ + title: { key: "warning" }, + content: { key: "oldAttachmentsNeedFixDesc" }, + acceptButtonText: { key: "learnMore" }, + cancelButtonText: { key: "close" }, + type: "warning", + }); + + if (learnMore) { + this.platformUtilsService.launchUri( + "https://bitwarden.com/help/attachments/#add-storage-space", + ); + } + + this.formGroup.controls.rotateUserKey.setValue(false); + return; + } + + const result = await this.dialogService.openSimpleDialog({ + title: { key: "rotateEncKeyTitle" }, + content: + this.i18nService.t("updateEncryptionKeyWarning") + + " " + + this.i18nService.t("updateEncryptionKeyExportWarning") + + " " + + this.i18nService.t("rotateEncKeyConfirmation"), + type: "warning", + }); + + if (!result) { + this.formGroup.controls.rotateUserKey.setValue(false); + } + } + } } diff --git a/libs/auth/src/common/abstractions/change-password.service.abstraction.ts b/libs/auth/src/common/abstractions/change-password.service.abstraction.ts index 8f81edab830..ed10aaec67b 100644 --- a/libs/auth/src/common/abstractions/change-password.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/change-password.service.abstraction.ts @@ -7,4 +7,9 @@ export abstract class ChangePasswordService { user: Account, hint: string, ): Promise; + + abstract rotateUserKeyAndEncryptedDataLegacy( + newPassword: string, + user: Account, + ): Promise; } diff --git a/libs/auth/src/common/services/change-password/default-change-password.service.ts b/libs/auth/src/common/services/change-password/default-change-password.service.ts index 29bc17b6de5..e5ebe97bb78 100644 --- a/libs/auth/src/common/services/change-password/default-change-password.service.ts +++ b/libs/auth/src/common/services/change-password/default-change-password.service.ts @@ -11,4 +11,11 @@ export class DefaultChangePasswordService implements ChangePasswordService { ): Promise { return null; } + + async rotateUserKeyAndEncryptedDataLegacy( + newPassword: string, + user: Account, + ): Promise { + return null; + } }