1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 10: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

@@ -0,0 +1,215 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
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 { EncryptionType } from "@bitwarden/common/platform/enums";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import {
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
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 { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
import { StateService } from "../../core";
import { EmergencyAccessService } from "../emergency-access";
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
import { UserKeyRotationService } from "./user-key-rotation.service";
describe("KeyRotationService", () => {
let keyRotationService: UserKeyRotationService;
let mockApiService: MockProxy<UserKeyRotationApiService>;
let mockCipherService: MockProxy<CipherService>;
let mockFolderService: MockProxy<FolderService>;
let mockSendService: MockProxy<SendService>;
let mockEmergencyAccessService: MockProxy<EmergencyAccessService>;
let mockResetPasswordService: MockProxy<OrganizationUserResetPasswordService>;
let mockDeviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let mockCryptoService: MockProxy<CryptoService>;
let mockEncryptService: MockProxy<EncryptService>;
let mockStateService: MockProxy<StateService>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
beforeAll(() => {
mockApiService = mock<UserKeyRotationApiService>();
mockCipherService = mock<CipherService>();
mockFolderService = mock<FolderService>();
mockSendService = mock<SendService>();
mockEmergencyAccessService = mock<EmergencyAccessService>();
mockResetPasswordService = mock<OrganizationUserResetPasswordService>();
mockDeviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
mockCryptoService = mock<CryptoService>();
mockEncryptService = mock<EncryptService>();
mockStateService = mock<StateService>();
mockConfigService = mock<ConfigServiceAbstraction>();
keyRotationService = new UserKeyRotationService(
mockApiService,
mockCipherService,
mockFolderService,
mockSendService,
mockEmergencyAccessService,
mockResetPasswordService,
mockDeviceTrustCryptoService,
mockCryptoService,
mockEncryptService,
mockStateService,
mockConfigService,
);
});
beforeEach(() => {
jest.clearAllMocks();
});
it("instantiates", () => {
expect(keyRotationService).not.toBeFalsy();
});
describe("rotateUserKeyAndEncryptedData", () => {
let folderViews: BehaviorSubject<FolderView[]>;
let sends: BehaviorSubject<Send[]>;
beforeAll(() => {
mockCryptoService.makeMasterKey.mockResolvedValue("mockMasterKey" as any);
mockCryptoService.makeUserKey.mockResolvedValue([
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
{
encryptedString: "mockEncryptedUserKey",
} as any,
]);
mockCryptoService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
mockConfigService.getFeatureFlag.mockResolvedValue(true);
// Mock private key
mockCryptoService.getPrivateKey.mockResolvedValue("MockPrivateKey" as any);
// Mock ciphers
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
mockCipherService.getAllDecrypted.mockResolvedValue(mockCiphers);
// Mock folders
const mockFolders = [createMockFolder("1", "Folder 1"), createMockFolder("2", "Folder 2")];
folderViews = new BehaviorSubject<FolderView[]>(mockFolders);
mockFolderService.folderViews$ = folderViews;
// Mock sends
const mockSends = [createMockSend("1", "Send 1"), createMockSend("2", "Send 2")];
sends = new BehaviorSubject<Send[]>(mockSends);
mockSendService.sends$ = sends;
// Mock encryption methods
mockEncryptService.encrypt.mockResolvedValue({
encryptedString: "mockEncryptedData",
} as any);
mockFolderService.encrypt.mockImplementation((folder, userKey) => {
const encryptedFolder = new Folder();
encryptedFolder.id = folder.id;
encryptedFolder.name = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"Encrypted: " + folder.name,
);
return Promise.resolve(encryptedFolder);
});
mockCipherService.encrypt.mockImplementation((cipher, userKey) => {
const encryptedCipher = new Cipher();
encryptedCipher.id = cipher.id;
encryptedCipher.name = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"Encrypted: " + cipher.name,
);
return Promise.resolve(encryptedCipher);
});
});
it("rotates the user key and encrypted data", async () => {
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword");
expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled();
const arg = mockApiService.postUserKeyUpdate.mock.calls[0][0];
expect(arg.ciphers.length).toBe(2);
expect(arg.folders.length).toBe(2);
});
it("throws if master password provided is falsey", async () => {
await expect(keyRotationService.rotateUserKeyAndEncryptedData("")).rejects.toThrow();
});
it("throws if master key creation fails", async () => {
mockCryptoService.makeMasterKey.mockResolvedValueOnce(null);
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"),
).rejects.toThrow();
});
it("throws if user key creation fails", async () => {
mockCryptoService.makeUserKey.mockResolvedValueOnce([null, null]);
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"),
).rejects.toThrow();
});
it("saves the master key in state after creation", async () => {
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword");
expect(mockCryptoService.setMasterKey).toHaveBeenCalledWith("mockMasterKey" as any);
});
it("uses legacy rotation if feature flag is off", async () => {
mockConfigService.getFeatureFlag.mockResolvedValueOnce(false);
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword");
expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled();
expect(mockEmergencyAccessService.postLegacyRotation).toHaveBeenCalled();
expect(mockResetPasswordService.postLegacyRotation).toHaveBeenCalled();
});
it("throws if server rotation fails", async () => {
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"),
).rejects.toThrow();
});
});
});
function createMockFolder(id: string, name: string): FolderView {
const folder = new FolderView();
folder.id = id;
folder.name = name;
return folder;
}
function createMockCipher(id: string, name: string): CipherView {
const cipher = new CipherView();
cipher.id = id;
cipher.name = name;
cipher.type = CipherType.Login;
return cipher;
}
function createMockSend(id: string, name: string): Send {
const send = new Send();
send.id = id;
send.name = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, name);
return send;
}