Skip to content
This repository has been archived by the owner on Jun 17, 2022. It is now read-only.

Commit

Permalink
Add support for Emergency Access (#204)
Browse files Browse the repository at this point in the history
* Add support for Emergency Access

* Resolve review comments
  • Loading branch information
Hinton committed Dec 22, 2020
1 parent 12321e5 commit 573eea6
Show file tree
Hide file tree
Showing 22 changed files with 358 additions and 117 deletions.
27 changes: 27 additions & 0 deletions src/abstractions/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -278,6 +289,22 @@ export abstract class ApiService {
postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise<any>;
postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise<any>;

getEmergencyAccessTrusted: () => Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>>;
getEmergencyAccessGranted: () => Promise<ListResponse<EmergencyAccessGrantorDetailsResponse>>;
getEmergencyAccess: (id: string) => Promise<EmergencyAccessGranteeDetailsResponse>;
putEmergencyAccess: (id: string, request: EmergencyAccessUpdateRequest) => Promise<any>;
deleteEmergencyAccess: (id: string) => Promise<any>;
postEmergencyAccessInvite: (request: EmergencyAccessInviteRequest) => Promise<any>;
postEmergencyAccessReinvite: (id: string) => Promise<any>;
postEmergencyAccessAccept: (id: string, request: EmergencyAccessAcceptRequest) => Promise<any>;
postEmergencyAccessConfirm: (id: string, request: EmergencyAccessConfirmRequest) => Promise<any>;
postEmergencyAccessInitiate: (id: string) => Promise<any>;
postEmergencyAccessApprove: (id: string) => Promise<any>;
postEmergencyAccessReject: (id: string) => Promise<any>;
postEmergencyAccessTakeover: (id: string) => Promise<EmergencyAccessTakeoverResponse>;
postEmergencyAccessPassword: (id: string, request: EmergencyAccessPasswordRequest) => Promise<any>;
postEmergencyAccessView: (id: string) => Promise<EmergencyAccessViewResponse>;

getOrganization: (id: string) => Promise<OrganizationResponse>;
getOrganizationBilling: (id: string) => Promise<BillingResponse>;
getOrganizationSubscription: (id: string) => Promise<OrganizationSubscriptionResponse>;
Expand Down
3 changes: 2 additions & 1 deletion src/abstractions/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ export abstract class CryptoService {
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
hashPassword: (password: string, key: SymmetricCryptoKey) => Promise<string>;
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<CipherString>;
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise<CipherString>;
rsaDecrypt: (encValue: string) => Promise<ArrayBuffer>;
decryptToBytes: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise<string>;
decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
Expand Down
80 changes: 44 additions & 36 deletions src/angular/components/change-password.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -58,44 +58,10 @@ 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'));
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'));
if (!await this.strongPassword()) {
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;
}
Expand Down Expand Up @@ -133,6 +99,48 @@ export class ChangePasswordComponent implements OnInit {
// Override in sub-class
}

async strongPassword(): Promise<boolean> {
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);
Expand Down
7 changes: 7 additions & 0 deletions src/enums/emergencyAccessStatusType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum EmergencyAccessStatusType {
Invited = 0,
Accepted = 1,
Confirmed = 2,
RecoveryInitiated = 3,
RecoveryApproved = 4,
}
5 changes: 5 additions & 0 deletions src/enums/emergencyAccessType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum EmergencyAccessType
{
View = 0,
Takeover = 1,
}
6 changes: 3 additions & 3 deletions src/models/domain/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export class Attachment extends Domain {
}, alreadyEncrypted, ['id', 'url', 'sizeName']);
}

async decrypt(orgId: string): Promise<AttachmentView> {
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<AttachmentView> {
const view = await this.decryptObj(new AttachmentView(this), {
fileName: null,
}, orgId);
}, orgId, encKey);

if (this.key != null) {
let cryptoService: CryptoService;
Expand All @@ -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?
Expand Down
5 changes: 3 additions & 2 deletions src/models/domain/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,15 +30,15 @@ export class Card extends Domain {
}, alreadyEncrypted, []);
}

decrypt(orgId: string): Promise<CardView> {
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<CardView> {
return this.decryptObj(new CardView(this), {
cardholderName: null,
brand: null,
number: null,
expMonth: null,
expYear: null,
code: null,
}, orgId);
}, orgId, encKey);
}

toCardData(): CardData {
Expand Down
19 changes: 10 additions & 9 deletions src/models/domain/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -102,26 +103,26 @@ export class Cipher extends Domain {
}
}

async decrypt(): Promise<CipherView> {
async decrypt(encKey?: SymmetricCryptoKey): Promise<CipherView> {
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;
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand Down
5 changes: 3 additions & 2 deletions src/models/domain/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,11 +26,11 @@ export class Field extends Domain {
}, alreadyEncrypted, []);
}

decrypt(orgId: string): Promise<FieldView> {
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<FieldView> {
return this.decryptObj(new FieldView(this), {
name: null,
value: null,
}, orgId);
}, orgId, encKey);
}

toFieldData(): FieldData {
Expand Down
5 changes: 3 additions & 2 deletions src/models/domain/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -53,7 +54,7 @@ export class Identity extends Domain {
}, alreadyEncrypted, []);
}

decrypt(orgId: string): Promise<IdentityView> {
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<IdentityView> {
return this.decryptObj(new IdentityView(this), {
title: null,
firstName: null,
Expand All @@ -73,7 +74,7 @@ export class Identity extends Domain {
username: null,
passportNumber: null,
licenseNumber: null,
}, orgId);
}, orgId, encKey);
}

toIdentityData(): IdentityData {
Expand Down
7 changes: 4 additions & 3 deletions src/models/domain/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -35,17 +36,17 @@ export class Login extends Domain {
}
}

async decrypt(orgId: string): Promise<LoginView> {
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<LoginView> {
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);
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/models/domain/loginUri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,10 +24,10 @@ export class LoginUri extends Domain {
}, alreadyEncrypted, []);
}

decrypt(orgId: string): Promise<LoginUriView> {
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<LoginUriView> {
return this.decryptObj(new LoginUriView(this), {
uri: null,
}, orgId);
}, orgId, encKey);
}

toLoginUriData(): LoginUriData {
Expand Down
Loading

0 comments on commit 573eea6

Please sign in to comment.