diff --git a/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts b/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts index a607c46babf..bab5d76c9e2 100644 --- a/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts +++ b/apps/web/src/app/auth/core/services/emergency-access/emergency-access.service.ts @@ -1,6 +1,157 @@ import { Injectable } from "@angular/core"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type"; +import { EmergencyAccessType } from "@bitwarden/common/auth/enums/emergency-access-type"; +import { EmergencyAccessAcceptRequest } from "@bitwarden/common/auth/models/request/emergency-access-accept.request"; +import { EmergencyAccessConfirmRequest } from "@bitwarden/common/auth/models/request/emergency-access-confirm.request"; +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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { EmergencyAccessApiService } from "./emergency-access-api.service"; @Injectable() export class EmergencyAccessService { - constructor() {} + constructor( + private emergencyAccessApiService: EmergencyAccessApiService, + private apiService: ApiService, + private cryptoService: CryptoService, + private logService: LogService + ) {} + + /** + * Invites the email address to be an emergency contact + * Step 1 of the 3 step setup flow + * Performed by 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); + } + + /** + * Edits an existing emergency access + * Performed by 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 + * Performed by 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 + * Performed by 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); + } + + /** + * Requests access to grantor's vault + * Performed by grantee + * @param id emergency access id + */ + requestAccess(id: string): Promise { + return this.emergencyAccessApiService.postEmergencyAccessInitiate(id); + } + + /** + * Approves access to grantor's vault + * Performed by grantor + * @param id emergency access id + */ + approve(id: string): Promise { + return this.emergencyAccessApiService.postEmergencyAccessApprove(id); + } + + /** + * Rejects access to grantor's vault + * Performed by grantor + * @param id emergency access id + */ + reject(id: string): Promise { + return this.emergencyAccessApiService.postEmergencyAccessReject(id); + } + + async rotateEmergencyAccess(newUserKey: UserKey) { + const emergencyAccess = await this.emergencyAccessApiService.getEmergencyAccessTrusted(); + // Any Invited or Accepted requests won't have the key yet, so we don't need to update them + const allowedStatuses = new Set([ + EmergencyAccessStatusType.Confirmed, + EmergencyAccessStatusType.RecoveryInitiated, + EmergencyAccessStatusType.RecoveryApproved, + ]); + const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.has(d.status)); + + for (const details of filteredAccesses) { + // Get public key of grantee + const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId); + const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); + + // Encrypt new user key with public key + const encryptedKey = await this.cryptoService.rsaEncrypt(newUserKey.key, publicKey); + + const updateRequest = new EmergencyAccessUpdateRequest(); + updateRequest.type = details.type; + updateRequest.waitTimeDays = details.waitTimeDays; + updateRequest.keyEncrypted = encryptedKey.encryptedString; + + await this.emergencyAccessApiService.putEmergencyAccess(details.id, updateRequest); + } + } + + 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/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept-emergency.component.ts index f65df3d2600..8ce56343bca 100644 --- a/apps/web/src/app/auth/emergency-access/accept-emergency.component.ts +++ b/apps/web/src/app/auth/emergency-access/accept-emergency.component.ts @@ -1,13 +1,12 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { EmergencyAccessAcceptRequest } from "@bitwarden/common/auth/models/request/emergency-access-accept.request"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BaseAcceptComponent } from "../../common/base.accept.component"; import { StateService } from "../../core"; import { I18nService } from "../../core/i18n.service"; -import { EmergencyAccessApiService } from "../core/services/emergency-access/emergency-access-api.service"; +import { EmergencyAccessService } from "../core/services/emergency-access/emergency-access.service"; @Component({ standalone: true, @@ -25,19 +24,14 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, route: ActivatedRoute, - private emergencyAccessApiService: EmergencyAccessApiService, - 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.emergencyAccessApiService.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/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 12be3d51ec1..b13fb0d335b 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,12 +1,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; 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 { EmergencyAccessApiService } from "../../core/services/emergency-access/emergency-access-api.service"; +import { EmergencyAccessService } from "../../core/services/emergency-access/emergency-access.service"; @Component({ selector: "emergency-access-add-edit", @@ -33,6 +32,7 @@ export class EmergencyAccessAddEditComponent implements OnInit { constructor( private emergencyAccessApiService: EmergencyAccessApiService, + private emergencyAccessService: EmergencyAccessService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private logService: LogService @@ -73,21 +73,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.emergencyAccessApiService.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.emergencyAccessApiService.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 72f4969e636..9ed0f510c20 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,24 +2,21 @@ 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 { EmergencyAccessApiService } from "../../core/services/emergency-access/emergency-access-api.service"; +import { EmergencyAccessService } from "../../core/services/emergency-access/emergency-access.service"; import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component"; import { EmergencyAccessConfirmComponent } from "./emergency-access-confirm.component"; @@ -48,11 +45,10 @@ export class EmergencyAccessComponent implements OnInit { constructor( private emergencyAccessApiService: EmergencyAccessApiService, - 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, @@ -143,7 +139,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(); @@ -158,7 +154,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(); @@ -216,7 +212,7 @@ export class EmergencyAccessComponent implements OnInit { return false; } - await this.emergencyAccessApiService.postEmergencyAccessInitiate(details.id); + await this.emergencyAccessService.requestAccess(details.id); details.status = EmergencyAccessStatusType.RecoveryInitiated; this.platformUtilsService.showToast( @@ -245,7 +241,7 @@ export class EmergencyAccessComponent implements OnInit { return false; } - await this.emergencyAccessApiService.postEmergencyAccessApprove(details.id); + await this.emergencyAccessService.approve(details.id); details.status = EmergencyAccessStatusType.RecoveryApproved; this.platformUtilsService.showToast( @@ -256,7 +252,7 @@ export class EmergencyAccessComponent implements OnInit { } async reject(details: EmergencyAccessGranteeDetailsResponse) { - await this.emergencyAccessApiService.postEmergencyAccessReject(details.id); + await this.emergencyAccessService.reject(details.id); details.status = EmergencyAccessStatusType.Confirmed; this.platformUtilsService.showToast( @@ -301,28 +297,4 @@ export class EmergencyAccessComponent implements OnInit { 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.emergencyAccessApiService.postEmergencyAccessConfirm(details.id, request); - } }