Organize source files into more directories (#137)

* organize into directories

* address comments from PR#137
pull/138/head
Adam Hull 2022-11-05 03:58:21 -07:00 zatwierdzone przez GitHub
rodzic 9bac93c011
commit bdc831d91f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
12 zmienionych plików z 207 dodań i 6182 usunięć

4
.gitignore vendored
Wyświetl plik

@ -1,6 +1,4 @@
.idea
.vscode/
node_modules
.staticrypt.json
/cli/README.md
/cli/LICENSE
/cli/password_template.html

Wyświetl plik

@ -133,19 +133,26 @@ The salt isn't secret, so you don't need to worry about hiding the config file.
## Contributing
### Source Directories
- `cli/` - The command-line interface published to NPM.
- `example/` - This file is encrypted as part of the build. The encrypted file is committed both to make this library easy to explore and as a review-time sanity check.
- `lib/` - Files shared across www and cli.
- `scripts/` - Build, test, deploy, CI, etc. See `npm run-script`.
- `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.
```
# From staticrypt/
$ cd cli
$ npm install
$ npm run build
```
### Test
Testing is currently manual to keep dependencies low.
[Build](#build), then open `example_encypted.html`.
[Build](#build), then open `example/example_encypted.html`.
## 🙏 Contribution

Wyświetl plik

@ -1,31 +1,35 @@
#!/usr/bin/env node
'use strict';
"use strict";
const CryptoJS = require("crypto-js");
const fs = require("fs");
const path = require("path");
const Yargs = require('yargs');
const Yargs = require("yargs");
const SCRIPT_URL = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js';
const SCRIPT_TAG = '<script src="' + SCRIPT_URL + '" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></script>';
const SCRIPT_URL =
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js";
const SCRIPT_TAG =
'<script src="' +
SCRIPT_URL +
'" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></script>';
/**
* Salt and encrypt a msg with a password.
* Inspired by https://github.com/adonespitogo
*/
function encrypt(msg, hashedPassphrase) {
var iv = CryptoJS.lib.WordArray.random(128 / 8);
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
});
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();
// iv will be hex 16 in length (32 characters)
// we prepend it to the ciphertext for use in decryption
return iv.toString() + encrypted.toString();
}
/**
@ -36,16 +40,16 @@ function encrypt(msg, hashedPassphrase) {
* @returns string
*/
function hashPassphrase(passphrase, salt) {
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
keySize: 256 / 32,
iterations: 1000
});
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
keySize: 256 / 32,
iterations: 1000,
});
return hashedPassphrase.toString();
return hashedPassphrase.toString();
}
function generateRandomSalt() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
return CryptoJS.lib.WordArray.random(128 / 8).toString();
}
/**
@ -61,120 +65,121 @@ function generateRandomSalt() {
* @returns {boolean}
*/
function isOptionSetByUser(option, yargs) {
function searchForOption(option) {
return process.argv.indexOf(option) > -1;
}
function searchForOption(option) {
return process.argv.indexOf(option) > -1;
}
if (searchForOption(`-${option}`) || searchForOption(`--${option}`)) {
return true;
}
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];
// 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;
}
if (searchForOption(`-${alias}`) || searchForOption(`--${alias}`))
return true;
}
return false;
return false;
}
const yargs = Yargs
.usage('Usage: staticrypt <filename> <passphrase> [options]')
.option('c', {
alias: 'config',
type: 'string',
describe: 'Path to the config file. Set to "false" 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('f', {
alias: 'file-template',
type: 'string',
describe: 'Path to custom HTML template with passphrase prompt.',
default: path.join(__dirname, 'password_template.html')
})
.option('i', {
alias: 'instructions',
type: 'string',
describe: 'Special instructions to display to the user.',
default: ''
})
.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 generated encrypted file.',
default: null
})
.option('passphrase-placeholder', {
type: 'string',
describe: 'Placeholder to use for the passphrase input.',
default: 'Passphrase'
})
.option('r', {
alias: 'remember',
type: 'number',
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('remember-label', {
type: 'string',
describe: 'Label to use for the "Remember me" checkbox.',
default: 'Remember me'
})
// 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',
})
.option('t', {
alias: 'title',
type: 'string',
describe: "Title for output HTML page.",
default: 'Protected Page'
});
const yargs = Yargs.usage("Usage: staticrypt <filename> <passphrase> [options]")
.option("c", {
alias: "config",
type: "string",
describe: 'Path to the config file. Set to "false" 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("f", {
alias: "file-template",
type: "string",
describe: "Path to custom HTML template with passphrase prompt.",
default: path.join(__dirname, "..", "lib", "password_template.html"),
})
.option("i", {
alias: "instructions",
type: "string",
describe: "Special instructions to display to the user.",
default: "",
})
.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 generated encrypted file.",
default: null,
})
.option("passphrase-placeholder", {
type: "string",
describe: "Placeholder to use for the passphrase input.",
default: "Passphrase",
})
.option("r", {
alias: "remember",
type: "number",
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("remember-label", {
type: "string",
describe: 'Label to use for the "Remember me" checkbox.',
default: "Remember me",
})
// 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",
})
.option("t", {
alias: "title",
type: "string",
describe: "Title for output HTML page.",
default: "Protected Page",
});
const namedArgs = yargs.argv;
// 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 (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
console.log(generateRandomSalt());
process.exit(0);
}
// validate the number of arguments
if (namedArgs._.length !== 2) {
Yargs.showHelp();
process.exit(1);
Yargs.showHelp();
process.exit(1);
}
// get config file
const isUsingconfigFile = namedArgs.config.toLowerCase() !== 'false';
const configPath = './' + namedArgs.config;
const isUsingconfigFile = namedArgs.config.toLowerCase() !== "false";
const configPath = "./" + namedArgs.config;
let config = {};
if (isUsingconfigFile && fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
}
/**
@ -183,41 +188,43 @@ if (isUsingconfigFile && fs.existsSync(configPath)) {
let salt;
// either a salt was provided by the user through the flag --salt
if (!!namedArgs.salt) {
salt = String(namedArgs.salt).toLowerCase();
salt = String(namedArgs.salt).toLowerCase();
}
// or we try to read the salt from config file
else if (!!config.salt) {
salt = config.salt;
salt = config.salt;
}
// or we generate a salt
else {
salt = generateRandomSalt();
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);
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));
config.salt = salt;
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
}
// parse input
const input = namedArgs._[0].toString(),
passphrase = namedArgs._[1].toString();
passphrase = namedArgs._[1].toString();
// get the file content
let contents;
try {
contents = fs.readFileSync(input, 'utf8');
contents = fs.readFileSync(input, "utf8");
} catch (e) {
console.log("Failure: input file does not exist!");
process.exit(1);
console.log("Failure: input file does not exist!");
process.exit(1);
}
// encrypt input
@ -225,63 +232,71 @@ 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)
const hmac = CryptoJS.HmacSHA256(encrypted, CryptoJS.SHA256(hashedPassphrase).toString()).toString();
const hmac = CryptoJS.HmacSHA256(
encrypted,
CryptoJS.SHA256(hashedPassphrase).toString()
).toString();
const encryptedMessage = hmac + encrypted;
// create crypto-js tag (embedded or not)
let cryptoTag = SCRIPT_TAG;
if (namedArgs.embed) {
try {
const embedContents = fs.readFileSync(path.join(__dirname, 'crypto-js.min.js'), 'utf8');
try {
const embedContents = fs.readFileSync(
path.join(__dirname, "..", "kryptojs-3.1.9-1.min"),
"utf8"
);
cryptoTag = '<script>' + embedContents + '</script>';
} catch (e) {
console.log("Failure: embed file does not exist!");
process.exit(1);
}
cryptoTag = "<script>" + embedContents + "</script>";
} catch (e) {
console.log("Failure: embed file does not exist!");
process.exit(1);
}
}
const data = {
crypto_tag: cryptoTag,
decrypt_button: namedArgs.decryptButton,
embed: namedArgs.embed,
encrypted: encryptedMessage,
instructions: namedArgs.instructions,
is_remember_enabled: namedArgs.noremember ? 'false' : 'true',
output_file_path: namedArgs.output !== null ? namedArgs.output : input.replace(/\.html$/, '') + "_encrypted.html",
passphrase_placeholder: namedArgs.passphrasePlaceholder,
remember_duration_in_days: namedArgs.remember,
remember_me: namedArgs.rememberLabel,
salt: salt,
title: namedArgs.title,
crypto_tag: cryptoTag,
decrypt_button: namedArgs.decryptButton,
embed: namedArgs.embed,
encrypted: encryptedMessage,
instructions: namedArgs.instructions,
is_remember_enabled: namedArgs.noremember ? "false" : "true",
output_file_path:
namedArgs.output !== null
? namedArgs.output
: input.replace(/\.html$/, "") + "_encrypted.html",
passphrase_placeholder: namedArgs.passphrasePlaceholder,
remember_duration_in_days: namedArgs.remember,
remember_me: namedArgs.rememberLabel,
salt: salt,
title: namedArgs.title,
};
genFile(data);
/**
* Fill the template with provided data and writes it to output file.
*
* @param data
*/
function genFile(data) {
let templateContents;
let templateContents;
try {
templateContents = fs.readFileSync(namedArgs.f, 'utf8');
} catch (e) {
console.log("Failure: could not read template!");
process.exit(1);
}
try {
templateContents = fs.readFileSync(namedArgs.f, "utf8");
} catch (e) {
console.log("Failure: could not read template!");
process.exit(1);
}
const renderedTemplate = render(templateContents, data);
const renderedTemplate = render(templateContents, data);
try {
fs.writeFileSync(data.output_file_path, renderedTemplate);
} catch (e) {
console.log("Failure: could not generate output file!");
process.exit(1);
}
try {
fs.writeFileSync(data.output_file_path, renderedTemplate);
} catch (e) {
console.log("Failure: could not generate output file!");
process.exit(1);
}
}
/**
@ -292,11 +307,11 @@ function genFile(data) {
* @returns string
*/
function render(tpl, data) {
return tpl.replace(/{(.*?)}/g, function (_, key) {
if (data && data[key] !== undefined) {
return data[key];
}
return tpl.replace(/{(.*?)}/g, function (_, key) {
if (data && data[key] !== undefined) {
return data[key];
}
return '';
});
return "";
});
}

Wyświetl plik

@ -166,7 +166,7 @@
<script>
// variables to be filled when generating the file
var encryptedMsg = 'e70f668363afe4238c1e68d5c1481f6c2e7cab002412e7c7f721c74ebb80127355925315930ceca9be53a5f2bde4f7a8U2FsdGVkX1+SR7cHNx6YjSbLrCQdv2s20Gw6tPy68pjlUVh+UCqNGdJ3+Nh0DJVpy00/7ACSjp/noo2VlzPPJWMN6Oiks/ZO3H3TgA3Snfjzv22CDmOkmpIDaWgkias5Iw1RlQRufAoPJMqrLNwcLMAfifat0Ra31oZMpPYq1KIdXoqYsQkY8uR6IRdiA947UxenEo6qUg2VJuCIsg13kQ==',
var encryptedMsg = '65a0577162396cc1bddae60b8f435291ff7a69644825b98bfc636a29089a28efbc9417689b983f2048bb776ca66eb25fU2FsdGVkX19n36H4ocM7GbaeFVganWX86ZTHEZk2w12z3z7rqWDW8OESK8MmGtbnPJetgyWi3jpz3iI+rE/gSilJkhQ2YR/4yCBintGLeh1hCgX+XPBEDT0w+ri4uqUWCxDUIvzyUhbnf1ZD2WsK9wmDHwRwF9YcucHXuyS7/GlUcVsYERzxxDd9frN6DbubNNbdY/QtG+vtmLSwHGZtwQ==',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
isRememberEnabled = true,
rememberDurationInDays = 0; // 0 means forever

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_encrypted.html">example</a>).
target="_blank" href="example/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> and is <a
@ -208,11 +208,9 @@ Your encrypted string</pre>
</div>
<!--
Crypto JS 3.1.9-1
Copied as is from https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.js
Filename changed to circumvent adblockers that mistake it for a crypto miner (see https://github.com/robinmoisson/staticrypt/issues/107)
-->
<script src="kryptojs-3.1.9-1-lib.js"></script>
<script src="lib/kryptojs-3.1.9-1.min.js"></script>
<script src="https://cdn.ckeditor.com/4.7.0/standard/ckeditor.js"></script>
@ -238,7 +236,7 @@ Filename changed to circumvent adblockers that mistake it for a crypto miner (se
*/
var setFileToDownload = function (data) {
var request = new XMLHttpRequest();
request.open('GET', 'password_template.html', true);
request.open('GET', 'lib/password_template.html', true);
request.onload = function () {
var renderedTmpl = renderTemplate(request.responseText, data);
@ -257,7 +255,7 @@ Filename changed to circumvent adblockers that mistake it for a crypto miner (se
*/
var setFileToDownloadWithEmbeddedCrypto = function (data) {
var request = new XMLHttpRequest();
request.open('GET', 'kryptojs-3.1.9-1-lib.js', true);
request.open('GET', 'lib/kryptojs-3.1.9-1.min.js', true);
request.onload = function () {
data['crypto_tag'] = '<script>' + request.responseText + '</scr' + 'ipt>';
setFileToDownload(data);

Plik diff jest za duży Load Diff

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -13,7 +13,7 @@
"yargs": ">=10.0.3"
},
"bin": {
"staticrypt": "index.js"
"staticrypt": "cli/index.js"
},
"devDependencies": {}
},

Wyświetl plik

@ -4,12 +4,11 @@
"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": [
"index.js",
"password_template.html",
"crypto-js.min.js"
"/cli",
"/lib"
],
"bin": {
"staticrypt": "./index.js"
"staticrypt": "./cli/index.js"
},
"dependencies": {
"crypto-js": ">=3.1.9-1",

Wyświetl plik

@ -1,11 +1,8 @@
# Usage: `npm run build`
# NPM establishes a reliable working directory
cd ..
# Copy files that should be included in `npm publish`
cp README.md LICENSE password_template.html cli/
# Encrypt the example file
npx ./cli example.html test \
npx . example/example.html test \
--no-embed \
--salt b93bbaf35459951c47721d1f3eaeb5b9 \
--instructions "Enter \"test\" to unlock the page"