From 42564285d950160dd3c1685618dfd470b6fb3956 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 4 May 2021 21:32:03 +0200 Subject: [PATCH] Password reprompt (#838) * Add support for password reprompt --- jslib | 2 +- src/app/services.module.ts | 8 ++-- src/app/vault/add-edit.component.html | 21 ++++++++-- src/app/vault/vault.component.ts | 33 ++++++++++----- src/app/vault/view.component.html | 10 ++++- src/app/vault/view.component.ts | 8 ++-- src/locales/en/messages.json | 9 ++++ src/scss/plugins.scss | 4 ++ src/services/electronPlatformUtils.service.ts | 42 +++++++++++++++++++ 9 files changed, 114 insertions(+), 23 deletions(-) create mode 100644 src/services/electronPlatformUtils.service.ts diff --git a/jslib b/jslib index e298ecfee37..a72c8a60c1b 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit e298ecfee378e6620e8e086362d9af744c5af8ff +Subproject commit a72c8a60c1b7a6980bceee456c53a9ea7b9b3451 diff --git a/src/app/services.module.ts b/src/app/services.module.ts index 7be969ff86e..97edd4d281b 100644 --- a/src/app/services.module.ts +++ b/src/app/services.module.ts @@ -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, diff --git a/src/app/vault/add-edit.component.html b/src/app/vault/add-edit.component.html index 63e7d77109a..97fbee6e22c 100644 --- a/src/app/vault/add-edit.component.html +++ b/src/app/vault/add-edit.component.html @@ -66,9 +66,19 @@ -
- - +
+
+ + +
+
+ + + +
@@ -239,6 +249,11 @@
+
+ + +
{{'attachments' | i18n}}
diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index c0af15560be..06a393095a1 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -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))); diff --git a/src/app/vault/view.component.html b/src/app/vault/view.component.html index 4161db1721d..e9d16c41bc2 100644 --- a/src/app/vault/view.component.html +++ b/src/app/vault/view.component.html @@ -87,11 +87,17 @@
{{'number' | i18n}} - {{cipher.card.number}} + {{cipher.card.maskedNumber}} + {{cipher.card.number}}
diff --git a/src/app/vault/view.component.ts b/src/app/vault/view.component.ts index 5e554234a75..ef3e1463d22 100644 --- a/src/app/vault/view.component.ts +++ b/src/app/vault/view.component.ts @@ -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'); } diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index c80c9d04009..00f8ac62e33 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -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." } } diff --git a/src/scss/plugins.scss b/src/scss/plugins.scss index 71670785016..5a240788adb 100644 --- a/src/scss/plugins.scss +++ b/src/scss/plugins.scss @@ -203,4 +203,8 @@ $fa-font-path: "~font-awesome/fonts"; } } } + + .swal2-validation-message { + margin-top: 20px; + } } diff --git a/src/services/electronPlatformUtils.service.ts b/src/services/electronPlatformUtils.service.ts new file mode 100644 index 00000000000..ae1c31bc7d6 --- /dev/null +++ b/src/services/electronPlatformUtils.service.ts @@ -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): + Promise { + 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 => { + if (await passwordValidation(value)) { + return false; + } + + return this.i18nService.t('invalidMasterPassword'); + }, + }); + + return result.isConfirmed; + } +}