mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +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:
@@ -20,7 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { DialogService } from "@bitwarden/components";
|
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({
|
@Component({
|
||||||
selector: "app-reset-password",
|
selector: "app-reset-password",
|
||||||
@@ -43,7 +43,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
|
|||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accountRecoveryService: AccountRecoveryService,
|
private resetPasswordService: OrganizationUserResetPasswordService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
@@ -144,7 +144,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.accountRecoveryService.resetMasterPassword(
|
this.formPromise = this.resetPasswordService.resetMasterPassword(
|
||||||
this.newPassword,
|
this.newPassword,
|
||||||
this.email,
|
this.email,
|
||||||
this.id,
|
this.id,
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import {
|
|||||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
|
|
||||||
import { AccountRecoveryService } from "./account-recovery.service";
|
import { OrganizationUserResetPasswordService } from "./organization-user-reset-password.service";
|
||||||
|
|
||||||
describe("AccountRecoveryService", () => {
|
describe("OrganizationUserResetPasswordService", () => {
|
||||||
let sut: AccountRecoveryService;
|
let sut: OrganizationUserResetPasswordService;
|
||||||
|
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
@@ -39,7 +39,7 @@ describe("AccountRecoveryService", () => {
|
|||||||
organizationApiService = mock<OrganizationApiService>();
|
organizationApiService = mock<OrganizationApiService>();
|
||||||
i18nService = mock<I18nService>();
|
i18nService = mock<I18nService>();
|
||||||
|
|
||||||
sut = new AccountRecoveryService(
|
sut = new OrganizationUserResetPasswordService(
|
||||||
cryptoService,
|
cryptoService,
|
||||||
encryptService,
|
encryptService,
|
||||||
organizationService,
|
organizationService,
|
||||||
@@ -161,7 +161,7 @@ describe("AccountRecoveryService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("rotate", () => {
|
describe("getRotatedKeys", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
organizationService.getAll.mockResolvedValue([
|
organizationService.getAll.mockResolvedValue([
|
||||||
createOrganization("1", "org1"),
|
createOrganization("1", "org1"),
|
||||||
@@ -178,29 +178,12 @@ describe("AccountRecoveryService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should rotate all of the user's recovery key", async () => {
|
it("should return all re-encrypted account recovery keys", async () => {
|
||||||
organizationApiService.getKeys.mockResolvedValue(
|
const result = await sut.getRotatedKeys(
|
||||||
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(
|
|
||||||
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||||
"test-master-password-hash",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(result).toHaveLength(2);
|
||||||
organizationUserService.putOrganizationUserResetPasswordEnrollment,
|
|
||||||
).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -4,8 +4,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
|
|||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
import {
|
import {
|
||||||
OrganizationUserResetPasswordEnrollmentRequest,
|
|
||||||
OrganizationUserResetPasswordRequest,
|
OrganizationUserResetPasswordRequest,
|
||||||
|
OrganizationUserResetPasswordWithIdRequest,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
||||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class AccountRecoveryService {
|
export class OrganizationUserResetPasswordService {
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private encryptService: EncryptService,
|
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 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();
|
const allOrgs = await this.organizationService.getAll();
|
||||||
|
|
||||||
|
if (!allOrgs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requests: OrganizationUserResetPasswordWithIdRequest[] = [];
|
||||||
for (const org of allOrgs) {
|
for (const org of allOrgs) {
|
||||||
// If not already enrolled, skip
|
// If not already enrolled, skip
|
||||||
if (!org.resetPasswordEnrolled) {
|
if (!org.resetPasswordEnrolled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Re-enroll - encrypt user key with organization public key
|
||||||
// Re-enroll - encrypt user key with organization public key
|
const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey);
|
||||||
const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey);
|
|
||||||
|
|
||||||
// Create/Execute request
|
// Create/Execute request
|
||||||
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
const request = new OrganizationUserResetPasswordWithIdRequest();
|
||||||
request.resetPasswordKey = encryptedKey;
|
request.organizationId = org.id;
|
||||||
request.masterPasswordHash = masterPasswordHash;
|
request.resetPasswordKey = encryptedKey;
|
||||||
|
request.masterPasswordHash = "ignored";
|
||||||
|
|
||||||
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
requests.push(request);
|
||||||
org.id,
|
}
|
||||||
org.userId,
|
return requests;
|
||||||
request,
|
}
|
||||||
);
|
|
||||||
} catch (e) {
|
/**
|
||||||
// If enrollment fails, continue to next org
|
* @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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { DialogService } from "@bitwarden/components";
|
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 {
|
interface EnrollMasterPasswordResetData {
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
@@ -33,7 +33,7 @@ export class EnrollMasterPasswordReset {
|
|||||||
constructor(
|
constructor(
|
||||||
private dialogRef: DialogRef,
|
private dialogRef: DialogRef,
|
||||||
@Inject(DIALOG_DATA) protected data: EnrollMasterPasswordResetData,
|
@Inject(DIALOG_DATA) protected data: EnrollMasterPasswordResetData,
|
||||||
private accountRecoveryService: AccountRecoveryService,
|
private resetPasswordService: OrganizationUserResetPasswordService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
@@ -53,7 +53,7 @@ export class EnrollMasterPasswordReset {
|
|||||||
)
|
)
|
||||||
.then(async (request) => {
|
.then(async (request) => {
|
||||||
// Create request and execute enrollment
|
// Create request and execute enrollment
|
||||||
request.resetPasswordKey = await this.accountRecoveryService.buildRecoveryKey(
|
request.resetPasswordKey = await this.resetPasswordService.buildRecoveryKey(
|
||||||
this.organization.id,
|
this.organization.id,
|
||||||
);
|
);
|
||||||
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ export class EmergencyAccessUpdateRequest {
|
|||||||
waitTimeDays: number;
|
waitTimeDays: number;
|
||||||
keyEncrypted?: string;
|
keyEncrypted?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class EmergencyAccessWithIdRequest extends EmergencyAccessUpdateRequest {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
|||||||
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
|
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
|
||||||
import { EmergencyAccessType } from "../enums/emergency-access-type";
|
import { EmergencyAccessType } from "../enums/emergency-access-type";
|
||||||
import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request";
|
import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request";
|
||||||
import { EmergencyAccessUpdateRequest } from "../request/emergency-access-update.request";
|
|
||||||
import {
|
import {
|
||||||
EmergencyAccessGranteeDetailsResponse,
|
EmergencyAccessGranteeDetailsResponse,
|
||||||
EmergencyAccessTakeoverResponse,
|
EmergencyAccessTakeoverResponse,
|
||||||
@@ -210,7 +209,7 @@ describe("EmergencyAccessService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("rotate", () => {
|
describe("getRotatedKeys", () => {
|
||||||
let mockUserKey: UserKey;
|
let mockUserKey: UserKey;
|
||||||
const allowedStatuses = [
|
const allowedStatuses = [
|
||||||
EmergencyAccessStatusType.Confirmed,
|
EmergencyAccessStatusType.Confirmed,
|
||||||
@@ -245,26 +244,10 @@ describe("EmergencyAccessService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Only updates emergency accesses with allowed statuses", async () => {
|
it("Only returns emergency accesses with allowed statuses", async () => {
|
||||||
await emergencyAccessService.rotate(mockUserKey);
|
const result = await emergencyAccessService.getRotatedKeys(mockUserKey);
|
||||||
|
|
||||||
let expectedCallCount = 0;
|
expect(result).toHaveLength(allowedStatuses.length);
|
||||||
|
|
||||||
mockEmergencyAccess.data.forEach((emergencyAccess) => {
|
|
||||||
if (allowedStatuses.includes(emergencyAccess.status)) {
|
|
||||||
expect(emergencyAccessApiService.putEmergencyAccess).toHaveBeenCalledWith(
|
|
||||||
emergencyAccess.id,
|
|
||||||
expect.any(EmergencyAccessUpdateRequest),
|
|
||||||
);
|
|
||||||
expectedCallCount++;
|
|
||||||
} else {
|
|
||||||
expect(emergencyAccessApiService.putEmergencyAccess).not.toHaveBeenCalledWith(
|
|
||||||
emergencyAccess.id,
|
|
||||||
expect.any(EmergencyAccessUpdateRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(emergencyAccessApiService.putEmergencyAccess).toHaveBeenCalledTimes(expectedCallCount);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import { EmergencyAccessAcceptRequest } from "../request/emergency-access-accept
|
|||||||
import { EmergencyAccessConfirmRequest } from "../request/emergency-access-confirm.request";
|
import { EmergencyAccessConfirmRequest } from "../request/emergency-access-confirm.request";
|
||||||
import { EmergencyAccessInviteRequest } from "../request/emergency-access-invite.request";
|
import { EmergencyAccessInviteRequest } from "../request/emergency-access-invite.request";
|
||||||
import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request";
|
import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request";
|
||||||
import { EmergencyAccessUpdateRequest } from "../request/emergency-access-update.request";
|
import {
|
||||||
|
EmergencyAccessUpdateRequest,
|
||||||
|
EmergencyAccessWithIdRequest,
|
||||||
|
} from "../request/emergency-access-update.request";
|
||||||
|
|
||||||
import { EmergencyAccessApiService } from "./emergency-access-api.service";
|
import { EmergencyAccessApiService } from "./emergency-access-api.service";
|
||||||
|
|
||||||
@@ -252,13 +255,19 @@ export class EmergencyAccessService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotates the user key for all existing emergency access.
|
* Returns existing emergency access keys re-encrypted with new user key.
|
||||||
* Intended for grantor.
|
* Intended for grantor.
|
||||||
* @param newUserKey the new user key
|
* @param newUserKey the new user key
|
||||||
*/
|
*/
|
||||||
async rotate(newUserKey: UserKey): Promise<void> {
|
async getRotatedKeys(newUserKey: UserKey): Promise<EmergencyAccessWithIdRequest[]> {
|
||||||
|
const requests: EmergencyAccessWithIdRequest[] = [];
|
||||||
const existingEmergencyAccess =
|
const existingEmergencyAccess =
|
||||||
await this.emergencyAccessApiService.getEmergencyAccessTrusted();
|
await this.emergencyAccessApiService.getEmergencyAccessTrusted();
|
||||||
|
|
||||||
|
if (!existingEmergencyAccess || existingEmergencyAccess.data.length === 0) {
|
||||||
|
return requests;
|
||||||
|
}
|
||||||
|
|
||||||
// Any Invited or Accepted requests won't have the key yet, so we don't need to update them
|
// Any Invited or Accepted requests won't have the key yet, so we don't need to update them
|
||||||
const allowedStatuses = new Set([
|
const allowedStatuses = new Set([
|
||||||
EmergencyAccessStatusType.Confirmed,
|
EmergencyAccessStatusType.Confirmed,
|
||||||
@@ -277,16 +286,29 @@ export class EmergencyAccessService {
|
|||||||
// Encrypt new user key with public key
|
// Encrypt new user key with public key
|
||||||
const encryptedKey = await this.encryptKey(newUserKey, publicKey);
|
const encryptedKey = await this.encryptKey(newUserKey, publicKey);
|
||||||
|
|
||||||
const updateRequest = new EmergencyAccessUpdateRequest();
|
const updateRequest = new EmergencyAccessWithIdRequest();
|
||||||
|
updateRequest.id = details.id;
|
||||||
updateRequest.type = details.type;
|
updateRequest.type = details.type;
|
||||||
updateRequest.waitTimeDays = details.waitTimeDays;
|
updateRequest.waitTimeDays = details.waitTimeDays;
|
||||||
updateRequest.keyEncrypted = encryptedKey;
|
updateRequest.keyEncrypted = encryptedKey;
|
||||||
|
requests.push(updateRequest);
|
||||||
await this.emergencyAccessApiService.putEmergencyAccess(details.id, updateRequest);
|
|
||||||
}
|
}
|
||||||
|
return requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
||||||
return (await this.cryptoService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
|
return (await this.cryptoService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Nov 6, 2023: Use new Key Rotation Service for posting rotated data.
|
||||||
|
*/
|
||||||
|
async postLegacyRotation(requests: EmergencyAccessWithIdRequest[]): Promise<void> {
|
||||||
|
if (requests == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const request of requests) {
|
||||||
|
await this.emergencyAccessApiService.putEmergencyAccess(request.id, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
||||||
|
import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request";
|
||||||
|
import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request";
|
||||||
|
import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.request";
|
||||||
|
|
||||||
|
import { EmergencyAccessWithIdRequest } from "../../emergency-access/request/emergency-access-update.request";
|
||||||
|
|
||||||
|
export class UpdateKeyRequest {
|
||||||
|
masterPasswordHash: string;
|
||||||
|
key: string;
|
||||||
|
privateKey: string;
|
||||||
|
ciphers: CipherWithIdRequest[] = [];
|
||||||
|
folders: FolderWithIdRequest[] = [];
|
||||||
|
sends: SendWithIdRequest[] = [];
|
||||||
|
emergencyAccessKeys: EmergencyAccessWithIdRequest[] = [];
|
||||||
|
resetPasswordKeys: OrganizationUserResetPasswordWithIdRequest[] = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
|
||||||
|
import { UpdateKeyRequest } from "./request/update-key.request";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserKeyRotationApiService {
|
||||||
|
readonly apiService = inject(ApiService);
|
||||||
|
|
||||||
|
postUserKeyUpdate(request: UpdateKeyRequest): Promise<any> {
|
||||||
|
return this.apiService.send("POST", "/accounts/key", request, true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
|
||||||
|
import { UserKeyRotationService } from "./user-key-rotation.service";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: [UserKeyRotationService, UserKeyRotationApiService],
|
||||||
|
})
|
||||||
|
export class UserKeyRotationModule {}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,16 +8,14 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
import { SharedModule } from "../../shared";
|
import { SharedModule } from "../../shared";
|
||||||
import { EmergencyAccessModule } from "../emergency-access";
|
import { UserKeyRotationModule } from "../key-rotation/user-key-rotation.module";
|
||||||
|
import { UserKeyRotationService } from "../key-rotation/user-key-rotation.service";
|
||||||
import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service";
|
|
||||||
|
|
||||||
// The master key was originally used to encrypt user data, before the user key was introduced.
|
// The master key was originally used to encrypt user data, before the user key was introduced.
|
||||||
// This component is used to migrate from the old encryption scheme to the new one.
|
// This component is used to migrate from the old encryption scheme to the new one.
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, EmergencyAccessModule],
|
imports: [SharedModule, UserKeyRotationModule],
|
||||||
providers: [MigrateFromLegacyEncryptionService],
|
|
||||||
templateUrl: "migrate-legacy-encryption.component.html",
|
templateUrl: "migrate-legacy-encryption.component.html",
|
||||||
})
|
})
|
||||||
export class MigrateFromLegacyEncryptionComponent {
|
export class MigrateFromLegacyEncryptionComponent {
|
||||||
@@ -26,9 +24,9 @@ export class MigrateFromLegacyEncryptionComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private keyRotationService: UserKeyRotationService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private migrationService: MigrateFromLegacyEncryptionService,
|
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
@@ -50,22 +48,7 @@ export class MigrateFromLegacyEncryptionComponent {
|
|||||||
const masterPassword = this.formGroup.value.masterPassword;
|
const masterPassword = this.formGroup.value.masterPassword;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create new user key
|
await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword);
|
||||||
const [newUserKey, masterKeyEncUserKey] =
|
|
||||||
await this.migrationService.createNewUserKey(masterPassword);
|
|
||||||
|
|
||||||
// Update admin recover keys
|
|
||||||
await this.migrationService.updateAllAdminRecoveryKeys(masterPassword, newUserKey);
|
|
||||||
|
|
||||||
// Update emergency access
|
|
||||||
await this.migrationService.updateEmergencyAccesses(newUserKey);
|
|
||||||
|
|
||||||
// Update keys, folders, ciphers, and sends
|
|
||||||
await this.migrationService.updateKeysAndEncryptedData(
|
|
||||||
masterPassword,
|
|
||||||
newUserKey,
|
|
||||||
masterKeyEncUserKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
|
||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
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 { EncryptionType, KdfType } from "@bitwarden/common/platform/enums";
|
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
||||||
import {
|
|
||||||
MasterKey,
|
|
||||||
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 { CsprngArray } from "@bitwarden/common/types/csprng";
|
|
||||||
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 { 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 { AccountRecoveryService } from "../../admin-console/organizations/members/services/account-recovery/account-recovery.service";
|
|
||||||
import { EmergencyAccessService } from "../emergency-access";
|
|
||||||
|
|
||||||
import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service";
|
|
||||||
|
|
||||||
describe("migrateFromLegacyEncryptionService", () => {
|
|
||||||
let migrateFromLegacyEncryptionService: MigrateFromLegacyEncryptionService;
|
|
||||||
|
|
||||||
const emergencyAccessService = mock<EmergencyAccessService>();
|
|
||||||
const accountRecoveryService = mock<AccountRecoveryService>();
|
|
||||||
const apiService = mock<ApiService>();
|
|
||||||
const encryptService = mock<EncryptService>();
|
|
||||||
const cryptoService = mock<CryptoService>();
|
|
||||||
const syncService = mock<SyncService>();
|
|
||||||
const cipherService = mock<CipherService>();
|
|
||||||
const folderService = mock<FolderService>();
|
|
||||||
const sendService = mock<SendService>();
|
|
||||||
const stateService = mock<StateService>();
|
|
||||||
let folderViews: BehaviorSubject<FolderView[]>;
|
|
||||||
let sends: BehaviorSubject<Send[]>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
migrateFromLegacyEncryptionService = new MigrateFromLegacyEncryptionService(
|
|
||||||
emergencyAccessService,
|
|
||||||
accountRecoveryService,
|
|
||||||
apiService,
|
|
||||||
cryptoService,
|
|
||||||
encryptService,
|
|
||||||
syncService,
|
|
||||||
cipherService,
|
|
||||||
folderService,
|
|
||||||
sendService,
|
|
||||||
stateService,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("instantiates", () => {
|
|
||||||
expect(migrateFromLegacyEncryptionService).not.toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("createNewUserKey", () => {
|
|
||||||
it("validates master password and legacy user", async () => {
|
|
||||||
const mockMasterPassword = "mockMasterPassword";
|
|
||||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
|
||||||
const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey;
|
|
||||||
stateService.getEmail.mockResolvedValue("mockEmail");
|
|
||||||
stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256);
|
|
||||||
stateService.getKdfConfig.mockResolvedValue({ iterations: 100000 });
|
|
||||||
cryptoService.makeMasterKey.mockResolvedValue(mockMasterKey);
|
|
||||||
cryptoService.isLegacyUser.mockResolvedValue(false);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
migrateFromLegacyEncryptionService.createNewUserKey(mockMasterPassword),
|
|
||||||
).rejects.toThrowError("Invalid master password or user may not be legacy");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("updateKeysAndEncryptedData", () => {
|
|
||||||
let mockMasterPassword: string;
|
|
||||||
let mockUserKey: UserKey;
|
|
||||||
let mockEncUserKey: EncString;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockMasterPassword = "mockMasterPassword";
|
|
||||||
|
|
||||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
|
||||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
|
||||||
mockEncUserKey = new EncString("mockEncUserKey");
|
|
||||||
|
|
||||||
const mockFolders = [createMockFolder("1", "Folder 1"), createMockFolder("2", "Folder 2")];
|
|
||||||
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
|
|
||||||
const mockSends = [createMockSend("1", "Send 1"), createMockSend("2", "Send 2")];
|
|
||||||
|
|
||||||
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64) as CsprngArray);
|
|
||||||
cryptoService.rsaEncrypt.mockResolvedValue(
|
|
||||||
new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "Encrypted"),
|
|
||||||
);
|
|
||||||
|
|
||||||
folderViews = new BehaviorSubject<FolderView[]>(mockFolders);
|
|
||||||
folderService.folderViews$ = folderViews;
|
|
||||||
|
|
||||||
cipherService.getAllDecrypted.mockResolvedValue(mockCiphers);
|
|
||||||
|
|
||||||
sends = new BehaviorSubject<Send[]>(mockSends);
|
|
||||||
sendService.sends$ = sends;
|
|
||||||
|
|
||||||
encryptService.encrypt.mockImplementation((plainValue, userKey) => {
|
|
||||||
return Promise.resolve(
|
|
||||||
new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "Encrypted: " + plainValue),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
folderService.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);
|
|
||||||
});
|
|
||||||
|
|
||||||
cipherService.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("derives the master key in case it hasn't been set", async () => {
|
|
||||||
await migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
|
||||||
mockMasterPassword,
|
|
||||||
mockUserKey,
|
|
||||||
mockEncUserKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(cryptoService.getOrDeriveMasterKey).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("syncs latest data", async () => {
|
|
||||||
await migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
|
||||||
mockMasterPassword,
|
|
||||||
mockUserKey,
|
|
||||||
mockEncUserKey,
|
|
||||||
);
|
|
||||||
expect(syncService.fullSync).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not post new account data if sync fails", async () => {
|
|
||||||
syncService.fullSync.mockRejectedValueOnce(new Error("sync failed"));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
|
||||||
mockMasterPassword,
|
|
||||||
mockUserKey,
|
|
||||||
mockEncUserKey,
|
|
||||||
),
|
|
||||||
).rejects.toThrowError("sync failed");
|
|
||||||
|
|
||||||
expect(apiService.postAccountKey).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not post new account data if data retrieval fails", async () => {
|
|
||||||
(migrateFromLegacyEncryptionService as any).encryptCiphers = async () => {
|
|
||||||
throw new Error("Ciphers failed to be retrieved");
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
|
||||||
mockMasterPassword,
|
|
||||||
mockUserKey,
|
|
||||||
mockEncUserKey,
|
|
||||||
),
|
|
||||||
).rejects.toThrowError("Ciphers failed to be retrieved");
|
|
||||||
|
|
||||||
expect(apiService.postAccountKey).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("updateEmergencyAccesses", () => {
|
|
||||||
let mockUserKey: UserKey;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
|
||||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Uses emergency access service to rotate", async () => {
|
|
||||||
await migrateFromLegacyEncryptionService.updateEmergencyAccesses(mockUserKey);
|
|
||||||
|
|
||||||
expect(emergencyAccessService.rotate).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { Injectable } from "@angular/core";
|
|
||||||
import { firstValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
|
|
||||||
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, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
||||||
import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
||||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
|
||||||
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 { 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 { AccountRecoveryService } from "../../admin-console/organizations/members/services/account-recovery/account-recovery.service";
|
|
||||||
import { EmergencyAccessService } from "../emergency-access";
|
|
||||||
|
|
||||||
// TODO: PM-3797 - This service should be expanded and used for user key rotations in change-password.component.ts
|
|
||||||
@Injectable()
|
|
||||||
export class MigrateFromLegacyEncryptionService {
|
|
||||||
constructor(
|
|
||||||
private emergencyAccessService: EmergencyAccessService,
|
|
||||||
private accountRecoveryService: AccountRecoveryService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private encryptService: EncryptService,
|
|
||||||
private syncService: SyncService,
|
|
||||||
private cipherService: CipherService,
|
|
||||||
private folderService: FolderService,
|
|
||||||
private sendService: SendService,
|
|
||||||
private stateService: StateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the master password and creates a new user key.
|
|
||||||
* @returns A new user key along with the encrypted version
|
|
||||||
*/
|
|
||||||
async createNewUserKey(masterPassword: string): Promise<[UserKey, EncString]> {
|
|
||||||
// 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("Invalid master password");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await this.cryptoService.isLegacyUser(masterKey))) {
|
|
||||||
throw new Error("Invalid master password or user may not be legacy");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set master key again in case it was lost (could be lost on refresh)
|
|
||||||
await this.cryptoService.setMasterKey(masterKey);
|
|
||||||
return await this.cryptoService.makeUserKey(masterKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the user key, master key hash, private key, folders, ciphers, and sends
|
|
||||||
* on the server.
|
|
||||||
* @param masterPassword The master password
|
|
||||||
* @param newUserKey The new user key
|
|
||||||
* @param newEncUserKey The new encrypted user key
|
|
||||||
*/
|
|
||||||
async updateKeysAndEncryptedData(
|
|
||||||
masterPassword: string,
|
|
||||||
newUserKey: UserKey,
|
|
||||||
newEncUserKey: EncString,
|
|
||||||
): Promise<void> {
|
|
||||||
// Create new request and add master key and hash
|
|
||||||
const request = new UpdateKeyRequest();
|
|
||||||
request.key = newEncUserKey.encryptedString;
|
|
||||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
|
||||||
masterPassword,
|
|
||||||
await this.cryptoService.getOrDeriveMasterKey(masterPassword),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sync before encrypting to make sure we have latest data
|
|
||||||
await this.syncService.fullSync(true);
|
|
||||||
|
|
||||||
request.privateKey = await this.encryptPrivateKey(newUserKey);
|
|
||||||
request.folders = await this.encryptFolders(newUserKey);
|
|
||||||
request.ciphers = await this.encryptCiphers(newUserKey);
|
|
||||||
request.sends = await this.encryptSends(newUserKey);
|
|
||||||
|
|
||||||
return this.apiService.postAccountKey(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets user's emergency access details from server and encrypts with new user key
|
|
||||||
* on the server.
|
|
||||||
* @param newUserKey The new user key
|
|
||||||
*/
|
|
||||||
updateEmergencyAccesses(newUserKey: UserKey) {
|
|
||||||
return this.emergencyAccessService.rotate(newUserKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Updates all admin recovery keys on the server with the new user key
|
|
||||||
* @param masterPassword The user's master password
|
|
||||||
* @param newUserKey The new user key
|
|
||||||
*/
|
|
||||||
async updateAllAdminRecoveryKeys(masterPassword: string, newUserKey: UserKey) {
|
|
||||||
const masterPasswordHash = await this.cryptoService.hashMasterKey(
|
|
||||||
masterPassword,
|
|
||||||
await this.cryptoService.getOrDeriveMasterKey(masterPassword),
|
|
||||||
);
|
|
||||||
await this.accountRecoveryService.rotate(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 encryptFolders(newUserKey: UserKey): Promise<FolderWithIdRequest[] | null> {
|
|
||||||
const folders = await firstValueFrom(this.folderService.folderViews$);
|
|
||||||
if (!folders) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return await Promise.all(
|
|
||||||
folders.map(async (folder) => {
|
|
||||||
const encryptedFolder = await this.folderService.encrypt(folder, newUserKey);
|
|
||||||
return new FolderWithIdRequest(encryptedFolder);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async encryptCiphers(newUserKey: UserKey): Promise<CipherWithIdRequest[] | null> {
|
|
||||||
const ciphers = await this.cipherService.getAllDecrypted();
|
|
||||||
if (!ciphers) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return await Promise.all(
|
|
||||||
ciphers.map(async (cipher) => {
|
|
||||||
const encryptedCipher = await this.cipherService.encrypt(cipher, newUserKey);
|
|
||||||
return new CipherWithIdRequest(encryptedCipher);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async encryptSends(newUserKey: UserKey): Promise<SendWithIdRequest[] | null> {
|
|
||||||
const sends = await firstValueFrom(this.sendService.sends$);
|
|
||||||
if (!sends) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return await Promise.all(
|
|
||||||
sends.map(async (send) => {
|
|
||||||
const sendKey = await this.encryptService.decryptToBytes(send.key, null);
|
|
||||||
send.key = (await this.encryptService.encrypt(sendKey, newUserKey)) ?? send.key;
|
|
||||||
return new SendWithIdRequest(send);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,28 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { firstValueFrom, Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
|
||||||
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 } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
|
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { MasterKey, UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { MasterKey, UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
|
||||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
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 { 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 { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { EmergencyAccessService } from "../emergency-access";
|
import { UserKeyRotationService } from "../key-rotation/user-key-rotation.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-change-password",
|
selector: "app-change-password",
|
||||||
@@ -58,20 +46,14 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
private auditService: AuditService,
|
private auditService: AuditService,
|
||||||
private folderService: FolderService,
|
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private emergencyAccessService: EmergencyAccessService,
|
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private sendService: SendService,
|
|
||||||
private organizationService: OrganizationService,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
|
||||||
private organizationUserService: OrganizationUserService,
|
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
|
private keyRotationService: UserKeyRotationService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
@@ -201,7 +183,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||||||
try {
|
try {
|
||||||
if (this.rotateUserKey) {
|
if (this.rotateUserKey) {
|
||||||
this.formPromise = this.apiService.postPassword(request).then(() => {
|
this.formPromise = this.apiService.postPassword(request).then(() => {
|
||||||
return this.updateKey(newMasterKey, request.newMasterPasswordHash);
|
return this.updateKey();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.formPromise = this.apiService.postPassword(request);
|
this.formPromise = this.apiService.postPassword(request);
|
||||||
@@ -220,81 +202,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateKey(masterKey: MasterKey, masterPasswordHash: string) {
|
private async updateKey() {
|
||||||
const [newUserKey, masterKeyEncUserKey] = await this.cryptoService.makeUserKey(masterKey);
|
await this.keyRotationService.rotateUserKeyAndEncryptedData(this.masterPassword);
|
||||||
const userPrivateKey = await this.cryptoService.getPrivateKey();
|
|
||||||
let encPrivateKey: EncString = null;
|
|
||||||
if (userPrivateKey != null) {
|
|
||||||
encPrivateKey = await this.cryptoService.encrypt(userPrivateKey, newUserKey);
|
|
||||||
}
|
|
||||||
const request = new UpdateKeyRequest();
|
|
||||||
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
|
|
||||||
request.key = masterKeyEncUserKey.encryptedString;
|
|
||||||
request.masterPasswordHash = masterPasswordHash;
|
|
||||||
|
|
||||||
const folders = await firstValueFrom(this.folderService.folderViews$);
|
|
||||||
for (let i = 0; i < folders.length; i++) {
|
|
||||||
if (folders[i].id == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const folder = await this.folderService.encrypt(folders[i], newUserKey);
|
|
||||||
request.folders.push(new FolderWithIdRequest(folder));
|
|
||||||
}
|
|
||||||
|
|
||||||
const ciphers = await this.cipherService.getAllDecrypted();
|
|
||||||
for (let i = 0; i < ciphers.length; i++) {
|
|
||||||
if (ciphers[i].organizationId != null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cipher = await this.cipherService.encrypt(ciphers[i], newUserKey);
|
|
||||||
request.ciphers.push(new CipherWithIdRequest(cipher));
|
|
||||||
}
|
|
||||||
|
|
||||||
const sends = await firstValueFrom(this.sendService.sends$);
|
|
||||||
await Promise.all(
|
|
||||||
sends.map(async (send) => {
|
|
||||||
const sendKey = await this.cryptoService.decryptToBytes(send.key, null);
|
|
||||||
send.key = (await this.cryptoService.encrypt(sendKey, newUserKey)) ?? send.key;
|
|
||||||
request.sends.push(new SendWithIdRequest(send));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.deviceTrustCryptoService.rotateDevicesTrust(newUserKey, masterPasswordHash);
|
|
||||||
|
|
||||||
await this.apiService.postAccountKey(request);
|
|
||||||
|
|
||||||
await this.emergencyAccessService.rotate(newUserKey);
|
|
||||||
|
|
||||||
await this.updateAllResetPasswordKeys(newUserKey, masterPasswordHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateAllResetPasswordKeys(userKey: UserKey, masterPasswordHash: string) {
|
|
||||||
const orgs = await this.organizationService.getAll();
|
|
||||||
|
|
||||||
for (const org of orgs) {
|
|
||||||
// If not already enrolled, skip
|
|
||||||
if (!org.resetPasswordEnrolled) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve public key
|
|
||||||
const response = await this.organizationApiService.getKeys(org.id);
|
|
||||||
const publicKey = Utils.fromB64ToArray(response?.publicKey);
|
|
||||||
|
|
||||||
// Re-enroll - encrypt user's encKey.key with organization public key
|
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
|
||||||
|
|
||||||
// Create/Execute request
|
|
||||||
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
|
||||||
request.masterPasswordHash = masterPasswordHash;
|
|
||||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
|
||||||
|
|
||||||
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
|
||||||
org.id,
|
|
||||||
org.userId,
|
|
||||||
request,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PasswordCalloutComponent } from "@bitwarden/auth";
|
|||||||
|
|
||||||
import { SharedModule } from "../../shared";
|
import { SharedModule } from "../../shared";
|
||||||
import { EmergencyAccessModule } from "../emergency-access";
|
import { EmergencyAccessModule } from "../emergency-access";
|
||||||
|
import { UserKeyRotationModule } from "../key-rotation/user-key-rotation.module";
|
||||||
|
|
||||||
import { ChangePasswordComponent } from "./change-password.component";
|
import { ChangePasswordComponent } from "./change-password.component";
|
||||||
import { WebauthnLoginSettingsModule } from "./webauthn-login-settings";
|
import { WebauthnLoginSettingsModule } from "./webauthn-login-settings";
|
||||||
@@ -14,6 +15,7 @@ import { WebauthnLoginSettingsModule } from "./webauthn-login-settings";
|
|||||||
WebauthnLoginSettingsModule,
|
WebauthnLoginSettingsModule,
|
||||||
EmergencyAccessModule,
|
EmergencyAccessModule,
|
||||||
PasswordCalloutComponent,
|
PasswordCalloutComponent,
|
||||||
|
UserKeyRotationModule,
|
||||||
],
|
],
|
||||||
declarations: [ChangePasswordComponent],
|
declarations: [ChangePasswordComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ import { RegisterRequest } from "../models/request/register.request";
|
|||||||
import { StorageRequest } from "../models/request/storage.request";
|
import { StorageRequest } from "../models/request/storage.request";
|
||||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||||
import { UpdateDomainsRequest } from "../models/request/update-domains.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 { VerifyDeleteRecoverRequest } from "../models/request/verify-delete-recover.request";
|
||||||
import { VerifyEmailRequest } from "../models/request/verify-email.request";
|
import { VerifyEmailRequest } from "../models/request/verify-email.request";
|
||||||
import { BreachAccountResponse } from "../models/response/breach-account.response";
|
import { BreachAccountResponse } from "../models/response/breach-account.response";
|
||||||
@@ -176,7 +175,6 @@ export abstract class ApiService {
|
|||||||
postAccountStorage: (request: StorageRequest) => Promise<PaymentResponse>;
|
postAccountStorage: (request: StorageRequest) => Promise<PaymentResponse>;
|
||||||
postAccountPayment: (request: PaymentRequest) => Promise<void>;
|
postAccountPayment: (request: PaymentRequest) => Promise<void>;
|
||||||
postAccountLicense: (data: FormData) => Promise<any>;
|
postAccountLicense: (data: FormData) => Promise<any>;
|
||||||
postAccountKey: (request: UpdateKeyRequest) => Promise<any>;
|
|
||||||
postAccountKeys: (request: KeysRequest) => Promise<any>;
|
postAccountKeys: (request: KeysRequest) => Promise<any>;
|
||||||
postAccountVerifyEmail: () => Promise<any>;
|
postAccountVerifyEmail: () => Promise<any>;
|
||||||
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
|
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ import { SecretVerificationRequest } from "../../../../auth/models/request/secre
|
|||||||
export class OrganizationUserResetPasswordEnrollmentRequest extends SecretVerificationRequest {
|
export class OrganizationUserResetPasswordEnrollmentRequest extends SecretVerificationRequest {
|
||||||
resetPasswordKey: string;
|
resetPasswordKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class OrganizationUserResetPasswordWithIdRequest extends OrganizationUserResetPasswordEnrollmentRequest {
|
||||||
|
organizationId: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export enum FeatureFlag {
|
|||||||
FlexibleCollections = "flexible-collections",
|
FlexibleCollections = "flexible-collections",
|
||||||
FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional
|
FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional
|
||||||
BulkCollectionAccess = "bulk-collection-access",
|
BulkCollectionAccess = "bulk-collection-access",
|
||||||
|
KeyRotationImprovements = "key-rotation-improvements",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -105,7 +105,6 @@ import { RegisterRequest } from "../models/request/register.request";
|
|||||||
import { StorageRequest } from "../models/request/storage.request";
|
import { StorageRequest } from "../models/request/storage.request";
|
||||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||||
import { UpdateDomainsRequest } from "../models/request/update-domains.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 { VerifyDeleteRecoverRequest } from "../models/request/verify-delete-recover.request";
|
||||||
import { VerifyEmailRequest } from "../models/request/verify-email.request";
|
import { VerifyEmailRequest } from "../models/request/verify-email.request";
|
||||||
import { BreachAccountResponse } from "../models/response/breach-account.response";
|
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);
|
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> {
|
postAccountVerifyEmail(): Promise<any> {
|
||||||
return this.send("POST", "/accounts/verify-email", null, true, false);
|
return this.send("POST", "/accounts/verify-email", null, true, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
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 { SendData } from "../models/data/send.data";
|
||||||
import { Send } from "../models/domain/send";
|
import { Send } from "../models/domain/send";
|
||||||
|
import { SendWithIdRequest } from "../models/request/send-with-id.request";
|
||||||
import { SendView } from "../models/view/send.view";
|
import { SendView } from "../models/view/send.view";
|
||||||
|
|
||||||
export abstract class SendService {
|
export abstract class SendService {
|
||||||
@@ -17,6 +18,13 @@ export abstract class SendService {
|
|||||||
key?: SymmetricCryptoKey,
|
key?: SymmetricCryptoKey,
|
||||||
) => Promise<[Send, EncArrayBuffer]>;
|
) => Promise<[Send, EncArrayBuffer]>;
|
||||||
get: (id: string) => Send;
|
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
|
* @deprecated Do not call this, use the sends$ observable collection
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
|||||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||||
import { StateService } from "../../../platform/abstractions/state.service";
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
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 { ContainerService } from "../../../platform/services/container.service";
|
||||||
import { SendData } from "../models/data/send.data";
|
import { SendData } from "../models/data/send.data";
|
||||||
import { Send } from "../models/domain/send";
|
import { Send } from "../models/domain/send";
|
||||||
@@ -92,6 +93,37 @@ describe("SendService", () => {
|
|||||||
expect(stateService.getDecryptedSends).toHaveBeenCalledTimes(1);
|
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
|
// InternalSendService
|
||||||
|
|
||||||
it("upsert", async () => {
|
it("upsert", async () => {
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { StateService } from "../../../platform/abstractions/state.service";
|
|||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
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 { SendType } from "../enums/send-type";
|
||||||
import { SendData } from "../models/data/send.data";
|
import { SendData } from "../models/data/send.data";
|
||||||
import { Send } from "../models/domain/send";
|
import { Send } from "../models/domain/send";
|
||||||
import { SendFile } from "../models/domain/send-file";
|
import { SendFile } from "../models/domain/send-file";
|
||||||
import { SendText } from "../models/domain/send-text";
|
import { SendText } from "../models/domain/send-text";
|
||||||
|
import { SendWithIdRequest } from "../models/request/send-with-id.request";
|
||||||
import { SendView } from "../models/view/send.view";
|
import { SendView } from "../models/view/send.view";
|
||||||
import { SEND_KDF_ITERATIONS } from "../send-kdf";
|
import { SEND_KDF_ITERATIONS } from "../send-kdf";
|
||||||
|
|
||||||
@@ -212,6 +213,22 @@ export class SendService implements InternalSendServiceAbstraction {
|
|||||||
await this.stateService.setEncryptedSends(sends);
|
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> {
|
private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|||||||
Reference in New Issue
Block a user