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

Commit

Permalink
Password reprompt (#357)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton committed Apr 29, 2021
1 parent 1eb40a4 commit a72c8a6
Show file tree
Hide file tree
Showing 16 changed files with 161 additions and 12 deletions.
4 changes: 4 additions & 0 deletions src/abstractions/passwordReprompt.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export abstract class PasswordRepromptService {
protectedFields: () => string[];
showPasswordPrompt: () => Promise<boolean>;
}
1 change: 1 addition & 0 deletions src/abstractions/platformUtils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export abstract class PlatformUtilsService {
options?: any) => void;
showDialog: (body: string, title?: string, confirmText?: string, cancelText?: string,
type?: string, bodyIsHtml?: boolean) => Promise<boolean>;
showPasswordDialog: (title: string, body: string, passwordValidation: (value: string) => Promise<boolean>) => Promise<boolean>;
isDev: () => boolean;
isSelfHost: () => boolean;
copyToClipboard: (text: string, options?: any) => void;
Expand Down
20 changes: 20 additions & 0 deletions src/angular/components/add-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { LoginUriView } from '../../models/view/loginUriView';
import { LoginView } from '../../models/view/loginView';
import { SecureNoteView } from '../../models/view/secureNoteView';

import { CipherRepromptType } from '../../enums/cipherRepromptType';
import { Utils } from '../../misc/utils';

@Directive()
Expand Down Expand Up @@ -71,6 +72,7 @@ export class AddEditComponent implements OnInit {
restorePromise: Promise<any>;
checkPasswordPromise: Promise<number>;
showPassword: boolean = false;
showCardNumber: boolean = false;
showCardCode: boolean = false;
cipherType = CipherType;
fieldType = FieldType;
Expand All @@ -84,6 +86,7 @@ export class AddEditComponent implements OnInit {
ownershipOptions: any[] = [];
currentDate = new Date();
allowPersonal = true;
reprompt: boolean = false;

protected writeableCollections: CollectionView[];
private previousCipherId: string;
Expand Down Expand Up @@ -245,6 +248,7 @@ export class AddEditComponent implements OnInit {
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
}
this.previousCipherId = this.cipherId;
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
}

async submit(): Promise<boolean> {
Expand Down Expand Up @@ -422,6 +426,13 @@ export class AddEditComponent implements OnInit {
}
}

async toggleCardNumber() {
this.showCardNumber = !this.showCardNumber;
if (this.showCardNumber) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
}
}

toggleCardCode() {
this.showCardCode = !this.showCardCode;
document.getElementById('cardCode').focus();
Expand Down Expand Up @@ -488,6 +499,15 @@ export class AddEditComponent implements OnInit {
}
}

repromptChanged() {
this.reprompt = !this.reprompt;
if (this.reprompt) {
this.cipher.reprompt = CipherRepromptType.Password;
} else {
this.cipher.reprompt = CipherRepromptType.None;
}
}

protected async loadCollections() {
const allCollections = await this.collectionService.getAllDecrypted();
return allCollections.filter(c => !c.readOnly);
Expand Down
81 changes: 70 additions & 11 deletions src/angular/components/view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import { CipherService } from '../../abstractions/cipher.service';
import { CryptoService } from '../../abstractions/crypto.service';
import { EventService } from '../../abstractions/event.service';
import { I18nService } from '../../abstractions/i18n.service';
import { PasswordRepromptService } from '../../abstractions/passwordReprompt.service';
import { PlatformUtilsService } from '../../abstractions/platformUtils.service';
import { TokenService } from '../../abstractions/token.service';
import { TotpService } from '../../abstractions/totp.service';
import { UserService } from '../../abstractions/user.service';

import { ErrorResponse } from '../../models/response/errorResponse';

import { CipherRepromptType } from '../../enums/cipherRepromptType';
import { AttachmentView } from '../../models/view/attachmentView';
import { CipherView } from '../../models/view/cipherView';
import { FieldView } from '../../models/view/fieldView';
Expand All @@ -45,6 +47,7 @@ export class ViewComponent implements OnDestroy, OnInit {

cipher: CipherView;
showPassword: boolean;
showCardNumber: boolean;
showCardCode: boolean;
canAccessPremium: boolean;
totpCode: string;
Expand All @@ -57,14 +60,16 @@ export class ViewComponent implements OnDestroy, OnInit {

private totpInterval: any;
private previousCipherId: string;
private passwordReprompted: boolean = false;

constructor(protected cipherService: CipherService, protected totpService: TotpService,
protected tokenService: TokenService, protected i18nService: I18nService,
protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService,
protected auditService: AuditService, protected win: Window,
protected broadcasterService: BroadcasterService, protected ngZone: NgZone,
protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService,
protected eventService: EventService, protected apiService: ApiService) { }
protected eventService: EventService, protected apiService: ApiService,
protected passwordRepromptService: PasswordRepromptService) { }

ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
Expand Down Expand Up @@ -110,19 +115,38 @@ export class ViewComponent implements OnDestroy, OnInit {
this.previousCipherId = this.cipherId;
}

edit() {
this.onEditCipher.emit(this.cipher);
async edit() {
if (await this.promptPassword()) {
this.onEditCipher.emit(this.cipher);
return true;
}

return false;
}

clone() {
this.onCloneCipher.emit(this.cipher);
async clone() {
if (await this.promptPassword()) {
this.onCloneCipher.emit(this.cipher);
return true;
}

return false;
}

share() {
this.onShareCipher.emit(this.cipher);
async share() {
if (await this.promptPassword()) {
this.onShareCipher.emit(this.cipher);
return true;
}

return false;
}

async delete(): Promise<boolean> {
if (!await this.promptPassword()) {
return;
}

const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'),
this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
Expand Down Expand Up @@ -161,14 +185,33 @@ export class ViewComponent implements OnDestroy, OnInit {
return true;
}

togglePassword() {
async togglePassword() {
if (!await this.promptPassword()) {
return;
}

this.showPassword = !this.showPassword;
if (this.showPassword) {
this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId);
}
}

toggleCardCode() {
async toggleCardNumber() {
if (!await this.promptPassword()) {
return;
}

this.showCardNumber = !this.showCardNumber;
if (this.showCardNumber) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
}
}

async toggleCardCode() {
if (!await this.promptPassword()) {
return;
}

this.showCardCode = !this.showCardCode;
if (this.showCardCode) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
Expand All @@ -191,7 +234,11 @@ export class ViewComponent implements OnDestroy, OnInit {
}
}

toggleFieldValue(field: FieldView) {
async toggleFieldValue(field: FieldView) {
if (!await this.promptPassword()) {
return;
}

const f = (field as any);
f.showValue = !f.showValue;
if (f.showValue) {
Expand All @@ -211,11 +258,15 @@ export class ViewComponent implements OnDestroy, OnInit {
this.platformUtilsService.launchUri(uri.launchUri);
}

copy(value: string, typeI18nKey: string, aType: string) {
async copy(value: string, typeI18nKey: string, aType: string) {
if (value == null) {
return;
}

if (this.passwordRepromptService.protectedFields().includes(aType) && !await this.promptPassword()) {
return;
}

const copyOptions = this.win != null ? { window: this.win } : null;
this.platformUtilsService.copyToClipboard(value, copyOptions);
this.platformUtilsService.showToast('info', null,
Expand Down Expand Up @@ -290,6 +341,14 @@ export class ViewComponent implements OnDestroy, OnInit {
return this.cipherService.restoreWithServer(this.cipher.id);
}

protected async promptPassword() {
if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) {
return true;
}

return this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt();
}

private cleanUp() {
this.totpCode = null;
this.cipher = null;
Expand Down
5 changes: 5 additions & 0 deletions src/cli/services/cliPlatformUtils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
throw new Error('Not implemented.');
}

showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise<boolean>):
Promise<boolean> {
throw new Error('Not implemented.');
}

isDev(): boolean {
return process.env.BWCLI_ENV === 'development';
}
Expand Down
7 changes: 6 additions & 1 deletion src/electron/services/electronPlatformUtils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {

private deviceCache: DeviceType = null;

constructor(private i18nService: I18nService, private messagingService: MessagingService,
constructor(protected i18nService: I18nService, private messagingService: MessagingService,
private isDesktopApp: boolean, private storageService: StorageService) {
this.identityClientId = isDesktopApp ? 'desktop' : 'connector';
}
Expand Down Expand Up @@ -151,6 +151,11 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return Promise.resolve(result.response === 0);
}

async showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise<boolean>):
Promise<boolean> {
throw new Error('Not implemented.');
}

isDev(): boolean {
return isDev();
}
Expand Down
4 changes: 4 additions & 0 deletions src/enums/cipherRepromptType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum CipherRepromptType {
None = 0,
Password = 1,
}
1 change: 1 addition & 0 deletions src/enums/eventType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum EventType {
Cipher_ClientAutofilled = 1114,
Cipher_SoftDeleted = 1115,
Cipher_Restored = 1116,
Cipher_ClientToggledCardNumberVisible = 1117,

Collection_Created = 1300,
Collection_Updated = 1301,
Expand Down
3 changes: 3 additions & 0 deletions src/models/data/cipherData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CipherRepromptType } from '../../enums/cipherRepromptType';
import { CipherType } from '../../enums/cipherType';

import { AttachmentData } from './attachmentData';
Expand Down Expand Up @@ -33,6 +34,7 @@ export class CipherData {
passwordHistory?: PasswordHistoryData[];
collectionIds?: string[];
deletedDate: string;
reprompt: CipherRepromptType;

constructor(response?: CipherResponse, userId?: string, collectionIds?: string[]) {
if (response == null) {
Expand All @@ -53,6 +55,7 @@ export class CipherData {
this.notes = response.notes;
this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds;
this.deletedDate = response.deletedDate;
this.reprompt = response.reprompt;

switch (this.type) {
case CipherType.Login:
Expand Down
4 changes: 4 additions & 0 deletions src/models/domain/cipher.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CipherRepromptType } from '../../enums/cipherRepromptType';
import { CipherType } from '../../enums/cipherType';

import { CipherData } from '../data/cipherData';
Expand Down Expand Up @@ -37,6 +38,7 @@ export class Cipher extends Domain {
passwordHistory: Password[];
collectionIds: string[];
deletedDate: Date;
reprompt: CipherRepromptType;

constructor(obj?: CipherData, alreadyEncrypted: boolean = false, localData: any = null) {
super();
Expand Down Expand Up @@ -66,6 +68,7 @@ export class Cipher extends Domain {
this.collectionIds = obj.collectionIds;
this.localData = localData;
this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : null;
this.reprompt = obj.reprompt;

switch (this.type) {
case CipherType.Login:
Expand Down Expand Up @@ -183,6 +186,7 @@ export class Cipher extends Domain {
c.type = this.type;
c.collectionIds = this.collectionIds;
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
c.reprompt = this.reprompt;

this.buildDataModel(this, c, {
name: null,
Expand Down
3 changes: 3 additions & 0 deletions src/models/request/cipherRequest.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CipherRepromptType } from '../../enums/cipherRepromptType';
import { CipherType } from '../../enums/cipherType';

import { Cipher } from '../domain/cipher';
Expand Down Expand Up @@ -29,6 +30,7 @@ export class CipherRequest {
attachments: { [id: string]: string; };
attachments2: { [id: string]: AttachmentRequest; };
lastKnownRevisionDate: Date;
reprompt: CipherRepromptType;

constructor(cipher: Cipher) {
this.type = cipher.type;
Expand All @@ -38,6 +40,7 @@ export class CipherRequest {
this.notes = cipher.notes ? cipher.notes.encryptedString : null;
this.favorite = cipher.favorite;
this.lastKnownRevisionDate = cipher.revisionDate;
this.reprompt = cipher.reprompt;

switch (this.type) {
case CipherType.Login:
Expand Down
4 changes: 4 additions & 0 deletions src/models/response/cipherResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AttachmentResponse } from './attachmentResponse';
import { BaseResponse } from './baseResponse';
import { PasswordHistoryResponse } from './passwordHistoryResponse';

import { CipherRepromptType } from '../../enums/cipherRepromptType';
import { CardApi } from '../api/cardApi';
import { FieldApi } from '../api/fieldApi';
import { IdentityApi } from '../api/identityApi';
Expand Down Expand Up @@ -29,6 +30,7 @@ export class CipherResponse extends BaseResponse {
passwordHistory: PasswordHistoryResponse[];
collectionIds: string[];
deletedDate: string;
reprompt: CipherRepromptType;

constructor(response: any) {
super(response);
Expand Down Expand Up @@ -84,5 +86,7 @@ export class CipherResponse extends BaseResponse {
if (passwordHistory != null) {
this.passwordHistory = passwordHistory.map((h: any) => new PasswordHistoryResponse(h));
}

this.reprompt = this.getResponseProperty('Reprompt') || CipherRepromptType.None;
}
}
4 changes: 4 additions & 0 deletions src/models/view/cardView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export class CardView implements View {
return this.code != null ? '•'.repeat(this.code.length) : null;
}

get maskedNumber(): string {
return this.number != null ? '•'.repeat(this.number.length) : null;
}

get brand(): string {
return this._brand;
}
Expand Down
Loading

0 comments on commit a72c8a6

Please sign in to comment.