mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-6789] finish key rotation distribution and fix legacy user (#9498)
* finish key rotation distribution and fix legacy user * add ticket to TODO * PR feedback: docs and renaming * fix webauthn tests * add test for send service * add await to test
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AccountInfo } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.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 { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../core";
|
||||
@@ -37,8 +35,6 @@ export class UserKeyRotationService {
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private cryptoService: CryptoService,
|
||||
private encryptService: EncryptService,
|
||||
private stateService: StateService,
|
||||
private accountService: AccountService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private syncService: SyncService,
|
||||
private webauthnLoginAdminService: WebauthnLoginAdminService,
|
||||
@@ -48,7 +44,10 @@ export class UserKeyRotationService {
|
||||
* 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): Promise<void> {
|
||||
async rotateUserKeyAndEncryptedData(
|
||||
masterPassword: string,
|
||||
user: { id: UserId } & AccountInfo,
|
||||
): Promise<void> {
|
||||
if (!masterPassword) {
|
||||
throw new Error("Invalid master password");
|
||||
}
|
||||
@@ -62,7 +61,7 @@ export class UserKeyRotationService {
|
||||
// Create master key to validate the master password
|
||||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email))),
|
||||
user.email,
|
||||
await this.kdfConfigService.getKdfConfig(),
|
||||
);
|
||||
|
||||
@@ -71,9 +70,7 @@ export class UserKeyRotationService {
|
||||
}
|
||||
|
||||
// Set master key again in case it was lost (could be lost on refresh)
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const oldUserKey = await firstValueFrom(this.cryptoService.userKey$(userId));
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, user.id);
|
||||
const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey);
|
||||
|
||||
if (!newUserKey || !newEncUserKey) {
|
||||
@@ -90,61 +87,86 @@ export class UserKeyRotationService {
|
||||
const masterPasswordHash = await this.cryptoService.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.cryptoService.getUserKeyWithLegacySupport(user.id);
|
||||
|
||||
// Add re-encrypted data
|
||||
request.privateKey = await this.encryptPrivateKey(newUserKey);
|
||||
request.ciphers = await this.encryptCiphers(newUserKey);
|
||||
request.folders = await this.encryptFolders(newUserKey);
|
||||
request.sends = await this.sendService.getRotatedKeys(newUserKey);
|
||||
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
|
||||
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
|
||||
request.webauthnKeys = await this.webauthnLoginAdminService.rotateWebAuthnKeys(
|
||||
oldUserKey,
|
||||
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;
|
||||
}
|
||||
|
||||
await this.apiService.postUserKeyUpdate(request);
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.deviceTrustService.rotateDevicesTrust(
|
||||
activeAccount.id,
|
||||
newUserKey,
|
||||
masterPasswordHash,
|
||||
);
|
||||
// TODO PM-2199: Add device trust rotation support to the user key rotation endpoint
|
||||
await this.deviceTrustService.rotateDevicesTrust(user.id, newUserKey, masterPasswordHash);
|
||||
}
|
||||
|
||||
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
|
||||
const privateKey = await this.cryptoService.getPrivateKey();
|
||||
private async encryptPrivateKey(
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<EncryptedString | null> {
|
||||
const privateKey = await firstValueFrom(
|
||||
this.cryptoService.userPrivateKeyWithLegacySupport$(userId),
|
||||
);
|
||||
if (!privateKey) {
|
||||
return;
|
||||
throw new Error("No private key found for user key rotation");
|
||||
}
|
||||
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
|
||||
}
|
||||
|
||||
private async encryptCiphers(newUserKey: UserKey): Promise<CipherWithIdRequest[]> {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
if (!ciphers) {
|
||||
// Must return an empty array for backwards compatibility
|
||||
return [];
|
||||
}
|
||||
return await Promise.all(
|
||||
ciphers.map(async (cipher) => {
|
||||
const encryptedCipher = await this.cipherService.encrypt(cipher, newUserKey);
|
||||
return new CipherWithIdRequest(encryptedCipher);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async encryptFolders(newUserKey: UserKey): Promise<FolderWithIdRequest[]> {
|
||||
const folders = await firstValueFrom(this.folderService.folderViews$);
|
||||
if (!folders) {
|
||||
// Must return an empty array for backwards compatibility
|
||||
return [];
|
||||
}
|
||||
return await Promise.all(
|
||||
folders.map(async (folder) => {
|
||||
const encryptedFolder = await this.folderService.encrypt(folder, newUserKey);
|
||||
return new FolderWithIdRequest(encryptedFolder);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user