kopia lustrzana https://github.com/robinmoisson/staticrypt
raise PBKDF2 iterations in backward compatible way
rodzic
e85077f503
commit
a90d62ca0b
150
cli/helpers.js
150
cli/helpers.js
|
@ -3,8 +3,12 @@ const fs = require("fs");
|
|||
const cryptoEngine = require("../lib/cryptoEngine/cryptojsEngine");
|
||||
const path = require("path");
|
||||
const {renderTemplate} = require("../lib/formater.js");
|
||||
const Yargs = require("yargs");
|
||||
const { generateRandomSalt } = cryptoEngine;
|
||||
|
||||
const PASSWORD_TEMPLATE_DEFAULT_PATH = path.join(__dirname, "..", "lib", "password_template.html");
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
|
@ -132,21 +136,28 @@ function convertCommonJSToBrowserJS(modulePath) {
|
|||
}
|
||||
exports.convertCommonJSToBrowserJS = convertCommonJSToBrowserJS;
|
||||
|
||||
/**
|
||||
* @param {string} filePath
|
||||
* @param {string} errorName
|
||||
* @returns {string}
|
||||
*/
|
||||
function readFile(filePath, errorName = file) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
} catch (e) {
|
||||
exitEarly(`Failure: could not read ${errorName}!`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the template with provided data and writes it to output file.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {string} outputFilePath
|
||||
* @param {string} inputFilePath
|
||||
* @param {string} templateFilePath
|
||||
*/
|
||||
function genFile(data, outputFilePath, inputFilePath) {
|
||||
let templateContents;
|
||||
|
||||
try {
|
||||
templateContents = fs.readFileSync(inputFilePath, "utf8");
|
||||
} catch (e) {
|
||||
exitEarly("Failure: could not read template!");
|
||||
}
|
||||
function genFile(data, outputFilePath, templateFilePath) {
|
||||
const templateContents = readFile(templateFilePath, "template");
|
||||
|
||||
const renderedTemplate = renderTemplate(templateContents, data);
|
||||
|
||||
|
@ -156,4 +167,123 @@ function genFile(data, outputFilePath, inputFilePath) {
|
|||
exitEarly("Failure: could not generate output file!");
|
||||
}
|
||||
}
|
||||
exports.genFile = genFile;
|
||||
exports.genFile = genFile;
|
||||
|
||||
/**
|
||||
* TODO: remove in next major version
|
||||
*
|
||||
* This method checks whether the password template support the security fix increasing PBKDF2 iterations. Users using
|
||||
* an old custom password_template might have logic that doesn't benefit from the fix.
|
||||
*
|
||||
* @param {string} templatePathParameter
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isCustomPasswordTemplateLegacy(templatePathParameter) {
|
||||
// if the user uses the default template, it's up to date
|
||||
if (templatePathParameter === PASSWORD_TEMPLATE_DEFAULT_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const customTemplateContent = readFile(templatePathParameter, "template");
|
||||
|
||||
// if the template injects the crypto engine, it's up to date
|
||||
if (customTemplateContent.includes("js_crypto_engine")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
exports.isCustomPasswordTemplateLegacy = isCustomPasswordTemplateLegacy;
|
||||
|
||||
function parseCommandLineArguments() {
|
||||
return 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: PASSWORD_TEMPLATE_DEFAULT_PATH,
|
||||
})
|
||||
.option("i", {
|
||||
alias: "instructions",
|
||||
type: "string",
|
||||
describe: "Special instructions to display to the user.",
|
||||
default: "",
|
||||
})
|
||||
.option("label-error", {
|
||||
type: "string",
|
||||
describe: "Error message to display on entering wrong passphrase.",
|
||||
default: "Bad password!",
|
||||
})
|
||||
.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 the generated encrypted file.",
|
||||
default: null,
|
||||
})
|
||||
.option("passphrase-placeholder", {
|
||||
type: "string",
|
||||
describe: "Placeholder to use for the passphrase input.",
|
||||
default: "Password",
|
||||
})
|
||||
.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 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 to 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",
|
||||
})
|
||||
// do not give a default option to this parameter - we want to see when the flag is included with no
|
||||
// value and when it's not included at all
|
||||
.option("share", {
|
||||
describe:
|
||||
'Get a link containing your hashed password that will auto-decrypt the page. Pass your URL as a value to append '
|
||||
+ '"?staticrypt_pwd=<hashed_pwd>", or leave empty to display the hash to append.',
|
||||
type: "string",
|
||||
})
|
||||
.option("t", {
|
||||
alias: "title",
|
||||
type: "string",
|
||||
describe: "Title for the output HTML page.",
|
||||
default: "Protected Page",
|
||||
});
|
||||
}
|
||||
exports.parseCommandLineArguments = parseCommandLineArguments;
|
||||
|
|
118
cli/index.js
118
cli/index.js
|
@ -12,6 +12,7 @@ require('dotenv').config();
|
|||
const cryptoEngine = require("../lib/cryptoEngine/cryptojsEngine");
|
||||
const codec = require("../lib/codec");
|
||||
const { convertCommonJSToBrowserJS, exitEarly, isOptionSetByUser, genFile, getPassword, getFileContent, getSalt} = require("./helpers");
|
||||
const { isCustomPasswordTemplateLegacy, parseCommandLineArguments} = require("./helpers.js");
|
||||
const { generateRandomSalt } = cryptoEngine;
|
||||
const { encode } = codec.init(cryptoEngine);
|
||||
|
||||
|
@ -22,95 +23,8 @@ const SCRIPT_TAG =
|
|||
SCRIPT_URL +
|
||||
'" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></script>';
|
||||
|
||||
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("label-error", {
|
||||
type: "string",
|
||||
describe: "Error message to display on entering wrong passphrase.",
|
||||
default: "Bad password!",
|
||||
})
|
||||
.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 the generated encrypted file.",
|
||||
default: null,
|
||||
})
|
||||
.option("passphrase-placeholder", {
|
||||
type: "string",
|
||||
describe: "Placeholder to use for the passphrase input.",
|
||||
default: "Password",
|
||||
})
|
||||
.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 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 to 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",
|
||||
})
|
||||
// do not give a default option to this parameter - we want to see when the flag is included with no
|
||||
// value and when it's not included at all
|
||||
.option("share", {
|
||||
describe:
|
||||
'Get a link containing your hashed password that will auto-decrypt the page. Pass your URL as a value to append '
|
||||
+ '"?staticrypt_pwd=<hashed_pwd>", or leave empty to display the hash to append.',
|
||||
type: "string",
|
||||
})
|
||||
.option("t", {
|
||||
alias: "title",
|
||||
type: "string",
|
||||
describe: "Title for the output HTML page.",
|
||||
default: "Protected Page",
|
||||
});
|
||||
// parse arguments
|
||||
const yargs = parseCommandLineArguments();
|
||||
const namedArgs = yargs.argv;
|
||||
|
||||
// if the 's' flag is passed without parameter, generate a salt, display & exit
|
||||
|
@ -122,7 +36,7 @@ if (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
|
|||
// validate the number of arguments
|
||||
const positionalArguments = namedArgs._;
|
||||
if (positionalArguments.length > 2 || positionalArguments.length === 0) {
|
||||
Yargs.showHelp();
|
||||
yargs.showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -166,8 +80,22 @@ if (isOptionSetByUser("share", yargs)) {
|
|||
// get the file content
|
||||
const contents = getFileContent(inputFilepath);
|
||||
|
||||
// TODO: remove in the next major version bump. This is to allow a security update to some versions without breaking
|
||||
// older ones. If the password template is custom AND created before 2.2.0 we need to use the old hashing algorithm.
|
||||
const isLegacy = isCustomPasswordTemplateLegacy(namedArgs.f);
|
||||
|
||||
if (isLegacy) {
|
||||
console.log(
|
||||
"#################################\n\n" +
|
||||
"[StatiCrypt] SECURITY WARNING: You are using an old version of the password template, which has been found to " +
|
||||
"be less secure. Please update your custom password_template logic to match version 2.2.0 or higher." +
|
||||
"\nYou can find the template here: https://github.com/robinmoisson/staticrypt/blob/main/lib/password_template.html" +
|
||||
"\n\n#################################"
|
||||
);
|
||||
}
|
||||
|
||||
// encrypt input
|
||||
const encryptedMessage = encode(contents, password, salt);
|
||||
const encryptedMessage = encode(contents, password, salt, isLegacy);
|
||||
|
||||
// create crypto-js tag (embedded or not)
|
||||
let cryptoTag = SCRIPT_TAG;
|
||||
|
@ -191,7 +119,9 @@ const data = {
|
|||
encrypted: encryptedMessage,
|
||||
instructions: namedArgs.instructions,
|
||||
is_remember_enabled: namedArgs.noremember ? "false" : "true",
|
||||
js_codec: convertCommonJSToBrowserJS("lib/codec"),
|
||||
// TODO: remove on next major version bump. This is a hack to pass the salt to the injected js_codec in a backward
|
||||
// compatible way (not requiring to update the password_template).
|
||||
js_codec: convertCommonJSToBrowserJS("lib/codec").replace('##SALT##', salt),
|
||||
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine/cryptojsEngine"),
|
||||
label_error: namedArgs.labelError,
|
||||
passphrase_placeholder: namedArgs.passphrasePlaceholder,
|
||||
|
@ -201,8 +131,8 @@ const data = {
|
|||
title: namedArgs.title,
|
||||
};
|
||||
|
||||
const outputFilePath = namedArgs.output !== null
|
||||
const outputFilepath = namedArgs.output !== null
|
||||
? namedArgs.output
|
||||
: inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
|
||||
|
||||
genFile(data, outputFilePath, namedArgs.f);
|
||||
genFile(data, outputFilepath, namedArgs.f);
|
||||
|
|
29
lib/codec.js
29
lib/codec.js
|
@ -4,7 +4,12 @@
|
|||
* @param cryptoEngine - the engine to use for encryption / decryption
|
||||
*/
|
||||
function init(cryptoEngine) {
|
||||
// TODO: remove on next major version bump. This is a hack to make the salt available in all functions here in a
|
||||
// backward compatible way (not requiring to change the password_template).
|
||||
const backwardCompatibleSalt = '##SALT##';
|
||||
|
||||
const exports = {};
|
||||
|
||||
/**
|
||||
* Top-level function for encoding a message.
|
||||
* Includes passphrase hashing, encryption, and signing.
|
||||
|
@ -12,11 +17,15 @@ function init(cryptoEngine) {
|
|||
* @param {string} msg
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @param {boolean} isLegacy - whether to use the legacy hashing algorithm (1k iterations) or not
|
||||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
function encode(msg, passphrase, salt) {
|
||||
const hashedPassphrase = cryptoEngine.hashPassphrase(passphrase, salt);
|
||||
function encode(msg, passphrase, salt, isLegacy = false) {
|
||||
// TODO: remove in the next major version bump. This is to not break backwards compatibility with the old way of hashing
|
||||
const hashedPassphrase = isLegacy
|
||||
? cryptoEngine.hashLegacyRound(passphrase, salt)
|
||||
: cryptoEngine.hashPassphrase(passphrase, salt);
|
||||
const encrypted = cryptoEngine.encrypt(msg, 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)
|
||||
|
@ -28,19 +37,31 @@ function init(cryptoEngine) {
|
|||
|
||||
/**
|
||||
* Top-level function for decoding a message.
|
||||
* Includes signature check, an decryption.
|
||||
* Includes signature check and decryption.
|
||||
*
|
||||
* @param {string} signedMsg
|
||||
* @param {string} hashedPassphrase
|
||||
* @param {boolean} shouldTryBackwardCompatible
|
||||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
function decode(signedMsg, hashedPassphrase) {
|
||||
function decode(signedMsg, hashedPassphrase, shouldTryBackwardCompatible = true) {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
|
||||
|
||||
if (decryptedHMAC !== encryptedHMAC) {
|
||||
// TODO: remove in next major version bump. This is to not break backwards compatibility with the old 1k
|
||||
// iterations in PBKDF2 - if the key we try isn't working, it might be because it's a remember-me/autodecrypt
|
||||
// link key, generated with 1k iterations. Try again with the updated iteration count.
|
||||
if (shouldTryBackwardCompatible) {
|
||||
return decode(
|
||||
signedMsg,
|
||||
cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return { success: false, message: "Signature mismatch" };
|
||||
}
|
||||
return {
|
||||
|
|
|
@ -47,15 +47,48 @@ exports.decrypt = decrypt;
|
|||
* @returns string
|
||||
*/
|
||||
function hashPassphrase(passphrase, salt) {
|
||||
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
|
||||
keySize: 256 / 32,
|
||||
iterations: 1000,
|
||||
});
|
||||
// we hash the passphrase in two steps: first 1k iterations, then we add iterations. This is because we used to use 1k,
|
||||
// so for backwards compatibility with remember-me/autodecrypt links, we need to support going from that to more
|
||||
// iterations
|
||||
var hashedPassphrase = hashLegacyRound(passphrase, salt);
|
||||
|
||||
return hashedPassphrase.toString();
|
||||
return hashSecondRound(hashedPassphrase, salt);
|
||||
}
|
||||
exports.hashPassphrase = hashPassphrase;
|
||||
|
||||
/**
|
||||
* This hashes the passphrase with 1k iterations. This is a low number, we need this function to support backwards
|
||||
* compatibility.
|
||||
*
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @returns {string}
|
||||
*/
|
||||
function hashLegacyRound(passphrase, salt) {
|
||||
return CryptoJS.PBKDF2(passphrase, salt, {
|
||||
keySize: 256 / 32,
|
||||
iterations: 1000,
|
||||
}).toString();
|
||||
}
|
||||
exports.hashLegacyRound = hashLegacyRound;
|
||||
|
||||
/**
|
||||
* Add a second round of iterations. This is because we used to use 1k, so for backwards compatibility with
|
||||
* remember-me/autodecrypt links, we need to support going from that to more iterations.
|
||||
*
|
||||
* @param hashedPassphrase
|
||||
* @param salt
|
||||
* @returns {string}
|
||||
*/
|
||||
function hashSecondRound(hashedPassphrase, salt) {
|
||||
return CryptoJS.PBKDF2(hashedPassphrase, salt, {
|
||||
keySize: 256 / 32,
|
||||
iterations: 50000,
|
||||
hasher: CryptoJS.algo.SHA256,
|
||||
}).toString();
|
||||
}
|
||||
exports.hashSecondRound = hashSecondRound;
|
||||
|
||||
function generateRandomSalt() {
|
||||
return CryptoJS.lib.WordArray.random(128 / 8).toString();
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue