diff --git a/app/scripts/comp/extension/browser-extension-connector.js b/app/scripts/comp/extension/browser-extension-connector.js index a29569d9..b869e0b0 100644 --- a/app/scripts/comp/extension/browser-extension-connector.js +++ b/app/scripts/comp/extension/browser-extension-connector.js @@ -116,15 +116,9 @@ const BrowserExtensionConnector = { window.removeEventListener('message', this.browserWindowMessage); }, - isEnabledOnDesktop() { - for (const browser of SupportedBrowsers) { - for (const ext of SupportedExtensions) { - if (AppSettingsModel[`extensionEnabled${ext.alias}${browser}`]) { - return true; - } - } - } - return false; + enable(browser, extension, enabled) { + const { ipcRenderer } = Launcher.electron(); + ipcRenderer.invoke('browserExtensionConnectorEnable', browser, extension, enabled); }, async startDesktopAppListener() { diff --git a/app/scripts/views/settings/settings-browser-view.js b/app/scripts/views/settings/settings-browser-view.js index f1ccfead..88a38633 100644 --- a/app/scripts/views/settings/settings-browser-view.js +++ b/app/scripts/views/settings/settings-browser-view.js @@ -4,7 +4,11 @@ import { Features } from 'util/features'; import { Links } from 'const/links'; import { AppSettingsModel } from 'models/app-settings-model'; import { Locale } from 'util/locale'; -import { SupportedBrowsers, SupportedExtensions } from 'comp/extension/browser-extension-connector'; +import { + BrowserExtensionConnector, + SupportedBrowsers, + SupportedExtensions +} from 'comp/extension/browser-extension-connector'; class SettingsBrowserView extends View { template = template; @@ -23,9 +27,7 @@ class SettingsBrowserView extends View { if (Features.isDesktop) { data.extensionNames = ['KeeWeb Connect', 'KeePassXC-Browser']; data.settingsPerBrowser = this.getSettingsPerBrowser(); - data.anyBrowserIsEnabled = data.settingsPerBrowser.some((perBrowser) => - perBrowser.extensions.some((ext) => ext.enabled) - ); + data.anyBrowserIsEnabled = BrowserExtensionConnector.isEnabled(); } else { const extensionBrowserFamily = Features.extensionBrowserFamily; data.extensionBrowserFamily = Features.extensionBrowserFamily; @@ -69,6 +71,8 @@ class SettingsBrowserView extends View { delete AppSettingsModel[setting]; } + BrowserExtensionConnector.enable(browser, extension, enabled); + this.render(); } diff --git a/desktop/scripts/ipc-handlers/browser-extension-connector.js b/desktop/scripts/ipc-handlers/browser-extension-connector.js index e818ea9c..196e125c 100644 --- a/desktop/scripts/ipc-handlers/browser-extension-connector.js +++ b/desktop/scripts/ipc-handlers/browser-extension-connector.js @@ -5,9 +5,11 @@ const net = require('net'); const { ipcMain, app } = require('electron'); const { Logger } = require('../logger'); const { getProcessInfo } = require('../util/process-utils'); +const browserExtensionInstaller = require('../util/browser-extension-installer'); ipcMain.handle('browserExtensionConnectorStart', browserExtensionConnectorStart); ipcMain.handle('browserExtensionConnectorStop', browserExtensionConnectorStop); +ipcMain.handle('browserExtensionConnectorEnable', browserExtensionConnectorEnable); ipcMain.handle('browserExtensionConnectorSocketResult', browserExtensionConnectorSocketResult); ipcMain.handle('browserExtensionConnectorSocketEvent', browserExtensionConnectorSocketEvent); @@ -15,11 +17,11 @@ const logger = new Logger('browser-extension-connector'); const MaxIncomingDataLength = 10_000; const ExtensionOrigins = { - 'chrome-extension://aphablpbogbpmocgkpeeadeljldnphon/': 'keeweb-connect', 'safari-keeweb-connect': 'keeweb-connect', 'keeweb-connect@keeweb.info': 'keeweb-connect', - 'chrome-extension://oboonakemofpalcgghocfoadofidjkkk/': 'keepassxc-browser', + 'chrome-extension://aphablpbogbpmocgkpeeadeljldnphon/': 'keeweb-connect', 'keepassxc-browser@keepassxc.org': 'keepassxc-browser', + 'chrome-extension://oboonakemofpalcgghocfoadofidjkkk/': 'keepassxc-browser', 'chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/': 'keepassxc-browser' }; @@ -67,9 +69,24 @@ function browserExtensionConnectorStop() { } connectedSockets = new Map(); connectedSocketState = new WeakMap(); + logger.info('Stopped'); } +async function browserExtensionConnectorEnable(e, browser, extension, enabled) { + logger.info(enabled ? 'Enable' : 'Disable', browser, extension); + + try { + if (enabled) { + await browserExtensionInstaller.install(browser, extension); + } else { + await browserExtensionInstaller.uninstall(browser, extension); + } + } catch (e) { + logger.error(`Error installing extension: ${e}`); + } +} + function browserExtensionConnectorSocketResult(e, socketId, result) { sendResultToSocket(socketId, result); } diff --git a/desktop/scripts/util/browser-extension-installer.js b/desktop/scripts/util/browser-extension-installer.js new file mode 100644 index 00000000..e0f32e01 --- /dev/null +++ b/desktop/scripts/util/browser-extension-installer.js @@ -0,0 +1,152 @@ +const fs = require('fs'); +const path = require('path'); +const { isDev } = require('./app-info'); +const { app } = require('electron'); + +function getManifestDir(browser) { + const home = app.getPath('home'); + switch (process.platform) { + case 'darwin': + switch (browser) { + case 'Chrome': + return `${home}/Library/Application Support/Google/Chrome/NativeMessagingHosts/`; + case 'Firefox': + return `${home}/Library/Application Support/Mozilla/NativeMessagingHosts/`; + case 'Edge': + return `${home}/Library/Application Support/Microsoft Edge/NativeMessagingHosts/`; + default: + return undefined; + } + case 'win32': + throw new Error('not implemented'); + case 'linux': + switch (browser) { + case 'Chrome': + return `${home}/.config/google-chrome/NativeMessagingHosts/`; + case 'Firefox': + return `${home}/.mozilla/native-messaging-hosts/`; + case 'Edge': + return `${home}/.config/microsoft-edge/NativeMessagingHosts/`; + default: + return undefined; + } + } +} + +function getManifestFileName(extension) { + switch (extension) { + case 'KWC': + return 'net.antelle.keeweb.keeweb_connect.json'; + case 'KPXC': + return 'org.keepassxc.keepassxc_browser.json'; + } +} + +function createManifest(extension) { + switch (extension) { + case 'KWC': + return { + 'allowed_origins': ['chrome-extension://aphablpbogbpmocgkpeeadeljldnphon/'], + 'allowed_extensions': ['keeweb-connect@keeweb.info'], + description: 'KeeWeb native messaging host', + name: 'net.antelle.keeweb.keeweb_connect', + type: 'stdio' + }; + case 'KPXC': + return { + 'allowed_origins': [ + 'chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/', + 'chrome-extension://oboonakemofpalcgghocfoadofidjkkk/' + ], + 'allowed_extensions': ['keepassxc-browser@keepassxc.org'], + description: 'Native messaging host created by KeeWeb', + name: 'org.keepassxc.keepassxc_browser', + type: 'stdio' + }; + } +} + +function getNativeMessagingHostPath() { + if (isDev) { + const packageBase = path.resolve('node_modules/@keeweb/keeweb-native-messaging-host'); + const extension = process.platform === 'win32' ? '.exe' : ''; + const exeName = `keeweb-native-messaging-host${extension}`; + return path.join(packageBase, `${process.platform}-${process.arch}`, exeName); + } + switch (process.platform) { + case 'darwin': + return path.join(app.getPath('exe'), '..', 'util', 'keeweb-native-messaging-host'); + case 'win32': + return path.join(app.getPath('exe'), '..', 'keeweb-native-messaging-host.exe'); + case 'linux': + return path.join(app.getPath('exe'), '..', 'keeweb-native-messaging-host'); + } +} + +module.exports.install = async function (browser, extension) { + const manifestDir = getManifestDir(browser); + if (!manifestDir) { + return; + } + + await fs.promises.mkdir(manifestDir, { recursive: true }); + + const manifestFileName = getManifestFileName(extension); + if (!manifestFileName) { + return; + } + + const fullPath = path.join(manifestDir, manifestFileName); + + let manifest; + if (extension === 'KPXC') { + try { + await fs.promises.access(fullPath); + manifest = JSON.parse(await fs.promises.readFile(fullPath, 'utf8')); + manifest.pathKPXC = manifest.path; + } catch {} + } + + if (!manifest) { + manifest = createManifest(extension); + } + if (!manifest) { + return; + } + + manifest.path = getNativeMessagingHostPath(); + + await fs.promises.writeFile(fullPath, JSON.stringify(manifest, null, 4)); +}; + +module.exports.uninstall = async function (browser, extension) { + const manifestDir = getManifestDir(browser); + if (!manifestDir) { + return; + } + + const manifestFileName = getManifestFileName(extension); + if (!manifestFileName) { + return; + } + const fullPath = path.join(manifestDir, manifestFileName); + + try { + await fs.promises.access(fullPath); + } catch { + return; + } + + if (extension === 'KPXC') { + const manifest = JSON.parse(await fs.promises.readFile(fullPath, 'utf8')); + if (manifest.pathKPXC) { + manifest.path = manifest.pathKPXC; + delete manifest.pathKPXC; + await fs.promises.writeFile(fullPath, JSON.stringify(manifest, null, 4)); + } else { + await fs.promises.unlink(fullPath); + } + } else { + await fs.promises.unlink(fullPath); + } +};