diff --git a/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.spec.ts b/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.spec.ts index 393cdac20f6..7b54b497f99 100644 --- a/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.spec.ts @@ -10,6 +10,49 @@ describe("EmergencyAccessService", () => { // emergencyAccessService = new EmergencyAccessService(); }); + describe("updateEmergencyAccesses", () => { + let mockUserKey: UserKey; + + beforeEach(() => { + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + const mockEmergencyAccess = { + data: [ + createMockEmergencyAccess("0", "EA 0", EmergencyAccessStatusType.Invited), + createMockEmergencyAccess("1", "EA 1", EmergencyAccessStatusType.Accepted), + createMockEmergencyAccess("2", "EA 2", EmergencyAccessStatusType.Confirmed), + createMockEmergencyAccess("3", "EA 3", EmergencyAccessStatusType.RecoveryInitiated), + createMockEmergencyAccess("4", "EA 4", EmergencyAccessStatusType.RecoveryApproved), + ], + } as ListResponse; + emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue(mockEmergencyAccess); + apiService.getUserPublicKey.mockResolvedValue({ + userId: "mockUserId", + publicKey: "mockPublicKey", + } as UserKeyResponse); + + cryptoService.rsaEncrypt.mockImplementation((plainValue, publicKey) => { + return Promise.resolve( + new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue) + ); + }); + }); + + it("Only updates emergency accesses with allowed statuses", async () => { + await migrateFromLegacyEncryptionService.updateEmergencyAccesses(mockUserKey); + + expect(emergencyAccessApiService.putEmergencyAccess).not.toHaveBeenCalledWith( + "0", + expect.any(EmergencyAccessUpdateRequest) + ); + expect(emergencyAccessApiService.putEmergencyAccess).not.toHaveBeenCalledWith( + "1", + expect.any(EmergencyAccessUpdateRequest) + ); + }); + }); + // describe("createCredential", () => { // it("should return undefined when navigator.credentials throws", async () => { // credentials.create.mockRejectedValue(new Error("Mocked error")); diff --git a/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts b/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts index c9d0935e9b2..f237b2d52fe 100644 --- a/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts +++ b/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts @@ -1,6 +1,8 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -40,6 +42,14 @@ export class EmergencyAccessService { private logService: LogService ) {} + /** + * Gets an emergency access by id. + * @param id emergency access id + */ + getEmergencyAccess(id: string): Promise { + return this.emergencyAccessApiService.getEmergencyAccess(id); + } + /** * Gets all emergency access that the user has been granted. */ @@ -54,6 +64,20 @@ export class EmergencyAccessService { return (await this.emergencyAccessApiService.getEmergencyAccessGranted()).data; } + /** + * Returns policies that apply to the grantor. + * Intended for grantee. + * @param id emergency access id + */ + async getGrantorPolicies(id: string): Promise { + const response = await this.emergencyAccessApiService.getEmergencyGrantorPolicies(id); + let policies: Policy[]; + if (response.data != null && response.data.length > 0) { + policies = response.data.map((policyResponse) => new Policy(new PolicyData(policyResponse))); + } + return policies; + } + /** * Invites the email address to be an emergency contact. * Step 1 of the 3 step setup flow. @@ -230,7 +254,12 @@ export class EmergencyAccessService { this.emergencyAccessApiService.postEmergencyAccessPassword(id, request); } - async rotateEmergencyAccess(newUserKey: UserKey) { + /** + * Rotates the user key for all existing emergency access. + * Intended for grantor. + * @param newUserKey the new user key + */ + async rotate(newUserKey: UserKey): Promise { const emergencyAccess = await this.emergencyAccessApiService.getEmergencyAccessTrusted(); // Any Invited or Accepted requests won't have the key yet, so we don't need to update them const allowedStatuses = new Set([ diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts index 4a058ad76e7..db3ea5fbc7d 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts @@ -8,9 +8,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type"; -import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request"; -import { EmergencyAccessGranteeDetailsResponse } from "@bitwarden/common/auth/models/response/emergency-access.response"; import { EncryptionType, KdfType } from "@bitwarden/common/enums"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response"; @@ -35,7 +32,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service"; -import { EmergencyAccessApiService } from "../core/services/emergency-access/emergency-access-api.service"; +import { EmergencyAccessService } from "../core/services/emergency-access/emergency-access.service"; describe("migrateFromLegacyEncryptionService", () => { let migrateFromLegacyEncryptionService: MigrateFromLegacyEncryptionService; @@ -43,7 +40,7 @@ describe("migrateFromLegacyEncryptionService", () => { const organizationService = mock(); const organizationApiService = mock(); const organizationUserService = mock(); - const emergencyAccessApiService = mock(); + const emergencyAccessService = mock(); const apiService = mock(); const encryptService = mock(); const cryptoService = mock(); @@ -62,7 +59,7 @@ describe("migrateFromLegacyEncryptionService", () => { organizationService, organizationApiService, organizationUserService, - emergencyAccessApiService, + emergencyAccessService, apiService, cryptoService, encryptService, @@ -112,6 +109,9 @@ describe("migrateFromLegacyEncryptionService", () => { 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(mockFolders); folderService.folderViews$ = folderViews; @@ -204,40 +204,12 @@ describe("migrateFromLegacyEncryptionService", () => { beforeEach(() => { const mockRandomBytes = new Uint8Array(64) as CsprngArray; mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; - - const mockEmergencyAccess = { - data: [ - createMockEmergencyAccess("0", "EA 0", EmergencyAccessStatusType.Invited), - createMockEmergencyAccess("1", "EA 1", EmergencyAccessStatusType.Accepted), - createMockEmergencyAccess("2", "EA 2", EmergencyAccessStatusType.Confirmed), - createMockEmergencyAccess("3", "EA 3", EmergencyAccessStatusType.RecoveryInitiated), - createMockEmergencyAccess("4", "EA 4", EmergencyAccessStatusType.RecoveryApproved), - ], - } as ListResponse; - emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue(mockEmergencyAccess); - apiService.getUserPublicKey.mockResolvedValue({ - userId: "mockUserId", - publicKey: "mockPublicKey", - } as UserKeyResponse); - - cryptoService.rsaEncrypt.mockImplementation((plainValue, publicKey) => { - return Promise.resolve( - new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue) - ); - }); }); - it("Only updates emergency accesses with allowed statuses", async () => { + it("Uses emergency access service to rotate", async () => { await migrateFromLegacyEncryptionService.updateEmergencyAccesses(mockUserKey); - expect(emergencyAccessApiService.putEmergencyAccess).not.toHaveBeenCalledWith( - "0", - expect.any(EmergencyAccessUpdateRequest) - ); - expect(emergencyAccessApiService.putEmergencyAccess).not.toHaveBeenCalledWith( - "1", - expect.any(EmergencyAccessUpdateRequest) - ); + expect(emergencyAccessService.rotate).toHaveBeenCalled(); }); }); @@ -325,19 +297,6 @@ function createMockSend(id: string, name: string): Send { return send; } -function createMockEmergencyAccess( - id: string, - name: string, - status: EmergencyAccessStatusType -): EmergencyAccessGranteeDetailsResponse { - const emergencyAccess = new EmergencyAccessGranteeDetailsResponse({}); - emergencyAccess.id = id; - emergencyAccess.name = name; - emergencyAccess.type = 0; - emergencyAccess.status = status; - return emergencyAccess; -} - function createOrganization(id: string, name: string, resetPasswordEnrolled: boolean) { const org = new Organization(); org.id = id; diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts index d0e398c942b..ccb54cea5b8 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts @@ -6,8 +6,6 @@ import { OrganizationUserService } from "@bitwarden/common/abstractions/organiza import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests"; 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 { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type"; -import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request"; 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"; @@ -22,7 +20,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde 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 { EmergencyAccessApiService } from "../core/services/emergency-access/emergency-access-api.service"; +import { EmergencyAccessService } from "../core/services/emergency-access/emergency-access.service"; // TODO: PM-3797 - This service should be expanded and used for user key rotations in change-password.component.ts @Injectable() @@ -31,7 +29,7 @@ export class MigrateFromLegacyEncryptionService { private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, - private emergencyAccessApiService: EmergencyAccessApiService, + private emergencyAccessService: EmergencyAccessService, private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, @@ -104,31 +102,8 @@ export class MigrateFromLegacyEncryptionService { * on the server. * @param newUserKey The new user key */ - async updateEmergencyAccesses(newUserKey: UserKey) { - const emergencyAccess = await this.emergencyAccessApiService.getEmergencyAccessTrusted(); - // Any Invited or Accepted requests won't have the key yet, so we don't need to update them - const allowedStatuses = new Set([ - EmergencyAccessStatusType.Confirmed, - EmergencyAccessStatusType.RecoveryInitiated, - EmergencyAccessStatusType.RecoveryApproved, - ]); - const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.has(d.status)); - - for (const details of filteredAccesses) { - // Get public key of grantee - const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId); - const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - - // Encrypt new user key with public key - const encryptedKey = await this.cryptoService.rsaEncrypt(newUserKey.key, publicKey); - - const updateRequest = new EmergencyAccessUpdateRequest(); - updateRequest.type = details.type; - updateRequest.waitTimeDays = details.waitTimeDays; - updateRequest.keyEncrypted = encryptedKey.encryptedString; - - await this.emergencyAccessApiService.putEmergencyAccess(details.id, updateRequest); - } + updateEmergencyAccesses(newUserKey: UserKey) { + return this.emergencyAccessService.rotate(newUserKey); } /** Updates all admin recovery keys on the server with the new user key diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index bb3f11bbee3..03eeeb4f196 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -23,11 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl 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 { - MasterKey, - SymmetricCryptoKey, - 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 { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -37,9 +33,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv 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 { EmergencyAccessApiService } from "../core/services/emergency-access/emergency-access-api.service"; -import { EmergencyAccessStatusType } from "../core/enums/emergency-access-status-type"; -import { EmergencyAccessUpdateRequest } from "../core/services/emergency-access/request/emergency-access-update.request"; +import { EmergencyAccessService } from "../core/services/emergency-access/emergency-access.service"; @Component({ selector: "app-change-password", @@ -66,7 +60,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { private folderService: FolderService, private cipherService: CipherService, private syncService: SyncService, - private emergencyAccessApiService: EmergencyAccessApiService, + private emergencyAccessService: EmergencyAccessService, private apiService: ApiService, private sendService: SendService, private organizationService: OrganizationService, @@ -269,36 +263,11 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { await this.apiService.postAccountKey(request); - await this.updateEmergencyAccesses(newUserKey); + await this.emergencyAccessService.rotate(newUserKey); await this.updateAllResetPasswordKeys(newUserKey, masterPasswordHash); } - private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) { - const emergencyAccess = await this.emergencyAccessApiService.getEmergencyAccessTrusted(); - const allowedStatuses = [ - EmergencyAccessStatusType.Confirmed, - EmergencyAccessStatusType.RecoveryInitiated, - EmergencyAccessStatusType.RecoveryApproved, - ]; - - const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.includes(d.status)); - - for (const details of filteredAccesses) { - const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId); - const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - - const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey); - - const updateRequest = new EmergencyAccessUpdateRequest(); - updateRequest.type = details.type; - updateRequest.waitTimeDays = details.waitTimeDays; - updateRequest.keyEncrypted = encryptedKey.encryptedString; - - await this.emergencyAccessApiService.putEmergencyAccess(details.id, updateRequest); - } - } - private async updateAllResetPasswordKeys(userKey: UserKey, masterPasswordHash: string) { const orgs = await this.organizationService.getAll(); diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index b13fb0d335b..64486f73770 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -1,10 +1,9 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { EmergencyAccessType } from "@bitwarden/common/auth/enums/emergency-access-type"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { EmergencyAccessApiService } from "../../core/services/emergency-access/emergency-access-api.service"; +import { EmergencyAccessType } from "../../core/enums/emergency-access-type"; import { EmergencyAccessService } from "../../core/services/emergency-access/emergency-access.service"; @Component({ @@ -31,7 +30,6 @@ export class EmergencyAccessAddEditComponent implements OnInit { waitTime: number; constructor( - private emergencyAccessApiService: EmergencyAccessApiService, private emergencyAccessService: EmergencyAccessService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, @@ -54,7 +52,7 @@ export class EmergencyAccessAddEditComponent implements OnInit { this.editMode = true; this.title = this.i18nService.t("editEmergencyContact"); try { - const emergencyAccess = await this.emergencyAccessApiService.getEmergencyAccess( + const emergencyAccess = await this.emergencyAccessService.getEmergencyAccess( this.emergencyAccessId ); this.type = emergencyAccess.type; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.ts index 6533481abbd..d27c5e15685 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.ts @@ -3,9 +3,6 @@ import { takeUntil } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { KdfType } from "@bitwarden/common/enums"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -16,7 +13,6 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; -import { EmergencyAccessApiService } from "../../core/services/emergency-access/emergency-access-api.service"; import { EmergencyAccessService } from "../../core/services/emergency-access/emergency-access.service"; @Component({ @@ -46,7 +42,6 @@ export class EmergencyAccessTakeoverComponent platformUtilsService: PlatformUtilsService, policyService: PolicyService, private emergencyAccessService: EmergencyAccessService, - private emergencyAccessApiService: EmergencyAccessApiService, private logService: LogService, dialogService: DialogService ) { @@ -63,19 +58,11 @@ export class EmergencyAccessTakeoverComponent } async ngOnInit() { - const response = await this.emergencyAccessApiService.getEmergencyGrantorPolicies( - this.emergencyAccessId - ); - if (response.data != null && response.data.length > 0) { - const policies = response.data.map( - (policyResponse: PolicyResponse) => new Policy(new PolicyData(policyResponse)) - ); - - this.policyService - .masterPasswordPolicyOptions$(policies) - .pipe(takeUntil(this.destroy$)) - .subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions)); - } + const policies = await this.emergencyAccessService.getGrantorPolicies(this.emergencyAccessId); + this.policyService + .masterPasswordPolicyOptions$(policies) + .pipe(takeUntil(this.destroy$)) + .subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions)); } // eslint-disable-next-line rxjs-angular/prefer-takeuntil