From f311101ed941b203c8c55c75f225345c5b3a3ec3 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 9 Oct 2020 17:16:15 +0200 Subject: [PATCH] Initial work of biometric unlock for browser --- package-lock.json | 6 +++ package.json | 3 +- src/_locales/en/messages.json | 6 +++ src/background.ts | 10 ----- src/background/contextMenus.background.ts | 4 +- src/background/main.background.ts | 6 ++- src/background/nativeMessaging.background.ts | 40 +++++++++++++++++++ src/browser/browserApi.ts | 8 ++++ src/globals.d.ts | 2 - src/popup/settings/settings.component.html | 4 ++ src/popup/settings/settings.component.ts | 39 ++++++++++++++++++ .../browserPlatformUtils.service.spec.ts | 12 +++--- src/services/browserPlatformUtils.service.ts | 17 +++++--- tsconfig.json | 4 +- 14 files changed, 133 insertions(+), 28 deletions(-) create mode 100644 src/background/nativeMessaging.background.ts diff --git a/package-lock.json b/package-lock.json index 6a8db88c15..fd6c462d7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -777,6 +777,12 @@ "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=", "dev": true }, + "@types/firefox-webext-browser": { + "version": "78.0.1", + "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-78.0.1.tgz", + "integrity": "sha512-0d7oiI9K6Y4efP4Crl3JB88zYl7vaRdLtumqz8v6axMF8RCnK0NaGUjL4DnyQ7GLPo98b+s0BSRalaxAXgvPAQ==", + "dev": true + }, "@types/jasmine": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.12.tgz", diff --git a/package.json b/package.json index 837ea32bbf..3da070d914 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@angular/compiler-cli": "^9.1.12", "@ngtools/webpack": "^9.1.12", "@types/chrome": "^0.0.73", + "@types/firefox-webext-browser": "^78.0.1", "@types/jasmine": "^3.3.12", "@types/lunr": "^2.3.3", "@types/mousetrap": "^1.6.0", @@ -48,7 +49,6 @@ "cross-env": "^5.2.0", "css-loader": "^1.0.0", "del": "^3.0.0", - "mini-css-extract-plugin": "^0.9.0", "file-loader": "^2.0.0", "gulp": "^4.0.0", "gulp-filter": "^5.1.0", @@ -68,6 +68,7 @@ "karma-jasmine": "^2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "karma-typescript": "^4.0.0", + "mini-css-extract-plugin": "^0.9.0", "node-sass": "^4.13.1", "sass-loader": "^7.1.0", "style-loader": "^0.23.0", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index c5667b7b13..5354fe5e4e 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -1247,6 +1247,12 @@ "yourVaultIsLockedPinCode": { "message": "Your vault is locked. Verify your PIN code to continue." }, + "unlockWithBiometric": { + "message": "Unlock with biometric" + }, + "awaitDesktop": { + "message": "Awaiting biometric confirmation from desktop application." + }, "lockWithMasterPassOnRestart": { "message": "Lock with master password on browser restart" }, diff --git a/src/background.ts b/src/background.ts index ef62efffaa..301fe1a289 100644 --- a/src/background.ts +++ b/src/background.ts @@ -4,13 +4,3 @@ const bitwardenMain = (window as any).bitwardenMain = new MainBackground(); bitwardenMain.bootstrap().then(() => { // Finished bootstrapping }); - -const port = chrome.runtime.connectNative('com.8bit.bitwarden'); - -port.onMessage.addListener((msg: any) => { - console.log('Received' + msg); -}); -port.onDisconnect.addListener(() => { - console.log('Disconnected'); -}); -port.postMessage({ text: 'Hello, my_application' }); diff --git a/src/background/contextMenus.background.ts b/src/background/contextMenus.background.ts index fb335c879f..7a34acd659 100644 --- a/src/background/contextMenus.background.ts +++ b/src/background/contextMenus.background.ts @@ -55,8 +55,8 @@ export default class ContextMenusBackground { private async cipherAction(info: any) { const id = info.menuItemId.split('_')[1]; if (id === 'noop') { - if (chrome.browserAction && chrome.browserAction.openPopup) { - chrome.browserAction.openPopup(); + if (chrome.browserAction && (chrome.browserAction as any).openPopup) { + (chrome.browserAction as any).openPopup(); } return; } diff --git a/src/background/main.background.ts b/src/background/main.background.ts index fa30ca2481..c5a0cf0538 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -82,6 +82,7 @@ import I18nService from '../services/i18n.service'; import { PopupUtilsService } from '../popup/services/popup-utils.service'; import { AutofillService as AutofillServiceAbstraction } from '../services/abstractions/autofill.service'; +import { NativeMessagingBackground } from './nativeMessaging.background'; export default class MainBackground { messagingService: MessagingServiceAbstraction; @@ -137,8 +138,11 @@ export default class MainBackground { private menuOptionsLoaded: any[] = []; private syncTimeout: any; private isSafari: boolean; + nativeMessagingBackground: NativeMessagingBackground; constructor() { + this.nativeMessagingBackground = new NativeMessagingBackground(); + // Services this.messagingService = new BrowserMessagingService(); this.platformUtilsService = new BrowserPlatformUtilsService(this.messagingService, @@ -146,7 +150,7 @@ export default class MainBackground { if (this.systemService != null) { this.systemService.clearClipboard(clipboardValue, clearMs); } - }); + }, this.nativeMessagingBackground); this.storageService = new BrowserStorageService(this.platformUtilsService); this.secureStorageService = new BrowserStorageService(this.platformUtilsService); this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); diff --git a/src/background/nativeMessaging.background.ts b/src/background/nativeMessaging.background.ts new file mode 100644 index 0000000000..dbb673dc5f --- /dev/null +++ b/src/background/nativeMessaging.background.ts @@ -0,0 +1,40 @@ +import { BrowserApi } from "../browser/browserApi"; + +export class NativeMessagingBackground { + private connected = false; + private port: browser.runtime.Port | chrome.runtime.Port; + + private resolver: any = null; + + connect() { + this.port = BrowserApi.connectNative("com.8bit.bitwarden"); + + this.connected = true; + this.port.onMessage.addListener((msg: any) => { + if (this.resolver) { + this.resolver(msg); + } else { + console.error('NO RESOLVER'); + } + }); + this.port.onDisconnect.addListener(() => { + this.connected = false; + console.log('Disconnected'); + }); + } + + send(message: object) { + // If not connected, try to connect + if (!this.connected) { + this.connect(); + } + + this.port.postMessage(message); + } + + await(): Promise { + return new Promise((resolve, reject) => { + this.resolver = resolve; + }); + } +} diff --git a/src/browser/browserApi.ts b/src/browser/browserApi.ts index 2e4a7f2d03..1ee54a45b1 100644 --- a/src/browser/browserApi.ts +++ b/src/browser/browserApi.ts @@ -221,4 +221,12 @@ export class BrowserApi { }); } } + + static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port { + if (BrowserApi.isWebExtensionsApi) { + return browser.runtime.connectNative(application); + } else if (BrowserApi.isChromeApi) { + return chrome.runtime.connectNative(application); + } + } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 2868ff5ae7..120d119434 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,6 +1,4 @@ declare function escape(s: string): string; declare function unescape(s: string): string; declare var opr: any; -declare var chrome: any; -declare var browser: any; declare var safari: any; diff --git a/src/popup/settings/settings.component.html b/src/popup/settings/settings.component.html index 61e8d7088d..6439d6e42a 100644 --- a/src/popup/settings/settings.component.html +++ b/src/popup/settings/settings.component.html @@ -42,6 +42,10 @@ +
+ + +
{{'lockNow' | i18n}}
diff --git a/src/popup/settings/settings.component.ts b/src/popup/settings/settings.component.ts index a831a994bf..bbdf30cdb3 100644 --- a/src/popup/settings/settings.component.ts +++ b/src/popup/settings/settings.component.ts @@ -51,6 +51,7 @@ export class SettingsComponent implements OnInit { vaultTimeoutActions: any[]; vaultTimeoutAction: string; pin: boolean = null; + biometric: boolean = null; previousVaultTimeout: number = null; constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, @@ -100,6 +101,7 @@ export class SettingsComponent implements OnInit { const pinSet = await this.vaultTimeoutService.isPinLockSet(); this.pin = pinSet[0] || pinSet[1]; + this.biometric = await this.vaultTimeoutService.isBiometricLockSet(); } async saveVaultTimeout(newValue: number) { @@ -204,6 +206,43 @@ export class SettingsComponent implements OnInit { } } + async updateBiometric() { + const current = this.biometric; + if (this.biometric) { + this.biometric = false; + } else { + const div = document.createElement('div'); + div.innerHTML = `
${this.i18nService.t('awaitDesktop')}
`; + + const submitted = Swal.fire({ + heightAuto: false, + buttonsStyling: false, + html: div, + showCancelButton: true, + cancelButtonText: this.i18nService.t('cancel'), + showConfirmButton: false, + }); + + // TODO: Show waiting message + this.biometric = await this.platformUtilsService.authenticateBiometric(); + 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() { this.analytics.eventTrack.next({ action: 'Lock Now' }); await this.vaultTimeoutService.lock(true); diff --git a/src/services/browserPlatformUtils.service.spec.ts b/src/services/browserPlatformUtils.service.spec.ts index 74881853a6..6b3711ef47 100644 --- a/src/services/browserPlatformUtils.service.spec.ts +++ b/src/services/browserPlatformUtils.service.spec.ts @@ -27,7 +27,7 @@ describe('Browser Utils Service', () => { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36', }); - const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); + const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.ChromeExtension); }); @@ -37,7 +37,7 @@ describe('Browser Utils Service', () => { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0', }); - const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); + const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.FirefoxExtension); }); @@ -52,7 +52,7 @@ describe('Browser Utils Service', () => { value: {}, }); - const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); + const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.OperaExtension); }); @@ -62,7 +62,7 @@ describe('Browser Utils Service', () => { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43', }); - const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); + const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.EdgeExtension); }); @@ -77,7 +77,7 @@ describe('Browser Utils Service', () => { value: true, }); - const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); + const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.SafariExtension); Object.defineProperty(window, 'safariAppExtension', { @@ -92,7 +92,7 @@ describe('Browser Utils Service', () => { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.97 Safari/537.36 Vivaldi/1.94.1008.40', }); - const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); + const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.VivaldiExtension); }); }); diff --git a/src/services/browserPlatformUtils.service.ts b/src/services/browserPlatformUtils.service.ts index 488f0d00cd..63a549fcc2 100644 --- a/src/services/browserPlatformUtils.service.ts +++ b/src/services/browserPlatformUtils.service.ts @@ -1,5 +1,6 @@ import { BrowserApi } from '../browser/browserApi'; import { SafariApp } from '../browser/safariApp'; +import { NativeMessagingBackground } from '../background/nativeMessaging.background'; import { DeviceType } from 'jslib/enums/deviceType'; @@ -18,7 +19,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService private analyticsIdCache: string = null; constructor(private messagingService: MessagingService, - private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void) { } + private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, + private nativeMessagingBackground: NativeMessagingBackground) { } getDevice(): DeviceType { if (this.deviceCache) { @@ -288,13 +290,18 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService } supportsBiometric() { - return Promise.resolve(false); + return Promise.resolve(true); } - authenticateBiometric() { - return Promise.resolve(false); - } + async authenticateBiometric() { + const responsePromise = this.nativeMessagingBackground.await(); + this.nativeMessagingBackground.send({'command': 'biometricUnlock'}); + const response = await responsePromise; + + return response.response == 'unlocked'; + } + sidebarViewName(): string { if ((window as any).chrome.sidebarAction && this.isFirefox()) { return 'sidebar'; diff --git a/tsconfig.json b/tsconfig.json index e1b5cca413..a1991acf1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,9 @@ "sourceMap": true, "types": [ "jasmine", - "sweetalert2" + "sweetalert2", + "@types/chrome", + "@types/firefox-webext-browser" ], "baseUrl": ".", "paths": {