From 059701ce891958433b82b6934442094c13618841 Mon Sep 17 00:00:00 2001
From: robinmoisson
Date: Thu, 30 Mar 2023 12:03:18 +0200
Subject: [PATCH] refactor password template to make it simpler (closes #167)
---
.gitignore | 2 +
README.md | 94 ++-
cli/helpers.js | 22 +-
cli/index.js | 33 +-
.../example.html} | 195 +++--
index.html | 670 +++++++++++++++++-
lib/formater.js | 18 +-
lib/password_template.html | 205 +-----
lib/staticryptJs.js | 215 ++++++
scripts/build.sh | 17 +-
scripts/buildIndex.js | 3 +-
scripts/index_template.html | 71 +-
12 files changed, 1168 insertions(+), 377 deletions(-)
rename example/{example_encrypted.html => encrypted/example.html} (78%)
create mode 100644 lib/staticryptJs.js
diff --git a/.gitignore b/.gitignore
index 5100467..a10bedf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@
node_modules
.staticrypt.json
.env
+encrypted/
+!example/encrypted/
diff --git a/README.md b/README.md
index 20140d0..1e3bd10 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# StatiCrypt
-StatiCrypt uses AES-256 to encrypt your HTML file with your long password and return a static page including a password prompt and the javascript decryption logic that you can safely upload anywhere (see [what the page looks like](https://robinmoisson.github.io/staticrypt/example/example_encrypted.html)).
+StatiCrypt uses AES-256 to encrypt your HTML file with your long password and return a static page including a password prompt and the javascript decryption logic that you can safely upload anywhere (see [what the page looks like](https://robinmoisson.github.io/staticrypt/example/encrypted/example.html)).
This means you can **password protect the content of your _public_ static HTML file, without any back-end** - serving it over Netlify, GitHub pages, etc. (see the detail of [how it works](#how-staticrypt-works)).
@@ -63,55 +63,51 @@ find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {}
The password argument is optional if `STATICRYPT_PASSWORD` is set in the environment or `.env` file.
- Usage: staticrypt [] [options]
+ Usage: staticrypt [options]
Options:
- --help Show help [boolean]
- --version Show version number [boolean]
- -c, --config Path to the config file. Set to "false" to
- disable. [string] [default: ".staticrypt.json"]
- --decrypt-button Label to use for the decrypt button. Default:
- "DECRYPT". [string] [default: "DECRYPT"]
- -e, --embed Whether or not to embed crypto-js in the page
- (or use an external CDN).
- [boolean] [default: true]
- --engine The crypto engine to use. WebCrypto uses 600k
- iterations and is more secure, CryptoJS 15k.
- Possible values: 'cryptojs', 'webcrypto'.
- [string] [default: "cryptojs"]
- -f, --file-template Path to custom HTML template with password
- prompt.
- [string] [default: "/code/staticrypt/lib/password_template.html"]
- -i, --instructions Special instructions to display to the user.
- [string] [default: ""]
- --label-error Error message to display on entering wrong
- password. [string] [default: "Bad password!"]
- --noremember Set this flag to remove the "Remember me"
- checkbox. [boolean] [default: false]
- -o, --output File name/path for the generated encrypted file.
- [string] [default: null]
- --passphrase-placeholder Placeholder to use for the password input.
- [string] [default: "Password"]
- -r, --remember Expiration in days of the "Remember me" checkbox
- that will save the (salted + hashed) password in
- localStorage when entered by the user. Default:
- "0", no expiration. [number] [default: 0]
- --remember-label Label to use for the "Remember me" checkbox.
- [string] [default: "Remember me"]
- -s, --salt 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.
- Include the empty flag to generate a random salt
- you can use: "statycrypt -s". [string]
- --share Get a link containing your hashed password that
- will auto-decrypt the page. Pass your URL as a
- value to append "#staticrypt_pwd=",
- or leave empty to display the hash to append.
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ -c, --config Path to the config file. Set to "false" to
+ disable. [string] [default: ".staticrypt.json"]
+ -o, --output Name of the directory where the encrypted files
+ will be saved. [string] [default: "encrypted/"]
+ -p, --password The password to encrypt your file with. Leave
+ empty to be prompted for it. If
+ STATICRYPT_PASSWORD is set in the env, we'll use
+ that instead. [string] [default: null]
+ --remember Expiration in days of the "Remember me" checkbox
+ that will save the (salted + hashed) password in
+ localStorage when entered by the user. Set to
+ "false" to hide the box. Default: "0", no
+ expiration. [number] [default: 0]
+ -s, --salt 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.
+ Include the empty flag to generate a random salt
+ you can use: "statycrypt -s". [string]
+ --share Get a link containing your hashed password that
+ will auto-decrypt the page. Pass your URL as a
+ value to append "#staticrypt_pwd=",
+ or leave empty to display the hash to append.
[string]
- --short Hide the "short password" warning.
+ --short Hide the "short password" warning.
[boolean] [default: false]
- -t, --title Title for the output HTML page.
+ -t, --template Path to custom HTML template with password
+ prompt.
+ [string] [default: "/code/staticrypt/lib/password_template.html"]
+ --template-button Label to use for the decrypt button. Default:
+ "DECRYPT". [string] [default: "DECRYPT"]
+ --template-instructions Special instructions to display to the user.
+ [string] [default: ""]
+ --template-error Error message to display on entering wrong
+ password. [string] [default: "Bad password!"]
+ --template-placeholder Placeholder to use for the password input.
+ [string] [default: "Password"]
+ --template-remember Label to use for the "Remember me" checkbox.
+ [string] [default: "Remember me"]
+ --template-title Title for the output HTML page.
[string] [default: "Protected Page"]
@@ -137,7 +133,9 @@ On the technical aspects: we use AES in CBC mode (see a discussion on why it's a
### Can I customize the password prompt?
-Yes! Just copy `lib/password_template.html`, modify it to suit your style and point to your template file with the `-f path/to/my/file.html` flag. Be careful to not break the encrypting javascript part, the variables replaced by StatiCrypt are between curly brackets: `{salt}`.
+Yes! Just copy `lib/password_template.html`, modify it to suit your style and point to your template file with the `-t path/to/my/file.html` flag.
+
+Be careful to not break the encrypting javascript part, the variables replaced by StatiCrypt are in this format: `/*[|variable|]*/0`. Don't leave out the `0` at the end, this weird syntax is to avoid conflict with other templating engines while still being read as valid JS to parsers so we can use auto-formatting on the template files.
### Can I remove the "Remember me" checkbox?
@@ -223,7 +221,7 @@ npm run build
#### Test
-The testing is done manually for now - run [build](#build), then open `example/example_encypted.html` and check everything works correctly.
+The testing is done manually for now - run [build](#build), then open `example/encrypted/example.html` and check everything works correctly.
## Community and alternatives
diff --git a/cli/helpers.js b/cli/helpers.js
index e3341b5..b26a6c8 100644
--- a/cli/helpers.js
+++ b/cli/helpers.js
@@ -3,7 +3,7 @@ const fs = require("fs");
const readline = require('readline');
const { generateRandomSalt } = require("../lib/cryptoEngine.js");
-const {renderTemplate} = require("../lib/formater.js");
+const { renderTemplate } = require("../lib/formater.js");
const Yargs = require("yargs");
const PASSWORD_TEMPLATE_DEFAULT_PATH = path.join(__dirname, "..", "lib", "password_template.html");
@@ -158,6 +158,23 @@ function convertCommonJSToBrowserJS(modulePath) {
}
exports.convertCommonJSToBrowserJS = convertCommonJSToBrowserJS;
+/**
+ * Build the staticrypt script string to inject in our template.
+ *
+ * @returns {string}
+ */
+function buildStaticryptJS() {
+ let staticryptJS = convertCommonJSToBrowserJS("lib/staticryptJs");
+
+ const scriptsToInject = {
+ js_codec: convertCommonJSToBrowserJS("lib/codec"),
+ js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
+ };
+
+ return renderTemplate(staticryptJS, scriptsToInject);
+}
+exports.buildStaticryptJS = buildStaticryptJS;
+
/**
* @param {string} filePath
* @param {string} errorName
@@ -226,7 +243,8 @@ function parseCommandLineArguments() {
.option("p", {
alias: "password",
type: "string",
- describe: "The password to encrypt your file with.",
+ describe: "The password to encrypt your file with. Leave empty to be prompted for it. If STATICRYPT_PASSWORD" +
+ " is set in the env, we'll use that instead.",
default: null,
})
.option("remember", {
diff --git a/cli/index.js b/cli/index.js
index ec73e9e..72b8803 100755
--- a/cli/index.js
+++ b/cli/index.js
@@ -11,8 +11,7 @@ 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 { parseCommandLineArguments} = require("./helpers.js");
+const { parseCommandLineArguments, buildStaticryptJS, exitWithError, isOptionSetByUser, genFile, getPassword, getFileContent, getSalt } = require("./helpers.js");
// parse arguments
const yargs = parseCommandLineArguments();
@@ -85,21 +84,25 @@ async function runStatiCrypt() {
const contents = getFileContent(inputFilepath);
// encrypt input
- const encryptedMessage = await encode(contents, password, salt);
+ const encryptedMsg = await encode(contents, password, salt);
+
+ const isRememberEnabled = namedArgs.remember !== "false";
const data = {
- decrypt_button: namedArgs.templateButton,
- encrypted: encryptedMessage,
- instructions: namedArgs.templateInstructions,
- is_remember_enabled: namedArgs.remember === "false" ? "false" : "true",
- js_codec: convertCommonJSToBrowserJS("lib/codec"),
- js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
- label_error: namedArgs.templateError,
- passphrase_placeholder: namedArgs.templatePlaceholder,
- remember_duration_in_days: namedArgs.remember,
- remember_me: namedArgs.templateRemember,
- salt: salt,
- title: namedArgs.templateTitle,
+ is_remember_enabled: JSON.stringify(isRememberEnabled),
+ js_staticrypt: buildStaticryptJS(),
+ staticrypt_config: {
+ encryptedMsg,
+ isRememberEnabled,
+ rememberDurationInDays: namedArgs.remember,
+ salt,
+ },
+ template_button: namedArgs.templateButton,
+ template_error: namedArgs.templateError,
+ template_instructions: namedArgs.templateInstructions,
+ template_placeholder: namedArgs.templatePlaceholder,
+ template_remember: namedArgs.templateRemember,
+ template_title: namedArgs.templateTitle,
};
const outputFilepath = namedArgs.output.replace(/\/+$/, '') + "/" + inputFilepath;
diff --git a/example/example_encrypted.html b/example/encrypted/example.html
similarity index 78%
rename from example/example_encrypted.html
rename to example/encrypted/example.html
index e500d97..7be759a 100644
--- a/example/example_encrypted.html
+++ b/example/encrypted/example.html
@@ -183,14 +183,11 @@
-
-
-
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 @@ exports.init = init;
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 @@ exports.init = init;
* @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 @@ exports.renderTemplate = renderTemplate;
})())
+
+
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)
-# encrypt the example file
-node cli/index.js example/example.html test \
- --engine webcrypto \
- --short \
- --salt b93bbaf35459951c47721d1f3eaeb5b9 \
- --instructions "Enter \"test\" to unlock the page"
-
# build the index.html file
node ./scripts/buildIndex.js
+
+# encrypt the example file
+cd example
+node ../cli/index.js example.html \
+ -p test \
+ --short \
+ --salt b93bbaf35459951c47721d1f3eaeb5b9 \
+ --config false \
+ --template-instructions "Enter \"test\" to unlock the page"
+
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 @@ Your encrypted string
+
+