mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
[PM-22194] Remove key rotation v1 (#14945)
This commit is contained in:
@@ -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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
@@ -47,7 +41,6 @@ export class ChangePasswordComponent
|
|||||||
masterPasswordHint: string;
|
masterPasswordHint: string;
|
||||||
checkForBreaches = true;
|
checkForBreaches = true;
|
||||||
characterMinimumMessage = "";
|
characterMinimumMessage = "";
|
||||||
userkeyRotationV2 = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
@@ -67,7 +60,6 @@ export class ChangePasswordComponent
|
|||||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
@@ -84,8 +76,6 @@ export class ChangePasswordComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.userkeyRotationV2 = await this.configService.getFeatureFlag(FeatureFlag.UserKeyRotationV2);
|
|
||||||
|
|
||||||
if (!(await this.userVerificationService.hasMasterPassword())) {
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@@ -148,22 +138,14 @@ export class ChangePasswordComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
if (this.userkeyRotationV2) {
|
this.loading = true;
|
||||||
this.loading = true;
|
|
||||||
await this.submitNew();
|
|
||||||
this.loading = false;
|
|
||||||
} else {
|
|
||||||
await this.submitOld();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitNew() {
|
|
||||||
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: this.i18nService.t("errorOccurred"),
|
title: this.i18nService.t("errorOccurred"),
|
||||||
message: this.i18nService.t("masterPasswordRequired"),
|
message: this.i18nService.t("masterPasswordRequired"),
|
||||||
});
|
});
|
||||||
|
this.loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +158,7 @@ export class ChangePasswordComponent
|
|||||||
title: this.i18nService.t("errorOccurred"),
|
title: this.i18nService.t("errorOccurred"),
|
||||||
message: this.i18nService.t("hintEqualsPassword"),
|
message: this.i18nService.t("hintEqualsPassword"),
|
||||||
});
|
});
|
||||||
|
this.loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +168,7 @@ export class ChangePasswordComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(await this.strongPassword())) {
|
if (!(await this.strongPassword())) {
|
||||||
|
this.loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +191,8 @@ export class ChangePasswordComponent
|
|||||||
title: this.i18nService.t("errorOccurred"),
|
title: this.i18nService.t("errorOccurred"),
|
||||||
message: e.message,
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,21 +244,6 @@ describe("KeyRotationService", () => {
|
|||||||
mockWebauthnLoginAdminService.getRotatedData.mockResolvedValue(webauthn);
|
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 () => {
|
it("rotates the userkey and encrypted data and changes master password", async () => {
|
||||||
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
||||||
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
|
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
|
||||||
@@ -383,34 +368,12 @@ describe("KeyRotationService", () => {
|
|||||||
expect(mockApiService.postUserKeyUpdateV2).not.toHaveBeenCalled();
|
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 () => {
|
it("throws if master password provided is falsey", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData("", "", mockUser),
|
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData("", "", mockUser),
|
||||||
).rejects.toThrow();
|
).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 () => {
|
it("throws if no private key is found", async () => {
|
||||||
keyPair.next(null);
|
keyPair.next(null);
|
||||||
|
|
||||||
@@ -423,16 +386,6 @@ describe("KeyRotationService", () => {
|
|||||||
).rejects.toThrow();
|
).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 () => {
|
it("throws if master password is incorrect", async () => {
|
||||||
mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce(
|
mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce(
|
||||||
new Error("Invalid master password"),
|
new Error("Invalid master password"),
|
||||||
@@ -447,14 +400,6 @@ describe("KeyRotationService", () => {
|
|||||||
).rejects.toThrow();
|
).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 () => {
|
it("throws if server rotation fails", async () => {
|
||||||
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
||||||
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
|
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
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";
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
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 { UserKey } from "@bitwarden/common/types/key";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
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 { MasterPasswordUnlockDataRequest } from "./request/master-password-unlock-data.request";
|
||||||
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
|
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
|
||||||
import { UnlockDataRequest } from "./request/unlock-data.request";
|
import { UnlockDataRequest } from "./request/unlock-data.request";
|
||||||
import { UpdateKeyRequest } from "./request/update-key.request";
|
|
||||||
import { UserDataRequest } from "./request/userdata.request";
|
import { UserDataRequest } from "./request/userdata.request";
|
||||||
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
|
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
|
||||||
|
|
||||||
@@ -302,152 +299,4 @@ export class UserKeyRotationService {
|
|||||||
// temporary until userkey can be better verified
|
// temporary until userkey can be better verified
|
||||||
await this.vaultTimeoutService.logOut();
|
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<void> {
|
|
||||||
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<EncryptedString | undefined> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export enum FeatureFlag {
|
|||||||
|
|
||||||
/* Key Management */
|
/* Key Management */
|
||||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||||
UserKeyRotationV2 = "userkey-rotation-v2",
|
|
||||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||||
UseSDKForDecryption = "use-sdk-for-decryption",
|
UseSDKForDecryption = "use-sdk-for-decryption",
|
||||||
PM17987_BlockType0 = "pm-17987-block-type-0",
|
PM17987_BlockType0 = "pm-17987-block-type-0",
|
||||||
@@ -116,7 +115,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
|
|
||||||
/* Key Management */
|
/* Key Management */
|
||||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||||
[FeatureFlag.UserKeyRotationV2]: FALSE,
|
|
||||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||||
[FeatureFlag.UseSDKForDecryption]: FALSE,
|
[FeatureFlag.UseSDKForDecryption]: FALSE,
|
||||||
[FeatureFlag.PM17987_BlockType0]: FALSE,
|
[FeatureFlag.PM17987_BlockType0]: FALSE,
|
||||||
|
|||||||
Reference in New Issue
Block a user