From 894d245361c404323fa8d36247d4327ad5c214d7 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 12 Oct 2020 18:01:34 +0200 Subject: [PATCH] Ensure biometric unlock works even if popup is not in focus --- src/_locales/en/messages.json | 5 +- src/background/main.background.ts | 12 +++-- src/background/nativeMessaging.background.ts | 44 ++++++++++++++---- src/background/runtime.background.ts | 1 + src/popup/settings/settings.component.ts | 49 +++++++++++--------- src/services/browserPlatformUtils.service.ts | 11 ++--- 6 files changed, 80 insertions(+), 42 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 5354fe5e4e1..ca584a831b2 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -1251,7 +1251,10 @@ "message": "Unlock with biometric" }, "awaitDesktop": { - "message": "Awaiting biometric confirmation from desktop application." + "message": "Awaiting confirmation from desktop" + }, + "awaitDesktopDesc": { + "message": "Please confirm using biometrics in the Bitwarden desktop application to enable biometrics for browser." }, "lockWithMasterPassOnRestart": { "message": "Lock with master password on browser restart" diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 19758b94374..27916cef21f 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -141,8 +141,6 @@ export default class MainBackground { private nativeMessagingBackground: NativeMessagingBackground; constructor() { - this.nativeMessagingBackground = new NativeMessagingBackground(); - // Services this.messagingService = new BrowserMessagingService(); this.platformUtilsService = new BrowserPlatformUtilsService(this.messagingService, @@ -150,7 +148,14 @@ export default class MainBackground { if (this.systemService != null) { this.systemService.clearClipboard(clipboardValue, clearMs); } - }, this.nativeMessagingBackground); + }, + () => { + if (this.nativeMessagingBackground != null) { + const promise = this.nativeMessagingBackground.await(); + this.nativeMessagingBackground.send({command: 'biometricUnlock'}) + return promise.then((result) => result.response === 'unlocked'); + } + }); this.storageService = new BrowserStorageService(this.platformUtilsService); this.secureStorageService = new BrowserStorageService(this.platformUtilsService); this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); @@ -228,6 +233,7 @@ export default class MainBackground { this.platformUtilsService as BrowserPlatformUtilsService, this.storageService, this.i18nService, this.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService, this.environmentService); + this.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.vaultTimeoutService, this.runtimeBackground); this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, this.platformUtilsService, this.analytics, this.vaultTimeoutService); diff --git a/src/background/nativeMessaging.background.ts b/src/background/nativeMessaging.background.ts index 30028acb8c4..7b3b93c7ff8 100644 --- a/src/background/nativeMessaging.background.ts +++ b/src/background/nativeMessaging.background.ts @@ -1,4 +1,8 @@ +import { CryptoService, VaultTimeoutService } from 'jslib/abstractions'; +import { StorageService } from 'jslib/abstractions/storage.service'; +import { ConstantsService } from 'jslib/services'; import { BrowserApi } from '../browser/browserApi'; +import RuntimeBackground from './runtime.background'; export class NativeMessagingBackground { private connected = false; @@ -6,18 +10,16 @@ export class NativeMessagingBackground { private resolver: any = null; + constructor(private storageService: StorageService, private cryptoService: CryptoService, + private vaultTimeoutService: VaultTimeoutService, private runtimeBackground: RuntimeBackground) {} + connect() { this.port = BrowserApi.connectNative('com.8bit.bitwarden'); this.connected = true; - this.port.onMessage.addListener((msg: any) => { - if (this.resolver) { - this.resolver(msg); - } else { - // tslint:disable-next-line - console.error('NO RESOLVER'); - } - }); + + this.port.onMessage.addListener((msg) => this.onMessage(msg)); + this.port.onDisconnect.addListener(() => { this.connected = false; }); @@ -37,4 +39,30 @@ export class NativeMessagingBackground { this.resolver = resolve; }); } + + private async onMessage(msg: any) { + switch(msg.command) { + case 'biometricUnlock': { + await this.storageService.remove(ConstantsService.biometricAwaitingAcceptance); + + const enabled = await this.storageService.get(ConstantsService.biometricUnlockKey); + if (enabled === null || enabled === false) { + if (msg.response === 'unlocked') { + await this.storageService.save(ConstantsService.biometricUnlockKey, true); + } + + await this.cryptoService.toggleKey(); + } + + if (this.vaultTimeoutService.biometricLocked) { + this.runtimeBackground.processMessage({command: 'unlocked'}, null, null); + this.vaultTimeoutService.biometricLocked = false; + } + } + } + + if (this.resolver) { + this.resolver(msg); + } + } } diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index aea6b05b1f3..8f0ebf7cd62 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -22,6 +22,7 @@ import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service'; import { BrowserApi } from '../browser/browserApi'; import MainBackground from './main.background'; +import { NativeMessagingBackground } from './nativeMessaging.background'; import { Analytics } from 'jslib/misc'; import { Utils } from 'jslib/misc/utils'; diff --git a/src/popup/settings/settings.component.ts b/src/popup/settings/settings.component.ts index 6d1c06463b8..1029fe29f13 100644 --- a/src/popup/settings/settings.component.ts +++ b/src/popup/settings/settings.component.ts @@ -51,7 +51,7 @@ export class SettingsComponent implements OnInit { vaultTimeoutActions: any[]; vaultTimeoutAction: string; pin: boolean = null; - biometric: boolean = null; + biometric: boolean = false; previousVaultTimeout: number = null; constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, @@ -207,40 +207,45 @@ export class SettingsComponent implements OnInit { } async updateBiometric() { - const current = this.biometric; if (this.biometric) { this.biometric = false; + // TODO: Remove biometric stuff + await this.storageService.remove(ConstantsService.biometricUnlockKey); + this.vaultTimeoutService.biometricLocked = false; } else { - const div = document.createElement('div'); - div.innerHTML = `
${this.i18nService.t('awaitDesktop')}
`; - const submitted = Swal.fire({ heightAuto: false, buttonsStyling: false, - html: div, + title: this.i18nService.t('awaitDesktop'), + text: this.i18nService.t('awaitDesktopDesc'), + icon: 'info', + iconHtml: '', showCancelButton: true, cancelButtonText: this.i18nService.t('cancel'), showConfirmButton: false, + allowOutsideClick: false, }); - // TODO: Show waiting message - this.biometric = await this.platformUtilsService.authenticateBiometric(); - Swal.close(); + await this.storageService.save(ConstantsService.biometricAwaitingAcceptance, true); + await this.cryptoService.toggleKey(); - if (this.biometric === false) { - this.platformUtilsService.showToast('error', 'Unable to enable biometrics', 'Ensure the desktop application is running, and browser integration is enabled.'); - } + await Promise.race([ + submitted.then((result) => { + if (result.dismiss === Swal.DismissReason.cancel) { + this.biometric = false; + this.storageService.remove(ConstantsService.biometricAwaitingAcceptance); + } + }), + this.platformUtilsService.authenticateBiometric().then((result) => { + this.biometric = result; + + Swal.close(); + if (this.biometric === false) { + this.platformUtilsService.showToast('error', 'Unable to enable biometrics', 'Ensure the desktop application is running, and browser integration is enabled.'); + } + }) + ]); } - if (this.biometric === current) { - return; - } - if (this.biometric) { - await this.storageService.save(ConstantsService.biometricUnlockKey, true); - } else { - await this.storageService.remove(ConstantsService.biometricUnlockKey); - } - this.vaultTimeoutService.biometricLocked = false; - await this.cryptoService.toggleKey(); } async lock() { diff --git a/src/services/browserPlatformUtils.service.ts b/src/services/browserPlatformUtils.service.ts index fe7e14b7991..e0439f6bcb9 100644 --- a/src/services/browserPlatformUtils.service.ts +++ b/src/services/browserPlatformUtils.service.ts @@ -20,7 +20,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService constructor(private messagingService: MessagingService, private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - private nativeMessagingBackground: NativeMessagingBackground) { } + private biometricCallback: () => Promise) { } getDevice(): DeviceType { if (this.deviceCache) { @@ -293,13 +293,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService return Promise.resolve(true); } - async authenticateBiometric() { - const responsePromise = this.nativeMessagingBackground.await(); - this.nativeMessagingBackground.send({'command': 'biometricUnlock'}); - - const response = await responsePromise; - - return response.response === 'unlocked'; + authenticateBiometric() { + return this.biometricCallback(); } sidebarViewName(): string {