diff --git a/jslib b/jslib index b6f102938f..a72c8a60c1 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit b6f102938fe7c17631cb1b2e356438c5e4456529 +Subproject commit a72c8a60c1b7a6980bceee456c53a9ea7b9b3451 diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 667a625dbe..4beda13991 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -1703,5 +1703,14 @@ }, "sendOptionsPolicyInEffect": { "message": "One or more organization policies are affecting your Send options." + }, + "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/background/main.background.ts b/src/background/main.background.ts index c8a9947fd7..5f63542781 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -1,4 +1,5 @@ import { CipherType } from 'jslib/enums'; +import { CipherRepromptType } from 'jslib/enums/cipherRepromptType'; import { ApiService, @@ -63,6 +64,7 @@ import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/po import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service'; import { SendService as SendServiceAbstraction } from 'jslib/abstractions/send.service'; import { SystemService as SystemServiceAbstraction } from 'jslib/abstractions/system.service'; +import { AutofillService as AutofillServiceAbstraction } from '../services/abstractions/autofill.service'; import { Utils } from 'jslib/misc/utils'; @@ -86,8 +88,6 @@ import BrowserStorageService from '../services/browserStorage.service'; import I18nService from '../services/i18n.service'; import VaultTimeoutService from '../services/vaultTimeout.service'; -import { AutofillService as AutofillServiceAbstraction } from '../services/abstractions/autofill.service'; - export default class MainBackground { messagingService: MessagingServiceAbstraction; storageService: StorageServiceAbstraction; @@ -583,7 +583,7 @@ export default class MainBackground { } private async loadLoginContextMenuOptions(cipher: any) { - if (cipher == null || cipher.type !== CipherType.Login) { + if (cipher == null || cipher.type !== CipherType.Login || cipher.reprompt !== CipherRepromptType.None) { return; } diff --git a/src/popup/app.component.ts b/src/popup/app.component.ts index 43cba5d5e0..5f42fa5a8c 100644 --- a/src/popup/app.component.ts +++ b/src/popup/app.component.ts @@ -33,6 +33,7 @@ import { StorageService } from 'jslib/abstractions/storage.service'; import { ConstantsService } from 'jslib/services/constants.service'; +import BrowserPlatformUtilsService from 'src/services/browserPlatformUtils.service'; import { routerTransition } from './app-routing.animations'; @Component({ @@ -105,6 +106,8 @@ export class AppComponent implements OnInit { }); } else if (msg.command === 'showDialog') { await this.showDialog(msg); + } else if (msg.command === 'showPasswordDialog') { + await this.showPasswordDialog(msg); } else if (msg.command === 'showToast') { this.ngZone.run(() => { this.showToast(msg); @@ -248,4 +251,30 @@ export class AppComponent implements OnInit { confirmed: confirmed.value, }); } + + private async showPasswordDialog(msg: any) { + const platformUtils = this.platformUtilsService as BrowserPlatformUtilsService; + const result = await Swal.fire({ + heightAuto: false, + title: msg.title, + input: 'password', + text: msg.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 platformUtils.resolvePasswordDialogPromise(msg.dialogId, false, value)) { + return false; + } + + return this.i18nService.t('invalidMasterPassword'); + }, + }); + + platformUtils.resolvePasswordDialogPromise(msg.dialogId, true, null); + } } diff --git a/src/popup/components/action-buttons.component.ts b/src/popup/components/action-buttons.component.ts index d1bc61e36e..b61233dea5 100644 --- a/src/popup/components/action-buttons.component.ts +++ b/src/popup/components/action-buttons.component.ts @@ -14,12 +14,11 @@ import { CipherView } from 'jslib/models/view/cipherView'; import { EventService } from 'jslib/abstractions/event.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PasswordRepromptService } from 'jslib/abstractions/passwordReprompt.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { TotpService } from 'jslib/abstractions/totp.service'; import { UserService } from 'jslib/abstractions/user.service'; -import { PopupUtilsService } from '../services/popup-utils.service'; - @Component({ selector: 'app-action-buttons', templateUrl: 'action-buttons.component.html', @@ -35,7 +34,8 @@ export class ActionButtonsComponent { constructor(private toasterService: ToasterService, private i18nService: I18nService, 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(); @@ -46,6 +46,10 @@ export class ActionButtonsComponent { } async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) { + if (this.passwordRepromptService.protectedFields().includes(aType) && !await this.passwordRepromptService.showPasswordPrompt()) { + return; + } + if (value == null || aType === 'TOTP' && !this.displayTotpCopyButton(cipher)) { return; } else if (value === cipher.login.totp) { diff --git a/src/popup/scss/plugins.scss b/src/popup/scss/plugins.scss index 59cbbdf442..a28a3026d4 100644 --- a/src/popup/scss/plugins.scss +++ b/src/popup/scss/plugins.scss @@ -211,6 +211,10 @@ $fa-font-path: "~font-awesome/fonts"; } } } + + .swal2-validation-message { + margin-top: 20px; + } } date-input-polyfill { diff --git a/src/popup/send/send-add-edit.component.ts b/src/popup/send/send-add-edit.component.ts index 60539756ff..d90b1302e2 100644 --- a/src/popup/send/send-add-edit.component.ts +++ b/src/popup/send/send-add-edit.component.ts @@ -16,6 +16,7 @@ import { MessagingService } from 'jslib/abstractions/messaging.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PolicyService } from 'jslib/abstractions/policy.service'; import { SendService } from 'jslib/abstractions/send.service'; +import { TokenService } from 'jslib/abstractions/token.service'; import { UserService } from 'jslib/abstractions/user.service'; import { PopupUtilsService } from '../services/popup-utils.service'; diff --git a/src/popup/services/services.module.ts b/src/popup/services/services.module.ts index 634c564dd0..45b359112a 100644 --- a/src/popup/services/services.module.ts +++ b/src/popup/services/services.module.ts @@ -31,6 +31,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service'; import { MessagingService } from 'jslib/abstractions/messaging.service'; import { NotificationsService } from 'jslib/abstractions/notifications.service'; import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; +import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib/abstractions/passwordReprompt.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PolicyService } from 'jslib/abstractions/policy.service'; import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service'; @@ -43,6 +44,7 @@ import { TokenService } from 'jslib/abstractions/token.service'; import { TotpService } from 'jslib/abstractions/totp.service'; import { UserService } from 'jslib/abstractions/user.service'; import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service'; +import { PasswordRepromptService } from 'jslib/services/passwordReprompt.service'; import { AutofillService } from '../../services/abstractions/autofill.service'; import BrowserMessagingService from '../../services/browserMessaging.service'; @@ -63,11 +65,13 @@ function getBgService(service: string) { }; } -export const stateService = new StateService(); -export const messagingService = new BrowserMessagingService(); -export const searchService = new PopupSearchService(getBgService('searchService')(), +const stateService = new StateService(); +const messagingService = new BrowserMessagingService(); +const searchService = new PopupSearchService(getBgService('searchService')(), getBgService('cipherService')(), getBgService('consoleLogService')(), getBgService('i18nService')()); +const passwordRepromptService = new PasswordRepromptService(getBgService('i18nService')(), + getBgService('cryptoService')(), getBgService('platformUtilsService')()); export function initFactory(platformUtilsService: PlatformUtilsService, i18nService: I18nService, storageService: StorageService, popupUtilsService: PopupUtilsService): Function { @@ -174,6 +178,7 @@ export function initFactory(platformUtilsService: PlatformUtilsService, i18nServ useFactory: () => getBgService('i18nService')().translationLocale, deps: [], }, + { provide: PasswordRepromptServiceAbstraction, useValue: passwordRepromptService }, ], }) export class ServicesModule { diff --git a/src/popup/vault/add-edit.component.html b/src/popup/vault/add-edit.component.html index eba3c193cc..271f17e894 100644 --- a/src/popup/vault/add-edit.component.html +++ b/src/popup/vault/add-edit.component.html @@ -83,10 +83,19 @@ -
- - +
+
+ + +
+
+ + + +
@@ -271,6 +280,11 @@
+
+ + +
{{'attachments' | i18n}}
diff --git a/src/popup/vault/current-tab.component.ts b/src/popup/vault/current-tab.component.ts index 9134ab427a..b944f33386 100644 --- a/src/popup/vault/current-tab.component.ts +++ b/src/popup/vault/current-tab.component.ts @@ -14,12 +14,14 @@ import { BrowserApi } from '../../browser/browserApi'; import { BroadcasterService } from 'jslib/angular/services/broadcaster.service'; +import { CipherRepromptType } from 'jslib/enums/cipherRepromptType'; import { CipherType } from 'jslib/enums/cipherType'; import { CipherView } from 'jslib/models/view/cipherView'; import { CipherService } from 'jslib/abstractions/cipher.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PasswordRepromptService } from 'jslib/abstractions/passwordReprompt.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { SearchService } from 'jslib/abstractions/search.service'; import { StorageService } from 'jslib/abstractions/storage.service'; @@ -61,7 +63,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private toasterService: ToasterService, private i18nService: I18nService, private router: Router, private ngZone: NgZone, private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, private syncService: SyncService, - private searchService: SearchService, private storageService: StorageService) { + private searchService: SearchService, private storageService: StorageService, + private passwordRepromptService: PasswordRepromptService) { } async ngOnInit() { @@ -128,6 +131,10 @@ export class CurrentTabComponent implements OnInit, OnDestroy { } async fillCipher(cipher: CipherView) { + if (cipher.reprompt !== CipherRepromptType.None && !await this.passwordRepromptService.showPasswordPrompt()) { + return; + } + this.totpCode = null; if (this.totpTimeout != null) { window.clearTimeout(this.totpTimeout); diff --git a/src/popup/vault/view.component.html b/src/popup/vault/view.component.html index c19b581cfa..a97d62b893 100644 --- a/src/popup/vault/view.component.html +++ b/src/popup/vault/view.component.html @@ -99,11 +99,17 @@
{{'number' | i18n}} - {{cipher.card.number}} + {{cipher.card.maskedNumber}} + {{cipher.card.number}}
diff --git a/src/popup/vault/view.component.ts b/src/popup/vault/view.component.ts index 13168872ea..6c7b936021 100644 --- a/src/popup/vault/view.component.ts +++ b/src/popup/vault/view.component.ts @@ -16,6 +16,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 { TokenService } from 'jslib/abstractions/token.service'; import { TotpService } from 'jslib/abstractions/totp.service'; @@ -53,10 +54,12 @@ export class ViewComponent extends BaseViewComponent { private router: Router, private location: Location, broadcasterService: BroadcasterService, ngZone: NgZone, changeDetectorRef: ChangeDetectorRef, userService: UserService, - eventService: EventService, private autofillService: AutofillService, apiService: ApiService, - private messagingService: MessagingService, private popupUtilsService: PopupUtilsService) { + eventService: EventService, private autofillService: AutofillService, + private messagingService: MessagingService, private popupUtilsService: PopupUtilsService, + apiService: ApiService, 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() { @@ -112,32 +115,45 @@ export class ViewComponent extends BaseViewComponent { await this.loadPageDetails(); } - edit() { + async edit() { if (this.cipher.isDeleted) { return false; } - super.edit(); + if (!await super.edit()) { + return false; + } + this.router.navigate(['/edit-cipher'], { queryParams: { cipherId: this.cipher.id } }); + return true; } - clone() { + async clone() { if (this.cipher.isDeleted) { return false; } - super.clone(); + + if (!await super.clone()) { + return false; + } + this.router.navigate(['/clone-cipher'], { queryParams: { cloneMode: true, cipherId: this.cipher.id, }, }); + return true; } - share() { - super.share(); + async share() { + if (!await super.share()) { + return false; + } + if (this.cipher.organizationId == null) { this.router.navigate(['/share-cipher'], { replaceUrl: true, queryParams: { cipherId: this.cipher.id } }); } + return true; } async fillCipher() { @@ -220,6 +236,10 @@ export class ViewComponent extends BaseViewComponent { } private async doAutofill() { + if (!await this.promptPassword()) { + return false; + } + if (this.pageDetails == null || this.pageDetails.length === 0) { this.platformUtilsService.showToast('error', null, this.i18nService.t('autofillError')); diff --git a/src/services/autofill.service.ts b/src/services/autofill.service.ts index 841e27098d..f75e4a5d95 100644 --- a/src/services/autofill.service.ts +++ b/src/services/autofill.service.ts @@ -20,6 +20,7 @@ import { } from 'jslib/abstractions'; import { EventService } from 'jslib/abstractions/event.service'; +import { CipherRepromptType } from 'jslib/enums/cipherRepromptType'; import { EventType } from 'jslib/enums/eventType'; const CardAttributes: string[] = ['autoCompleteType', 'data-stripe', 'htmlName', 'htmlID', 'label-tag', @@ -254,6 +255,10 @@ export default class AutofillService implements AutofillServiceInterface { } } + if (cipher.reprompt !== CipherRepromptType.None) { + return; + } + const totpCode = await this.doAutoFill({ cipher: cipher, pageDetails: pageDetails, diff --git a/src/services/browserPlatformUtils.service.ts b/src/services/browserPlatformUtils.service.ts index 08f847933f..9d47059653 100644 --- a/src/services/browserPlatformUtils.service.ts +++ b/src/services/browserPlatformUtils.service.ts @@ -12,6 +12,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService identityClientId: string = 'browser'; private showDialogResolves = new Map void, date: Date }>(); + private passwordDialogResolves = new Map Promise, date: Date }>(); private deviceCache: DeviceType = null; private prefersColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)'); @@ -149,6 +150,33 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService }); } + async showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise) { + const dialogId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + + this.messagingService.send('showPasswordDialog', { + title: title, + body: body, + dialogId: dialogId, + }); + + return new Promise(resolve => { + this.passwordDialogResolves.set(dialogId, { + tryResolve: async (canceled: boolean, password: string) => { + if (canceled) { + resolve(false); + return false; + } + + if (await passwordValidation(password)) { + resolve(true); + return true; + } + }, + date: new Date(), + }); + }); + } + isDev(): boolean { return process.env.ENV === 'development'; } @@ -256,16 +284,33 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService } // Clean up old promises - const deleteIds: number[] = []; this.showDialogResolves.forEach((val, key) => { const age = new Date().getTime() - val.date.getTime(); if (age > DialogPromiseExpiration) { - deleteIds.push(key); + this.showDialogResolves.delete(key); } }); - deleteIds.forEach(id => { - this.showDialogResolves.delete(id); + } + + async resolvePasswordDialogPromise(dialogId: number, canceled: boolean, password: string): Promise { + let result = false; + if (this.passwordDialogResolves.has(dialogId)) { + const resolveObj = this.passwordDialogResolves.get(dialogId); + if (await resolveObj.tryResolve(canceled, password)) { + this.passwordDialogResolves.delete(dialogId); + result = true; + } + } + + // Clean up old promises + this.passwordDialogResolves.forEach((val, key) => { + const age = new Date().getTime() - val.date.getTime(); + if (age > DialogPromiseExpiration) { + this.passwordDialogResolves.delete(key); + } }); + + return result; } async supportsBiometric() {