Merge pull request #170 from robinmoisson/v3

StatiCrypt V3
pull/163/head
Robin Moisson 2023-03-30 18:53:59 +02:00 zatwierdzone przez GitHub
commit dbbad9127b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
19 zmienionych plików z 1427 dodań i 912 usunięć

2
.gitignore vendored
Wyświetl plik

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

34
MIGRATING.md 100644
Wyświetl plik

@ -0,0 +1,34 @@
# Migration guide
## From 2.x to 3.x
StatiCrypt 3.x brings a number of improvements: strong default security with WebCrypto, cleaner CLI options and a much simpler `password_template`. This has been done while preserving auto-decrypt "share" links and remember-me functionality: if you used those with StatiCrypt 2.x, your links will still work with 3.x and you'll still be logged in.
There are a few breaking changes, but they should be easy to fix. If you have any trouble, feel free to open an issue.
### Breaking changes
#### The CLI
When encrypting `secret.html`, the CLI will now create a folder with your encrypted file `encrypted/secret.html`. It will not create a `secret_encrypted.html` file anymore.
Passwords shorter than 14 characters used to trigger a warning, now they trigger a blocking promp ("Do you want to use that password [yn]"). Add `--short` to hide that prompt.
The options and parameters have been changed:
- all template related options have been renamed to `--template-*`: pick your file with `--template`, set title with `--template-title`, etc.
- the password is now an optional argument: set with `-p <password>`, or leave blank to be prompted for it.
- many other options have been renamed, refer to the help (`--help`) or documentation for the full reference.
#### The password template
If you don't use a custom password template, you don't need to do anything.
If you do, you need to update your template. To do so:
- get `lib/password_template.html`
- replace the javascript part from this file in your custom template (the new template is logic is much simpler)
- update the injected variables in your template (notice we use new template tags, they now are `/*[|variable|]*/0` instead of `{variable}`):
- `{title}` => `/*[|template_title|]*/0`
- `{instructions}` => `/*[|template_instructions|]*/0`
- `{remember_me}` => `/*[|template_remember|]*/0`
- `{passphrase_placeholder}` => `/*[|template_placeholder|]*/0`
- `{decrypt_button}` => `/*[|template_button|]*/0`

156
README.md
Wyświetl plik

