Initial work of biometric unlock for browser

This commit is contained in:
Hinton 2020-10-09 17:16:15 +02:00
parent 296ccb6829
commit f311101ed9
14 changed files with 133 additions and 28 deletions

6
package-lock.json generated
View File

@ -777,6 +777,12 @@
"integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=", "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=",
"dev": true "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": { "@types/jasmine": {
"version": "3.3.12", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.12.tgz", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.12.tgz",

View File

@ -30,6 +30,7 @@
"@angular/compiler-cli": "^9.1.12", "@angular/compiler-cli": "^9.1.12",
"@ngtools/webpack": "^9.1.12", "@ngtools/webpack": "^9.1.12",
"@types/chrome": "^0.0.73", "@types/chrome": "^0.0.73",
"@types/firefox-webext-browser": "^78.0.1",
"@types/jasmine": "^3.3.12", "@types/jasmine": "^3.3.12",
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/mousetrap": "^1.6.0", "@types/mousetrap": "^1.6.0",
@ -48,7 +49,6 @@
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"del": "^3.0.0", "del": "^3.0.0",
"mini-css-extract-plugin": "^0.9.0",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-filter": "^5.1.0", "gulp-filter": "^5.1.0",
@ -68,6 +68,7 @@
"karma-jasmine": "^2.0.1", "karma-jasmine": "^2.0.1",
"karma-jasmine-html-reporter": "^1.4.0", "karma-jasmine-html-reporter": "^1.4.0",
"karma-typescript": "^4.0.0", "karma-typescript": "^4.0.0",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"style-loader": "^0.23.0", "style-loader": "^0.23.0",

View File

@ -1247,6 +1247,12 @@
"yourVaultIsLockedPinCode": { "yourVaultIsLockedPinCode": {
"message": "Your vault is locked. Verify your PIN code to continue." "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": { "lockWithMasterPassOnRestart": {
"message": "Lock with master password on browser restart" "message": "Lock with master password on browser restart"
}, },

View File

@ -4,13 +4,3 @@ const bitwardenMain = (window as any).bitwardenMain = new MainBackground();
bitwardenMain.bootstrap().then(() => { bitwardenMain.bootstrap().then(() => {
// Finished bootstrapping // 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' });

View File

@ -55,8 +55,8 @@ export default class ContextMenusBackground {
private async cipherAction(info: any) { private async cipherAction(info: any) {
const id = info.menuItemId.split('_')[1]; const id = info.menuItemId.split('_')[1];
if (id === 'noop') { if (id === 'noop') {
if (chrome.browserAction && chrome.browserAction.openPopup) { if (chrome.browserAction && (chrome.browserAction as any).openPopup) {
chrome.browserAction.openPopup(); (chrome.browserAction as any).openPopup();
} }
return; return;
} }

View File

@ -82,6 +82,7 @@ import I18nService from '../services/i18n.service';
import { PopupUtilsService } from '../popup/services/popup-utils.service'; import { PopupUtilsService } from '../popup/services/popup-utils.service';
import { AutofillService as AutofillServiceAbstraction } from '../services/abstractions/autofill.service'; import { AutofillService as AutofillServiceAbstraction } from '../services/abstractions/autofill.service';
import { NativeMessagingBackground } from './nativeMessaging.background';
export default class MainBackground { export default class MainBackground {
messagingService: MessagingServiceAbstraction; messagingService: MessagingServiceAbstraction;
@ -137,8 +138,11 @@ export default class MainBackground {
private menuOptionsLoaded: any[] = []; private menuOptionsLoaded: any[] = [];
private syncTimeout: any; private syncTimeout: any;
private isSafari: boolean; private isSafari: boolean;
nativeMessagingBackground: NativeMessagingBackground;
constructor() { constructor() {
this.nativeMessagingBackground = new NativeMessagingBackground();
// Services // Services
this.messagingService = new BrowserMessagingService(); this.messagingService = new BrowserMessagingService();
this.platformUtilsService = new BrowserPlatformUtilsService(this.messagingService, this.platformUtilsService = new BrowserPlatformUtilsService(this.messagingService,
@ -146,7 +150,7 @@ export default class MainBackground {
if (this.systemService != null) { if (this.systemService != null) {
this.systemService.clearClipboard(clipboardValue, clearMs); this.systemService.clearClipboard(clipboardValue, clearMs);
} }
}); }, this.nativeMessagingBackground);
this.storageService = new BrowserStorageService(this.platformUtilsService); this.storageService = new BrowserStorageService(this.platformUtilsService);
this.secureStorageService = new BrowserStorageService(this.platformUtilsService); this.secureStorageService = new BrowserStorageService(this.platformUtilsService);
this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); this.i18nService = new I18nService(BrowserApi.getUILanguage(window));

View File

@ -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<any> {
return new Promise((resolve, reject) => {
this.resolver = resolve;
});
}
}

View File

@ -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);
}
}
} }

2
src/globals.d.ts vendored
View File

@ -1,6 +1,4 @@
declare function escape(s: string): string; declare function escape(s: string): string;
declare function unescape(s: string): string; declare function unescape(s: string): string;
declare var opr: any; declare var opr: any;
declare var chrome: any;
declare var browser: any;
declare var safari: any; declare var safari: any;

View File

@ -42,6 +42,10 @@
<label for="pin">{{'unlockWithPin' | i18n}}</label> <label for="pin">{{'unlockWithPin' | i18n}}</label>
<input id="pin" type="checkbox" (change)="updatePin()" [(ngModel)]="pin"> <input id="pin" type="checkbox" (change)="updatePin()" [(ngModel)]="pin">
</div> </div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="biometric">{{'unlockWithBiometric' | i18n}}</label>
<input id="biometric" type="checkbox" (change)="updateBiometric()" [ngModel]="biometric">
</div>
<a class="box-content-row box-content-row-flex text-default" href="#" appStopClick appBlurClick <a class="box-content-row box-content-row-flex text-default" href="#" appStopClick appBlurClick
(click)="lock()"> (click)="lock()">
<div class="row-main">{{'lockNow' | i18n}}</div> <div class="row-main">{{'lockNow' | i18n}}</div>

View File

@ -51,6 +51,7 @@ export class SettingsComponent implements OnInit {
vaultTimeoutActions: any[]; vaultTimeoutActions: any[];
vaultTimeoutAction: string; vaultTimeoutAction: string;
pin: boolean = null; pin: boolean = null;
biometric: boolean = null;
previousVaultTimeout: number = null; previousVaultTimeout: number = null;
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
@ -100,6 +101,7 @@ export class SettingsComponent implements OnInit {
const pinSet = await this.vaultTimeoutService.isPinLockSet(); const pinSet = await this.vaultTimeoutService.isPinLockSet();
this.pin = pinSet[0] || pinSet[1]; this.pin = pinSet[0] || pinSet[1];
this.biometric = await this.vaultTimeoutService.isBiometricLockSet();
} }
async saveVaultTimeout(newValue: number) { 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 = `<div class="swal2-text">${this.i18nService.t('awaitDesktop')}</div>`;
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() { async lock() {
this.analytics.eventTrack.next({ action: 'Lock Now' }); this.analytics.eventTrack.next({ action: 'Lock Now' });
await this.vaultTimeoutService.lock(true); await this.vaultTimeoutService.lock(true);

View File

@ -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', 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); 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', 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); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.FirefoxExtension);
}); });
@ -52,7 +52,7 @@ describe('Browser Utils Service', () => {
value: {}, value: {},
}); });
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null);
expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.OperaExtension); 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', 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); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.EdgeExtension);
}); });
@ -77,7 +77,7 @@ describe('Browser Utils Service', () => {
value: true, value: true,
}); });
const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null); const browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null);
expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.SafariExtension); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.SafariExtension);
Object.defineProperty(window, 'safariAppExtension', { 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', 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); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.VivaldiExtension);
}); });
}); });

