From f2f27699e29198c27dceaf955a8d396d7958e121 Mon Sep 17 00:00:00 2001 From: Petteri Date: Sat, 6 Jan 2018 14:17:04 +0200 Subject: [PATCH] Change Cryptr to custom encrypter for stronger encryption --- StudioHelper.js | 84 ++++++++++------------- lib/secret-helper.js | 160 +++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 22 +++--- package.json | 1 - 4 files changed, 205 insertions(+), 62 deletions(-) create mode 100644 lib/secret-helper.js diff --git a/StudioHelper.js b/StudioHelper.js index 157a357..f592c91 100644 --- a/StudioHelper.js +++ b/StudioHelper.js @@ -3,10 +3,10 @@ const request = require('request'), mime = require('mime-types'), Promise = require('bluebird'), + secretHelper = require('./lib/secret-helper'), fs = require('fs'), path = require('path'), os = require('os'), - Cryptr = require('cryptr'), ignore = require('ignore'), throat = require('throat')(Promise), ProgressBar = require('progress'); @@ -57,8 +57,7 @@ class StudioHelper { throw Error('StudioHelper#constructor: settings.studio must be set'); } - this.credentialsSecret = this._createCredentialsSecret(CREDENTIALS_SECRET_BASE); - this.cryptr = new Cryptr(this.credentialsSecret); + this._createCredentialsSecret(CREDENTIALS_SECRET_BASE); this.apiUrl = 'https://' + settings.studio + API_URL; this.authToken = ''; @@ -165,19 +164,21 @@ class StudioHelper { * @private */ _createCredentialsSecret(secretBase) { - // Include mac address in secret - let mac = this._getFirstMac(); - // Add parts of current path for little bit of extra secrecy :P - let currentDirParts = __dirname.split('').map((l,i) => (i + 1) % 3 ? l : '').join(''); - // And cpu model - let cpuModel = ''; - - let cpus = os.cpus(); - if (cpus && cpus.length) { - cpuModel = cpus[0].model; - } + const secretStr = secretBase + + secretHelper.getFirstMAC() + + secretHelper.getCPUModel() + + secretHelper.getCurrentPath(); + + this._credentialsSecret = secretStr; - return secretBase + mac + currentDirParts + cpuModel; + return this._credentialsSecret; + } + + /** + * @private + */ + _getCredentialsSecret() { + return this._credentialsSecret; } /** @@ -185,14 +186,15 @@ class StudioHelper { */ _getCredentials() { let data = null; + let dataString = null; try { - data = JSON.parse(fs.readFileSync(this.credentialsFile, 'utf8')); + dataString = fs.readFileSync(this.credentialsFile, 'utf8'); } catch (e) { } - if (data) { - data = this._getDecryptedData(data); + if (dataString) { + data = this._getDecryptedData(dataString); } return data; @@ -553,45 +555,31 @@ class StudioHelper { /** * @private */ - _getEncryptedData(data) { - let encryptedData = null; - for (let key in data) { - if (data.hasOwnProperty(key)) { - if (!encryptedData) { - encryptedData = {}; - } + _getEncryptedString(data) { + const encryptedString = secretHelper.encryptSync( + this._getCredentialsSecret(), + JSON.stringify(data) + ); - encryptedData[key] = this.cryptr.encrypt(data[key]); - } - } - return encryptedData; + return encryptedString; } /** * @private */ - _getDecryptedData(data) { - let decryptedData = null; - let decrypted = false; + _getDecryptedData(dataString) { + let data = null; try { - for (let key in data) { - if (data.hasOwnProperty(key)) { - if (!decryptedData) { - decryptedData = {}; - } - - decryptedData[key] = this.cryptr.decrypt(data[key]); + const decryptedString = secretHelper.decryptSync( + this._getCredentialsSecret(), + dataString + ); - // Test that authToken is formatted correctly - if (key === 'authToken' && /^[\w:\-]+$/.test(decryptedData[key])) { - decrypted = true; - } - } - } + data = JSON.parse(decryptedString); } catch (e) {} - return decrypted ? decryptedData : null; + return data; } /** @@ -600,9 +588,9 @@ class StudioHelper { _updateCredentials(data) { let self = this; - data = this._getEncryptedData(data); + const dataString = this._getEncryptedString(data); - fs.writeFile(this.credentialsFile, JSON.stringify(data), function(err) { + fs.writeFile(this.credentialsFile, dataString, function(err) { if (err) { self._log(err); } diff --git a/lib/secret-helper.js b/lib/secret-helper.js new file mode 100644 index 0000000..72ced0e --- /dev/null +++ b/lib/secret-helper.js @@ -0,0 +1,160 @@ +const os = require('os'); +const Crypto = require('crypto'); + +class SecretHelper { + constructor() { + this.iterations = 196935; + } + /** + * Get first non-internal MAC address if any available + * @return {string} MAC + */ + getFirstMAC() { + const allowed = ['eth0', 'eth1', 'en0', 'en1']; + const interfaces = os.networkInterfaces(); + + for (let iface in interfaces) { + if (interfaces.hasOwnProperty(iface)) { + const interfaceArr = interfaces[iface]; + + if (allowed.indexOf(iface) !== -1) { + for (let i = 0, l = interfaceArr.length; i < l; i++) { + const addressData = interfaceArr[i]; + + if (!addressData.internal && addressData.mac) { + return addressData.mac; + } + } + } + } + } + + return ''; + } + + /** + * Get first CPU Model + * @return {string} CPU + */ + getCPUModel() { + // And cpu model + const cpus = os.cpus(); + if (cpus && cpus.length) { + return cpus[0].model; + } + } + + /** + * Get parts of current path + * @param {number} [divider=3] Return every nth letter, default is 3 + * @return {string} path + */ + getCurrentPath(divider) { + divider = divider >= 0 ? divider : 3; + // Add parts of current path for little bit of extra secrecy :P + return __dirname.split('').map((l,i) => (i + 1) % divider ? l : '').join(''); + } + + /** + * @private + * @param {string} secret - Secret string + * @param {string} salt - Salt string + * @return {Buffer} key + */ + getKeyBuffer(secret, salt) { + const iterations = this.iterations || 196934; + + return new Promise((resolve, reject) => { + Crypto.pbkdf2(secret, salt, iterations, 16, 'sha512', (err, derivedKey) => { + if (err) { + return reject(err); + } + //console.log('key', derivedKey.toString('hex')); + return resolve(derivedKey); + }); + }); + } + + /** + * @private + * @param {string} secret - Secret string + * @param {string} salt - Salt string + * @return {Buffer} key + */ + getKeyBufferSync(secret, salt) { + const iterations = this.iterations || 196934; + + return Crypto.pbkdf2Sync(secret, salt, iterations, 16, 'sha512'); + } + + /** + * Encrypts string + * @param {string} secret - Encrypt using this as secret + * @param {string} dataString - String to encrypt + * @param {Object} options - options + * @return {Promise} hash string + */ + encrypt(secret, dataString, options) { + const iv = Crypto.randomBytes(16); + const salt = Crypto.randomBytes(16); + + const getCipher = (keyBuffer) => { + const cipher = Crypto.createCipheriv('aes-128-cbc', keyBuffer, iv); + const encrypted = cipher.update(dataString); + const finalBuffer = Buffer.concat([encrypted, cipher.final()]); + + //Need to retain salt and IV for decryption, so this can be appended to the output with a separator (non-hex for this example) + return salt.toString('hex') + ':' + iv.toString('hex') + ':' + finalBuffer.toString('hex'); + } + + if (options && options.sync) { + const keyBuffer = this.getKeyBufferSync(secret, salt); + return getCipher(keyBuffer); + } + + return this.getKeyBuffer(secret, salt).then(keyBuffer => { + return getCipher(keyBuffer); + }); + } + + /** + * Decrypts string + * @param {string} secret - Decrypt using this as secret + * @param {string} dataString - String to decrypt as provided by secretHelper.encrypt() (salt:iv:cipher) + * @param {Object} options - options + * @return {undefined} + */ + decrypt(secret, dataString, options) { + const parts = dataString.split(':'); + const salt = new Buffer(parts[0], 'hex'); + const iv = new Buffer(parts[1], 'hex'); + const cipher = new Buffer(parts[2], 'hex'); + + const decipher = (keyBuffer) => { + const decipher = Crypto.createDecipheriv('aes-128-cbc', keyBuffer, iv); + const decrypted = decipher.update(cipher); + const clearText = Buffer.concat([decrypted, decipher.final()]).toString(); + + return clearText; + } + + if(options && options.sync) { + const keyBuffer = this.getKeyBufferSync(secret, salt); + return decipher(keyBuffer); + } + + return this.getKeyBuffer(secret, salt).then(keyBuffer => { + return decipher(keyBuffer); + }); + } + + decryptSync(secret, dataString) { + return this.decrypt(secret, dataString, {sync: true}); + } + + encryptSync(secret, dataString) { + return this.encrypt(secret, dataString, {sync: true}); + } +} + +module.exports = new SecretHelper(); diff --git a/package-lock.json b/package-lock.json index b53cbad..562e264 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,6 +2,7 @@ "name": "studio-helper", "version": "1.8.1", "lockfileVersion": 1, + "requires": true, "dependencies": { "acorn": { "version": "5.1.1", @@ -463,11 +464,6 @@ "boom": "2.10.1" } }, - "cryptr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-2.0.0.tgz", - "integrity": "sha1-rf9gAWL0orHZjE4hhdzdposKCnI=" - }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -2233,14 +2229,6 @@ "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", "dev": true }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -2251,6 +2239,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", diff --git a/package.json b/package.json index e5a3638..e8ef55c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "author": "Petteri", "dependencies": { "bluebird": "^3.4.5", - "cryptr": "^2.0.0", "ignore": "^3.1.5", "inquirer": "^1.1.3", "mime-types": "^2.1.11",