From b2139f3930cd063acd381dfaa888c3d5146bbac7 Mon Sep 17 00:00:00 2001 From: Adam Hull Date: Sun, 23 Oct 2022 13:04:58 -0700 Subject: [PATCH] organize into directories --- .gitignore | 4 +- README.md | 11 +- cli/index.js | 353 +++++++++--------- example.html => example/example.html | 0 .../example_encrypted.html | 2 +- .../password_template.html | 0 cli/package-lock.json => package-lock.json | 2 +- cli/package.json => package.json | 7 +- {cli/scripts => scripts}/build.sh | 7 +- index.html => www/index.html | 0 .../kryptojs-3.1.9-1-lib.js | 0 11 files changed, 202 insertions(+), 184 deletions(-) rename example.html => example/example.html (100%) rename example_encrypted.html => example/example_encrypted.html (96%) rename password_template.html => lib/password_template.html (100%) rename cli/package-lock.json => package-lock.json (99%) rename cli/package.json => package.json (91%) rename {cli/scripts => scripts}/build.sh (61%) rename index.html => www/index.html (100%) rename kryptojs-3.1.9-1-lib.js => www/kryptojs-3.1.9-1-lib.js (100%) diff --git a/.gitignore b/.gitignore index ac6606f8..bb5ea02a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ .idea node_modules .staticrypt.json -/cli/README.md -/cli/LICENSE -/cli/password_template.html +/www/password_template.html diff --git a/README.md b/README.md index f3988057..0614c2a0 100644 --- a/README.md +++ b/README.md @@ -133,19 +133,26 @@ The salt isn't secret, so you don't need to worry about hiding the config file. ## Contributing +### Source Directories + +- `cli/` - The command-line interface published to NPM. +- `example/` - This file is encrypted as part of the build. The encrypted file is committed both to make this library easy to explore and as a review-time sanity check. +- `lib/` - Files shared across www and cli. +- `scripts/` - Build, test, deploy, CI, etc. See `npm run-script`. +- `www/` - The source for the in-browser encryption site hosted at https://robinmoisson.github.io/staticrypt. + ### Build Built assets are committed to main. Run build before submitting a PR or publishing to npm. ``` # From staticrypt/ -$ cd cli $ npm install $ npm run build ``` ### Test Testing is currently manual to keep dependencies low. -[Build](#build), then open `example_encypted.html`. +[Build](#build), then open `example/example_encypted.html`. ## 🙏 Contribution diff --git a/cli/index.js b/cli/index.js index 1098b2b6..08c6ea45 100755 --- a/cli/index.js +++ b/cli/index.js @@ -1,31 +1,35 @@ #!/usr/bin/env node -'use strict'; +"use strict"; const CryptoJS = require("crypto-js"); const fs = require("fs"); const path = require("path"); -const Yargs = require('yargs'); +const Yargs = require("yargs"); -const SCRIPT_URL = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js'; -const SCRIPT_TAG = ''; +const SCRIPT_URL = + "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js"; +const SCRIPT_TAG = + ''; /** * Salt and encrypt a msg with a password. * Inspired by https://github.com/adonespitogo */ function encrypt(msg, hashedPassphrase) { - var iv = CryptoJS.lib.WordArray.random(128 / 8); + var iv = CryptoJS.lib.WordArray.random(128 / 8); - var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, { - iv: iv, - padding: CryptoJS.pad.Pkcs7, - mode: CryptoJS.mode.CBC - }); + var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, { + iv: iv, + padding: CryptoJS.pad.Pkcs7, + mode: CryptoJS.mode.CBC, + }); - // iv will be hex 16 in length (32 characters) - // we prepend it to the ciphertext for use in decryption - return iv.toString() + encrypted.toString(); + // iv will be hex 16 in length (32 characters) + // we prepend it to the ciphertext for use in decryption + return iv.toString() + encrypted.toString(); } /** @@ -36,16 +40,16 @@ function encrypt(msg, hashedPassphrase) { * @returns string */ function hashPassphrase(passphrase, salt) { - var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, { - keySize: 256 / 32, - iterations: 1000 - }); + var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, { + keySize: 256 / 32, + iterations: 1000, + }); - return hashedPassphrase.toString(); + return hashedPassphrase.toString(); } function generateRandomSalt() { - return CryptoJS.lib.WordArray.random(128 / 8).toString(); + return CryptoJS.lib.WordArray.random(128 / 8).toString(); } /** @@ -61,120 +65,121 @@ function generateRandomSalt() { * @returns {boolean} */ function isOptionSetByUser(option, yargs) { - function searchForOption(option) { - return process.argv.indexOf(option) > -1; - } + function searchForOption(option) { + return process.argv.indexOf(option) > -1; + } - if (searchForOption(`-${option}`) || searchForOption(`--${option}`)) { - return true; - } + if (searchForOption(`-${option}`) || searchForOption(`--${option}`)) { + return true; + } - // Handle aliases for same option - for (let aliasIndex in yargs.parsed.aliases[option]) { - const alias = yargs.parsed.aliases[option][aliasIndex]; + // Handle aliases for same option + for (let aliasIndex in yargs.parsed.aliases[option]) { + const alias = yargs.parsed.aliases[option][aliasIndex]; - if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`)) - return true; - } + if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`)) + return true; + } - return false; + return false; } -const yargs = Yargs - .usage('Usage: staticrypt [options]') - .option('c', { - alias: 'config', - type: 'string', - describe: 'Path to the config file. Set to "false" to disable.', - default: '.staticrypt.json', - }) - .option('decrypt-button', { - type: 'string', - describe: 'Label to use for the decrypt button. Default: "DECRYPT".', - default: 'DECRYPT' - }) - .option('e', { - alias: 'embed', - type: 'boolean', - describe: 'Whether or not to embed crypto-js in the page (or use an external CDN).', - default: true - }) - .option('f', { - alias: 'file-template', - type: 'string', - describe: 'Path to custom HTML template with passphrase prompt.', - default: path.join(__dirname, 'password_template.html') - }) - .option('i', { - alias: 'instructions', - type: 'string', - describe: 'Special instructions to display to the user.', - default: '' - }) - .option('noremember', { - type: 'boolean', - describe: 'Set this flag to remove the "Remember me" checkbox.', - default: false, - }) - .option('o', { - alias: 'output', - type: 'string', - describe: 'File name / path for generated encrypted file.', - default: null - }) - .option('passphrase-placeholder', { - type: 'string', - describe: 'Placeholder to use for the passphrase input.', - default: 'Passphrase' - }) - .option('r', { - alias: 'remember', - type: 'number', - describe: 'Expiration in days of the "Remember me" checkbox that will save the (salted + hashed) passphrase ' + - 'in localStorage when entered by the user. Default: "0", no expiration.', - default: 0, - }) - .option('remember-label', { - type: 'string', - describe: 'Label to use for the "Remember me" checkbox.', - default: 'Remember me' - }) - // do not give a default option to this 'remember' parameter - we want to see when the flag is included with no - // value and when it's not included at all - .option('s', { - alias: 'salt', - describe: 'Set the salt manually. It should be set if you want use "Remember me" through multiple pages. It ' + - 'needs to be a 32 character long hexadecimal string.\nInclude the empty flag to generate a random salt you ' + - 'can use: "statycrypt -s".', - type: 'string', - }) - .option('t', { - alias: 'title', - type: 'string', - describe: "Title for output HTML page.", - default: 'Protected Page' - }); +const yargs = Yargs.usage("Usage: staticrypt [options]") + .option("c", { + alias: "config", + type: "string", + describe: 'Path to the config file. Set to "false" to disable.', + default: ".staticrypt.json", + }) + .option("decrypt-button", { + type: "string", + describe: 'Label to use for the decrypt button. Default: "DECRYPT".', + default: "DECRYPT", + }) + .option("e", { + alias: "embed", + type: "boolean", + describe: + "Whether or not to embed crypto-js in the page (or use an external CDN).", + default: true, + }) + .option("f", { + alias: "file-template", + type: "string", + describe: "Path to custom HTML template with passphrase prompt.", + default: path.join(__dirname, "..", "lib", "password_template.html"), + }) + .option("i", { + alias: "instructions", + type: "string", + describe: "Special instructions to display to the user.", + default: "", + }) + .option("noremember", { + type: "boolean", + describe: 'Set this flag to remove the "Remember me" checkbox.', + default: false, + }) + .option("o", { + alias: "output", + type: "string", + describe: "File name / path for generated encrypted file.", + default: null, + }) + .option("passphrase-placeholder", { + type: "string", + describe: "Placeholder to use for the passphrase input.", + default: "Passphrase", + }) + .option("r", { + alias: "remember", + type: "number", + describe: + 'Expiration in days of the "Remember me" checkbox that will save the (salted + hashed) passphrase ' + + 'in localStorage when entered by the user. Default: "0", no expiration.', + default: 0, + }) + .option("remember-label", { + type: "string", + describe: 'Label to use for the "Remember me" checkbox.', + default: "Remember me", + }) + // do not give a default option to this 'remember' parameter - we want to see when the flag is included with no + // value and when it's not included at all + .option("s", { + alias: "salt", + describe: + 'Set the salt manually. It should be set if you want use "Remember me" through multiple pages. It ' + + "needs to be a 32 character long hexadecimal string.\nInclude the empty flag to generate a random salt you " + + 'can use: "statycrypt -s".', + type: "string", + }) + .option("t", { + alias: "title", + type: "string", + describe: "Title for output HTML page.", + default: "Protected Page", + }); const namedArgs = yargs.argv; - // if the 's' flag is passed without parameter, generate a salt, display & exit -if (isOptionSetByUser('s', yargs) && !namedArgs.salt) { - console.log(generateRandomSalt()); - process.exit(0); +if (isOptionSetByUser("s", yargs) && !namedArgs.salt) { + console.log(generateRandomSalt()); + process.exit(0); } // validate the number of arguments if (namedArgs._.length !== 2) { - Yargs.showHelp(); - process.exit(1); + Yargs.showHelp(); + process.exit(1); } // get config file -const isUsingconfigFile = namedArgs.config.toLowerCase() !== 'false'; -const configPath = './' + namedArgs.config; +const isUsingconfigFile = namedArgs.config.toLowerCase() !== "false"; +const configPath = "./" + namedArgs.config; let config = {}; if (isUsingconfigFile && fs.existsSync(configPath)) { - config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + config = JSON.parse(fs.readFileSync(configPath, "utf8")); } /** @@ -183,41 +188,43 @@ if (isUsingconfigFile && fs.existsSync(configPath)) { let salt; // either a salt was provided by the user through the flag --salt if (!!namedArgs.salt) { - salt = String(namedArgs.salt).toLowerCase(); + salt = String(namedArgs.salt).toLowerCase(); } // or we try to read the salt from config file else if (!!config.salt) { - salt = config.salt; + salt = config.salt; } // or we generate a salt else { - salt = generateRandomSalt(); + salt = generateRandomSalt(); } // validate the salt if (salt.length !== 32 || /[^a-f0-9]/.test(salt)) { - console.log("The salt should be a 32 character long hexadecimal string (only [0-9a-f] characters allowed)"); - console.log("Detected salt: " + salt); - process.exit(1); + console.log( + "The salt should be a 32 character long hexadecimal string (only [0-9a-f] characters allowed)" + ); + console.log("Detected salt: " + salt); + process.exit(1); } // write salt to config file if (isUsingconfigFile && config.salt !== salt) { - config.salt = salt; - fs.writeFileSync(configPath, JSON.stringify(config, null, 4)); + config.salt = salt; + fs.writeFileSync(configPath, JSON.stringify(config, null, 4)); } // parse input const input = namedArgs._[0].toString(), - passphrase = namedArgs._[1].toString(); + passphrase = namedArgs._[1].toString(); // get the file content let contents; try { - contents = fs.readFileSync(input, 'utf8'); + contents = fs.readFileSync(input, "utf8"); } catch (e) { - console.log("Failure: input file does not exist!"); - process.exit(1); + console.log("Failure: input file does not exist!"); + process.exit(1); } // encrypt input @@ -225,63 +232,71 @@ const hashedPassphrase = hashPassphrase(passphrase, salt); const encrypted = encrypt(contents, hashedPassphrase); // we use the hashed passphrase in the HMAC because this is effectively what will be used a passphrase (so we can store // it in localStorage safely, we don't use the clear text passphrase) -const hmac = CryptoJS.HmacSHA256(encrypted, CryptoJS.SHA256(hashedPassphrase).toString()).toString(); +const hmac = CryptoJS.HmacSHA256( + encrypted, + CryptoJS.SHA256(hashedPassphrase).toString() +).toString(); const encryptedMessage = hmac + encrypted; // create crypto-js tag (embedded or not) let cryptoTag = SCRIPT_TAG; if (namedArgs.embed) { - try { - const embedContents = fs.readFileSync(path.join(__dirname, 'crypto-js.min.js'), 'utf8'); - - cryptoTag = ''; - } catch (e) { - console.log("Failure: embed file does not exist!"); - process.exit(1); - } + try { + const embedContents = fs.readFileSync( + path.join(__dirname, "crypto-js.min.js"), + "utf8" + ); + + cryptoTag = ""; + } catch (e) { + console.log("Failure: embed file does not exist!"); + process.exit(1); + } } const data = { - crypto_tag: cryptoTag, - decrypt_button: namedArgs.decryptButton, - embed: namedArgs.embed, - encrypted: encryptedMessage, - instructions: namedArgs.instructions, - is_remember_enabled: namedArgs.noremember ? 'false' : 'true', - output_file_path: namedArgs.output !== null ? namedArgs.output : input.replace(/\.html$/, '') + "_encrypted.html", - passphrase_placeholder: namedArgs.passphrasePlaceholder, - remember_duration_in_days: namedArgs.remember, - remember_me: namedArgs.rememberLabel, - salt: salt, - title: namedArgs.title, + crypto_tag: cryptoTag, + decrypt_button: namedArgs.decryptButton, + embed: namedArgs.embed, + encrypted: encryptedMessage, + instructions: namedArgs.instructions, + is_remember_enabled: namedArgs.noremember ? "false" : "true", + output_file_path: + namedArgs.output !== null + ? namedArgs.output + : input.replace(/\.html$/, "") + "_encrypted.html", + passphrase_placeholder: namedArgs.passphrasePlaceholder, + remember_duration_in_days: namedArgs.remember, + remember_me: namedArgs.rememberLabel, + salt: salt, + title: namedArgs.title, }; genFile(data); - /** * Fill the template with provided data and writes it to output file. * * @param data */ function genFile(data) { - let templateContents; + let templateContents; - try { - templateContents = fs.readFileSync(namedArgs.f, 'utf8'); - } catch (e) { - console.log("Failure: could not read template!"); - process.exit(1); - } + try { + templateContents = fs.readFileSync(namedArgs.f, "utf8"); + } catch (e) { + console.log("Failure: could not read template!"); + process.exit(1); + } - const renderedTemplate = render(templateContents, data); + const renderedTemplate = render(templateContents, data); - try { - fs.writeFileSync(data.output_file_path, renderedTemplate); - } catch (e) { - console.log("Failure: could not generate output file!"); - process.exit(1); - } + try { + fs.writeFileSync(data.output_file_path, renderedTemplate); + } catch (e) { + console.log("Failure: could not generate output file!"); + process.exit(1); + } } /** @@ -292,11 +307,11 @@ function genFile(data) { * @returns string */ function render(tpl, data) { - return tpl.replace(/{(.*?)}/g, function (_, key) { - if (data && data[key] !== undefined) { - return data[key]; - } + return tpl.replace(/{(.*?)}/g, function (_, key) { + if (data && data[key] !== undefined) { + return data[key]; + } - return ''; - }); + return ""; + }); } diff --git a/example.html b/example/example.html similarity index 100% rename from example.html rename to example/example.html diff --git a/example_encrypted.html b/example/example_encrypted.html similarity index 96% rename from example_encrypted.html rename to example/example_encrypted.html index 7942082f..0be75c54 100644 --- a/example_encrypted.html +++ b/example/example_encrypted.html @@ -166,7 +166,7 @@