From 4b17ac19dc70b55added2e590244c1a601d0b8a2 Mon Sep 17 00:00:00 2001 From: antelle Date: Tue, 6 Apr 2021 20:10:42 +0200 Subject: [PATCH] browser extension backend 101 --- app/scripts/app.js | 2 + .../comp/app/browser-extension-connector.js | 167 ++++++++++++++++++ app/scripts/const/default-app-settings.js | 1 + app/templates/settings/settings-about.hbs | 10 +- build/webpack.config.js | 1 + package-lock.json | 5 +- package.json | 1 + 7 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 app/scripts/comp/app/browser-extension-connector.js diff --git a/app/scripts/app.js b/app/scripts/app.js index 267bbb05..039d327d 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -6,6 +6,7 @@ import { ExportApi } from 'comp/app/export-api'; import { SingleInstanceChecker } from 'comp/app/single-instance-checker'; import { Updater } from 'comp/app/updater'; import { UsbListener } from 'comp/app/usb-listener'; +import { BrowserExtensionConnector } from 'comp/app/browser-extension-connector'; import { FeatureTester } from 'comp/browser/feature-tester'; import { FocusDetector } from 'comp/browser/focus-detector'; import { IdleTracker } from 'comp/browser/idle-tracker'; @@ -177,6 +178,7 @@ ready(() => { AppRightsChecker.init(); IdleTracker.init(); UsbListener.init(); + BrowserExtensionConnector.init(); setTimeout(() => { PluginManager.runAutoUpdate(); }, Timeouts.AutoUpdatePluginsAfterStart); diff --git a/app/scripts/comp/app/browser-extension-connector.js b/app/scripts/comp/app/browser-extension-connector.js new file mode 100644 index 00000000..b6ceca43 --- /dev/null +++ b/app/scripts/comp/app/browser-extension-connector.js @@ -0,0 +1,167 @@ +import kdbxweb from 'kdbxweb'; +import { box as tweetnaclBox } from 'tweetnacl'; +import { RuntimeInfo } from 'const/runtime-info'; +import { Launcher } from 'comp/launcher'; +import { AppSettingsModel } from 'models/app-settings-model'; + +const connectedClients = {}; + +function incrementNonce(nonce) { + // from libsodium/utils.c, like it is in KeePassXC + let i = 0; + let c = 1; + for (; i < nonce.length; ++i) { + c += nonce[i]; + nonce[i] = c; + c >>= 8; + } +} + +function getClient(request) { + if (!request.clientID) { + throw new Error('Empty clientID'); + } + const client = connectedClients[request.clientID]; + if (!client) { + throw new Error(`Client not connected: ${request.clientID}`); + } + return client; +} + +function decryptRequest(request) { + const client = getClient(request); + + if (!request.nonce) { + throw new Error('Empty nonce'); + } + if (!request.message) { + throw new Error('Empty message'); + } + + const nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce); + const message = kdbxweb.ByteUtils.base64ToBytes(request.message); + + const data = tweetnaclBox.open(message, nonce, client.publicKey, client.keys.secretKey); + + const json = new TextDecoder().decode(data); + const payload = JSON.parse(json); + + if (payload?.action !== request.action) { + throw new Error(`Bad action in decrypted payload`); + } + + return payload; +} + +function encryptResponse(request, payload) { + const client = getClient(request); + + const json = JSON.stringify(payload); + const data = new TextEncoder().encode(json); + + let nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce); + incrementNonce(nonce); + + const encrypted = tweetnaclBox(data, nonce, client.publicKey, client.keys.secretKey); + + const message = kdbxweb.ByteUtils.bytesToBase64(encrypted); + nonce = kdbxweb.ByteUtils.bytesToBase64(nonce); + + return { + action: request.action, + message, + nonce + }; +} + +const ProtocolHandlers = { + 'ping'({ data }) { + return { data }; + }, + + 'change-public-keys'({ publicKey, clientID: clientId }) { + const keys = tweetnaclBox.keyPair(); + publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey); + + connectedClients[clientId] = { publicKey, keys }; + + return { + action: 'change-public-keys', + version: RuntimeInfo.version, + publicKey: kdbxweb.ByteUtils.bytesToBase64(keys.publicKey), + success: 'true' + }; + }, + + 'get-databasehash'(request) { + decryptRequest(request); + return encryptResponse(request, { + action: 'hash', + version: RuntimeInfo.version, + hash: 'TODO' + }); + } +}; + +const BrowserExtensionConnector = { + init() { + AppSettingsModel.on('change:browserExtension', (model, enabled) => { + if (enabled) { + this.start(); + } else { + this.stop(); + } + }); + if (AppSettingsModel.browserExtension) { + this.start(); + } + }, + + start() { + if (!Launcher) { + this.startWebMessageListener(); + } + }, + + stop() { + if (!Launcher) { + this.stopWebMessageListener(); + } + }, + + startWebMessageListener() { + window.addEventListener('message', this.browserWindowMessage); + }, + + stopWebMessageListener() { + window.removeEventListener('message', this.browserWindowMessage); + }, + + browserWindowMessage(e) { + if (e.origin !== location.origin) { + return; + } + if (e.source !== window) { + return; + } + if (e?.data?.kwConnect !== 'request') { + return; + } + let response; + try { + const handler = ProtocolHandlers[e.data.action]; + if (!handler) { + throw new Error(`Handler not found: ${e.data.action}`); + } + response = handler(e.data) || {}; + } catch (e) { + response = { error: e.message || 'Unknown error' }; + } + if (response) { + response.kwConnect = 'response'; + postMessage(response, window.location.origin); + } + } +}; + +export { BrowserExtensionConnector }; diff --git a/app/scripts/const/default-app-settings.js b/app/scripts/const/default-app-settings.js index cc7e9a5c..2091d34b 100644 --- a/app/scripts/const/default-app-settings.js +++ b/app/scripts/const/default-app-settings.js @@ -47,6 +47,7 @@ const DefaultAppSettings = { deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID disableOfflineStorage: false, // don't cache loaded files in offline storage shortLivedStorageToken: false, // short-lived sessions in cloud storage providers + browserExtension: false, // support browser extension interaction yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files diff --git a/app/templates/settings/settings-about.hbs b/app/templates/settings/settings-about.hbs index b639a21f..6414dfca 100644 --- a/app/templates/settings/settings-about.hbs +++ b/app/templates/settings/settings-about.hbs @@ -14,10 +14,7 @@
  • morphdom, fast and lightweight DOM diffing/patching, © Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
  • lodash, a modern JavaScript utility library delivering modularity, performance & extras, © OpenJS Foundation and other contributors <https://openjsf.org/>
  • jQuery, fast, small, and feature-rich JavaScript library, © OpenJS Foundation and other contributors, https://openjsf.org/
  • -
  • marked, a markdown parser and compiler, © 2018+, MarkedJS (https://github.com/markedjs/) © 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
  • -
  • dompurify, a DOM-only, super-fast, uber-tolerant XSS sanitizer, © 2015 Mario Heiderich, - Apache-2.0 license
  • -
  • node-phonetic, generates unique, pronounceable names, © 2013 Tom Frost
  • +

    Core components

    @@ -48,8 +45,13 @@

    Utils

    Styles

    diff --git a/build/webpack.config.js b/build/webpack.config.js index d8456f99..8492ff13 100644 --- a/build/webpack.config.js +++ b/build/webpack.config.js @@ -56,6 +56,7 @@ function config(options) { argon2: 'argon2-browser/dist/argon2.js', marked: devMode ? 'marked/lib/marked.js' : 'marked/marked.min.js', dompurify: `dompurify/dist/purify${devMode ? '' : '.min'}.js`, + tweetnacl: `tweetnacl/nacl${devMode ? '' : '.min'}.js`, hbs: 'handlebars/runtime.js', 'argon2-wasm': 'argon2-browser/dist/argon2.wasm', templates: path.join(rootDir, 'app/templates'), diff --git a/package-lock.json b/package-lock.json index d73bcaae..1e6b077c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "keeweb", - "version": "1.17.4", + "version": "1.17.5", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.17.4", + "version": "1.17.5", "license": "MIT", "dependencies": { "@babel/core": "^7.13.8", @@ -94,6 +94,7 @@ "svgicons2svgfont": "^9.1.1", "terser-webpack-plugin": "^5.1.1", "time-grunt": "2.0.0", + "tweetnacl": "^0.14.5", "url-loader": "^4.1.1", "wawoff2": "^1.0.2", "webpack": "^5.24.3", diff --git a/package.json b/package.json index 3173b185..3819f425 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "svgicons2svgfont": "^9.1.1", "terser-webpack-plugin": "^5.1.1", "time-grunt": "2.0.0", + "tweetnacl": "^0.14.5", "url-loader": "^4.1.1", "wawoff2": "^1.0.2", "webpack": "^5.24.3",