From 92691a1994b16f1e83dfbfb1f6b1e31ead3806d3 Mon Sep 17 00:00:00 2001
From: robinmoisson
Date: Wed, 29 Mar 2023 16:12:44 +0200
Subject: [PATCH 01/11] make WebCrypto the only engine available (closes #168)
---
cli/helpers.js | 47 +-----
cli/index.js | 124 ++++------------
example/example_encrypted.html | 4 +-
index.html | 11 +-
lib/codec-sync.js | 95 ------------
.../webcryptoEngine.js => cryptoEngine.js} | 2 -
lib/cryptoEngine/cryptojsEngine.js | 138 ------------------
lib/kryptojs-3.1.9-1.min.js | 7 -
package.json | 2 +-
scripts/buildIndex.js | 2 +-
scripts/index_template.html | 9 +-
11 files changed, 34 insertions(+), 407 deletions(-)
delete mode 100644 lib/codec-sync.js
rename lib/{cryptoEngine/webcryptoEngine.js => cryptoEngine.js} (99%)
delete mode 100644 lib/cryptoEngine/cryptojsEngine.js
delete mode 100644 lib/kryptojs-3.1.9-1.min.js
diff --git a/cli/helpers.js b/cli/helpers.js
index 89b1662..2b10a41 100644
--- a/cli/helpers.js
+++ b/cli/helpers.js
@@ -1,6 +1,6 @@
const fs = require("fs");
-const { generateRandomSalt } = require("../lib/cryptoEngine/webcryptoEngine.js");
+const { generateRandomSalt } = require("../lib/cryptoEngine.js");
const path = require("path");
const {renderTemplate} = require("../lib/formater.js");
const Yargs = require("yargs");
@@ -168,39 +168,6 @@ function genFile(data, outputFilePath, templateFilePath) {
}
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) {
- const customTemplateContent = readFile(templatePathParameter, "template");
-
- // if the template injects the crypto engine, it's up to date
- return !customTemplateContent.includes("js_crypto_engine");
-}
-exports.isCustomPasswordTemplateLegacy = isCustomPasswordTemplateLegacy;
-
-/**
- * TODO: remove in next major version
- *
- * This method checks whether the password template support the async logic.
- *
- * @param {string} templatePathParameter
- * @returns {boolean}
- */
-function isPasswordTemplateUsingAsync(templatePathParameter) {
- const customTemplateContent = readFile(templatePathParameter, "template");
-
- // if the template includes this comment, it's up to date
- return customTemplateContent.includes("// STATICRYPT_VERSION: async");
-}
-exports.isPasswordTemplateUsingAsync = isPasswordTemplateUsingAsync;
-
/**
* @param {string} templatePathParameter
* @returns {boolean}
@@ -224,18 +191,6 @@ function parseCommandLineArguments() {
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("engine", {
- type: "string",
- describe: "The crypto engine to use. WebCrypto uses 600k iterations and is more secure, CryptoJS 15k.\n" +
- "Possible values: 'cryptojs', 'webcrypto'.",
- default: "cryptojs",
- })
.option("f", {
alias: "file-template",
type: "string",
diff --git a/cli/index.js b/cli/index.js
index 5f35bd0..78244f8 100755
--- a/cli/index.js
+++ b/cli/index.js
@@ -3,31 +3,21 @@
"use strict";
const fs = require("fs");
-const path = require("path");
// parse .env file into process.env
require('dotenv').config();
-const cryptojsEngine = require("../lib/cryptoEngine/cryptojsEngine");
-const webcryptoEngine = require("../lib/cryptoEngine/webcryptoEngine");
+const cryptoEngine = require("../lib/cryptoEngine.js");
const codec = require("../lib/codec.js");
+const { generateRandomSalt, generateRandomString } = cryptoEngine;
+const { encode } = codec.init(cryptoEngine);
const { convertCommonJSToBrowserJS, exitWithError, isOptionSetByUser, genFile, getPassword, getFileContent, getSalt} = require("./helpers");
-const { isCustomPasswordTemplateLegacy, parseCommandLineArguments, isPasswordTemplateUsingAsync} = require("./helpers.js");
-
-const CRYPTOJS_SCRIPT_TAG =
- '';
+const { parseCommandLineArguments} = require("./helpers.js");
// parse arguments
const yargs = parseCommandLineArguments();
const namedArgs = yargs.argv;
-// set the crypto engine
-const isWebcrypto = namedArgs.engine === "webcrypto";
-const cryptoEngine = isWebcrypto ? webcryptoEngine : cryptojsEngine;
-const { generateRandomSalt, generateRandomString } = cryptoEngine;
-const { encode } = codec.init(cryptoEngine);
-
async function runStatiCrypt() {
// if the 's' flag is passed without parameter, generate a salt, display & exit
if (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
@@ -91,94 +81,32 @@ async function runStatiCrypt() {
console.log(url + "#staticrypt_pwd=" + hashedPassword);
}
- // 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" +
- "SECURITY WARNING [StatiCrypt]: 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 the latest version." +
- "\nYou can find instructions here: https://github.com/robinmoisson/staticrypt/issues/161" +
- "\n\n#################################"
- );
- }
-
- if (!isWebcrypto) {
- console.log(
- "WARNING: If you are viewing the file over HTTPS or locally, we recommend " +
- (isPasswordTemplateUsingAsync(namedArgs.f) ? "" : "updating your password template to the latest version and ") +
- "using the '--engine webcrypto' more secure engine. It will become the default in StatiCrypt next major version."
- );
- } else if (!isPasswordTemplateUsingAsync(namedArgs.f) && isWebcrypto) {
- exitWithError(
- "The '--engine webcrypto' engine is only available for password templates that use async/await. Please " +
- "update your password template to the latest version or use the '--engine cryptojs' engine."
- )
- }
-
- // create crypto-js tag (embedded or not)
- let cryptoTag = CRYPTOJS_SCRIPT_TAG;
- if (isWebcrypto) {
- cryptoTag = "";
- } else if (namedArgs.embed) {
- try {
- const embedContents = fs.readFileSync(
- path.join(__dirname, "..", "lib", "kryptojs-3.1.9-1.min.js"),
- "utf8"
- );
-
- cryptoTag = "";
- } catch (e) {
- exitWithError("Embed file does not exist.");
- }
- }
-
- const cryptoEngineString = isWebcrypto
- ? convertCommonJSToBrowserJS("lib/cryptoEngine/webcryptoEngine")
- : convertCommonJSToBrowserJS("lib/cryptoEngine/cryptojsEngine");
-
// get the file content
const contents = getFileContent(inputFilepath);
// encrypt input
- encode(contents, password, salt, isLegacy).then((encryptedMessage) => {
- let codecString;
- if (isWebcrypto) {
- codecString = convertCommonJSToBrowserJS("lib/codec");
- } else {
- // TODO: remove on next major version bump. The replace is a hack to pass the salt to the injected js_codec in
- // a backward compatible way (not requiring to update the password_template). Same for using a "sync" version
- // of the codec.
- codecString = convertCommonJSToBrowserJS("lib/codec-sync").replace('##SALT##', salt);
- }
-
- const data = {
- crypto_tag: cryptoTag,
- decrypt_button: namedArgs.decryptButton,
- // TODO: deprecated option here for backward compat, remove on next major version bump
- embed: isWebcrypto ? false : namedArgs.embed,
- encrypted: encryptedMessage,
- instructions: namedArgs.instructions,
- is_remember_enabled: namedArgs.noremember ? "false" : "true",
- js_codec: codecString,
- js_crypto_engine: cryptoEngineString,
- label_error: namedArgs.labelError,
- passphrase_placeholder: namedArgs.passphrasePlaceholder,
- remember_duration_in_days: namedArgs.remember,
- remember_me: namedArgs.rememberLabel,
- salt: salt,
- title: namedArgs.title,
- };
-
- const outputFilepath = namedArgs.output !== null
- ? namedArgs.output
- : inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
-
- genFile(data, outputFilepath, namedArgs.f);
-
- });
+ const encryptedMessage = await encode(contents, password, salt);
+
+ const data = {
+ decrypt_button: namedArgs.decryptButton,
+ encrypted: encryptedMessage,
+ instructions: namedArgs.instructions,
+ is_remember_enabled: namedArgs.noremember ? "false" : "true",
+ js_codec: convertCommonJSToBrowserJS("lib/codec"),
+ js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
+ label_error: namedArgs.labelError,
+ passphrase_placeholder: namedArgs.passphrasePlaceholder,
+ remember_duration_in_days: namedArgs.remember,
+ remember_me: namedArgs.rememberLabel,
+ salt: salt,
+ title: namedArgs.title,
+ };
+
+ const outputFilepath = namedArgs.output !== null
+ ? namedArgs.output
+ : inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
+
+ genFile(data, outputFilepath, namedArgs.f);
}
runStatiCrypt();
diff --git a/example/example_encrypted.html b/example/example_encrypted.html
index 6033bfb..e500d97 100644
--- a/example/example_encrypted.html
+++ b/example/example_encrypted.html
@@ -201,8 +201,6 @@
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
- *
- * Mirrors the API of CryptoJS.enc.Hex
*/
const HexEncoder = {
/**
@@ -564,7 +562,7 @@
const decode = codec.init(cryptoEngine).decode;
// variables to be filled when generating the file
- const encryptedMsg = 'df7428ed075decd3c4ff9d8ab6d2bea4410854c863d705789fb22e14b7da7ee20e94c593047abb9ba34d6519eebc879d2097bb918c0af0d4e248959849fb9c6bbb93aba054806c8773d1e4b63ec317185ad5462a9919dda986716c67bb57a89a044de3e25707cded482657c4a0208e9916aaa9d839f090eaaeb95603e05db11fe4bc37c4d98b9170124ce1c7ca18fe39c2f179e23eee61ba7d79cb3145e8833936c62adeffce1f5e129745c89541faa8100bfde4733bfa9c0ecf04768b3d1889',
+ const encryptedMsg = '2c0a13159934226fa022225a06c64af6dfffe80dd6cb908f0f6ad93311563d78150cfc5465e3c7d70d682194a6d4a0c6082e37aaca8dbd83036d9e9cf629a112132b8fb6004a028b31ea4fb5a9f82d505096fe59e109970261733b4c8f21110b6f365d8b087d0ec15866917341e3cd105c65c9c7542626bae08903cd10675ed7fbd71062a1e87e35d30341c8251ab452352eaeecd6e44ed0a256979b30287032bccefaecf1685e948bf0fc3a3b5ad1f7b70092d9e32ceb60818ee49e821f8933',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
labelError = 'Bad password!',
isRememberEnabled = true,
diff --git a/index.html b/index.html
index 6fedba7..23104b5 100644
--- a/index.html
+++ b/index.html
@@ -184,11 +184,6 @@ Encrypted HTML
-
-
-
-
diff --git a/index.html b/index.html
index 23104b5..851343d 100644
--- a/index.html
+++ b/index.html
@@ -46,7 +46,7 @@
Download your encrypted string in a HTML page with a password prompt you can upload anywhere (see example).
+ target="_blank" href="example/encrypted/example.html">example).
The tool is also available as a CLI on NPM and is
@@ -565,7 +565,7 @@ Encrypted HTML
window.formater = ((function(){
const exports = {};
/**
- * Replace the placeholder tags (between '{tag}') in the template string with provided data.
+ * Replace the placeholder tags (between '/*[|tag|]* /0') in the template string with provided data.
*
* @param {string} templateString
* @param {Object} data
@@ -573,12 +573,16 @@ Encrypted HTML
* @returns string
*/
function renderTemplate(templateString, data) {
- return templateString.replace(/{\s*(\w+)\s*}/g, function (_, key) {
- if (data && data[key] !== undefined) {
- return data[key];
+ return templateString.replace(/\/\*\[\|\s*(\w+)\s*\|]\*\/0/g, function (_, key) {
+ if (!data || data[key] === undefined) {
+ return key;
}
- return "";
+ if (typeof data[key] === 'object') {
+ return JSON.stringify(data[key]);
+ }
+
+ return data[key];
});
}
exports.renderTemplate = renderTemplate;
@@ -588,19 +592,608 @@ Encrypted HTML
})())
+
+
diff --git a/lib/staticryptJs.js b/lib/staticryptJs.js
new file mode 100644
index 0000000..55d5d4c
--- /dev/null
+++ b/lib/staticryptJs.js
@@ -0,0 +1,215 @@
+const cryptoEngine = /*[|js_crypto_engine|]*/0
+const codec = /*[|js_codec|]*/0
+const decode = codec.init(cryptoEngine).decode;
+
+
+/**
+ * Initialize the staticrypt module, that exposes functions callbable by the password_template.
+ *
+ * @param {{
+ * encryptedMsg: string,
+ * isRememberEnabled: boolean,
+ * rememberDurationInDays: number,
+ * salt: string,
+ * }} staticryptConfig - object of data that is stored on the password_template at encryption time.
+ *
+ * @param {{
+ * rememberExpirationKey: string,
+ * rememberPassphraseKey: string,
+ * replaceHtmlCallback: function,
+ * clearLocalStorageCallback: function,
+ * }} templateConfig - object of data that can be configured by a custom password_template.
+ */
+function init(staticryptConfig, templateConfig) {
+ const exports = {};
+
+ /**
+ * Decrypt our encrypted page, replace the whole HTML.
+ *
+ * @param {string} hashedPassphrase
+ * @returns {Promise}
+ */
+ async function decryptAndReplaceHtml(hashedPassphrase) {
+ const { encryptedMsg, salt } = staticryptConfig;
+ const { replaceHtmlCallback } = templateConfig;
+
+ const result = await decode(encryptedMsg, hashedPassphrase, salt);
+ if (!result.success) {
+ return false;
+ }
+ const plainHTML = result.decoded;
+
+ // if the user configured a callback call it, otherwise just replace the whole HTML
+ if (typeof replaceHtmlCallback === 'function') {
+ replaceHtmlCallback(plainHTML);
+ } else {
+ document.write(plainHTML);
+ document.close();
+ }
+
+ return true;
+ }
+
+ /**
+ * Attempt to decrypt the page and replace the whole HTML.
+ *
+ * @param {string} password
+ * @param {boolean} isRememberChecked
+ *
+ * @returns {Promise<{isSuccessful: boolean, hashedPassword?: string}>} - we return an object, so that if we want to
+ * expose more information in the future we can do it without breaking the password_template
+ */
+ async function handleDecryptionOfPage(password, isRememberChecked) {
+ const { isRememberEnabled, rememberDurationInDays, salt } = staticryptConfig;
+ const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
+
+ // decrypt and replace the whole page
+ const hashedPassword = await cryptoEngine.hashPassphrase(password, salt);
+
+ const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassword);
+
+ if (!isDecryptionSuccessful) {
+ return {
+ isSuccessful: false,
+ hashedPassword,
+ };
+ }
+
+ // remember the hashedPassword and set its expiration if necessary
+ if (isRememberEnabled && isRememberChecked) {
+ window.localStorage.setItem(rememberPassphraseKey, hashedPassword);
+
+ // set the expiration if the duration isn't 0 (meaning no expiration)
+ if (rememberDurationInDays > 0) {
+ window.localStorage.setItem(
+ rememberExpirationKey,
+ (new Date().getTime() + rememberDurationInDays * 24 * 60 * 60 * 1000).toString()
+ );
+ }
+ }
+
+ return {
+ isSuccessful: true,
+ hashedPassword,
+ };
+ }
+ exports.handleDecryptionOfPage = handleDecryptionOfPage;
+
+ /**
+ * Clear localstorage from staticrypt related values
+ */
+ function clearLocalStorage() {
+ const { clearLocalStorageCallback, rememberExpirationKey, rememberPassphraseKey } = templateConfig;
+
+ if (typeof clearLocalStorageCallback === 'function') {
+ clearLocalStorageCallback();
+ } else {
+ localStorage.removeItem(rememberPassphraseKey);
+ localStorage.removeItem(rememberExpirationKey);
+ }
+ }
+
+ async function handleDecryptOnLoad() {
+ let isSuccessful = await decryptOnLoadFromUrl();
+
+ if (!isSuccessful) {
+ isSuccessful = await decryptOnLoadFromRememberMe();
+ }
+
+ return { isSuccessful };
+ }
+ exports.handleDecryptOnLoad = handleDecryptOnLoad;
+
+ /**
+ * Clear storage if we are logging out
+ *
+ * @returns {boolean} - whether we logged out
+ */
+ function logoutIfNeeded() {
+ const logoutKey = "staticrypt_logout";
+
+ // handle logout through query param
+ const queryParams = new URLSearchParams(window.location.search);
+ if (queryParams.has(logoutKey)) {
+ clearLocalStorage();
+ return true;
+ }
+
+ // handle logout through URL fragment
+ const hash = window.location.hash.substring(1);
+ if (hash.includes(logoutKey)) {
+ clearLocalStorage();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * To be called on load: check if we want to try to decrypt and replace the HTML with the decrypted content, and
+ * try to do it if needed.
+ *
+ * @returns {Promise} true if we derypted and replaced the whole page, false otherwise
+ */
+ async function decryptOnLoadFromRememberMe() {
+ const { rememberDurationInDays } = staticryptConfig;
+ const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
+
+ // if we are login out, terminate
+ if (logoutIfNeeded()) {
+ return false;
+ }
+
+ // if there is expiration configured, check if we're not beyond the expiration
+ if (rememberDurationInDays && rememberDurationInDays > 0) {
+ const expiration = localStorage.getItem(rememberExpirationKey),
+ isExpired = expiration && new Date().getTime() > parseInt(expiration);
+
+ if (isExpired) {
+ clearLocalStorage();
+ return false;
+ }
+ }
+
+ const hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
+
+ if (hashedPassphrase) {
+ // try to decrypt
+ const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
+
+ // if the decryption is unsuccessful the password might be wrong - silently clear the saved data and let
+ // the user fill the password form again
+ if (!isDecryptionSuccessful) {
+ clearLocalStorage();
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ function decryptOnLoadFromUrl() {
+ const passwordKey = "staticrypt_pwd";
+
+ // get the password from the query param
+ const queryParams = new URLSearchParams(window.location.search);
+ const hashedPassphraseQuery = queryParams.get(passwordKey);
+
+ // get the password from the url fragment
+ const hashRegexMatch = window.location.hash.substring(1).match(new RegExp(passwordKey + "=(.*)"));
+ const hashedPassphraseFragment = hashRegexMatch ? hashRegexMatch[1] : null;
+
+ const hashedPassphrase = hashedPassphraseFragment || hashedPassphraseQuery;
+
+ if (hashedPassphrase) {
+ return decryptAndReplaceHtml(hashedPassphrase);
+ }
+
+ return false;
+ }
+
+ return exports;
+}
+exports.init = init;
\ No newline at end of file
diff --git a/scripts/build.sh b/scripts/build.sh
index 1b27e27..16c7f33 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -1,12 +1,15 @@
# Build the website files
# Should be run with "npm run build" - npm handles the pathing better (so no "#!/usr/bin/env" bash on top)
+# build the index.html file
+node ./scripts/buildIndex.js
+
# encrypt the example file
-node cli/index.js example/example.html test \
- --engine webcrypto \
+cd example
+node ../cli/index.js example.html \
+ -p test \
--short \
--salt b93bbaf35459951c47721d1f3eaeb5b9 \
- --instructions "Enter \"test\" to unlock the page"
+ --config false \
+ --template-instructions "Enter \"test\" to unlock the page"
-# build the index.html file
-node ./scripts/buildIndex.js
diff --git a/scripts/buildIndex.js b/scripts/buildIndex.js
index 02dd000..3309f02 100644
--- a/scripts/buildIndex.js
+++ b/scripts/buildIndex.js
@@ -1,9 +1,10 @@
-const { convertCommonJSToBrowserJS, genFile } = require("../cli/helpers.js");
+const { convertCommonJSToBrowserJS, genFile, buildStaticryptJS} = require("../cli/helpers.js");
const data = {
js_codec: convertCommonJSToBrowserJS("lib/codec"),
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
js_formater: convertCommonJSToBrowserJS("lib/formater"),
+ js_staticrypt: buildStaticryptJS(),
};
genFile(data, "./index.html", "./scripts/index_template.html");
diff --git a/scripts/index_template.html b/scripts/index_template.html
index 63fc232..3af59e0 100644
--- a/scripts/index_template.html
+++ b/scripts/index_template.html
@@ -46,7 +46,7 @@
Download your encrypted string in a HTML page with a password prompt you can upload anywhere (see example).
+ target="_blank" href="example/encrypted/example.html">example).
The tool is also available as a CLI on NPM and is
@@ -187,22 +187,26 @@ Encrypted HTML
+
+