1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 11:13:46 +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

@@ -20,7 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { DialogService } from "@bitwarden/components";
import { AccountRecoveryService } from "../services/account-recovery/account-recovery.service";
import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service";
@Component({
selector: "app-reset-password",
@@ -43,7 +43,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(
private accountRecoveryService: AccountRecoveryService,
private resetPasswordService: OrganizationUserResetPasswordService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
@@ -144,7 +144,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
}
try {
this.formPromise = this.accountRecoveryService.resetMasterPassword(
this.formPromise = this.resetPasswordService.resetMasterPassword(
this.newPassword,
this.email,
this.id,

View File

@@ -19,10 +19,10 @@ import {
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { AccountRecoveryService } from "./account-recovery.service";
import { OrganizationUserResetPasswordService } from "./organization-user-reset-password.service";
describe("AccountRecoveryService", () => {
let sut: AccountRecoveryService;
describe("OrganizationUserResetPasswordService", () => {
let sut: OrganizationUserResetPasswordService;
let cryptoService: MockProxy<CryptoService>;
let encryptService: MockProxy<EncryptService>;
@@ -39,7 +39,7 @@ describe("AccountRecoveryService", () => {
organizationApiService = mock<OrganizationApiService>();
i18nService = mock<I18nService>();
sut = new AccountRecoveryService(
sut = new OrganizationUserResetPasswordService(
cryptoService,
encryptService,
organizationService,
@@ -161,7 +161,7 @@ describe("AccountRecoveryService", () => {
});
});
describe("rotate", () => {
describe("getRotatedKeys", () => {
beforeEach(() => {
organizationService.getAll.mockResolvedValue([
createOrganization("1", "org1"),
@@ -178,29 +178,12 @@ describe("AccountRecoveryService", () => {
);
});
it("should rotate all of the user's recovery key", async () => {
organizationApiService.getKeys.mockResolvedValue(
new OrganizationKeysResponse({
privateKey: "test-private-key",
publicKey: "test-public-key",
}),
);
cryptoService.rsaEncrypt.mockResolvedValue(
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
);
organizationService.getAll.mockResolvedValue([
createOrganization("1", "org1"),
createOrganization("2", "org2"),
]);
await sut.rotate(
it("should return all re-encrypted account recovery keys", async () => {
const result = await sut.getRotatedKeys(
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
"test-master-password-hash",
);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalledTimes(2);
expect(result).toHaveLength(2);
});
});
});

View File

@@ -4,8 +4,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import {
OrganizationUserResetPasswordEnrollmentRequest,
OrganizationUserResetPasswordRequest,
OrganizationUserResetPasswordWithIdRequest,
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@@ -21,7 +21,7 @@ import {
@Injectable({
providedIn: "root",
})
export class AccountRecoveryService {
export class OrganizationUserResetPasswordService {
constructor(
private cryptoService: CryptoService,
private encryptService: EncryptService,
@@ -120,36 +120,60 @@ export class AccountRecoveryService {
}
/**
* Rotates the user's recovery key for all enrolled organizations.
* Returns existing account recovery keys re-encrypted with the new user key.
* @param newUserKey the new user key
* @param masterPasswordHash the user's master password hash (required for user verification)
* @throws Error if new user key is null
*/
async rotate(newUserKey: UserKey, masterPasswordHash: string): Promise<void> {
async getRotatedKeys(
newUserKey: UserKey,
): Promise<OrganizationUserResetPasswordWithIdRequest[] | null> {
if (newUserKey == null) {
throw new Error("New user key is required for rotation.");
}
const allOrgs = await this.organizationService.getAll();
if (!allOrgs) {
return;
}
const requests: OrganizationUserResetPasswordWithIdRequest[] = [];
for (const org of allOrgs) {
// If not already enrolled, skip
if (!org.resetPasswordEnrolled) {
continue;
}
try {
// Re-enroll - encrypt user key with organization public key
const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey);
// Re-enroll - encrypt user key with organization public key
const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey);
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = encryptedKey;
request.masterPasswordHash = masterPasswordHash;
// Create/Execute request
const request = new OrganizationUserResetPasswordWithIdRequest();
request.organizationId = org.id;
request.resetPasswordKey = encryptedKey;
request.masterPasswordHash = "ignored";
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
org.id,
org.userId,
request,
);
} catch (e) {
// If enrollment fails, continue to next org
}
requests.push(request);
}
return requests;
}
/**
* @deprecated Nov 6, 2023: Use new Key Rotation Service for posting rotated data.
*/
async postLegacyRotation(
userId: string,
requests: OrganizationUserResetPasswordWithIdRequest[],
): Promise<void> {
if (requests == null) {
return;
}
for (const request of requests) {
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
request.organizationId,
userId,
request,
);
}
}
}

View File

@@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { AccountRecoveryService } from "../members/services/account-recovery/account-recovery.service";
import { OrganizationUserResetPasswordService } from "../members/services/organization-user-reset-password/organization-user-reset-password.service";
interface EnrollMasterPasswordResetData {
organization: Organization;
@@ -33,7 +33,7 @@ export class EnrollMasterPasswordReset {
constructor(
private dialogRef: DialogRef,
@Inject(DIALOG_DATA) protected data: EnrollMasterPasswordResetData,
private accountRecoveryService: AccountRecoveryService,
private resetPasswordService: OrganizationUserResetPasswordService,
private userVerificationService: UserVerificationService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
@@ -53,7 +53,7 @@ export class EnrollMasterPasswordReset {
)
.then(async (request) => {
// Create request and execute enrollment
request.resetPasswordKey = await this.accountRecoveryService.buildRecoveryKey(
request.resetPasswordKey = await this.resetPasswordService.buildRecoveryKey(
this.organization.id,
);
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(