diff --git a/cli.js b/cli.js index 94ff17c..3c5bad2 100755 --- a/cli.js +++ b/cli.js @@ -13,6 +13,7 @@ program .option('-v, --verbose', 'enable verbosity') .option('-t, --trace', 'enable stacktrace') .option('-p, --tmp ', 'change temporary directory') + .option('-H, --hide', 'enable filenames encryption. A metadata file will be created in the same directory. Do not loose the metadata file if you want to be able to restore the names. The decription of names is automatic') .description('encrypt a file or all files in a directory') .action(encrypt); diff --git a/src/UserInterface.js b/src/UserInterface.js index 9e9cb3b..0b024f3 100644 --- a/src/UserInterface.js +++ b/src/UserInterface.js @@ -23,6 +23,19 @@ function showHeader() { }); } +async function askHideNames() { + showHeader(); + return inquirer.prompt({ + name: 'hide', + type: 'list', + message: 'Do you also want to encrypt your files name?', + choices: ['Yes', 'No'], + default: 'Yes' + }).then((input) => { + return input.hide; + }) +} + async function askMode() { showHeader(); return inquirer.prompt({ @@ -42,7 +55,7 @@ async function askOptions() { name: 'options', type: 'checkbox', message: 'Choose options you want to enable:', - choices: ['verbose', 'trace'], + choices: ['verbose', 'trace errors'], }).then((input) => { return input.options; }) @@ -62,18 +75,23 @@ async function askFile() { } async function showUI() { - const options = { parent: { verbose: false, trace: false, tmp: false } }; + const options = {verbose: false, trace: false, tmp: false, hide: true }; const mode = await askMode(); const enabledOptions = await askOptions(); - options.parent.verbose = enabledOptions.includes('verbose'); - options.parent.trace = enabledOptions.includes('trace'); + options.verbose = enabledOptions.includes('verbose'); + options.trace = enabledOptions.includes('trace errors'); const file = await askFile(); - showHeader(); if (mode === MODE_DECRYPT) { + showHeader(); await decrypt(file, options) } else { + const hide = await askHideNames(); + if(hide === 'No'){ + options.hide=false; + } + showHeader() await encrypt(file, options) } } diff --git a/src/index.js b/src/index.js index 68a4a47..b11d745 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ const Spinner = require('./Spinner'); const os = require('os'); const path = require('path'); const mkdirp = require('mkdirp'); +const fs = require('fs'); function clearline() { process.stdout.write('\u001b[1G\u001b[2K'); @@ -51,10 +52,13 @@ function display_verbose(message, filename, trace = false, error = "") { stream.write('\r\n'); } -async function encrypt(file, { verbose, trace, tmp }) { +async function encrypt(file, {verbose, trace, tmp, hide}) { + try { const key = await ask_password(true); + let added = 0; + let current = 0; let done = 0; let failed = 0; @@ -83,6 +87,7 @@ async function encrypt(file, { verbose, trace, tmp }) { const encryption = warshield.encryptRecursive(file, key, tmp); encryption.on('crawl-found', filename => { + added++; if (verbose) { display_verbose("Added file", filename); } else { @@ -108,43 +113,57 @@ async function encrypt(file, { verbose, trace, tmp }) { encryption.on('done', filename => { done++; - + current++; if (verbose) { - display_verbose("\x1b[32mDone encrypting", filename); + display_verbose(`\x1b[32mDone encrypting [${current}/${added}]`, filename); } else { - spinner.query = `Encrypting files... ${filename}`; + let percent = parseInt((current/added)*100) + spinner.query = `Encrypting files ${percent}% : ${filename}`; } }); encryption.on('failed', (filename, err) => { failed++; - + current++; if (verbose) { - display_verbose("\x1b[31mFailed encrypting", filename, trace, err); + display_verbose(`\x1b[31mFailed encrypting [${current}/${added}]`, filename, trace, err); } else { - spinner.query = `Encrypting files... ${filename}`; + let percent = parseInt((current/added)*100) + spinner.query = `Encrypting files ${percent}% : ${filename}`; } }); encryption.on('end', () => { - const diff = process.hrtime(start); - if (spinner) { - spinner.stop(); - } + function finish(){ + const diff = process.hrtime(start); + if (spinner) { + spinner.stop(); + } - if (verbose) { - process.stdout.write('\r\n'); - } else { - clearline(); + if (verbose) { + process.stdout.write('\r\n'); + } else { + clearline(); + } + + console.log(`Finished encrypting files!`); + console.log(`Elapsed time: ${((diff[0] * 1e9 + diff[1]) / 1e9).toFixed(2)}s!`); + console.log(`Total encrypted files: ${done}`); + console.log(`Failed: ${failed} (read-only or access denied files)`); + + process.exit(); } - console.log(`Finished encrypting files!`); - console.log(`Elapsed time: ${((diff[0] * 1e9 + diff[1]) / 1e9).toFixed(2)}s!`); - console.log(`Total encrypted files: ${done}`); - console.log(`Failed: ${failed} (read-only or access denied files)`); + if(hide){ + var metadata=warshield.encryptNames(file) + warshield.encryptFile(metadata, key, tmp).then(()=>{ + finish() + }); + }else{ + finish(); + } - process.exit(); }); } catch (e) { console.error(e.message); @@ -158,6 +177,8 @@ async function decrypt(file, { verbose, trace, tmp }) { let done = 0; let failed = 0; + let added = 0; + let current = 0; if (!verbose) { var spinner = new Spinner("Starting decryption..."); @@ -179,14 +200,15 @@ async function decrypt(file, { verbose, trace, tmp }) { process.stdout.write(' Done!\n'); process.stdout.write('Starting decrypting files...\n'); } - + const start = process.hrtime(); - - const decryption = warshield.decryptRecursive(file, key, tmp); + const newfile = await warshield.decryptNames(file, key, tmp); + const decryption = warshield.decryptRecursive(newfile, key, tmp); decryption.on('crawl-found', filename => { + added++; if (verbose) { - display_verbose("Failed adding", filename); + display_verbose("Added file", filename); } else { spinner.query = `Crawling files... ${filename}`; } @@ -210,43 +232,49 @@ async function decrypt(file, { verbose, trace, tmp }) { decryption.on('done', filename => { done++; - + current++; if (verbose) { - display_verbose("\x1b[32mDone decrypting", filename); + display_verbose(`\x1b[32mDone decrypting [${current}/${added}]`, filename); } else { - spinner.query = `Decrypting files... ${filename}`; + let percent = parseInt((current/added)*100) + spinner.query = `Decrypting files ${percent}% : ${filename}`; } }); decryption.on('failed', (filename, err) => { failed++; - + current++; if (verbose) { - display_verbose("\x1b[31mFailed decrypting", filename, trace, err); + display_verbose(`\x1b[32mFailed decrypting [${current}/${added}]`, filename, trace, err); } else { - spinner.query = `Decrypting files... ${filename}`; + let percent = parseInt((current/added)*100) + spinner.query = `Decrypting files ${percent}% : ${filename}`; } }); decryption.on('end', () => { - const diff = process.hrtime(start); + function finish(){ + const diff = process.hrtime(start); - if (spinner) { - spinner.stop(); - } + if (spinner) { + spinner.stop(); + } - if (verbose) { - process.stdout.write('\r\n'); - } else { - clearline(); - } + if (verbose) { + process.stdout.write('\r\n'); + } else { + clearline(); + } - console.log(`Finished decrypting files!`); - console.log(`Elapsed time: ${((diff[0] * 1e9 + diff[1]) / 1e9).toFixed(2)}s!`); - console.log(`Total decrypted files: ${done}`); - console.log(`Failed: ${failed} (read-only, access denied or non-encrypted files)`); + console.log(`Finished decrypting files!`); + console.log(`Elapsed time: ${((diff[0] * 1e9 + diff[1]) / 1e9).toFixed(2)}s!`); + console.log(`Total decrypted files: ${done}`); + console.log(`Failed: ${failed} (read-only, access denied or non-encrypted files)`); - process.exit(); + process.exit(); + } + + finish(); }); } catch (e) { console.error(e.message); diff --git a/src/warshield.js b/src/warshield.js index 2a19063..40d8e0e 100644 --- a/src/warshield.js +++ b/src/warshield.js @@ -7,6 +7,7 @@ const path = require('path'); const ENCRYPTION_ALGORITHM = 'aes-256-gcm'; const MIN_ROUNDS = 3000; const MAX_ROUNDS = 9000; +const METADATA_FILE = 'metadata.json' /** * @param {any[]} arr @@ -411,6 +412,122 @@ function getFiles(directory) { return em; } +/** + * Computes the sha256 of a name using a random salt each time. + * @param {string} name + * @returns {string} + */ +function shaFileName(name){ + var salt = crypto.randomBytes(64); + return crypto.createHash('sha256').update(name+salt).digest('hex'); +} + +/** + * Rename all the subdirectories of a given path with the name sha256 value, + * and fill the obj with the bijective corrispondence (sha -> name, name -> sha) + * @param {string} filePath + * @param {Object} obj + */ +function hideNames(filePath, obj={}){ + var file = path.basename(filePath); + var dir = path.dirname(filePath); + var shaName = shaFileName(file); + var newPath = path.join(dir, shaName) + obj[shaName] = file; + obj[file] = shaName; + fs.renameSync(filePath, newPath); + var stat = fs.statSync(newPath); + if(stat.isDirectory()){ + var files = fs.readdirSync(newPath); + for(var x of files){ + var newfilePath= path.join(newPath, x); + hideNames(newfilePath, obj); + } + } +} + +/** + * Sync function to encrypt names. + * @param {string} filePath + * @returns {string} metadataPath +*/ +function encryptNames(filePath){ + var metadata_filenames={}; + var dirPath = path.dirname(filePath); + var oldName = path.basename(filePath) + + hideNames(filePath, metadata_filenames); + var newPath = path.join(dirPath, metadata_filenames[oldName]); + if(fs.statSync(newPath).isDirectory()){ + var metadataPath = path.join(newPath, METADATA_FILE); + fs.writeFileSync(metadataPath, JSON.stringify(metadata_filenames, null, 2)); + }else{ + var metadataPath = METADATA_FILE; + fs.writeFileSync(METADATA_FILE, JSON.stringify(metadata_filenames, null, 2)); + } + + return metadataPath; +} + +/** + * Rename all the subdirectories of a given path with previously stored data in the json metadata obj + * @param {string} filePath + * @param {Object} obj + */ +function showNames(filePath, obj={}){ + var file = path.basename(filePath); + var dir = path.dirname(filePath); + var realname = obj[file]; + var newPath = path.join(dir, realname) + + fs.renameSync(filePath, newPath); + var stat = fs.statSync(newPath); + if(stat.isDirectory()){ + var files = fs.readdirSync(newPath); + for(var x of files){ + var newfilePath= path.join(newPath, x); + showNames(newfilePath, obj); + } + } +} + +/** + * Function to decrypt files name. It returns the new file path (the one restored). + * If it doesn't find the metadata file it returns the file path passed. To restore the files name, + * it decrypts the found metadata file and deletes it. If somenthing wrong happens during the metadata file decryption + * it returns the original file path. + * @param {string} filePath + * @param {string} key + * @param {string} tmp + * @returns {string} + */ +async function decryptNames(filePath, key, tmp){ + var stat = fs.statSync(filePath); + var metadata = path.join(path.dirname(filePath), METADATA_FILE); + + if(stat.isDirectory()){ + metadata = path.join(filePath, METADATA_FILE); + } + + if(fs.existsSync(metadata)){ + try{ + await decryptFile(metadata, key, tmp); + const infoObj = JSON.parse(fs.readFileSync(metadata)); + fs.unlinkSync(metadata); + showNames(filePath, infoObj); + var newPath= path.join(path.dirname(filePath), infoObj[path.basename(filePath)]) + return newPath; + }catch(err){ + return filePath; + } + + }else{ + return filePath + } + +} + + module.exports = { generateKey, encryptStream, @@ -419,4 +536,7 @@ module.exports = { decryptFile, encryptRecursive, decryptRecursive, + encryptNames, + decryptNames, + } \ No newline at end of file