kopia lustrzana https://github.com/robinmoisson/staticrypt
add and run prettier
rodzic
4db6e90b8c
commit
f5721f079a
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.{js,html}",
|
||||
"options": {
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
240
cli/helpers.js
240
cli/helpers.js
|
@ -1,14 +1,13 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const readline = require('readline');
|
||||
const readline = require("readline");
|
||||
|
||||
const { generateRandomSalt, generateRandomString} = require("../lib/cryptoEngine.js");
|
||||
const { generateRandomSalt, generateRandomString } = require("../lib/cryptoEngine.js");
|
||||
const { renderTemplate } = require("../lib/formater.js");
|
||||
const Yargs = require("yargs");
|
||||
|
||||
const PASSWORD_TEMPLATE_DEFAULT_PATH = path.join(__dirname, "..", "lib", "password_template.html");
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
|
@ -43,8 +42,9 @@ function isOptionSetByUser(option, yargs) {
|
|||
for (let aliasIndex in yargs.parsed.aliases[option]) {
|
||||
const alias = yargs.parsed.aliases[option][aliasIndex];
|
||||
|
||||
if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`))
|
||||
if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -57,7 +57,7 @@ exports.isOptionSetByUser = isOptionSetByUser;
|
|||
* @param {string} question
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function prompt (question) {
|
||||
function prompt(question) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
|
@ -77,11 +77,11 @@ async function getValidatedPassword(passwordArgument, isShortAllowed) {
|
|||
if (password.length < 14 && !isShortAllowed) {
|
||||
const shouldUseShort = await prompt(
|
||||
`WARNING: Your password is less than 14 characters (length: ${password.length})` +
|
||||
" and it's easy to try brute-forcing on public files. For better security we recommend using a longer one, for example: "
|
||||
+ generateRandomString(21)
|
||||
+ "\nYou can hide this warning by increasing your password length or adding the '--short' flag." +
|
||||
" Do you want to use the short password? [y/N] "
|
||||
)
|
||||
" and it's easy to try brute-forcing on public files. For better security we recommend using a longer one, for example: " +
|
||||
generateRandomString(21) +
|
||||
"\nYou can hide this warning by increasing your password length or adding the '--short' flag." +
|
||||
" Do you want to use the short password? [y/N] "
|
||||
);
|
||||
|
||||
if (!shouldUseShort.match(/^\s*(y|yes)\s*$/i)) {
|
||||
console.log("Aborting.");
|
||||
|
@ -135,7 +135,7 @@ async function getPassword(passwordArgument) {
|
|||
}
|
||||
|
||||
// prompt the user for their password
|
||||
return prompt('Enter your long, unusual password: ');
|
||||
return prompt("Enter your long, unusual password: ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,8 +162,9 @@ function getValidatedSalt(namedArgs, config) {
|
|||
// validate the salt
|
||||
if (salt.length !== 32 || /[^a-f0-9]/.test(salt)) {
|
||||
exitWithError(
|
||||
"the salt should be a 32 character long hexadecimal string (only [0-9a-f] characters allowed)"
|
||||
+ "\nDetected salt: " + salt
|
||||
"the salt should be a 32 character long hexadecimal string (only [0-9a-f] characters allowed)" +
|
||||
"\nDetected salt: " +
|
||||
salt
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -199,16 +200,14 @@ function getSalt(namedArgs, config) {
|
|||
* @param {string} modulePath - path from staticrypt root directory
|
||||
*/
|
||||
function convertCommonJSToBrowserJS(modulePath) {
|
||||
const rootDirectory = path.join(__dirname, '..');
|
||||
const rootDirectory = path.join(__dirname, "..");
|
||||
const resolvedPath = path.join(rootDirectory, ...modulePath.split("/")) + ".js";
|
||||
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
exitWithError(`could not find module to convert at path "${resolvedPath}"`);
|
||||
}
|
||||
|
||||
const moduleText = fs
|
||||
.readFileSync(resolvedPath, "utf8")
|
||||
.replace(/^.*\brequire\(.*$\n/gm, "");
|
||||
const moduleText = fs.readFileSync(resolvedPath, "utf8").replace(/^.*\brequire\(.*$\n/gm, "");
|
||||
|
||||
return `
|
||||
((function(){
|
||||
|
@ -289,107 +288,110 @@ function isCustomPasswordTemplateDefault(templatePathParameter) {
|
|||
exports.isCustomPasswordTemplateDefault = isCustomPasswordTemplateDefault;
|
||||
|
||||
function parseCommandLineArguments() {
|
||||
return Yargs.usage("Usage: staticrypt <filename> [<filename> ...] [options]")
|
||||
.option("c", {
|
||||
alias: "config",
|
||||
type: "string",
|
||||
describe: 'Path to the config file. Set to "false" to disable.',
|
||||
default: ".staticrypt.json",
|
||||
})
|
||||
.option("d", {
|
||||
alias: "directory",
|
||||
type: "string",
|
||||
describe: "Name of the directory where the encrypted files will be saved.",
|
||||
default: "encrypted/",
|
||||
})
|
||||
.option("p", {
|
||||
alias: "password",
|
||||
type: "string",
|
||||
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("r", {
|
||||
alias: "recursive",
|
||||
type: "boolean",
|
||||
describe: "Whether to recursively encrypt the input directory.",
|
||||
default: false,
|
||||
})
|
||||
.option("remember", {
|
||||
type: "number",
|
||||
describe:
|
||||
'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.',
|
||||
default: 0,
|
||||
})
|
||||
// do not give a default option to this parameter - we want to see when the flag is included with no
|
||||
// value and when it's not included at all
|
||||
.option("s", {
|
||||
alias: "salt",
|
||||
describe:
|
||||
'Generate a config file or set the salt manually. Pass a 32-character-long hexadecimal string ' +
|
||||
'to use as salt, or leave empty to generate, display and save to config a random salt. This won\'t' +
|
||||
' overwrite an existing config file.',
|
||||
type: "string",
|
||||
})
|
||||
// do not give a default option to this parameter - we want to see when the flag is included with no
|
||||
// value and when it's not included at all
|
||||
.option("share", {
|
||||
describe:
|
||||
'Get a link containing your hashed password that will auto-decrypt the page. Pass your URL as a value to append '
|
||||
+ '"#staticrypt_pwd=<hashed_pwd>", or leave empty to display the hash to append.',
|
||||
type: "string",
|
||||
})
|
||||
.option("short", {
|
||||
describe: 'Hide the "short password" warning.',
|
||||
type: "boolean",
|
||||
default: false,
|
||||
})
|
||||
.option("t", {
|
||||
alias: "template",
|
||||
type: "string",
|
||||
describe: "Path to custom HTML template with password prompt.",
|
||||
default: PASSWORD_TEMPLATE_DEFAULT_PATH,
|
||||
})
|
||||
.option("template-button", {
|
||||
type: "string",
|
||||
describe: 'Label to use for the decrypt button. Default: "DECRYPT".',
|
||||
default: "DECRYPT",
|
||||
})
|
||||
.option("template-color-primary", {
|
||||
type: "string",
|
||||
describe: "Primary color (button...)",
|
||||
default: "#4CAF50",
|
||||
})
|
||||
.option("template-color-secondary", {
|
||||
type: "string",
|
||||
describe: "Secondary color (page background...)",
|
||||
default: "#76B852",
|
||||
})
|
||||
.option("template-instructions", {
|
||||
type: "string",
|
||||
describe: "Special instructions to display to the user.",
|
||||
default: "",
|
||||
})
|
||||
.option("template-error", {
|
||||
type: "string",
|
||||
describe: "Error message to display on entering wrong password.",
|
||||
default: "Bad password!",
|
||||
})
|
||||
.option("template-placeholder", {
|
||||
type: "string",
|
||||
describe: "Placeholder to use for the password input.",
|
||||
default: "Password",
|
||||
})
|
||||
.option("template-remember", {
|
||||
type: "string",
|
||||
describe: 'Label to use for the "Remember me" checkbox.',
|
||||
default: "Remember me",
|
||||
})
|
||||
.option("template-title", {
|
||||
type: "string",
|
||||
describe: "Title for the output HTML page.",
|
||||
default: "Protected Page",
|
||||
});
|
||||
return (
|
||||
Yargs.usage("Usage: staticrypt <filename> [<filename> ...] [options]")
|
||||
.option("c", {
|
||||
alias: "config",
|
||||
type: "string",
|
||||
describe: 'Path to the config file. Set to "false" to disable.',
|
||||
default: ".staticrypt.json",
|
||||
})
|
||||
.option("d", {
|
||||
alias: "directory",
|
||||
type: "string",
|
||||
describe: "Name of the directory where the encrypted files will be saved.",
|
||||
default: "encrypted/",
|
||||
})
|
||||
.option("p", {
|
||||
alias: "password",
|
||||
type: "string",
|
||||
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("r", {
|
||||
alias: "recursive",
|
||||
type: "boolean",
|
||||
describe: "Whether to recursively encrypt the input directory.",
|
||||
default: false,
|
||||
})
|
||||
.option("remember", {
|
||||
type: "number",
|
||||
describe:
|
||||
'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.',
|
||||
default: 0,
|
||||
})
|
||||
// do not give a default option to this parameter - we want to see when the flag is included with no
|
||||
// value and when it's not included at all
|
||||
.option("s", {
|
||||
alias: "salt",
|
||||
describe:
|
||||
"Generate a config file or set the salt manually. Pass a 32-character-long hexadecimal string " +
|
||||
"to use as salt, or leave empty to generate, display and save to config a random salt. This won't" +
|
||||
" overwrite an existing config file.",
|
||||
type: "string",
|
||||
})
|
||||
// do not give a default option to this parameter - we want to see when the flag is included with no
|
||||
// value and when it's not included at all
|
||||
.option("share", {
|
||||
describe:
|
||||
"Get a link containing your hashed password that will auto-decrypt the page. Pass your URL as a value to append " +
|
||||
'"#staticrypt_pwd=<hashed_pwd>", or leave empty to display the hash to append.',
|
||||
type: "string",
|
||||
})
|
||||
.option("short", {
|
||||
describe: 'Hide the "short password" warning.',
|
||||
type: "boolean",
|
||||
default: false,
|
||||
})
|
||||
.option("t", {
|
||||
alias: "template",
|
||||
type: "string",
|
||||
describe: "Path to custom HTML template with password prompt.",
|
||||
default: PASSWORD_TEMPLATE_DEFAULT_PATH,
|
||||
})
|
||||
.option("template-button", {
|
||||
type: "string",
|
||||
describe: 'Label to use for the decrypt button. Default: "DECRYPT".',
|
||||
default: "DECRYPT",
|
||||
})
|
||||
.option("template-color-primary", {
|
||||
type: "string",
|
||||
describe: "Primary color (button...)",
|
||||
default: "#4CAF50",
|
||||
})
|
||||
.option("template-color-secondary", {
|
||||
type: "string",
|
||||
describe: "Secondary color (page background...)",
|
||||
default: "#76B852",
|
||||
})
|
||||
.option("template-instructions", {
|
||||
type: "string",
|
||||
describe: "Special instructions to display to the user.",
|
||||
default: "",
|
||||
})
|
||||
.option("template-error", {
|
||||
type: "string",
|
||||
describe: "Error message to display on entering wrong password.",
|
||||
default: "Bad password!",
|
||||
})
|
||||
.option("template-placeholder", {
|
||||
type: "string",
|
||||
describe: "Placeholder to use for the password input.",
|
||||
default: "Password",
|
||||
})
|
||||
.option("template-remember", {
|
||||
type: "string",
|
||||
describe: 'Label to use for the "Remember me" checkbox.',
|
||||
default: "Remember me",
|
||||
})
|
||||
.option("template-title", {
|
||||
type: "string",
|
||||
describe: "Title for the output HTML page.",
|
||||
default: "Protected Page",
|
||||
})
|
||||
);
|
||||
}
|
||||
exports.parseCommandLineArguments = parseCommandLineArguments;
|
||||
|
|
34
cli/index.js
34
cli/index.js
|
@ -10,7 +10,7 @@ if (nodeVersion[0] < 16) {
|
|||
}
|
||||
|
||||
// parse .env file into process.env
|
||||
require('dotenv').config();
|
||||
require("dotenv").config();
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
|
@ -18,9 +18,16 @@ const cryptoEngine = require("../lib/cryptoEngine.js");
|
|||
const codec = require("../lib/codec.js");
|
||||
const { generateRandomSalt } = cryptoEngine;
|
||||
const { encodeWithHashedPassword } = codec.init(cryptoEngine);
|
||||
const { parseCommandLineArguments, buildStaticryptJS, isOptionSetByUser, genFile, getFileContent,
|
||||
const {
|
||||
parseCommandLineArguments,
|
||||
buildStaticryptJS,
|
||||
isOptionSetByUser,
|
||||
genFile,
|
||||
getFileContent,
|
||||
getValidatedSalt,
|
||||
getValidatedPassword, getConfig, writeConfig
|
||||
getValidatedPassword,
|
||||
getConfig,
|
||||
writeConfig,
|
||||
} = require("./helpers.js");
|
||||
|
||||
// parse arguments
|
||||
|
@ -100,27 +107,26 @@ async function runStatiCrypt() {
|
|||
|
||||
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
|
||||
|
||||
positionalArguments.forEach(path => encodeAndGenerateFile(
|
||||
path.toString(),
|
||||
hashedPassword,
|
||||
salt,
|
||||
baseTemplateData,
|
||||
isRememberEnabled,
|
||||
namedArgs
|
||||
));
|
||||
positionalArguments.forEach((path) =>
|
||||
encodeAndGenerateFile(path.toString(), hashedPassword, salt, baseTemplateData, isRememberEnabled, namedArgs)
|
||||
);
|
||||
}
|
||||
|
||||
async function encodeAndGenerateFile(path, hashedPassword, salt, baseTemplateData, isRememberEnabled, namedArgs) {
|
||||
// if the path is a directory, get into it and process all files
|
||||
if (fs.statSync(path).isDirectory()) {
|
||||
if (!namedArgs.recursive) {
|
||||
console.log("ERROR: The path '" + path + "' is a directory. Use the -r|--recursive flag to process all files in the directory.");
|
||||
console.log(
|
||||
"ERROR: The path '" +
|
||||
path +
|
||||
"' is a directory. Use the -r|--recursive flag to process all files in the directory."
|
||||
);
|
||||
|
||||
// just return instead of exiting the process, that way all other files can be processed
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readdirSync(path).forEach(filePath => {
|
||||
fs.readdirSync(path).forEach((filePath) => {
|
||||
const fullPath = `${path}/${filePath}`;
|
||||
|
||||
encodeAndGenerateFile(fullPath, hashedPassword, salt, baseTemplateData, isRememberEnabled, namedArgs);
|
||||
|
@ -145,7 +151,7 @@ async function encodeAndGenerateFile(path, hashedPassword, salt, baseTemplateDat
|
|||
staticrypt_config: staticryptConfig,
|
||||
};
|
||||
|
||||
const outputFilepath = namedArgs.directory.replace(/\/+$/, '') + "/" + path;
|
||||
const outputFilepath = namedArgs.directory.replace(/\/+$/, "") + "/" + path;
|
||||
|
||||
genFile(templateData, outputFilepath, namedArgs.template);
|
||||
}
|
||||
|
|
|
@ -1,188 +1,188 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html class="staticrypt-html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Protected Page</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Protected Page</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- do not cache this page -->
|
||||
<meta http-equiv="cache-control" content="max-age=0"/>
|
||||
<meta http-equiv="cache-control" content="no-cache"/>
|
||||
<meta http-equiv="expires" content="0"/>
|
||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT"/>
|
||||
<meta http-equiv="pragma" content="no-cache"/>
|
||||
<!-- do not cache this page -->
|
||||
<meta http-equiv="cache-control" content="max-age=0" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
|
||||
<style>
|
||||
.staticrypt-hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.staticrypt-page {
|
||||
width: 360px;
|
||||
padding: 8% 0 0;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.staticrypt-form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: #FFFFFF;
|
||||
max-width: 360px;
|
||||
margin: 0 auto 100px;
|
||||
padding: 45px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.staticrypt-form input[type="password"] {
|
||||
outline: 0;
|
||||
background: #f2f2f2;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0 0 15px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.staticrypt-form .staticrypt-decrypt-button {
|
||||
text-transform: uppercase;
|
||||
outline: 0;
|
||||
background: #4CAF50;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 15px;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.staticrypt-form .staticrypt-decrypt-button:hover, .staticrypt-form .staticrypt-decrypt-button:active, .staticrypt-form .staticrypt-decrypt-button:focus {
|
||||
background: #4CAF50;
|
||||
filter: brightness(92%);
|
||||
}
|
||||
|
||||
.staticrypt-html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.staticrypt-body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.staticrypt-content {
|
||||
height: 100%;
|
||||
margin-bottom: 1em;
|
||||
background: #76B852;
|
||||
font-family: "Arial", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.staticrypt-instructions {
|
||||
margin-top: -1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
label.staticrypt-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-remember input[type=checkbox] {
|
||||
transform: scale(1.5);
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.staticrypt-spinner-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.staticrypt-spinner {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 0.25em solid gray;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: spinner-border .75s linear infinite;
|
||||
animation: spinner-border .75s linear infinite;
|
||||
animation-duration: 0.75s;
|
||||
animation-timing-function: linear;
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: normal;
|
||||
animation-fill-mode: none;
|
||||
animation-play-state: running;
|
||||
animation-name: spinner-border;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
<style>
|
||||
.staticrypt-hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="staticrypt-body">
|
||||
.staticrypt-page {
|
||||
width: 360px;
|
||||
padding: 8% 0 0;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
<div id="staticrypt_loading" class="staticrypt-spinner-container">
|
||||
<div class="staticrypt-spinner"></div>
|
||||
</div>
|
||||
.staticrypt-form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: #ffffff;
|
||||
max-width: 360px;
|
||||
margin: 0 auto 100px;
|
||||
padding: 45px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
<div id="staticrypt_content" class="staticrypt-content hidden">
|
||||
<div class="staticrypt-page">
|
||||
<div class="staticrypt-form">
|
||||
<div class="staticrypt-instructions">
|
||||
<p class="staticrypt-title">Protected Page</p>
|
||||
<p>Enter "test" to unlock the page</p>
|
||||
</div>
|
||||
.staticrypt-form input[type="password"] {
|
||||
outline: 0;
|
||||
background: #f2f2f2;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0 0 15px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
<hr class="staticrypt-hr">
|
||||
.staticrypt-form .staticrypt-decrypt-button {
|
||||
text-transform: uppercase;
|
||||
outline: 0;
|
||||
background: #4CAF50;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 15px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
<form id="staticrypt-form" action="#" method="post">
|
||||
<input id="staticrypt-password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
autofocus/>
|
||||
.staticrypt-form .staticrypt-decrypt-button:hover,
|
||||
.staticrypt-form .staticrypt-decrypt-button:active,
|
||||
.staticrypt-form .staticrypt-decrypt-button:focus {
|
||||
background: #4CAF50;
|
||||
filter: brightness(92%);
|
||||
}
|
||||
|
||||
<label id="staticrypt-remember-label" class="staticrypt-remember hidden">
|
||||
<input id="staticrypt-remember"
|
||||
type="checkbox"
|
||||
name="remember"/>
|
||||
Remember me
|
||||
</label>
|
||||
.staticrypt-html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
<input type="submit" class="staticrypt-decrypt-button" value="DECRYPT"/>
|
||||
</form>
|
||||
.staticrypt-body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.staticrypt-content {
|
||||
height: 100%;
|
||||
margin-bottom: 1em;
|
||||
background: #76B852;
|
||||
font-family: "Arial", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.staticrypt-instructions {
|
||||
margin-top: -1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
label.staticrypt-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-remember input[type="checkbox"] {
|
||||
transform: scale(1.5);
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.staticrypt-spinner-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.staticrypt-spinner {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 0.25em solid gray;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: spinner-border 0.75s linear infinite;
|
||||
animation: spinner-border 0.75s linear infinite;
|
||||
animation-duration: 0.75s;
|
||||
animation-timing-function: linear;
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: normal;
|
||||
animation-fill-mode: none;
|
||||
animation-play-state: running;
|
||||
animation-name: spinner-border;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="staticrypt-body">
|
||||
<div id="staticrypt_loading" class="staticrypt-spinner-container">
|
||||
<div class="staticrypt-spinner"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="staticrypt_content" class="staticrypt-content hidden">
|
||||
<div class="staticrypt-page">
|
||||
<div class="staticrypt-form">
|
||||
<div class="staticrypt-instructions">
|
||||
<p class="staticrypt-title">Protected Page</p>
|
||||
<p>Enter "test" to unlock the page</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// these variables will be filled when generating the file - the template format is 'variable_name'
|
||||
const staticryptInitiator = ((function(){
|
||||
<hr class="staticrypt-hr" />
|
||||
|
||||
<form id="staticrypt-form" action="#" method="post">
|
||||
<input
|
||||
id="staticrypt-password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<label id="staticrypt-remember-label" class="staticrypt-remember hidden">
|
||||
<input id="staticrypt-remember" type="checkbox" name="remember" />
|
||||
Remember me
|
||||
</label>
|
||||
|
||||
<input type="submit" class="staticrypt-decrypt-button" value="DECRYPT" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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 = {};
|
||||
|
@ -256,13 +256,7 @@ async function encrypt(msg, hashedPassword) {
|
|||
// 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(hashedPassword),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
const key = await subtle.importKey("raw", HexEncoder.parse(hashedPassword), ENCRYPTION_ALGO, false, ["encrypt"]);
|
||||
|
||||
const encrypted = await subtle.encrypt(
|
||||
{
|
||||
|
@ -290,13 +284,7 @@ async function decrypt(encryptedMsg, hashedPassword) {
|
|||
const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
|
||||
const encrypted = encryptedMsg.substring(ivLength);
|
||||
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassword),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
const key = await subtle.importKey("raw", HexEncoder.parse(hashedPassword), ENCRYPTION_ALGO, false, ["decrypt"]);
|
||||
|
||||
const outBuffer = await subtle.decrypt(
|
||||
{
|
||||
|
@ -378,13 +366,7 @@ exports.hashThirdRound = hashThirdRound;
|
|||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function pbkdf2(password, salt, iterations, hashAlgorithm) {
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
UTF8Encoder.parse(password),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
const key = await subtle.importKey("raw", UTF8Encoder.parse(password), "PBKDF2", false, ["deriveBits"]);
|
||||
|
||||
const keyBytes = await subtle.deriveBits(
|
||||
{
|
||||
|
@ -424,7 +406,6 @@ async function signMessage(hashedPassword, message) {
|
|||
}
|
||||
exports.signMessage = signMessage;
|
||||
|
||||
|
||||
function getRandomAlphanum() {
|
||||
const possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
|
@ -452,7 +433,7 @@ function getRandomAlphanum() {
|
|||
* @returns {string}
|
||||
*/
|
||||
function generateRandomString(length) {
|
||||
let randomString = '';
|
||||
let randomString = "";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
randomString += getRandomAlphanum();
|
||||
|
@ -462,9 +443,8 @@ function generateRandomString(length) {
|
|||
}
|
||||
exports.generateRandomString = generateRandomString;
|
||||
|
||||
|
||||
return exports;
|
||||
})())
|
||||
})());
|
||||
const codec = ((function(){
|
||||
const exports = {};
|
||||
/**
|
||||
|
@ -473,109 +453,102 @@ const codec = ((function(){
|
|||
* @param cryptoEngine - the engine to use for encryption / decryption
|
||||
*/
|
||||
function init(cryptoEngine) {
|
||||
const exports = {};
|
||||
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 hashedPassword = await cryptoEngine.hashPassword(password, salt);
|
||||
/**
|
||||
* 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 hashedPassword = await cryptoEngine.hashPassword(password, salt);
|
||||
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
|
||||
// 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(hashedPassword, encrypted);
|
||||
// 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(hashedPassword, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encode = encode;
|
||||
|
||||
/**
|
||||
* Encode using a password that has already been hashed. This is useful to encode multiple messages in a row, that way
|
||||
* we don't need to hash the password multiple times.
|
||||
*
|
||||
* @param {string} msg
|
||||
* @param {string} hashedPassword
|
||||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
async function encodeWithHashedPassword(msg, hashedPassword) {
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
|
||||
// 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(hashedPassword, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encodeWithHashedPassword = encodeWithHashedPassword;
|
||||
|
||||
/**
|
||||
* Top-level function for decoding a message.
|
||||
* Includes signature check and decryption.
|
||||
*
|
||||
* @param {string} signedMsg
|
||||
* @param {string} hashedPassword
|
||||
* @param {string} salt
|
||||
* @param {int} backwardCompatibleAttempt
|
||||
* @param {string} originalPassword
|
||||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
async function decode(
|
||||
signedMsg,
|
||||
hashedPassword,
|
||||
salt,
|
||||
backwardCompatibleAttempt = 0,
|
||||
originalPassword = ''
|
||||
) {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = await cryptoEngine.signMessage(hashedPassword, 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.
|
||||
originalPassword = originalPassword || hashedPassword;
|
||||
if (backwardCompatibleAttempt === 0) {
|
||||
const updatedHashedPassword = await cryptoEngine.hashThirdRound(originalPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
if (backwardCompatibleAttempt === 1) {
|
||||
let updatedHashedPassword = await cryptoEngine.hashSecondRound(originalPassword, salt);
|
||||
updatedHashedPassword = await cryptoEngine.hashThirdRound(updatedHashedPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
|
||||
return { success: false, message: "Signature mismatch" };
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encode = encode;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassword),
|
||||
};
|
||||
}
|
||||
exports.decode = decode;
|
||||
/**
|
||||
* Encode using a password that has already been hashed. This is useful to encode multiple messages in a row, that way
|
||||
* we don't need to hash the password multiple times.
|
||||
*
|
||||
* @param {string} msg
|
||||
* @param {string} hashedPassword
|
||||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
async function encodeWithHashedPassword(msg, hashedPassword) {
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
|
||||
return exports;
|
||||
// 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(hashedPassword, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encodeWithHashedPassword = encodeWithHashedPassword;
|
||||
|
||||
/**
|
||||
* Top-level function for decoding a message.
|
||||
* Includes signature check and decryption.
|
||||
*
|
||||
* @param {string} signedMsg
|
||||
* @param {string} hashedPassword
|
||||
* @param {string} salt
|
||||
* @param {int} backwardCompatibleAttempt
|
||||
* @param {string} originalPassword
|
||||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
async function decode(signedMsg, hashedPassword, salt, backwardCompatibleAttempt = 0, originalPassword = "") {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = await cryptoEngine.signMessage(hashedPassword, 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.
|
||||
originalPassword = originalPassword || hashedPassword;
|
||||
if (backwardCompatibleAttempt === 0) {
|
||||
const updatedHashedPassword = await cryptoEngine.hashThirdRound(originalPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
if (backwardCompatibleAttempt === 1) {
|
||||
let updatedHashedPassword = await cryptoEngine.hashSecondRound(originalPassword, salt);
|
||||
updatedHashedPassword = await cryptoEngine.hashThirdRound(updatedHashedPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
|
||||
return { success: false, message: "Signature mismatch" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassword),
|
||||
};
|
||||
}
|
||||
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.
|
||||
*
|
||||
|
@ -613,7 +586,7 @@ function init(staticryptConfig, templateConfig) {
|
|||
const plainHTML = result.decoded;
|
||||
|
||||
// if the user configured a callback call it, otherwise just replace the whole HTML
|
||||
if (typeof replaceHtmlCallback === 'function') {
|
||||
if (typeof replaceHtmlCallback === "function") {
|
||||
replaceHtmlCallback(plainHTML);
|
||||
} else {
|
||||
document.write(plainHTML);
|
||||
|
@ -674,7 +647,7 @@ function init(staticryptConfig, templateConfig) {
|
|||
function clearLocalStorage() {
|
||||
const { clearLocalStorageCallback, rememberExpirationKey, rememberPassphraseKey } = templateConfig;
|
||||
|
||||
if (typeof clearLocalStorageCallback === 'function') {
|
||||
if (typeof clearLocalStorageCallback === "function") {
|
||||
clearLocalStorageCallback();
|
||||
} else {
|
||||
localStorage.removeItem(rememberPassphraseKey);
|
||||
|
@ -786,55 +759,56 @@ function init(staticryptConfig, templateConfig) {
|
|||
return exports;
|
||||
}
|
||||
exports.init = init;
|
||||
|
||||
return exports;
|
||||
})())
|
||||
const templateError = 'Bad password!',
|
||||
isRememberEnabled = true,
|
||||
staticryptConfig = {"encryptedMsg":"eaad887f76cba7e862577a8d69c79acfa062d4a1af097faf1fa2d5c56ca2383ea696b3a2f62c6507bebe2f6497d84bcbf7f989136c9ea7f6a95191c1da90a92c330615d9643f382beb9bc3eecaef67a1e2243913e3bd7165820eef4518ac95287e12c1af22bcdaf6190167da3215800ca6809cf5b011847b4618f36e53b40b39d77d174b52a5c8ffaf47f9309478f0d3739f29d606ebe5a2a2334bf6c0f06ed53cdf7f4d703a4a97a41e47b34c50ac4103b7c4a8f0ba92f31010fe097f57bf1c","isRememberEnabled":true,"rememberDurationInDays":0,"salt":"b93bbaf35459951c47721d1f3eaeb5b9"};
|
||||
})());
|
||||
const templateError = "Bad password!",
|
||||
isRememberEnabled = true,
|
||||
staticryptConfig = {"encryptedMsg":"bf0f821fe4fdab54ffde29fd6d812fdf7dd423a0e4e36369c41864d369b71aa6a826caf47e563ba5e4bdfc2b55ea323c5d9fabc6ece3e99cdacc30afd560ec57b6bdda0beef1b2b220e934f214202134e8eb284a5a58f94418c970ca2172622b9286eb5931fff69e345d737c00832ecfbb77057a4e814dd633ab1ef294514d28ef7cb047bb541000b12b8f4c15851fa8ca1e6cd0cbee64c67b4467a46e2bc154896bb4deed9987e222f0c280c8d766ea16769515f9d337592e6807dfa07534ef","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,
|
||||
};
|
||||
// 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);
|
||||
// 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 () {
|
||||
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
|
||||
// try to automatically decrypt on load if there is a saved password
|
||||
window.onload = async function () {
|
||||
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
|
||||
|
||||
// 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();
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
// show the remember me checkbox
|
||||
if (isRememberEnabled) {
|
||||
document.getElementById("staticrypt-remember-label").classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// handle password form submission
|
||||
document.getElementById('staticrypt-form').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
// handle password form submission
|
||||
document.getElementById("staticrypt-form").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = document.getElementById('staticrypt-password').value,
|
||||
isRememberChecked = document.getElementById('staticrypt-remember').checked;
|
||||
const password = document.getElementById("staticrypt-password").value,
|
||||
isRememberChecked = document.getElementById("staticrypt-remember").checked;
|
||||
|
||||
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(password, isRememberChecked);
|
||||
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(password, isRememberChecked);
|
||||
|
||||
if (!isSuccessful) {
|
||||
alert(templateError);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
if (!isSuccessful) {
|
||||
alert(templateError);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
170
lib/codec.js
170
lib/codec.js
|
@ -4,100 +4,94 @@
|
|||
* @param cryptoEngine - the engine to use for encryption / decryption
|
||||
*/
|
||||
function init(cryptoEngine) {
|
||||
const exports = {};
|
||||
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 hashedPassword = await cryptoEngine.hashPassword(password, salt);
|
||||
/**
|
||||
* 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 hashedPassword = await cryptoEngine.hashPassword(password, salt);
|
||||
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
|
||||
// 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(hashedPassword, encrypted);
|
||||
// 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(hashedPassword, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encode = encode;
|
||||
|
||||
/**
|
||||
* Encode using a password that has already been hashed. This is useful to encode multiple messages in a row, that way
|
||||
* we don't need to hash the password multiple times.
|
||||
*
|
||||
* @param {string} msg
|
||||
* @param {string} hashedPassword
|
||||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
async function encodeWithHashedPassword(msg, hashedPassword) {
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
|
||||
// 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(hashedPassword, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encodeWithHashedPassword = encodeWithHashedPassword;
|
||||
|
||||
/**
|
||||
* Top-level function for decoding a message.
|
||||
* Includes signature check and decryption.
|
||||
*
|
||||
* @param {string} signedMsg
|
||||
* @param {string} hashedPassword
|
||||
* @param {string} salt
|
||||
* @param {int} backwardCompatibleAttempt
|
||||
* @param {string} originalPassword
|
||||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
async function decode(
|
||||
signedMsg,
|
||||
hashedPassword,
|
||||
salt,
|
||||
backwardCompatibleAttempt = 0,
|
||||
originalPassword = ''
|
||||
) {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = await cryptoEngine.signMessage(hashedPassword, 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.
|
||||
originalPassword = originalPassword || hashedPassword;
|
||||
if (backwardCompatibleAttempt === 0) {
|
||||
const updatedHashedPassword = await cryptoEngine.hashThirdRound(originalPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
if (backwardCompatibleAttempt === 1) {
|
||||
let updatedHashedPassword = await cryptoEngine.hashSecondRound(originalPassword, salt);
|
||||
updatedHashedPassword = await cryptoEngine.hashThirdRound(updatedHashedPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
|
||||
return { success: false, message: "Signature mismatch" };
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encode = encode;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassword),
|
||||
};
|
||||
}
|
||||
exports.decode = decode;
|
||||
/**
|
||||
* Encode using a password that has already been hashed. This is useful to encode multiple messages in a row, that way
|
||||
* we don't need to hash the password multiple times.
|
||||
*
|
||||
* @param {string} msg
|
||||
* @param {string} hashedPassword
|
||||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
async function encodeWithHashedPassword(msg, hashedPassword) {
|
||||
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
|
||||
|
||||
return exports;
|
||||
// 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(hashedPassword, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
exports.encodeWithHashedPassword = encodeWithHashedPassword;
|
||||
|
||||
/**
|
||||
* Top-level function for decoding a message.
|
||||
* Includes signature check and decryption.
|
||||
*
|
||||
* @param {string} signedMsg
|
||||
* @param {string} hashedPassword
|
||||
* @param {string} salt
|
||||
* @param {int} backwardCompatibleAttempt
|
||||
* @param {string} originalPassword
|
||||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
async function decode(signedMsg, hashedPassword, salt, backwardCompatibleAttempt = 0, originalPassword = "") {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = await cryptoEngine.signMessage(hashedPassword, 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.
|
||||
originalPassword = originalPassword || hashedPassword;
|
||||
if (backwardCompatibleAttempt === 0) {
|
||||
const updatedHashedPassword = await cryptoEngine.hashThirdRound(originalPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
if (backwardCompatibleAttempt === 1) {
|
||||
let updatedHashedPassword = await cryptoEngine.hashSecondRound(originalPassword, salt);
|
||||
updatedHashedPassword = await cryptoEngine.hashThirdRound(updatedHashedPassword, salt);
|
||||
|
||||
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
|
||||
}
|
||||
|
||||
return { success: false, message: "Signature mismatch" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassword),
|
||||
};
|
||||
}
|
||||
exports.decode = decode;
|
||||
|
||||
return exports;
|
||||
}
|
||||
exports.init = init;
|
||||
|
|
|
@ -69,13 +69,7 @@ async function encrypt(msg, hashedPassword) {
|
|||
// 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(hashedPassword),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
const key = await subtle.importKey("raw", HexEncoder.parse(hashedPassword), ENCRYPTION_ALGO, false, ["encrypt"]);
|
||||
|
||||
const encrypted = await subtle.encrypt(
|
||||
{
|
||||
|
@ -103,13 +97,7 @@ async function decrypt(encryptedMsg, hashedPassword) {
|
|||
const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
|
||||
const encrypted = encryptedMsg.substring(ivLength);
|
||||
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassword),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
const key = await subtle.importKey("raw", HexEncoder.parse(hashedPassword), ENCRYPTION_ALGO, false, ["decrypt"]);
|
||||
|
||||
const outBuffer = await subtle.decrypt(
|
||||
{
|
||||
|
@ -191,13 +179,7 @@ exports.hashThirdRound = hashThirdRound;
|
|||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function pbkdf2(password, salt, iterations, hashAlgorithm) {
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
UTF8Encoder.parse(password),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
const key = await subtle.importKey("raw", UTF8Encoder.parse(password), "PBKDF2", false, ["deriveBits"]);
|
||||
|
||||
const keyBytes = await subtle.deriveBits(
|
||||
{
|
||||
|
@ -237,7 +219,6 @@ async function signMessage(hashedPassword, message) {
|
|||
}
|
||||
exports.signMessage = signMessage;
|
||||
|
||||
|
||||
function getRandomAlphanum() {
|
||||
const possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
|
@ -265,7 +246,7 @@ function getRandomAlphanum() {
|
|||
* @returns {string}
|
||||
*/
|
||||
function generateRandomString(length) {
|
||||
let randomString = '';
|
||||
let randomString = "";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
randomString += getRandomAlphanum();
|
||||
|
@ -274,4 +255,3 @@ function generateRandomString(length) {
|
|||
return randomString;
|
||||
}
|
||||
exports.generateRandomString = generateRandomString;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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.
|
||||
* as '0'), so we can still use auto-formatting. The auto-formatter might add a space before the '0', we accept both.
|
||||
*
|
||||
* @param {string} templateString
|
||||
* @param {Object} data
|
||||
|
@ -11,12 +11,12 @@
|
|||
* @returns string
|
||||
*/
|
||||
function renderTemplate(templateString, data) {
|
||||
return templateString.replace(/\/\*\[\|\s*(\w+)\s*\|]\*\/0/g, function (_, key) {
|
||||
return templateString.replace(/\/\*\[\|\s*(\w+)\s*\|]\*\/\s*0/g, function (_, key) {
|
||||
if (!data || data[key] === undefined) {
|
||||
return key;
|
||||
}
|
||||
|
||||
if (typeof data[key] === 'object') {
|
||||
if (typeof data[key] === "object") {
|
||||
return JSON.stringify(data[key]);
|
||||
}
|
||||
|
||||
|
@ -24,4 +24,3 @@ function renderTemplate(templateString, data) {
|
|||
});
|
||||
}
|
||||
exports.renderTemplate = renderTemplate;
|
||||
|
||||
|
|
|
@ -1,235 +1,235 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html class="staticrypt-html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>/*[|template_title|]*/0</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>/*[|template_title|]*/0</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- do not cache this page -->
|
||||
<meta http-equiv="cache-control" content="max-age=0"/>
|
||||
<meta http-equiv="cache-control" content="no-cache"/>
|
||||
<meta http-equiv="expires" content="0"/>
|
||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT"/>
|
||||
<meta http-equiv="pragma" content="no-cache"/>
|
||||
<!-- do not cache this page -->
|
||||
<meta http-equiv="cache-control" content="max-age=0" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
|
||||
<style>
|
||||
.staticrypt-hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.staticrypt-page {
|
||||
width: 360px;
|
||||
padding: 8% 0 0;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.staticrypt-form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: #FFFFFF;
|
||||
max-width: 360px;
|
||||
margin: 0 auto 100px;
|
||||
padding: 45px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.staticrypt-form input[type="password"] {
|
||||
outline: 0;
|
||||
background: #f2f2f2;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0 0 15px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.staticrypt-form .staticrypt-decrypt-button {
|
||||
text-transform: uppercase;
|
||||
outline: 0;
|
||||
background: /*[|template_color_primary|]*/0;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 15px;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.staticrypt-form .staticrypt-decrypt-button:hover, .staticrypt-form .staticrypt-decrypt-button:active, .staticrypt-form .staticrypt-decrypt-button:focus {
|
||||
background: /*[|template_color_primary|]*/0;
|
||||
filter: brightness(92%);
|
||||
}
|
||||
|
||||
.staticrypt-html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.staticrypt-body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.staticrypt-content {
|
||||
height: 100%;
|
||||
margin-bottom: 1em;
|
||||
background: /*[|template_color_secondary|]*/0;
|
||||
font-family: "Arial", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.staticrypt-instructions {
|
||||
margin-top: -1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
label.staticrypt-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-remember input[type=checkbox] {
|
||||
transform: scale(1.5);
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.staticrypt-spinner-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.staticrypt-spinner {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 0.25em solid gray;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: spinner-border .75s linear infinite;
|
||||
animation: spinner-border .75s linear infinite;
|
||||
animation-duration: 0.75s;
|
||||
animation-timing-function: linear;
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: normal;
|
||||
animation-fill-mode: none;
|
||||
animation-play-state: running;
|
||||
animation-name: spinner-border;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
<style>
|
||||
.staticrypt-hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="staticrypt-body">
|
||||
.staticrypt-page {
|
||||
width: 360px;
|
||||
padding: 8% 0 0;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
<div id="staticrypt_loading" class="staticrypt-spinner-container">
|
||||
<div class="staticrypt-spinner"></div>
|
||||
</div>
|
||||
.staticrypt-form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: #ffffff;
|
||||
max-width: 360px;
|
||||
margin: 0 auto 100px;
|
||||
padding: 45px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
<div id="staticrypt_content" class="staticrypt-content hidden">
|
||||
<div class="staticrypt-page">
|
||||
<div class="staticrypt-form">
|
||||
<div class="staticrypt-instructions">
|
||||
<p class="staticrypt-title">/*[|template_title|]*/0</p>
|
||||
<p>/*[|template_instructions|]*/0</p>
|
||||
</div>
|
||||
.staticrypt-form input[type="password"] {
|
||||
outline: 0;
|
||||
background: #f2f2f2;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0 0 15px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
<hr class="staticrypt-hr">
|
||||
.staticrypt-form .staticrypt-decrypt-button {
|
||||
text-transform: uppercase;
|
||||
outline: 0;
|
||||
background: /*[|template_color_primary|]*/ 0;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 15px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
<form id="staticrypt-form" action="#" method="post">
|
||||
<input id="staticrypt-password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="/*[|template_placeholder|]*/0"
|
||||
autofocus/>
|
||||
.staticrypt-form .staticrypt-decrypt-button:hover,
|
||||
.staticrypt-form .staticrypt-decrypt-button:active,
|
||||
.staticrypt-form .staticrypt-decrypt-button:focus {
|
||||
background: /*[|template_color_primary|]*/ 0;
|
||||
filter: brightness(92%);
|
||||
}
|
||||
|
||||
<label id="staticrypt-remember-label" class="staticrypt-remember hidden">
|
||||
<input id="staticrypt-remember"
|
||||
type="checkbox"
|
||||
name="remember"/>
|
||||
/*[|template_remember|]*/0
|
||||
</label>
|
||||
.staticrypt-html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
<input type="submit" class="staticrypt-decrypt-button" value="/*[|template_button|]*/0"/>
|
||||
</form>
|
||||
.staticrypt-body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.staticrypt-content {
|
||||
height: 100%;
|
||||
margin-bottom: 1em;
|
||||
background: /*[|template_color_secondary|]*/ 0;
|
||||
font-family: "Arial", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.staticrypt-instructions {
|
||||
margin-top: -1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
label.staticrypt-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.staticrypt-remember input[type="checkbox"] {
|
||||
transform: scale(1.5);
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.staticrypt-spinner-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.staticrypt-spinner {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 0.25em solid gray;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: spinner-border 0.75s linear infinite;
|
||||
animation: spinner-border 0.75s linear infinite;
|
||||
animation-duration: 0.75s;
|
||||
animation-timing-function: linear;
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: normal;
|
||||
animation-fill-mode: none;
|
||||
animation-play-state: running;
|
||||
animation-name: spinner-border;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="staticrypt-body">
|
||||
<div id="staticrypt_loading" class="staticrypt-spinner-container">
|
||||
<div class="staticrypt-spinner"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="staticrypt_content" class="staticrypt-content hidden">
|
||||
<div class="staticrypt-page">
|
||||
<div class="staticrypt-form">
|
||||
<div class="staticrypt-instructions">
|
||||
<p class="staticrypt-title">/*[|template_title|]*/0</p>
|
||||
<p>/*[|template_instructions|]*/0</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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;
|
||||
<hr class="staticrypt-hr" />
|
||||
|
||||
// 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,
|
||||
};
|
||||
<form id="staticrypt-form" action="#" method="post">
|
||||
<input
|
||||
id="staticrypt-password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="/*[|template_placeholder|]*/0"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
// init the staticrypt engine
|
||||
const staticrypt = staticryptInitiator.init(staticryptConfig, templateConfig);
|
||||
<label id="staticrypt-remember-label" class="staticrypt-remember hidden">
|
||||
<input id="staticrypt-remember" type="checkbox" name="remember" />
|
||||
/*[|template_remember|]*/0
|
||||
</label>
|
||||
|
||||
// try to automatically decrypt on load if there is a saved password
|
||||
window.onload = async function () {
|
||||
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
|
||||
<input type="submit" class="staticrypt-decrypt-button" value="/*[|template_button|]*/0" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// 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();
|
||||
<script>
|
||||
// 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;
|
||||
|
||||
// show the remember me checkbox
|
||||
if (isRememberEnabled) {
|
||||
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
};
|
||||
|
||||
// handle password form submission
|
||||
document.getElementById('staticrypt-form').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
// init the staticrypt engine
|
||||
const staticrypt = staticryptInitiator.init(staticryptConfig, templateConfig);
|
||||
|
||||
const password = document.getElementById('staticrypt-password').value,
|
||||
isRememberChecked = document.getElementById('staticrypt-remember').checked;
|
||||
// try to automatically decrypt on load if there is a saved password
|
||||
window.onload = async function () {
|
||||
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
|
||||
|
||||
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(password, isRememberChecked);
|
||||
// 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();
|
||||
|
||||
if (!isSuccessful) {
|
||||
alert(templateError);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
// show the remember me checkbox
|
||||
if (isRememberEnabled) {
|
||||
document.getElementById("staticrypt-remember-label").classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// handle password form submission
|
||||
document.getElementById("staticrypt-form").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = document.getElementById("staticrypt-password").value,
|
||||
isRememberChecked = document.getElementById("staticrypt-remember").checked;
|
||||
|
||||
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(password, isRememberChecked);
|
||||
|
||||
if (!isSuccessful) {
|
||||
alert(templateError);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
const cryptoEngine = /*[|js_crypto_engine|]*/0
|
||||
const codec = /*[|js_codec|]*/0
|
||||
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.
|
||||
*
|
||||
|
@ -40,7 +39,7 @@ function init(staticryptConfig, templateConfig) {
|
|||
const plainHTML = result.decoded;
|
||||
|
||||
// if the user configured a callback call it, otherwise just replace the whole HTML
|
||||
if (typeof replaceHtmlCallback === 'function') {
|
||||
if (typeof replaceHtmlCallback === "function") {
|
||||
replaceHtmlCallback(plainHTML);
|
||||
} else {
|
||||
document.write(plainHTML);
|
||||
|
@ -101,7 +100,7 @@ function init(staticryptConfig, templateConfig) {
|
|||
function clearLocalStorage() {
|
||||
const { clearLocalStorageCallback, rememberExpirationKey, rememberPassphraseKey } = templateConfig;
|
||||
|
||||
if (typeof clearLocalStorageCallback === 'function') {
|
||||
if (typeof clearLocalStorageCallback === "function") {
|
||||
clearLocalStorageCallback();
|
||||
} else {
|
||||
localStorage.removeItem(rememberPassphraseKey);
|
||||
|
@ -212,4 +211,4 @@ function init(staticryptConfig, templateConfig) {
|
|||
|
||||
return exports;
|
||||
}
|
||||
exports.init = init;
|
||||
exports.init = init;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { convertCommonJSToBrowserJS, genFile, buildStaticryptJS} = require("../cli/helpers.js");
|
||||
const { convertCommonJSToBrowserJS, genFile, buildStaticryptJS } = require("../cli/helpers.js");
|
||||
|
||||
const data = {
|
||||
js_codec: convertCommonJSToBrowserJS("lib/codec"),
|
||||
|
|
|
@ -1,358 +1,421 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>StatiCrypt: Password protect static HTML</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
|
||||
crossorigin="anonymous">
|
||||
<style>
|
||||
a.no-style {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>StatiCrypt: Password protect static HTML</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<style>
|
||||
a.no-style {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
label.no-style {
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
label.no-style {
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h1>
|
||||
StatiCrypt
|
||||
<div class="pull-right">
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=robinmoisson&repo=staticrypt&type=star&size=large"
|
||||
frameborder="0" scrolling="0" width="80px" height="30px"></iframe>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=robinmoisson&repo=staticrypt&type=fork&size=large"
|
||||
frameborder="0" scrolling="0" width="80px" height="30px"></iframe>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h1>
|
||||
StatiCrypt
|
||||
<div class="pull-right">
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=robinmoisson&repo=staticrypt&type=star&size=large"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="80px"
|
||||
height="30px"
|
||||
></iframe>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=robinmoisson&repo=staticrypt&type=fork&size=large"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="80px"
|
||||
height="30px"
|
||||
></iframe>
|
||||
</div>
|
||||
<br />
|
||||
<small>Password protect a static HTML page</small>
|
||||
</h1>
|
||||
<p>
|
||||
StatiCrypt uses AES-256 with WebCrypto to encrypt your html string with your long password, in
|
||||
your browser (client side).
|
||||
</p>
|
||||
<p>
|
||||
Download your encrypted string in a HTML page with a password prompt you can upload anywhere
|
||||
(see <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 href="https://github.com/robinmoisson/staticrypt">open source on GitHub</a>.
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<h4>
|
||||
<a class="no-style" id="toggle-concept" href="#">
|
||||
<span id="toggle-concept-sign">►</span> HOW IT WORKS
|
||||
</a>
|
||||
</h4>
|
||||
<div id="concept" class="hidden">
|
||||
<p>
|
||||
<b class="text-danger">Disclaimer</b> if you are an at-risk activist, or have extra
|
||||
sensitive banking data, you should probably use something else!
|
||||
</p>
|
||||
<p>
|
||||
StatiCrypt generates a static, password protected page that can be decrypted in-browser:
|
||||
just send or upload the generated page to a place serving static content (github pages, for
|
||||
example) and you're done: the javascript will prompt users for password, decrypt the page
|
||||
and load your HTML.
|
||||
</p>
|
||||
<p>
|
||||
The page is encrypted with AES-256 in CBC mode (see why this mode is appropriate for
|
||||
StatiCrypt in
|
||||
<a href="https://github.com/robinmoisson/staticrypt/issues/19">#19</a>). The password is
|
||||
hashed with PBKDF2 (599k iterations with SHA-256, plus 1k with SHA-1 for legacy reasons (see
|
||||
<a href="https://github.com/robinmoisson/staticrypt/issues/159">#159</a>), for the added
|
||||
<a
|
||||
href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2"
|
||||
>recommended total</a
|
||||
>
|
||||
of 600k) and used to encrypt the page.
|
||||
</p>
|
||||
<p>
|
||||
It basically encrypts your page and puts everything with a user-friendly way to use a
|
||||
password in the new file. AES-256 is state of the art but
|
||||
<b
|
||||
>brute-force/dictionary attacks would be easy to do at a really fast pace: use a long,
|
||||
unusual password!</b
|
||||
>
|
||||
<br />
|
||||
=> To be safe, we recommend 16+ alphanum characters, and using a password manager like the
|
||||
open-source <a href="http://bitwarden.com">Bitwarden</a>.
|
||||
</p>
|
||||
<p>
|
||||
Feel free to contribute or report any thought to the
|
||||
<a href="https://github.com/robinmoisson/staticrypt">GitHub project</a>.
|
||||
</p>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<form id="encrypt_form">
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder="Password (choose a long one!)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="unencrypted_html">HTML/string to encrypt</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="unencrypted_html"
|
||||
placeholder="<html><head>..."
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="no-style">
|
||||
<input type="checkbox" id="remember" checked />
|
||||
Add "Remember me" checkbox (append <code>#staticrypt_logout</code> to your URL to
|
||||
logout)
|
||||
<small>
|
||||
<abbr
|
||||
class="text-muted"
|
||||
title='The password will be stored in clear text in the browser's localStorage upon entry by the user. See "More options" to set the expiration (default: none)'
|
||||
>
|
||||
(?)
|
||||
</abbr>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a href="#" id="toggle-extra-option">+ More options</a>
|
||||
</p>
|
||||
<div id="extra-options" class="hidden">
|
||||
<div class="form-group">
|
||||
<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="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="template_placeholder">Password input placeholder</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="template_placeholder"
|
||||
placeholder="Default: 'Password'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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">
|
||||
<label for="remember_in_days">"Remember me" expiration in days</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="remember_in_days"
|
||||
step="any"
|
||||
placeholder="Default: 0 (no expiration)"
|
||||
/>
|
||||
<small class="form-text text-muted">
|
||||
After this many days, the user will have to enter the password again. Leave empty or
|
||||
set to 0 for no expiration.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_button">Decrypt button label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="template_button"
|
||||
placeholder="Default: 'DECRYPT'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary pull-right" type="submit">
|
||||
Generate password protected HTML
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-xs-12">
|
||||
<h2>Encrypted HTML</h2>
|
||||
<p>
|
||||
<a
|
||||
class="btn btn-success download"
|
||||
download="encrypted.html"
|
||||
id="download-link"
|
||||
disabled="disabled"
|
||||
>Download html file with password prompt</a
|
||||
>
|
||||
</p>
|
||||
<pre id="encrypted_html_display">Your encrypted string</pre>
|
||||
</div>
|
||||
<br>
|
||||
<small>Password protect a static HTML page</small>
|
||||
</h1>
|
||||
<p>
|
||||
StatiCrypt uses AES-256 with WebCrypto to encrypt your html string with your long password, in your browser (client side).
|
||||
</p>
|
||||
<p>
|
||||
Download your encrypted string in a HTML page with a password prompt you can upload anywhere (see <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
|
||||
href="https://github.com/robinmoisson/staticrypt">open source on GitHub</a>.
|
||||
</p>
|
||||
<br>
|
||||
|
||||
<h4>
|
||||
<a class="no-style" id="toggle-concept" href="#">
|
||||
<span id="toggle-concept-sign">►</span> HOW IT WORKS
|
||||
</a>
|
||||
</h4>
|
||||
<div id="concept" class="hidden">
|
||||
<p>
|
||||
<b class="text-danger">Disclaimer</b> if you are an at-risk activist, or have extra sensitive
|
||||
banking data, you should probably use something else!
|
||||
</p>
|
||||
<p>
|
||||
StatiCrypt generates a static, password protected page that can be decrypted in-browser:
|
||||
just send or upload the generated page to a place serving static content (github pages, for example)
|
||||
and you're done: the javascript will prompt users for password, decrypt the page and load your HTML.
|
||||
</p>
|
||||
<p>
|
||||
The page is encrypted with AES-256 in CBC mode (see why this mode is appropriate for StatiCrypt in
|
||||
<a href="https://github.com/robinmoisson/staticrypt/issues/19">#19</a>). The password is hashed with
|
||||
PBKDF2 (599k iterations with SHA-256, plus 1k with SHA-1 for legacy reasons (see
|
||||
<a href="https://github.com/robinmoisson/staticrypt/issues/159">#159</a>), for the added
|
||||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2">recommended
|
||||
total</a> of 600k) and used to encrypt the page.
|
||||
</p>
|
||||
<p>
|
||||
It basically encrypts your page and puts everything with a user-friendly way to use a password
|
||||
in the new file. AES-256 is state of the art but <b>brute-force/dictionary attacks would be easy to
|
||||
do at a really fast pace: use a long, unusual password!</b>
|
||||
<br/> => To be safe, we recommend 16+ alphanum characters, and using a password manager like the
|
||||
open-source <a href="http://bitwarden.com">Bitwarden</a>.
|
||||
</p>
|
||||
<p>
|
||||
Feel free to contribute or report any thought to the
|
||||
<a href="https://github.com/robinmoisson/staticrypt">GitHub project</a>.
|
||||
</p>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<form id="encrypt_form">
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password"
|
||||
placeholder="Password (choose a long one!)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="unencrypted_html">HTML/string to encrypt</label>
|
||||
<textarea class="form-control"
|
||||
id="unencrypted_html"
|
||||
placeholder="<html><head>..."
|
||||
rows="5"></textarea>
|
||||
</div>
|
||||
<script src="https://cdn.ckeditor.com/4.7.0/standard/ckeditor.js"></script>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="no-style">
|
||||
<input type="checkbox" id="remember" checked>
|
||||
Add "Remember me" checkbox (append <code>#staticrypt_logout</code> to your URL to logout)
|
||||
<small>
|
||||
<abbr class="text-muted"
|
||||
title="The password will be stored in clear text in the browser's localStorage upon entry by the user. See "More options" to set the expiration (default: none)">
|
||||
(?)
|
||||
</abbr>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<script id="cryptoEngine">
|
||||
window.cryptoEngine = /*[|js_crypto_engine|]*/ 0;
|
||||
</script>
|
||||
|
||||
<p>
|
||||
<a href="#" id="toggle-extra-option">+ More options</a>
|
||||
</p>
|
||||
<div id="extra-options" class="hidden">
|
||||
<div class="form-group">
|
||||
<label for="template_title">Page title</label>
|
||||
<input type="text" class="form-control" id="template_title" placeholder="Default: 'Protected Page'">
|
||||
</div>
|
||||
<script id="codec">
|
||||
window.codec = /*[|js_codec|]*/ 0;
|
||||
</script>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_instructions">Instructions to display the user</label>
|
||||
<textarea class="form-control" id="template_instructions" placeholder="Default: nothing."></textarea>
|
||||
</div>
|
||||
<script id="formater">
|
||||
window.formater = /*[|js_formater|]*/ 0;
|
||||
</script>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_placeholder">Password input placeholder</label>
|
||||
<input type="text" class="form-control" id="template_placeholder"
|
||||
placeholder="Default: 'Password'">
|
||||
</div>
|
||||
<script id="staticrypt">
|
||||
window.staticrypt = /*[|js_staticrypt|]*/ 0;
|
||||
</script>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_remember">"Remember me" checkbox label</label>
|
||||
<input type="text" class="form-control" id="template_remember" placeholder="Default: 'Remember me'">
|
||||
</div>
|
||||
<script>
|
||||
const encode = codec.init(cryptoEngine).encode;
|
||||
|
||||
<div class="form-group">
|
||||
<label for="remember_in_days">"Remember me" expiration in days</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="remember_in_days"
|
||||
step="any"
|
||||
placeholder="Default: 0 (no expiration)">
|
||||
<small class="form-text text-muted">
|
||||
After this many days, the user will have to enter the password again. Leave empty or set
|
||||
to 0 for no expiration.
|
||||
</small>
|
||||
</div>
|
||||
// enable CKEDIRTOR
|
||||
CKEDITOR.replace("template_instructions");
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_button">Decrypt button label</label>
|
||||
<input type="text" class="form-control" id="template_button" placeholder="Default: 'DECRYPT'">
|
||||
</div>
|
||||
</div>
|
||||
let htmlToDownload;
|
||||
|
||||
<button class="btn btn-primary pull-right" type="submit">Generate password protected HTML</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-xs-12">
|
||||
<h2>Encrypted HTML</h2>
|
||||
<p><a class="btn btn-success download"
|
||||
download="encrypted.html"
|
||||
id="download-link"
|
||||
disabled="disabled">Download html file with password prompt</a></p>
|
||||
<pre id="encrypted_html_display">
|
||||
Your encrypted string</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.ckeditor.com/4.7.0/standard/ckeditor.js"></script>
|
||||
|
||||
<script id="cryptoEngine">
|
||||
window.cryptoEngine = /*[|js_crypto_engine|]*/0
|
||||
</script>
|
||||
|
||||
<script id="codec">
|
||||
window.codec = /*[|js_codec|]*/0
|
||||
</script>
|
||||
|
||||
<script id="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('template_instructions');
|
||||
|
||||
let htmlToDownload;
|
||||
|
||||
/**
|
||||
* Extract js code from <script> tag and return it as a string
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
function getScriptAsString(id) {
|
||||
return document.getElementById(id)
|
||||
.innerText.replace(/window\.\w+ = /, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {string} action
|
||||
*/
|
||||
function trackEvent(action) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'https://zlgpaemmniviswibzuwt.supabase.co/rest/v1/rpc/increment_analytics', true);
|
||||
xhr.setRequestHeader('Content-type', 'application/json; charset=UTF-8')
|
||||
xhr.setRequestHeader('apikey', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpsZ3BhZW1tbml2aXN3aWJ6dXd0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjkxMjM0OTcsImV4cCI6MTk4NDY5OTQ5N30.wNoVDHG7F6INx-IPotMs3fL1nudfaF2qvQDgG-1PhNI')
|
||||
xhr.setRequestHeader('Authorization', 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpsZ3BhZW1tbml2aXN3aWJ6dXd0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjkxMjM0OTcsImV4cCI6MTk4NDY5OTQ5N30.wNoVDHG7F6INx-IPotMs3fL1nudfaF2qvQDgG-1PhNI')
|
||||
xhr.send(
|
||||
JSON.stringify({
|
||||
action_input: action
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the password prompt template with data provided.
|
||||
* @param data
|
||||
*/
|
||||
function setFileToDownload (data) {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', 'lib/password_template.html', true);
|
||||
request.onload = function () {
|
||||
const renderedTmpl = formater.renderTemplate(request.responseText, data);
|
||||
|
||||
const downloadLink = document.querySelector('a.download');
|
||||
downloadLink.href = 'data:text/html,' + encodeURIComponent(renderedTmpl);
|
||||
downloadLink.removeAttribute('disabled');
|
||||
|
||||
htmlToDownload = renderedTmpl;
|
||||
};
|
||||
request.send();
|
||||
}
|
||||
|
||||
// register page load
|
||||
window.onload = function () {
|
||||
trackEvent('show_index');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
*/
|
||||
document.getElementById('encrypt_form').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
trackEvent('generate_encrypted');
|
||||
|
||||
// update instruction textarea value with CKEDITOR content
|
||||
// (see https://stackoverflow.com/questions/3147670/ckeditor-update-textarea)
|
||||
CKEDITOR.instances['template_instructions'].updateElement();
|
||||
|
||||
const unencrypted = document.getElementById('unencrypted_html').value,
|
||||
password = document.getElementById('password').value;
|
||||
|
||||
const salt = cryptoEngine.generateRandomSalt();
|
||||
const encryptedMsg = await encode(unencrypted, password, salt);
|
||||
|
||||
const templateButton = document.getElementById('template_button').value,
|
||||
templateInstructions = document.getElementById('template_instructions').value,
|
||||
isRememberEnabled = document.getElementById('remember').checked,
|
||||
templateTitle = document.getElementById('template_title').value.trim(),
|
||||
templatePlaceholder = document.getElementById('template_placeholder').value.trim(),
|
||||
rememberDurationInDays = document.getElementById('remember_in_days').value || 0,
|
||||
templateRemember = document.getElementById('template_remember').value;
|
||||
|
||||
const data = {
|
||||
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 || 'Password',
|
||||
template_remember: templateRemember || 'Remember me',
|
||||
template_title: templateTitle || 'Protected Page',
|
||||
};
|
||||
|
||||
document.getElementById('encrypted_html_display').textContent = encryptedMsg;
|
||||
|
||||
setFileToDownload(data);
|
||||
});
|
||||
|
||||
document.getElementById('toggle-extra-option')
|
||||
.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('extra-options').classList.toggle('hidden');
|
||||
});
|
||||
|
||||
let isConceptShown = false;
|
||||
document.getElementById('toggle-concept')
|
||||
.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
isConceptShown = !isConceptShown;
|
||||
|
||||
document.getElementById('toggle-concept-sign').innerText = isConceptShown ? '▼' : '►';
|
||||
|
||||
document.getElementById('concept').classList.toggle('hidden');
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Browser specific download code.
|
||||
*/
|
||||
document.getElementById('download-link')
|
||||
.addEventListener('click', function (e) {
|
||||
// only register the click event if there is actually a generated file
|
||||
if (htmlToDownload) {
|
||||
trackEvent('download_encrypted');
|
||||
/**
|
||||
* Extract js code from <script> tag and return it as a string
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
function getScriptAsString(id) {
|
||||
return document.getElementById(id).innerText.replace(/window\.\w+ = /, "");
|
||||
}
|
||||
|
||||
const isIE = (navigator.userAgent.indexOf("MSIE") !== -1) || (!!document.documentMode === true); // >= 10
|
||||
const isEdge = navigator.userAgent.indexOf("Edge") !== -1;
|
||||
/**
|
||||
* 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 {string} action
|
||||
*/
|
||||
function trackEvent(action) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "https://zlgpaemmniviswibzuwt.supabase.co/rest/v1/rpc/increment_analytics", true);
|
||||
xhr.setRequestHeader("Content-type", "application/json; charset=UTF-8");
|
||||
xhr.setRequestHeader(
|
||||
"apikey",
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpsZ3BhZW1tbml2aXN3aWJ6dXd0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjkxMjM0OTcsImV4cCI6MTk4NDY5OTQ5N30.wNoVDHG7F6INx-IPotMs3fL1nudfaF2qvQDgG-1PhNI"
|
||||
);
|
||||
xhr.setRequestHeader(
|
||||
"Authorization",
|
||||
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpsZ3BhZW1tbml2aXN3aWJ6dXd0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjkxMjM0OTcsImV4cCI6MTk4NDY5OTQ5N30.wNoVDHG7F6INx-IPotMs3fL1nudfaF2qvQDgG-1PhNI"
|
||||
);
|
||||
xhr.send(
|
||||
JSON.stringify({
|
||||
action_input: action,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// download with MS specific feature
|
||||
if (htmlToDownload && (isIE || isEdge)) {
|
||||
/**
|
||||
* Fill the password prompt template with data provided.
|
||||
* @param data
|
||||
*/
|
||||
function setFileToDownload(data) {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open("GET", "lib/password_template.html", true);
|
||||
request.onload = function () {
|
||||
const renderedTmpl = formater.renderTemplate(request.responseText, data);
|
||||
|
||||
const downloadLink = document.querySelector("a.download");
|
||||
downloadLink.href = "data:text/html," + encodeURIComponent(renderedTmpl);
|
||||
downloadLink.removeAttribute("disabled");
|
||||
|
||||
htmlToDownload = renderedTmpl;
|
||||
};
|
||||
request.send();
|
||||
}
|
||||
|
||||
// register page load
|
||||
window.onload = function () {
|
||||
trackEvent("show_index");
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
*/
|
||||
document.getElementById("encrypt_form").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
const blobObject = new Blob([htmlToDownload]);
|
||||
window.navigator.msSaveOrOpenBlob(blobObject, 'encrypted.html');
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
</script>
|
||||
trackEvent("generate_encrypted");
|
||||
|
||||
</body>
|
||||
// update instruction textarea value with CKEDITOR content
|
||||
// (see https://stackoverflow.com/questions/3147670/ckeditor-update-textarea)
|
||||
CKEDITOR.instances["template_instructions"].updateElement();
|
||||
|
||||
const unencrypted = document.getElementById("unencrypted_html").value,
|
||||
password = document.getElementById("password").value;
|
||||
|
||||
const salt = cryptoEngine.generateRandomSalt();
|
||||
const encryptedMsg = await encode(unencrypted, password, salt);
|
||||
|
||||
const templateButton = document.getElementById("template_button").value,
|
||||
templateInstructions = document.getElementById("template_instructions").value,
|
||||
isRememberEnabled = document.getElementById("remember").checked,
|
||||
templateTitle = document.getElementById("template_title").value.trim(),
|
||||
templatePlaceholder = document.getElementById("template_placeholder").value.trim(),
|
||||
rememberDurationInDays = document.getElementById("remember_in_days").value || 0,
|
||||
templateRemember = document.getElementById("template_remember").value;
|
||||
|
||||
const data = {
|
||||
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 || "Password",
|
||||
template_remember: templateRemember || "Remember me",
|
||||
template_title: templateTitle || "Protected Page",
|
||||
};
|
||||
|
||||
document.getElementById("encrypted_html_display").textContent = encryptedMsg;
|
||||
|
||||
setFileToDownload(data);
|
||||
});
|
||||
|
||||
document.getElementById("toggle-extra-option").addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
document.getElementById("extra-options").classList.toggle("hidden");
|
||||
});
|
||||
|
||||
let isConceptShown = false;
|
||||
document.getElementById("toggle-concept").addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
isConceptShown = !isConceptShown;
|
||||
|
||||
document.getElementById("toggle-concept-sign").innerText = isConceptShown ? "▼" : "►";
|
||||
|
||||
document.getElementById("concept").classList.toggle("hidden");
|
||||
});
|
||||
|
||||
/**
|
||||
* Browser specific download code.
|
||||
*/
|
||||
document.getElementById("download-link").addEventListener("click", function (e) {
|
||||
// only register the click event if there is actually a generated file
|
||||
if (htmlToDownload) {
|
||||
trackEvent("download_encrypted");
|
||||
}
|
||||
|
||||
const isIE = navigator.userAgent.indexOf("MSIE") !== -1 || !!document.documentMode === true; // >= 10
|
||||
const isEdge = navigator.userAgent.indexOf("Edge") !== -1;
|
||||
|
||||
// download with MS specific feature
|
||||
if (htmlToDownload && (isIE || isEdge)) {
|
||||
e.preventDefault();
|
||||
const blobObject = new Blob([htmlToDownload]);
|
||||
window.navigator.msSaveOrOpenBlob(blobObject, "encrypted.html");
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Ładowanie…
Reference in New Issue