From 2d20ac9a54521d893bcc6c2ff54d266169b26f7c Mon Sep 17 00:00:00 2001 From: HR Date: Sat, 29 Jun 2019 21:16:49 +0100 Subject: [PATCH] Implement folder encryption and decryption fix #12 #37 --- app/core/Db.js | 2 +- app/core/crypto.js | 237 ++++++++++------------- app/index.js | 4 +- app/src/crypter.js | 2 +- app/src/masterPassPrompt.js | 2 +- app/src/settings.js | 2 +- app/src/setup.js | 2 +- app/static/js/crypter.js | 2 +- app/{script => utils}/logger.js | 0 app/{script/utils.js => utils/update.js} | 12 -- app/utils/utils.js | 14 ++ test/test.js | 2 +- test/ui/test.js | 2 +- test2.txt | 1 + 14 files changed, 131 insertions(+), 153 deletions(-) rename app/{script => utils}/logger.js (100%) rename app/{script/utils.js => utils/update.js} (85%) create mode 100644 app/utils/utils.js create mode 100644 test2.txt diff --git a/app/core/Db.js b/app/core/Db.js index 23c31b9..a99f58a 100644 --- a/app/core/Db.js +++ b/app/core/Db.js @@ -5,7 +5,7 @@ ******************************/ const _ = require('lodash') -const logger = require('../script/logger') +const logger = require('../utils/logger') const fs = require('fs-extra') /** diff --git a/app/core/crypto.js b/app/core/crypto.js index 222d41b..44c70de 100644 --- a/app/core/crypto.js +++ b/app/core/crypto.js @@ -7,10 +7,11 @@ const fs = require('fs-extra') const path = require('path') const scrypto = require('crypto') -const logger = require('../script/logger') -const Readable = require('stream').Readable +const logger = require('../utils/logger') +const Readable = require('stream') + .Readable const tar = require('tar-fs') -const {CRYPTO, REGEX, ERRORS} = require('../config') +const { CRYPTO, REGEX, ERRORS } = require('../config') // Helper functions @@ -54,17 +55,19 @@ exports.encrypt = (origpath, mpkey) => { exports.deriveKey(mpkey, null, CRYPTO.DEFAULTS.ITERATIONS) .then((dcreds) => { let tag + let isDirectory = fs.lstatSync(origpath) + .isDirectory() let tempd = `${path.dirname(origpath)}/${CRYPTO.ENCRYPTION_TMP_DIR}` let dataDestPath = `${tempd}/data` let credsDestPath = `${tempd}/creds` - logger.verbose(`tempd: ${tempd}, dataDestPath: ${dataDestPath}, credsDestPath: ${credsDestPath}`) + logger.verbose(`tempd: ${tempd}, dataDestPath: ${dataDestPath}, credsDestPath: ${credsDestPath}, isDirectory: ${isDirectory}`) // create tempd temporary directory fs.mkdirs(tempd, (err) => { if (err) reject(err) logger.verbose(`Created ${tempd} successfully`) // readstream to read the (unencrypted) file - const origin = fs.createReadStream(origpath) + const origin = isDirectory ? tar.pack(origpath) : fs.createReadStream(origpath) // create data and creds file const dataDest = fs.createWriteStream(dataDestPath) const credsDest = fs.createWriteStream(credsDestPath) @@ -75,33 +78,23 @@ exports.encrypt = (origpath, mpkey) => { // Read file, apply tranformation (encryption) to stream and // then write stream to filesystem - // origin.pipe(zip).pipe(cipher).pipe(dest, { end: false }) - origin.pipe(cipher).pipe(dataDest) - - cipher.on('end', () => { - // get the generated Message Authentication Code - tag = cipher.getAuthTag() - // Write crdentials used to encrypt in creds file - // write in format Crypter#iv#authTag#salt - credsDest.end(`Crypter#${iv.toString('hex')}#${tag.toString('hex')}#${dcreds.salt.toString('hex')}`) - }) - - // readstream error handler - origin.on('error', (err) => { - // reject on readstream error - reject(err) - }) - - // writestream error handler - dataDest.on('error', (err) => { - // reject on writestream - reject(err) - }) - - credsDest.on('error', (err) => { - // reject on writestream - reject(err) - }) + origin + .on('error', (err) => reject(err)) + .pipe(cipher) + .on('error', (err) => reject(err)) + .pipe(dataDest) + .on('error', (err) => reject(err)) + .on('finish', () => { + // get the generated Message Authentication Code + tag = cipher.getAuthTag() + // Write crdentials used to encrypt in creds file + // write in format Crypter#iv#authTag#salt#isDir + let creds = `Crypter#${iv.toString('hex')}#${tag.toString('hex')}#${dcreds.salt.toString('hex')}` + if (isDirectory) { + creds += '#dir' + } + credsDest.end(creds) + }) // writestream finish handler credsDest.on('finish', () => { @@ -109,40 +102,29 @@ exports.encrypt = (origpath, mpkey) => { const tarDest = fs.createWriteStream(tarDestPath) const tarPack = tar.pack(tempd) // Pack directory and zip into a .crypto file - tarPack.pipe(tarDest) - - tarDest.on('error', (err) => { - // reject on writestream - reject(err) - }) - - tarPack.on('error', (err) => { - // reject on writestream - reject(err) - }) - - tarDest.on('finish', () => { - // Remove temporary dir tempd - fs.remove(tempd, (err) => { - if (err) reject(err) - // return all the credentials and parameters used for encryption - logger.verbose('Successfully deleted tempd!') - resolve({ - salt: dcreds.salt, - key: dcreds.key, - cryptpath: tarDestPath, - tag: tag, - iv: iv + tarPack + .on('error', (err) => reject(err)) + .pipe(tarDest) + .on('error', (err) => reject(err)) + .on('finish', () => { + // Remove temporary dir tempd + fs.remove(tempd, (err) => { + if (err) reject(err) + // return all the credentials and parameters used for encryption + logger.verbose('Successfully deleted tempd!') + resolve({ + salt: dcreds.salt, + key: dcreds.key, + cryptpath: tarDestPath, + tag: tag, + iv: iv + }) }) }) - }) }) }) }) - .catch((err) => { - // reject if error occured while deriving key - reject(err) - }) + .catch((err) => reject(err)) }) } @@ -158,33 +140,30 @@ exports.decrypt = (origpath, mpkey) => { let tarOrig = fs.createReadStream(origpath) let tarExtr = tar.extract(tempd) // Extract tar to CRYPTO.DECRYPTION_TMP_DIR directory - tarOrig.pipe(tarExtr) - - tarOrig.on('error', (err) => { - // reject on writestream - reject(err) - }) - - tarExtr.on('error', (err) => { - // reject on extraction error - reject(err) - }) - - tarExtr.on('finish', () => { - // Now read creds and use to decrypt data - logger.verbose('Finished extracting') + tarOrig + .on('error', (err) => reject(err)) + .pipe(tarExtr) + .on('error', (err) => reject(err)) + .on('finish', () => { + // Now read creds and use to decrypt data + logger.verbose('Finished extracting') + + readFile(credsOrigPath) + .then((credsLines) => { + let credsLine = credsLines.trim() + .match(REGEX.ENCRYPTION_CREDS) + + if (!credsLine) { + reject(new Error(ERRORS.DECRYPT)) + } - readFile(credsOrigPath) - .then((credsLines) => { - let credsLine = credsLines.trim().match(REGEX.ENCRYPTION_CREDS) - - if (credsLine) { let creds = credsLine[0].split('#') logger.verbose(`creds: ${creds}, credsLine: ${credsLine}`) const iv = Buffer.from(creds[1], 'hex') const authTag = Buffer.from(creds[2], 'hex') const salt = Buffer.from(creds[3], 'hex') + const isDir = creds[4] logger.verbose(`Extracted data, iv: ${iv}, authTag: ${authTag}, salt: ${salt}`) // Read encrypted data stream const dataOrig = fs.createReadStream(dataOrigPath) @@ -194,51 +173,42 @@ exports.decrypt = (origpath, mpkey) => { try { let decipher = scrypto.createDecipheriv(CRYPTO.DEFAULTS.ALGORITHM, dcreds.key, iv) decipher.setAuthTag(authTag) - const dataDest = fs.createWriteStream(dataDestPath) - dataOrig.pipe(decipher).pipe(dataDest) - - decipher.on('error', (err) => { - reject(err) - }) - - dataOrig.on('error', (err) => { - reject(err) - }) - - dataDest.on('error', (err) => { - reject(err) - }) - - dataDest.on('finish', () => { - logger.verbose(`Encrypted to ${dataDestPath}`) - // Now delete tempd (temporary directory) - fs.remove(tempd, (err) => { - if (err) - reject(err) - logger.verbose(`Removed temp dir ${tempd}`) - resolve({ - op: CRYPTO.DECRYPT_OP, - name: path.basename(origpath), - path: origpath, - cryptPath: dataDestPath, - salt: salt.toString('hex'), - key: dcreds.key.toString('hex'), - iv: iv.toString('hex'), - authTag: authTag.toString('hex') + let dataDest = isDir ? tar.extract(dataDestPath) : fs.createWriteStream(dataDestPath) + + dataOrig + .on('error', (err) => reject(err)) + .pipe(decipher) + .on('error', (err) => reject(err)) + .pipe(dataDest) + .on('error', (err) => reject(err)) + .on('finish', () => { + logger.verbose(`Encrypted to ${dataDestPath}`) + // Now delete tempd (temporary directory) + fs.remove(tempd, (err) => { + if (err) + reject(err) + logger.verbose(`Removed temp dir ${tempd}`) + resolve({ + op: CRYPTO.DECRYPT_OP, + name: path.basename(origpath), + path: origpath, + cryptPath: dataDestPath, + salt: salt.toString('hex'), + key: dcreds.key.toString('hex'), + iv: iv.toString('hex'), + authTag: authTag.toString('hex') + }) }) }) - }) } catch (err) { reject(err) } }) - } else { - reject(new Error(ERRORS.DECRYPT)) - } - }).catch((err) => { - reject(err) + }) + .catch((err) => { + reject(err) + }) }) - }) }) } @@ -251,17 +221,17 @@ exports.deriveKey = (pass, psalt) => { // If psalt is provided and is not a Buffer then coerce it and assign it // If psalt is not provided then generate a cryptographically secure salt // and assign it - const salt = (psalt) - ? ((Buffer.isBuffer(psalt)) - ? psalt - : Buffer.from(psalt)) - : scrypto.randomBytes(CRYPTO.DEFAULTS.KEYLENGTH) + const salt = (psalt) ? + ((Buffer.isBuffer(psalt)) ? + psalt : + Buffer.from(psalt)) : + scrypto.randomBytes(CRYPTO.DEFAULTS.KEYLENGTH) // derive the key using the salt, password and default crypto setup scrypto.pbkdf2(pass, salt, CRYPTO.DEFAULTS.MPK_ITERATIONS, CRYPTO.DEFAULTS.KEYLENGTH, CRYPTO.DEFAULTS.DIGEST, (err, key) => { if (err) reject(err) // return the key and the salt - resolve({key, salt}) + resolve({ key, salt }) }) }) } @@ -278,15 +248,20 @@ exports.genPassHash = (masterpass, salt) => { if (salt) { // create hash from the contanation of the pass and salt // assign the hex digest of the created hash - const hash = scrypto.createHash(CRYPTO.DEFAULTS.HASH_ALG).update(`${pass}${salt}`).digest('hex') - resolve({hash, key: masterpass}) + const hash = scrypto.createHash(CRYPTO.DEFAULTS.HASH_ALG) + .update(`${pass}${salt}`) + .digest('hex') + resolve({ hash, key: masterpass }) } else { // generate a cryptographically secure salt and use it as the salt - const salt = scrypto.randomBytes(CRYPTO.DEFAULTS.KEYLENGTH).toString('hex') + const salt = scrypto.randomBytes(CRYPTO.DEFAULTS.KEYLENGTH) + .toString('hex') // create hash from the contanation of the pass and salt // assign the hex digest of the created hash - const hash = scrypto.createHash(CRYPTO.DEFAULTS.HASH_ALG).update(`${pass}${salt}`).digest('hex') - resolve({hash, salt, key: masterpass}) + const hash = scrypto.createHash(CRYPTO.DEFAULTS.HASH_ALG) + .update(`${pass}${salt}`) + .digest('hex') + resolve({ hash, salt, key: masterpass }) } }) } @@ -309,4 +284,4 @@ exports.timingSafeEqual = (a, b) => { result |= a[l] ^ b[l] } return result === 0 -} +} \ No newline at end of file diff --git a/app/index.js b/app/index.js index 3ad147b..9872cf1 100644 --- a/app/index.js +++ b/app/index.js @@ -19,8 +19,8 @@ global.paths = { documents: app.getPath('documents') } -const logger = require('./script/logger') -const { checkUpdate } = require('./script/utils') +const logger = require('./utils/logger') +const { checkUpdate } = require('./utils/update') // Core const Db = require('./core/Db') // Windows diff --git a/app/src/crypter.js b/app/src/crypter.js index 779290a..cf7c734 100644 --- a/app/src/crypter.js +++ b/app/src/crypter.js @@ -2,7 +2,7 @@ const {app, ipcMain, Menu, BrowserWindow} = require('electron') const {VIEWS, ERRORS, WINDOW_OPTS} = require('../config') const crypto = require('../core/crypto') const menuTemplate = require('./menu') -const logger = require('../script/logger') +const logger = require('../utils/logger') exports.window = function (global, callback) { // creates a new BrowserWindow diff --git a/app/src/masterPassPrompt.js b/app/src/masterPassPrompt.js index 338dc62..3c5b44f 100644 --- a/app/src/masterPassPrompt.js +++ b/app/src/masterPassPrompt.js @@ -2,7 +2,7 @@ const {app, ipcMain, BrowserWindow} = require('electron') const {VIEWS, WINDOW_OPTS} = require('../config') const MasterPass = require('../core/MasterPass') const MasterPassKey = require('../core/MasterPassKey') -const logger = require('../script/logger') +const logger = require('../utils/logger') exports.window = function (global, callback) { diff --git a/app/src/settings.js b/app/src/settings.js index 9ff0eea..313ab76 100644 --- a/app/src/settings.js +++ b/app/src/settings.js @@ -1,6 +1,6 @@ const {app, ipcMain, BrowserWindow} = require('electron') const {CRYPTO, VIEWS, SETTINGS, ERRORS, WINDOW_OPTS} = require('../config') -const logger = require('../script/logger') +const logger = require('../utils/logger') const fs = require('fs-extra') const _ = require('lodash') diff --git a/app/src/setup.js b/app/src/setup.js index 10b19cb..056df53 100644 --- a/app/src/setup.js +++ b/app/src/setup.js @@ -2,7 +2,7 @@ const {app, ipcMain, BrowserWindow} = require('electron') const {VIEWS, WINDOW_OPTS} = require('../config') const MasterPass = require('../core/MasterPass') const MasterPassKey = require('../core/MasterPassKey') -const logger = require('../script/logger') +const logger = require('../utils/logger') exports.window = function (global, callback) { // setup view controller diff --git a/app/static/js/crypter.js b/app/static/js/crypter.js index 314c997..609ebcf 100644 --- a/app/static/js/crypter.js +++ b/app/static/js/crypter.js @@ -97,7 +97,7 @@ function handler () { dialog.showOpenDialog({ title: 'Choose a file to Encrypt', defaultPath: paths.documents, // open dialog at home directory - properties: ['openFile'] + properties: ['openFile', 'openDirectory'] }, function (filePath) { // callback for selected file returns undefined if file not selected by user if (filePath && filePath.length === 1) { diff --git a/app/script/logger.js b/app/utils/logger.js similarity index 100% rename from app/script/logger.js rename to app/utils/logger.js diff --git a/app/script/utils.js b/app/utils/update.js similarity index 85% rename from app/script/utils.js rename to app/utils/update.js index 616a770..7cb6196 100644 --- a/app/script/utils.js +++ b/app/utils/update.js @@ -10,18 +10,6 @@ function parseV(str) { } module.exports = { - isRenderer: function () { - // running in a web browser - if (typeof process === 'undefined') return true - - // node-integration is disabled - if (!process) return true - - // We're in node.js somehow - if (!process.type) return false - - return process.type === 'renderer' - }, checkUpdate: function () { return new Promise((resolve, reject) => { https.get(REPO.RELEASES_API_URL, { diff --git a/app/utils/utils.js b/app/utils/utils.js new file mode 100644 index 0000000..0aaa0d2 --- /dev/null +++ b/app/utils/utils.js @@ -0,0 +1,14 @@ +module.exports = { + isRenderer: function () { + // running in a web browser + if (typeof process === 'undefined') return true + + // node-integration is disabled + if (!process) return true + + // We're in node.js somehow + if (!process.type) return false + + return process.type === 'renderer' + } +} \ No newline at end of file diff --git a/test/test.js b/test/test.js index 3a49f64..36129b3 100644 --- a/test/test.js +++ b/test/test.js @@ -147,7 +147,7 @@ describe("Crypter Core Modules' tests", function () { return crypto.encrypt('', `${global.paths.tmp}`, global.MasterPassKey.get()) .catch((err) => { expect(err).to.be.an('error') - expect(err.message).to.equal("ENOENT: no such file or directory, open ''") + expect(err.message).to.equal("ENOENT: no such file or directory, lstat") }) }) }) diff --git a/test/ui/test.js b/test/ui/test.js index 2cc869a..1945867 100644 --- a/test/ui/test.js +++ b/test/ui/test.js @@ -6,7 +6,7 @@ const fs = require('fs-extra') const expect = chai.expect const chaiAsPromised = require('chai-as-promised') const path = require('path') -const logger = require('../../app/script/logger') +const logger = require('../../app/utils/logger') chai.should() chai.use(chaiAsPromised) diff --git a/test2.txt b/test2.txt new file mode 100644 index 0000000..110c58e --- /dev/null +++ b/test2.txt @@ -0,0 +1 @@ +Test 2