add support for config file + save salt

pull/131/head
Robin Moisson 2022-04-23 12:00:18 +02:00
rodzic 8548bf3ce4
commit cad496355f
4 zmienionych plików z 160 dodań i 83 usunięć

2
.gitignore vendored
Wyświetl plik

@ -1,2 +1,4 @@
.idea
node_modules
cli/.staticrypt.json
.staticrypt.json

Wyświetl plik

@ -29,55 +29,64 @@ Staticrypt is available through npm as a CLI, install with `npm install -g stati
Options:
--help Show help [boolean]
--version Show version number [boolean]
-c, --config Path to the config file. Set to "none" 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]
-o, --output File name / path for generated encrypted file.
[string] [default: null]
-t, --title Title for output HTML page.
[string] [default: "Protected Page"]
-i, --instructions Special instructions to display to the user.
[string] [default: ""]
-f, --file-template Path to custom HTML template with passphrase
prompt.
[string] [default: "./password_template.html"]
[string] [default: "/geek/staticrypt/cli/password_template.html"]
-i, --instructions Special instructions to display to the user.
[string] [default: ""]
--noremember Set this flag to remove the "Remember me"
checkbox. [boolean] [default: false]
-o, --output File name / path for generated encrypted file.
[string] [default: null]
--passphrase-placeholder Placeholder to use for the passphrase input.
[string] [default: "Passphrase"]
-r, --remember Expiration in days of the "Remember me" checkbox
that will save the (salted + hashed) passphrase
in localStorage when entered by the user.
Default: "0", no expiration.
[number] [default: 0]
--noremember Set this flag to remove the "Remember me"
checkbox. [boolean] [default: false]
--remember-label Label to use for the "Remember me" checkbox.
[string] [default: "Remember me"]
--passphrase-placeholder Placeholder to use for the passphrase input.
[string] [default: "Passphrase"]
-s, --salt Set the salt manually. It should be set if you
want 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]
--decrypt-button Label to use for the decrypt button. Default:
"DECRYPT". [string] [default: "DECRYPT"]
-t, --title Title for output HTML page.
[string] [default: "Protected Page"]
### Example usages
> 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 "none".
Encrypt `test.html` and create a `test_encrypted.html` file (add `-o my_encrypted_file.html` to change the name of the output file):
```
staticrypt test.html MY_PASSPHRASE
```
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 a `encrypted/` directory, you could use `-o encrypted/{}`):
```
find . -type f -name "*.html" -exec staticrypt {} MY_PASSPHRASE -o {} \;
```
Encrypt all html files in a directory except the ones ending in `_encrypted.html`:
```
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_PASSPHRASE -s MY_SALT \;
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_PASSPHRASE \;
```
Replace `MY_PASSPHRASE` with a secure passphrase, and `MY_SALT` with a random 32 character long hexadecimal string (it should look like this `c5bcf27cc5e5bb1ecbc41f3da4470dea`, you can generate one with `staticrypt -s` or `staticrypt --salt`). The salt parameter is required if you want to have the same "Remember me" checkbox work on all pages, see detail in the corresponding section of this doc.
### "Remember me" checkbox
By default, the CLI will add a "Remember me" checkbox on the password prompt. If checked, when the user enters their passphrase its salted hashed value will be stored in localStorage. In case this value becomes compromised an attacker can decrypt the page, but this should hopefully protect against password reuse attack (of course please use a unique passphrase nonetheless).
@ -110,6 +119,16 @@ If you don't want the checkbox to be included, you can add the `--noremember` fl
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`.
### Why does staticrypt create a config file?
The "Remember me" feature stores the user password hashed and salted in the browser's localStorage, so it needs the salt to be the same each time you encrypt otherwise the user would be logged out when you encrypt the page again. The config file is a way to store the salt in between runs, so you don't have to remember it and pass it manually.
When deciding what salt to use, staticrypt will first look for a `--salt` flag, then try to get the salt from the config file, and if it still doesn't find a salt it will generate a random one. It then saves the salt in the config file.
If you don't want staticrypt to create or use the config file, you can set `--config none` to disable it.
The salt isn't secret, so you don't need to worry about hiding the config file.
## 🙏 Contribution
Thank you: [@AaronCoplan](https://github.com/AaronCoplan) for bringing the CLI to life, [@epicfaace](https://github.com/epicfaace) & [@thomasmarr](https://github.com/thomasmarr) for sparking the caching of the passphrase in localStorage (allowing the "Remember me" checkbox)

Wyświetl plik

@ -1,3 +1,5 @@
![password prompt preview](preview.png)
# StatiCrypt
Based on the [crypto-js](https://github.com/brix/crypto-js) library, StatiCrypt uses AES-256 to encrypt your string with your passphrase in your browser (client side).
@ -27,55 +29,64 @@ Staticrypt is available through npm as a CLI, install with `npm install -g stati
Options:
--help Show help [boolean]
--version Show version number [boolean]
-c, --config Path to the config file. Set to "none" 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]
-o, --output File name / path for generated encrypted file.
[string] [default: null]
-t, --title Title for output HTML page.
[string] [default: "Protected Page"]
-i, --instructions Special instructions to display to the user.
[string] [default: ""]
-f, --file-template Path to custom HTML template with passphrase
prompt.
[string] [default: "./password_template.html"]
[string] [default: "/geek/staticrypt/cli/password_template.html"]
-i, --instructions Special instructions to display to the user.
[string] [default: ""]
--noremember Set this flag to remove the "Remember me"
checkbox. [boolean] [default: false]
-o, --output File name / path for generated encrypted file.
[string] [default: null]
--passphrase-placeholder Placeholder to use for the passphrase input.
[string] [default: "Passphrase"]
-r, --remember Expiration in days of the "Remember me" checkbox
that will save the (salted + hashed) passphrase
in localStorage when entered by the user.
Default: "0", no expiration.
[number] [default: 0]
--noremember Set this flag to remove the "Remember me"
checkbox. [boolean] [default: false]
--remember-label Label to use for the "Remember me" checkbox.
[string] [default: "Remember me"]
--passphrase-placeholder Placeholder to use for the passphrase input.
[string] [default: "Passphrase"]
-s, --salt Set the salt manually. It should be set if you
want 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]
--decrypt-button Label to use for the decrypt button. Default:
"DECRYPT". [string] [default: "DECRYPT"]
-t, --title Title for output HTML page.
[string] [default: "Protected Page"]
### Example usages
> 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 "none".
Encrypt `test.html` and create a `test_encrypted.html` file (add `-o my_encrypted_file.html` to change the name of the output file):
```
staticrypt test.html MY_PASSPHRASE
```
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 a `encrypted/` directory, you could use `-o encrypted/{}`):
```
find . -type f -name "*.html" -exec staticrypt {} MY_PASSPHRASE -o {} \;
```
Encrypt all html files in a directory except the ones ending in `_encrypted.html`:
```
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_PASSPHRASE -s MY_SALT \;
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_PASSPHRASE \;
```
Replace `MY_PASSPHRASE` with a secure passphrase, and `MY_SALT` with a random 32 character long hexadecimal string (it should look like this `c5bcf27cc5e5bb1ecbc41f3da4470dea`, you can generate one with `staticrypt -s` or `staticrypt --salt`). The salt parameter is required if you want to have the same "Remember me" checkbox work on all pages, see detail in the corresponding section of this doc.
### "Remember me" checkbox
By default, the CLI will add a "Remember me" checkbox on the password prompt. If checked, when the user enters their passphrase its salted hashed value will be stored in localStorage. In case this value becomes compromised an attacker can decrypt the page, but this should hopefully protect against password reuse attack (of course please use a unique passphrase nonetheless).
@ -108,6 +119,16 @@ If you don't want the checkbox to be included, you can add the `--noremember` fl
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`.
### Why does staticrypt create a config file?
The "Remember me" feature stores the user password hashed and salted in the browser's localStorage, so it needs the salt to be the same each time you encrypt otherwise the user would be logged out when you encrypt the page again. The config file is a way to store the salt in between runs, so you don't have to remember it and pass it manually.
When deciding what salt to use, staticrypt will first look for a `--salt` flag, then try to get the salt from the config file, and if it still doesn't find a salt it will generate a random one. It then saves the salt in the config file.
If you don't want staticrypt to create or use the config file, you can set `--config none` to disable it.
The salt isn't secret, so you don't need to worry about hiding the config file.
## 🙏 Contribution
Thank you: [@AaronCoplan](https://github.com/AaronCoplan) for bringing the CLI to life, [@epicfaace](https://github.com/epicfaace) & [@thomasmarr](https://github.com/thomasmarr) for sparking the caching of the passphrase in localStorage (allowing the "Remember me" checkbox)

Wyświetl plik

@ -2,8 +2,8 @@
'use strict';
var CryptoJS = require("crypto-js");
var FileSystem = require("fs");
const CryptoJS = require("crypto-js");
const fs = require("fs");
const path = require("path");
const Yargs = require('yargs');
@ -44,6 +44,10 @@ function hashPassphrase(passphrase, salt) {
return hashedPassphrase.toString();
}
function generateRandomSalt() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
}
/**
* Check if a particular option has been set by the user. Useful for distinguishing default value with flag without
* parameter.
@ -78,23 +82,28 @@ function isOptionSetByUser(option, yargs) {
const yargs = Yargs
.usage('Usage: staticrypt <filename> <passphrase> [options]')
.option('c', {
alias: 'config',
type: 'string',
describe: 'Path to the config file. Set to "none" to disable.',
default: '.staticrypt.json',
})
.option('decrypt-button', {
type: 'string',
describe: 'Label to use for the decrypt button. Default: "DECRYPT".',
default: 'DECRYPT'
})
.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('o', {
alias: 'output',
.option('f', {
alias: 'file-template',
type: 'string',
describe: 'File name / path for generated encrypted file.',
default: null
})
.option('t', {
alias: 'title',
type: 'string',
describe: "Title for output HTML page.",
default: 'Protected Page'
describe: 'Path to custom HTML template with passphrase prompt.',
default: path.join(__dirname, 'password_template.html')
})
.option('i', {
alias: 'instructions',
@ -102,11 +111,21 @@ const yargs = Yargs
describe: 'Special instructions to display to the user.',
default: ''
})
.option('f', {
alias: 'file-template',
.option('noremember', {
type: 'boolean',
describe: 'Set this flag to remove the "Remember me" checkbox.',
default: false,
})
.option('o', {
alias: 'output',
type: 'string',
describe: 'Path to custom HTML template with passphrase prompt.',
default: path.join(__dirname, 'password_template.html')
describe: 'File name / path for generated encrypted file.',
default: null
})
.option('passphrase-placeholder', {
type: 'string',
describe: 'Placeholder to use for the passphrase input.',
default: 'Passphrase'
})
.option('r', {
alias: 'remember',
@ -115,21 +134,11 @@ const yargs = Yargs
'in localStorage when entered by the user. Default: "0", no expiration.',
default: 0,
})
.option('noremember', {
type: 'boolean',
describe: 'Set this flag to remove the "Remember me" checkbox.',
default: false,
})
.option('remember-label', {
type: 'string',
describe: 'Label to use for the "Remember me" checkbox.',
default: 'Remember me'
})
.option('passphrase-placeholder', {
type: 'string',
describe: 'Placeholder to use for the passphrase input.',
default: 'Passphrase'
})
// do not give a default option to this 'remember' parameter - we want to see when the flag is included with no
// value and when it's not included at all
.option('s', {
@ -139,39 +148,65 @@ const yargs = Yargs
'can use: "statycrypt -s".',
type: 'string',
})
.option('decrypt-button', {
.option('t', {
alias: 'title',
type: 'string',
describe: 'Label to use for the decrypt button. Default: "DECRYPT".',
default: 'DECRYPT'
describe: "Title for output HTML page.",
default: 'Protected Page'
});
const namedArgs = yargs.argv;
// get the salt to use
let salt = CryptoJS.lib.WordArray.random(128 / 8).toString();
if (isOptionSetByUser('s', yargs)) {
// if the flag is passed without parameter, generate a salt, display & exit
if (!namedArgs.salt) {
console.log(salt);
process.exit(0);
}
// else use the user provided salt
else {
salt = String(namedArgs.salt).toLowerCase();
// validate the salt
if (salt.length !== 32 || /[^a-f0-9]/.test(salt)) {
console.log("The salt should be a 32 character long hexadecimal string (only [0-9a-f] characters allowed)");
process.exit(1);
}
}
// if the 's' flag is passed without parameter, generate a salt, display & exit
if (isOptionSetByUser('s', yargs) && !namedArgs.salt) {
console.log(generateRandomSalt());
process.exit(0);
}
// if we haven't returned by now, ensure we have the correct number of arguments
// validate the number of arguments
if (namedArgs._.length !== 2) {
Yargs.showHelp();
process.exit(1);
}
// get config file
const isUsingconfigFile = namedArgs.config.toLowerCase() !== 'none';
const configPath = path.join(__dirname, namedArgs.config);
let config = {};
if (isUsingconfigFile && fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
}
/**
* Get the salt to use
*/
let salt;
// either a salt was provided by the user through the flag --salt
if (!!namedArgs.salt) {
salt = String(namedArgs.salt).toLowerCase();
}
// or we try to read the salt from config file
else if (!!config.salt) {
salt = config.salt;
}
// or we generate a salt
else {
salt = generateRandomSalt();
}
// validate the salt
if (salt.length !== 32 || /[^a-f0-9]/.test(salt)) {
console.log("The salt should be a 32 character long hexadecimal string (only [0-9a-f] characters allowed)");
console.log("Detected salt: " + salt);
process.exit(1);
}
// write salt to config file
if (isUsingconfigFile && config.salt !== salt) {
config.salt = salt;
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
}
// parse input
const input = namedArgs._[0].toString(),
passphrase = namedArgs._[1].toString();
@ -179,7 +214,7 @@ const input = namedArgs._[0].toString(),
// get the file content
let contents;
try {
contents = FileSystem.readFileSync(input, 'utf8');
contents = fs.readFileSync(input, 'utf8');
} catch (e) {
console.log("Failure: input file does not exist!");
process.exit(1);
@ -197,7 +232,7 @@ const encryptedMessage = hmac + encrypted;
let cryptoTag = SCRIPT_TAG;
if (namedArgs.embed) {
try {
const embedContents = FileSystem.readFileSync(path.join(__dirname, 'crypto-js.min.js'), 'utf8');
const embedContents = fs.readFileSync(path.join(__dirname, 'crypto-js.min.js'), 'utf8');
cryptoTag = '<script>' + embedContents + '</script>';
} catch (e) {
@ -233,7 +268,7 @@ function genFile(data) {
let templateContents;
try {
templateContents = FileSystem.readFileSync(namedArgs.f, 'utf8');
templateContents = fs.readFileSync(namedArgs.f, 'utf8');
} catch (e) {
console.log("Failure: could not read template!");
process.exit(1);
@ -242,7 +277,7 @@ function genFile(data) {
const renderedTemplate = render(templateContents, data);
try {
FileSystem.writeFileSync(data.output_file_path, renderedTemplate);
fs.writeFileSync(data.output_file_path, renderedTemplate);
} catch (e) {
console.log("Failure: could not generate output file!");
process.exit(1);