mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
186 lines
7.4 KiB
TypeScript
186 lines
7.4 KiB
TypeScript
import { Injectable } from "@angular/core";
|
|
import { firstValueFrom } from "rxjs";
|
|
|
|
import { AccountInfo } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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 { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
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";
|
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
|
import { KeyService } from "@bitwarden/key-management";
|
|
|
|
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
|
import { WebauthnLoginAdminService } from "../core";
|
|
import { EmergencyAccessService } from "../emergency-access";
|
|
|
|
import { UpdateKeyRequest } from "./request/update-key.request";
|
|
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
|
|
|
|
@Injectable()
|
|
export class UserKeyRotationService {
|
|
constructor(
|
|
private userVerificationService: UserVerificationService,
|
|
private apiService: UserKeyRotationApiService,
|
|
private cipherService: CipherService,
|
|
private folderService: FolderService,
|
|
private sendService: SendService,
|
|
private emergencyAccessService: EmergencyAccessService,
|
|
private resetPasswordService: OrganizationUserResetPasswordService,
|
|
private deviceTrustService: DeviceTrustServiceAbstraction,
|
|
private keyService: KeyService,
|
|
private encryptService: EncryptService,
|
|
private syncService: SyncService,
|
|
private webauthnLoginAdminService: WebauthnLoginAdminService,
|
|
private logService: LogService,
|
|
) {}
|
|
|
|
/**
|
|
* Creates a new user key and re-encrypts all required data with the it.
|
|
* @param masterPassword current master password (used for validation)
|
|
*/
|
|
async rotateUserKeyAndEncryptedData(
|
|
masterPassword: string,
|
|
user: { id: UserId } & AccountInfo,
|
|
): Promise<void> {
|
|
this.logService.info("[Userkey rotation] Starting 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.",
|
|
);
|
|
}
|
|
|
|
// 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 || !newEncUserKey) {
|
|
this.logService.info("[Userkey rotation] User key could not be created. Aborting!");
|
|
throw new Error("User key could not be created");
|
|
}
|
|
|
|
// Create new request
|
|
const request = new UpdateKeyRequest();
|
|
|
|
// Add new user key
|
|
request.key = newEncUserKey.encryptedString;
|
|
|
|
// Add master key hash
|
|
const masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey);
|
|
request.masterPasswordHash = masterPasswordHash;
|
|
|
|
// 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
|
|
request.privateKey = await this.encryptPrivateKey(newUserKey, user.id);
|
|
|
|
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 rotatedEmergencyAccessKeys = await this.emergencyAccessService.getRotatedData(
|
|
originalUserKey,
|
|
newUserKey,
|
|
user.id,
|
|
);
|
|
if (rotatedEmergencyAccessKeys != null) {
|
|
request.emergencyAccessKeys = rotatedEmergencyAccessKeys;
|
|
}
|
|
|
|
// 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,
|
|
newUserKey,
|
|
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");
|
|
}
|
|
|
|
private async encryptPrivateKey(
|
|
newUserKey: UserKey,
|
|
userId: UserId,
|
|
): Promise<EncryptedString | null> {
|
|
const privateKey = await firstValueFrom(
|
|
this.keyService.userPrivateKeyWithLegacySupport$(userId),
|
|
);
|
|
if (!privateKey) {
|
|
throw new Error("No private key found for user key rotation");
|
|
}
|
|
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
|
|
}
|
|
}
|