@ -2,7 +2,7 @@
# StatiCrypt
StatiCrypt uses AES-256 to encrypt your HTML file with your long password and return a static page including a password prompt and the javascript decryption logic that you can safely upload anywhere (see [what the page looks like](https://robinmoisson.github.io/staticrypt/example/example_encrypted.html)).
StatiCrypt uses AES-256 and WebCrypto to encrypt your HTML file with your long password and return a static page including a password prompt and the javascript decryption logic that you can safely upload anywhere (see [what the page looks like](https://robinmoisson.github.io/staticrypt/example/encrypted/example.html)).
This means you can **password protect the content of your _public_ static HTML file, without any back-end** - serving it over Netlify, GitHub pages, etc. (see the detail of [how it works](#how-staticrypt-works)).
@ -10,6 +10,8 @@ You can encrypt a file online in your browser (client side) at https://robinmois
## CLI
**Migration:** v3 brings many improvements, a clearer CLI and simpler `password_template` over v2. See the [migration guide from v2 to v3](MIGRATING.md). v3 uses WebCrypto which is only available in HTTPS or localhost contexts, so if you need to use it in HTTP you'll need to use v2.
### Installation
Staticrypt is available through npm as a CLI, install with
@ -22,96 +24,84 @@ You can then run it with `npx staticrypt ...`. You can also install globally wit
### Examples
> If you're viewing your file over HTTPS or localhost, you should use the `--engine webcrypto` flag to use the WebCrypto engine, which is more secure here. Otherwise the CryptoJS engine will be used.
>
> These examples will create a `.staticrypt.json` file in the current directory, see the FAQ as to why. You can prevent it by setting the `--config` flag to "false".
**Encrypt a file:** Encrypt `test.html` and create a `test_encrypted.html` file (add `-o my_encrypted_file.html` to change the name of the output file):
**Encrypt a file:** encrypt `test.html` and create a `encrypted/test.html` file (use `-d my_directory` to change the output directory):
```bash
staticrypt test.html MY_LONG_PASSWORD --engine webcrypto
staticrypt test.html -p MY_LONG_PASSWORD
```
**Encrypt a file with the password in an environment variable:** set your long password in the `STATICRYPT_PASSWORD` environment variable ([`.env` files](https://www.npmjs.com/package/dotenv#usage) are supported):
```bash
# the password is in the STATICRYPT_PASSWORD env variable
staticrypt test.html --engine webcrypto
staticrypt test.html
```
**Encrypt a file and get a shareable link containing the hashed password** - you can include your file URL or leave blank:
```bash
# you can also pass '--share' without specifying the URL to get the `#staticrypt_pwd=...`
staticrypt test.html MY_LONG_PASSWORD --share https://example.com/test_encrypted.html --engine webcrypto
staticrypt test.html MY_LONG_PASSWORD --share https://example.com/test_encrypted.html
# => https://example.com/test_encrypted.html#staticrypt_pwd=5bfbf1343c7257cd7be23ecd74bb37fa2c76d041042654f358b6255baeab898f
```
**Encrypt all html files in a directory** and replace them with encrypted versions (`{}` will be replaced with each file name by the `find` command - if you wanted to move the encrypted files to an `encrypted/` directory, you could use `-o encrypted/{}`):
**Encrypt all html files from a directory** and put them in a `encrypted/` directory:
```bash
find . -type f -name "*.html" -exec staticrypt {} MY_LONG_PASSWORD -o {} --engine webcrypto \;
```
**Encrypt all html files in a directory except** the ones ending in `_encrypted.html`:
```bash
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_LONG_PASSWORD --engine webcrypto \;
find . -type f -name "*.html" -exec staticrypt {} MY_LONG_PASSWORD \;
```
### CLI Reference
The password argument is optional if `STATICRYPT_PASSWORD` is set in the environment or `.env` file.
Usage: staticrypt <filename> [<password>] [options]
Usage: staticrypt <filename> [options]
Options:
--help Show help [boolean]
--version Show version number [boolean]
-c, --config Path to the config file. Set to "false" to
disable. [string] [default: ".staticrypt.json"]
--decrypt-button Label to use for the decrypt button. Default:
"DECRYPT". [string] [default: "DECRYPT"]
-e, --embed Whether or not to embed crypto-js in the page
(or use an external CDN).
[boolean] [default: true]
--engine The crypto engine to use. WebCrypto uses 600k
iterations and is more secure, CryptoJS 15k.
Possible values: 'cryptojs', 'webcrypto'.
[string] [default: "cryptojs"]
-f, --file-template Path to custom HTML template with password
prompt.
[string] [default: "/code/staticrypt/lib/password_template.html"]
-i, --instructions Special instructions to display to the user.
[string] [default: ""]
--label-error Error message to display on entering wrong
password. [string] [default: "Bad password!"]
--noremember Set this flag to remove the "Remember me"
checkbox. [boolean] [default: false]
-o, --output File name/path for the generated encrypted file.
[string] [default: null]
--passphrase-placeholder Placeholder to use for the password input.
[string] [default: "Password"]
-r, --remember Expiration in days of the "Remember me" checkbox
that will save the (salted + hashed) password in
localStorage when entered by the user. Default:
"0", no expiration. [number] [default: 0]
--remember-label Label to use for the "Remember me" checkbox.
[string] [default: "Remember me"]
-s, --salt Set the salt manually. It should be set if you
want to use "Remember me" through multiple
pages. It needs to be a 32-character-long
hexadecimal string.
Include the empty flag to generate a random salt
you can use: "statycrypt -s". [string]
--share Get a link containing your hashed password that
will auto-decrypt the page. Pass your URL as a
value to append "#staticrypt_pwd=<hashed_pwd>",
or leave empty to display the hash to append.
--help Show help [boolean]
--version Show version number [boolean]
-c, --config Path to the config file. Set to "false" to
disable. [string] [default: ".staticrypt.json"]
-d, --directory Name of the directory where the encrypted files
will be saved. [string] [default: "encrypted/"]
-p, --password The password to encrypt your file with. Leave
empty to be prompted for it. If
STATICRYPT_PASSWORD is set in the env, we'll use
that instead. [string] [default: null]
--remember Expiration in days of the "Remember me" checkbox
that will save the (salted + hashed) password in
localStorage when entered by the user. Set to
"false" to hide the box. Default: "0", no
expiration. [number] [default: 0]
-s, --salt Set the salt manually. It should be set if you
want to use "Remember me" through multiple pages.
It needs to be a 32-character-long hexadecimal
string.
Include the empty flag to generate a random salt
you can use: "statycrypt -s". [string]
--share Get a link containing your hashed password that
will auto-decrypt the page. Pass your URL as a
value to append "#staticrypt_pwd=<hashed_pwd>",
or leave empty to display the hash to append.
[string]
--short Hide the "short password" warning.
--short Hide the "short password" warning.
[boolean] [default: false]
-t, --title Title for the output HTML page.
-t, --template Path to custom HTML template with password
prompt.
[string] [default: "/code/staticrypt/lib/password_template.html"]
--template-button Label to use for the decrypt button. Default:
"DECRYPT". [string] [default: "DECRYPT"]
--template-instructions Special instructions to display to the user.
[string] [default: ""]
--template-error Error message to display on entering wrong
password. [string] [default: "Bad password!"]
--template-placeholder Placeholder to use for the password input.
[string] [default: "Password"]
--template-remember Label to use for the "Remember me" checkbox.
[string] [default: "Remember me"]
--template-title Title for the output HTML page.
[string] [default: "Protected Page"]
@ -119,43 +109,35 @@ The password argument is optional if `STATICRYPT_PASSWORD` is set in the environ
So, how can you password protect html without a back-end?
StatiCrypt uses the [crypto-js](https://github.com/brix/crypto-js) library or WebCrypto to generate a static, password protected page that can be decrypted in-browser. You can then just send or upload the generated page to a place serving static content (github pages, for example) and you're done: the page will prompt users for a password, and the javascript will decrypt and load your HTML, all done in the browser.
StatiCrypt uses WebCrypto to generate a static, password protected page that can be decrypted in-browser. You can then just send or upload the generated page to a place serving static content (github pages, for example) and you're done: the page will prompt users for a password, and the javascript will decrypt and load your HTML, all done in the browser.
So it basically encrypts your page and puts everything in a user-friendly way to use a password in the new file.
So it basically encrypts your page and puts everything in a user-friendly way to enter the password in the new file.
## FAQ
### Is it secure?
Simple answer: your file content has been encrypted with AES-256, a popular and strong encryption algorithm. You can now upload it in any public place and no one will be able to read it without the password. So if you used a long, strong password, then yes it should be pretty secure.
Simple answer: your file content has been encrypted with AES-256, a popular and strong encryption algorithm. You can now upload it to any public place and no one will be able to read it without the password. So if you used a long, strong password, then yes it should be pretty secure.
That being said, actual security always depends on a number of factors and on the threat model you want to protect against. Because your full encrypted file is accessible client side, brute-force/dictionary attacks would be easy to do at a really fast pace: **use a long, unusual password**. We recommend 16+ alphanum characters, [Bitwarden](https://bitwarden.com/) is a great open-source password manager if you don't have one already.
On the technical aspects: we use AES in CBC mode (see a discussion on why it's appropriate for StatiCrypt in [#19](https://github.com/robinmoisson/staticrypt/issues/19)) and 600k PBKDF2 iterations when using the WebCrypto engine (it's 15k when using CryptoJS, read a detailed report on why these numbers in [#159](https://github.com/robinmoisson/staticrypt/issues/159)).
On the technical aspects: we use AES in CBC mode (see a discussion on why this mode is appropriate for StatiCrypt in [#19](https://github.com/robinmoisson/staticrypt/issues/19)) and 600k PBKDF2-SHA256 iterations (which is the [recommended number](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) by OWASP - read a detailed report on why this number and the security model of StatiCrypt in [#159](https://github.com/robinmoisson/staticrypt/issues/159)).
**Also, disclaimer:** I am not a cryptographer - the concept is simple and I try my best to implement it correctly but please adjust accordingly: if you are an at-risk activist or have sensitive crypto data to protect, you might want to use something else.
**Also, disclaimer:** I am not a cryptographer - I try my best to get the implementation right, listen to feedback and be transparent but please adjust accordingly depending on your threat model. If you are an at-risk activist or have sensitive crypto assets to protect, you might want to use something else.
### Can I customize the password prompt?
Yes! Just copy `lib/password_template.html`, modify it to suit your style and point to your template file with the `-f path/to/my/file.html` flag. Be careful to not break the encrypting javascript part, the variables replaced by StatiCrypt are between curly brackets: `{salt}`.
Yes! Just copy `lib/password_template.html`, modify it to suit your style and point to your template file with the `-t path/to/my/file.html` flag.
Be careful to not break the encrypting javascript part, the variables replaced by StatiCrypt are in this format: `/*[|variable|]*/0`. Don't leave out the `0` at the end, this weird syntax is to avoid conflict with other templating engines while still being read as valid JS to parsers so we can use auto-formatting on the template files.
### Can I remove the "Remember me" checkbox?
If you don't want the checkbox to be included, you can add the `--noremember` flag to disable it.
If you don't want the checkbox to be included, you can set the `--remember false` flag to disable it.
### Should I use the WebCrypto or CryptoJS engine?
### Why doesn't StatiCrypt work in HTTP?
CryptoJS is the JS library that StatiCrypt used at first to do its crypto operations. WebCrypto is a browser API which exposes crypto methods, without having to rely on an external library.
WebCrypto is faster, which allows us to do more hashing rounds and make StatiCrypt more robust against brute-force attacks - if you can, **you should use WebCrypto**. The only limitation is it's only available in HTTPS context (which [is annoying people](https://github.com/w3c/webcrypto/issues/28)) or on localhost and on non-ancient browsers, so if you need that you can use `--engine cryptojs` which works everywhere. WebCrypto will be the only available option in our next major version.
> **Will switching break share links/remember-me?** If you encrypted a file with the CryptoJS engine and shared auto-decrypt links, or activated the remember-me flag, then switch to WebCrypto, the change is backward compatible and the file should still autodecrypt. The reverse isn't true - don't create an auto-decrypt link with WebCrypto then encrypt your file with CryptoJS.
>
> This is because we use more hashing rounds with the faster WebCrypto, making it more secure, but we can't remove hashing rounds to convert back (which is the whole point of a hash).
### Why do we embed the whole crypto-js library in each encrypted file when using the CryptoJS engine by default?
Some adblockers used to see the `crypto-js.min.js` served by CDN, think that's a crypto miner and block it. If you don't want to include it and serve from a CDN instead, you can add `--embed false`.
From version 3.x StatiCrypt only uses the browser WebCrypto API, which makes it more secure but is only available in HTTPS or on localhost. If you need to use it in HTTP, you can use version 2.x which offers the CryptoJS engine as an option, and will work everywhere.
### Why does StatiCrypt create a config file?
@ -169,13 +151,13 @@ The salt isn't secret, so you don't need to worry about hiding the config file.
### How does the "Remember me" checkbox work?
The CLI will add a "Remember me" checkbox on the password prompt by default (`--noremember` to disable). If the user checks it, the (salted + hashed) password will be stored in their browser's localStorage and the page will attempt to auto-decrypt when they come back.
The CLI will add a "Remember me" checkbox on the password prompt by default (`--remember false` to disable). If the user checks it, the (salted + hashed) password will be stored in their browser's localStorage and the page will attempt to auto-decrypt when they come back.
If no value is provided the stored password doesn't expire, you can also give it a value in days for how long should the store value be kept with `-r NUMBER_OF_DAYS`. If the user reconnects to the page after the expiration date the stored value will be cleared.
If no value is provided the stored password doesn't expire, you can also give it a value in days for how long should the store value be kept with `--remember NUMBER_OF_DAYS`. If the user reconnects to the page after the expiration date the stored value will be cleared.
#### "Logging out"
You can clear StatiCrypt values in localStorage (effectively "logging out") at any time by appending `staticrypt_logout` to the URL fragment (`mysite.com#staticrypt_logout`).
You can clear StatiCrypt values in localStorage (effectively "logging out") at any time by appending `staticrypt_logout` to the URL fragment (`https://mysite.com#staticrypt_logout`).
#### Encrypting multiple pages
@ -202,6 +184,10 @@ It's fine to open issues with suggestions and bug reports.
If you find a serious security bug please open an issue, I'll try to fix it relatively quickly.
### Security
You can find the security policy and secure contact details in [SECURITY.md](SECURITY.md). If you have general ideas or feedback around the implementation or StatiCrypt security model they are very welcome, if it's not extra sensitive feel free to open an issue. A couple of place where security was discussed previously are [#19](https://github.com/robinmoisson/staticrypt/issues/19) and [#159](https://github.com/robinmoisson/staticrypt/issues/159).
### Guidelines to contributing
#### Source map
@ -209,12 +195,12 @@ If you find a serious security bug please open an issue, I'll try to fix it rela
- `cli/` - The command-line interface published to NPM.
- `example/` - Example encrypted files, used as an example in the public website and for manual testing.
- `lib/` - Files shared across www and cli.
- `scripts/` - Build, test, deploy, CI, etc. See `npm run-script`.
- `scripts/` - Convenient scripts for building the project.
- `index.html` - The root of the in-browser encryption site hosted at https://robinmoisson.github.io/staticrypt. Kept in the root of the repo for easy deploys to GitHub Pages.
#### Build
Built assets are committed to main. Run build before submitting a PR or publishing to npm.
When editing StatiCrypt logic, we want to reflect the changes in the browser version, the CLI and the example files. To do so, run:
```
npm install
@ -223,7 +209,7 @@ npm run build
#### Test
The testing is done manually for now - run [build](#build), then open `example/example_encypted.html` and check everything works correctly.
The testing is done manually for now - you can run [build](#build), then open `example/encrypted/example.html` and check everything works correctly. There is an open issue to automate this in [#136](https://github.com/robinmoisson/staticrypt/issues/136), feel free to contribute to setting up a test framework if you'd like!
## Community and alternatives

Wyświetl plik

@ -1,8 +1,9 @@
const fs = require("fs");
const { generateRandomSalt } = require("../lib/cryptoEngine/webcryptoEngine.js");
const path = require("path");
const {renderTemplate} = require("../lib/formater.js");
const fs = require("fs");
const readline = require('readline');
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");
@ -51,26 +52,87 @@ function isOptionSetByUser(option, yargs) {
exports.isOptionSetByUser = isOptionSetByUser;
/**
* Get the password from the command arguments
* Prompts the user for input on the CLI.
*
* @param {string[]} positionalArguments
* @returns {string}
* @param {string} question
* @returns {Promise<string>}
*/
function getPassword(positionalArguments) {
let password = process.env.STATICRYPT_PASSWORD;
const hasEnvPassword = password !== undefined && password !== "";
function prompt (question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
if (hasEnvPassword) {
return password;
}
if (positionalArguments.length < 2) {
exitWithError("missing password, please provide an argument or set the STATICRYPT_PASSWORD environment variable in the environment or .env file");
}
return positionalArguments[1].toString();
return new Promise((resolve) => {
return rl.question(question, (answer) => {
rl.close();
return resolve(answer);
});
});
}
async function getValidatedPassword(passwordArgument, isShortAllowed) {
const password = await getPassword(passwordArgument);
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] "
)
if (!shouldUseShort.match(/^\s*(y|yes)\s*$/i)) {
console.log("Aborting.");
process.exit(0);
}
}
return password;
}
exports.getValidatedPassword = getValidatedPassword;
/**
* Get the config from the config file.
*
* @param {string} configArgument
* @returns {{}|object}
*/
function getConfig(configArgument) {
const isUsingconfigFile = configArgument.toLowerCase() !== "false";
const configPath = "./" + configArgument;
if (isUsingconfigFile && fs.existsSync(configPath)) {
return JSON.parse(fs.readFileSync(configPath, "utf8"));
}
return {};
}
exports.getConfig = getConfig;
/**
* Get the password from the command arguments or environment variables.
*
* @param {string} passwordArgument - password from the command line
* @returns {Promise<string>}
*/
async function getPassword(passwordArgument) {
// try to get the password from the environment variable
const envPassword = process.env.STATICRYPT_PASSWORD;
const hasEnvPassword = envPassword !== undefined && envPassword !== "";
if (hasEnvPassword) {
return envPassword;
}
// try to get the password from the command line arguments
if (passwordArgument !== null) {
return passwordArgument;
}
// prompt the user for their password
return prompt('Enter your long, unusual password: ');
}
exports.getPassword = getPassword;
/**
* @param {string} filepath
@ -85,6 +147,26 @@ function getFileContent(filepath) {
}
exports.getFileContent = getFileContent;
/**
* @param {object} namedArgs
* @param {object} config
* @returns {string}
*/
function getValidatedSalt(namedArgs, config) {
const salt = getSalt(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
);
}
return salt;
}
exports.getValidatedSalt = getValidatedSalt;
/**
* @param {object} namedArgs
* @param {object} config
@ -103,7 +185,6 @@ function getSalt(namedArgs, config) {
return generateRandomSalt();
}
exports.getSalt = getSalt;
/**
* A dead-simple alternative to webpack or rollup for inlining simple
@ -135,16 +216,34 @@ function convertCommonJSToBrowserJS(modulePath) {
}
exports.convertCommonJSToBrowserJS = convertCommonJSToBrowserJS;
/**
* Build the staticrypt script string to inject in our template.
*
* @returns {string}
*/
function buildStaticryptJS() {
let staticryptJS = convertCommonJSToBrowserJS("lib/staticryptJs");
const scriptsToInject = {
js_codec: convertCommonJSToBrowserJS("lib/codec"),
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
};
return renderTemplate(staticryptJS, scriptsToInject);
}
exports.buildStaticryptJS = buildStaticryptJS;
/**
* @param {string} filePath
* @param {string} errorName
* @returns {string}
*/
function readFile(filePath, errorName = file) {
function readFile(filePath, errorName = "file") {
try {
return fs.readFileSync(filePath, "utf8");
} catch (e) {
exitWithError(`could not read ${errorName}!`);
console.error(e);
exitWithError(`could not read ${errorName} at path "${filePath}"`);
}
}
@ -160,47 +259,21 @@ function genFile(data, outputFilePath, templateFilePath) {
const renderedTemplate = renderTemplate(templateContents, data);
// create output directory if it does not exist
const dirname = path.dirname(outputFilePath);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
try {
fs.writeFileSync(outputFilePath, renderedTemplate);
} catch (e) {
exitWithError("could not generate output file!");
console.error(e);
exitWithError("could not generate output file");
}
}
exports.genFile = genFile;
/**
* TODO: remove in next major version
*
* This method checks whether the password template support the security fix increasing PBKDF2 iterations. Users using
* an old custom password_template might have logic that doesn't benefit from the fix.
*
* @param {string} templatePathParameter
* @returns {boolean}
*/
function isCustomPasswordTemplateLegacy(templatePathParameter) {
const customTemplateContent = readFile(templatePathParameter, "template");
// if the template injects the crypto engine, it's up to date
return !customTemplateContent.includes("js_crypto_engine");
}
exports.isCustomPasswordTemplateLegacy = isCustomPasswordTemplateLegacy;
/**
* TODO: remove in next major version
*
* This method checks whether the password template support the async logic.
*
* @param {string} templatePathParameter
* @returns {boolean}
*/
function isPasswordTemplateUsingAsync(templatePathParameter) {
const customTemplateContent = readFile(templatePathParameter, "template");
// if the template includes this comment, it's up to date
return customTemplateContent.includes("// STATICRYPT_VERSION: async");
}
exports.isPasswordTemplateUsingAsync = isPasswordTemplateUsingAsync;
/**
* @param {string} templatePathParameter
* @returns {boolean}
@ -212,76 +285,33 @@ function isCustomPasswordTemplateDefault(templatePathParameter) {
exports.isCustomPasswordTemplateDefault = isCustomPasswordTemplateDefault;
function parseCommandLineArguments() {
return Yargs.usage("Usage: staticrypt <filename> [<password>] [options]")
return Yargs.usage("Usage: staticrypt <filename> [options]")
.option("c", {
alias: "config",
type: "string",
describe: 'Path to the config file. Set to "false" to disable.',
default: ".staticrypt.json",
})
.option("decrypt-button", {
.option("d", {
alias: "directory",
type: "string",
describe: 'Label to use for the decrypt button. Default: "DECRYPT".',
default: "DECRYPT",
describe: "Name of the directory where the encrypted files will be saved.",
default: "encrypted/",
})
.option("e", {
alias: "embed",
type: "boolean",
describe: "Whether or not to embed crypto-js in the page (or use an external CDN).",
default: true,
})
.option("engine", {
.option("p", {
alias: "password",
type: "string",
describe: "The crypto engine to use. WebCrypto uses 600k iterations and is more secure, CryptoJS 15k.\n" +
"Possible values: 'cryptojs', 'webcrypto'.",
default: "cryptojs",
})
.option("f", {
alias: "file-template",
type: "string",
describe: "Path to custom HTML template with password prompt.",
default: PASSWORD_TEMPLATE_DEFAULT_PATH,
})
.option("i", {
alias: "instructions",
type: "string",
describe: "Special instructions to display to the user.",
default: "",
})
.option("label-error", {
type: "string",
describe: "Error message to display on entering wrong password.",
default: "Bad password!",
})
.option("noremember", {
type: "boolean",
describe: 'Set this flag to remove the "Remember me" checkbox.',
default: false,
})
.option("o", {
alias: "output",
type: "string",
describe: "File name/path for the generated encrypted file.",
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("passphrase-placeholder", {
type: "string",
describe: "Placeholder to use for the password input.",
default: "Password",
})
.option("r", {
alias: "remember",
.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. Default: "0", no expiration.',
'in localStorage when entered by the user. Set to "false" to hide the box. Default: "0", no expiration.',
default: 0,
})
.option("remember-label", {
type: "string",
describe: 'Label to use for the "Remember me" checkbox.',
default: "Remember me",
})
// 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", {
@ -306,7 +336,37 @@ function parseCommandLineArguments() {
default: false,
})
.option("t", {
alias: "title",
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-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",

Wyświetl plik

@ -3,182 +3,97 @@
"use strict";
const fs = require("fs");
const path = require("path");
// parse .env file into process.env
require('dotenv').config();
const cryptojsEngine = require("../lib/cryptoEngine/cryptojsEngine");
const webcryptoEngine = require("../lib/cryptoEngine/webcryptoEngine");
const cryptoEngine = require("../lib/cryptoEngine.js");
const codec = require("../lib/codec.js");
const { convertCommonJSToBrowserJS, exitWithError, isOptionSetByUser, genFile, getPassword, getFileContent, getSalt} = require("./helpers");
const { isCustomPasswordTemplateLegacy, parseCommandLineArguments, isPasswordTemplateUsingAsync} = require("./helpers.js");
const CRYPTOJS_SCRIPT_TAG =
'<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js" ' +
'integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></script>';
const { generateRandomSalt } = cryptoEngine;
const { encode } = codec.init(cryptoEngine);
const { parseCommandLineArguments, buildStaticryptJS, isOptionSetByUser, genFile, getPassword, getFileContent, getSalt,
getValidatedSalt,
getValidatedPassword, getConfig
} = require("./helpers.js");
// parse arguments
const yargs = parseCommandLineArguments();
const namedArgs = yargs.argv;
// set the crypto engine
const isWebcrypto = namedArgs.engine === "webcrypto";
const cryptoEngine = isWebcrypto ? webcryptoEngine : cryptojsEngine;
const { generateRandomSalt, generateRandomString } = cryptoEngine;
const { encode } = codec.init(cryptoEngine);
async function runStatiCrypt() {
const hasSaltFlag = isOptionSetByUser("s", yargs);
const hasShareFlag = isOptionSetByUser("share", yargs);
const positionalArguments = namedArgs._;
// validate the number of arguments
if (!hasShareFlag && !hasSaltFlag) {
if (positionalArguments.length === 0) {
yargs.showHelp();
process.exit(1);
}
}
// if the 's' flag is passed without parameter, generate a salt, display & exit
if (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
if (hasSaltFlag && !namedArgs.salt) {
console.log(generateRandomSalt());
process.exit(0);
}
// validate the number of arguments
const positionalArguments = namedArgs._;
if (positionalArguments.length > 2 || positionalArguments.length === 0) {
yargs.showHelp();
process.exit(1);
}
// parse input
const inputFilepath = positionalArguments[0].toString(),
password = getPassword(positionalArguments);
if (password.length < 16 && !namedArgs.short) {
console.log(
`WARNING: Your password is less than 16 characters (length: ${password.length}). Brute-force attacks are easy to `
+ `try on public files, and you are most safe when using a long password.\n\n`
+ `👉️ Here's a strong generated password you could use: `
+ generateRandomString(21)
+ "\n\nThe file was encrypted with your password. You can hide this warning by increasing your password length or"
+ " adding the '--short' flag."
)
}
// get config file
const isUsingconfigFile = namedArgs.config.toLowerCase() !== "false";
const configPath = "./" + namedArgs.config;
let config = {};
if (isUsingconfigFile && fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
}
const config = getConfig(namedArgs.config);
// get the salt
const salt = getSalt(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
);
}
// write salt to config file
if (isUsingconfigFile && config.salt !== salt) {
config.salt = salt;
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
}
// get the salt & password
const salt = getValidatedSalt(namedArgs, config);
const password = await getValidatedPassword(namedArgs.password, namedArgs.short);
// display the share link with the hashed password if the --share flag is set
if (isOptionSetByUser("share", yargs)) {
if (hasShareFlag) {
const url = namedArgs.share || "";
const hashedPassword = await cryptoEngine.hashPassphrase(password, salt);
console.log(url + "#staticrypt_pwd=" + hashedPassword);
process.exit(0);
}
// TODO: remove in the next major version bump. This is to allow a security update to some versions without breaking
// older ones. If the password template is custom AND created before 2.2.0 we need to use the old hashing algorithm.
const isLegacy = isCustomPasswordTemplateLegacy(namedArgs.f);
if (isLegacy) {
console.log(
"#################################\n\n" +
"SECURITY WARNING [StatiCrypt]: You are using an old version of the password template, which has been found to " +
"be less secure. Please update your custom password_template logic to match the latest version." +
"\nYou can find instructions here: https://github.com/robinmoisson/staticrypt/issues/161" +
"\n\n#################################"
);
// write salt to config file
const isUsingconfigFile = namedArgs.config.toLowerCase() !== "false";
const configPath = "./" + namedArgs.config;
if (isUsingconfigFile && config.salt !== salt) {
config.salt = salt;
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
}
if (!isWebcrypto) {
console.log(
"WARNING: If you are viewing the file over HTTPS or locally, we recommend " +
(isPasswordTemplateUsingAsync(namedArgs.f) ? "" : "updating your password template to the latest version and ") +
"using the '--engine webcrypto' more secure engine. It will become the default in StatiCrypt next major version."
);
} else if (!isPasswordTemplateUsingAsync(namedArgs.f) && isWebcrypto) {
exitWithError(
"The '--engine webcrypto' engine is only available for password templates that use async/await. Please " +
"update your password template to the latest version or use the '--engine cryptojs' engine."
)
}
// create crypto-js tag (embedded or not)
let cryptoTag = CRYPTOJS_SCRIPT_TAG;
if (isWebcrypto) {
cryptoTag = "";
} else if (namedArgs.embed) {
try {
const embedContents = fs.readFileSync(
path.join(__dirname, "..", "lib", "kryptojs-3.1.9-1.min.js"),
"utf8"
);
cryptoTag = "<script>" + embedContents + "</script>";
} catch (e) {
exitWithError("Embed file does not exist.");
}
}
const cryptoEngineString = isWebcrypto
? convertCommonJSToBrowserJS("lib/cryptoEngine/webcryptoEngine")
: convertCommonJSToBrowserJS("lib/cryptoEngine/cryptojsEngine");
// get the file content
const inputFilepath = positionalArguments[0].toString();
const contents = getFileContent(inputFilepath);
// encrypt input
encode(contents, password, salt, isLegacy).then((encryptedMessage) => {
let codecString;
if (isWebcrypto) {
codecString = convertCommonJSToBrowserJS("lib/codec");
} else {
// TODO: remove on next major version bump. The replace is a hack to pass the salt to the injected js_codec in
// a backward compatible way (not requiring to update the password_template). Same for using a "sync" version
// of the codec.
codecString = convertCommonJSToBrowserJS("lib/codec-sync").replace('##SALT##', salt);
}
const encryptedMsg = await encode(contents, password, salt);
const data = {
crypto_tag: cryptoTag,
decrypt_button: namedArgs.decryptButton,
// TODO: deprecated option here for backward compat, remove on next major version bump
embed: isWebcrypto ? false : namedArgs.embed,
encrypted: encryptedMessage,
instructions: namedArgs.instructions,
is_remember_enabled: namedArgs.noremember ? "false" : "true",
js_codec: codecString,
js_crypto_engine: cryptoEngineString,
label_error: namedArgs.labelError,
passphrase_placeholder: namedArgs.passphrasePlaceholder,
remember_duration_in_days: namedArgs.remember,
remember_me: namedArgs.rememberLabel,
salt: salt,
title: namedArgs.title,
};
const isRememberEnabled = namedArgs.remember !== "false";
const outputFilepath = namedArgs.output !== null
? namedArgs.output
: inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
const data = {
is_remember_enabled: JSON.stringify(isRememberEnabled),
js_staticrypt: buildStaticryptJS(),
staticrypt_config: {
encryptedMsg,
isRememberEnabled,
rememberDurationInDays: namedArgs.remember,
salt,
},
template_button: namedArgs.templateButton,
template_error: namedArgs.templateError,
template_instructions: namedArgs.templateInstructions,
template_placeholder: namedArgs.templatePlaceholder,
template_remember: namedArgs.templateRemember,
template_title: namedArgs.templateTitle,
};
genFile(data, outputFilepath, namedArgs.f);
const outputFilepath = namedArgs.directory.replace(/\/+$/, '') + "/" + inputFilepath;
});
genFile(data, outputFilepath, namedArgs.template);
}
runStatiCrypt();

Wyświetl plik

@ -183,14 +183,11 @@
</div>
</div>
<script>
// do not remove this comment, it is used to detect the version of the script
// STATICRYPT_VERSION: async
const cryptoEngine = ((function(){
// these variables will be filled when generating the file - the template format is 'variable_name'
const staticryptInitiator = ((function(){
const exports = {};
const cryptoEngine = ((function(){
const exports = {};
const { subtle } = crypto;
@ -201,8 +198,6 @@ const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*
* Mirrors the API of CryptoJS.enc.Hex
*/
const HexEncoder = {
/**
@ -473,7 +468,7 @@ exports.generateRandomString = generateRandomString;
return exports;
})())
const codec = ((function(){
const codec = ((function(){
const exports = {};
/**
* Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
@ -561,49 +556,130 @@ exports.init = init;
return exports;
})())
const decode = codec.init(cryptoEngine).decode;
const decode = codec.init(cryptoEngine).decode;
// variables to be filled when generating the file
const encryptedMsg = 'df7428ed075decd3c4ff9d8ab6d2bea4410854c863d705789fb22e14b7da7ee20e94c593047abb9ba34d6519eebc879d2097bb918c0af0d4e248959849fb9c6bbb93aba054806c8773d1e4b63ec317185ad5462a9919dda986716c67bb57a89a044de3e25707cded482657c4a0208e9916aaa9d839f090eaaeb95603e05db11fe4bc37c4d98b9170124ce1c7ca18fe39c2f179e23eee61ba7d79cb3145e8833936c62adeffce1f5e129745c89541faa8100bfde4733bfa9c0ecf04768b3d1889',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
labelError = 'Bad password!',
isRememberEnabled = true,
rememberDurationInDays = 0; // 0 means forever
// constants
const rememberPassphraseKey = 'staticrypt_passphrase',
rememberExpirationKey = 'staticrypt_expiration';
/**
* Initialize the staticrypt module, that exposes functions callbable by the password_template.
*
* @param {{
* encryptedMsg: string,
* isRememberEnabled: boolean,
* rememberDurationInDays: number,
* salt: string,
* }} staticryptConfig - object of data that is stored on the password_template at encryption time.
*
* @param {{
* rememberExpirationKey: string,
* rememberPassphraseKey: string,
* replaceHtmlCallback: function,
* clearLocalStorageCallback: function,
* }} templateConfig - object of data that can be configured by a custom password_template.
*/
function init(staticryptConfig, templateConfig) {
const exports = {};
/**
* Decrypt our encrypted page, replace the whole HTML.
*
* @param hashedPassphrase
* @param {string} hashedPassphrase
* @returns {Promise<boolean>}
*/
async function decryptAndReplaceHtml(hashedPassphrase) {
const { encryptedMsg, salt } = staticryptConfig;
const { replaceHtmlCallback } = templateConfig;
const result = await decode(encryptedMsg, hashedPassphrase, salt);
if (!result.success) {
return false;
}
const plainHTML = result.decoded;
document.write(plainHTML);
document.close();
// if the user configured a callback call it, otherwise just replace the whole HTML
if (typeof replaceHtmlCallback === 'function') {
replaceHtmlCallback(plainHTML);
} else {
document.write(plainHTML);
document.close();
}
return true;
}
/**
* Attempt to decrypt the page and replace the whole HTML.
*
* @param {string} password
* @param {boolean} isRememberChecked
*
* @returns {Promise<{isSuccessful: boolean, hashedPassword?: string}>} - we return an object, so that if we want to
* expose more information in the future we can do it without breaking the password_template
*/
async function handleDecryptionOfPage(password, isRememberChecked) {
const { isRememberEnabled, rememberDurationInDays, salt } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// decrypt and replace the whole page
const hashedPassword = await cryptoEngine.hashPassphrase(password, salt);
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassword);
if (!isDecryptionSuccessful) {
return {
isSuccessful: false,
hashedPassword,
};
}
// remember the hashedPassword and set its expiration if necessary
if (isRememberEnabled && isRememberChecked) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassword);
// set the expiration if the duration isn't 0 (meaning no expiration)
if (rememberDurationInDays > 0) {
window.localStorage.setItem(
rememberExpirationKey,
(new Date().getTime() + rememberDurationInDays * 24 * 60 * 60 * 1000).toString()
);
}
}
return {
isSuccessful: true,
hashedPassword,
};
}
exports.handleDecryptionOfPage = handleDecryptionOfPage;
/**
* Clear localstorage from staticrypt related values
*/
function clearLocalStorage() {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
const { clearLocalStorageCallback, rememberExpirationKey, rememberPassphraseKey } = templateConfig;
if (typeof clearLocalStorageCallback === 'function') {
clearLocalStorageCallback();
} else {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
}
}
async function handleDecryptOnLoad() {
let isSuccessful = await decryptOnLoadFromUrl();
if (!isSuccessful) {
isSuccessful = await decryptOnLoadFromRememberMe();
}
return { isSuccessful };
}
exports.handleDecryptOnLoad = handleDecryptOnLoad;
/**
* Clear storage if we are logging out
*
* @returns - whether we logged out
* @returns {boolean} - whether we logged out
*/
function logoutIfNeeded() {
const logoutKey = "staticrypt_logout";
@ -632,12 +708,8 @@ exports.init = init;
* @returns {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
*/
async function decryptOnLoadFromRememberMe() {
if (!isRememberEnabled) {
return false;
}
// show the remember me checkbox
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
const { rememberDurationInDays } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// if we are login out, terminate
if (logoutIfNeeded()) {
@ -694,20 +766,42 @@ exports.init = init;
return false;
}
return exports;
}
exports.init = init;
return exports;
})())
const templateError = 'Bad password!',
isRememberEnabled = true,
staticryptConfig = {"encryptedMsg":"ec966bcc5ec514148b6b329f4f4f4e5e38767cf6c88303e29f692b4e8de540eb678dccbe750aeabe7cd361384bdc79c66b85254710d5b20673d4d28781a6129d980f0287980b5307cca6d41371eb9542b79b8582c334dfa9f5dfa45a3ec3205eb6462c309b36b94e0a5aa76fe26013b1919175e802b354ff942f4330879245425b5851b069d78a4e606240f43496913150f2c34780f658c875480a6ea07a0d2d92967d9079622b3ebcfc9b4540f8bcc4d49f356db6895f614d13e8287b7be4ad","isRememberEnabled":true,"rememberDurationInDays":0,"salt":"b93bbaf35459951c47721d1f3eaeb5b9"};
// you can edit these values to customize some of the behavior of StatiCrypt
const templateConfig = {
rememberExpirationKey: 'staticrypt_expiration',
rememberPassphraseKey: 'staticrypt_passphrase',
replaceHtmlCallback: null,
clearLocalStorageCallback: null,
};
// init the staticrypt engine
const staticrypt = staticryptInitiator.init(staticryptConfig, templateConfig);
// try to automatically decrypt on load if there is a saved password
window.onload = async function () {
let hasDecrypted = await decryptOnLoadFromUrl();
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
if (!hasDecrypted) {
hasDecrypted = await decryptOnLoadFromRememberMe();
}
// if we didn't decrypt anything, show the password prompt. Otherwise the content has already been replaced, no
// need to do anything
if (!hasDecrypted) {
// if we didn't decrypt anything on load, show the password prompt. Otherwise the content has already been
// replaced, no need to do anything
if (!isSuccessful) {
// hide loading screen
document.getElementById("staticrypt_loading").classList.add("hidden");
document.getElementById("staticrypt_content").classList.remove("hidden");
document.getElementById("staticrypt-password").focus();
// show the remember me checkbox
if (isRememberEnabled) {
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
}
}
}
@ -716,27 +810,12 @@ exports.init = init;
e.preventDefault();
const passphrase = document.getElementById('staticrypt-password').value,
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
isRememberChecked = document.getElementById('staticrypt-remember').checked;
// decrypt and replace the whole page
const hashedPassphrase = await cryptoEngine.hashPassphrase(passphrase, salt);
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(passphrase, isRememberChecked);
if (isDecryptionSuccessful) {
// remember the hashedPassphrase and set its expiration if necessary
if (isRememberEnabled && shouldRememberPassphrase) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassphrase);
// set the expiration if the duration isn't 0 (meaning no expiration)
if (rememberDurationInDays > 0) {
window.localStorage.setItem(
rememberExpirationKey,
(new Date().getTime() + rememberDurationInDays * 24 * 60 * 60 * 1000).toString()
);
}
}
} else {
alert(labelError);
if (!isSuccessful) {
alert(templateError);
}
});
</script>

Wyświetl plik

@ -46,7 +46,7 @@
</p>
<p>
Download your encrypted string in a HTML page with a password prompt you can upload anywhere (see <a
target="_blank" href="example/example_encrypted.html">example</a>).
target="_blank" href="example/encrypted/example.html">example</a>).
</p>
<p>
The tool is also available as <a href="https://npmjs.com/package/staticrypt">a CLI on NPM</a> and is <a
@ -127,24 +127,24 @@
</p>
<div id="extra-options" class="hidden">
<div class="form-group">
<label for="title">Page title</label>
<input type="text" class="form-control" id="title" placeholder="Default: 'Protected Page'">
<label for="template_title">Page title</label>
<input type="text" class="form-control" id="template_title" placeholder="Default: 'Protected Page'">
</div>
<div class="form-group">
<label for="instructions">Instructions to display the user</label>
<textarea class="form-control" id="instructions" placeholder="Default: nothing."></textarea>
<label for="template_instructions">Instructions to display the user</label>
<textarea class="form-control" id="template_instructions" placeholder="Default: nothing."></textarea>
</div>
<div class="form-group">
<label for="passphrase_placeholder">Passphrase input placeholder</label>
<input type="text" class="form-control" id="passphrase_placeholder"
<label for="template_placeholder">Passphrase input placeholder</label>
<input type="text" class="form-control" id="template_placeholder"
placeholder="Default: 'Passphrase'">
</div>
<div class="form-group">
<label for="remember_me">"Remember me" checkbox label</label>
<input type="text" class="form-control" id="remember_me" placeholder="Default: 'Remember me'">
<label for="template_remember">"Remember me" checkbox label</label>
<input type="text" class="form-control" id="template_remember" placeholder="Default: 'Remember me'">
</div>
<div class="form-group">
@ -161,8 +161,8 @@
</div>
<div class="form-group">
<label for="decrypt_button">Decrypt button label</label>
<input type="text" class="form-control" id="decrypt_button" placeholder="Default: 'DECRYPT'">
<label for="template_button">Decrypt button label</label>
<input type="text" class="form-control" id="template_button" placeholder="Default: 'DECRYPT'">
</div>
</div>
@ -184,11 +184,6 @@ Your encrypted string</pre>
</div>
</div>
<!--
Filename changed to circumvent adblockers that mistake it for a crypto miner (see https://github.com/robinmoisson/staticrypt/issues/107)
-->
<script src="lib/kryptojs-3.1.9-1.min.js"></script>
<script src="https://cdn.ckeditor.com/4.7.0/standard/ckeditor.js"></script>
<script id="cryptoEngine">
@ -203,8 +198,6 @@ const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*
* Mirrors the API of CryptoJS.enc.Hex
*/
const HexEncoder = {
/**
@ -572,7 +565,11 @@ exports.init = init;
window.formater = ((function(){
const exports = {};
/**
* Replace the placeholder tags (between '{tag}') in the template string with provided data.
* Replace the variable in template tags, between '/*[|variable|]* /0' (without the space in '* /0', ommiting it would
* break this comment), with the provided data.
*
* This weird format is so that we have something that doesn't break JS parser in the template files (it understands it
* as '0'), so we can still use auto-formatting.
*
* @param {string} templateString
* @param {Object} data
@ -580,12 +577,16 @@ exports.init = init;
* @returns string
*/
function renderTemplate(templateString, data) {
return templateString.replace(/{\s*(\w+)\s*}/g, function (_, key) {
if (data && data[key] !== undefined) {
return data[key];
return templateString.replace(/\/\*\[\|\s*(\w+)\s*\|]\*\/0/g, function (_, key) {
if (!data || data[key] === undefined) {
return key;
}
return "";
if (typeof data[key] === 'object') {
return JSON.stringify(data[key]);
}
return data[key];
});
}
exports.renderTemplate = renderTemplate;
@ -595,19 +596,608 @@ exports.renderTemplate = renderTemplate;
})())
</script>
<script id="staticrypt">
window.staticrypt = ((function(){
const exports = {};
const cryptoEngine = ((function(){
const exports = {};
const { subtle } = crypto;
const IV_BITS = 16 * 8;
const HEX_BITS = 4;
const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*/
const HexEncoder = {
/**
* hex string -> bytes
* @param {string} hexString
* @returns {Uint8Array}
*/
parse: function (hexString) {
if (hexString.length % 2 !== 0) throw "Invalid hexString";
const arrayBuffer = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) {
const byteValue = parseInt(hexString.substring(i, i + 2), 16);
if (isNaN(byteValue)) {
throw "Invalid hexString";
}
arrayBuffer[i / 2] = byteValue;
}
return arrayBuffer;
},
/**
* bytes -> hex string
* @param {Uint8Array} bytes
* @returns {string}
*/
stringify: function (bytes) {
const hexBytes = [];
for (let i = 0; i < bytes.length; ++i) {
let byteString = bytes[i].toString(16);
if (byteString.length < 2) {
byteString = "0" + byteString;
}
hexBytes.push(byteString);
}
return hexBytes.join("");
},
};
/**
* Translates between utf8 strings and Uint8Array bytes.
*/
const UTF8Encoder = {
parse: function (str) {
return new TextEncoder().encode(str);
},
stringify: function (bytes) {
return new TextDecoder().decode(bytes);
},
};
/**
* Salt and encrypt a msg with a password.
*/
async function encrypt(msg, hashedPassphrase) {
// Must be 16 bytes, unpredictable, and preferably cryptographically random. However, it need not be secret.
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#parameters
const iv = crypto.getRandomValues(new Uint8Array(IV_BITS / 8));
const key = await subtle.importKey(
"raw",
HexEncoder.parse(hashedPassphrase),
ENCRYPTION_ALGO,
false,
["encrypt"]
);
const encrypted = await subtle.encrypt(
{
name: ENCRYPTION_ALGO,
iv: iv,
},
key,
UTF8Encoder.parse(msg)
);
// iv will be 32 hex characters, we prepend it to the ciphertext for use in decryption
return HexEncoder.stringify(iv) + HexEncoder.stringify(new Uint8Array(encrypted));
}
exports.encrypt = encrypt;
/**
* Decrypt a salted msg using a password.
*
* @param {string} encryptedMsg
* @param {string} hashedPassphrase
* @returns {Promise<string>}
*/
async function decrypt(encryptedMsg, hashedPassphrase) {
const ivLength = IV_BITS / HEX_BITS;
const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
const encrypted = encryptedMsg.substring(ivLength);
const key = await subtle.importKey(
"raw",
HexEncoder.parse(hashedPassphrase),
ENCRYPTION_ALGO,
false,
["decrypt"]
);
const outBuffer = await subtle.decrypt(
{
name: ENCRYPTION_ALGO,
iv: iv,
},
key,
HexEncoder.parse(encrypted)
);
return UTF8Encoder.stringify(new Uint8Array(outBuffer));
}
exports.decrypt = decrypt;
/**
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} passphrase
* @param {string} salt
* @returns {Promise<string>}
*/
async function hashPassphrase(passphrase, salt) {
// we hash the passphrase in multiple steps, each adding more iterations. This is because we used to allow less
// iterations, so for backward compatibility reasons, we need to support going from that to more iterations.
let hashedPassphrase = await hashLegacyRound(passphrase, salt);
hashedPassphrase = await hashSecondRound(hashedPassphrase, salt);
return hashThirdRound(hashedPassphrase, salt);
}
exports.hashPassphrase = hashPassphrase;
/**
* This hashes the passphrase with 1k iterations. This is a low number, we need this function to support backwards
* compatibility.
*
* @param {string} passphrase
* @param {string} salt
* @returns {Promise<string>}
*/
function hashLegacyRound(passphrase, salt) {
return pbkdf2(passphrase, salt, 1000, "SHA-1");
}
exports.hashLegacyRound = hashLegacyRound;
/**
* Add a second round of iterations. This is because we used to use 1k, so for backwards compatibility with
* remember-me/autodecrypt links, we need to support going from that to more iterations.
*
* @param hashedPassphrase
* @param salt
* @returns {Promise<string>}
*/
function hashSecondRound(hashedPassphrase, salt) {
return pbkdf2(hashedPassphrase, salt, 14000, "SHA-256");
}
exports.hashSecondRound = hashSecondRound;
/**
* Add a third round of iterations to bring total number to 600k. This is because we used to use 1k, then 15k, so for
* backwards compatibility with remember-me/autodecrypt links, we need to support going from that to more iterations.
*
* @param hashedPassphrase
* @param salt
* @returns {Promise<string>}
*/
function hashThirdRound(hashedPassphrase, salt) {
return pbkdf2(hashedPassphrase, salt, 585000, "SHA-256");
}
exports.hashThirdRound = hashThirdRound;
/**
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} passphrase
* @param {string} salt
* @param {int} iterations
* @param {string} hashAlgorithm
* @returns {Promise<string>}
*/
async function pbkdf2(passphrase, salt, iterations, hashAlgorithm) {
const key = await subtle.importKey(
"raw",
UTF8Encoder.parse(passphrase),
"PBKDF2",
false,
["deriveBits"]
);
const keyBytes = await subtle.deriveBits(
{
name: "PBKDF2",
hash: hashAlgorithm,
iterations,
salt: UTF8Encoder.parse(salt),
},
key,
256
);
return HexEncoder.stringify(new Uint8Array(keyBytes));
}
function generateRandomSalt() {
const bytes = crypto.getRandomValues(new Uint8Array(128 / 8));
return HexEncoder.stringify(new Uint8Array(bytes));
}
exports.generateRandomSalt = generateRandomSalt;
async function signMessage(hashedPassphrase, message) {
const key = await subtle.importKey(
"raw",
HexEncoder.parse(hashedPassphrase),
{
name: "HMAC",
hash: "SHA-256",
},
false,
["sign"]
);
const signature = await subtle.sign("HMAC", key, UTF8Encoder.parse(message));
return HexEncoder.stringify(new Uint8Array(signature));
}
exports.signMessage = signMessage;
function getRandomAlphanum() {
const possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let byteArray;
let parsedInt;
// Keep generating new random bytes until we get a value that falls
// within a range that can be evenly divided by possibleCharacters.length
do {
byteArray = crypto.getRandomValues(new Uint8Array(1));
// extract the lowest byte to get an int from 0 to 255 (probably unnecessary, since we're only generating 1 byte)
parsedInt = byteArray[0] & 0xff;
} while (parsedInt >= 256 - (256 % possibleCharacters.length));
// Take the modulo of the parsed integer to get a random number between 0 and totalLength - 1
const randomIndex = parsedInt % possibleCharacters.length;
return possibleCharacters[randomIndex];
}
/**
* Generate a random string of a given length.
*
* @param {int} length
* @returns {string}
*/
function generateRandomString(length) {
let randomString = '';
for (let i = 0; i < length; i++) {
randomString += getRandomAlphanum();
}
return randomString;
}
exports.generateRandomString = generateRandomString;
return exports;
})())
const codec = ((function(){
const exports = {};
/**
* Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
*
* @param cryptoEngine - the engine to use for encryption / decryption
*/
function init(cryptoEngine) {
const exports = {};
/**
* Top-level function for encoding a message.
* Includes password hashing, encryption, and signing.
*
* @param {string} msg
* @param {string} password
* @param {string} salt
*
* @returns {string} The encoded text
*/
async function encode(msg, password, salt) {
const hashedPassphrase = await cryptoEngine.hashPassphrase(password, salt);
const encrypted = await cryptoEngine.encrypt(msg, hashedPassphrase);
// we use the hashed password in the HMAC because this is effectively what will be used a password (so we can store
// it in localStorage safely, we don't use the clear text password)
const hmac = await cryptoEngine.signMessage(hashedPassphrase, encrypted);
return hmac + encrypted;
}
exports.encode = encode;
/**
* Top-level function for decoding a message.
* Includes signature check and decryption.
*
* @param {string} signedMsg
* @param {string} hashedPassphrase
* @param {string} salt
* @param {int} backwardCompatibleAttempt
* @param {string} originalPassphrase
*
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
*/
async function decode(
signedMsg,
hashedPassphrase,
salt,
backwardCompatibleAttempt = 0,
originalPassphrase = ''
) {
const encryptedHMAC = signedMsg.substring(0, 64);
const encryptedMsg = signedMsg.substring(64);
const decryptedHMAC = await cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
if (decryptedHMAC !== encryptedHMAC) {
// we have been raising the number of iterations in the hashing algorithm multiple times, so to support the old
// remember-me/autodecrypt links we need to try bringing the old hashes up to speed.
originalPassphrase = originalPassphrase || hashedPassphrase;
if (backwardCompatibleAttempt === 0) {
const updatedHashedPassphrase = await cryptoEngine.hashThirdRound(originalPassphrase, salt);
return decode(signedMsg, updatedHashedPassphrase, salt, backwardCompatibleAttempt + 1, originalPassphrase);
}
if (backwardCompatibleAttempt === 1) {
let updatedHashedPassphrase = await cryptoEngine.hashSecondRound(originalPassphrase, salt);
updatedHashedPassphrase = await cryptoEngine.hashThirdRound(updatedHashedPassphrase, salt);
return decode(signedMsg, updatedHashedPassphrase, salt, backwardCompatibleAttempt + 1, originalPassphrase);
}
return { success: false, message: "Signature mismatch" };
}
return {
success: true,
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
};
}
exports.decode = decode;
return exports;
}
exports.init = init;
return exports;
})())
const decode = codec.init(cryptoEngine).decode;
/**
* Initialize the staticrypt module, that exposes functions callbable by the password_template.
*
* @param {{
* encryptedMsg: string,
* isRememberEnabled: boolean,
* rememberDurationInDays: number,
* salt: string,
* }} staticryptConfig - object of data that is stored on the password_template at encryption time.
*
* @param {{
* rememberExpirationKey: string,
* rememberPassphraseKey: string,
* replaceHtmlCallback: function,
* clearLocalStorageCallback: function,
* }} templateConfig - object of data that can be configured by a custom password_template.
*/
function init(staticryptConfig, templateConfig) {
const exports = {};
/**
* Decrypt our encrypted page, replace the whole HTML.
*
* @param {string} hashedPassphrase
* @returns {Promise<boolean>}
*/
async function decryptAndReplaceHtml(hashedPassphrase) {
const { encryptedMsg, salt } = staticryptConfig;
const { replaceHtmlCallback } = templateConfig;
const result = await decode(encryptedMsg, hashedPassphrase, salt);
if (!result.success) {
return false;
}
const plainHTML = result.decoded;
// if the user configured a callback call it, otherwise just replace the whole HTML
if (typeof replaceHtmlCallback === 'function') {
replaceHtmlCallback(plainHTML);
} else {
document.write(plainHTML);
document.close();
}
return true;
}
/**
* Attempt to decrypt the page and replace the whole HTML.
*
* @param {string} password
* @param {boolean} isRememberChecked
*
* @returns {Promise<{isSuccessful: boolean, hashedPassword?: string}>} - we return an object, so that if we want to
* expose more information in the future we can do it without breaking the password_template
*/
async function handleDecryptionOfPage(password, isRememberChecked) {
const { isRememberEnabled, rememberDurationInDays, salt } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// decrypt and replace the whole page
const hashedPassword = await cryptoEngine.hashPassphrase(password, salt);
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassword);
if (!isDecryptionSuccessful) {
return {
isSuccessful: false,
hashedPassword,
};
}
// remember the hashedPassword and set its expiration if necessary
if (isRememberEnabled && isRememberChecked) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassword);
// set the expiration if the duration isn't 0 (meaning no expiration)
if (rememberDurationInDays > 0) {
window.localStorage.setItem(
rememberExpirationKey,
(new Date().getTime() + rememberDurationInDays * 24 * 60 * 60 * 1000).toString()
);
}
}
return {
isSuccessful: true,
hashedPassword,
};
}
exports.handleDecryptionOfPage = handleDecryptionOfPage;
/**
* Clear localstorage from staticrypt related values
*/
function clearLocalStorage() {
const { clearLocalStorageCallback, rememberExpirationKey, rememberPassphraseKey } = templateConfig;
if (typeof clearLocalStorageCallback === 'function') {
clearLocalStorageCallback();
} else {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
}
}
async function handleDecryptOnLoad() {
let isSuccessful = await decryptOnLoadFromUrl();
if (!isSuccessful) {
isSuccessful = await decryptOnLoadFromRememberMe();
}
return { isSuccessful };
}
exports.handleDecryptOnLoad = handleDecryptOnLoad;
/**
* Clear storage if we are logging out
*
* @returns {boolean} - whether we logged out
*/
function logoutIfNeeded() {
const logoutKey = "staticrypt_logout";
// handle logout through query param
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has(logoutKey)) {
clearLocalStorage();
return true;
}
// handle logout through URL fragment
const hash = window.location.hash.substring(1);
if (hash.includes(logoutKey)) {
clearLocalStorage();
return true;
}
return false;
}
/**
* To be called on load: check if we want to try to decrypt and replace the HTML with the decrypted content, and
* try to do it if needed.
*
* @returns {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
*/
async function decryptOnLoadFromRememberMe() {
const { rememberDurationInDays } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// if we are login out, terminate
if (logoutIfNeeded()) {
return false;
}
// if there is expiration configured, check if we're not beyond the expiration
if (rememberDurationInDays && rememberDurationInDays > 0) {
const expiration = localStorage.getItem(rememberExpirationKey),
isExpired = expiration && new Date().getTime() > parseInt(expiration);
if (isExpired) {
clearLocalStorage();
return false;
}
}
const hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
if (hashedPassphrase) {
// try to decrypt
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
// if the decryption is unsuccessful the password might be wrong - silently clear the saved data and let
// the user fill the password form again
if (!isDecryptionSuccessful) {
clearLocalStorage();
return false;
}
return true;
}
return false;
}
function decryptOnLoadFromUrl() {
const passwordKey = "staticrypt_pwd";
// get the password from the query param
const queryParams = new URLSearchParams(window.location.search);
const hashedPassphraseQuery = queryParams.get(passwordKey);
// get the password from the url fragment
const hashRegexMatch = window.location.hash.substring(1).match(new RegExp(passwordKey + "=(.*)"));
const hashedPassphraseFragment = hashRegexMatch ? hashRegexMatch[1] : null;
const hashedPassphrase = hashedPassphraseFragment || hashedPassphraseQuery;
if (hashedPassphrase) {
return decryptAndReplaceHtml(hashedPassphrase);
}
return false;
}
return exports;
}
exports.init = init;
return exports;
})())
</script>
<script>
const encode = codec.init(cryptoEngine).encode;
// enable CKEDIRTOR
CKEDITOR.replace('instructions');
CKEDITOR.replace('template_instructions');
let htmlToDownload;
/**
* Extract js code from <script> tag and return it as a string
*
* @param id
* @returns
* @param {string} id
* @returns {string}
*/
function getScriptAsString(id) {
return document.getElementById(id)
@ -615,10 +1205,10 @@ exports.renderTemplate = renderTemplate;
}
/**
* Register something happened - this uses a simple Supabase function to implement a counter, and allows use to drop
* google analytics.
* Register something happened - this uses a simple Supabase function to implement a counter, and allows to drop
* google analytics. We don't store any personal data or IP.
*
* @param action
* @param {string} action
*/
function trackEvent(action) {
const xhr = new XMLHttpRequest();
@ -667,7 +1257,7 @@ exports.renderTemplate = renderTemplate;
// update instruction textarea value with CKEDITOR content
// (see https://stackoverflow.com/questions/3147670/ckeditor-update-textarea)
CKEDITOR.instances['instructions'].updateElement();
CKEDITOR.instances['template_instructions'].updateElement();
const unencrypted = document.getElementById('unencrypted_html').value,
passphrase = document.getElementById('passphrase').value;
@ -675,27 +1265,28 @@ exports.renderTemplate = renderTemplate;
const salt = cryptoEngine.generateRandomSalt();
const encryptedMsg = await encode(unencrypted, passphrase, salt);
const decryptButton = document.getElementById('decrypt_button').value,
instructions = document.getElementById('instructions').value,
const templateButton = document.getElementById('template_button').value,
templateInstructions = document.getElementById('template_instructions').value,
isRememberEnabled = document.getElementById('remember').checked,
pageTitle = document.getElementById('title').value.trim(),
passphrasePlaceholder = document.getElementById('passphrase_placeholder').value.trim(),
templateTitle = document.getElementById('template_title').value.trim(),
templatePlaceholder = document.getElementById('template_placeholder').value.trim(),
rememberDurationInDays = document.getElementById('remember_in_days').value || 0,
rememberMe = document.getElementById('remember_me').value;
templateRemember = document.getElementById('template_remember').value;
const data = {
crypto_tag: '<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></scr' + 'ipt>',
decrypt_button: decryptButton ? decryptButton : 'DECRYPT',
encrypted: encryptedMsg,
instructions: instructions ? instructions : '',
is_remember_enabled: isRememberEnabled ? 'true' : 'false',
js_codec: getScriptAsString('codec'),
js_crypto_engine: getScriptAsString('cryptoEngine'),
passphrase_placeholder: passphrasePlaceholder ? passphrasePlaceholder : 'Passphrase',
remember_duration_in_days: rememberDurationInDays.toString(),
remember_me: rememberMe ? rememberMe : 'Remember me',
salt: salt,
title: pageTitle ? pageTitle : 'Protected Page',
staticrypt_config: {
encryptedMsg,
isRememberEnabled,
rememberDurationInDays,
salt,
},
is_remember_enabled: JSON.stringify(isRememberEnabled),
js_staticrypt: getScriptAsString('staticrypt'),
template_button: templateButton ? templateButton : 'DECRYPT',
template_instructions: templateInstructions || '',
template_placeholder: templatePlaceholder || 'Passphrase',
template_remember: templateRemember || 'Remember me',
template_title: templateTitle || 'Protected Page',
};
document.getElementById('encrypted_html_display').textContent = encryptedMsg;

Wyświetl plik

@ -1,95 +0,0 @@
/**
* TODO: delete this file in next major version. This is a version of the codec that doesn't use async, so we can use it
* in old custom password_template. It will only be used with the cryptoJS engine.
*
* Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
*
* @param cryptoEngine - the engine to use for encryption / decryption
*/
function init(cryptoEngine) {
// TODO: remove on next major version bump. This is a hack to make the salt available in all functions here in a
// backward compatible way (not requiring to change the password_template).
const backwardCompatibleSalt = '##SALT##';
const exports = {};
/**
* Top-level function for encoding a message.
* Includes password hashing, encryption, and signing.
*
* @param {string} msg
* @param {string} password
* @param {string} salt
* @param {boolean} isLegacy - whether to use the legacy hashing algorithm (1k iterations) or not
*
* @returns {string} The encoded text
*/
function encode(msg, password, salt, isLegacy = false) {
// TODO: remove in the next major version bump. This is to not break backwards compatibility with the old way of hashing
const hashedPassphrase = isLegacy
? cryptoEngine.hashLegacyRound(password, salt)
: cryptoEngine.hashPassphrase(password, salt);
const encrypted = cryptoEngine.encrypt(msg, hashedPassphrase);
// we use the hashed password in the HMAC because this is effectively what will be used a password (so we can store
// it in localStorage safely, we don't use the clear text password)
const hmac = cryptoEngine.signMessage(hashedPassphrase, encrypted);
return hmac + encrypted;
}
exports.encode = encode;
/**
* Top-level function for decoding a message.
* Includes signature check and decryption.
*
* @param {string} signedMsg
* @param {string} hashedPassphrase
* @param {string} backwardCompatibleHashedPassword
*
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
*/
function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
const encryptedHMAC = signedMsg.substring(0, 64);
const encryptedMsg = signedMsg.substring(64);
const decryptedHMAC = cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
if (decryptedHMAC !== encryptedHMAC) {
// TODO: remove in next major version bump. This is to not break backwards compatibility with the old 1k
// iterations in PBKDF2 - if the key we try isn't working, it might be because it's a remember-me/autodecrypt
// link key, generated with 1k iterations. Try again with the updated iteration count.
if (!backwardCompatibleHashedPassword) {
return decode(
signedMsg,
cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
hashedPassphrase
);
}
return { success: false, message: "Signature mismatch" };
}
// TODO: remove in next major version bump. If we're trying to double hash for backward compatibility reasons,
// and the attempt is successful, we check if we should update the stored password in localStorage. This avoids
// having to compute the upgrade each time.
if (backwardCompatibleHashedPassword) {
if (window && window.localStorage) {
const storedPassword = window.localStorage.getItem('staticrypt_passphrase');
// check the stored password is actually the backward compatible one, so we don't save the new one and trigger
// the "remember-me" by mistake, leaking the password
if (storedPassword === backwardCompatibleHashedPassword) {
window.localStorage.setItem('staticrypt_passphrase', hashedPassphrase);
}
}
}
return {
success: true,
decoded: cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
};
}
exports.decode = decode;
return exports;
}
exports.init = init;

Wyświetl plik

@ -8,8 +8,6 @@ const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*
* Mirrors the API of CryptoJS.enc.Hex
*/
const HexEncoder = {
/**

Wyświetl plik

@ -1,138 +0,0 @@
const CryptoJS = require("crypto-js");
/**
* Salt and encrypt a msg with a password.
*/
function encrypt(msg, hashedPassphrase) {
var iv = CryptoJS.lib.WordArray.random(128 / 8);
var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, {
iv: iv,
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC,
});
// iv will be hex 16 in length (32 characters)
// we prepend it to the ciphertext for use in decryption
return iv.toString() + encrypted.toString();
}
exports.encrypt = encrypt;
/**
* Decrypt a salted msg using a password.
*
* @param {string} encryptedMsg
* @param {string} hashedPassphrase
* @returns {string}
*/
function decrypt(encryptedMsg, hashedPassphrase) {
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, 32));
var encrypted = encryptedMsg.substring(32);
return CryptoJS.AES.decrypt(encrypted, hashedPassphrase, {
iv: iv,
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC,
}).toString(CryptoJS.enc.Utf8);
}
exports.decrypt = decrypt;
/**
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} passphrase
* @param {string} salt
* @returns string
*/
function hashPassphrase(passphrase, salt) {
// we hash the passphrase in two steps: first 1k iterations, then we add iterations. This is because we used to use 1k,
// so for backwards compatibility with remember-me/autodecrypt links, we need to support going from that to more
// iterations
var hashedPassphrase = hashLegacyRound(passphrase, salt);
return hashSecondRound(hashedPassphrase, salt);
}
exports.hashPassphrase = hashPassphrase;
/**
* This hashes the passphrase with 1k iterations. This is a low number, we need this function to support backwards
* compatibility.
*
* @param {string} passphrase
* @param {string} salt
* @returns {string}
*/
function hashLegacyRound(passphrase, salt) {
return CryptoJS.PBKDF2(passphrase, salt, {
keySize: 256 / 32,
iterations: 1000,
}).toString();
}
exports.hashLegacyRound = hashLegacyRound;
/**
* Add a second round of iterations. This is because we used to use 1k, so for backwards compatibility with
* remember-me/autodecrypt links, we need to support going from that to more iterations.
*
* @param hashedPassphrase
* @param salt
* @returns {string}
*/
function hashSecondRound(hashedPassphrase, salt) {
return CryptoJS.PBKDF2(hashedPassphrase, salt, {
keySize: 256 / 32,
iterations: 14000,
hasher: CryptoJS.algo.SHA256,
}).toString();
}
exports.hashSecondRound = hashSecondRound;
function generateRandomSalt() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
}
exports.generateRandomSalt = generateRandomSalt;
function getRandomAlphanum() {
var possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var byteArray;
var parsedInt;
// Keep generating new random bytes until we get a value that falls
// within a range that can be evenly divided by possibleCharacters.length
do {
byteArray = CryptoJS.lib.WordArray.random(1);
// extract the lowest byte to get an int from 0 to 255 (probably unnecessary, since we're only generating 1 byte)
parsedInt = byteArray.words[0] & 0xff;
} while (parsedInt >= 256 - (256 % possibleCharacters.length));
// Take the modulo of the parsed integer to get a random number between 0 and totalLength - 1
var randomIndex = parsedInt % possibleCharacters.length;
return possibleCharacters[randomIndex];
}
/**
* Generate a random string of a given length.
*
* @param {int} length
* @returns {string}
*/
function generateRandomString(length) {
var randomString = '';
for (var i = 0; i < length; i++) {
randomString += getRandomAlphanum();
}
return randomString;
}
exports.generateRandomString = generateRandomString;
function signMessage(hashedPassphrase, message) {
return CryptoJS.HmacSHA256(
message,
CryptoJS.SHA256(hashedPassphrase).toString()
).toString();
}
exports.signMessage = signMessage;

Wyświetl plik

@ -1,5 +1,9 @@
/**
* Replace the placeholder tags (between '{tag}') in the template string with provided data.
* Replace the variable in template tags, between '/*[|variable|]* /0' (without the space in '* /0', ommiting it would
* break this comment), with the provided data.
*
* This weird format is so that we have something that doesn't break JS parser in the template files (it understands it
* as '0'), so we can still use auto-formatting.
*
* @param {string} templateString
* @param {Object} data
@ -7,12 +11,16 @@
* @returns string
*/
function renderTemplate(templateString, data) {
return templateString.replace(/{\s*(\w+)\s*}/g, function (_, key) {
if (data && data[key] !== undefined) {
return data[key];
return templateString.replace(/\/\*\[\|\s*(\w+)\s*\|]\*\/0/g, function (_, key) {
if (!data || data[key] === undefined) {
return key;
}
return "";
if (typeof data[key] === 'object') {
return JSON.stringify(data[key]);
}
return data[key];
});
}
exports.renderTemplate = renderTemplate;

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -2,7 +2,7 @@
<html class="staticrypt-html">
<head>
<meta charset="utf-8">
<title>{title}</title>
<title>/*[|template_title|]*/0</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- do not cache this page -->
@ -156,8 +156,8 @@
<div class="staticrypt-page">
<div class="staticrypt-form">
<div class="staticrypt-instructions">
<p class="staticrypt-title">{title}</p>
<p>{instructions}</p>
<p class="staticrypt-title">/*[|template_title|]*/0</p>
<p>/*[|template_instructions|]*/0</p>
</div>
<hr class="staticrypt-hr">
@ -166,179 +166,57 @@
<input id="staticrypt-password"
type="password"
name="password"
placeholder="{passphrase_placeholder}"
placeholder="/*[|template_placeholder|]*/0"
autofocus/>
<label id="staticrypt-remember-label" class="staticrypt-remember hidden">
<input id="staticrypt-remember"
type="checkbox"
name="remember"/>
{remember_me}
/*[|template_remember|]*/0
</label>
<input type="submit" class="staticrypt-decrypt-button" value="{decrypt_button}"/>
<input type="submit" class="staticrypt-decrypt-button" value="/*[|template_button|]*/0"/>
</form>
</div>
</div>
</div>
{crypto_tag}
<script>
// do not remove this comment, it is used to detect the version of the script
// STATICRYPT_VERSION: async
// these variables will be filled when generating the file - the template format is '/*[|variable_name|]*/0'
const staticryptInitiator = /*[|js_staticrypt|]*/0
const templateError = '/*[|template_error|]*/0',
isRememberEnabled = /*[|is_remember_enabled|]*/0,
staticryptConfig = /*[|staticrypt_config|]*/0;
const cryptoEngine = {js_crypto_engine}
const codec = {js_codec}
const decode = codec.init(cryptoEngine).decode;
// you can edit these values to customize some of the behavior of StatiCrypt
const templateConfig = {
rememberExpirationKey: 'staticrypt_expiration',
rememberPassphraseKey: 'staticrypt_passphrase',
replaceHtmlCallback: null,
clearLocalStorageCallback: null,
};
// variables to be filled when generating the file
const encryptedMsg = '{encrypted}',
salt = '{salt}',
labelError = '{label_error}',
isRememberEnabled = {is_remember_enabled},
rememberDurationInDays = {remember_duration_in_days}; // 0 means forever
// constants
const rememberPassphraseKey = 'staticrypt_passphrase',
rememberExpirationKey = 'staticrypt_expiration';
/**
* Decrypt our encrypted page, replace the whole HTML.
*
* @param {string} hashedPassphrase
* @returns {Promise<boolean>}
*/
async function decryptAndReplaceHtml(hashedPassphrase) {
const result = await decode(encryptedMsg, hashedPassphrase, salt);
if (!result.success) {
return false;
}
const plainHTML = result.decoded;
document.write(plainHTML);
document.close();
return true;
}
/**
* Clear localstorage from staticrypt related values
*/
function clearLocalStorage() {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
}
/**
* Clear storage if we are logging out
*
* @returns {boolean} - whether we logged out
*/
function logoutIfNeeded() {
const logoutKey = "staticrypt_logout";
// handle logout through query param
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has(logoutKey)) {
clearLocalStorage();
return true;
}
// handle logout through URL fragment
const hash = window.location.hash.substring(1);
if (hash.includes(logoutKey)) {
clearLocalStorage();
return true;
}
return false;
}
/**
* To be called on load: check if we want to try to decrypt and replace the HTML with the decrypted content, and
* try to do it if needed.
*
* @returns {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
*/
async function decryptOnLoadFromRememberMe() {
if (!isRememberEnabled) {
return false;
}
// show the remember me checkbox
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
// if we are login out, terminate
if (logoutIfNeeded()) {
return false;
}
// if there is expiration configured, check if we're not beyond the expiration
if (rememberDurationInDays && rememberDurationInDays > 0) {
const expiration = localStorage.getItem(rememberExpirationKey),
isExpired = expiration && new Date().getTime() > parseInt(expiration);
if (isExpired) {
clearLocalStorage();
return false;
}
}
const hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
if (hashedPassphrase) {
// try to decrypt
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
// if the decryption is unsuccessful the password might be wrong - silently clear the saved data and let
// the user fill the password form again
if (!isDecryptionSuccessful) {
clearLocalStorage();
return false;
}
return true;
}
return false;
}
function decryptOnLoadFromUrl() {
const passwordKey = "staticrypt_pwd";
// get the password from the query param
const queryParams = new URLSearchParams(window.location.search);
const hashedPassphraseQuery = queryParams.get(passwordKey);
// get the password from the url fragment
const hashRegexMatch = window.location.hash.substring(1).match(new RegExp(passwordKey + "=(.*)"));
const hashedPassphraseFragment = hashRegexMatch ? hashRegexMatch[1] : null;
const hashedPassphrase = hashedPassphraseFragment || hashedPassphraseQuery;
if (hashedPassphrase) {
return decryptAndReplaceHtml(hashedPassphrase);
}
return false;
}
// init the staticrypt engine
const staticrypt = staticryptInitiator.init(staticryptConfig, templateConfig);
// try to automatically decrypt on load if there is a saved password
window.onload = async function () {
let hasDecrypted = await decryptOnLoadFromUrl();
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
if (!hasDecrypted) {
hasDecrypted = await decryptOnLoadFromRememberMe();
}
// if we didn't decrypt anything, show the password prompt. Otherwise the content has already been replaced, no
// need to do anything
if (!hasDecrypted) {
// if we didn't decrypt anything on load, show the password prompt. Otherwise the content has already been
// replaced, no need to do anything
if (!isSuccessful) {
// hide loading screen
document.getElementById("staticrypt_loading").classList.add("hidden");
document.getElementById("staticrypt_content").classList.remove("hidden");
document.getElementById("staticrypt-password").focus();
// show the remember me checkbox
if (isRememberEnabled) {
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
}
}
}
@ -347,27 +225,12 @@
e.preventDefault();
const passphrase = document.getElementById('staticrypt-password').value,
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
isRememberChecked = document.getElementById('staticrypt-remember').checked;
// decrypt and replace the whole page
const hashedPassphrase = await cryptoEngine.hashPassphrase(passphrase, salt);
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(passphrase, isRememberChecked);
if (isDecryptionSuccessful) {
// remember the hashedPassphrase and set its expiration if necessary
if (isRememberEnabled && shouldRememberPassphrase) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassphrase);
// set the expiration if the duration isn't 0 (meaning no expiration)
if (rememberDurationInDays > 0) {
window.localStorage.setItem(
rememberExpirationKey,
(new Date().getTime() + rememberDurationInDays * 24 * 60 * 60 * 1000).toString()
);
}
}
} else {
alert(labelError);
if (!isSuccessful) {
alert(templateError);
}
});
</script>

215
lib/staticryptJs.js 100644
Wyświetl plik

@ -0,0 +1,215 @@
const cryptoEngine = /*[|js_crypto_engine|]*/0
const codec = /*[|js_codec|]*/0
const decode = codec.init(cryptoEngine).decode;
/**
* Initialize the staticrypt module, that exposes functions callbable by the password_template.
*
* @param {{
* encryptedMsg: string,
* isRememberEnabled: boolean,
* rememberDurationInDays: number,
* salt: string,
* }} staticryptConfig - object of data that is stored on the password_template at encryption time.
*
* @param {{
* rememberExpirationKey: string,
* rememberPassphraseKey: string,
* replaceHtmlCallback: function,
* clearLocalStorageCallback: function,
* }} templateConfig - object of data that can be configured by a custom password_template.
*/
function init(staticryptConfig, templateConfig) {
const exports = {};
/**
* Decrypt our encrypted page, replace the whole HTML.
*
* @param {string} hashedPassphrase
* @returns {Promise<boolean>}
*/
async function decryptAndReplaceHtml(hashedPassphrase) {
const { encryptedMsg, salt } = staticryptConfig;
const { replaceHtmlCallback } = templateConfig;
const result = await decode(encryptedMsg, hashedPassphrase, salt);
if (!result.success) {
return false;
}
const plainHTML = result.decoded;
// if the user configured a callback call it, otherwise just replace the whole HTML
if (typeof replaceHtmlCallback === 'function') {
replaceHtmlCallback(plainHTML);
} else {
document.write(plainHTML);
document.close();
}
return true;
}
/**
* Attempt to decrypt the page and replace the whole HTML.
*
* @param {string} password
* @param {boolean} isRememberChecked
*
* @returns {Promise<{isSuccessful: boolean, hashedPassword?: string}>} - we return an object, so that if we want to
* expose more information in the future we can do it without breaking the password_template
*/
async function handleDecryptionOfPage(password, isRememberChecked) {
const { isRememberEnabled, rememberDurationInDays, salt } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// decrypt and replace the whole page
const hashedPassword = await cryptoEngine.hashPassphrase(password, salt);
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassword);
if (!isDecryptionSuccessful) {
return {
isSuccessful: false,
hashedPassword,
};
}
// remember the hashedPassword and set its expiration if necessary
if (isRememberEnabled && isRememberChecked) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassword);
// set the expiration if the duration isn't 0 (meaning no expiration)
if (rememberDurationInDays > 0) {
window.localStorage.setItem(
rememberExpirationKey,
(new Date().getTime() + rememberDurationInDays * 24 * 60 * 60 * 1000).toString()
);
}
}
return {
isSuccessful: true,
hashedPassword,
};
}
exports.handleDecryptionOfPage = handleDecryptionOfPage;
/**
* Clear localstorage from staticrypt related values
*/
function clearLocalStorage() {
const { clearLocalStorageCallback, rememberExpirationKey, rememberPassphraseKey } = templateConfig;
if (typeof clearLocalStorageCallback === 'function') {
clearLocalStorageCallback();
} else {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
}
}
async function handleDecryptOnLoad() {
let isSuccessful = await decryptOnLoadFromUrl();
if (!isSuccessful) {
isSuccessful = await decryptOnLoadFromRememberMe();
}
return { isSuccessful };
}
exports.handleDecryptOnLoad = handleDecryptOnLoad;
/**
* Clear storage if we are logging out
*
* @returns {boolean} - whether we logged out
*/
function logoutIfNeeded() {
const logoutKey = "staticrypt_logout";
// handle logout through query param
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has(logoutKey)) {
clearLocalStorage();
return true;
}
// handle logout through URL fragment
const hash = window.location.hash.substring(1);
if (hash.includes(logoutKey)) {
clearLocalStorage();
return true;
}
return false;
}
/**
* To be called on load: check if we want to try to decrypt and replace the HTML with the decrypted content, and
* try to do it if needed.
*
* @returns {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
*/
async function decryptOnLoadFromRememberMe() {
const { rememberDurationInDays } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// if we are login out, terminate
if (logoutIfNeeded()) {
return false;
}
// if there is expiration configured, check if we're not beyond the expiration
if (rememberDurationInDays && rememberDurationInDays > 0) {
const expiration = localStorage.getItem(rememberExpirationKey),
isExpired = expiration && new Date().getTime() > parseInt(expiration);
if (isExpired) {
clearLocalStorage();
return false;
}
}
const hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
if (hashedPassphrase) {
// try to decrypt
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
// if the decryption is unsuccessful the password might be wrong - silently clear the saved data and let
// the user fill the password form again
if (!isDecryptionSuccessful) {
clearLocalStorage();
return false;
}
return true;
}
return false;
}
function decryptOnLoadFromUrl() {
const passwordKey = "staticrypt_pwd";
// get the password from the query param
const queryParams = new URLSearchParams(window.location.search);
const hashedPassphraseQuery = queryParams.get(passwordKey);
// get the password from the url fragment
const hashRegexMatch = window.location.hash.substring(1).match(new RegExp(passwordKey + "=(.*)"));
const hashedPassphraseFragment = hashRegexMatch ? hashRegexMatch[1] : null;
const hashedPassphrase = hashedPassphraseFragment || hashedPassphraseQuery;
if (hashedPassphrase) {
return decryptAndReplaceHtml(hashedPassphrase);
}
return false;
}
return exports;
}
exports.init = init;