View File

@ -1,5 +1,6 @@
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from '../browser/browserApi';
import { SafariApp } from '../browser/safariApp'; import { SafariApp } from '../browser/safariApp';
import { NativeMessagingBackground } from '../background/nativeMessaging.background';
import { DeviceType } from 'jslib/enums/deviceType'; import { DeviceType } from 'jslib/enums/deviceType';
@ -18,7 +19,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
private analyticsIdCache: string = null; private analyticsIdCache: string = null;
constructor(private messagingService: MessagingService, constructor(private messagingService: MessagingService,
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void) { } private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
private nativeMessagingBackground: NativeMessagingBackground) { }
getDevice(): DeviceType { getDevice(): DeviceType {
if (this.deviceCache) { if (this.deviceCache) {
@ -288,11 +290,16 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
} }
supportsBiometric() { supportsBiometric() {
return Promise.resolve(false); return Promise.resolve(true);
} }
authenticateBiometric() { async authenticateBiometric() {
return Promise.resolve(false); const responsePromise = this.nativeMessagingBackground.await();
this.nativeMessagingBackground.send({'command': 'biometricUnlock'});
const response = await responsePromise;
return response.response == 'unlocked';
} }
sidebarViewName(): string { sidebarViewName(): string {

View File

@ -10,7 +10,9 @@
"sourceMap": true, "sourceMap": true,
"types": [ "types": [
"jasmine", "jasmine",
"sweetalert2" "sweetalert2",
"@types/chrome",
"@types/firefox-webext-browser"
], ],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {