kopia lustrzana https://github.com/robinmoisson/staticrypt
add support for config file + save salt
rodzic
8548bf3ce4
commit
cad496355f
|
@ -1,2 +1,4 @@
|
|||
.idea
|
||||
node_modules
|
||||
cli/.staticrypt.json
|
||||
.staticrypt.json
|
||||
|
|
51
README.md
51
README.md
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
137
cli/index.js
137
cli/index.js
|
@ -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);
|
||||
|
|
Ładowanie…
Reference in New Issue