mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-3797] Client changes to use new key rotation process (#6881)
## Type of change <!-- (mark with an `X`) --> ``` - [ ] Bug fix - [ ] New feature development - [x] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Build/deploy pipeline (DevOps) - [ ] Other ``` ## Objective <!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding--> Final Client changes for Key Rotation Improvements. - Introduces a new `KeyRotationService` that is responsible for owning rotation process. - Moves `Send` re-encryption to the `SendService` (`KeyRotationService` shouldn't have knowledge about how domains are encrypted). - Moves `EmergencyAccess` re-encryption to the `EmergencyAccessService`. - Renames `AccountRecoveryService` to `OrganizationUserResetPasswordService` after feedback from Admin Console ## Code changes <!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes--> <!--Also refer to any related changes or PRs in other repositories--> Auth - **emergency-access-update.request.ts:** New request model for domain updates that includes Id - **emergency-access.service.ts:** Moved `EmergencyAccess` re-encryption to the `EmergencyAccessService`. Add deprecated method for legacy key rotations if feature flag is off - **key-rotation.service/api/spec/module:** New key rotation service for owning the rotation process. Added api service, module, and spec file. - **update-key.request.ts:** Moved to Auth ownership. Also added new properties for including other domains. - **migrate-legacy-encryption.component.ts:** Use new key rotation service instead of old component specific service. Delete old service. - **change-password.component.ts:** Use new key rotation service. - **settings.module.ts:** Import key rotation module. Admin Console - **organization-user-reset-password.service.ts/spec:** Responsible for re-encryption of reset password keys during key rotation. Added tests. - **organization-user-reset-password-enrollment.request.ts:** New request model for key rotations - **reset-password.component.ts:** Update `AccountRecoveryService` to `OrganizationUserResetPasswordService` - **enroll-master-password-reset.component.ts:** Update `AccountRecoveryService` to `OrganizationUserResetPasswordService` Tools - **send.service/spec.ts:** Responsible only for re-encryption of sends during key rotation. Added tests. Other - **api.service.ts:** Move `postAccountKey` to `KeyRotationApiService` - **feature-flag.enum.ts:** add new feature flag ## Screenshots <!--Required for any UI changes. Delete if not applicable--> ## Before you submit - Please add **unit tests** where it makes sense to do so (encouraged but not required) - If this change requires a **documentation update** - notify the documentation team - If this change has particular **deployment requirements** - notify the DevOps team - Ensure that all UI additions follow [WCAG AA requirements](https://contributing.bitwarden.com/contributing/accessibility/)
This commit is contained in:
143
apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts
Normal file
143
apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.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 { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.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 { 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 apiService: UserKeyRotationApiService,
|
||||
private cipherService: CipherService,
|
||||
private folderService: FolderService,
|
||||
private sendService: SendService,
|
||||
private emergencyAccessService: EmergencyAccessService,
|
||||
private resetPasswordService: OrganizationUserResetPasswordService,
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
private cryptoService: CryptoService,
|
||||
private encryptService: EncryptService,
|
||||
private stateService: StateService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
if (!masterPassword) {
|
||||
throw new Error("Invalid master password");
|
||||
}
|
||||
|
||||
// Create master key to validate the master password
|
||||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
await this.stateService.getEmail(),
|
||||
await this.stateService.getKdfType(),
|
||||
await this.stateService.getKdfConfig(),
|
||||
);
|
||||
|
||||
if (!masterKey) {
|
||||
throw new Error("Master key could not be created");
|
||||
}
|
||||
|
||||
// Set master key again in case it was lost (could be lost on refresh)
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey);
|
||||
|
||||
if (!newUserKey || !newEncUserKey) {
|
||||
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.cryptoService.hashMasterKey(masterPassword, masterKey);
|
||||
request.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
// 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);
|
||||
|
||||
if (await this.configService.getFeatureFlag<boolean>(FeatureFlag.KeyRotationImprovements)) {
|
||||
await this.apiService.postUserKeyUpdate(request);
|
||||
} else {
|
||||
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
||||
}
|
||||
|
||||
await this.deviceTrustCryptoService.rotateDevicesTrust(newUserKey, masterPasswordHash);
|
||||
}
|
||||
|
||||
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
|
||||
const privateKey = await this.cryptoService.getPrivateKey();
|
||||
if (!privateKey) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async rotateUserKeyAndEncryptedDataLegacy(request: UpdateKeyRequest): Promise<void> {
|
||||
// Update keys, ciphers, folders, and sends
|
||||
await this.apiService.postUserKeyUpdate(request);
|
||||
|
||||
// Update emergency access keys
|
||||
await this.emergencyAccessService.postLegacyRotation(request.emergencyAccessKeys);
|
||||
|
||||
// Update account recovery keys
|
||||
const userId = await this.stateService.getUserId();
|
||||
await this.resetPasswordService.postLegacyRotation(userId, request.resetPasswordKeys);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user