mirror of
https://github.com/bitwarden/browser
synced 2026-01-03 17:13:47 +00:00
Add support for Emergency Access (#707)
* Add support for Emergency Access * Cleanup & Bugfix * Apply suggestions from code review Co-authored-by: Addison Beck <addisonbeck1@gmail.com> * Cleanup some more imports * Restrict emergency access invite to premium users * Restrict editing existing emergency accesses to premium account. * Handle changes in jslib * Add some info messages for when you haven't been granted or invited emergency contacts * Resolve review comments * Update jslib Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
This commit is contained in:
@@ -16,10 +16,14 @@ import {
|
||||
ChangePasswordComponent as BaseChangePasswordComponent,
|
||||
} from 'jslib/angular/components/change-password.component';
|
||||
|
||||
import { EmergencyAccessStatusType } from 'jslib/enums/emergencyAccessStatusType';
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { CipherString } from 'jslib/models/domain/cipherString';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
|
||||
import { EmergencyAccessUpdateRequest } from 'jslib/models/request/emergencyAccessUpdateRequest';
|
||||
import { FolderWithIdRequest } from 'jslib/models/request/folderWithIdRequest';
|
||||
import { PasswordRequest } from 'jslib/models/request/passwordRequest';
|
||||
import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
|
||||
@@ -160,5 +164,32 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
}
|
||||
|
||||
await this.apiService.postAccountKey(request);
|
||||
|
||||
await this.updateEmergencyAccesses(encKey[0]);
|
||||
}
|
||||
|
||||
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.buffer);
|
||||
|
||||
const updateRequest = new EmergencyAccessUpdateRequest();
|
||||
updateRequest.type = details.type;
|
||||
updateRequest.waitTimeDays = details.waitTimeDays;
|
||||
updateRequest.keyEncrypted = encryptedKey.encryptedString;
|
||||
|
||||
await this.apiService.putEmergencyAccess(details.id, updateRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
76
src/app/settings/emergency-access-add-edit.component.html
Normal file
76
src/app/settings/emergency-access-add-edit.component.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userAddEditTitle">
|
||||
<span class="badge badge-primary" *ngIf="readOnly">{{'premium' | i18n}}</span>
|
||||
{{title}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p>{{'inviteEmergencyContactDesc' | i18n}}</p>
|
||||
<div class="form-group mb-4">
|
||||
<label for="email">{{'email' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3>
|
||||
{{'userAccess' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="emergencyTypeView"
|
||||
[value]="emergencyAccessType.View" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="emergencyTypeView">
|
||||
{{'view' | i18n}}
|
||||
<small>{{'viewDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="emergencyTypeTakeover"
|
||||
[value]="emergencyAccessType.Takeover" [(ngModel)]="type" [disabled]="readOnly">
|
||||
<label class="form-check-label" for="emergencyTypeTakeover">
|
||||
{{'takeover' | i18n}}
|
||||
<small>{{'takeoverDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group col-6 mt-4">
|
||||
<label for="waitTime">{{'waitTime' | i18n}}</label>
|
||||
<select id="waitTime" name="waitTime" [(ngModel)]="waitTime" class="form-control" [disabled]="readOnly">
|
||||
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
<small class="text-muted">{{'waitTimeDesc' | i18n}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" [disabled]="loading || readOnly">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true" *ngIf="loading"></i>
|
||||
<span *ngIf="!loading">{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<div class="ml-auto">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
99
src/app/settings/emergency-access-add-edit.component.ts
Normal file
99
src/app/settings/emergency-access-add-edit.component.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { EmergencyAccessType } from 'jslib/enums/emergencyAccessType';
|
||||
import { EmergencyAccessInviteRequest } from 'jslib/models/request/emergencyAccessInviteRequest';
|
||||
import { EmergencyAccessUpdateRequest } from 'jslib/models/request/emergencyAccessUpdateRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'emergency-access-add-edit',
|
||||
templateUrl: 'emergency-access-add-edit.component.html',
|
||||
})
|
||||
export class EmergencyAccessAddEditComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() emergencyAccessId: string;
|
||||
@Output() onSaved = new EventEmitter();
|
||||
@Output() onDeleted = new EventEmitter();
|
||||
|
||||
loading = true;
|
||||
readOnly: boolean = false;
|
||||
editMode: boolean = false;
|
||||
title: string;
|
||||
email: string;
|
||||
type: EmergencyAccessType = EmergencyAccessType.View;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
emergencyAccessType = EmergencyAccessType;
|
||||
waitTimes: { name: string; value: number; }[];
|
||||
waitTime: number;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private toasterService: ToasterService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.editMode = this.loading = this.emergencyAccessId != null;
|
||||
|
||||
this.waitTimes = [
|
||||
{ name: this.i18nService.t('oneDay'), value: 1 },
|
||||
{ name: this.i18nService.t('days', '2'), value: 2 },
|
||||
{ name: this.i18nService.t('days', '7'), value: 7 },
|
||||
{ name: this.i18nService.t('days', '14'), value: 14 },
|
||||
{ name: this.i18nService.t('days', '30'), value: 30 },
|
||||
{ name: this.i18nService.t('days', '90'), value: 90 },
|
||||
];
|
||||
|
||||
if (this.editMode) {
|
||||
this.editMode = true;
|
||||
this.title = this.i18nService.t('editEmergencyContact');
|
||||
try {
|
||||
const emergencyAccess = await this.apiService.getEmergencyAccess(this.emergencyAccessId);
|
||||
this.type = emergencyAccess.type;
|
||||
this.waitTime = emergencyAccess.waitTimeDays;
|
||||
} catch { }
|
||||
} else {
|
||||
this.title = this.i18nService.t('inviteEmergencyContact');
|
||||
this.waitTime = this.waitTimes[2].value;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
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);
|
||||
} 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.formPromise;
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
|
||||
this.onSaved.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async delete() {
|
||||
this.onDeleted.emit();
|
||||
}
|
||||
}
|
||||
38
src/app/settings/emergency-access-confirm.component.html
Normal file
38
src/app/settings/emergency-access-confirm.component.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="confirmUserTitle">
|
||||
{{'confirmUser' | i18n}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{'fingerprintEnsureIntegrityVerify' | i18n}}
|
||||
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener">
|
||||
{{'learnMore' | i18n}}</a>
|
||||
</p>
|
||||
<p><code>{{fingerprint}}</code></p>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain"
|
||||
[(ngModel)]="dontAskAgain">
|
||||
<label class="form-check-label" for="dontAskAgain">
|
||||
{{'dontAskFingerprintAgain' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'confirm' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
61
src/app/settings/emergency-access-confirm.component.ts
Normal file
61
src/app/settings/emergency-access-confirm.component.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'emergency-access-confirm',
|
||||
templateUrl: 'emergency-access-confirm.component.html',
|
||||
})
|
||||
export class EmergencyAccessConfirmComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() userId: string;
|
||||
@Input() emergencyAccessId: string;
|
||||
@Output() onConfirmed = new EventEmitter();
|
||||
|
||||
dontAskAgain = false;
|
||||
loading = true;
|
||||
fingerprint: string;
|
||||
|
||||
constructor(private apiService: ApiService, private cryptoService: CryptoService,
|
||||
private storageService: StorageService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
|
||||
if (publicKeyResponse != null) {
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey.buffer);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join('-');
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dontAskAgain) {
|
||||
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
|
||||
}
|
||||
|
||||
try {
|
||||
this.onConfirmed.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
44
src/app/settings/emergency-access-takeover.component.html
Normal file
44
src/app/settings/emergency-access-takeover.component.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userAddEditTitle">
|
||||
{{'takeover' | i18n}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'newMasterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
|
||||
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
|
||||
class="form-control" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
81
src/app/settings/emergency-access-takeover.component.ts
Normal file
81
src/app/settings/emergency-access-takeover.component.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { ChangePasswordComponent } from 'jslib/angular/components/change-password.component';
|
||||
|
||||
import { KdfType } from 'jslib/enums/kdfType';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
import { EmergencyAccessPasswordRequest } from 'jslib/models/request/emergencyAccessPasswordRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'emergency-access-takeover',
|
||||
templateUrl: 'emergency-access-takeover.component.html',
|
||||
})
|
||||
export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent implements OnInit {
|
||||
@Output() onDone = new EventEmitter();
|
||||
@Input() emergencyAccessId: string;
|
||||
@Input() name: string;
|
||||
@Input() email: string;
|
||||
@Input() kdf: KdfType;
|
||||
@Input() kdfIterations: number;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(i18nService: I18nService, cryptoService: CryptoService,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
passwordGenerationService: PasswordGenerationService,
|
||||
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
|
||||
private apiService: ApiService, private toasterService: ToasterService) {
|
||||
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
|
||||
platformUtilsService, policyService);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line
|
||||
async ngOnInit() { }
|
||||
|
||||
async submit() {
|
||||
if (!await this.strongPassword()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const takeoverResponse = await this.apiService.postEmergencyAccessTakeover(this.emergencyAccessId);
|
||||
|
||||
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
|
||||
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
|
||||
|
||||
if (oldEncKey == null) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), this.i18nService.t('unexpectedError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const key = await this.cryptoService.makeKey(this.masterPassword, this.email, takeoverResponse.kdf, takeoverResponse.kdfIterations);
|
||||
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||
|
||||
const encKey = await this.cryptoService.remakeEncKey(key, oldEncKey);
|
||||
|
||||
const request = new EmergencyAccessPasswordRequest();
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.key = encKey[1].encryptedString;
|
||||
|
||||
this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request);
|
||||
|
||||
try {
|
||||
this.onDone.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
31
src/app/settings/emergency-access-view.component.html
Normal file
31
src/app/settings/emergency-access-view.component.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="page-header">
|
||||
<h1>{{'vault' | i18n}}</h1>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<ng-container *ngIf="!organization && c.organizationId">
|
||||
<i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'shared' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i class="fa fa-paperclip" appStopProp title="{{'attachments' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'attachments' | i18n}}</span>
|
||||
</ng-container>
|
||||
<br>
|
||||
<small>{{c.subTitle}}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
94
src/app/settings/emergency-access-view.component.ts
Normal file
94
src/app/settings/emergency-access-view.component.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
|
||||
import { CipherData } from 'jslib/models/data';
|
||||
import { Cipher, SymmetricCryptoKey } from 'jslib/models/domain';
|
||||
import { EmergencyAccessViewResponse } from 'jslib/models/response/emergencyAccessResponse';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
|
||||
import { EmergencyAddEditComponent } from './emergency-add-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'emergency-access-view',
|
||||
templateUrl: 'emergency-access-view.component.html',
|
||||
})
|
||||
export class EmergencyAccessViewComponent implements OnInit {
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
|
||||
|
||||
id: string;
|
||||
ciphers: CipherView[] = [];
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private cipherService: CipherService, private cryptoService: CryptoService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private router: Router,
|
||||
private route: ActivatedRoute, private apiService: ApiService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe((qParams) => {
|
||||
if (qParams.id == null) {
|
||||
return this.router.navigate(['settings/emergency-access']);
|
||||
}
|
||||
|
||||
this.id = qParams.id;
|
||||
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
||||
selectCipher(cipher: CipherView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.cipherAddEditModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<EmergencyAddEditComponent>(EmergencyAddEditComponent, this.cipherAddEditModalRef);
|
||||
|
||||
childComponent.cipherId = cipher == null ? null : cipher.id;
|
||||
childComponent.cipher = cipher;
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.postEmergencyAccessView(this.id);
|
||||
this.ciphers = await this.getAllCiphers(response);
|
||||
}
|
||||
|
||||
protected async getAllCiphers(response: EmergencyAccessViewResponse): Promise<CipherView[]> {
|
||||
const ciphers = response.ciphers;
|
||||
|
||||
const decCiphers: CipherView[] = [];
|
||||
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
|
||||
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
|
||||
|
||||
const promises: any[] = [];
|
||||
ciphers.forEach((cipherResponse) => {
|
||||
const cipherData = new CipherData(cipherResponse);
|
||||
const cipher = new Cipher(cipherData);
|
||||
promises.push(cipher.decrypt(oldEncKey).then((c) => decCiphers.push(c)));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
|
||||
|
||||
return decCiphers;
|
||||
}
|
||||
}
|
||||
159
src/app/settings/emergency-access.component.html
Normal file
159
src/app/settings/emergency-access.component.html
Normal file
@@ -0,0 +1,159 @@
|
||||
<div class="page-header">
|
||||
<h1>{{'emergencyAccess' | i18n}}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{'emergencyAccessDesc' | i18n}}
|
||||
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener">
|
||||
{{'learnMore' | i18n}}.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="page-header d-flex">
|
||||
<h2>
|
||||
{{'trustedEmergencyContacts' | i18n}}
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!canAccessPremium" (click)="premiumRequired()">
|
||||
{{'premium' | i18n}}
|
||||
</a>
|
||||
</h2>
|
||||
<div class="ml-auto d-flex">
|
||||
<button class="btn btn-sm btn-outline-primary ml-3" type="button" (click)="invite()" [disabled]="!canAccessPremium">
|
||||
<i aria-hidden="true" class="fa fa-plus fa-fw"></i>
|
||||
{{'addEmergencyContact' |i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-list mb-0" *ngIf="trustedContacts && trustedContacts.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of trustedContacts; let i = index">
|
||||
<td width="30">
|
||||
<app-avatar [data]="c.name || c.email" [email]="c.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(c)">{{c.email}}</a>
|
||||
<span class="badge badge-secondary"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated">{{'emergencyAccessRecoveryInitiated' | i18n}}</span>
|
||||
<span class="badge badge-success"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{'emergencyAccessRecoveryApproved' | i18n}}</span>
|
||||
|
||||
<span class="badge badge-primary"
|
||||
*ngIf="c.type === emergencyAccessType.View">{{'view' | i18n}}</span>
|
||||
<span class="badge badge-primary"
|
||||
*ngIf="c.type === emergencyAccessType.Takeover">{{'takeover' | i18n}}</span>
|
||||
|
||||
<small class="text-muted d-block" *ngIf="c.name">{{c.name}}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(c)"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Invited">
|
||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||
{{'resendInvitation' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(c)"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted">
|
||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||
{{'confirm' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="approve(c)"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated">
|
||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||
{{'approve' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-warning" href="#" appStopClick (click)="reject(c)"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated || c.status === emergencyAccessStatusType.RecoveryApproved">
|
||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||
{{'reject' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(c)">
|
||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||
{{'remove' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p *ngIf="!trustedContacts || !trustedContacts.length">{{'noTrustedContacts' | i18n}}</p>
|
||||
|
||||
<div class="page-header spaced-header">
|
||||
<h2>{{'designatedEmergencyContacts' | i18n}}</h2>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-list mb-0" *ngIf="grantedContacts && grantedContacts.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of grantedContacts; let i = index">
|
||||
<td width="30">
|
||||
<app-avatar [data]="c.name || c.email" [email]="c.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{c.email}}</span>
|
||||
<span class="badge badge-secondary"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated">{{'emergencyAccessRecoveryInitiated' | i18n}}</span>
|
||||
<span class="badge badge-success"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{'emergencyAccessRecoveryApproved' | i18n}}</span>
|
||||
|
||||
<span class="badge badge-primary"
|
||||
*ngIf="c.type === emergencyAccessType.View">{{'view' | i18n}}</span>
|
||||
<span class="badge badge-primary"
|
||||
*ngIf="c.type === emergencyAccessType.Takeover">{{'takeover' | i18n}}</span>
|
||||
|
||||
<small class="text-muted d-block" *ngIf="c.name">{{c.name}}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="requestAccess(c)"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Confirmed">
|
||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||
{{'requestAccess' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="takeover(c)"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved && c.type === emergencyAccessType.Takeover">
|
||||
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
|
||||
{{'takeover' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" [routerLink]="c.id"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved && c.type === emergencyAccessType.View">
|
||||
<i class="fa fa-fw fa-eye" aria-hidden="true"></i>
|
||||
{{'view' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(c)">
|
||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||
{{'remove' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p *ngIf="!grantedContacts || !grantedContacts.length">{{'noGrantedAccess' | i18n}}</p>
|
||||
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #takeoverTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
274
src/app/settings/emergency-access.component.ts
Normal file
274
src/app/settings/emergency-access.component.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { Component, ComponentFactoryResolver, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { EmergencyAccessStatusType } from 'jslib/enums/emergencyAccessStatusType';
|
||||
import { EmergencyAccessType } from 'jslib/enums/emergencyAccessType';
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
import { EmergencyAccessConfirmRequest } from 'jslib/models/request/emergencyAccessConfirmRequest';
|
||||
import { EmergencyAccessGranteeDetailsResponse, EmergencyAccessGrantorDetailsResponse } from 'jslib/models/response/emergencyAccessResponse';
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
import { EmergencyAccessAddEditComponent } from './emergency-access-add-edit.component';
|
||||
import { EmergencyAccessConfirmComponent } from './emergency-access-confirm.component';
|
||||
import { EmergencyAccessTakeoverComponent } from './emergency-access-takeover.component';
|
||||
|
||||
@Component({
|
||||
selector: 'emergency-access',
|
||||
templateUrl: 'emergency-access.component.html',
|
||||
})
|
||||
export class EmergencyAccessComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('takeoverTemplate', { read: ViewContainerRef, static: true}) takeoverModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
|
||||
|
||||
canAccessPremium: boolean;
|
||||
trustedContacts: EmergencyAccessGranteeDetailsResponse[];
|
||||
grantedContacts: EmergencyAccessGrantorDetailsResponse[];
|
||||
emergencyAccessType = EmergencyAccessType;
|
||||
emergencyAccessStatusType = EmergencyAccessStatusType;
|
||||
actionPromise: Promise<any>;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService,
|
||||
private storageService: StorageService, private userService: UserService,
|
||||
private messagingService: MessagingService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.canAccessPremium = await this.userService.canAccessPremium();
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.trustedContacts = (await this.apiService.getEmergencyAccessTrusted()).data;
|
||||
this.grantedContacts = (await this.apiService.getEmergencyAccessGranted()).data;
|
||||
}
|
||||
|
||||
async premiumRequired() {
|
||||
if (!this.canAccessPremium) {
|
||||
this.messagingService.send('premiumRequired');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
edit(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.addEditModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<EmergencyAccessAddEditComponent>(
|
||||
EmergencyAccessAddEditComponent, this.addEditModalRef);
|
||||
|
||||
childComponent.name = details?.name ?? details?.email;
|
||||
childComponent.emergencyAccessId = details?.id;
|
||||
childComponent.readOnly = !this.canAccessPremium;
|
||||
childComponent.onSaved.subscribe(() => {
|
||||
this.modal.close();
|
||||
this.load();
|
||||
});
|
||||
childComponent.onDeleted.subscribe(() => {
|
||||
this.modal.close();
|
||||
this.remove(details);
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
invite() {
|
||||
this.edit(null);
|
||||
}
|
||||
|
||||
async reinvite(contact: EmergencyAccessGranteeDetailsResponse) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
this.actionPromise = this.apiService.postEmergencyAccessReinvite(contact.id);
|
||||
await this.actionPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenReinvited', contact.email));
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async confirm(contact: EmergencyAccessGranteeDetailsResponse) {
|
||||
function updateUser() {
|
||||
contact.status = EmergencyAccessStatusType.Confirmed;
|
||||
}
|
||||
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoConfirm = await this.storageService.get<boolean>(ConstantsService.autoConfirmFingerprints);
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.confirmModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<EmergencyAccessConfirmComponent>(
|
||||
EmergencyAccessConfirmComponent, this.confirmModalRef);
|
||||
|
||||
childComponent.name = contact?.name ?? contact?.email;
|
||||
childComponent.emergencyAccessId = contact.id;
|
||||
childComponent.userId = contact?.granteeId;
|
||||
childComponent.onConfirmed.subscribe(async () => {
|
||||
this.modal.close();
|
||||
|
||||
await this.doConfirmation(contact);
|
||||
updateUser();
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', contact.name || contact.email));
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.doConfirmation(contact);
|
||||
await this.actionPromise;
|
||||
updateUser();
|
||||
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', contact.name || contact.email));
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async remove(details: EmergencyAccessGranteeDetailsResponse | EmergencyAccessGrantorDetailsResponse) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('removeUserConfirmation'), details.name || details.email,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiService.deleteEmergencyAccess(details.id);
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', details.name || details.email));
|
||||
|
||||
if (details instanceof EmergencyAccessGranteeDetailsResponse) {
|
||||
this.removeGrantee(details);
|
||||
} else {
|
||||
this.removeGrantor(details);
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async requestAccess(details: EmergencyAccessGrantorDetailsResponse) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('requestAccessConfirmation', details.waitTimeDays.toString()),
|
||||
details.name || details.email,
|
||||
this.i18nService.t('requestAccess'),
|
||||
this.i18nService.t('no'),
|
||||
'warning'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.apiService.postEmergencyAccessInitiate(details.id);
|
||||
|
||||
details.status = EmergencyAccessStatusType.RecoveryInitiated;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('requestSent', details.name || details.email));
|
||||
}
|
||||
|
||||
async approve(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
const type = this.i18nService.t(details.type === EmergencyAccessType.View ? 'view' : 'takeover');
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('approveAccessConfirmation', details.name, type),
|
||||
details.name || details.email,
|
||||
this.i18nService.t('approve'),
|
||||
this.i18nService.t('no'),
|
||||
'warning'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.apiService.postEmergencyAccessApprove(details.id);
|
||||
details.status = EmergencyAccessStatusType.RecoveryApproved;
|
||||
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('emergencyApproved', details.name || details.email));
|
||||
}
|
||||
|
||||
async reject(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
await this.apiService.postEmergencyAccessReject(details.id);
|
||||
details.status = EmergencyAccessStatusType.Confirmed;
|
||||
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('emergencyRejected', details.name || details.email));
|
||||
}
|
||||
|
||||
async takeover(details: EmergencyAccessGrantorDetailsResponse) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.addEditModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<EmergencyAccessTakeoverComponent>(
|
||||
EmergencyAccessTakeoverComponent, this.takeoverModalRef);
|
||||
|
||||
childComponent.name = details != null ? details.name || details.email : null;
|
||||
childComponent.email = details.email;
|
||||
childComponent.emergencyAccessId = details != null ? details.id : null;
|
||||
|
||||
childComponent.onDone.subscribe(() => {
|
||||
this.modal.close();
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('passwordResetFor', details.name || details.email));
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
private removeGrantee(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
const index = this.trustedContacts.indexOf(details);
|
||||
if (index > -1) {
|
||||
this.trustedContacts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private removeGrantor(details: EmergencyAccessGrantorDetailsResponse) {
|
||||
const index = this.grantedContacts.indexOf(details);
|
||||
if (index > -1) {
|
||||
this.grantedContacts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt the master password hash using the grantees public key, and send it to bitwarden for escrow.
|
||||
private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
const encKey = await this.cryptoService.getEncKey();
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
try {
|
||||
// tslint:disable-next-line
|
||||
console.log('User\'s fingerprint: ' +
|
||||
(await this.cryptoService.getFingerprint(details.granteeId, publicKey.buffer)).join('-'));
|
||||
} catch { }
|
||||
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
|
||||
const request = new EmergencyAccessConfirmRequest();
|
||||
request.key = encryptedKey.encryptedString;
|
||||
await this.apiService.postEmergencyAccessConfirm(details.id, request);
|
||||
}
|
||||
}
|
||||
47
src/app/settings/emergency-add-edit.component.ts
Normal file
47
src/app/settings/emergency-add-edit.component.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { EventService } from 'jslib/abstractions/event.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { TotpService } from 'jslib/abstractions/totp.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from '../vault/add-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-vault-add-edit',
|
||||
templateUrl: '../vault/add-edit.component.html',
|
||||
})
|
||||
export class EmergencyAddEditComponent extends BaseAddEditComponent {
|
||||
originalCipher: Cipher = null;
|
||||
viewOnly = true;
|
||||
|
||||
constructor(cipherService: CipherService, folderService: FolderService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService, stateService: StateService,
|
||||
userService: UserService, collectionService: CollectionService,
|
||||
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
|
||||
messagingService: MessagingService, eventService: EventService, policyService: PolicyService) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
|
||||
userService, collectionService, totpService, passwordGenerationService, messagingService,
|
||||
eventService, policyService);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.title = this.i18nService.t('viewItem');
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
return Promise.resolve(this.originalCipher);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@
|
||||
<a routerLink="domain-rules" class="list-group-item" routerLinkActive="active">
|
||||
{{'domainRules' | i18n}}
|
||||
</a>
|
||||
<a routerLink="emergency-access" class="list-group-item" routerLinkActive="active">
|
||||
{{'emergencyAccess' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user