refactor password template to make it simpler (closes #167)

v3
robinmoisson 2023-03-30 12:03:18 +02:00
rodzic 7a07a670b2
commit 059701ce89
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 9419716500078583
12 zmienionych plików z 1168 dodań i 377 usunięć

2
.gitignore vendored
Wyświetl plik

@ -3,3 +3,5 @@
node_modules
.staticrypt.json
.env
encrypted/
!example/encrypted/

Wyświetl plik

@ -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 <filename> [<password>] [options]
Usage: staticrypt <filename> [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=<hashed_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=<hashed_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

Wyświetl plik

@ -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", {

Wyświetl plik

@ -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;

Wyświetl plik

@ -183,14 +183,11 @@
</div>
</div>
<script>
// do not remove this comment, it is used to detect the version of the script
// STATICRYPT_VERSION: async
const cryptoEngine = ((function(){
// these variables will be filled when generating the file - the template format is 'variable_name'
const staticryptInitiator = ((function(){
const exports = {};
const cryptoEngine = ((function(){
const exports = {};
const { subtle } = crypto;
@ -471,7 +468,7 @@ exports.generateRandomString = generateRandomString;
return exports;
})())
const codec = ((function(){
const codec = ((function(){
const exports = {};
/**
* Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
@ -559,49 +556,130 @@ exports.init = init;
return exports;
})())
const decode = codec.init(cryptoEngine).decode;
const decode = codec.init(cryptoEngine).decode;
// variables to be filled when generating the file
const encryptedMsg = '2c0a13159934226fa022225a06c64af6dfffe80dd6cb908f0f6ad93311563d78150cfc5465e3c7d70d682194a6d4a0c6082e37aaca8dbd83036d9e9cf629a112132b8fb6004a028b31ea4fb5a9f82d505096fe59e109970261733b4c8f21110b6f365d8b087d0ec15866917341e3cd105c65c9c7542626bae08903cd10675ed7fbd71062a1e87e35d30341c8251ab452352eaeecd6e44ed0a256979b30287032bccefaecf1685e948bf0fc3a3b5ad1f7b70092d9e32ceb60818ee49e821f8933',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
labelError = 'Bad password!',
isRememberEnabled = true,
rememberDurationInDays = 0; // 0 means forever
// constants
const rememberPassphraseKey = 'staticrypt_passphrase',
rememberExpirationKey = 'staticrypt_expiration';
/**
* 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 hashedPassphrase
* @param {string} hashedPassphrase
* @returns {Promise<boolean>}
*/
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;
document.write(plainHTML);
document.close();
// 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() {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
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 - whether we logged out
* @returns {boolean} - whether we logged out
*/
function logoutIfNeeded() {
const logoutKey = "staticrypt_logout";
@ -630,12 +708,8 @@ exports.init = init;
* @returns {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
*/
async function decryptOnLoadFromRememberMe() {
if (!isRememberEnabled) {
return false;
}
// show the remember me checkbox
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
const { rememberDurationInDays } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// if we are login out, terminate
if (logoutIfNeeded()) {
@ -692,20 +766,42 @@ exports.init = init;
return false;
}
return exports;
}
exports.init = init;
return exports;
})())
const templateError = 'Bad password!',
isRememberEnabled = true,
staticryptConfig = {"encryptedMsg":"8a6a36f590c75f2196594c3e955c060ebd4fc20fd39b7abe84a7f1032bec5b4037cae9ba6f0290bfb8fc4c40edb809a5a228f5917f53f4c2e72199854d5748b6290124201568f6b3d23f7bb2bfa30ac4c7dd9897fa32d65010aaddea5813e6c542b6761e0714d464b2d4f2a6574dea491af2856c6704d19026a301b3094d17ba43fad651256b870d659c41e33564e26c7e54bec1b69adf9f26f19cc6dad658485fdc01eb3b19685481ee1c58ba13f74c2962343da552c3bfb5d44197736e8743","isRememberEnabled":true,"rememberDurationInDays":0,"salt":"b93bbaf35459951c47721d1f3eaeb5b9"};
// you can edit these values to customize some of the behavior of StatiCrypt
const templateConfig = {
rememberExpirationKey: 'staticrypt_expiration',
rememberPassphraseKey: 'staticrypt_passphrase',
replaceHtmlCallback: null,
clearLocalStorageCallback: null,
};
// init the staticrypt engine
const staticrypt = staticryptInitiator.init(staticryptConfig, templateConfig);
// try to automatically decrypt on load if there is a saved password
window.onload = async function () {
let hasDecrypted = await decryptOnLoadFromUrl();
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
if (!hasDecrypted) {
hasDecrypted = await decryptOnLoadFromRememberMe();
}
// if we didn't decrypt anything, show the password prompt. Otherwise the content has already been replaced, no
// need to do anything
if (!hasDecrypted) {
// if we didn't decrypt anything on load, show the password prompt. Otherwise the content has already been
// replaced, no need to do anything
if (!isSuccessful) {
// hide loading screen
document.getElementById("staticrypt_loading").classList.add("hidden");
document.getElementById("staticrypt_content").classList.remove("hidden");
document.getElementById("staticrypt-password").focus();
// show the remember me checkbox
if (isRememberEnabled) {
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
}
}
}
@ -714,27 +810,12 @@ exports.init = init;
e.preventDefault();
const passphrase = document.getElementById('staticrypt-password').value,
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
isRememberChecked = document.getElementById('staticrypt-remember').checked;
// decrypt and replace the whole page
const hashedPassphrase = await cryptoEngine.hashPassphrase(passphrase, salt);
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(passphrase, isRememberChecked);
if (isDecryptionSuccessful) {
// remember the hashedPassphrase and set its expiration if necessary
if (isRememberEnabled && shouldRememberPassphrase) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassphrase);
// 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()
);
}
}
} else {
alert(labelError);
if (!isSuccessful) {
alert(templateError);
}
});
</script>

Wyświetl plik

@ -46,7 +46,7 @@
</p>
<p>
Download your encrypted string in a HTML page with a password prompt you can upload anywhere (see <a
target="_blank" href="example/example_encrypted.html">example</a>).
target="_blank" href="example/encrypted/example.html">example</a>).
</p>
<p>
The tool is also available as <a href="https://npmjs.com/package/staticrypt">a CLI on NPM</a> and is <a
@ -127,24 +127,24 @@
</p>
<div id="extra-options" class="hidden">
<div class="form-group">
<label for="title">Page title</label>
<input type="text" class="form-control" id="title" placeholder="Default: 'Protected Page'">
<label for="template_title">Page title</label>
<input type="text" class="form-control" id="template_title" placeholder="Default: 'Protected Page'">
</div>
<div class="form-group">
<label for="instructions">Instructions to display the user</label>
<textarea class="form-control" id="instructions" placeholder="Default: nothing."></textarea>
<label for="template_instructions">Instructions to display the user</label>
<textarea class="form-control" id="template_instructions" placeholder="Default: nothing."></textarea>
</div>
<div class="form-group">
<label for="passphrase_placeholder">Passphrase input placeholder</label>
<input type="text" class="form-control" id="passphrase_placeholder"
<label for="template_placeholder">Passphrase input placeholder</label>
<input type="text" class="form-control" id="template_placeholder"
placeholder="Default: 'Passphrase'">
</div>
<div class="form-group">
<label for="remember_me">"Remember me" checkbox label</label>
<input type="text" class="form-control" id="remember_me" placeholder="Default: 'Remember me'">
<label for="template_remember">"Remember me" checkbox label</label>
<input type="text" class="form-control" id="template_remember" placeholder="Default: 'Remember me'">
</div>
<div class="form-group">
@ -161,8 +161,8 @@
</div>
<div class="form-group">
<label for="decrypt_button">Decrypt button label</label>
<input type="text" class="form-control" id="decrypt_button" placeholder="Default: 'DECRYPT'">
<label for="template_button">Decrypt button label</label>
<input type="text" class="form-control" id="template_button" placeholder="Default: 'DECRYPT'">
</div>
</div>
@ -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;
})())
</script>
<script id="staticrypt">
window.staticrypt = ((function(){
const exports = {};
const cryptoEngine = ((function(){
const exports = {};
const { subtle } = crypto;
const IV_BITS = 16 * 8;
const HEX_BITS = 4;
const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*/
const HexEncoder = {
/**
* hex string -> bytes
* @param {string} hexString
* @returns {Uint8Array}
*/
parse: function (hexString) {
if (hexString.length % 2 !== 0) throw "Invalid hexString";
const arrayBuffer = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) {
const byteValue = parseInt(hexString.substring(i, i + 2), 16);
if (isNaN(byteValue)) {
throw "Invalid hexString";
}
arrayBuffer[i / 2] = byteValue;
}
return arrayBuffer;
},
/**
* bytes -> hex string
* @param {Uint8Array} bytes
* @returns {string}
*/
stringify: function (bytes) {
const hexBytes = [];
for (let i = 0; i < bytes.length; ++i) {
let byteString = bytes[i].toString(16);
if (byteString.length < 2) {
byteString = "0" + byteString;
}
hexBytes.push(byteString);
}
return hexBytes.join("");
},
};
/**
* Translates between utf8 strings and Uint8Array bytes.
*/
const UTF8Encoder = {
parse: function (str) {
return new TextEncoder().encode(str);
},
stringify: function (bytes) {
return new TextDecoder().decode(bytes);
},
};
/**
* Salt and encrypt a msg with a password.
*/
async function encrypt(msg, hashedPassphrase) {
// Must be 16 bytes, unpredictable, and preferably cryptographically random. However, it need not be secret.
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#parameters
const iv = crypto.getRandomValues(new Uint8Array(IV_BITS / 8));
const key = await subtle.importKey(
"raw",
HexEncoder.parse(hashedPassphrase),
ENCRYPTION_ALGO,
false,
["encrypt"]
);
const encrypted = await subtle.encrypt(
{
name: ENCRYPTION_ALGO,
iv: iv,
},
key,
UTF8Encoder.parse(msg)
);
// iv will be 32 hex characters, we prepend it to the ciphertext for use in decryption
return HexEncoder.stringify(iv) + HexEncoder.stringify(new Uint8Array(encrypted));
}
exports.encrypt = encrypt;
/**
* Decrypt a salted msg using a password.
*
* @param {string} encryptedMsg
* @param {string} hashedPassphrase
* @returns {Promise<string>}
*/
async function decrypt(encryptedMsg, hashedPassphrase) {
const ivLength = IV_BITS / HEX_BITS;
const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
const encrypted = encryptedMsg.substring(ivLength);
const key = await subtle.importKey(
"raw",
HexEncoder.parse(hashedPassphrase),
ENCRYPTION_ALGO,
false,
["decrypt"]
);
const outBuffer = await subtle.decrypt(
{
name: ENCRYPTION_ALGO,
iv: iv,
},
key,
HexEncoder.parse(encrypted)
);
return UTF8Encoder.stringify(new Uint8Array(outBuffer));
}
exports.decrypt = decrypt;
/**
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} passphrase
* @param {string} salt
* @returns {Promise<string>}
*/
async function hashPassphrase(passphrase, salt) {
// we hash the passphrase in multiple steps, each adding more iterations. This is because we used to allow less
// iterations, so for backward compatibility reasons, we need to support going from that to more iterations.
let hashedPassphrase = await hashLegacyRound(passphrase, salt);
hashedPassphrase = await hashSecondRound(hashedPassphrase, salt);
return hashThirdRound(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 {Promise<string>}
*/
function hashLegacyRound(passphrase, salt) {
return pbkdf2(passphrase, salt, 1000, "SHA-1");
}
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 {Promise<string>}
*/
function hashSecondRound(hashedPassphrase, salt) {
return pbkdf2(hashedPassphrase, salt, 14000, "SHA-256");
}
exports.hashSecondRound = hashSecondRound;
/**
* Add a third round of iterations to bring total number to 600k. This is because we used to use 1k, then 15k, so for
* backwards compatibility with remember-me/autodecrypt links, we need to support going from that to more iterations.
*
* @param hashedPassphrase
* @param salt
* @returns {Promise<string>}
*/
function hashThirdRound(hashedPassphrase, salt) {
return pbkdf2(hashedPassphrase, salt, 585000, "SHA-256");
}
exports.hashThirdRound = hashThirdRound;
/**
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} passphrase
* @param {string} salt
* @param {int} iterations
* @param {string} hashAlgorithm
* @returns {Promise<string>}
*/
async function pbkdf2(passphrase, salt, iterations, hashAlgorithm) {
const key = await subtle.importKey(
"raw",
UTF8Encoder.parse(passphrase),
"PBKDF2",
false,
["deriveBits"]
);
const keyBytes = await subtle.deriveBits(
{
name: "PBKDF2",
hash: hashAlgorithm,
iterations,
salt: UTF8Encoder.parse(salt),
},
key,
256
);
return HexEncoder.stringify(new Uint8Array(keyBytes));
}
function generateRandomSalt() {
const bytes = crypto.getRandomValues(new Uint8Array(128 / 8));
return HexEncoder.stringify(new Uint8Array(bytes));
}
exports.generateRandomSalt = generateRandomSalt;
async function signMessage(hashedPassphrase, message) {
const key = await subtle.importKey(
"raw",
HexEncoder.parse(hashedPassphrase),
{
name: "HMAC",
hash: "SHA-256",
},
false,
["sign"]
);
const signature = await subtle.sign("HMAC", key, UTF8Encoder.parse(message));
return HexEncoder.stringify(new Uint8Array(signature));
}
exports.signMessage = signMessage;
function getRandomAlphanum() {
const possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let byteArray;
let parsedInt;
// Keep generating new random bytes until we get a value that falls
// within a range that can be evenly divided by possibleCharacters.length
do {
byteArray = crypto.getRandomValues(new Uint8Array(1));
// extract the lowest byte to get an int from 0 to 255 (probably unnecessary, since we're only generating 1 byte)
parsedInt = byteArray[0] & 0xff;
} while (parsedInt >= 256 - (256 % possibleCharacters.length));
// Take the modulo of the parsed integer to get a random number between 0 and totalLength - 1
const randomIndex = parsedInt % possibleCharacters.length;
return possibleCharacters[randomIndex];
}
/**
* Generate a random string of a given length.
*
* @param {int} length
* @returns {string}
*/
function generateRandomString(length) {
let randomString = '';
for (let i = 0; i < length; i++) {
randomString += getRandomAlphanum();
}
return randomString;
}
exports.generateRandomString = generateRandomString;
return exports;
})())
const codec = ((function(){
const exports = {};
/**
* Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
*
* @param cryptoEngine - the engine to use for encryption / decryption
*/
function init(cryptoEngine) {
const exports = {};
/**
* Top-level function for encoding a message.
* Includes password hashing, encryption, and signing.
*
* @param {string} msg
* @param {string} password
* @param {string} salt
*
* @returns {string} The encoded text
*/
async function encode(msg, password, salt) {
const hashedPassphrase = await cryptoEngine.hashPassphrase(password, salt);
const encrypted = await cryptoEngine.encrypt(msg, hashedPassphrase);
// we use the hashed password in the HMAC because this is effectively what will be used a password (so we can store
// it in localStorage safely, we don't use the clear text password)
const hmac = await cryptoEngine.signMessage(hashedPassphrase, encrypted);
return hmac + encrypted;
}
exports.encode = encode;
/**
* Top-level function for decoding a message.
* Includes signature check and decryption.
*
* @param {string} signedMsg
* @param {string} hashedPassphrase
* @param {string} salt
* @param {int} backwardCompatibleAttempt
* @param {string} originalPassphrase
*
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
*/
async function decode(
signedMsg,
hashedPassphrase,
salt,
backwardCompatibleAttempt = 0,
originalPassphrase = ''
) {
const encryptedHMAC = signedMsg.substring(0, 64);
const encryptedMsg = signedMsg.substring(64);
const decryptedHMAC = await cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
if (decryptedHMAC !== encryptedHMAC) {
// we have been raising the number of iterations in the hashing algorithm multiple times, so to support the old
// remember-me/autodecrypt links we need to try bringing the old hashes up to speed.
originalPassphrase = originalPassphrase || hashedPassphrase;
if (backwardCompatibleAttempt === 0) {
const updatedHashedPassphrase = await cryptoEngine.hashThirdRound(originalPassphrase, salt);
return decode(signedMsg, updatedHashedPassphrase, salt, backwardCompatibleAttempt + 1, originalPassphrase);
}
if (backwardCompatibleAttempt === 1) {
let updatedHashedPassphrase = await cryptoEngine.hashSecondRound(originalPassphrase, salt);
updatedHashedPassphrase = await cryptoEngine.hashThirdRound(updatedHashedPassphrase, salt);
return decode(signedMsg, updatedHashedPassphrase, salt, backwardCompatibleAttempt + 1, originalPassphrase);
}
return { success: false, message: "Signature mismatch" };
}
return {
success: true,
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
};
}
exports.decode = decode;
return exports;
}
exports.init = init;
return exports;
})())
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<boolean>}
*/
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<boolean>} 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;
return exports;
})())
</script>
<script>
const encode = codec.init(cryptoEngine).encode;
// enable CKEDIRTOR
CKEDITOR.replace('instructions');
CKEDITOR.replace('template_instructions');
let htmlToDownload;
/**
* Extract js code from <script> tag and return it as a string
*
* @param id
* @returns
* @param {string} id
* @returns {string}
*/
function getScriptAsString(id) {
return document.getElementById(id)
@ -611,7 +1204,7 @@ exports.renderTemplate = renderTemplate;
* Register something happened - this uses a simple Supabase function to implement a counter, and allows to drop
* google analytics. We don't store any personal data or IP.
*
* @param action
* @param {string} action
*/
function trackEvent(action) {
const xhr = new XMLHttpRequest();
@ -660,7 +1253,7 @@ exports.renderTemplate = renderTemplate;
// update instruction textarea value with CKEDITOR content
// (see https://stackoverflow.com/questions/3147670/ckeditor-update-textarea)
CKEDITOR.instances['instructions'].updateElement();
CKEDITOR.instances['template_instructions'].updateElement();
const unencrypted = document.getElementById('unencrypted_html').value,
passphrase = document.getElementById('passphrase').value;
@ -668,27 +1261,28 @@ exports.renderTemplate = renderTemplate;
const salt = cryptoEngine.generateRandomSalt();
const encryptedMsg = await encode(unencrypted, passphrase, salt);
const decryptButton = document.getElementById('decrypt_button').value,
instructions = document.getElementById('instructions').value,
const templateButton = document.getElementById('template_button').value,
templateInstructions = document.getElementById('template_instructions').value,
isRememberEnabled = document.getElementById('remember').checked,
pageTitle = document.getElementById('title').value.trim(),
passphrasePlaceholder = document.getElementById('passphrase_placeholder').value.trim(),
templateTitle = document.getElementById('template_title').value.trim(),
templatePlaceholder = document.getElementById('template_placeholder').value.trim(),
rememberDurationInDays = document.getElementById('remember_in_days').value || 0,
rememberMe = document.getElementById('remember_me').value;
templateRemember = document.getElementById('template_remember').value;
const data = {
crypto_tag: '<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></scr' + 'ipt>',
decrypt_button: decryptButton ? decryptButton : 'DECRYPT',
encrypted: encryptedMsg,
instructions: instructions ? instructions : '',
is_remember_enabled: isRememberEnabled ? 'true' : 'false',
js_codec: getScriptAsString('codec'),
js_crypto_engine: getScriptAsString('cryptoEngine'),
passphrase_placeholder: passphrasePlaceholder ? passphrasePlaceholder : 'Passphrase',
remember_duration_in_days: rememberDurationInDays.toString(),
remember_me: rememberMe ? rememberMe : 'Remember me',
salt: salt,
title: pageTitle ? pageTitle : 'Protected Page',
staticrypt_config: {
encryptedMsg,
isRememberEnabled,
rememberDurationInDays,
salt,
},
is_remember_enabled: JSON.stringify(isRememberEnabled),
js_staticrypt: getScriptAsString('staticrypt'),
template_button: templateButton ? templateButton : 'DECRYPT',
template_instructions: templateInstructions || '',
template_placeholder: templatePlaceholder || 'Passphrase',
template_remember: templateRemember || 'Remember me',
template_title: templateTitle || 'Protected Page',
};
document.getElementById('encrypted_html_display').textContent = encryptedMsg;

Wyświetl plik

@ -1,5 +1,9 @@
/**
* Replace the placeholder tags (between '{tag}') in the template string with provided data.
* Replace the variable in template tags, between '/*[|variable|]* /0' (without the space in '* /0', ommiting it would
* break this comment), with the provided data.
*
* This weird format is so that we have something that doesn't break JS parser in the template files (it understands it
* as '0'), so we can still use auto-formatting.
*
* @param {string} templateString
* @param {Object} data
@ -7,12 +11,16 @@
* @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;

Wyświetl plik

@ -2,7 +2,7 @@
<html class="staticrypt-html">
<head>
<meta charset="utf-8">
<title>{title}</title>
<title>/*[|template_title|]*/0</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- do not cache this page -->
@ -156,8 +156,8 @@
<div class="staticrypt-page">
<div class="staticrypt-form">
<div class="staticrypt-instructions">
<p class="staticrypt-title">{title}</p>
<p>{instructions}</p>
<p class="staticrypt-title">/*[|template_title|]*/0</p>
<p>/*[|template_instructions|]*/0</p>
</div>
<hr class="staticrypt-hr">
@ -166,179 +166,57 @@
<input id="staticrypt-password"
type="password"
name="password"
placeholder="{passphrase_placeholder}"
placeholder="/*[|template_placeholder|]*/0"
autofocus/>
<label id="staticrypt-remember-label" class="staticrypt-remember hidden">
<input id="staticrypt-remember"
type="checkbox"
name="remember"/>
{remember_me}
/*[|template_remember|]*/0
</label>
<input type="submit" class="staticrypt-decrypt-button" value="{decrypt_button}"/>
<input type="submit" class="staticrypt-decrypt-button" value="/*[|template_button|]*/0"/>
</form>
</div>
</div>
</div>
{crypto_tag}
<script>
// do not remove this comment, it is used to detect the version of the script
// STATICRYPT_VERSION: async
// these variables will be filled when generating the file - the template format is '/*[|variable_name|]*/0'
const staticryptInitiator = /*[|js_staticrypt|]*/0
const templateError = '/*[|template_error|]*/0',
isRememberEnabled = /*[|is_remember_enabled|]*/0,
staticryptConfig = /*[|staticrypt_config|]*/0;
const cryptoEngine = {js_crypto_engine}
const codec = {js_codec}
const decode = codec.init(cryptoEngine).decode;
// you can edit these values to customize some of the behavior of StatiCrypt
const templateConfig = {
rememberExpirationKey: 'staticrypt_expiration',
rememberPassphraseKey: 'staticrypt_passphrase',
replaceHtmlCallback: null,
clearLocalStorageCallback: null,
};
// variables to be filled when generating the file
const encryptedMsg = '{encrypted}',
salt = '{salt}',
labelError = '{label_error}',
isRememberEnabled = {is_remember_enabled},
rememberDurationInDays = {remember_duration_in_days}; // 0 means forever
// constants
const rememberPassphraseKey = 'staticrypt_passphrase',
rememberExpirationKey = 'staticrypt_expiration';
/**
* Decrypt our encrypted page, replace the whole HTML.
*
* @param {string} hashedPassphrase
* @returns {Promise<boolean>}
*/
async function decryptAndReplaceHtml(hashedPassphrase) {
const result = await decode(encryptedMsg, hashedPassphrase, salt);
if (!result.success) {
return false;
}
const plainHTML = result.decoded;
document.write(plainHTML);
document.close();
return true;
}
/**
* Clear localstorage from staticrypt related values
*/
function clearLocalStorage() {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
}
/**
* 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<boolean>} true if we derypted and replaced the whole page, false otherwise
*/
async function decryptOnLoadFromRememberMe() {
if (!isRememberEnabled) {
return false;
}
// show the remember me checkbox
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
// 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;
}
// init the staticrypt engine
const staticrypt = staticryptInitiator.init(staticryptConfig, templateConfig);
// try to automatically decrypt on load if there is a saved password
window.onload = async function () {
let hasDecrypted = await decryptOnLoadFromUrl();
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
if (!hasDecrypted) {
hasDecrypted = await decryptOnLoadFromRememberMe();
}
// if we didn't decrypt anything, show the password prompt. Otherwise the content has already been replaced, no
// need to do anything
if (!hasDecrypted) {
// if we didn't decrypt anything on load, show the password prompt. Otherwise the content has already been
// replaced, no need to do anything
if (!isSuccessful) {
// hide loading screen
document.getElementById("staticrypt_loading").classList.add("hidden");
document.getElementById("staticrypt_content").classList.remove("hidden");
document.getElementById("staticrypt-password").focus();
// show the remember me checkbox
if (isRememberEnabled) {
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
}
}
}
@ -347,27 +225,12 @@
e.preventDefault();
const passphrase = document.getElementById('staticrypt-password').value,
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
isRememberChecked = document.getElementById('staticrypt-remember').checked;
// decrypt and replace the whole page
const hashedPassphrase = await cryptoEngine.hashPassphrase(passphrase, salt);
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(passphrase, isRememberChecked);
if (isDecryptionSuccessful) {
// remember the hashedPassphrase and set its expiration if necessary
if (isRememberEnabled && shouldRememberPassphrase) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassphrase);
// 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()
);
}
}
} else {
alert(labelError);
if (!isSuccessful) {
alert(templateError);
}
});
</script>

215
lib/staticryptJs.js 100644
Wyświetl plik

@ -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<boolean>}
*/
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<boolean>} 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;

