1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-08 03:23:50 +00:00

[PM-22194] Remove key rotation v1 (#14945)

This commit is contained in:
Bernd Schoolmann
2025-06-03 23:52:53 +02:00
committed by GitHub
parent 6ea944393b
commit 9aaeacf2be
4 changed files with 7 additions and 341 deletions

View File

@@ -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;

View File

@@ -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<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;
}
}