1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +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:
Jake Fink
2023-12-22 10:31:24 -05:00
committed by GitHub
parent e079fb4ab6
commit a62f8cd652
25 changed files with 569 additions and 608 deletions

View File

@@ -99,7 +99,6 @@ import { RegisterRequest } from "../models/request/register.request";
import { StorageRequest } from "../models/request/storage.request";
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
import { UpdateKeyRequest } from "../models/request/update-key.request";
import { VerifyDeleteRecoverRequest } from "../models/request/verify-delete-recover.request";
import { VerifyEmailRequest } from "../models/request/verify-email.request";
import { BreachAccountResponse } from "../models/response/breach-account.response";
@@ -176,7 +175,6 @@ export abstract class ApiService {
postAccountStorage: (request: StorageRequest) => Promise<PaymentResponse>;
postAccountPayment: (request: PaymentRequest) => Promise<void>;
postAccountLicense: (data: FormData) => Promise<any>;
postAccountKey: (request: UpdateKeyRequest) => Promise<any>;
postAccountKeys: (request: KeysRequest) => Promise<any>;
postAccountVerifyEmail: () => Promise<any>;
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;

View File

@@ -3,3 +3,7 @@ import { SecretVerificationRequest } from "../../../../auth/models/request/secre
export class OrganizationUserResetPasswordEnrollmentRequest extends SecretVerificationRequest {
resetPasswordKey: string;
}
export class OrganizationUserResetPasswordWithIdRequest extends OrganizationUserResetPasswordEnrollmentRequest {
organizationId: string;
}

View File

@@ -10,6 +10,7 @@ export enum FeatureFlag {
FlexibleCollections = "flexible-collections",
FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional
BulkCollectionAccess = "bulk-collection-access",
KeyRotationImprovements = "key-rotation-improvements",
}
// Replace this with a type safe lookup of the feature flag values in PM-2282

View File

@@ -1,12 +0,0 @@
import { SendWithIdRequest } from "../../tools/send/models/request/send-with-id.request";
import { CipherWithIdRequest } from "../../vault/models/request/cipher-with-id.request";
import { FolderWithIdRequest } from "../../vault/models/request/folder-with-id.request";
export class UpdateKeyRequest {
ciphers: CipherWithIdRequest[] = [];
folders: FolderWithIdRequest[] = [];
sends: SendWithIdRequest[] = [];
masterPasswordHash: string;
privateKey: string;
key: string;
}

View File

@@ -105,7 +105,6 @@ import { RegisterRequest } from "../models/request/register.request";
import { StorageRequest } from "../models/request/storage.request";
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
import { UpdateKeyRequest } from "../models/request/update-key.request";
import { VerifyDeleteRecoverRequest } from "../models/request/verify-delete-recover.request";
import { VerifyEmailRequest } from "../models/request/verify-email.request";
import { BreachAccountResponse } from "../models/response/breach-account.response";
@@ -412,10 +411,6 @@ export class ApiService implements ApiServiceAbstraction {
return this.send("POST", "/accounts/keys", request, true, false);
}
postAccountKey(request: UpdateKeyRequest): Promise<any> {
return this.send("POST", "/accounts/key", request, true, false);
}
postAccountVerifyEmail(): Promise<any> {
return this.send("POST", "/accounts/verify-email", null, true, false);
}

View File

@@ -1,9 +1,10 @@
import { Observable } from "rxjs";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { SymmetricCryptoKey, UserKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { SendData } from "../models/data/send.data";
import { Send } from "../models/domain/send";
import { SendWithIdRequest } from "../models/request/send-with-id.request";
import { SendView } from "../models/view/send.view";
export abstract class SendService {
@@ -17,6 +18,13 @@ export abstract class SendService {
key?: SymmetricCryptoKey,
) => Promise<[Send, EncArrayBuffer]>;
get: (id: string) => Send;
/**
* Provides re-encrypted user sends for the key rotation process
* @param newUserKey The new user key to use for re-encryption
* @throws Error if the new user key is null or undefined
* @returns A list of user sends that have been re-encrypted with the new user key
*/
getRotatedKeys: (newUserKey: UserKey) => Promise<SendWithIdRequest[]>;
/**
* @deprecated Do not call this, use the sends$ observable collection
*/

View File

@@ -7,6 +7,7 @@ import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey, UserKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../platform/services/container.service";
import { SendData } from "../models/data/send.data";
import { Send } from "../models/domain/send";
@@ -92,6 +93,37 @@ describe("SendService", () => {
expect(stateService.getDecryptedSends).toHaveBeenCalledTimes(1);
});
describe("getRotatedKeys", () => {
let encryptedKey: EncString;
beforeEach(() => {
cryptoService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptedKey = new EncString("Re-encrypted Send Key");
cryptoService.encrypt.mockResolvedValue(encryptedKey);
});
it("returns re-encrypted user sends", async () => {
const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
const result = await sendService.getRotatedKeys(newUserKey);
expect(result).toMatchObject([{ id: "1", key: "Re-encrypted Send Key" }]);
});
it("returns null if there are no sends", async () => {
sendService.replace(null);
const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
const result = await sendService.getRotatedKeys(newUserKey);
expect(result).toEqual([]);
});
it("throws if the new user key is null", async () => {
await expect(sendService.getRotatedKeys(null)).rejects.toThrowError(
"New user key is required for rotation.",
);
});
});
// InternalSendService
it("upsert", async () => {

View File

@@ -7,12 +7,13 @@ import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { SymmetricCryptoKey, UserKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { SendType } from "../enums/send-type";
import { SendData } from "../models/data/send.data";
import { Send } from "../models/domain/send";
import { SendFile } from "../models/domain/send-file";
import { SendText } from "../models/domain/send-text";
import { SendWithIdRequest } from "../models/request/send-with-id.request";
import { SendView } from "../models/view/send.view";
import { SEND_KDF_ITERATIONS } from "../send-kdf";
@@ -212,6 +213,22 @@ export class SendService implements InternalSendServiceAbstraction {
await this.stateService.setEncryptedSends(sends);
}
async getRotatedKeys(newUserKey: UserKey): Promise<SendWithIdRequest[]> {
if (newUserKey == null) {
throw new Error("New user key is required for rotation.");
}
const requests = await Promise.all(
this._sends.value.map(async (send) => {
const sendKey = await this.cryptoService.decryptToBytes(send.key);
send.key = await this.cryptoService.encrypt(sendKey, newUserKey);
return new SendWithIdRequest(send);
}),
);
// separate return for easier debugging
return requests;
}
private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();