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] 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 d1a6dd85..b67992c2 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 262107e7..48247fef 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 6cb2aada..e60382b7 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; }