Wyświetl plik

@ -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"

Wyświetl plik

@ -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");

Wyświetl plik

@ -46,7 +46,7 @@
</p>
<p>
Download your encrypted string in a HTML page with a password prompt you can upload anywhere (see <a
target="_blank" href="example/example_encrypted.html">example</a>).
target="_blank" href="example/encrypted/example.html">example</a>).
</p>
<p>
The tool is also available as <a href="https://npmjs.com/package/staticrypt">a CLI on NPM</a> and is <a
@ -127,24 +127,24 @@
</p>
<div id="extra-options" class="hidden">
<div class="form-group">
<label for="title">Page title</label>
<input type="text" class="form-control" id="title" placeholder="Default: 'Protected Page'">
<label for="template_title">Page title</label>
<input type="text" class="form-control" id="template_title" placeholder="Default: 'Protected Page'">
</div>
<div class="form-group">
<label for="instructions">Instructions to display the user</label>
<textarea class="form-control" id="instructions" placeholder="Default: nothing."></textarea>
<label for="template_instructions">Instructions to display the user</label>
<textarea class="form-control" id="template_instructions" placeholder="Default: nothing."></textarea>
</div>
<div class="form-group">
<label for="passphrase_placeholder">Passphrase input placeholder</label>
<input type="text" class="form-control" id="passphrase_placeholder"
<label for="template_placeholder">Passphrase input placeholder</label>
<input type="text" class="form-control" id="template_placeholder"
placeholder="Default: 'Passphrase'">
</div>
<div class="form-group">
<label for="remember_me">"Remember me" checkbox label</label>
<input type="text" class="form-control" id="remember_me" placeholder="Default: 'Remember me'">
<label for="template_remember">"Remember me" checkbox label</label>
<input type="text" class="form-control" id="template_remember" placeholder="Default: 'Remember me'">
</div>
<div class="form-group">
@ -161,8 +161,8 @@
</div>
<div class="form-group">
<label for="decrypt_button">Decrypt button label</label>
<input type="text" class="form-control" id="decrypt_button" placeholder="Default: 'DECRYPT'">
<label for="template_button">Decrypt button label</label>
<input type="text" class="form-control" id="template_button" placeholder="Default: 'DECRYPT'">
</div>
</div>
@ -187,22 +187,26 @@ Your encrypted string</pre>
<script src="https://cdn.ckeditor.com/4.7.0/standard/ckeditor.js"></script>
<script id="cryptoEngine">
window.cryptoEngine = {js_crypto_engine}
window.cryptoEngine = /*[|js_crypto_engine|]*/0
</script>
<script id="codec">
window.codec = {js_codec}
window.codec = /*[|js_codec|]*/0
</script>
<script id="formater">
window.formater = {js_formater}
window.formater = /*[|js_formater|]*/0
</script>
<script id="staticrypt">
window.staticrypt = /*[|js_staticrypt|]*/0
</script>
<script>
const encode = codec.init(cryptoEngine).encode;
// enable CKEDIRTOR
CKEDITOR.replace('instructions');
CKEDITOR.replace('template_instructions');
let htmlToDownload;
@ -270,7 +274,7 @@ Your encrypted string</pre>
// update instruction textarea value with CKEDITOR content
// (see https://stackoverflow.com/questions/3147670/ckeditor-update-textarea)
CKEDITOR.instances['instructions'].updateElement();
CKEDITOR.instances['template_instructions'].updateElement();
const unencrypted = document.getElementById('unencrypted_html').value,
passphrase = document.getElementById('passphrase').value;
@ -278,27 +282,28 @@ Your encrypted string</pre>
const salt = cryptoEngine.generateRandomSalt();
const encryptedMsg = await encode(unencrypted, passphrase, salt);
const decryptButton = document.getElementById('decrypt_button').value,
instructions = document.getElementById('instructions').value,
const templateButton = document.getElementById('template_button').value,
templateInstructions = document.getElementById('template_instructions').value,
isRememberEnabled = document.getElementById('remember').checked,
pageTitle = document.getElementById('title').value.trim(),
passphrasePlaceholder = document.getElementById('passphrase_placeholder').value.trim(),
templateTitle = document.getElementById('template_title').value.trim(),
templatePlaceholder = document.getElementById('template_placeholder').value.trim(),
rememberDurationInDays = document.getElementById('remember_in_days').value || 0,
rememberMe = document.getElementById('remember_me').value;
templateRemember = document.getElementById('template_remember').value;
const data = {
crypto_tag: '<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></scr' + 'ipt>',
decrypt_button: decryptButton ? decryptButton : 'DECRYPT',
encrypted: encryptedMsg,
instructions: instructions ? instructions : '',
is_remember_enabled: isRememberEnabled ? 'true' : 'false',
js_codec: getScriptAsString('codec'),
js_crypto_engine: getScriptAsString('cryptoEngine'),
passphrase_placeholder: passphrasePlaceholder ? passphrasePlaceholder : 'Passphrase',
remember_duration_in_days: rememberDurationInDays.toString(),
remember_me: rememberMe ? rememberMe : 'Remember me',
salt: salt,
title: pageTitle ? pageTitle : 'Protected Page',
staticrypt_config: {
encryptedMsg,
isRememberEnabled,
rememberDurationInDays,
salt,
},
is_remember_enabled: JSON.stringify(isRememberEnabled),
js_staticrypt: getScriptAsString('staticrypt'),
template_button: templateButton ? templateButton : 'DECRYPT',
template_instructions: templateInstructions || '',
template_placeholder: templatePlaceholder || 'Passphrase',
template_remember: templateRemember || 'Remember me',
template_title: templateTitle || 'Protected Page',
};
document.getElementById('encrypted_html_display').textContent = encryptedMsg;