2017-12-15 21:37:02 +00:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
2017-12-14 22:38:34 +00:00
|
|
|
'use strict';
|
|
|
|
|
2022-04-23 10:00:18 +00:00
|
|
|
const CryptoJS = require("crypto-js");
|
|
|
|
const fs = require("fs");
|
2018-01-17 11:31:57 +00:00
|
|
|
const path = require("path");
|
2017-12-27 03:35:24 +00:00
|
|
|
const Yargs = require('yargs');
|
2017-12-14 22:38:34 +00:00
|
|
|
|
|
|
|
const SCRIPT_URL = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js';
|
|
|
|
const SCRIPT_TAG = '<script src="' + SCRIPT_URL + '" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></script>';
|
2017-12-14 22:00:11 +00:00
|
|
|
|
2019-06-28 14:23:13 +00:00
|
|
|
/**
|
|
|
|
* Salt and encrypt a msg with a password.
|
|
|
|
* Inspired by https://github.com/adonespitogo
|
|
|
|
*/
|
2022-02-10 08:22:32 +00:00
|
|
|
function encrypt(msg, hashedPassphrase) {
|
|
|
|
var iv = CryptoJS.lib.WordArray.random(128 / 8);
|
2019-06-28 14:23:13 +00:00
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, {
|
2019-06-28 14:23:13 +00:00
|
|
|
iv: iv,
|
|
|
|
padding: CryptoJS.pad.Pkcs7,
|
|
|
|
mode: CryptoJS.mode.CBC
|
|
|
|
});
|
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
// iv will be hex 16 in length (32 characters)
|
|
|
|
// we prepend it to the ciphertext for use in decryption
|
|
|
|
return iv.toString() + encrypted.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
|
|
|
|
*
|
|
|
|
* @param {string} passphrase
|
2022-02-27 18:37:17 +00:00
|
|
|
* @param {string} salt
|
|
|
|
* @returns string
|
2022-02-10 08:22:32 +00:00
|
|
|
*/
|
2022-02-27 16:50:15 +00:00
|
|
|
function hashPassphrase(passphrase, salt) {
|
2022-02-10 08:22:32 +00:00
|
|
|
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
|
|
|
|
keySize: 256 / 32,
|
|
|
|
iterations: 1000
|
|
|
|
});
|
|
|
|
|
2022-02-27 18:37:17 +00:00
|
|
|
return hashedPassphrase.toString();
|
2022-02-10 08:22:32 +00:00
|
|
|
}
|
|
|
|
|
2022-04-23 10:00:18 +00:00
|
|
|
function generateRandomSalt() {
|
|
|
|
return CryptoJS.lib.WordArray.random(128 / 8).toString();
|
|
|
|
}
|
|
|
|
|
2022-02-27 18:37:17 +00:00
|
|
|
/**
|
|
|
|
* Check if a particular option has been set by the user. Useful for distinguishing default value with flag without
|
|
|
|
* parameter.
|
|
|
|
*
|
|
|
|
* Ex use case: '-s' means "give me a salt", '-s 1234' means "use 1234 as salt"
|
|
|
|
*
|
|
|
|
* From https://github.com/yargs/yargs/issues/513#issuecomment-221412008
|
|
|
|
*
|
|
|
|
* @param option
|
|
|
|
* @param yargs
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
function isOptionSetByUser(option, yargs) {
|
|
|
|
function searchForOption(option) {
|
|
|
|
return process.argv.indexOf(option) > -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
|
|
if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const yargs = Yargs
|
2022-02-10 08:22:32 +00:00
|
|
|
.usage('Usage: staticrypt <filename> <passphrase> [options]')
|
2022-04-23 10:00:18 +00:00
|
|
|
.option('c', {
|
|
|
|
alias: 'config',
|
|
|
|
type: 'string',
|
2022-04-23 10:23:39 +00:00
|
|
|
describe: 'Path to the config file. Set to "false" to disable.',
|
2022-04-23 10:00:18 +00:00
|
|
|
default: '.staticrypt.json',
|
|
|
|
})
|
|
|
|
.option('decrypt-button', {
|
|
|
|
type: 'string',
|
|
|
|
describe: 'Label to use for the decrypt button. Default: "DECRYPT".',
|
|
|
|
default: 'DECRYPT'
|
|
|
|
})
|
2022-02-10 08:22:32 +00:00
|
|
|
.option('e', {
|
|
|
|
alias: 'embed',
|
|
|
|
type: 'boolean',
|
|
|
|
describe: 'Whether or not to embed crypto-js in the page (or use an external CDN).',
|
|
|
|
default: true
|
|
|
|
})
|
2022-04-23 10:00:18 +00:00
|
|
|
.option('f', {
|
|
|
|
alias: 'file-template',
|
2022-02-10 08:22:32 +00:00
|
|
|
type: 'string',
|
2022-04-23 10:00:18 +00:00
|
|
|
describe: 'Path to custom HTML template with passphrase prompt.',
|
|
|
|
default: path.join(__dirname, 'password_template.html')
|
2022-02-10 08:22:32 +00:00
|
|
|
})
|
|
|
|
.option('i', {
|
|
|
|
alias: 'instructions',
|
|
|
|
type: 'string',
|
|
|
|
describe: 'Special instructions to display to the user.',
|
|
|
|
default: ''
|
|
|
|
})
|
2022-04-23 10:00:18 +00:00
|
|
|
.option('noremember', {
|
|
|
|
type: 'boolean',
|
|
|
|
describe: 'Set this flag to remove the "Remember me" checkbox.',
|
|
|
|
default: false,
|
|
|
|
})
|
|
|
|
.option('o', {
|
|
|
|
alias: 'output',
|
2022-02-10 08:22:32 +00:00
|
|
|
type: 'string',
|
2022-04-23 10:00:18 +00:00
|
|
|
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'
|
2022-02-10 08:22:32 +00:00
|
|
|
})
|
|
|
|
.option('r', {
|
|
|
|
alias: 'remember',
|
|
|
|
type: 'number',
|
2022-02-27 18:37:17 +00:00
|
|
|
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.',
|
2022-02-10 18:58:07 +00:00
|
|
|
default: 0,
|
|
|
|
})
|
2022-02-10 08:22:32 +00:00
|
|
|
.option('remember-label', {
|
|
|
|
type: 'string',
|
2022-02-27 18:37:17 +00:00
|
|
|
describe: 'Label to use for the "Remember me" checkbox.',
|
2022-02-10 08:22:32 +00:00
|
|
|
default: 'Remember me'
|
|
|
|
})
|
2022-02-27 18:37:17 +00:00
|
|
|
// 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".',
|
2022-02-27 16:50:15 +00:00
|
|
|
type: 'string',
|
|
|
|
})
|
2022-04-23 10:00:18 +00:00
|
|
|
.option('t', {
|
|
|
|
alias: 'title',
|
2022-02-10 08:22:32 +00:00
|
|
|
type: 'string',
|
2022-04-23 10:00:18 +00:00
|
|
|
describe: "Title for output HTML page.",
|
|
|
|
default: 'Protected Page'
|
2022-02-27 18:37:17 +00:00
|
|
|
});
|
|
|
|
const namedArgs = yargs.argv;
|
2022-02-10 08:22:32 +00:00
|
|
|
|
2022-04-23 10:00:18 +00:00
|
|
|
|
|
|
|
// 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);
|
2022-02-27 18:37:17 +00:00
|
|
|
}
|
|
|
|
|
2022-04-23 10:00:18 +00:00
|
|
|
// validate the number of arguments
|
2022-02-10 08:22:32 +00:00
|
|
|
if (namedArgs._.length !== 2) {
|
|
|
|
Yargs.showHelp();
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
2022-04-23 10:00:18 +00:00
|
|
|
// get config file
|
2022-04-23 10:23:39 +00:00
|
|
|
const isUsingconfigFile = namedArgs.config.toLowerCase() !== 'false';
|
2022-04-23 17:56:59 +00:00
|
|
|
const configPath = './' + namedArgs.config;
|
2022-04-23 10:00:18 +00:00
|
|
|
let config = {};
|
|
|
|
if (isUsingconfigFile && fs.existsSync(configPath)) {
|
|
|
|
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the salt to use
|
|
|
|
*/
|
|
|
|
let salt;
|
|
|
|
// either a salt was provided by the user through the flag --salt
|
|
|
|
if (!!namedArgs.salt) {
|
|
|
|
salt = String(namedArgs.salt).toLowerCase();
|
|
|
|
}
|
|
|
|
// or we try to read the salt from config file
|
|
|
|
else if (!!config.salt) {
|
|
|
|
salt = config.salt;
|
|
|
|
}
|
|
|
|
// or we generate a salt
|
|
|
|
else {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
// write salt to config file
|
|
|
|
if (isUsingconfigFile && config.salt !== salt) {
|
|
|
|
config.salt = salt;
|
|
|
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
|
|
|
|
}
|
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
// parse input
|
|
|
|
const input = namedArgs._[0].toString(),
|
|
|
|
passphrase = namedArgs._[1].toString();
|
|
|
|
|
|
|
|
// get the file content
|
|
|
|
let contents;
|
|
|
|
try {
|
2022-04-23 10:00:18 +00:00
|
|
|
contents = fs.readFileSync(input, 'utf8');
|
2022-02-10 08:22:32 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.log("Failure: input file does not exist!");
|
|
|
|
process.exit(1);
|
2019-06-28 14:23:13 +00:00
|
|
|
}
|
|
|
|
|
2017-12-28 11:32:27 +00:00
|
|
|
// encrypt input
|
2022-02-27 18:37:17 +00:00
|
|
|
const hashedPassphrase = hashPassphrase(passphrase, salt);
|
2022-02-10 08:22:32 +00:00
|
|
|
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 encryptedMessage = hmac + encrypted;
|
2017-12-14 22:38:34 +00:00
|
|
|
|
2017-12-28 11:32:27 +00:00
|
|
|
// create crypto-js tag (embedded or not)
|
2022-02-10 08:22:32 +00:00
|
|
|
let cryptoTag = SCRIPT_TAG;
|
2017-12-28 11:32:27 +00:00
|
|
|
if (namedArgs.embed) {
|
|
|
|
try {
|
2022-04-23 10:00:18 +00:00
|
|
|
const embedContents = fs.readFileSync(path.join(__dirname, 'crypto-js.min.js'), 'utf8');
|
2022-02-10 08:22:32 +00:00
|
|
|
|
|
|
|
cryptoTag = '<script>' + embedContents + '</script>';
|
|
|
|
} catch (e) {
|
2017-12-19 01:32:27 +00:00
|
|
|
console.log("Failure: embed file does not exist!");
|
2017-12-14 22:38:34 +00:00
|
|
|
process.exit(1);
|
2017-12-19 01:32:27 +00:00
|
|
|
}
|
2017-12-14 22:38:34 +00:00
|
|
|
}
|
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
const data = {
|
2017-12-28 11:32:27 +00:00
|
|
|
crypto_tag: cryptoTag,
|
2022-02-10 08:22:32 +00:00
|
|
|
decrypt_button: namedArgs.decryptButton,
|
2017-12-28 11:32:27 +00:00
|
|
|
embed: namedArgs.embed,
|
2022-02-10 08:22:32 +00:00
|
|
|
encrypted: encryptedMessage,
|
|
|
|
instructions: namedArgs.instructions,
|
2022-02-10 18:58:07 +00:00
|
|
|
is_remember_enabled: namedArgs.noremember ? 'false' : 'true',
|
2022-02-10 08:22:32 +00:00
|
|
|
output_file_path: namedArgs.output !== null ? namedArgs.output : input.replace(/\.html$/, '') + "_encrypted.html",
|
|
|
|
passphrase_placeholder: namedArgs.passphrasePlaceholder,
|
2022-02-10 18:58:07 +00:00
|
|
|
remember_duration_in_days: namedArgs.remember,
|
2022-02-10 08:22:32 +00:00
|
|
|
remember_me: namedArgs.rememberLabel,
|
|
|
|
salt: salt,
|
|
|
|
title: namedArgs.title,
|
2017-12-28 11:32:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
genFile(data);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fill the template with provided data and writes it to output file.
|
|
|
|
*
|
|
|
|
* @param data
|
|
|
|
*/
|
2022-02-10 08:22:32 +00:00
|
|
|
function genFile(data) {
|
|
|
|
let templateContents;
|
|
|
|
|
|
|
|
try {
|
2022-04-23 10:00:18 +00:00
|
|
|
templateContents = fs.readFileSync(namedArgs.f, 'utf8');
|
2022-02-10 08:22:32 +00:00
|
|
|
} catch (e) {
|
2017-12-14 22:38:34 +00:00
|
|
|
console.log("Failure: could not read template!");
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
const renderedTemplate = render(templateContents, data);
|
2017-12-14 22:38:34 +00:00
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
try {
|
2022-04-23 10:00:18 +00:00
|
|
|
fs.writeFileSync(data.output_file_path, renderedTemplate);
|
2022-02-10 08:22:32 +00:00
|
|
|
} catch (e) {
|
2017-12-14 22:38:34 +00:00
|
|
|
console.log("Failure: could not generate output file!");
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-28 11:32:27 +00:00
|
|
|
/**
|
|
|
|
* Replace the placeholder tags (between '{tag}') in 'tpl' string with provided data.
|
|
|
|
*
|
|
|
|
* @param tpl
|
|
|
|
* @param data
|
|
|
|
* @returns string
|
|
|
|
*/
|
2022-02-10 08:22:32 +00:00
|
|
|
function render(tpl, data) {
|
2017-12-14 22:38:34 +00:00
|
|
|
return tpl.replace(/{(.*?)}/g, function (_, key) {
|
2022-02-10 08:22:32 +00:00
|
|
|
if (data && data[key] !== undefined) {
|
|
|
|
return data[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
2017-12-14 22:38:34 +00:00
|
|
|
});
|
|
|
|
}
|