Skip to content

Commit

Permalink
Support for WebCrypto in 2.x (#164)
Browse files Browse the repository at this point in the history
Closes #139 , closes #20 , closes #111
  • Loading branch information
robinmoisson authored Mar 29, 2023
1 parent f824538 commit 2697766
Show file tree
Hide file tree
Showing 14 changed files with 1,220 additions and 567 deletions.
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,41 @@ You can then run it with `npx staticrypt ...`. You can also install globally wit

### Examples

> These 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".
> 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):

```bash
staticrypt test.html MY_LONG_PASSWORD
staticrypt test.html MY_LONG_PASSWORD --engine webcrypto
```

**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
staticrypt test.html --engine webcrypto
```

**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
staticrypt test.html MY_LONG_PASSWORD --share https://example.com/test_encrypted.html --engine webcrypto
# => 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/{}`):

```bash
find . -type f -name "*.html" -exec staticrypt {} MY_LONG_PASSWORD -o {} \;
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 \;
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_LONG_PASSWORD --engine webcrypto \;
```

### CLI Reference
Expand All @@ -73,6 +75,10 @@ The password argument is optional if `STATICRYPT_PASSWORD` is set in the environ
-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"]
Expand Down Expand Up @@ -113,19 +119,19 @@ 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 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 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.

So it basically encrypts your page and puts everything in a user-friendly way to use a password in the new file.

## FAQ

### Is it secure?

Simple answer: your file content has been encrypted with AES-256 (CBC), 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 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.

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 15k PBKDF2 iterations (it will be 600k when we'll switch to WebCrypto, 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 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)).

**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.

Expand All @@ -137,7 +143,17 @@ Yes! Just copy `lib/password_template.html`, modify it to suit your style and po

If you don't want the checkbox to be included, you can add the `--noremember` flag to disable it.

### Why do we embed the whole crypto-js library in each encrypted file by default?
### Should I use the WebCrypto or CryptoJS engine?

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`.

Expand Down Expand Up @@ -218,6 +234,4 @@ Here are some other projects and community resources you might find interesting

### Based on StatiCrypt

**WebCrypto:** https://github.com/tarpdalton/staticrypt/tree/webcrypto is a fork that uses the WebCrypto browser api to encrypt and decrypt the page, which removes the need for `crypto-js`. There's a PR open towards here which I haven't checked in detail yet. WebCrypto is only available in HTTPS context (which [is annoying people](https://github.com/w3c/webcrypto/issues/28)) so it won't work if you're on HTTP.

**Template to host an encrypted single page website with Github Pages:** [a-nau/password-protected-website-template](https://github.com/a-nau/password-protected-website-template) is a demonstration of how to build a protected page on Github Pages, integrating with Github Actions
65 changes: 43 additions & 22 deletions cli/helpers.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
const fs = require("fs");

const cryptoEngine = require("../lib/cryptoEngine/cryptojsEngine");
const { generateRandomSalt } = require("../lib/cryptoEngine/webcryptoEngine.js");
const path = require("path");
const {renderTemplate} = require("../lib/formater.js");
const Yargs = require("yargs");
const { generateRandomSalt } = cryptoEngine;

const PASSWORD_TEMPLATE_DEFAULT_PATH = path.join(__dirname, "..", "lib", "password_template.html");


/**
* @param {string} message
*/
function exitEarly(message) {
console.log(message);
function exitWithError(message) {
console.log("ERROR: " + message);
process.exit(1);
}
exports.exitEarly = exitEarly;
exports.exitWithError = exitWithError;

/**
* Check if a particular option has been set by the user. Useful for distinguishing default value with flag without
Expand Down Expand Up @@ -66,7 +65,7 @@ function getPassword(positionalArguments) {
}

if (positionalArguments.length < 2) {
exitEarly("Missing password: please provide an argument or set the STATICRYPT_PASSWORD environment variable in the environment or .env file");
exitWithError("missing password, please provide an argument or set the STATICRYPT_PASSWORD environment variable in the environment or .env file");
}

return positionalArguments[1].toString();
Expand All @@ -81,7 +80,7 @@ function getFileContent(filepath) {
try {
return fs.readFileSync(filepath, "utf8");
} catch (e) {
exitEarly("Failure: input file does not exist!");
exitWithError("input file does not exist!");
}
}
exports.getFileContent = getFileContent;
Expand Down Expand Up @@ -119,7 +118,7 @@ function convertCommonJSToBrowserJS(modulePath) {
const resolvedPath = path.join(rootDirectory, ...modulePath.split("/")) + ".js";

if (!fs.existsSync(resolvedPath)) {
exitEarly(`Failure: could not find module to convert at path "${resolvedPath}"`);
exitWithError(`could not find module to convert at path "${resolvedPath}"`);
}

const moduleText = fs
Expand All @@ -145,7 +144,7 @@ function readFile(filePath, errorName = file) {
try {
return fs.readFileSync(filePath, "utf8");
} catch (e) {
exitEarly(`Failure: could not read ${errorName}!`);
exitWithError(`could not read ${errorName}!`);
}
}

Expand All @@ -164,7 +163,7 @@ function genFile(data, outputFilePath, templateFilePath) {
try {
fs.writeFileSync(outputFilePath, renderedTemplate);
} catch (e) {
exitEarly("Failure: could not generate output file!");
exitWithError("could not generate output file!");
}
}
exports.genFile = genFile;
Expand All @@ -179,22 +178,39 @@ exports.genFile = genFile;
* @returns {boolean}
*/
function isCustomPasswordTemplateLegacy(templatePathParameter) {
// if the user uses the default template, it's up to date
if (templatePathParameter === PASSWORD_TEMPLATE_DEFAULT_PATH) {
return false;
}

const customTemplateContent = readFile(templatePathParameter, "template");

// if the template injects the crypto engine, it's up to date
if (customTemplateContent.includes("js_crypto_engine")) {
return false;
}

return true;
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}
*/
function isCustomPasswordTemplateDefault(templatePathParameter) {
// if the user uses the default template, it's up to date
return templatePathParameter === PASSWORD_TEMPLATE_DEFAULT_PATH;
}
exports.isCustomPasswordTemplateDefault = isCustomPasswordTemplateDefault;

function parseCommandLineArguments() {
return Yargs.usage("Usage: staticrypt <filename> [<password>] [options]")
.option("c", {
Expand All @@ -211,10 +227,15 @@ function parseCommandLineArguments() {
.option("e", {
alias: "embed",
type: "boolean",
describe:
"Whether or not to embed crypto-js in the page (or use an external CDN).",
describe: "Whether or not to embed crypto-js in the page (or use an external CDN).",
default: true,
})
.option("engine", {
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",
Expand Down
Loading

0 comments on commit 2697766

Please sign in to comment.