From c13d6f810e77191d1d96f25b728b9b9ff55e34ca Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 16 Dec 2020 15:42:46 +0100 Subject: [PATCH 1/7] Add support for communicating with multiple extensions at the same time --- src/services/nativeMessaging.service.ts | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/services/nativeMessaging.service.ts b/src/services/nativeMessaging.service.ts index a594c953de..029aa61a77 100644 --- a/src/services/nativeMessaging.service.ts +++ b/src/services/nativeMessaging.service.ts @@ -16,7 +16,7 @@ const MessageValidTimeout = 10 * 1000; const EncryptionAlgorithm = 'sha1'; export class NativeMessagingService { - private sharedSecret: any; + private sharedSecrets = new Map(); constructor(private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private platformUtilService: PlatformUtilsService, private logService: LogService, private i18nService: I18nService, @@ -26,7 +26,9 @@ export class NativeMessagingService { }); } - private async messageHandler(rawMessage: any) { + private async messageHandler(msg: any) { + const appId = msg.appId; + const rawMessage = msg.message; // Request to setup secure encryption if (rawMessage.command === 'setupEncryption') { @@ -50,15 +52,15 @@ export class NativeMessagingService { return; } - this.secureCommunication(remotePublicKey); + this.secureCommunication(remotePublicKey, appId); return; } - const message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); + const message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecrets.get(appId))); // Shared secret is invalidated, force re-authentication if (message == null) { - ipcRenderer.send('nativeMessagingReply', {command: 'invalidateEncryption'}); + ipcRenderer.send('nativeMessagingReply', {command: 'invalidateEncryption', appId: appId}); return; } @@ -70,14 +72,14 @@ export class NativeMessagingService { switch (message.command) { case 'biometricUnlock': if (! this.platformUtilService.supportsBiometric()) { - return this.send({command: 'biometricUnlock', response: 'not supported'}); + return this.send({command: 'biometricUnlock', response: 'not supported'}, appId); } const response = await this.platformUtilService.authenticateBiometric(); if (response) { - this.send({command: 'biometricUnlock', response: 'unlocked', keyB64: (await this.cryptoService.getKey()).keyB64}); + this.send({command: 'biometricUnlock', response: 'unlocked', keyB64: (await this.cryptoService.getKey()).keyB64}, appId); } else { - this.send({command: 'biometricUnlock', response: 'canceled'}); + this.send({command: 'biometricUnlock', response: 'canceled'}, appId); } break; @@ -86,19 +88,19 @@ export class NativeMessagingService { } } - private async send(message: any) { + private async send(message: any, appId: string) { message.timestamp = Date.now(); - const encrypted = await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret); + const encrypted = await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecrets.get(appId)); - ipcRenderer.send('nativeMessagingReply', encrypted); + ipcRenderer.send('nativeMessagingReply', {appId: appId, message: encrypted}); } - private async secureCommunication(remotePublicKey: ArrayBuffer) { + private async secureCommunication(remotePublicKey: ArrayBuffer, appId: string) { const secret = await this.cryptoFunctionService.randomBytes(64); - this.sharedSecret = new SymmetricCryptoKey(secret); + this.sharedSecrets.set(appId, new SymmetricCryptoKey(secret)); const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(secret, remotePublicKey, EncryptionAlgorithm); - ipcRenderer.send('nativeMessagingReply', {command: 'setupEncryption', sharedSecret: Utils.fromBufferToB64(encryptedSecret)}); + ipcRenderer.send('nativeMessagingReply', {appId: appId, command: 'setupEncryption', sharedSecret: Utils.fromBufferToB64(encryptedSecret)}); } } From d20aaeb0e51f5ba28b905d3243b46abe816bc9dc Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 16 Dec 2020 17:25:30 +0100 Subject: [PATCH 2/7] Display error message when biometric is disabled in the desktop --- src/app/services.module.ts | 2 +- src/locales/en/messages.json | 8 +++++++- src/services/nativeMessaging.service.ts | 17 +++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/app/services.module.ts b/src/app/services.module.ts index 22566ed6d7..b44085654e 100644 --- a/src/app/services.module.ts +++ b/src/app/services.module.ts @@ -135,7 +135,7 @@ const eventService = new EventService(storageService, apiService, userService, c const systemService = new SystemService(storageService, vaultTimeoutService, messagingService, platformUtilsService, null); const nativeMessagingService = new NativeMessagingService(cryptoFunctionService, cryptoService, platformUtilsService, - logService, i18nService, userService, messagingService); + logService, i18nService, userService, messagingService, vaultTimeoutService); const analytics = new Analytics(window, () => isDev(), platformUtilsService, storageService, appIdService); containerService.attachToGlobal(window); diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index a891f31327..6f70527abe 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -1453,9 +1453,15 @@ "verifyBrowserTitle": { "message": "Verify browser connection" }, - "verifyBrowserDescription": { + "verifyBrowserDesc": { "message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension." }, + "biometricsNotEnabledTitle": { + "message": "Biometrics not enabled" + }, + "biometricsNotEnabledDesc": { + "message": "Browser biometric requires desktop biometric to be enabled in the settings first." + }, "personalOwnershipSubmitError": { "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections." } diff --git a/src/services/nativeMessaging.service.ts b/src/services/nativeMessaging.service.ts index 029aa61a77..a45f6bfc80 100644 --- a/src/services/nativeMessaging.service.ts +++ b/src/services/nativeMessaging.service.ts @@ -8,6 +8,7 @@ import { LogService } from 'jslib/abstractions/log.service'; import { MessagingService } from 'jslib/abstractions/messaging.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { UserService } from 'jslib/abstractions/user.service'; +import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service'; import { Utils } from 'jslib/misc/utils'; import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey'; @@ -20,7 +21,7 @@ export class NativeMessagingService { constructor(private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private platformUtilService: PlatformUtilsService, private logService: LogService, private i18nService: I18nService, - private userService: UserService, private messagingService: MessagingService) { + private userService: UserService, private messagingService: MessagingService, private vaultTimeoutService: VaultTimeoutService) { ipcRenderer.on('nativeMessaging', async (event: any, message: any) => { this.messageHandler(message); }); @@ -40,7 +41,7 @@ export class NativeMessagingService { // Await confirmation that fingerprint is correct const submitted = await Swal.fire({ title: this.i18nService.t('verifyBrowserTitle'), - html: `${this.i18nService.t('verifyBrowserDescription')}

${fingerprint}`, + html: `${this.i18nService.t('verifyBrowserDesc')}

${fingerprint}`, showCancelButton: true, cancelButtonText: this.i18nService.t('cancel'), showConfirmButton: true, @@ -75,6 +76,18 @@ export class NativeMessagingService { return this.send({command: 'biometricUnlock', response: 'not supported'}, appId); } + if (! await this.vaultTimeoutService.isBiometricLockSet()) { + this.send({command: 'biometricUnlock', response: 'not enabled'}, appId); + + return await Swal.fire({ + title: this.i18nService.t('biometricsNotEnabledTitle'), + text: this.i18nService.t('biometricsNotEnabledDesc'), + showCancelButton: true, + cancelButtonText: this.i18nService.t('cancel'), + showConfirmButton: false, + }); + } + const response = await this.platformUtilService.authenticateBiometric(); if (response) { this.send({command: 'biometricUnlock', response: 'unlocked', keyB64: (await this.cryptoService.getKey()).keyB64}, appId); From 02a3fbde995a0de29c16970620936efa68a0a4eb Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 16 Dec 2020 21:49:05 +0100 Subject: [PATCH 3/7] Disable browser integration on Mac for non MAS version (for now) --- src/app/accounts/settings.component.ts | 13 +++++++++++-- src/locales/en/messages.json | 6 ++++++ src/proxy/nativemessage.ts | 6 +++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/accounts/settings.component.ts b/src/app/accounts/settings.component.ts index 396eb8dd7f..e6f108f76c 100644 --- a/src/app/accounts/settings.component.ts +++ b/src/app/accounts/settings.component.ts @@ -317,9 +317,18 @@ export class SettingsComponent implements OnInit { } async saveBrowserIntegration() { + if (process.platform ==='darwin' && !this.platformUtilsService.isMacAppStore()) { + await this.platformUtilsService.showDialog( + this.i18nService.t('browserIntegrationMasOnlyDesc'), + this.i18nService.t('browserIntegrationMasOnlyTitle'), + this.i18nService.t('ok'), null, 'warning'); + + this.enableBrowserIntegration = false; + return; + } + await this.storageService.save(ElectronConstants.enableBrowserIntegration, this.enableBrowserIntegration); - this.messagingService.send( - this.enableBrowserIntegration ? 'enableBrowserIntegration' : 'disableBrowserIntegration'); + this.messagingService.send(this.enableBrowserIntegration ? 'enableBrowserIntegration' : 'disableBrowserIntegration'); } private callAnalytics(name: string, enabled: boolean) { diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 6f70527abe..307440c577 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -1447,6 +1447,12 @@ "enableBrowserIntegrationDesc": { "message": "Browser integration is used for biometrics in browser." }, + "browserIntegrationMasOnlyTitle": { + "message": "Browser integration not supported" + }, + "browserIntegrationMasOnlyDesc": { + "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." + }, "approve": { "message": "Approve" }, diff --git a/src/proxy/nativemessage.ts b/src/proxy/nativemessage.ts index 3bcee29a6c..6bc5488217 100644 --- a/src/proxy/nativemessage.ts +++ b/src/proxy/nativemessage.ts @@ -83,7 +83,11 @@ export default class NativeMessage { chunks.push(chunk); } - processData(); + try { + processData(); + } catch(e) { + console.error(e); + } }); } } From e639fa667467825d58003ccbb0b2812d8289cd05 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 18 Dec 2020 15:47:48 +0100 Subject: [PATCH 4/7] Make fingerprint validation optional, update readme with debug info for native messaging --- README.md | 12 ++++++++ src/app/accounts/settings.component.html | 10 ++++++ src/app/accounts/settings.component.ts | 11 +++++++ src/app/services.module.ts | 2 +- src/locales/en/messages.json | 6 ++++ src/main/nativeMessaging.main.ts | 15 +++++---- src/services/nativeMessaging.service.ts | 39 ++++++++++++++---------- 7 files changed, 72 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index bc305b47aa..a4976a6ae4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@ npm install npm run electron ``` +**Debug Native Messaging** + +Native Messaging (communication with the browser extension) works by having the browser start a lightweight proxy application baked into our desktop binary. To setup an environment which allows +for easy debugging you will need to build the application for distribution, i.e. `npm run dist:`, start the dist version and enable desktop integration. This will write some manifests +to disk, Consult the [native manifests](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#Manifest_location) documentation for more details of the manigest +format, and the exact locations for the different platforms. *Note* that disabling the desktop integration will delete the manifests, and the files will need to be updated again. + +The generated manifests are pre-configured with the production ID for the browser extensions. In order to use them with the development builds, the browser extension ID of the development build +needs to be added to the `allowed_extensions` section of the manifest. These IDs are generated by the browser, and can be found in the extension settings within the browser. + +It will then be possible to run the desktop application as usual using `npm run electron` and communicate with the browser. + # Contribute Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file. diff --git a/src/app/accounts/settings.component.html b/src/app/accounts/settings.component.html index e7c9965e71..a223f7c43e 100644 --- a/src/app/accounts/settings.component.html +++ b/src/app/accounts/settings.component.html @@ -98,6 +98,16 @@ {{'enableBrowserIntegrationDesc' | i18n}} +
+
+ +
+ {{'enableBrowserIntegrationFingerprintDesc' | i18n}} +