add "--salt" flag to handle multiple pages encryption

pull/131/head
Robin Moisson 2022-02-27 19:37:17 +01:00
rodzic 70912c3b95
commit 07edf6fdfd
7 zmienionych plików z 472 dodań i 250 usunięć

Wyświetl plik

@ -38,30 +38,43 @@ Staticrypt is available through npm as a CLI, install with `npm install -g stati
[string] [default: ""]
-f, --file-template Path to custom HTML template with passphrase
prompt.
[string] [default: "/geek/staticrypt/cli/password_template.html"]
-r, --remember Show a "Remember me" checkbox that will save the
(salted + hashed) passphrase in localStorage
when entered by the user.
You can set the expiration in days as value (no
value means "0", no expiration).
[string] [default: "./password_template.html"]
-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.
Default: "Remember me".
[string] [default: "Remember me"]
--passphrase-placeholder Placeholder to use for the passphrase input.
Default: "Passphrase".
[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"]
Example usages:
- `staticrypt test.html mySecretPassphrase` -> creates a `test_encrypted.html` file
- `find . -type f -name "*.html" -exec staticrypt {} mypassword \;` -> create encrypted files for all HTML files in your directory
### Example usages
You can use a custom template for the password prompt - just copy `cli/password_template.html` and modify it to suit your presentation style and point to your template file with the `-f` flag. Be careful to not break the encrypting javascript part, the variables replaced by staticrypt are between curly brackets: `{instructions}`.
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 except the ones ending in `_encrypted.html`:
```
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_PASSPHRASE -s MY_SALT \;
```
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
@ -69,19 +82,31 @@ By default, the CLI will add a "Remember me" checkbox on the password prompt. If
This allows encrypting multiple page on a single domain with the same password: if you check "Remember me", you'll have to enter you password once then all the pages on that domain will automatically decrypt their content.
If no value is provided the stored passphrase doesn't expire, you can also give it a value in days for how long should the store value be kept. If the user reconnects to the page after the expiration date the store value will be cleared.
If no value is provided the stored passphrase 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 store value will be cleared.
You can clear the values in localStorage (effectively "login out") at any time by appending `staticrypt_logout` to the URL query paramets (`mysite.com?staticrypt_logout`).
#### `--noremember`
#### Encrypting multiple pages
If you don't want the checkbox to be included, you can add the `--noremember` flag to disable it.
If you want to encrypt multiple pages and have the "Remember me" checkbox work for all pages (so you have to enter your password on one page and then all other pages are automatically decrypted), you need to pass a `--salt MY_SALT` argument with the same salt for all encrypted pages. The salt isn't secret, so you don't have to worry about hiding it in the command prompt.
### `--embed` and crypto-js
Because the hashed value is stored in the browser's localStorage, this will only work if all the pages are on the same domain name.
If you do not embed crypto-js and serve it from a CDN, some adblockers see the `crypto-js.min.js`, think that's a crypto miner and block it.
## FAQ
## Contribution
### Can I customize the password prompt?
Yes! Just copy `cli/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}`.
### Can I prevent the "Remember me" checkbox?
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?
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`.
## 🙏 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)
@ -93,4 +118,4 @@ If you find a serious security bug please open an issue, I'll try to fix it rela
https://github.com/MaxLaumeister/PageCrypt is a similar project (I think it predates staticrypt).
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 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.
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.

Wyświetl plik

