diff --git a/hak/keytar/build.js b/hak/keytar/build.js new file mode 100644 index 0000000..335149e --- /dev/null +++ b/hak/keytar/build.js @@ -0,0 +1,45 @@ +/* +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'); + +const mkdirp = require('mkdirp'); +const fsExtra = require('fs-extra'); + +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..921e0e4 --- /dev/null +++ b/hak/keytar/check.js @@ -0,0 +1,58 @@ +/* +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) { + // of course tcl doesn't have a --version + if (!hakEnv.isLinux()) { + await new Promise((resolve, reject) => { + const proc = childProcess.spawn('tclsh', [], { + stdio: ['pipe', 'ignore', 'ignore'], + }); + proc.on('exit', (code) => { + if (code !== 0) { + reject("Can't find tclsh - have you installed TCL?"); + } else { + resolve(); + } + }); + proc.stdin.end(); + }); + } + + const tools = [['python', '--version']]; // node-gyp uses python for reasons beyond comprehension + if (hakEnv.isWin()) { + tools.push(['nmake', '/?']); + } else { + tools.push(['make', '--version']); + } + + 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/fetchDeps.js b/hak/keytar/fetchDeps.js new file mode 100644 index 0000000..8d27a6f --- /dev/null +++ b/hak/keytar/fetchDeps.js @@ -0,0 +1,26 @@ +/* +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'); + +const fs = require('fs'); +const fsProm = require('fs').promises; +const needle = require('needle'); +const tar = require('tar'); + +module.exports = async function(hakEnv, moduleInfo) { +}; diff --git a/hak/keytar/hak.json b/hak/keytar/hak.json new file mode 100644 index 0000000..6c9fe99 --- /dev/null +++ b/hak/keytar/hak.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "check": "check.js", + "fetchDeps": "fetchDeps.js", + "build": "build.js" + }, + "prune": "native", + "copy": "build/Release/keytar.node", + "dependencies": { + "libsecret": "0.20.3" + } +} diff --git a/package.json b/package.json index cb72b09..7ff4982 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,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 34a6a5a..f66dfea 100644 --- a/src/electron-main.js +++ b/src/electron-main.js @@ -43,6 +43,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 +377,40 @@ 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,