1
0
mirror of https://github.com/bitwarden/desktop.git synced 2024-11-24 11:55:50 +01:00

Password reprompt (#838)

* Add support for password reprompt
This commit is contained in:
Oscar Hinton 2021-05-04 21:32:03 +02:00 committed by GitHub
parent 20b8a83dd8
commit 42564285d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 114 additions and 23 deletions

2
jslib

@ -1 +1 @@
Subproject commit e298ecfee378e6620e8e086362d9af744c5af8ff
Subproject commit a72c8a60c1b7a6980bceee456c53a9ea7b9b3451

View File

@ -7,14 +7,13 @@ import {
import { ToasterModule } from 'angular2-toaster';
import { ElectronLogService } from 'jslib/electron/services/electronLog.service';
import { ElectronPlatformUtilsService } from 'jslib/electron/services/electronPlatformUtils.service';
import { ElectronRendererMessagingService } from 'jslib/electron/services/electronRendererMessaging.service';
import { ElectronRendererSecureStorageService } from 'jslib/electron/services/electronRendererSecureStorage.service';
import { ElectronRendererStorageService } from 'jslib/electron/services/electronRendererStorage.service';
import { isDev } from 'jslib/electron/utils';
import { DeviceType } from 'jslib/enums/deviceType';
import { ElectronPlatformUtilsService } from '../services/electronPlatformUtils.service';
import { I18nService } from '../services/i18n.service';
import { NativeMessagingService } from '../services/nativeMessaging.service';
@ -54,7 +53,6 @@ import { VaultTimeoutService } from 'jslib/services/vaultTimeout.service';
import { WebCryptoFunctionService } from 'jslib/services/webCryptoFunction.service';
import { ApiService as ApiServiceAbstraction } from 'jslib/abstractions/api.service';
import { AppIdService as AppIdServiceAbstraction } from 'jslib/abstractions/appId.service';
import { AuditService as AuditServiceAbstraction } from 'jslib/abstractions/audit.service';
import { AuthService as AuthServiceAbstraction } from 'jslib/abstractions/auth.service';
import { CipherService as CipherServiceAbstraction } from 'jslib/abstractions/cipher.service';
@ -73,6 +71,7 @@ import { NotificationsService as NotificationsServiceAbstraction } from 'jslib/a
import {
PasswordGenerationService as PasswordGenerationServiceAbstraction,
} from 'jslib/abstractions/passwordGeneration.service';
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib/abstractions/passwordReprompt.service';
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/policy.service';
import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service';
@ -86,6 +85,7 @@ import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/toke
import { TotpService as TotpServiceAbstraction } from 'jslib/abstractions/totp.service';
import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.service';
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from 'jslib/abstractions/vaultTimeout.service';
import { PasswordRepromptService } from 'jslib/services/passwordReprompt.service';
const logService = new ElectronLogService();
const i18nService = new I18nService(window.navigator.language, './locales');
@ -137,6 +137,7 @@ const systemService = new SystemService(storageService, vaultTimeoutService, mes
null);
const nativeMessagingService = new NativeMessagingService(cryptoFunctionService, cryptoService, platformUtilsService,
logService, i18nService, userService, messagingService, vaultTimeoutService, storageService);
const passwordRepromptService = new PasswordRepromptService(i18nService, cryptoService, platformUtilsService);
containerService.attachToGlobal(window);
@ -223,6 +224,7 @@ export function initFactory(): Function {
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{ provide: NativeMessagingService, useValue: nativeMessagingService },
{ provide: FileUploadServiceAbstraction, useValue: fileUploadService },
{ provide: PasswordRepromptServiceAbstraction, useValue: passwordRepromptService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,

View File

@ -66,9 +66,19 @@
<input id="cardCardholderName" type="text" name="Card.CardCardholderName"
[(ngModel)]="cipher.card.cardholderName">
</div>
<div class="box-content-row" appBoxRow>
<label for="cardNumber">{{'number' | i18n}}</label>
<input id="cardNumber" type="text" name="Card.Number" [(ngModel)]="cipher.card.number">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="cardNumber">{{'number' | i18n}}</label>
<input id="cardNumber" class="monospaced" type="{{showCardNumber ? 'text' : 'password'}}"
name="Card.Number" [(ngModel)]="cipher.card.number">
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick appBlurClick role="button"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleCardNumber()">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showCardNumber, 'fa-eye-slash': showCardNumber}"></i>
</a>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardBrand">{{'brand' | i18n}}</label>
@ -239,6 +249,11 @@
<label for="favorite">{{'favorite' | i18n}}</label>
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="passwordPrompt">{{'passwordPrompt' | i18n}}</label>
<input id="passwordPrompt" type="checkbox" name="PasswordPrompt" [ngModel]="reprompt"
(change)="repromptChanged()">
</div>
<a class="box-content-row box-content-row-flex text-default" href="#" appStopClick appBlurClick
(click)="attachments()" *ngIf="editMode && !cloneMode" role="button">
<div class="row-main">{{'attachments' | i18n}}</div>

View File

@ -39,11 +39,13 @@ import { FolderView } from 'jslib/models/view/folderView';
import { EventService } from 'jslib/abstractions/event.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordRepromptService } from 'jslib/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { TotpService } from 'jslib/abstractions/totp.service';
import { UserService } from 'jslib/abstractions/user.service';
import { invokeMenu, RendererMenuItem } from 'jslib/electron/utils';
import { CipherRepromptType } from 'jslib/enums/cipherRepromptType';
const BroadcasterSubscriptionId = 'VaultComponent';
@ -84,7 +86,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private ngZone: NgZone, private syncService: SyncService,
private toasterService: ToasterService, private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService, private eventService: EventService,
private totpService: TotpService, private userService: UserService) { }
private totpService: TotpService, private userService: UserService, private passwordRepromptService: PasswordRepromptService) { }
async ngOnInit() {
this.userHasPremiumAccess = await this.userService.canAccessPremium();
@ -129,7 +131,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const uCipher = uComponent != null ? uComponent.cipher : null;
if (this.cipherId != null && uCipher != null && uCipher.id === this.cipherId &&
uCipher.login != null && uCipher.login.username != null) {
this.copyValue(uCipher.login.username, 'username');
this.copyValue(uCipher, uCipher.login.username, 'username', 'Username');
}
break;
case 'copyPassword':
@ -137,7 +139,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const pCipher = pComponent != null ? pComponent.cipher : null;
if (this.cipherId != null && pCipher != null && pCipher.id === this.cipherId &&
pCipher.login != null && pCipher.login.password != null && pCipher.viewPassword) {
this.copyValue(pCipher.login.password, 'password');
this.copyValue(pCipher, pCipher.login.password, 'password', 'Password');
}
break;
case 'copyTotp':
@ -146,7 +148,7 @@ export class VaultComponent implements OnInit, OnDestroy {
if (this.cipherId != null && tCipher != null && tCipher.id === this.cipherId &&
tCipher.login != null && tCipher.login.hasTotp && this.userHasPremiumAccess) {
const value = await this.totpService.getCode(tCipher.login.totp);
this.copyValue(value, 'verificationCodeTotp');
this.copyValue(tCipher, value, 'verificationCodeTotp', 'TOTP');
}
default:
detectChanges = false;
@ -276,14 +278,14 @@ export class VaultComponent implements OnInit, OnDestroy {
if (cipher.login.username != null) {
menu.push({
label: this.i18nService.t('copyUsername'),
click: () => this.copyValue(cipher.login.username, 'username'),
click: () => this.copyValue(cipher, cipher.login.username, 'username', 'Username'),
});
}
if (cipher.login.password != null && cipher.viewPassword) {
menu.push({
label: this.i18nService.t('copyPassword'),
click: () => {
this.copyValue(cipher.login.password, 'password');
this.copyValue(cipher, cipher.login.password, 'password', 'Password');
this.eventService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
},
});
@ -293,7 +295,7 @@ export class VaultComponent implements OnInit, OnDestroy {
label: this.i18nService.t('copyVerificationCodeTotp'),
click: async () => {
const value = await this.totpService.getCode(cipher.login.totp);
this.copyValue(value, 'verificationCodeTotp');
this.copyValue(cipher, value, 'verificationCodeTotp', 'TOTP');
},
});
}
@ -305,14 +307,14 @@ export class VaultComponent implements OnInit, OnDestroy {
if (cipher.card.number != null) {
menu.push({
label: this.i18nService.t('copyNumber'),
click: () => this.copyValue(cipher.card.number, 'number'),
click: () => this.copyValue(cipher, cipher.card.number, 'number', 'Card Number'),
});
}
if (cipher.card.code != null) {
menu.push({
label: this.i18nService.t('copySecurityCode'),
click: () => {
this.copyValue(cipher.card.code, 'securityCode');
this.copyValue(cipher, cipher.card.code, 'securityCode', 'Security Code');
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
},
});
@ -330,6 +332,8 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
} else if (this.dirtyInput() && await this.wantsToSaveChanges()) {
return;
} else if (cipher.reprompt !== CipherRepromptType.None && !await this.passwordRepromptService.showPasswordPrompt()) {
return;
}
this.cipherId = cipher.id;
@ -342,6 +346,8 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
} else if (this.dirtyInput() && await this.wantsToSaveChanges()) {
return;
} else if (cipher.reprompt !== CipherRepromptType.None && !await this.passwordRepromptService.showPasswordPrompt()) {
return;
}
this.cipherId = cipher.id;
@ -639,8 +645,13 @@ export class VaultComponent implements OnInit, OnDestroy {
this.functionWithChangeDetection(() => this.addCipher(type));
}
private copyValue(value: string, labelI18nKey: string) {
this.functionWithChangeDetection(() => {
private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) {
this.functionWithChangeDetection(async () => {
if (cipher.reprompt !== CipherRepromptType.None && this.passwordRepromptService.protectedFields().includes(aType) &&
!await this.passwordRepromptService.showPasswordPrompt()) {
return;
}
this.platformUtilsService.copyToClipboard(value);
this.toasterService.popAsync('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(labelI18nKey)));

View File

@ -87,11 +87,17 @@
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.number">
<div class="row-main">
<span class="row-label">{{'number' | i18n}}</span>
{{cipher.card.number}}
<span [hidden]="showCardNumber" class="monospaced">{{cipher.card.maskedNumber}}</span>
<span [hidden]="!showCardNumber" class="monospaced">{{cipher.card.number}}</span>
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="toggleCardNumber()" role="button">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showCardNumber, 'fa-eye-slash': showCardNumber}"></i>
</a>
<a class="row-btn" href="#" appStopClick appA11yTitle="{{'copyNumber' | i18n}}"
(click)="copy(cipher.card.number, 'number', 'Number')" role="button">
(click)="copy(cipher.card.number, 'number', 'Card Number')" role="button">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</a>
</div>

View File

@ -14,6 +14,7 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
import { EventService } from 'jslib/abstractions/event.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordRepromptService } from 'jslib/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { TokenService } from 'jslib/abstractions/token.service';
@ -41,9 +42,10 @@ export class ViewComponent extends BaseViewComponent implements OnChanges {
auditService: AuditService, broadcasterService: BroadcasterService,
ngZone: NgZone, changeDetectorRef: ChangeDetectorRef,
userService: UserService, eventService: EventService, apiService: ApiService,
private messagingService: MessagingService, private storageService: StorageService) {
private messagingService: MessagingService, passwordRepromptService: PasswordRepromptService) {
super(cipherService, totpService, tokenService, i18nService, cryptoService, platformUtilsService,
auditService, window, broadcasterService, ngZone, changeDetectorRef, userService, eventService, apiService);
auditService, window, broadcasterService, ngZone, changeDetectorRef, userService, eventService,
apiService, passwordRepromptService);
}
ngOnInit() {
super.ngOnInit();
@ -72,7 +74,7 @@ export class ViewComponent extends BaseViewComponent implements OnChanges {
this.onViewCipherPasswordHistory.emit(this.cipher);
}
copy(value: string, typeI18nKey: string, aType: string) {
async copy(value: string, typeI18nKey: string, aType: string) {
super.copy(value, typeI18nKey, aType);
this.messagingService.send('minimizeOnCopy');
}

View File

@ -1668,5 +1668,14 @@
},
"emailVerificationRequiredDesc": {
"message": "You must verify your email to use this feature."
},
"passwordPrompt": {
"message": "Master password re-prompt"
},
"passwordConfirmation": {
"message": "Master password confirmation"
},
"passwordConfirmationDesc": {
"message": "This action is protected. To continue, please re-enter your master password to verify your identity."
}
}

View File

@ -203,4 +203,8 @@ $fa-font-path: "~font-awesome/fonts";
}
}
}
.swal2-validation-message {
margin-top: 20px;
}
}

View File

@ -0,0 +1,42 @@
import Swal from 'sweetalert2';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import {
ElectronPlatformUtilsService as BaseElectronPlatformUtilsService
} from 'jslib/electron/services/electronPlatformUtils.service';
export class ElectronPlatformUtilsService extends BaseElectronPlatformUtilsService {
constructor(i18nService: I18nService, messagingService: MessagingService,
isDesktopApp: boolean, storageService: StorageService) {
super(i18nService, messagingService, isDesktopApp, storageService);
}
async showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise<boolean>):
Promise<boolean> {
const result = await Swal.fire({
heightAuto: false,
title: title,
input: 'password',
text: body,
confirmButtonText: this.i18nService.t('ok'),
showCancelButton: true,
cancelButtonText: this.i18nService.t('cancel'),
inputAttributes: {
autocapitalize: 'off',
autocorrect: 'off',
},
inputValidator: async (value: string): Promise<any> => {
if (await passwordValidation(value)) {
return false;
}
return this.i18nService.t('invalidMasterPassword');
},
});
return result.isConfirmed;
}
}