@ -38,30 +38,43 @@ Staticrypt is available through npm as a CLI, install with `npm install -g stati
[string] [default: ""]
-f, --file-template Path to custom HTML template with passphrase
prompt.
[string] [default: "/geek/staticrypt/cli/password_template.html"]
-r, --remember Show a "Remember me" checkbox that will save the
(salted + hashed) passphrase in localStorage
when entered by the user.
You can set the expiration in days as value (no
value means "0", no expiration).
[string] [default: "./password_template.html"]
-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.
Default: "Remember me".
[string] [default: "Remember me"]
--passphrase-placeholder Placeholder to use for the passphrase input.
Default: "Passphrase".
[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"]
Example usages:
- `staticrypt test.html mySecretPassphrase` -> creates a `test_encrypted.html` file
- `find . -type f -name "*.html" -exec staticrypt {} mypassword \;` -> create encrypted files for all HTML files in your directory
### Example usages
You can use a custom template for the password prompt - just copy `cli/password_template.html` and modify it to suit your presentation style and point to your template file with the `-f` flag. Be careful to not break the encrypting javascript part, the variables replaced by staticrypt are between curly brackets: `{instructions}`.
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 except the ones ending in `_encrypted.html`:
```
find . -type f -name "*.html" -not -name "*_encrypted.html" -exec staticrypt {} MY_PASSPHRASE -s MY_SALT \;
```
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
@ -69,19 +82,31 @@ By default, the CLI will add a "Remember me" checkbox on the password prompt. If
This allows encrypting multiple page on a single domain with the same password: if you check "Remember me", you'll have to enter you password once then all the pages on that domain will automatically decrypt their content.
If no value is provided the stored passphrase doesn't expire, you can also give it a value in days for how long should the store value be kept. If the user reconnects to the page after the expiration date the store value will be cleared.
If no value is provided the stored passphrase 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 store value will be cleared.
You can clear the values in localStorage (effectively "login out") at any time by appending `staticrypt_logout` to the URL query paramets (`mysite.com?staticrypt_logout`).
#### `--noremember`
#### Encrypting multiple pages
If you want to encrypt multiple pages and have the "Remember me" checkbox work for all pages (so you have to enter your password on one page and then all other pages are automatically decrypted), you need to pass a `--salt MY_SALT` argument with the same salt for all encrypted pages. The salt isn't secret, so you don't have to worry about hiding it in the command prompt.
Because the hashed value is stored in the browser's localStorage, this will only work if all the pages are on the same domain name.
## FAQ
### Can I customize the password prompt?
Yes! Just copy `cli/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}`.
### Can I prevent the "Remember me" checkbox?
If you don't want the checkbox to be included, you can add the `--noremember` flag to disable it.
### `--embed` and crypto-js
### Why do we embed the whole crypto-js library in each encrypted file by default?
If you do not embed crypto-js and serve it from a CDN, some adblockers see the `crypto-js.min.js`, think that's a crypto miner and block it.
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`.
## Contribution
## 🙏 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)
@ -93,4 +118,4 @@ If you find a serious security bug please open an issue, I'll try to fix it rela
https://github.com/MaxLaumeister/PageCrypt is a similar project (I think it predates staticrypt).
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 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.
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.

Wyświetl plik

