From 8ca9e4ccb179f86deaa1d52c9ce8a5fd666f5b12 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 4 Apr 2020 00:22:15 +0100 Subject: [PATCH 1/5] Fix Electron SSO handling to support multiple profiles Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- electron_app/src/electron-main.js | 14 +++++++++++++- src/vector/platform/ElectronPlatform.js | 8 ++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index 91258c6cfd..9453839f7a 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -68,7 +68,14 @@ if (argv["help"]) { app.exit(); } -if (argv['profile-dir']) { +// check if we are passed a profile in the SSO callback url +const deeplinkUrl = argv["_"].find(arg => arg.startsWith('riot://')); +if (deeplinkUrl && deeplinkUrl.includes('riot-desktop-user-data-path')) { + const parsedUrl = new URL(deeplinkUrl); + if (parsedUrl.protocol === 'riot:') { + app.setPath('userData', parsedUrl.searchParams.get('riot-desktop-user-data-path')); + } +} else if (argv['profile-dir']) { app.setPath('userData', argv['profile-dir']); } else if (argv['profile']) { app.setPath('userData', `${app.getPath('userData')}-${argv['profile']}`); @@ -233,6 +240,11 @@ ipcMain.on('ipcCall', async function(ev, payload) { case 'getConfig': ret = vectorConfig; break; + case 'getUserDataPath': + if (argv['profile-dir'] || argv['profile']) { + ret = app.getPath('userData'); + } + break; default: mainWindow.webContents.send('ipcReply', { diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js index 6b75c1eea9..09312480c4 100644 --- a/src/vector/platform/ElectronPlatform.js +++ b/src/vector/platform/ElectronPlatform.js @@ -228,6 +228,11 @@ export default class ElectronPlatform extends VectorBasePlatform { description: _td("Open user settings"), }); } + + // we assume this happens before any SSO actions occur but do not block. + this._ipcCall('getUserDataPath').then(userDataPath => { + this.userDataPath = userDataPath; + }); } async getConfig(): Promise<{}> { @@ -424,6 +429,9 @@ export default class ElectronPlatform extends VectorBasePlatform { getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { const url = super.getSSOCallbackUrl(hsUrl, isUrl); url.protocol = "riot"; + if (this.userDataPath) { + url.searchParams.set("riot-desktop-user-data-path", this.userDataPath); + } return url; } From 67cf1e753621108803779db8739d1a77756aaef5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 4 Apr 2020 00:48:26 +0100 Subject: [PATCH 2/5] rejig Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- electron_app/src/electron-main.js | 11 ++--- electron_app/src/protocol.js | 69 +++++++++++++++++++------------ 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index 9453839f7a..b112fe1bdb 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -35,7 +35,7 @@ const tray = require('./tray'); const vectorMenu = require('./vectormenu'); const webContentsHandler = require('./webcontents-handler'); const updater = require('./updater'); -const protocolInit = require('./protocol'); +const {getProfileFromDeeplink, protocolInit} = require('./protocol'); const windowStateKeeper = require('electron-window-state'); const Store = require('electron-store'); @@ -69,12 +69,9 @@ if (argv["help"]) { } // check if we are passed a profile in the SSO callback url -const deeplinkUrl = argv["_"].find(arg => arg.startsWith('riot://')); -if (deeplinkUrl && deeplinkUrl.includes('riot-desktop-user-data-path')) { - const parsedUrl = new URL(deeplinkUrl); - if (parsedUrl.protocol === 'riot:') { - app.setPath('userData', parsedUrl.searchParams.get('riot-desktop-user-data-path')); - } +const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]); +if (userDataPathInProtocol) { + app.setPath('userData', userDataPathInProtocol); } else if (argv['profile-dir']) { app.setPath('userData', argv['profile-dir']); } else if (argv['profile']) { diff --git a/electron_app/src/protocol.js b/electron_app/src/protocol.js index 153ff64084..9a3d3286ba 100644 --- a/electron_app/src/protocol.js +++ b/electron_app/src/protocol.js @@ -16,38 +16,55 @@ limitations under the License. const {app} = require('electron'); +const PROTOCOL = "riot://"; +const SEARCH_PARAM = "riot-desktop-user-data-path"; + const processUrl = (url) => { if (!global.mainWindow) return; console.log("Handling link: ", url); - global.mainWindow.loadURL(url.replace("riot://", "vector://")); + global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://")); }; -module.exports = () => { - // get all args except `hidden` as it'd mean the app would not get focused - // XXX: passing args to protocol handlers only works on Windows, - // so unpackaged deep-linking and --profile passing won't work on Mac/Linux - const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden"); - if (app.isPackaged) { - app.setAsDefaultProtocolClient('riot', process.execPath, args); - } else if (process.platform === 'win32') { // on Mac/Linux this would just cause the electron binary to open - // special handler for running without being packaged, e.g `electron .` by passing our app path to electron - app.setAsDefaultProtocolClient('riot', process.execPath, [app.getAppPath(), ...args]); - } +module.exports = { + getProfileFromDeeplink: (args) => { + // check if we are passed a profile in the SSO callback url + const deeplinkUrl = args.find(arg => arg.startsWith('riot://')); + if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) { + const parsedUrl = new URL(deeplinkUrl); + if (parsedUrl.protocol === 'riot:') { + const profile = parsedUrl.searchParams.get(SEARCH_PARAM); + console.log("Forwarding to profile: ", profile); + return profile; + } + } + }, + protocolInit: () => { + // get all args except `hidden` as it'd mean the app would not get focused + // XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking + // --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url + const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden"); + if (app.isPackaged) { + app.setAsDefaultProtocolClient('riot', process.execPath, args); + } else if (process.platform === 'win32') { // on Mac/Linux this would just cause the electron binary to open + // special handler for running without being packaged, e.g `electron .` by passing our app path to electron + app.setAsDefaultProtocolClient('riot', process.execPath, [app.getAppPath(), ...args]); + } - if (process.platform === 'darwin') { - // Protocol handler for macos - app.on('open-url', function(ev, url) { - ev.preventDefault(); - processUrl(url); - }); - } else { - // Protocol handler for win32/Linux - app.on('second-instance', (ev, commandLine) => { - const url = commandLine[commandLine.length - 1]; - if (!url.startsWith("riot://")) return; - processUrl(url); - }); - } + if (process.platform === 'darwin') { + // Protocol handler for macos + app.on('open-url', function(ev, url) { + ev.preventDefault(); + processUrl(url); + }); + } else { + // Protocol handler for win32/Linux + app.on('second-instance', (ev, commandLine) => { + const url = commandLine[commandLine.length - 1]; + if (!url.startsWith(PROTOCOL)) return; + processUrl(url); + }); + } + }, }; From 6fdeca93b6c908f9396ca80ea1f1e6d89048f028 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Apr 2020 16:21:52 +0100 Subject: [PATCH 3/5] Make the riot-desktop callback args more generic and encrypt the args Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- electron_app/src/electron-main.js | 8 +++--- electron_app/src/protocol.js | 35 ++++++++++++++++++++++--- src/vector/platform/ElectronPlatform.js | 8 +++--- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index b112fe1bdb..d1a6dd85ea 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -35,7 +35,7 @@ const tray = require('./tray'); const vectorMenu = require('./vectormenu'); const webContentsHandler = require('./webcontents-handler'); const updater = require('./updater'); -const {getProfileFromDeeplink, protocolInit} = require('./protocol'); +const {getProfileFromDeeplink, protocolInit, getArgs} = require('./protocol'); const windowStateKeeper = require('electron-window-state'); const Store = require('electron-store'); @@ -237,10 +237,8 @@ ipcMain.on('ipcCall', async function(ev, payload) { case 'getConfig': ret = vectorConfig; break; - case 'getUserDataPath': - if (argv['profile-dir'] || argv['profile']) { - ret = app.getPath('userData'); - } + case 'getRiotDesktopSsoArgs': + ret = getArgs(argv); break; default: diff --git a/electron_app/src/protocol.js b/electron_app/src/protocol.js index 9a3d3286ba..5049745ae9 100644 --- a/electron_app/src/protocol.js +++ b/electron_app/src/protocol.js @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -const {app} = require('electron'); +const {app} = require("electron"); +const crypto = require("crypto"); const PROTOCOL = "riot://"; -const SEARCH_PARAM = "riot-desktop-user-data-path"; +const SEARCH_PARAM = "riot-desktop-args"; const processUrl = (url) => { if (!global.mainWindow) return; @@ -25,7 +26,35 @@ const processUrl = (url) => { global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://")); }; +const algorithm = "aes-192-cbc"; + +const getKeyIv = () => ({ + key: crypto.scryptSync(app.getPath("exe"), "salt", 24), + iv: Buffer.alloc(16, 0), +}); + +const encrypt = (plaintext) => { + const {key, iv} = getKeyIv(); + const cipher = crypto.createCipheriv(algorithm, key, iv); + let ciphertext = cipher.update(plaintext, "utf8", "hex"); + ciphertext += cipher.final("hex"); + return ciphertext; +}; + +const decrypt = (ciphertext) => { + const {key, iv} = getKeyIv(); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + let plaintext = decipher.update(ciphertext, "hex", "utf8"); + plaintext += decipher.final("utf8"); + return plaintext; +}; + module.exports = { + getArgs: (argv) => { + if (argv['profile-dir'] || argv['profile']) { + return encrypt(app.getPath('userData')); + } + }, getProfileFromDeeplink: (args) => { // check if we are passed a profile in the SSO callback url const deeplinkUrl = args.find(arg => arg.startsWith('riot://')); @@ -34,7 +63,7 @@ module.exports = { if (parsedUrl.protocol === 'riot:') { const profile = parsedUrl.searchParams.get(SEARCH_PARAM); console.log("Forwarding to profile: ", profile); - return profile; + return decrypt(profile); } } }, diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js index 09312480c4..6cb2aada69 100644 --- a/src/vector/platform/ElectronPlatform.js +++ b/src/vector/platform/ElectronPlatform.js @@ -230,8 +230,8 @@ export default class ElectronPlatform extends VectorBasePlatform { } // we assume this happens before any SSO actions occur but do not block. - this._ipcCall('getUserDataPath').then(userDataPath => { - this.userDataPath = userDataPath; + this._ipcCall('getRiotDesktopSsoArgs').then(riotDesktopSsoArgs => { + this.riotDesktopSsoArgs = riotDesktopSsoArgs; }); } @@ -429,8 +429,8 @@ export default class ElectronPlatform extends VectorBasePlatform { getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { const url = super.getSSOCallbackUrl(hsUrl, isUrl); url.protocol = "riot"; - if (this.userDataPath) { - url.searchParams.set("riot-desktop-user-data-path", this.userDataPath); + if (this.riotDesktopSsoArgs) { + url.searchParams.set("riot-desktop-args", this.riotDesktopSsoArgs); } return url; } From 4afd29f62c6cb54de66550139608ffdbac6fa1b2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Apr 2020 16:23:41 +0100 Subject: [PATCH 4/5] add comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- electron_app/src/protocol.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electron_app/src/protocol.js b/electron_app/src/protocol.js index 5049745ae9..262107e7ca 100644 --- a/electron_app/src/protocol.js +++ b/electron_app/src/protocol.js @@ -26,6 +26,8 @@ const processUrl = (url) => { global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://")); }; +// we encrypt anything that we expose to be passed back to our callback protocol +// so that homeservers don't see our directory paths and have the ability to manipulate them. const algorithm = "aes-192-cbc"; const getKeyIv = () => ({ From 15bb819c8a8dd4bac856a476d2cb6bf0e7f5746d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Apr 2020 21:17:37 +0100 Subject: [PATCH 5/5] Instead of encrypting, pass the HS an opaque token which we locally resolve in a map to our profile data Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- electron_app/src/electron-main.js | 6 +-- electron_app/src/protocol.js | 61 +++++++++++++------------ src/vector/platform/ElectronPlatform.js | 12 ++--- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index d1a6dd85ea..b67992c2c9 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -35,7 +35,7 @@ const tray = require('./tray'); const vectorMenu = require('./vectormenu'); const webContentsHandler = require('./webcontents-handler'); const updater = require('./updater'); -const {getProfileFromDeeplink, protocolInit, getArgs} = require('./protocol'); +const {getProfileFromDeeplink, protocolInit, recordSSOSession} = require('./protocol'); const windowStateKeeper = require('electron-window-state'); const Store = require('electron-store'); @@ -237,8 +237,8 @@ ipcMain.on('ipcCall', async function(ev, payload) { case 'getConfig': ret = vectorConfig; break; - case 'getRiotDesktopSsoArgs': - ret = getArgs(argv); + case 'startSSOFlow': + recordSSOSession(args[0]); break; default: diff --git a/electron_app/src/protocol.js b/electron_app/src/protocol.js index 262107e7ca..48247fef98 100644 --- a/electron_app/src/protocol.js +++ b/electron_app/src/protocol.js @@ -15,10 +15,15 @@ limitations under the License. */ const {app} = require("electron"); -const crypto = require("crypto"); +const path = require("path"); +const fs = require("fs"); const PROTOCOL = "riot://"; -const SEARCH_PARAM = "riot-desktop-args"; +const SEARCH_PARAM = "riot-desktop-ssoid"; +const STORE_FILE_NAME = "sso-sessions.json"; + +// we getPath userData before electron-main changes it, so this is the default value +const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME); const processUrl = (url) => { if (!global.mainWindow) return; @@ -26,36 +31,33 @@ const processUrl = (url) => { global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://")); }; -// we encrypt anything that we expose to be passed back to our callback protocol -// so that homeservers don't see our directory paths and have the ability to manipulate them. -const algorithm = "aes-192-cbc"; - -const getKeyIv = () => ({ - key: crypto.scryptSync(app.getPath("exe"), "salt", 24), - iv: Buffer.alloc(16, 0), -}); - -const encrypt = (plaintext) => { - const {key, iv} = getKeyIv(); - const cipher = crypto.createCipheriv(algorithm, key, iv); - let ciphertext = cipher.update(plaintext, "utf8", "hex"); - ciphertext += cipher.final("hex"); - return ciphertext; +const readStore = () => { + try { + const s = fs.readFileSync(storePath, { encoding: "utf8" }); + const o = JSON.parse(s); + return typeof o === "object" ? o : {}; + } catch (e) { + return {}; + } }; -const decrypt = (ciphertext) => { - const {key, iv} = getKeyIv(); - const decipher = crypto.createDecipheriv(algorithm, key, iv); - let plaintext = decipher.update(ciphertext, "hex", "utf8"); - plaintext += decipher.final("utf8"); - return plaintext; +const writeStore = (data) => { + fs.writeFileSync(storePath, JSON.stringify(data)); }; module.exports = { - getArgs: (argv) => { - if (argv['profile-dir'] || argv['profile']) { - return encrypt(app.getPath('userData')); + recordSSOSession: (sessionID) => { + const userDataPath = app.getPath('userData'); + const store = readStore(); + for (const key in store) { + // ensure each instance only has one (the latest) session ID to prevent the file growing unbounded + if (store[key] === userDataPath) { + delete store[key]; + break; + } } + store[sessionID] = userDataPath; + writeStore(store); }, getProfileFromDeeplink: (args) => { // check if we are passed a profile in the SSO callback url @@ -63,9 +65,10 @@ module.exports = { if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) { const parsedUrl = new URL(deeplinkUrl); if (parsedUrl.protocol === 'riot:') { - const profile = parsedUrl.searchParams.get(SEARCH_PARAM); - console.log("Forwarding to profile: ", profile); - return decrypt(profile); + const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM); + const store = readStore(); + console.log("Forwarding to profile: ", store[ssoID]); + return store[ssoID]; } } }, diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js index 6cb2aada69..e60382b7df 100644 --- a/src/vector/platform/ElectronPlatform.js +++ b/src/vector/platform/ElectronPlatform.js @@ -32,6 +32,7 @@ import Spinner from "matrix-react-sdk/src/components/views/elements/Spinner"; import {Categories, Modifiers, registerShortcut} from "matrix-react-sdk/src/accessibility/KeyboardShortcuts"; import {Key} from "matrix-react-sdk/src/Keyboard"; import React from "react"; +import {randomString} from "matrix-js-sdk/src/randomstring"; const ipcRenderer = window.ipcRenderer; const isMac = navigator.platform.toUpperCase().includes('MAC'); @@ -229,10 +230,9 @@ export default class ElectronPlatform extends VectorBasePlatform { }); } - // we assume this happens before any SSO actions occur but do not block. - this._ipcCall('getRiotDesktopSsoArgs').then(riotDesktopSsoArgs => { - this.riotDesktopSsoArgs = riotDesktopSsoArgs; - }); + // this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile + this.ssoID = randomString(32); + this._ipcCall("startSSOFlow", this.ssoID); } async getConfig(): Promise<{}> { @@ -429,9 +429,7 @@ export default class ElectronPlatform extends VectorBasePlatform { getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { const url = super.getSSOCallbackUrl(hsUrl, isUrl); url.protocol = "riot"; - if (this.riotDesktopSsoArgs) { - url.searchParams.set("riot-desktop-args", this.riotDesktopSsoArgs); - } + url.searchParams.set("riot-desktop-ssoid", this.ssoID); return url; }