1
0
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:
Jake Fink
2024-06-20 11:36:24 -04:00
committed by GitHub
parent eadb1fa4ef
commit b306554675
23 changed files with 516 additions and 196 deletions

View File

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