@ -32,7 +32,8 @@ function encrypt(msg, hashedPassphrase) {
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} passphrase
* @returns {{salt: string, hashedPassphrase: string}}
* @param {string} salt
* @returns string
*/
function hashPassphrase(passphrase, salt) {
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
@ -40,15 +41,43 @@ function hashPassphrase(passphrase, salt) {
iterations: 1000
});
return {
salt: salt,
hashedPassphrase: hashedPassphrase.toString(),
};
return hashedPassphrase.toString();
}
const namedArgs = Yargs
/**
* Check if a particular option has been set by the user. Useful for distinguishing default value with flag without
* parameter.
*
* Ex use case: '-s' means "give me a salt", '-s 1234' means "use 1234 as salt"
*
* From https://github.com/yargs/yargs/issues/513#issuecomment-221412008
*
* @param option
* @param yargs
* @returns {boolean}
*/
function isOptionSetByUser(option, yargs) {
function searchForOption(option) {
return process.argv.indexOf(option) > -1;
}
if (searchForOption(`-${option}`) || searchForOption(`--${option}`)) {
return true;
}
// Handle aliases for same option
for (let aliasIndex in yargs.parsed.aliases[option]) {
const alias = yargs.parsed.aliases[option][aliasIndex];
if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`))
return true;
}
return false;
}
const yargs = Yargs
.usage('Usage: staticrypt <filename> <passphrase> [options]')
.demandCommand(2)
.option('e', {
alias: 'embed',
type: 'boolean',
@ -82,7 +111,8 @@ const namedArgs = Yargs
.option('r', {
alias: 'remember',
type: 'number',
describe: 'Show a "Remember me" checkbox that will save the (salted + hashed) passphrase in localStorage when entered by the user.\nYou can set the expiration in days as value (no value means "0", no expiration).',
describe: '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.',
default: 0,
})
.option('noremember', {
@ -92,25 +122,51 @@ const namedArgs = Yargs
})
.option('remember-label', {
type: 'string',
describe: 'Label to use for the "Remember me" checkbox. Default: "Remember me".',
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".',
describe: 'Placeholder to use for the passphrase input.',
default: 'Passphrase'
})
.option('salt', {
// 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', {
alias: 'salt',
describe: '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.\nInclude the empty flag to generate a random salt you ' +
'can use: "statycrypt -s".',
type: 'string',
describe: 'Set the salt manually, should be set if you want use "Remeber me" through multiple pages.',
default: null
})
.option('decrypt-button', {
type: 'string',
describe: 'Label to use for the decrypt button. Default: "DECRYPT".',
default: 'DECRYPT'
}).argv;
});
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 we haven't returned by now, ensure we have the correct number of arguments
if (namedArgs._.length !== 2) {
Yargs.showHelp();
process.exit(1);
@ -129,11 +185,8 @@ try {
process.exit(1);
}
const salt = namedArgs.salt !== null? namedArgs.salt : CryptoJS.lib.WordArray.random(128 / 8).toString();
// encrypt input
const hashed = hashPassphrase(passphrase, salt);
const hashedPassphrase = hashed.hashedPassphrase;
const hashedPassphrase = hashPassphrase(passphrase, salt);
const encrypted = encrypt(contents, hashedPassphrase);
// we use the hashed passphrase in the HMAC because this is effectively what will be used a passphrase (so we can store
// it in localStorage safely, we don't use the clear text passphrase)

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "staticrypt",
"version": "2.0.1",
"version": "2.0.2",
"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",
"bin": {

Wyświetl plik

@ -1,193 +1,3 @@
<!doctype html>
<html class="staticrypt-html">
<head>
<meta charset="utf-8">
<title>Protected Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- do not cache this page -->
<meta http-equiv="cache-control" content="max-age=0"/>
<meta http-equiv="cache-control" content="no-cache"/>
<meta http-equiv="expires" content="0"/>
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT"/>
<meta http-equiv="pragma" content="no-cache"/>
<style>
.staticrypt-hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.staticrypt-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
box-sizing: border-box;
}
.staticrypt-form {
position: relative;
z-index: 1;
background: #FFFFFF;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.staticrypt-form input {
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.staticrypt-form .staticrypt-decrypt-button {
text-transform: uppercase;
outline: 0;
background: #4CAF50;
width: 100%;
border: 0;
padding: 15px;
color: #FFFFFF;
font-size: 14px;
cursor: pointer;
}
.staticrypt-form .staticrypt-decrypt-button:hover, .staticrypt-form .staticrypt-decrypt-button:active, .staticrypt-form .staticrypt-decrypt-button:focus {
background: #43A047;
}
.staticrypt-html {
height: 100%;
}
.staticrypt-body {
margin-bottom: 1em;
background: #76b852; /* fallback for old browsers */
background: -webkit-linear-gradient(right, #76b852, #8DC26F);
background: -moz-linear-gradient(right, #76b852, #8DC26F);
background: -o-linear-gradient(right, #76b852, #8DC26F);
background: linear-gradient(to left, #76b852, #8DC26F);
font-family: "Arial", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.staticrypt-instructions {
margin-top: -1em;
margin-bottom: 1em;
}
.staticrypt-title {
font-size: 1.5em;
}
.staticrypt-footer {
position: fixed;
height: 20px;
font-size: 16px;
padding: 2px;
bottom: 0;
left: 0;
right: 0;
margin-bottom: 0;
}
.staticrypt-footer p {
margin: 2px;
text-align: center;
float: right;
}
.staticrypt-footer a {
text-decoration: none;
}
</style>
</head>
<body class="staticrypt-body">
<div class="staticrypt-page">
<div class="staticrypt-form">
<div class="staticrypt-instructions">
<p class="staticrypt-title">Protected Page</p>
<p><p>Enter &quot;test&quot; to unlock the page</p>
</p>
</div>
<hr class="staticrypt-hr">
<form id="staticrypt-form" action="#" method="post">
<input id="staticrypt-password"
type="password"
name="password"
placeholder="passphrase"
autofocus/>
<input type="submit" class="staticrypt-decrypt-button" value="DECRYPT"/>
</form>
</div>
</div>
<footer class="staticrypt-footer">
<p class="pull-right">Created with <a href="https://robinmoisson.github.io/staticrypt">StatiCrypt</a></p>
</footer>
<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>
<script>
/**
* Decrypt a salted msg using a password.
* Inspired by https://github.com/adonespitogo
*/
var keySize = 256;
var iterations = 1000;
function decrypt (encryptedMsg, pass) {
var salt = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, 32));
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(32, 32))
var encrypted = encryptedMsg.substring(64);
var key = CryptoJS.PBKDF2(pass, salt, {
keySize: keySize/32,
iterations: iterations
});
var decrypted = CryptoJS.AES.decrypt(encrypted, key, {
iv: iv,
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC
}).toString(CryptoJS.enc.Utf8);
return decrypted;
}
document.getElementById('staticrypt-form').addEventListener('submit', function(e) {
e.preventDefault();
var passphrase = document.getElementById('staticrypt-password').value,
encryptedMsg = '1c5350b7566531a9aa06d5c186b58de688287a5f64c1439fadab7c5f8ffd3c676a558d9e37e7c0c4b8df03fbb0149c292c02cff335c90cc2ccc7e02fcda394247bvtjq4PeULRWa0f/TewJBGyaHeltmS3NITxoMwMpHJoe4E4AXjJqHcFjHY+1H6u1IIxjQlP4/WRDUu6U+QG+eHIfOgwE4sz+9CkrHFSG8HX2p0NpEs7HYls8rZEk1SKcMrtlzuSwKBV60jIketViNVFHmnbPJYmK0FKVQcFh19EO1dtuvAK8M3CMVOPUIYh',
encryptedHMAC = encryptedMsg.substring(0, 64),
encryptedHTML = encryptedMsg.substring(64),
decryptedHMAC = CryptoJS.HmacSHA256(encryptedHTML, CryptoJS.SHA256(passphrase).toString()).toString();
if (decryptedHMAC !== encryptedHMAC) {
alert('Bad passphrase!');
return;
}
var plainHTML = decrypt(encryptedHTML, passphrase);
document.write(plainHTML);
document.close();
});
</script>
</body>
</html>
<h1>Many secrets</h1>
<p>You unlocked me!</p>
<p>Back to <a href="https://robinmoisson.github.com/staticrypt">StatiCrypt</a></p>

Wyświetl plik

@ -0,0 +1,309 @@
<!doctype html>
<html class="staticrypt-html">
<head>
<meta charset="utf-8">
<title>Protected Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- do not cache this page -->
<meta http-equiv="cache-control" content="max-age=0"/>
<meta http-equiv="cache-control" content="no-cache"/>
<meta http-equiv="expires" content="0"/>
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT"/>
<meta http-equiv="pragma" content="no-cache"/>
<style>
.staticrypt-hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.staticrypt-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
box-sizing: border-box;
}
.staticrypt-form {
position: relative;
z-index: 1;
background: #FFFFFF;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.staticrypt-form input[type="password"] {
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.staticrypt-form .staticrypt-decrypt-button {
text-transform: uppercase;
outline: 0;
background: #4CAF50;
width: 100%;
border: 0;
padding: 15px;
color: #FFFFFF;
font-size: 14px;
cursor: pointer;
}
.staticrypt-form .staticrypt-decrypt-button:hover, .staticrypt-form .staticrypt-decrypt-button:active, .staticrypt-form .staticrypt-decrypt-button:focus {
background: #43A047;
}
.staticrypt-html {
height: 100%;
}
.staticrypt-body {
margin-bottom: 1em;
background: #76b852; /* fallback for old browsers */
background: -webkit-linear-gradient(right, #76b852, #8DC26F);
background: -moz-linear-gradient(right, #76b852, #8DC26F);
background: -o-linear-gradient(right, #76b852, #8DC26F);
background: linear-gradient(to left, #76b852, #8DC26F);
font-family: "Arial", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.staticrypt-instructions {
margin-top: -1em;
margin-bottom: 1em;
}
.staticrypt-title {
font-size: 1.5em;
}
.staticrypt-footer {
position: fixed;
height: 20px;
font-size: 16px;
padding: 2px;
bottom: 0;
left: 0;
right: 0;
margin-bottom: 0;
}
.staticrypt-footer p {
margin: 2px;
text-align: center;
float: right;
}
.staticrypt-footer a {
text-decoration: none;
}
label.staticrypt-remember {
display: flex;
align-items: center;
margin-bottom: 1em;
}
.staticrypt-remember input[type=checkbox] {
transform: scale(1.5);
margin-right: 1em;
}
.hidden {
display: none !important;
}
</style>
</head>
<body class="staticrypt-body">
<div class="staticrypt-page">
<div class="staticrypt-form">
<div class="staticrypt-instructions">
<p class="staticrypt-title">Protected Page</p>
<p>Enter "test" to unlock the page</p>
</div>
<hr class="staticrypt-hr">
<form id="staticrypt-form" action="#" method="post">
<input id="staticrypt-password"
type="password"
name="password"
placeholder="Passphrase"
autofocus/>
<label id="staticrypt-remember-label" class="staticrypt-remember hidden">
<input id="staticrypt-remember"
type="checkbox"
name="remember"/>
Remember me
</label>
<input type="submit" class="staticrypt-decrypt-button" value="DECRYPT"/>
</form>
</div>
</div>
<footer class="staticrypt-footer">
<p class="pull-right">Created with <a href="https://robinmoisson.github.io/staticrypt">StatiCrypt</a></p>
</footer>
<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>
<script>
// variables to be filled when generating the file
var encryptedMsg = '72f69754076b1c1fd160a624fb21badc489a090719f7ad97d310019deed676d5b618af7da8d6db21709e68a45f9d5bc0U2FsdGVkX1+aXeIHqt6H30PjjAknmKIHAiSBihh/n0wlxUws/eSQ2Mdyr4Y0p9WakxPqazVvHq61pD4mqjMXCwaFjPAaUfdehpBDMU887rZ2KgK6s3W1kc3b/xd0iTKOVMojQkn88PqbHp+uPgH3OVhnK2tt+MdLHv7j3YKUTBBaqtbN0Aj1GYIwNbmKubHSjpo1ErmIFWtLjppzDyOFMw==',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
isRememberEnabled = true,
rememberDurationInDays = 0; // 0 means forever
// constants
var rememberPassphraseKey = 'staticrypt_passphrase',
rememberExpirationKey = 'staticrypt_expiration';
/**
* Decrypt a salted msg using a password.
* Inspired by https://github.com/adonespitogo
*
* @param encryptedMsg
* @param hashedPassphrase
* @returns
*/
function decryptMsg(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);
}
/**
* Decrypt our encrypted page, replace the whole HTML.
*
* @param hashedPassphrase
* @returns
*/
function decryptAndReplaceHtml(hashedPassphrase) {
var encryptedHMAC = encryptedMsg.substring(0, 64),
encryptedHTML = encryptedMsg.substring(64),
decryptedHMAC = CryptoJS.HmacSHA256(encryptedHTML, CryptoJS.SHA256(hashedPassphrase).toString()).toString();
if (decryptedHMAC !== encryptedHMAC) {
return false;
}
var plainHTML = decryptMsg(encryptedHTML, hashedPassphrase);
document.write(plainHTML);
document.close();
return true;
}
/**
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param passphrase
* @returns
*/
function hashPassphrase(passphrase) {
return CryptoJS.PBKDF2(passphrase, salt, {
keySize: 256 / 32,
iterations: 1000
}).toString();
}
/**
* Clear localstorage from staticrypt related values
*/
function clearLocalStorage() {
localStorage.removeItem(rememberPassphraseKey);
localStorage.removeItem(rememberExpirationKey);
}
// try to automatically decrypt on load if there is a saved password
window.onload = function () {
if (isRememberEnabled) {
// show the remember me checkbox
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
// if we are login out, clear the storage and terminate
var queryParams = new URLSearchParams(window.location.search);
if (queryParams.has("staticrypt_logout")) {
return clearLocalStorage();
}
// if there is expiration configured, check if we're not beyond the expiration
if (rememberDurationInDays && rememberDurationInDays > 0) {
var expiration = localStorage.getItem(rememberExpirationKey),
isExpired = expiration && new Date().getTime() > parseInt(expiration);
if (isExpired) {
return clearLocalStorage();
}
}
var hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
if (hashedPassphrase) {
// try to decrypt
var isDecryptionSuccessful = 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) {
return clearLocalStorage();
}
}
}
}
// handle password form submission
document.getElementById('staticrypt-form').addEventListener('submit', function (e) {
e.preventDefault();
var passphrase = document.getElementById('staticrypt-password').value,
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
// decrypt and replace the whole page
var hashedPassphrase = hashPassphrase(passphrase);
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);
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('Bad passphrase!');
}
});
</script>
</body>
</html>

Wyświetl plik

@ -65,7 +65,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.html">example</a>).
target="_blank" href="example_encrypted.html">example</a>).
</p>
<p>
The tool is also available as <a href="https://npmjs.com/package/staticrypt">a CLI on NPM</a>.