kopia lustrzana https://github.com/robinmoisson/staticrypt
refactor password template to make it simpler (closes #167)
rodzic
7a07a670b2
commit
059701ce89
|
@ -3,3 +3,5 @@
|
|||
node_modules
|
||||
.staticrypt.json
|
||||
.env
|
||||
encrypted/
|
||||
!example/encrypted/
|
||||
|
|
94
README.md
94
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 <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
|
||||
|
||||
|
|
|
@ -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", {
|
||||
|
|
33
cli/index.js
33
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;
|
||||
|
|
|
@ -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>
|
670
index.html
670
index.html
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
Ładowanie…
Reference in New Issue