2017-12-15 21:37:02 +00:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
"use strict";
|
2017-12-14 22:38:34 +00:00
|
|
|
|
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");
|
2022-11-05 10:58:21 +00:00
|
|
|
const Yargs = require("yargs");
|
2017-12-14 22:38:34 +00:00
|
|
|
|
2022-11-05 10:58:21 +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) {
|
2022-11-05 10:58:21 +00:00
|
|
|
var iv = CryptoJS.lib.WordArray.random(128 / 8);
|
2019-06-28 14:23:13 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, {
|
|
|
|
iv: iv,
|
|
|
|
padding: CryptoJS.pad.Pkcs7,
|
|
|
|
mode: CryptoJS.mode.CBC,
|
|
|
|
});
|
2019-06-28 14:23:13 +00:00
|
|
|
|
2022-11-05 10:58:21 +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();
|
2022-02-10 08:22:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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-11-05 10:58:21 +00:00
|
|
|
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
|
|
|
|
keySize: 256 / 32,
|
|
|
|
iterations: 1000,
|
|
|
|
});
|
2022-02-10 08:22:32 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
return hashedPassphrase.toString();
|
2022-02-10 08:22:32 +00:00
|
|
|
}
|
|
|
|
|
2022-04-23 10:00:18 +00:00
|
|
|
function generateRandomSalt() {
|
2022-11-05 10:58:21 +00:00
|
|
|
return CryptoJS.lib.WordArray.random(128 / 8).toString();
|
2022-04-23 10:00:18 +00:00
|
|
|
}
|
|
|
|
|
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) {
|
2022-11-05 10:58:21 +00:00
|
|
|
function searchForOption(option) {
|
|
|
|
return process.argv.indexOf(option) > -1;
|
|
|
|
}
|
2022-02-27 18:37:17 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
if (searchForOption(`-${option}`) || searchForOption(`--${option}`)) {
|
|
|
|
return true;
|
|
|
|
}
|
2022-02-27 18:37:17 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
// Handle aliases for same option
|
|
|
|
for (let aliasIndex in yargs.parsed.aliases[option]) {
|
|
|
|
const alias = yargs.parsed.aliases[option][aliasIndex];
|
2022-02-27 18:37:17 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`))
|
|
|
|
return true;
|
|
|
|
}
|
2022-02-27 18:37:17 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
return false;
|
2022-02-27 18:37:17 +00:00
|
|
|
}
|
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
const yargs = Yargs.usage("Usage: staticrypt <filename> <passphrase> [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",
|
|
|
|
});
|
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
|
2022-11-05 10:58:21 +00:00
|
|
|
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) {
|
2022-11-05 10:58:21 +00:00
|
|
|
Yargs.showHelp();
|
|
|
|
process.exit(1);
|
2022-02-10 08:22:32 +00:00
|
|
|
}
|
|
|
|
|
2022-04-23 10:00:18 +00:00
|
|
|
// get config file
|
2022-11-05 10:58:21 +00:00
|
|
|
const isUsingconfigFile = namedArgs.config.toLowerCase() !== "false";
|
|
|
|
const configPath = "./" + namedArgs.config;
|
2022-04-23 10:00:18 +00:00
|
|
|
let config = {};
|
|
|
|
if (isUsingconfigFile && fs.existsSync(configPath)) {
|
2022-11-05 10:58:21 +00:00
|
|
|
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
2022-04-23 10:00:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the salt to use
|
|
|
|
*/
|
|
|
|
let salt;
|
|
|
|
// either a salt was provided by the user through the flag --salt
|
|
|
|
if (!!namedArgs.salt) {
|
2022-11-05 10:58:21 +00:00
|
|
|
salt = String(namedArgs.salt).toLowerCase();
|
2022-04-23 10:00:18 +00:00
|
|
|
}
|
|
|
|
// or we try to read the salt from config file
|
|
|
|
else if (!!config.salt) {
|
2022-11-05 10:58:21 +00:00
|
|
|
salt = config.salt;
|
2022-04-23 10:00:18 +00:00
|
|
|
}
|
|
|
|
// or we generate a salt
|
|
|
|
else {
|
2022-11-05 10:58:21 +00:00
|
|
|
salt = generateRandomSalt();
|
2022-04-23 10:00:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// validate the salt
|
|
|
|
if (salt.length !== 32 || /[^a-f0-9]/.test(salt)) {
|
2022-11-05 10:58:21 +00:00
|
|
|
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);
|
2022-04-23 10:00:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// write salt to config file
|
|
|
|
if (isUsingconfigFile && config.salt !== salt) {
|
2022-11-05 10:58:21 +00:00
|
|
|
config.salt = salt;
|
|
|
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
|
2022-04-23 10:00:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
// parse input
|
|
|
|
const input = namedArgs._[0].toString(),
|
2022-11-05 10:58:21 +00:00
|
|
|
passphrase = namedArgs._[1].toString();
|
2022-02-10 08:22:32 +00:00
|
|
|
|
|
|
|
// get the file content
|
|
|
|
let contents;
|
|
|
|
try {
|
2022-11-05 10:58:21 +00:00
|
|
|
contents = fs.readFileSync(input, "utf8");
|
2022-02-10 08:22:32 +00:00
|
|
|
} catch (e) {
|
2022-11-05 10:58:21 +00:00
|
|
|
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)
|
2022-11-05 10:58:21 +00:00
|
|
|
const hmac = CryptoJS.HmacSHA256(
|
|
|
|
encrypted,
|
|
|
|
CryptoJS.SHA256(hashedPassphrase).toString()
|
|
|
|
).toString();
|
2022-02-10 08:22:32 +00:00
|
|
|
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) {
|
2022-11-05 10:58:21 +00:00
|
|
|
try {
|
|
|
|
const embedContents = fs.readFileSync(
|
|
|
|
path.join(__dirname, "..", "kryptojs-3.1.9-1.min"),
|
|
|
|
"utf8"
|
|
|
|
);
|
|
|
|
|
|
|
|
cryptoTag = "<script>" + embedContents + "</script>";
|
|
|
|
} catch (e) {
|
|
|
|
console.log("Failure: embed file does not exist!");
|
|
|
|
process.exit(1);
|
|
|
|
}
|
2017-12-14 22:38:34 +00:00
|
|
|
}
|
|
|
|
|
2022-02-10 08:22:32 +00:00
|
|
|
const data = {
|
2022-11-05 10:58:21 +00:00
|
|
|
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,
|
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) {
|
2022-11-05 10:58:21 +00:00
|
|
|
let templateContents;
|
2022-02-10 08:22:32 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
try {
|
|
|
|
templateContents = fs.readFileSync(namedArgs.f, "utf8");
|
|
|
|
} catch (e) {
|
|
|
|
console.log("Failure: could not read template!");
|
|
|
|
process.exit(1);
|
|
|
|
}
|
2017-12-14 22:38:34 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
const renderedTemplate = render(templateContents, data);
|
2017-12-14 22:38:34 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
try {
|
|
|
|
fs.writeFileSync(data.output_file_path, renderedTemplate);
|
|
|
|
} catch (e) {
|
|
|
|
console.log("Failure: could not generate output file!");
|
|
|
|
process.exit(1);
|
|
|
|
}
|
2017-12-14 22:38:34 +00:00
|
|
|
}
|
|
|
|
|
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) {
|
2022-11-05 10:58:21 +00:00
|
|
|
return tpl.replace(/{(.*?)}/g, function (_, key) {
|
|
|
|
if (data && data[key] !== undefined) {
|
|
|
|
return data[key];
|
|
|
|
}
|
2022-02-10 08:22:32 +00:00
|
|
|
|
2022-11-05 10:58:21 +00:00
|
|
|
return "";
|
|
|
|
});
|
2017-12-14 22:38:34 +00:00
|
|
|
}
|