From 573eea66eeff1a763e7ed0477fbba51e02c2c5b1 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Dec 2020 16:53:48 +0100 Subject: [PATCH] Add support for Emergency Access (#204) * Add support for Emergency Access * Resolve review comments --- src/abstractions/api.service.ts | 27 +++++ src/abstractions/crypto.service.ts | 3 +- .../components/change-password.component.ts | 80 +++++++------ src/enums/emergencyAccessStatusType.ts | 7 ++ src/enums/emergencyAccessType.ts | 5 + src/models/domain/attachment.ts | 6 +- src/models/domain/card.ts | 5 +- src/models/domain/cipher.ts | 19 +-- src/models/domain/field.ts | 5 +- src/models/domain/identity.ts | 5 +- src/models/domain/login.ts | 7 +- src/models/domain/loginUri.ts | 5 +- src/models/domain/password.ts | 5 +- src/models/domain/secureNote.ts | 3 +- .../request/emergencyAccessAcceptRequest.ts | 3 + .../request/emergencyAccessConfirmRequest.ts | 3 + .../request/emergencyAccessInviteRequest.ts | 7 ++ .../request/emergencyAccessPasswordRequest.ts | 4 + .../request/emergencyAccessUpdateRequest.ts | 7 ++ .../response/emergencyAccessResponse.ts | 81 +++++++++++++ src/services/api.service.ts | 78 +++++++++++++ src/services/crypto.service.ts | 110 +++++++++--------- 22 files changed, 358 insertions(+), 117 deletions(-) create mode 100644 src/enums/emergencyAccessStatusType.ts create mode 100644 src/enums/emergencyAccessType.ts create mode 100644 src/models/request/emergencyAccessAcceptRequest.ts create mode 100644 src/models/request/emergencyAccessConfirmRequest.ts create mode 100644 src/models/request/emergencyAccessInviteRequest.ts create mode 100644 src/models/request/emergencyAccessPasswordRequest.ts create mode 100644 src/models/request/emergencyAccessUpdateRequest.ts create mode 100644 src/models/response/emergencyAccessResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 65b8356074..955041f281 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -15,6 +15,11 @@ import { CollectionRequest } from '../models/request/collectionRequest'; import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; +import { EmergencyAccessAcceptRequest } from '../models/request/emergencyAccessAcceptRequest'; +import { EmergencyAccessConfirmRequest } from '../models/request/emergencyAccessConfirmRequest'; +import { EmergencyAccessInviteRequest } from '../models/request/emergencyAccessInviteRequest'; +import { EmergencyAccessPasswordRequest } from '../models/request/emergencyAccessPasswordRequest'; +import { EmergencyAccessUpdateRequest } from '../models/request/emergencyAccessUpdateRequest'; import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; @@ -73,6 +78,12 @@ import { CollectionResponse, } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; +import { + EmergencyAccessGranteeDetailsResponse, + EmergencyAccessGrantorDetailsResponse, + EmergencyAccessTakeoverResponse, + EmergencyAccessViewResponse +} from '../models/response/emergencyAccessResponse'; import { EventResponse } from '../models/response/eventResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { @@ -278,6 +289,22 @@ export abstract class ApiService { postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + getEmergencyAccessTrusted: () => Promise>; + getEmergencyAccessGranted: () => Promise>; + getEmergencyAccess: (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; + getOrganization: (id: string) => Promise; getOrganizationBilling: (id: string) => Promise; getOrganizationSubscription: (id: string) => Promise; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 53376e19e9..6fa312fc51 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -38,10 +38,11 @@ export abstract class CryptoService { makeSendKey: (keyMaterial: ArrayBuffer) => Promise; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; - remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; + remakeEncKey: (key: SymmetricCryptoKey, encKey?: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise; + rsaDecrypt: (encValue: string) => Promise; decryptToBytes: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; diff --git a/src/angular/components/change-password.component.ts b/src/angular/components/change-password.component.ts index f4ec66e31e..eaab13f7ac 100644 --- a/src/angular/components/change-password.component.ts +++ b/src/angular/components/change-password.component.ts @@ -21,11 +21,11 @@ export class ChangePasswordComponent implements OnInit { masterPasswordScore: number; enforcedPolicyOptions: MasterPasswordPolicyOptions; + protected email: string; protected kdf: KdfType; protected kdfIterations: number; private masterPasswordStrengthTimeout: any; - private email: string; constructor(protected i18nService: I18nService, protected cryptoService: CryptoService, protected messagingService: MessagingService, protected userService: UserService, @@ -58,43 +58,9 @@ export class ChangePasswordComponent implements OnInit { } async submit() { - if (this.masterPassword == null || this.masterPassword === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); + if (!await this.strongPassword()) { return; } - if (this.masterPassword.length < 8) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassLength')); - return; - } - if (this.masterPassword !== this.masterPasswordRetype) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassDoesntMatch')); - return; - } - - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, - this.getPasswordStrengthUserInput()); - - if (this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - strengthResult.score, - this.masterPassword, - this.enforcedPolicyOptions)) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); - return; - } - - if (strengthResult != null && strengthResult.score < 3) { - const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), - this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), - 'warning'); - if (!result) { - return; - } - } if (!await this.setupSubmitActions()) { return; @@ -133,6 +99,48 @@ export class ChangePasswordComponent implements OnInit { // Override in sub-class } + async strongPassword(): Promise { + if (this.masterPassword == null || this.masterPassword === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return false; + } + if (this.masterPassword.length < 8) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassLength')); + return false; + } + if (this.masterPassword !== this.masterPasswordRetype) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassDoesntMatch')); + return false; + } + + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, + this.getPasswordStrengthUserInput()); + + if (this.enforcedPolicyOptions != null && + !this.policyService.evaluateMasterPassword( + strengthResult.score, + this.masterPassword, + this.enforcedPolicyOptions)) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); + return false; + } + + if (strengthResult != null && strengthResult.score < 3) { + const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), + this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), + 'warning'); + if (!result) { + return false; + } + } + + return true; + } + updatePasswordStrength() { if (this.masterPasswordStrengthTimeout != null) { clearTimeout(this.masterPasswordStrengthTimeout); diff --git a/src/enums/emergencyAccessStatusType.ts b/src/enums/emergencyAccessStatusType.ts new file mode 100644 index 0000000000..eb362edfcc --- /dev/null +++ b/src/enums/emergencyAccessStatusType.ts @@ -0,0 +1,7 @@ +export enum EmergencyAccessStatusType { + Invited = 0, + Accepted = 1, + Confirmed = 2, + RecoveryInitiated = 3, + RecoveryApproved = 4, +} diff --git a/src/enums/emergencyAccessType.ts b/src/enums/emergencyAccessType.ts new file mode 100644 index 0000000000..803634f4e7 --- /dev/null +++ b/src/enums/emergencyAccessType.ts @@ -0,0 +1,5 @@ +export enum EmergencyAccessType +{ + View = 0, + Takeover = 1, +} diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index 838b83b9e6..096753c1ec 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -34,10 +34,10 @@ export class Attachment extends Domain { }, alreadyEncrypted, ['id', 'url', 'sizeName']); } - async decrypt(orgId: string): Promise { + async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { const view = await this.decryptObj(new AttachmentView(this), { fileName: null, - }, orgId); + }, orgId, encKey); if (this.key != null) { let cryptoService: CryptoService; @@ -50,7 +50,7 @@ export class Attachment extends Domain { try { const orgKey = await cryptoService.getOrgKey(orgId); - const decValue = await cryptoService.decryptToBytes(this.key, orgKey); + const decValue = await cryptoService.decryptToBytes(this.key, orgKey ?? encKey); view.key = new SymmetricCryptoKey(decValue); } catch (e) { // TODO: error? diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index c1ef7a22d3..7f294eb243 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -4,6 +4,7 @@ import { CipherString } from './cipherString'; import Domain from './domainBase'; import { CardView } from '../view/cardView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Card extends Domain { cardholderName: CipherString; @@ -29,7 +30,7 @@ export class Card extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new CardView(this), { cardholderName: null, brand: null, @@ -37,7 +38,7 @@ export class Card extends Domain { expMonth: null, expYear: null, code: null, - }, orgId); + }, orgId, encKey); } toCardData(): CardData { diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 9b31235a77..ddf78557e1 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -13,6 +13,7 @@ import { Identity } from './identity'; import { Login } from './login'; import { Password } from './password'; import { SecureNote } from './secureNote'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Cipher extends Domain { id: string; @@ -102,26 +103,26 @@ export class Cipher extends Domain { } } - async decrypt(): Promise { + async decrypt(encKey?: SymmetricCryptoKey): Promise { const model = new CipherView(this); await this.decryptObj(model, { name: null, notes: null, - }, this.organizationId); + }, this.organizationId, encKey); switch (this.type) { case CipherType.Login: - model.login = await this.login.decrypt(this.organizationId); + model.login = await this.login.decrypt(this.organizationId, encKey); break; case CipherType.SecureNote: - model.secureNote = await this.secureNote.decrypt(this.organizationId); + model.secureNote = await this.secureNote.decrypt(this.organizationId, encKey); break; case CipherType.Card: - model.card = await this.card.decrypt(this.organizationId); + model.card = await this.card.decrypt(this.organizationId, encKey); break; case CipherType.Identity: - model.identity = await this.identity.decrypt(this.organizationId); + model.identity = await this.identity.decrypt(this.organizationId, encKey); break; default: break; @@ -133,7 +134,7 @@ export class Cipher extends Domain { const attachments: any[] = []; await this.attachments.reduce((promise, attachment) => { return promise.then(() => { - return attachment.decrypt(orgId); + return attachment.decrypt(orgId, encKey); }).then((decAttachment) => { attachments.push(decAttachment); }); @@ -145,7 +146,7 @@ export class Cipher extends Domain { const fields: any[] = []; await this.fields.reduce((promise, field) => { return promise.then(() => { - return field.decrypt(orgId); + return field.decrypt(orgId, encKey); }).then((decField) => { fields.push(decField); }); @@ -157,7 +158,7 @@ export class Cipher extends Domain { const passwordHistory: any[] = []; await this.passwordHistory.reduce((promise, ph) => { return promise.then(() => { - return ph.decrypt(orgId); + return ph.decrypt(orgId, encKey); }).then((decPh) => { passwordHistory.push(decPh); }); diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index 2dec447f2b..ac345805a1 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -6,6 +6,7 @@ import { CipherString } from './cipherString'; import Domain from './domainBase'; import { FieldView } from '../view/fieldView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Field extends Domain { name: CipherString; @@ -25,11 +26,11 @@ export class Field extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new FieldView(this), { name: null, value: null, - }, orgId); + }, orgId, encKey); } toFieldData(): FieldData { diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index 87622e6c6a..81ed571d10 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -2,6 +2,7 @@ import { IdentityData } from '../data/identityData'; import { CipherString } from './cipherString'; import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; import { IdentityView } from '../view/identityView'; @@ -53,7 +54,7 @@ export class Identity extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new IdentityView(this), { title: null, firstName: null, @@ -73,7 +74,7 @@ export class Identity extends Domain { username: null, passportNumber: null, licenseNumber: null, - }, orgId); + }, orgId, encKey); } toIdentityData(): IdentityData { diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index c40a830aba..c8be800447 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -6,6 +6,7 @@ import { LoginView } from '../view/loginView'; import { CipherString } from './cipherString'; import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Login extends Domain { uris: LoginUri[]; @@ -35,17 +36,17 @@ export class Login extends Domain { } } - async decrypt(orgId: string): Promise { + async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { const view = await this.decryptObj(new LoginView(this), { username: null, password: null, totp: null, - }, orgId); + }, orgId, encKey); if (this.uris != null) { view.uris = []; for (let i = 0; i < this.uris.length; i++) { - const uri = await this.uris[i].decrypt(orgId); + const uri = await this.uris[i].decrypt(orgId, encKey); view.uris.push(uri); } } diff --git a/src/models/domain/loginUri.ts b/src/models/domain/loginUri.ts index 631d5b0d02..e688b757b9 100644 --- a/src/models/domain/loginUri.ts +++ b/src/models/domain/loginUri.ts @@ -6,6 +6,7 @@ import { LoginUriView } from '../view/loginUriView'; import { CipherString } from './cipherString'; import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class LoginUri extends Domain { uri: CipherString; @@ -23,10 +24,10 @@ export class LoginUri extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new LoginUriView(this), { uri: null, - }, orgId); + }, orgId, encKey); } toLoginUriData(): LoginUriData { diff --git a/src/models/domain/password.ts b/src/models/domain/password.ts index 72c374b6f8..4bd4023a60 100644 --- a/src/models/domain/password.ts +++ b/src/models/domain/password.ts @@ -4,6 +4,7 @@ import { CipherString } from './cipherString'; import Domain from './domainBase'; import { PasswordHistoryView } from '../view/passwordHistoryView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Password extends Domain { password: CipherString; @@ -21,10 +22,10 @@ export class Password extends Domain { this.lastUsedDate = new Date(obj.lastUsedDate); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new PasswordHistoryView(this), { password: null, - }, orgId); + }, orgId, encKey); } toPasswordHistoryData(): PasswordHistoryData { diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 620571fbb7..91b23fff99 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -5,6 +5,7 @@ import { SecureNoteData } from '../data/secureNoteData'; import Domain from './domainBase'; import { SecureNoteView } from '../view/secureNoteView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class SecureNote extends Domain { type: SecureNoteType; @@ -18,7 +19,7 @@ export class SecureNote extends Domain { this.type = obj.type; } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return Promise.resolve(new SecureNoteView(this)); } diff --git a/src/models/request/emergencyAccessAcceptRequest.ts b/src/models/request/emergencyAccessAcceptRequest.ts new file mode 100644 index 0000000000..ff6f41c482 --- /dev/null +++ b/src/models/request/emergencyAccessAcceptRequest.ts @@ -0,0 +1,3 @@ +export class EmergencyAccessAcceptRequest { + token: string; +} diff --git a/src/models/request/emergencyAccessConfirmRequest.ts b/src/models/request/emergencyAccessConfirmRequest.ts new file mode 100644 index 0000000000..5b138c8487 --- /dev/null +++ b/src/models/request/emergencyAccessConfirmRequest.ts @@ -0,0 +1,3 @@ +export class EmergencyAccessConfirmRequest { + key: string; +} diff --git a/src/models/request/emergencyAccessInviteRequest.ts b/src/models/request/emergencyAccessInviteRequest.ts new file mode 100644 index 0000000000..f97620d584 --- /dev/null +++ b/src/models/request/emergencyAccessInviteRequest.ts @@ -0,0 +1,7 @@ +import { EmergencyAccessType } from '../../enums/emergencyAccessType'; + +export class EmergencyAccessInviteRequest { + email: string; + type: EmergencyAccessType; + waitTimeDays: number; +} diff --git a/src/models/request/emergencyAccessPasswordRequest.ts b/src/models/request/emergencyAccessPasswordRequest.ts new file mode 100644 index 0000000000..9d95f86b13 --- /dev/null +++ b/src/models/request/emergencyAccessPasswordRequest.ts @@ -0,0 +1,4 @@ +export class EmergencyAccessPasswordRequest { + newMasterPasswordHash: string; + key: string; +} diff --git a/src/models/request/emergencyAccessUpdateRequest.ts b/src/models/request/emergencyAccessUpdateRequest.ts new file mode 100644 index 0000000000..ed535cb04b --- /dev/null +++ b/src/models/request/emergencyAccessUpdateRequest.ts @@ -0,0 +1,7 @@ +import { EmergencyAccessType } from '../../enums/emergencyAccessType'; + +export class EmergencyAccessUpdateRequest { + type: EmergencyAccessType; + waitTimeDays: number; + keyEncrypted?: string; +} diff --git a/src/models/response/emergencyAccessResponse.ts b/src/models/response/emergencyAccessResponse.ts new file mode 100644 index 0000000000..fbd6084938 --- /dev/null +++ b/src/models/response/emergencyAccessResponse.ts @@ -0,0 +1,81 @@ +import { EmergencyAccessStatusType } from '../../enums/emergencyAccessStatusType'; +import { EmergencyAccessType } from '../../enums/emergencyAccessType'; +import { KdfType } from '../../enums/kdfType'; +import { BaseResponse } from './baseResponse'; +import { CipherResponse } from './cipherResponse'; + +export class EmergencyAccessGranteeDetailsResponse extends BaseResponse { + id: string; + granteeId: string; + name: string; + email: string; + type: EmergencyAccessType; + status: EmergencyAccessStatusType; + waitTimeDays: number; + creationDate: string; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.granteeId = this.getResponseProperty('GranteeId'); + this.name = this.getResponseProperty('Name'); + this.email = this.getResponseProperty('Email'); + this.type = this.getResponseProperty('Type'); + this.status = this.getResponseProperty('Status'); + this.waitTimeDays = this.getResponseProperty('WaitTimeDays'); + this.creationDate = this.getResponseProperty('CreationDate'); + } +} + +export class EmergencyAccessGrantorDetailsResponse extends BaseResponse { + id: string; + grantorId: string; + name: string; + email: string; + type: EmergencyAccessType; + status: EmergencyAccessStatusType; + waitTimeDays: number; + creationDate: string; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.grantorId = this.getResponseProperty('GrantorId'); + this.name = this.getResponseProperty('Name'); + this.email = this.getResponseProperty('Email'); + this.type = this.getResponseProperty('Type'); + this.status = this.getResponseProperty('Status'); + this.waitTimeDays = this.getResponseProperty('WaitTimeDays'); + this.creationDate = this.getResponseProperty('CreationDate'); + } +} + +export class EmergencyAccessTakeoverResponse extends BaseResponse { + keyEncrypted: string; + kdf: KdfType; + kdfIterations: number; + + constructor(response: any) { + super(response); + + this.keyEncrypted = this.getResponseProperty('KeyEncrypted'); + this.kdf = this.getResponseProperty('Kdf'); + this.kdfIterations = this.getResponseProperty('KdfIterations'); + } +} + +export class EmergencyAccessViewResponse extends BaseResponse { + keyEncrypted: string; + ciphers: CipherResponse[] = []; + + constructor(response: any) { + super(response); + + this.keyEncrypted = this.getResponseProperty('KeyEncrypted'); + + const ciphers = this.getResponseProperty('Ciphers'); + if (ciphers != null) { + this.ciphers = ciphers.map((c: any) => new CipherResponse(c)); + } + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 037c8c6d9c..e396839108 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -19,6 +19,11 @@ import { CollectionRequest } from '../models/request/collectionRequest'; import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; +import { EmergencyAccessAcceptRequest } from '../models/request/emergencyAccessAcceptRequest'; +import { EmergencyAccessConfirmRequest } from '../models/request/emergencyAccessConfirmRequest'; +import { EmergencyAccessInviteRequest } from '../models/request/emergencyAccessInviteRequest'; +import { EmergencyAccessPasswordRequest } from '../models/request/emergencyAccessPasswordRequest'; +import { EmergencyAccessUpdateRequest } from '../models/request/emergencyAccessUpdateRequest'; import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; @@ -77,6 +82,12 @@ import { CollectionResponse, } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; +import { + EmergencyAccessGranteeDetailsResponse, + EmergencyAccessGrantorDetailsResponse, + EmergencyAccessTakeoverResponse, + EmergencyAccessViewResponse +} from '../models/response/emergencyAccessResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { EventResponse } from '../models/response/eventResponse'; import { FolderResponse } from '../models/response/folderResponse'; @@ -901,6 +912,73 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/two-factor/send-email-login', request, false, false); } + // 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); + } + + 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 { + const r = 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 getOrganization(id: string): Promise { diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 0b1061a0cf..f2040caf81 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -380,8 +380,10 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildEncKey(theKey, encKey); } - async remakeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { - const encKey = await this.getEncKey(); + async remakeEncKey(key: SymmetricCryptoKey, encKey?: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { + if (encKey == null) { + encKey = await this.getEncKey(); + } return this.buildEncKey(key, encKey.key); } @@ -434,6 +436,58 @@ export class CryptoService implements CryptoServiceAbstraction { return new CipherString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); } + async rsaDecrypt(encValue: string): Promise { + const headerPieces = encValue.split('.'); + let encType: EncryptionType = null; + let encPieces: string[]; + + if (headerPieces.length === 1) { + encType = EncryptionType.Rsa2048_OaepSha256_B64; + encPieces = [headerPieces[0]]; + } else if (headerPieces.length === 2) { + try { + encType = parseInt(headerPieces[0], null); + encPieces = headerPieces[1].split('|'); + } catch (e) { } + } + + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + // HmacSha256 types are deprecated + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Error('encType unavailable.'); + } + + if (encPieces == null || encPieces.length <= 0) { + throw new Error('encPieces unavailable.'); + } + + const data = Utils.fromB64ToArray(encPieces[0]).buffer; + const privateKey = await this.getPrivateKey(); + if (privateKey == null) { + throw new Error('No private key.'); + } + + let alg: 'sha1' | 'sha256' = 'sha1'; + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + alg = 'sha256'; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Error('encType unavailable.'); + } + + return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); + } + async decryptToBytes(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { const iv = Utils.fromB64ToArray(cipherString.iv).buffer; const data = Utils.fromB64ToArray(cipherString.data).buffer; @@ -604,58 +658,6 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); } - private async rsaDecrypt(encValue: string): Promise { - const headerPieces = encValue.split('.'); - let encType: EncryptionType = null; - let encPieces: string[]; - - if (headerPieces.length === 1) { - encType = EncryptionType.Rsa2048_OaepSha256_B64; - encPieces = [headerPieces[0]]; - } else if (headerPieces.length === 2) { - try { - encType = parseInt(headerPieces[0], null); - encPieces = headerPieces[1].split('|'); - } catch (e) { } - } - - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_B64: - // HmacSha256 types are deprecated - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error('encType unavailable.'); - } - - if (encPieces == null || encPieces.length <= 0) { - throw new Error('encPieces unavailable.'); - } - - const data = Utils.fromB64ToArray(encPieces[0]).buffer; - const privateKey = await this.getPrivateKey(); - if (privateKey == null) { - throw new Error('No private key.'); - } - - let alg: 'sha1' | 'sha256' = 'sha1'; - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - alg = 'sha256'; - break; - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error('encType unavailable.'); - } - - return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); - } - private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { if (key != null) { return key;