diff --git a/hak/keytar/build.js b/hak/keytar/build.js new file mode 100644 index 0000000..39319ef --- /dev/null +++ b/hak/keytar/build.js @@ -0,0 +1,42 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const path = require('path'); +const childProcess = require('child_process'); + +module.exports = async function(hakEnv, moduleInfo) { + await buildKeytar(hakEnv, moduleInfo); +}; + +async function buildKeytar(hakEnv, moduleInfo) { + const env = hakEnv.makeGypEnv(); + + console.log("Running yarn with env", env); + await new Promise((resolve, reject) => { + const proc = childProcess.spawn( + path.join(moduleInfo.nodeModuleBinDir, 'node-gyp' + (hakEnv.isWin() ? '.cmd' : '')), + ['rebuild'], + { + cwd: moduleInfo.moduleBuildDir, + env, + stdio: 'inherit', + }, + ); + proc.on('exit', (code) => { + code ? reject(code) : resolve(); + }); + }); +} diff --git a/hak/keytar/check.js b/hak/keytar/check.js new file mode 100644 index 0000000..8fcb788 --- /dev/null +++ b/hak/keytar/check.js @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const childProcess = require('child_process'); + +module.exports = async function(hakEnv, moduleInfo) { + const tools = [['python', '--version']]; // node-gyp uses python for reasons beyond comprehension + + for (const tool of tools) { + await new Promise((resolve, reject) => { + const proc = childProcess.spawn(tool[0], tool.slice(1), { + stdio: ['ignore'], + }); + proc.on('exit', (code) => { + if (code !== 0) { + reject("Can't find " + tool); + } else { + resolve(); + } + }); + }); + } +}; diff --git a/hak/keytar/hak.json b/hak/keytar/hak.json new file mode 100644 index 0000000..7597052 --- /dev/null +++ b/hak/keytar/hak.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "check": "check.js", + "build": "build.js" + }, + "copy": "build/Release/keytar.node", + "dependencies": { + "libsecret": "0.20.3" + } +} diff --git a/package.json b/package.json index 8a634de..6115a78 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "tar": "^6.0.1" }, "hakDependencies": { - "matrix-seshat": "^1.3.3" + "matrix-seshat": "^1.3.3", + "keytar": "^5.6.0" }, "build": { "appId": "im.riot.app", diff --git a/src/electron-main.js b/src/electron-main.js index 4c0bce7..9965b70 100644 --- a/src/electron-main.js +++ b/src/electron-main.js @@ -3,6 +3,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd Copyright 2018, 2019 New Vector Ltd Copyright 2017, 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -43,6 +44,18 @@ const Store = require('electron-store'); const fs = require('fs'); const afs = fs.promises; +const crypto = require('crypto'); +let keytar; +try { + keytar = require('keytar'); +} catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + console.log("Keytar isn't installed; secure key storage is disabled."); + } else { + console.warn("Keytar unexpected error:", e); + } +} + let seshatSupported = false; let Seshat; let SeshatRecovery; @@ -365,6 +378,41 @@ ipcMain.on('ipcCall', async function(ev, payload) { recordSSOSession(args[0]); break; + case 'getPickleKey': + try { + ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`); + } catch (e) { + // if an error is thrown (e.g. keytar can't connect to the keychain), + // then return null, which means the default pickle key will be used + ret = null; + } + break; + + case 'createPickleKey': + try { + const randomArray = await new Promise((resolve, reject) => { + crypto.randomBytes(32, (err, buf) => { + if (err) { + reject(err); + } else { + resolve(buf); + } + }); + }); + const pickleKey = randomArray.toString("base64").replace(/=+$/g, ''); + await keytar.setPassword("riot.im", `${args[0]}|${args[1]}`, pickleKey); + ret = pickleKey; + } catch (e) { + ret = null; + } + break; + + case 'destroyPickleKey': + try { + await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`); + } catch (e) {} + break; + default: mainWindow.webContents.send('ipcReply', { id: payload.id,