4
package-lock.json wygenerowano
Wyświetl plik

@ -1,12 +1,12 @@
{
"name": "staticrypt",
"version": "2.4.0",
"version": "3.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "staticrypt",
"version": "2.4.0",
"version": "3.0.0",
"license": "MIT",
"dependencies": {
"crypto-js": "3.1.9-1",

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "staticrypt",
"version": "2.6.0",
"version": "3.0.0",
"description": "Based on the [crypto-js](https://github.com/brix/crypto-js) library, StatiCrypt uses AES-256 to encrypt your input with your passphrase and put it in a HTML file with a password prompt that can decrypted in-browser (client side).",
"main": "index.js",
"files": [

Wyświetl plik

@ -1,12 +1,15 @@
# Build the website files
# Should be run with "npm run build" - npm handles the pathing better (so no "#!/usr/bin/env" bash on top)
# encrypt the example file
node cli/index.js example/example.html test \
--engine webcrypto \
--short \
--salt b93bbaf35459951c47721d1f3eaeb5b9 \
--instructions "Enter \"test\" to unlock the page"
# build the index.html file
node ./scripts/buildIndex.js
# encrypt the example file
cd example
node ../cli/index.js example.html \
-p test \
--short \
--salt b93bbaf35459951c47721d1f3eaeb5b9 \
--config false \
--template-instructions "Enter \"test\" to unlock the page"

Wyświetl plik

@ -1,9 +1,10 @@
const { convertCommonJSToBrowserJS, genFile } = require("../cli/helpers.js");
const { convertCommonJSToBrowserJS, genFile, buildStaticryptJS} = require("../cli/helpers.js");
const data = {
js_codec: convertCommonJSToBrowserJS("lib/codec"),
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine/webcryptoEngine"),
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
js_formater: convertCommonJSToBrowserJS("lib/formater"),
js_staticrypt: buildStaticryptJS(),
};
genFile(data, "./index.html", "./scripts/index_template.html");

Wyświetl plik

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