From 929a08339f39bacb25c4743b9aaf66589d0a5d56 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 8 Nov 2023 16:03:10 -0500 Subject: [PATCH] [PM-3797 Part 1] Add Emergency Access Service (#6612) * lazy load and move accept emergency component * create emergency access services - move api calls to specific api service and refactor * remove any from emergency api service * move emergency access logic to service * create emergency access view * move view ciphers logic to service * move models to web folder * move takeover logic to service * remove emergency api service dependency from other files * write tests for emergency access service * import shared module into component * fix imports * Revert "fix imports" This reverts commit d21cb02bd89e403f4a7bd82030d2eabe155d9ba1. * create emergency access module for service * move emergency access out of core folder - add more organization to components under settings * change EA views to domain models * move EA enums to folder * resolve PR feedback --- .../accept}/accept-emergency.component.html | 0 .../accept}/accept-emergency.component.ts | 18 +- .../emergency-access.module.ts | 11 + .../enums/emergency-access-status-type.ts | 0 .../enums/emergency-access-type.ts | 0 .../src/app/auth/emergency-access/index.ts | 2 + .../models/emergency-access.ts | 42 +++ .../emergency-access-accept.request.ts | 0 .../emergency-access-confirm.request.ts | 0 .../emergency-access-invite.request.ts | 2 +- .../emergency-access-password.request.ts | 0 .../emergency-access-update.request.ts | 2 +- .../response/emergency-access.response.ts | 11 +- .../services/emergency-access-api.service.ts | 131 ++++++++ .../services/emergency-access.service.spec.ts | 283 +++++++++++++++++ .../services/emergency-access.service.ts | 292 ++++++++++++++++++ .../auth/emergency-access/services/index.ts | 1 + .../migrate-legacy-encryption.component.ts | 3 +- .../migrate-legacy-encryption.service.spec.ts | 57 +--- .../migrate-legacy-encryption.service.ts | 32 +- .../settings/change-password.component.ts | 38 +-- .../emergency-access-attachments.component.ts | 2 +- .../emergency-access-confirm.component.html | 0 .../emergency-access-confirm.component.ts | 0 .../emergency-access-add-edit.component.ts | 26 +- .../emergency-access.component.ts | 95 ++---- .../emergency-access-takeover.component.html | 0 .../emergency-access-takeover.component.ts | 74 +---- .../emergency-access-view.component.html | 0 .../emergency-access-view.component.ts | 45 +-- .../emergency-add-edit-cipher.component.ts} | 6 +- .../src/app/auth/settings/settings.module.ts | 8 +- apps/web/src/app/oss-routing.module.ts | 8 +- .../src/app/shared/loose-components.module.ts | 17 +- libs/common/src/abstractions/api.service.ts | 31 -- libs/common/src/services/api.service.ts | 87 ------ 36 files changed, 889 insertions(+), 435 deletions(-) rename apps/web/src/app/auth/{ => emergency-access/accept}/accept-emergency.component.html (100%) rename apps/web/src/app/auth/{ => emergency-access/accept}/accept-emergency.component.ts (75%) create mode 100644 apps/web/src/app/auth/emergency-access/emergency-access.module.ts rename {libs/common/src/auth => apps/web/src/app/auth/emergency-access}/enums/emergency-access-status-type.ts (100%) rename {libs/common/src/auth => apps/web/src/app/auth/emergency-access}/enums/emergency-access-type.ts (100%) create mode 100644 apps/web/src/app/auth/emergency-access/index.ts create mode 100644 apps/web/src/app/auth/emergency-access/models/emergency-access.ts rename {libs/common/src/auth/models => apps/web/src/app/auth/emergency-access}/request/emergency-access-accept.request.ts (100%) rename {libs/common/src/auth/models => apps/web/src/app/auth/emergency-access}/request/emergency-access-confirm.request.ts (100%) rename {libs/common/src/auth/models => apps/web/src/app/auth/emergency-access}/request/emergency-access-invite.request.ts (61%) rename {libs/common/src/auth/models => apps/web/src/app/auth/emergency-access}/request/emergency-access-password.request.ts (100%) rename {libs/common/src/auth/models => apps/web/src/app/auth/emergency-access}/request/emergency-access-update.request.ts (63%) rename {libs/common/src/auth/models => apps/web/src/app/auth/emergency-access}/response/emergency-access.response.ts (87%) create mode 100644 apps/web/src/app/auth/emergency-access/services/emergency-access-api.service.ts create mode 100644 apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts create mode 100644 apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts create mode 100644 apps/web/src/app/auth/emergency-access/services/index.ts rename apps/web/src/app/auth/settings/emergency-access/{ => attachments}/emergency-access-attachments.component.ts (96%) rename apps/web/src/app/auth/settings/emergency-access/{ => confirm}/emergency-access-confirm.component.html (100%) rename apps/web/src/app/auth/settings/emergency-access/{ => confirm}/emergency-access-confirm.component.ts (100%) rename apps/web/src/app/auth/settings/emergency-access/{ => takeover}/emergency-access-takeover.component.html (100%) rename apps/web/src/app/auth/settings/emergency-access/{ => takeover}/emergency-access-takeover.component.ts (54%) rename apps/web/src/app/auth/settings/emergency-access/{ => view}/emergency-access-view.component.html (100%) rename apps/web/src/app/auth/settings/emergency-access/{ => view}/emergency-access-view.component.ts (51%) rename apps/web/src/app/auth/settings/emergency-access/{emergency-add-edit.component.ts => view/emergency-add-edit-cipher.component.ts} (94%) diff --git a/apps/web/src/app/auth/accept-emergency.component.html b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html similarity index 100% rename from apps/web/src/app/auth/accept-emergency.component.html rename to apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html diff --git a/apps/web/src/app/auth/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts similarity index 75% rename from apps/web/src/app/auth/accept-emergency.component.ts rename to apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts index 8ade8d6902..5acd7b53af 100644 --- a/apps/web/src/app/auth/accept-emergency.component.ts +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts @@ -1,16 +1,18 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EmergencyAccessAcceptRequest } from "@bitwarden/common/auth/models/request/emergency-access-accept.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BaseAcceptComponent } from "../common/base.accept.component"; +import { BaseAcceptComponent } from "../../../common/base.accept.component"; +import { SharedModule } from "../../../shared"; +import { EmergencyAccessModule } from "../emergency-access.module"; +import { EmergencyAccessService } from "../services/emergency-access.service"; @Component({ - selector: "app-accept-emergency", + standalone: true, + imports: [SharedModule, EmergencyAccessModule], templateUrl: "accept-emergency.component.html", }) export class AcceptEmergencyComponent extends BaseAcceptComponent { @@ -25,16 +27,14 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, route: ActivatedRoute, - private apiService: ApiService, - stateService: StateService + stateService: StateService, + private emergencyAccessService: EmergencyAccessService ) { super(router, platformUtilsService, i18nService, route, stateService); } async authedHandler(qParams: Params): Promise { - const request = new EmergencyAccessAcceptRequest(); - request.token = qParams.token; - this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request); + this.actionPromise = this.emergencyAccessService.accept(qParams.id, qParams.token); await this.actionPromise; await this.stateService.setEmergencyAccessInvitation(null); this.platformUtilService.showToast( diff --git a/apps/web/src/app/auth/emergency-access/emergency-access.module.ts b/apps/web/src/app/auth/emergency-access/emergency-access.module.ts new file mode 100644 index 0000000000..7cd81fdc87 --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/emergency-access.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from "@angular/core"; + +import { EmergencyAccessApiService } from "./services/emergency-access-api.service"; +import { EmergencyAccessService } from "./services/emergency-access.service"; + +@NgModule({ + declarations: [], + imports: [], + providers: [EmergencyAccessApiService, EmergencyAccessService], +}) +export class EmergencyAccessModule {} diff --git a/libs/common/src/auth/enums/emergency-access-status-type.ts b/apps/web/src/app/auth/emergency-access/enums/emergency-access-status-type.ts similarity index 100% rename from libs/common/src/auth/enums/emergency-access-status-type.ts rename to apps/web/src/app/auth/emergency-access/enums/emergency-access-status-type.ts diff --git a/libs/common/src/auth/enums/emergency-access-type.ts b/apps/web/src/app/auth/emergency-access/enums/emergency-access-type.ts similarity index 100% rename from libs/common/src/auth/enums/emergency-access-type.ts rename to apps/web/src/app/auth/emergency-access/enums/emergency-access-type.ts diff --git a/apps/web/src/app/auth/emergency-access/index.ts b/apps/web/src/app/auth/emergency-access/index.ts new file mode 100644 index 0000000000..3452873710 --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/index.ts @@ -0,0 +1,2 @@ +export * from "./emergency-access.module"; +export * from "./services"; diff --git a/apps/web/src/app/auth/emergency-access/models/emergency-access.ts b/apps/web/src/app/auth/emergency-access/models/emergency-access.ts new file mode 100644 index 0000000000..28d9476c03 --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/models/emergency-access.ts @@ -0,0 +1,42 @@ +import { KdfType } from "@bitwarden/common/enums"; +import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response"; + +import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; +import { EmergencyAccessType } from "../enums/emergency-access-type"; + +export class GranteeEmergencyAccess { + id: string; + granteeId: string; + name: string; + email: string; + type: EmergencyAccessType; + status: EmergencyAccessStatusType; + waitTimeDays: number; + creationDate: string; + avatarColor: string; +} + +export class GrantorEmergencyAccess { + id: string; + grantorId: string; + name: string; + email: string; + type: EmergencyAccessType; + status: EmergencyAccessStatusType; + waitTimeDays: number; + creationDate: string; + avatarColor: string; +} + +export class TakeoverTypeEmergencyAccess { + keyEncrypted: string; + kdf: KdfType; + kdfIterations: number; + kdfMemory?: number; + kdfParallelism?: number; +} + +export class ViewTypeEmergencyAccess { + keyEncrypted: string; + ciphers: CipherResponse[] = []; +} diff --git a/libs/common/src/auth/models/request/emergency-access-accept.request.ts b/apps/web/src/app/auth/emergency-access/request/emergency-access-accept.request.ts similarity index 100% rename from libs/common/src/auth/models/request/emergency-access-accept.request.ts rename to apps/web/src/app/auth/emergency-access/request/emergency-access-accept.request.ts diff --git a/libs/common/src/auth/models/request/emergency-access-confirm.request.ts b/apps/web/src/app/auth/emergency-access/request/emergency-access-confirm.request.ts similarity index 100% rename from libs/common/src/auth/models/request/emergency-access-confirm.request.ts rename to apps/web/src/app/auth/emergency-access/request/emergency-access-confirm.request.ts diff --git a/libs/common/src/auth/models/request/emergency-access-invite.request.ts b/apps/web/src/app/auth/emergency-access/request/emergency-access-invite.request.ts similarity index 61% rename from libs/common/src/auth/models/request/emergency-access-invite.request.ts rename to apps/web/src/app/auth/emergency-access/request/emergency-access-invite.request.ts index c5f038bd2c..f4ba5c5470 100644 --- a/libs/common/src/auth/models/request/emergency-access-invite.request.ts +++ b/apps/web/src/app/auth/emergency-access/request/emergency-access-invite.request.ts @@ -1,4 +1,4 @@ -import { EmergencyAccessType } from "../../enums/emergency-access-type"; +import { EmergencyAccessType } from "../enums/emergency-access-type"; export class EmergencyAccessInviteRequest { email: string; diff --git a/libs/common/src/auth/models/request/emergency-access-password.request.ts b/apps/web/src/app/auth/emergency-access/request/emergency-access-password.request.ts similarity index 100% rename from libs/common/src/auth/models/request/emergency-access-password.request.ts rename to apps/web/src/app/auth/emergency-access/request/emergency-access-password.request.ts diff --git a/libs/common/src/auth/models/request/emergency-access-update.request.ts b/apps/web/src/app/auth/emergency-access/request/emergency-access-update.request.ts similarity index 63% rename from libs/common/src/auth/models/request/emergency-access-update.request.ts rename to apps/web/src/app/auth/emergency-access/request/emergency-access-update.request.ts index 37e7e975aa..b2fc226564 100644 --- a/libs/common/src/auth/models/request/emergency-access-update.request.ts +++ b/apps/web/src/app/auth/emergency-access/request/emergency-access-update.request.ts @@ -1,4 +1,4 @@ -import { EmergencyAccessType } from "../../enums/emergency-access-type"; +import { EmergencyAccessType } from "../enums/emergency-access-type"; export class EmergencyAccessUpdateRequest { type: EmergencyAccessType; diff --git a/libs/common/src/auth/models/response/emergency-access.response.ts b/apps/web/src/app/auth/emergency-access/response/emergency-access.response.ts similarity index 87% rename from libs/common/src/auth/models/response/emergency-access.response.ts rename to apps/web/src/app/auth/emergency-access/response/emergency-access.response.ts index a8ec9a0c0f..1a9c1388f7 100644 --- a/libs/common/src/auth/models/response/emergency-access.response.ts +++ b/apps/web/src/app/auth/emergency-access/response/emergency-access.response.ts @@ -1,8 +1,9 @@ -import { KdfType } from "../../../enums"; -import { BaseResponse } from "../../../models/response/base.response"; -import { CipherResponse } from "../../../vault/models/response/cipher.response"; -import { EmergencyAccessStatusType } from "../../enums/emergency-access-status-type"; -import { EmergencyAccessType } from "../../enums/emergency-access-type"; +import { KdfType } from "@bitwarden/common/enums"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response"; + +import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; +import { EmergencyAccessType } from "../enums/emergency-access-type"; export class EmergencyAccessGranteeDetailsResponse extends BaseResponse { id: string; diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access-api.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access-api.service.ts new file mode 100644 index 0000000000..00df57a7ce --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access-api.service.ts @@ -0,0 +1,131 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; + +import { EmergencyAccessAcceptRequest } from "../request/emergency-access-accept.request"; +import { EmergencyAccessConfirmRequest } from "../request/emergency-access-confirm.request"; +import { EmergencyAccessInviteRequest } from "../request/emergency-access-invite.request"; +import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request"; +import { EmergencyAccessUpdateRequest } from "../request/emergency-access-update.request"; +import { + EmergencyAccessGranteeDetailsResponse, + EmergencyAccessGrantorDetailsResponse, + EmergencyAccessTakeoverResponse, + EmergencyAccessViewResponse, +} from "../response/emergency-access.response"; + +@Injectable() +export class EmergencyAccessApiService { + constructor(private apiService: ApiService) {} + + async getEmergencyAccessTrusted(): Promise> { + const r = await this.apiService.send("GET", "/emergency-access/trusted", null, true, true); + return new ListResponse(r, EmergencyAccessGranteeDetailsResponse); + } + + async getEmergencyAccessGranted(): Promise> { + const r = await this.apiService.send("GET", "/emergency-access/granted", null, true, true); + return new ListResponse(r, EmergencyAccessGrantorDetailsResponse); + } + + async getEmergencyAccess(id: string): Promise { + const r = await this.apiService.send("GET", "/emergency-access/" + id, null, true, true); + return new EmergencyAccessGranteeDetailsResponse(r); + } + + async getEmergencyGrantorPolicies(id: string): Promise> { + const r = await this.apiService.send( + "GET", + "/emergency-access/" + id + "/policies", + null, + true, + true + ); + return new ListResponse(r, PolicyResponse); + } + + putEmergencyAccess(id: string, request: EmergencyAccessUpdateRequest): Promise { + return this.apiService.send("PUT", "/emergency-access/" + id, request, true, false); + } + + deleteEmergencyAccess(id: string): Promise { + return this.apiService.send("DELETE", "/emergency-access/" + id, null, true, false); + } + + postEmergencyAccessInvite(request: EmergencyAccessInviteRequest): Promise { + return this.apiService.send("POST", "/emergency-access/invite", request, true, false); + } + + postEmergencyAccessReinvite(id: string): Promise { + return this.apiService.send("POST", "/emergency-access/" + id + "/reinvite", null, true, false); + } + + postEmergencyAccessAccept(id: string, request: EmergencyAccessAcceptRequest): Promise { + return this.apiService.send( + "POST", + "/emergency-access/" + id + "/accept", + request, + true, + false + ); + } + + postEmergencyAccessConfirm(id: string, request: EmergencyAccessConfirmRequest): Promise { + return this.apiService.send( + "POST", + "/emergency-access/" + id + "/confirm", + request, + true, + false + ); + } + + postEmergencyAccessInitiate(id: string): Promise { + return this.apiService.send("POST", "/emergency-access/" + id + "/initiate", null, true, false); + } + + postEmergencyAccessApprove(id: string): Promise { + return this.apiService.send("POST", "/emergency-access/" + id + "/approve", null, true, false); + } + + postEmergencyAccessReject(id: string): Promise { + return this.apiService.send("POST", "/emergency-access/" + id + "/reject", null, true, false); + } + + async postEmergencyAccessTakeover(id: string): Promise { + const r = await this.apiService.send( + "POST", + "/emergency-access/" + id + "/takeover", + null, + true, + true + ); + return new EmergencyAccessTakeoverResponse(r); + } + + async postEmergencyAccessPassword( + id: string, + request: EmergencyAccessPasswordRequest + ): Promise { + await this.apiService.send( + "POST", + "/emergency-access/" + id + "/password", + request, + true, + true + ); + } + + async postEmergencyAccessView(id: string): Promise { + const r = await this.apiService.send( + "POST", + "/emergency-access/" + id + "/view", + null, + true, + true + ); + return new EmergencyAccessViewResponse(r); + } +} diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts new file mode 100644 index 0000000000..596f3c1a5d --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -0,0 +1,283 @@ +import { MockProxy } from "jest-mock-extended"; +import mock from "jest-mock-extended/lib/Mock"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +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"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { + UserKey, + SymmetricCryptoKey, + MasterKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + +import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; +import { EmergencyAccessType } from "../enums/emergency-access-type"; +import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request"; +import { EmergencyAccessUpdateRequest } from "../request/emergency-access-update.request"; +import { + EmergencyAccessGranteeDetailsResponse, + EmergencyAccessTakeoverResponse, +} from "../response/emergency-access.response"; + +import { EmergencyAccessApiService } from "./emergency-access-api.service"; +import { EmergencyAccessService } from "./emergency-access.service"; + +describe("EmergencyAccessService", () => { + let emergencyAccessApiService: MockProxy; + let apiService: MockProxy; + let cryptoService: MockProxy; + let encryptService: MockProxy; + let cipherService: MockProxy; + let logService: MockProxy; + let emergencyAccessService: EmergencyAccessService; + + beforeAll(() => { + emergencyAccessApiService = mock(); + apiService = mock(); + cryptoService = mock(); + encryptService = mock(); + cipherService = mock(); + logService = mock(); + + emergencyAccessService = new EmergencyAccessService( + emergencyAccessApiService, + apiService, + cryptoService, + encryptService, + cipherService, + logService + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("3 step setup process", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("Step 1: invite", () => { + it("should post an emergency access invitation", async () => { + // Arrange + const email = "test@example.com"; + const type = EmergencyAccessType.View; + const waitTimeDays = 5; + + emergencyAccessApiService.postEmergencyAccessInvite.mockResolvedValueOnce(); + + // Act + await emergencyAccessService.invite(email, type, waitTimeDays); + + // Assert + expect(emergencyAccessApiService.postEmergencyAccessInvite).toHaveBeenCalledWith({ + email: email.trim(), + type: type, + waitTimeDays: waitTimeDays, + }); + }); + }); + + describe("Step 2: accept", () => { + it("should post an emergency access accept request", async () => { + // Arrange + const id = "some-id"; + const token = "some-token"; + + emergencyAccessApiService.postEmergencyAccessAccept.mockResolvedValueOnce(); + + // Act + await emergencyAccessService.accept(id, token); + + // Assert + expect(emergencyAccessApiService.postEmergencyAccessAccept).toHaveBeenCalledWith(id, { + token: token, + }); + }); + }); + + describe("Step 3: confirm", () => { + it("should post an emergency access confirmation", async () => { + // Arrange + const id = "some-id"; + const granteeId = "grantee-id"; + const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; + + const mockPublicKeyB64 = "some-public-key-in-base64"; + + // const publicKey = Utils.fromB64ToArray(publicKeyB64); + + const mockUserPublicKeyResponse = new UserKeyResponse({ + UserId: granteeId, + PublicKey: mockPublicKeyB64, + }); + + const mockUserPublicKeyEncryptedUserKey = new EncString( + EncryptionType.AesCbc256_HmacSha256_B64, + "mockUserPublicKeyEncryptedUserKey" + ); + + cryptoService.getUserKey.mockResolvedValueOnce(mockUserKey); + apiService.getUserPublicKey.mockResolvedValueOnce(mockUserPublicKeyResponse); + + cryptoService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey); + + emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce(); + + // Act + await emergencyAccessService.confirm(id, granteeId); + + // Assert + expect(emergencyAccessApiService.postEmergencyAccessConfirm).toHaveBeenCalledWith(id, { + key: mockUserPublicKeyEncryptedUserKey.encryptedString, + }); + }); + }); + }); + + describe("takeover", () => { + const mockId = "emergencyAccessId"; + const mockEmail = "emergencyAccessEmail"; + const mockName = "emergencyAccessName"; + + it("posts a new password when decryption succeeds", async () => { + // Arrange + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({ + keyEncrypted: "EncryptedKey", + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: 500, + } as EmergencyAccessTakeoverResponse); + + const mockDecryptedGrantorUserKey = new Uint8Array(64); + cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey); + + const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; + + cryptoService.makeMasterKey.mockResolvedValueOnce(mockMasterKey); + + const mockMasterKeyHash = "mockMasterKeyHash"; + cryptoService.hashMasterKey.mockResolvedValueOnce(mockMasterKeyHash); + + // must mock [UserKey, EncString] return from cryptoService.encryptUserKeyWithMasterKey + // where UserKey is the decrypted grantor user key + const mockMasterKeyEncryptedUserKey = new EncString( + EncryptionType.AesCbc256_HmacSha256_B64, + "mockMasterKeyEncryptedUserKey" + ); + + const mockUserKey = new SymmetricCryptoKey(mockDecryptedGrantorUserKey) as UserKey; + + cryptoService.encryptUserKeyWithMasterKey.mockResolvedValueOnce([ + mockUserKey, + mockMasterKeyEncryptedUserKey, + ]); + + const expectedEmergencyAccessPasswordRequest = new EmergencyAccessPasswordRequest(); + expectedEmergencyAccessPasswordRequest.newMasterPasswordHash = mockMasterKeyHash; + expectedEmergencyAccessPasswordRequest.key = mockMasterKeyEncryptedUserKey.encryptedString; + + // Act + await emergencyAccessService.takeover(mockId, mockEmail, mockName); + + // Assert + expect(emergencyAccessApiService.postEmergencyAccessPassword).toHaveBeenCalledWith( + mockId, + expectedEmergencyAccessPasswordRequest + ); + }); + + it("should not post a new password if decryption fails", async () => { + cryptoService.rsaDecrypt.mockResolvedValueOnce(null); + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({ + keyEncrypted: "EncryptedKey", + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: 500, + } as EmergencyAccessTakeoverResponse); + + await expect( + emergencyAccessService.takeover(mockId, mockEmail, mockName) + ).rejects.toThrowError("Failed to decrypt grantor key"); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); + }); + + describe("rotate", () => { + let mockUserKey: UserKey; + const allowedStatuses = [ + EmergencyAccessStatusType.Confirmed, + EmergencyAccessStatusType.RecoveryInitiated, + EmergencyAccessStatusType.RecoveryApproved, + ]; + + 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; + + beforeEach(() => { + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + 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 emergencyAccessService.rotate(mockUserKey); + + let expectedCallCount = 0; + + 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); + }); + }); +}); + +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; +} diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts new file mode 100644 index 0000000000..3a0fd8bd86 --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -0,0 +1,292 @@ +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"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; +import { EmergencyAccessType } from "../enums/emergency-access-type"; +import { GranteeEmergencyAccess, GrantorEmergencyAccess } from "../models/emergency-access"; +import { EmergencyAccessAcceptRequest } from "../request/emergency-access-accept.request"; +import { EmergencyAccessConfirmRequest } from "../request/emergency-access-confirm.request"; +import { EmergencyAccessInviteRequest } from "../request/emergency-access-invite.request"; +import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request"; +import { EmergencyAccessUpdateRequest } from "../request/emergency-access-update.request"; + +import { EmergencyAccessApiService } from "./emergency-access-api.service"; + +@Injectable() +export class EmergencyAccessService { + constructor( + private emergencyAccessApiService: EmergencyAccessApiService, + private apiService: ApiService, + private cryptoService: CryptoService, + private encryptService: EncryptService, + private cipherService: CipherService, + 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. + */ + async getEmergencyAccessTrusted(): Promise { + return (await this.emergencyAccessApiService.getEmergencyAccessTrusted()).data; + } + + /** + * Gets all emergency access that the user has granted. + */ + async getEmergencyAccessGranted(): Promise { + 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. + * Intended for grantor. + * @param email email address of trusted emergency contact + * @param type type of emergency access + * @param waitTimeDays number of days to wait before granting access + */ + async invite(email: string, type: EmergencyAccessType, waitTimeDays: number): Promise { + const request = new EmergencyAccessInviteRequest(); + request.email = email.trim(); + request.type = type; + request.waitTimeDays = waitTimeDays; + + await this.emergencyAccessApiService.postEmergencyAccessInvite(request); + } + + /** + * Sends another email for an existing emergency access invitation. + * Intended for grantor. + * @param id emergency access id + */ + reinvite(id: string): Promise { + return this.emergencyAccessApiService.postEmergencyAccessReinvite(id); + } + + /** + * Edits an existing emergency access. + * Intended for grantor. + * @param id emergency access id + * @param type type of emergency access + * @param waitTimeDays number of days to wait before granting access + */ + async update(id: string, type: EmergencyAccessType, waitTimeDays: number) { + const request = new EmergencyAccessUpdateRequest(); + request.type = type; + request.waitTimeDays = waitTimeDays; + + await this.emergencyAccessApiService.putEmergencyAccess(id, request); + } + + /** + * Accepts an emergency access invitation. + * Step 2 of the 3 step setup flow. + * Intended for grantee. + * @param id emergency access id + * @param token secret token provided in email + */ + async accept(id: string, token: string): Promise { + const request = new EmergencyAccessAcceptRequest(); + request.token = token; + + await this.emergencyAccessApiService.postEmergencyAccessAccept(id, request); + } + + /** + * Encrypts user key with grantee's public key and sends to bitwarden. + * Step 3 of the 3 step setup flow. + * Intended for grantor. + * @param id emergency access id + * @param token secret token provided in email + */ + async confirm(id: string, granteeId: string) { + const userKey = await this.cryptoService.getUserKey(); + if (!userKey) { + throw new Error("No user key found"); + } + const publicKeyResponse = await this.apiService.getUserPublicKey(granteeId); + const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); + + try { + this.logService.debug( + "User's fingerprint: " + + (await this.cryptoService.getFingerprint(granteeId, publicKey)).join("-") + ); + } catch { + // Ignore errors since it's just a debug message + } + + const request = new EmergencyAccessConfirmRequest(); + request.key = await this.encryptKey(userKey, publicKey); + await this.emergencyAccessApiService.postEmergencyAccessConfirm(id, request); + } + + /** + * Deletes an existing emergency access. + * Intended for either grantor or grantee. + * @param id emergency access id + */ + delete(id: string): Promise { + return this.emergencyAccessApiService.deleteEmergencyAccess(id); + } + + /** + * Requests access to grantor's vault. + * Intended for grantee. + * @param id emergency access id + */ + requestAccess(id: string): Promise { + return this.emergencyAccessApiService.postEmergencyAccessInitiate(id); + } + + /** + * Approves access to grantor's vault. + * Intended for grantor. + * @param id emergency access id + */ + approve(id: string): Promise { + return this.emergencyAccessApiService.postEmergencyAccessApprove(id); + } + + /** + * Rejects access to grantor's vault. + * Intended for grantor. + * @param id emergency access id + */ + reject(id: string): Promise { + return this.emergencyAccessApiService.postEmergencyAccessReject(id); + } + + /** + * Gets the grantor ciphers for an emergency access in view mode. + * Intended for grantee. + * @param id emergency access id + */ + async getViewOnlyCiphers(id: string): Promise { + const response = await this.emergencyAccessApiService.postEmergencyAccessView(id); + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted); + const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; + + const ciphers = await this.encryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey + ); + return ciphers.sort(this.cipherService.getLocaleSortingFunction()); + } + + /** + * Changes the password for an emergency access. + * Intended for grantee. + * @param id emergency access id + * @param masterPassword new master password + * @param email email address of grantee (must be consistent or login will fail) + */ + async takeover(id: string, masterPassword: string, email: string) { + const takeoverResponse = await this.emergencyAccessApiService.postEmergencyAccessTakeover(id); + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted); + if (grantorKeyBuffer == null) { + throw new Error("Failed to decrypt grantor key"); + } + + const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; + + const masterKey = await this.cryptoService.makeMasterKey( + masterPassword, + email, + takeoverResponse.kdf, + new KdfConfig( + takeoverResponse.kdfIterations, + takeoverResponse.kdfMemory, + takeoverResponse.kdfParallelism + ) + ); + const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey); + + const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey); + + const request = new EmergencyAccessPasswordRequest(); + request.newMasterPasswordHash = masterKeyHash; + request.key = encKey[1].encryptedString; + + this.emergencyAccessApiService.postEmergencyAccessPassword(id, request); + } + + /** + * Rotates the user key for all existing emergency access. + * Intended for grantor. + * @param newUserKey the new user key + */ + async rotate(newUserKey: UserKey): Promise { + const existingEmergencyAccess = + 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 = existingEmergencyAccess.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.encryptKey(newUserKey, publicKey); + + const updateRequest = new EmergencyAccessUpdateRequest(); + updateRequest.type = details.type; + updateRequest.waitTimeDays = details.waitTimeDays; + updateRequest.keyEncrypted = encryptedKey; + + await this.emergencyAccessApiService.putEmergencyAccess(details.id, updateRequest); + } + } + + private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise { + return (await this.cryptoService.rsaEncrypt(userKey.key, publicKey)).encryptedString; + } +} diff --git a/apps/web/src/app/auth/emergency-access/services/index.ts b/apps/web/src/app/auth/emergency-access/services/index.ts new file mode 100644 index 0000000000..9820b417a7 --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/services/index.ts @@ -0,0 +1 @@ +export * from "./emergency-access.service"; diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index d1bb74c066..b5ed677e2a 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -8,6 +8,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SharedModule } from "../../shared"; +import { EmergencyAccessModule } from "../emergency-access"; import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service"; @@ -15,7 +16,7 @@ import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption. // This component is used to migrate from the old encryption scheme to the new one. @Component({ standalone: true, - imports: [SharedModule], + imports: [SharedModule, EmergencyAccessModule], providers: [MigrateFromLegacyEncryptionService], templateUrl: "migrate-legacy-encryption.component.html", }) 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 f4b1409b0a..08f433cf5a 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,12 +8,7 @@ import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/commo 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"; 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"; @@ -34,6 +29,8 @@ 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 { EmergencyAccessService } from "../emergency-access"; + import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service"; describe("migrateFromLegacyEncryptionService", () => { @@ -42,6 +39,7 @@ describe("migrateFromLegacyEncryptionService", () => { const organizationService = mock(); const organizationApiService = mock(); const organizationUserService = mock(); + const emergencyAccessService = mock(); const apiService = mock(); const encryptService = mock(); const cryptoService = mock(); @@ -60,6 +58,7 @@ describe("migrateFromLegacyEncryptionService", () => { organizationService, organizationApiService, organizationUserService, + emergencyAccessService, apiService, cryptoService, encryptService, @@ -109,6 +108,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; @@ -201,40 +203,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; - apiService.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(apiService.putEmergencyAccess).not.toHaveBeenCalledWith( - "0", - expect.any(EmergencyAccessUpdateRequest) - ); - expect(apiService.putEmergencyAccess).not.toHaveBeenCalledWith( - "1", - expect.any(EmergencyAccessUpdateRequest) - ); + expect(emergencyAccessService.rotate).toHaveBeenCalled(); }); }); @@ -322,19 +296,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 06f1f157f3..df8158dd74 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 { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -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"; @@ -23,6 +21,8 @@ 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 { 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 { @@ -30,6 +30,7 @@ export class MigrateFromLegacyEncryptionService { private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, + private emergencyAccessService: EmergencyAccessService, private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, @@ -102,31 +103,8 @@ export class MigrateFromLegacyEncryptionService { * on the server. * @param newUserKey The new user key */ - async updateEmergencyAccesses(newUserKey: UserKey) { - const emergencyAccess = await this.apiService.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.apiService.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 00cbffa1fe..0bedb41d29 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -12,8 +12,6 @@ import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/commo 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 { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type"; -import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request"; @@ -25,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"; @@ -40,6 +34,8 @@ import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/ciph import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request"; import { DialogService } from "@bitwarden/components"; +import { EmergencyAccessService } from "../emergency-access"; + @Component({ selector: "app-change-password", templateUrl: "change-password.component.html", @@ -65,6 +61,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { private folderService: FolderService, private cipherService: CipherService, private syncService: SyncService, + private emergencyAccessService: EmergencyAccessService, private apiService: ApiService, private sendService: SendService, private organizationService: OrganizationService, @@ -267,36 +264,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.apiService.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.apiService.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-attachments.component.ts b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts similarity index 96% rename from apps/web/src/app/auth/settings/emergency-access/emergency-access-attachments.component.ts rename to apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts index 77ed083e57..d502a9f644 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-attachments.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts @@ -14,7 +14,7 @@ import { DialogService } from "@bitwarden/components"; @Component({ selector: "emergency-access-attachments", - templateUrl: "../../../vault/individual-vault/attachments.component.html", + templateUrl: "../../../../vault/individual-vault/attachments.component.html", }) export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent { viewOnly = true; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-confirm.component.html b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.html similarity index 100% rename from apps/web/src/app/auth/settings/emergency-access/emergency-access-confirm.component.html rename to apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.html diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts similarity index 100% rename from apps/web/src/app/auth/settings/emergency-access/emergency-access-confirm.component.ts rename to apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts 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 2406f7d061..55646d3467 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,13 +1,12 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EmergencyAccessType } from "@bitwarden/common/auth/enums/emergency-access-type"; -import { EmergencyAccessInviteRequest } from "@bitwarden/common/auth/models/request/emergency-access-invite.request"; -import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request"; 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 { EmergencyAccessService } from "../../emergency-access"; +import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; + @Component({ selector: "emergency-access-add-edit", templateUrl: "emergency-access-add-edit.component.html", @@ -32,7 +31,7 @@ export class EmergencyAccessAddEditComponent implements OnInit { waitTime: number; constructor( - private apiService: ApiService, + private emergencyAccessService: EmergencyAccessService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private logService: LogService @@ -54,7 +53,9 @@ export class EmergencyAccessAddEditComponent implements OnInit { this.editMode = true; this.title = this.i18nService.t("editEmergencyContact"); try { - const emergencyAccess = await this.apiService.getEmergencyAccess(this.emergencyAccessId); + const emergencyAccess = await this.emergencyAccessService.getEmergencyAccess( + this.emergencyAccessId + ); this.type = emergencyAccess.type; this.waitTime = emergencyAccess.waitTimeDays; } catch (e) { @@ -71,18 +72,9 @@ export class EmergencyAccessAddEditComponent implements OnInit { async submit() { try { if (this.editMode) { - const request = new EmergencyAccessUpdateRequest(); - request.type = this.type; - request.waitTimeDays = this.waitTime; - - this.formPromise = this.apiService.putEmergencyAccess(this.emergencyAccessId, request); + await this.emergencyAccessService.update(this.emergencyAccessId, this.type, this.waitTime); } else { - const request = new EmergencyAccessInviteRequest(); - request.email = this.email.trim(); - request.type = this.type; - request.waitTimeDays = this.waitTime; - - this.formPromise = this.apiService.postEmergencyAccessInvite(request); + await this.emergencyAccessService.invite(this.email, this.type, this.waitTime); } await this.formPromise; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index 06b5110222..f42d276acf 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -2,27 +2,25 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type"; -import { EmergencyAccessType } from "@bitwarden/common/auth/enums/emergency-access-type"; -import { EmergencyAccessConfirmRequest } from "@bitwarden/common/auth/models/request/emergency-access-confirm.request"; -import { - EmergencyAccessGranteeDetailsResponse, - EmergencyAccessGrantorDetailsResponse, -} from "@bitwarden/common/auth/models/response/emergency-access.response"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; +import { EmergencyAccessService } from "../../emergency-access"; +import { EmergencyAccessStatusType } from "../../emergency-access/enums/emergency-access-status-type"; +import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; +import { + GranteeEmergencyAccess, + GrantorEmergencyAccess, +} from "../../emergency-access/models/emergency-access"; + +import { EmergencyAccessConfirmComponent } from "./confirm/emergency-access-confirm.component"; import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component"; -import { EmergencyAccessConfirmComponent } from "./emergency-access-confirm.component"; -import { EmergencyAccessTakeoverComponent } from "./emergency-access-takeover.component"; +import { EmergencyAccessTakeoverComponent } from "./takeover/emergency-access-takeover.component"; @Component({ selector: "emergency-access", @@ -38,19 +36,18 @@ export class EmergencyAccessComponent implements OnInit { loaded = false; canAccessPremium: boolean; - trustedContacts: EmergencyAccessGranteeDetailsResponse[]; - grantedContacts: EmergencyAccessGrantorDetailsResponse[]; + trustedContacts: GranteeEmergencyAccess[]; + grantedContacts: GrantorEmergencyAccess[]; emergencyAccessType = EmergencyAccessType; emergencyAccessStatusType = EmergencyAccessStatusType; actionPromise: Promise; isOrganizationOwner: boolean; constructor( - private apiService: ApiService, + private emergencyAccessService: EmergencyAccessService, private i18nService: I18nService, private modalService: ModalService, private platformUtilsService: PlatformUtilsService, - private cryptoService: CryptoService, private messagingService: MessagingService, private userNamePipe: UserNamePipe, private logService: LogService, @@ -67,8 +64,8 @@ export class EmergencyAccessComponent implements OnInit { } async load() { - this.trustedContacts = (await this.apiService.getEmergencyAccessTrusted()).data; - this.grantedContacts = (await this.apiService.getEmergencyAccessGranted()).data; + this.trustedContacts = await this.emergencyAccessService.getEmergencyAccessTrusted(); + this.grantedContacts = await this.emergencyAccessService.getEmergencyAccessGranted(); this.loaded = true; } @@ -79,7 +76,7 @@ export class EmergencyAccessComponent implements OnInit { } } - async edit(details: EmergencyAccessGranteeDetailsResponse) { + async edit(details: GranteeEmergencyAccess) { const [modal] = await this.modalService.openViewRef( EmergencyAccessAddEditComponent, this.addEditModalRef, @@ -105,11 +102,11 @@ export class EmergencyAccessComponent implements OnInit { this.edit(null); } - async reinvite(contact: EmergencyAccessGranteeDetailsResponse) { + async reinvite(contact: GranteeEmergencyAccess) { if (this.actionPromise != null) { return; } - this.actionPromise = this.apiService.postEmergencyAccessReinvite(contact.id); + this.actionPromise = this.emergencyAccessService.reinvite(contact.id); await this.actionPromise; this.platformUtilsService.showToast( "success", @@ -119,7 +116,7 @@ export class EmergencyAccessComponent implements OnInit { this.actionPromise = null; } - async confirm(contact: EmergencyAccessGranteeDetailsResponse) { + async confirm(contact: GranteeEmergencyAccess) { function updateUser() { contact.status = EmergencyAccessStatusType.Confirmed; } @@ -141,7 +138,7 @@ export class EmergencyAccessComponent implements OnInit { comp.onConfirmed.subscribe(async () => { modal.close(); - comp.formPromise = this.doConfirmation(contact); + comp.formPromise = this.emergencyAccessService.confirm(contact.id, contact.granteeId); await comp.formPromise; updateUser(); @@ -156,7 +153,7 @@ export class EmergencyAccessComponent implements OnInit { return; } - this.actionPromise = this.doConfirmation(contact); + this.actionPromise = this.emergencyAccessService.confirm(contact.id, contact.granteeId); await this.actionPromise; updateUser(); @@ -168,9 +165,7 @@ export class EmergencyAccessComponent implements OnInit { this.actionPromise = null; } - async remove( - details: EmergencyAccessGranteeDetailsResponse | EmergencyAccessGrantorDetailsResponse - ) { + async remove(details: GranteeEmergencyAccess | GrantorEmergencyAccess) { const confirmed = await this.dialogService.openSimpleDialog({ title: this.userNamePipe.transform(details), content: { key: "removeUserConfirmation" }, @@ -182,14 +177,14 @@ export class EmergencyAccessComponent implements OnInit { } try { - await this.apiService.deleteEmergencyAccess(details.id); + await this.emergencyAccessService.delete(details.id); this.platformUtilsService.showToast( "success", null, this.i18nService.t("removedUserId", this.userNamePipe.transform(details)) ); - if (details instanceof EmergencyAccessGranteeDetailsResponse) { + if (details instanceof GranteeEmergencyAccess) { this.removeGrantee(details); } else { this.removeGrantor(details); @@ -199,7 +194,7 @@ export class EmergencyAccessComponent implements OnInit { } } - async requestAccess(details: EmergencyAccessGrantorDetailsResponse) { + async requestAccess(details: GrantorEmergencyAccess) { const confirmed = await this.dialogService.openSimpleDialog({ title: this.userNamePipe.transform(details), content: { @@ -214,7 +209,7 @@ export class EmergencyAccessComponent implements OnInit { return false; } - await this.apiService.postEmergencyAccessInitiate(details.id); + await this.emergencyAccessService.requestAccess(details.id); details.status = EmergencyAccessStatusType.RecoveryInitiated; this.platformUtilsService.showToast( @@ -224,7 +219,7 @@ export class EmergencyAccessComponent implements OnInit { ); } - async approve(details: EmergencyAccessGranteeDetailsResponse) { + async approve(details: GranteeEmergencyAccess) { const type = this.i18nService.t( details.type === EmergencyAccessType.View ? "view" : "takeover" ); @@ -243,7 +238,7 @@ export class EmergencyAccessComponent implements OnInit { return false; } - await this.apiService.postEmergencyAccessApprove(details.id); + await this.emergencyAccessService.approve(details.id); details.status = EmergencyAccessStatusType.RecoveryApproved; this.platformUtilsService.showToast( @@ -253,8 +248,8 @@ export class EmergencyAccessComponent implements OnInit { ); } - async reject(details: EmergencyAccessGranteeDetailsResponse) { - await this.apiService.postEmergencyAccessReject(details.id); + async reject(details: GranteeEmergencyAccess) { + await this.emergencyAccessService.reject(details.id); details.status = EmergencyAccessStatusType.Confirmed; this.platformUtilsService.showToast( @@ -264,7 +259,7 @@ export class EmergencyAccessComponent implements OnInit { ); } - async takeover(details: EmergencyAccessGrantorDetailsResponse) { + async takeover(details: GrantorEmergencyAccess) { const [modal] = await this.modalService.openViewRef( EmergencyAccessTakeoverComponent, this.takeoverModalRef, @@ -286,41 +281,17 @@ export class EmergencyAccessComponent implements OnInit { ); } - private removeGrantee(details: EmergencyAccessGranteeDetailsResponse) { + private removeGrantee(details: GranteeEmergencyAccess) { const index = this.trustedContacts.indexOf(details); if (index > -1) { this.trustedContacts.splice(index, 1); } } - private removeGrantor(details: EmergencyAccessGrantorDetailsResponse) { + private removeGrantor(details: GrantorEmergencyAccess) { const index = this.grantedContacts.indexOf(details); if (index > -1) { this.grantedContacts.splice(index, 1); } } - - // Encrypt the user key with the grantees public key, and send it to bitwarden for escrow. - private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) { - const userKey = await this.cryptoService.getUserKey(); - if (!userKey) { - throw new Error("No user key found"); - } - const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId); - const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - - try { - this.logService.debug( - "User's fingerprint: " + - (await this.cryptoService.getFingerprint(details.granteeId, publicKey)).join("-") - ); - } catch { - // Ignore errors since it's just a debug message - } - - const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); - const request = new EmergencyAccessConfirmRequest(); - request.key = encryptedKey.encryptedString; - await this.apiService.postEmergencyAccessConfirm(details.id, request); - } } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.html b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.html similarity index 100% rename from apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.html rename to apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.html 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/takeover/emergency-access-takeover.component.ts similarity index 54% rename from apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.ts rename to apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index db9917bb93..cbf32b8698 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -2,13 +2,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angu import { takeUntil } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { EmergencyAccessPasswordRequest } from "@bitwarden/common/auth/models/request/emergency-access-password.request"; 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,13 +10,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - SymmetricCryptoKey, - UserKey, -} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; +import { EmergencyAccessService } from "../../../emergency-access"; + @Component({ selector: "emergency-access-takeover", templateUrl: "emergency-access-takeover.component.html", @@ -49,7 +41,7 @@ export class EmergencyAccessTakeoverComponent passwordGenerationService: PasswordGenerationServiceAbstraction, platformUtilsService: PlatformUtilsService, policyService: PolicyService, - private apiService: ApiService, + private emergencyAccessService: EmergencyAccessService, private logService: LogService, dialogService: DialogService ) { @@ -66,17 +58,11 @@ export class EmergencyAccessTakeoverComponent } async ngOnInit() { - const response = await this.apiService.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 @@ -89,46 +75,20 @@ export class EmergencyAccessTakeoverComponent return; } - const takeoverResponse = await this.apiService.postEmergencyAccessTakeover( - this.emergencyAccessId - ); - - const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted); - const oldUserKey = new SymmetricCryptoKey(oldKeyBuffer) as UserKey; - - if (oldUserKey == null) { + try { + await this.emergencyAccessService.takeover( + this.emergencyAccessId, + this.masterPassword, + this.email + ); + this.onDone.emit(); + } catch (e) { + this.logService.error(e); this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), this.i18nService.t("unexpectedError") ); - return; - } - - const masterKey = await this.cryptoService.makeMasterKey( - this.masterPassword, - this.email, - takeoverResponse.kdf, - new KdfConfig( - takeoverResponse.kdfIterations, - takeoverResponse.kdfMemory, - takeoverResponse.kdfParallelism - ) - ); - const masterKeyHash = await this.cryptoService.hashMasterKey(this.masterPassword, masterKey); - - const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, oldUserKey); - - const request = new EmergencyAccessPasswordRequest(); - request.newMasterPasswordHash = masterKeyHash; - request.key = encKey[1].encryptedString; - - this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request); - - try { - this.onDone.emit(); - } catch (e) { - this.logService.error(e); } } } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-view.component.html b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html similarity index 100% rename from apps/web/src/app/auth/settings/emergency-access/emergency-access-view.component.html rename to apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-view.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts similarity index 51% rename from apps/web/src/app/auth/settings/emergency-access/emergency-access-view.component.ts rename to apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts index f7936d361b..a0b6cebf71 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-view.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts @@ -2,20 +2,12 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EmergencyAccessViewResponse } from "@bitwarden/common/auth/models/response/emergency-access.response"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { - SymmetricCryptoKey, - UserKey, -} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { EmergencyAccessAttachmentsComponent } from "./emergency-access-attachments.component"; -import { EmergencyAddEditComponent } from "./emergency-add-edit.component"; +import { EmergencyAccessService } from "../../../emergency-access"; +import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component"; + +import { EmergencyAddEditCipherComponent } from "./emergency-add-edit-cipher.component"; @Component({ selector: "emergency-access-view", @@ -33,12 +25,10 @@ export class EmergencyAccessViewComponent implements OnInit { loaded = false; constructor( - private cipherService: CipherService, - private cryptoService: CryptoService, private modalService: ModalService, private router: Router, private route: ActivatedRoute, - private apiService: ApiService + private emergencyAccessService: EmergencyAccessService ) {} ngOnInit() { @@ -57,7 +47,7 @@ export class EmergencyAccessViewComponent implements OnInit { async selectCipher(cipher: CipherView) { // eslint-disable-next-line const [_, childComponent] = await this.modalService.openViewRef( - EmergencyAddEditComponent, + EmergencyAddEditCipherComponent, this.cipherAddEditModalRef, (comp) => { comp.cipherId = cipher == null ? null : cipher.id; @@ -69,8 +59,7 @@ export class EmergencyAccessViewComponent implements OnInit { } async load() { - const response = await this.apiService.postEmergencyAccessView(this.id); - this.ciphers = await this.getAllCiphers(response); + this.ciphers = await this.emergencyAccessService.getViewOnlyCiphers(this.id); this.loaded = true; } @@ -84,24 +73,4 @@ export class EmergencyAccessViewComponent implements OnInit { } ); } - - protected async getAllCiphers(response: EmergencyAccessViewResponse): Promise { - const ciphers = response.ciphers; - - const decCiphers: CipherView[] = []; - const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted); - const oldUserKey = new SymmetricCryptoKey(oldKeyBuffer) as UserKey; - - const promises: any[] = []; - ciphers.forEach((cipherResponse) => { - const cipherData = new CipherData(cipherResponse); - const cipher = new Cipher(cipherData); - promises.push(cipher.decrypt(oldUserKey).then((c) => decCiphers.push(c))); - }); - - await Promise.all(promises); - decCiphers.sort(this.cipherService.getLocaleSortingFunction()); - - return decCiphers; - } } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts similarity index 94% rename from apps/web/src/app/auth/settings/emergency-access/emergency-add-edit.component.ts rename to apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts index 0e82a5c76a..91de00f43f 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts @@ -20,13 +20,13 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; -import { AddEditComponent as BaseAddEditComponent } from "../../../vault/individual-vault/add-edit.component"; +import { AddEditComponent as BaseAddEditComponent } from "../../../../vault/individual-vault/add-edit.component"; @Component({ selector: "app-org-vault-add-edit", - templateUrl: "../../../vault/individual-vault/add-edit.component.html", + templateUrl: "../../../../vault/individual-vault/add-edit.component.html", }) -export class EmergencyAddEditComponent extends BaseAddEditComponent { +export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { originalCipher: Cipher = null; viewOnly = true; protected override componentName = "app-org-vault-add-edit"; diff --git a/apps/web/src/app/auth/settings/settings.module.ts b/apps/web/src/app/auth/settings/settings.module.ts index 12ae6bcbf5..5e723b3d41 100644 --- a/apps/web/src/app/auth/settings/settings.module.ts +++ b/apps/web/src/app/auth/settings/settings.module.ts @@ -3,12 +3,18 @@ import { NgModule } from "@angular/core"; import { PasswordCalloutComponent } from "@bitwarden/auth"; import { SharedModule } from "../../shared"; +import { EmergencyAccessModule } from "../emergency-access"; import { ChangePasswordComponent } from "./change-password.component"; import { WebauthnLoginSettingsModule } from "./webauthn-login-settings"; @NgModule({ - imports: [SharedModule, WebauthnLoginSettingsModule, PasswordCalloutComponent], + imports: [ + SharedModule, + WebauthnLoginSettingsModule, + EmergencyAccessModule, + PasswordCalloutComponent, + ], declarations: [ChangePasswordComponent], providers: [], exports: [ChangePasswordComponent], diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 4f76e4f59c..386b9b3023 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -17,7 +17,6 @@ import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component"; -import { AcceptEmergencyComponent } from "./auth/accept-emergency.component"; import { AcceptOrganizationComponent } from "./auth/accept-organization.component"; import { HintComponent } from "./auth/hint.component"; import { LockComponent } from "./auth/lock.component"; @@ -28,8 +27,8 @@ import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; import { RemovePasswordComponent } from "./auth/remove-password.component"; import { SetPasswordComponent } from "./auth/set-password.component"; -import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/emergency-access-view.component"; import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component"; +import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component"; import { SsoComponent } from "./auth/sso.component"; import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component"; import { TwoFactorComponent } from "./auth/two-factor.component"; @@ -124,8 +123,11 @@ const routes: Routes = [ }, { path: "accept-emergency", - component: AcceptEmergencyComponent, data: { titleId: "acceptEmergency", doNotSaveUrl: false }, + loadComponent: () => + import("./auth/emergency-access/accept/accept-emergency.component").then( + (mod) => mod.AcceptEmergencyComponent + ), }, { path: "accept-families-for-enterprise", diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index f55cf72551..091f8ec68b 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -16,7 +16,6 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from import { ProvidersComponent } from "../admin-console/providers/providers.component"; import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component"; -import { AcceptEmergencyComponent } from "../auth/accept-emergency.component"; import { AcceptOrganizationComponent } from "../auth/accept-organization.component"; import { HintComponent } from "../auth/hint.component"; import { LockComponent } from "../auth/lock.component"; @@ -26,13 +25,13 @@ import { RegisterFormModule } from "../auth/register-form/register-form.module"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { DeauthorizeSessionsComponent } from "../auth/settings/deauthorize-sessions.component"; +import { EmergencyAccessAttachmentsComponent } from "../auth/settings/emergency-access/attachments/emergency-access-attachments.component"; +import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component"; import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component"; -import { EmergencyAccessAttachmentsComponent } from "../auth/settings/emergency-access/emergency-access-attachments.component"; -import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/emergency-access-confirm.component"; -import { EmergencyAccessTakeoverComponent } from "../auth/settings/emergency-access/emergency-access-takeover.component"; -import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/emergency-access-view.component"; import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component"; -import { EmergencyAddEditComponent } from "../auth/settings/emergency-access/emergency-add-edit.component"; +import { EmergencyAccessTakeoverComponent } from "../auth/settings/emergency-access/takeover/emergency-access-takeover.component"; +import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/view/emergency-access-view.component"; +import { EmergencyAddEditCipherComponent } from "../auth/settings/emergency-access/view/emergency-add-edit-cipher.component"; import { TwoFactorAuthenticatorComponent } from "../auth/settings/two-factor-authenticator.component"; import { TwoFactorDuoComponent } from "../auth/settings/two-factor-duo.component"; import { TwoFactorEmailComponent } from "../auth/settings/two-factor-email.component"; @@ -107,7 +106,6 @@ import { SharedModule } from "./shared.module"; PasswordCalloutComponent, ], declarations: [ - AcceptEmergencyComponent, AcceptFamilySponsorshipComponent, AcceptOrganizationComponent, AccessComponent, @@ -128,7 +126,7 @@ import { SharedModule } from "./shared.module"; EmergencyAccessConfirmComponent, EmergencyAccessTakeoverComponent, EmergencyAccessViewComponent, - EmergencyAddEditComponent, + EmergencyAddEditCipherComponent, FolderAddEditComponent, FooterComponent, FrontendLayoutComponent, @@ -192,7 +190,6 @@ import { SharedModule } from "./shared.module"; exports: [ UserVerificationModule, PremiumBadgeComponent, - AcceptEmergencyComponent, AcceptOrganizationComponent, AccessComponent, AccountComponent, @@ -213,7 +210,7 @@ import { SharedModule } from "./shared.module"; EmergencyAccessConfirmComponent, EmergencyAccessTakeoverComponent, EmergencyAccessViewComponent, - EmergencyAddEditComponent, + EmergencyAddEditCipherComponent, FolderAddEditComponent, FooterComponent, FrontendLayoutComponent, diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 24151425ee..1a8ca440d2 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -19,7 +19,6 @@ import { } from "../admin-console/models/response/organization-connection.response"; import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response"; import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response"; -import { PolicyResponse } from "../admin-console/models/response/policy.response"; import { ProviderOrganizationOrganizationDetailsResponse, ProviderOrganizationResponse, @@ -36,11 +35,6 @@ import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; import { EmailTokenRequest } from "../auth/models/request/email-token.request"; import { EmailRequest } from "../auth/models/request/email.request"; -import { EmergencyAccessAcceptRequest } from "../auth/models/request/emergency-access-accept.request"; -import { EmergencyAccessConfirmRequest } from "../auth/models/request/emergency-access-confirm.request"; -import { EmergencyAccessInviteRequest } from "../auth/models/request/emergency-access-invite.request"; -import { EmergencyAccessPasswordRequest } from "../auth/models/request/emergency-access-password.request"; -import { EmergencyAccessUpdateRequest } from "../auth/models/request/emergency-access-update.request"; import { PasswordTokenRequest } from "../auth/models/request/identity-token/password-token.request"; import { SsoTokenRequest } from "../auth/models/request/identity-token/sso-token.request"; import { UserApiTokenRequest } from "../auth/models/request/identity-token/user-api-token.request"; @@ -65,12 +59,6 @@ import { UpdateTwoFactorYubioOtpRequest } from "../auth/models/request/update-tw import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; -import { - EmergencyAccessGranteeDetailsResponse, - EmergencyAccessGrantorDetailsResponse, - EmergencyAccessTakeoverResponse, - EmergencyAccessViewResponse, -} from "../auth/models/response/emergency-access.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; @@ -365,25 +353,6 @@ export abstract class ApiService { request: DeviceVerificationRequest ) => Promise; - getEmergencyAccessTrusted: () => Promise>; - getEmergencyAccessGranted: () => Promise>; - getEmergencyAccess: (id: string) => Promise; - getEmergencyGrantorPolicies: (id: string) => Promise>; - putEmergencyAccess: (id: string, request: EmergencyAccessUpdateRequest) => Promise; - deleteEmergencyAccess: (id: string) => Promise; - postEmergencyAccessInvite: (request: EmergencyAccessInviteRequest) => Promise; - postEmergencyAccessReinvite: (id: string) => Promise; - postEmergencyAccessAccept: (id: string, request: EmergencyAccessAcceptRequest) => Promise; - postEmergencyAccessConfirm: (id: string, request: EmergencyAccessConfirmRequest) => Promise; - postEmergencyAccessInitiate: (id: string) => Promise; - postEmergencyAccessApprove: (id: string) => Promise; - postEmergencyAccessReject: (id: string) => Promise; - postEmergencyAccessTakeover: (id: string) => Promise; - postEmergencyAccessPassword: ( - id: string, - request: EmergencyAccessPasswordRequest - ) => Promise; - postEmergencyAccessView: (id: string) => Promise; getCloudCommunicationsEnabled: () => Promise; abstract getOrganizationConnection( id: string, diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 58be8afce9..51ffdcff88 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -20,7 +20,6 @@ import { } from "../admin-console/models/response/organization-connection.response"; import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response"; import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response"; -import { PolicyResponse } from "../admin-console/models/response/policy.response"; import { ProviderOrganizationOrganizationDetailsResponse, ProviderOrganizationResponse, @@ -38,11 +37,6 @@ import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; import { EmailTokenRequest } from "../auth/models/request/email-token.request"; import { EmailRequest } from "../auth/models/request/email.request"; -import { EmergencyAccessAcceptRequest } from "../auth/models/request/emergency-access-accept.request"; -import { EmergencyAccessConfirmRequest } from "../auth/models/request/emergency-access-confirm.request"; -import { EmergencyAccessInviteRequest } from "../auth/models/request/emergency-access-invite.request"; -import { EmergencyAccessPasswordRequest } from "../auth/models/request/emergency-access-password.request"; -import { EmergencyAccessUpdateRequest } from "../auth/models/request/emergency-access-update.request"; import { DeviceRequest } from "../auth/models/request/identity-token/device.request"; import { PasswordTokenRequest } from "../auth/models/request/identity-token/password-token.request"; import { SsoTokenRequest } from "../auth/models/request/identity-token/sso-token.request"; @@ -69,12 +63,6 @@ import { UpdateTwoFactorYubioOtpRequest } from "../auth/models/request/update-tw import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; -import { - EmergencyAccessGranteeDetailsResponse, - EmergencyAccessGrantorDetailsResponse, - EmergencyAccessTakeoverResponse, - EmergencyAccessViewResponse, -} from "../auth/models/response/emergency-access.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; @@ -1123,81 +1111,6 @@ export class ApiService implements ApiServiceAbstraction { return new DeviceVerificationResponse(r); } - // Emergency Access APIs - - async getEmergencyAccessTrusted(): Promise> { - const r = await this.send("GET", "/emergency-access/trusted", null, true, true); - return new ListResponse(r, EmergencyAccessGranteeDetailsResponse); - } - - async getEmergencyAccessGranted(): Promise> { - const r = await this.send("GET", "/emergency-access/granted", null, true, true); - return new ListResponse(r, EmergencyAccessGrantorDetailsResponse); - } - - async getEmergencyAccess(id: string): Promise { - const r = await this.send("GET", "/emergency-access/" + id, null, true, true); - return new EmergencyAccessGranteeDetailsResponse(r); - } - - async getEmergencyGrantorPolicies(id: string): Promise> { - const r = await this.send("GET", "/emergency-access/" + id + "/policies", null, true, true); - return new ListResponse(r, PolicyResponse); - } - - putEmergencyAccess(id: string, request: EmergencyAccessUpdateRequest): Promise { - return this.send("PUT", "/emergency-access/" + id, request, true, false); - } - - deleteEmergencyAccess(id: string): Promise { - return this.send("DELETE", "/emergency-access/" + id, null, true, false); - } - - postEmergencyAccessInvite(request: EmergencyAccessInviteRequest): Promise { - return this.send("POST", "/emergency-access/invite", request, true, false); - } - - postEmergencyAccessReinvite(id: string): Promise { - return this.send("POST", "/emergency-access/" + id + "/reinvite", null, true, false); - } - - postEmergencyAccessAccept(id: string, request: EmergencyAccessAcceptRequest): Promise { - return this.send("POST", "/emergency-access/" + id + "/accept", request, true, false); - } - - postEmergencyAccessConfirm(id: string, request: EmergencyAccessConfirmRequest): Promise { - return this.send("POST", "/emergency-access/" + id + "/confirm", request, true, false); - } - - postEmergencyAccessInitiate(id: string): Promise { - return this.send("POST", "/emergency-access/" + id + "/initiate", null, true, false); - } - - postEmergencyAccessApprove(id: string): Promise { - return this.send("POST", "/emergency-access/" + id + "/approve", null, true, false); - } - - postEmergencyAccessReject(id: string): Promise { - return this.send("POST", "/emergency-access/" + id + "/reject", null, true, false); - } - - async postEmergencyAccessTakeover(id: string): Promise { - const r = await this.send("POST", "/emergency-access/" + id + "/takeover", null, true, true); - return new EmergencyAccessTakeoverResponse(r); - } - - async postEmergencyAccessPassword( - id: string, - request: EmergencyAccessPasswordRequest - ): Promise { - await this.send("POST", "/emergency-access/" + id + "/password", request, true, true); - } - - async postEmergencyAccessView(id: string): Promise { - const r = await this.send("POST", "/emergency-access/" + id + "/view", null, true, true); - return new EmergencyAccessViewResponse(r); - } - // Organization APIs async getCloudCommunicationsEnabled(): Promise {