From 77094630b2a42facca4e5a1e2fb90b9a58a5a0c9 Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Wed, 17 Apr 2019 13:17:29 -0500 Subject: [PATCH] [#21: Add signature validation during installations [partial]] [Updated packages] [Removed Hyperspace] [Updated README] --- CHANGELOG.md | 4 ++ README.md | 4 +- package.json | 19 ++++---- public/electron.js | 109 +++++++++++++++++++++++++++++++++++++++++++-- releases.json | 3 ++ src/App.js | 4 ++ src/constants.js | 4 +- src/helpers.js | 85 +++++++++++++++++++++++++++++++++-- 8 files changed, 213 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aca636a..e8664fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog # ## 1.0.3 ## * Linux distribution support + * Bodhi 5.0.0 * Debian 9 + * Elementary OS 5.0 * Linux Mint 19 * Linux Mint 19.1 * Solus @@ -9,6 +11,8 @@ * Ubuntu 18.10 * Ubuntu 19.04 * Removed `react-css-modules` dependency +* Removed Hyperspace (no active development/insufficient host network) +* Added signature or SHA-256 validation to downloads ## 1.0.2 ## * Option to launch application hidden (notification icon only) diff --git a/README.md b/README.md index 85b86de..62a3386 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Repertory UI ![alt text](https://image.ibb.co/edOg5A/repertory-ui-1-0-2.png) ### GUI for [Repertory](https://bitbucket.org/blockstorage/repertory) ### -Repertory allows you to mount Sia, SiaPrime and/or Hyperspace blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows. +Repertory allows you to mount Sia and/or SiaPrime blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows. # Downloads # * [Repertory UI v1.0.3 Linux 64-bit]() * [Repertory UI v1.0.3 OS X 64-bit]() @@ -10,7 +10,9 @@ Repertory allows you to mount Sia, SiaPrime and/or Hyperspace blockchain storage * OS X 64-bit * Windows 64-bit * Linux 64-bit Distributions: + * Bodhi 5.0.0 * Debian 9 + * Elementary OS 5.0 * Linux Mint 19 * Linux Mint 19.1 * Solus diff --git a/package.json b/package.json index a4a0bf8..0f8be29 100644 --- a/package.json +++ b/package.json @@ -5,28 +5,29 @@ "author": "scott.e.graves@gmail.com", "description": "GUI for Repertory - Repertory allows you to mount Hyperspace, Sia and/or SiaPrime blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows.", "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.10", - "@fortawesome/free-solid-svg-icons": "^5.6.1", - "@fortawesome/react-fontawesome": "^0.1.0", + "@fortawesome/fontawesome-svg-core": "^1.2.17", + "@fortawesome/free-solid-svg-icons": "^5.8.1", + "@fortawesome/react-fontawesome": "^0.1.4", "auto-launch": "^5.0.5", "axios": "^0.18.0", "electron-debug": "^2.2.0", "font-awesome": "^4.7.0", - "node-schedule": "^1.3.1", + "randomstring": "^1.1.5", + "node-schedule": "^1.3.2", "react": "^16.8.6", "react-dom": "^16.8.6", "react-loader-spinner": "^2.3.0", "react-scripts": "2.1.8", - "react-tooltip": "^3.9.0", - "unzipper": "^0.9.6", + "react-tooltip": "^3.10.0", + "unzipper": "^0.9.11", "winreg": "^1.2.4" }, "devDependencies": { "cross-env": "^5.2.0", - "electron": "^4.1.0", - "electron-builder": "^20.38.5", + "electron": "^4.1.4", + "electron-builder": "^20.40.2", "extract-text-webpack-plugin": "^3.0.2", - "typescript": "^3.4.2", + "typescript": "^3.4.3", "webpack-browser-plugin": "^1.0.20" }, "scripts": { diff --git a/public/electron.js b/public/electron.js index 9543644..3c86304 100644 --- a/public/electron.js +++ b/public/electron.js @@ -11,10 +11,37 @@ const helpers = require('../src/helpers'); const fs = require('fs'); const unzip = require('unzipper'); const AutoLaunch = require('auto-launch'); + require.extensions['.sh'] = function (module, filename) { module.exports = fs.readFileSync(filename, 'utf8'); }; const detectScript = require('./detect_linux.sh'); +const publicKey = + '-----BEGIN PUBLIC KEY-----\n' + + 'MIIEIjANBgkqhkiG9w0BAQEFAAOCBA8AMIIECgKCBAEKfZmq5mMAtD4kSt2Gc/5J\n' + + 'H+HHTYtUZE6YYvsvz8TNG/bNL67ZtNRyaoMyhLTfIN4rPBNLUfD+owNS+u5Yk+lS\n' + + 'ZLYyOuhoCZIFefayYqKLr42G8EeuRbx0IMzXmJtN0a4rqxlWhkYufJubpdQ+V4DF\n' + + 'oeupcPdIATaadCKVeZC7A0G0uaSwoiAVMG5dZqjQW7F2LoQm3PhNkPvAybIJ6vBy\n' + + 'LqdBegS1JrDn43x/pvQHzLO+l+FIG23D1F7iF+yZm3DkzBdcmi/mOMYs/rXZpBym\n' + + '2/kTuSGh5buuJCeyOwR8N3WdvXw6+KHMU/wWU8qTCTT87mYbzH4YR8HgkjkLHxAO\n' + + '5waHK6vMu0TxugCdJmVV6BSbiarJsh66VRosn7+6hlq6AdgksxqCeNELZBS+LBki\n' + + 'tb5hKyL+jNZnaHiR0U7USWtmnqZG6FVVRzlCnxP7tZo5O5Ex9AAFGz5JzOzsFNbv\n' + + 'xwQ0zqaTQOze+MJbkda7JfRoC6TncD0+3hoXsiaF4mCn8PqUCn0DwhglcRucZlST\n' + + 'ZvDNDo1WAtxPJebb3aS6uymNhBIquQbVAWxVO4eTrOYEgutxwkHE3yO3is+ogp8d\n' + + 'xot7f/+vzlbsbIDyuZBDe0fFkbTIMTU48QuUUVZpRKmKZTHQloz4EHqminbfX1sh\n' + + 'M7wvDkpJEtqbc0VnG/BukUzP6e7Skvgc7eF1sI3+8jH8du2rivZeZAl7Q2f+L9JA\n' + + 'BY9pjaxttxsud7V5jeFi4tKuDHi21/XhSjlJK2c2C4AiUEK5/WhtGbQ5JjmcOjRq\n' + + 'yXFRqLlerzOcop2kbtU3Ar230wOx3Dj23Wg8++lV3LU4U9vMR/t0qnSbCSGJys7m\n' + + 'ax2JpFlTwj/0wYuTlVFoNQHZJ1cdfyRiRBY4Ou7XO0W5hcBBKiYsC+neEeMMHdCe\n' + + 'iTDIW/ojcVTdFovl+sq3n1u4SBknE90JC/3H+TPE1s2iB+fwORVg0KPosQSNDS0A\n' + + '7iK6AZCDC3YooFo+OzHkYMt9uLkXiXMSLx70az+qlIwOzVHKxCo7W/QpeKCXUCRZ\n' + + 'MMdlYEUs1PC8x2qIRUEVHuJ0XMTKNyOHmzVLuLK93wUWbToh+rdDxnbhX+emuESn\n' + + 'XH6aKiUwX4olEVKSylRUQw8nVckZGVWXzLDlgpzDrLHC8J8qHzFt7eCqOdiqsxhZ\n' + + 'x1U5LtugxwSWncTZ7vlKl0DuC/AWB7SuDi7bGRMSVp2n+MnD1VLKlsCclHXjIciE\n' + + 'W29n3G3lJ/sOta2sxqLd0j1XBQddrFXl5b609sIY81ocHqu8P2hRu5CpqJ/sGZC5\n' + + 'mMH3segHBkRj0xJcfOxceRLj1a+ULIIR3xL/3f8s5Id25TDo/nqBoCvu5PeCpo6L\n' + + '9wIDAQAB\n' + + '-----END PUBLIC KEY-----'; // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. @@ -582,9 +609,10 @@ ipcMain.on(Constants.IPC_Get_Config_Template, (event, data) => { ipcMain.on(Constants.IPC_Get_Platform, (event) => { let platform = os.platform(); if (platform === 'linux') { - fs.writeFileSync('/tmp/repertory_detect_linux.sh', detectScript); + const scriptFile = path.join(os.tmpdir(), 'repertory_detect_linux.sh'); + fs.writeFileSync(scriptFile, detectScript); helpers - .executeScript('/tmp/repertory_detect_linux.sh') + .executeScript(scriptFile) .then(data => { platform = data.replace(/(\r\n|\n|\r)/gm,""); event.sender.send(Constants.IPC_Get_Platform_Reply, { @@ -661,29 +689,101 @@ ipcMain.on(Constants.IPC_Install_Dependency, (event, data) => { }); ipcMain.on(Constants.IPC_Install_Upgrade, (event, data) => { + let tempSig; + let tempPub; + const hasSignature = data.Signature && (data.Signature.length > 0); + const hasHash = data.Sha256 && (data.Sha256.length > 0); + if (hasSignature) { + try { + const files = helpers.createSignatureFiles(data.Signature, publicKey); + tempPub = files.PublicKeyFile; + tempSig = files.SignatureFile; + } catch (e) { + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, e); + return; + } + } + + const cleanupFiles = () => { + try { + if (tempSig) { + fs.unlinkSync(tempSig); + } + if (tempPub) { + fs.unlinkSync(tempPub); + } + } catch (e) { + } + }; + if (os.platform() === 'win32') { - helpers + const executeInstall = () => { + helpers .executeAsync(data.Source) .then(() => { + cleanupFiles(); closeApplication(); }) .catch(error => { + cleanupFiles(); standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { Source: data.Source, }, error); }); + }; + if (hasSignature) { + helpers + .verifySignature(data.Source, tempSig, tempPub) + .then(() => { + executeInstall(); + }) + .catch(() => { + cleanupFiles(); + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, 'Failed to verify installation package signature'); + }); + } else { // TODO Check Sha256 + executeInstall(); + } } else if (data.Source.toLocaleLowerCase().endsWith('.dmg')) { - helpers + const executeInstall = () => { + helpers .executeAsync('open', ['-a', 'Finder', data.Source]) .then(() => { + cleanupFiles(); closeApplication(); }) .catch(error => { + cleanupFiles(); standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { Source: data.Source, }, error); }); + }; + + if (hasHash) { + helpers + .verifyHash(data.Source, data.Sha256) + .then(()=> { + executeInstall(); + }) + .catch(() => { + cleanupFiles(); + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, 'Failed to verify installation package hash'); + }); + } else { + executeInstall(); + } } else if (data.Source.toLocaleLowerCase().endsWith('.appimage')) { + cleanupFiles(); + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, Error('Not implemented upgrade: ' + data.Source)); // TODO Generate and execute script with delay /*helpers .executeAsync(data.Source) @@ -696,6 +796,7 @@ ipcMain.on(Constants.IPC_Install_Upgrade, (event, data) => { }, error); });*/ } else { + cleanupFiles(); standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { Source: data.Source, }, Error('Unsupported upgrade: ' + data.Source)); diff --git a/releases.json b/releases.json index cc311cd..1c2c28c 100644 --- a/releases.json +++ b/releases.json @@ -2,18 +2,21 @@ "Locations": { "win32": { "1.0.3": { + "sha256": "", "sig": "", "urls": [] } }, "darwin": { "1.0.3": { + "sha256": "", "sig": "", "urls": [] } }, "solus": { "1.0.3": { + "sha256": "", "sig": "", "urls": [] } diff --git a/src/App.js b/src/App.js index bf36b0f..092404c 100644 --- a/src/App.js +++ b/src/App.js @@ -295,7 +295,11 @@ class App extends IPCContainer { installUpgrade = data => { if (data.Success) { + const sha256 = this.state.LocationsLookup[this.props.platform][this.state.VersionLookup[this.props.platform][0]].sha256; + const signature = this.state.LocationsLookup[this.props.platform][this.state.VersionLookup[this.props.platform][0]].sig; this.sendRequest(Constants.IPC_Install_Upgrade, { + Sha256: sha256, + Signature: signature, Source: data.Destination, }); } else { diff --git a/src/constants.js b/src/constants.js index 5c9655c..b2fdf63 100644 --- a/src/constants.js +++ b/src/constants.js @@ -15,13 +15,13 @@ exports.DATA_LOCATIONS = { exports.UI_RELEASES_URL = 'https://bitbucket.org/blockstorage/repertory-ui/raw/1.0.3_branch/releases.json'; exports.PROVIDER_LIST = [ - 'Hyperspace', + //'Hyperspace', 'Sia', 'SiaPrime' ]; exports.PROVIDER_ARG = { - hyperspace: '-hs', + //hyperspace: '-hs', sia: '', siaprime: '-sp' }; diff --git a/src/helpers.js b/src/helpers.js index 6cf87bb..887b489 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -5,6 +5,7 @@ const axios = require('axios/index'); const exec = require('child_process').exec; const spawn = require('child_process').spawn; const Constants = require('./constants'); +const RandomString = require('randomstring'); const tryParse = (j, def) => { try { @@ -14,6 +15,29 @@ const tryParse = (j, def) => { } }; +module.exports.createSignatureFiles = (signature, publicKey) => { + const fileName1 = RandomString.generate({ + length: 12, + charset: 'alphabetic' + }); + const fileName2 = RandomString.generate({ + length: 12, + charset: 'alphabetic' + }); + + const signatureFile = path.join(os.tmpdir(), fileName1 + '.sig'); + const publicKeyFile = path.join(os.tmpdir(), fileName2 + '.pub'); + + const buffer = new Buffer(signature, 'base64'); + fs.writeFileSync(signatureFile, buffer); + fs.writeFileSync(publicKeyFile, publicKey); + + return { + PublicKeyFile: publicKeyFile, + SignatureFile: signatureFile, + }; +}; + module.exports.detectRepertoryMounts = (directory, version) => { return new Promise((resolve, reject) => { const processOptions = { @@ -93,13 +117,13 @@ module.exports.downloadFile = (url, destination, progressCallback, completeCallb }); }; -module.exports.executeAndWait = command => { +module.exports.executeAndWait = (command, ignoreResult) => { return new Promise((resolve, reject) => { const retryExecute = (count, lastError) => { if (++count <= 5) { - exec(command, (error) => { + exec(command, error => { if (error) { - if (error.code === 1) { + if (!ignoreResult && (error.code === 1)) { setTimeout(() => { retryExecute(count, error); }, 1000); @@ -517,4 +541,59 @@ module.exports.stopMountProcessSync = (directory, version, storageType) => { const process = new spawn(command, args, processOptions); process.unref(); +}; + +module.exports.verifySignature = (file, signatureFile, publicKeyFile) => { + return new Promise((resolve, reject) => { + const executeVerify = openssl => { + //openssl dgst -sha256 -verify $pubkeyfile -signature signature.sig file + const command = '"' + openssl + '" dgst -sha256 -verify "' + publicKeyFile + '" -signature "' + signatureFile + '"'; + exec(command, res => { + if (res.code !== 0) { + reject(res); + } else { + resolve(); + } + }); + }; + + if (os.platform() === 'win32') { + const Registry = require('winreg'); + const regKey = new Registry({ + hive: Registry.HKLM, + key: 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\OpenSSL (64-bit)_is1' + }); + regKey.valueExists('InstallLocation', (err, exists) => { + if (err) { + reject(err); + } else if (exists) { + regKey.get('InstallLocation', (err, item) => { + if (err) { + reject(err); + } else { + const openssl = path.join(item.value(), 'bin', 'openssl.exe'); + executeVerify(openssl); + } + }); + } else { + reject('Failed to locate \'openssl.exe\''); + } + }); + } else { + reject('Platform not supported: ' + os.platform()) + } + }); +}; + +module.exports.verifyHash = (file, hash) => { + return new Promise((resolve, reject) => { + if (os.platform() === 'darwin') { + reject('Not implemented'); + } else if (os.platform() === 'linux') { + reject('Not implemented'); + } + else { + reject('Platform not supported: ' + os.platform()) + } + }); }; \ No newline at end of file