add support for decrypting in the CLI (closes #112)

pull/177/head
robinmoisson 2023-04-23 00:21:30 +02:00
rodzic 3ca758eb0a
commit 5d99ce92ba
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 9419716500078583
7 zmienionych plików z 210 dodań i 71 usunięć

1
.gitignore vendored
Wyświetl plik

@ -5,3 +5,4 @@ node_modules
.env
encrypted/
!example/encrypted/
decrypted/

Wyświetl plik

@ -29,7 +29,7 @@ You can then run it with `npx staticrypt ...`. You can also install globally wit
**Encrypt a file:** encrypt `test.html` and create a `encrypted/test.html` file (use `-d my_directory` to change the output directory):
```bash
staticrypt test.html -p MY_LONG_PASSWORD
staticrypt test.html -p <long-password>
# or do not include the password if you want to be prompted for it:
staticrypt test.html
@ -45,22 +45,34 @@ staticrypt test.html
**Encrypt multiple files at once** and put them in a `encrypted/` directory:
```bash
# this will encrypt test_A.html, test_B.html and all files in the test/ directory
staticrypt test_A.html test_B.html test/* -p MY_LONG_PASSWORD
# => encrypted files are in encrypted/test_A.html, encrypted/test_B.html, encrypted/test/...
# this will encrypt test_A.html and test_B.html
staticrypt test_A.html test_B.html -p <long-password>
# => encrypted files are in encrypted/test_A.html and encrypted/test_B.html
# you can also use the -r flag to recursively encrypt all files in a directory
staticrypt dir_to_encrypt -p MY_LONG_PASSWORD -r
staticrypt dir_to_encrypt -p <long-password> -r
# => encrypted files are in encrypted/dir_to_encrypt/...
# if you don't want to include the directory name in the output path, you can use dir_to_encrypt/* instead
staticrypt dir_to_encrypt/* -p <long-password> -r
# => encrypted files are in encrypted/...
```
**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 -p MY_LONG_PASSWORD --share https://example.com/encrypted.html
staticrypt test.html -p <long-password> --share https://example.com/encrypted.html
# => https://example.com/encrypted.html#staticrypt_pwd=5bfbf1343c7257cd7be23ecd74bb37fa2c76d041042654f358b6255baeab898f
```
**Decrypt files you encrypted earlier** straight from the CLI by including the `--decrypt` flag. The `-r|--recursive` flag and output `-d|--directory` option work the same way as when encrypting (default name for the output directory is `decrypted`):
```bash
staticrypt encrypted/test.html -p <long-password> --decrypt
# => decrypted file is in decrypted/test.html
```
**Pin the salt to use staticrypt in your CI in a build step** - if you want want the "Remember-me" or share features to work accross multiple pages or multiple successive deployment, the salt needs to stay the same ([see why](https://github.com/robinmoisson/staticrypt#why-does-staticrypt-create-a-config-file)). If you run StatiCrypt in a CI step, you can pin the salt in two ways:
```bash
@ -69,7 +81,7 @@ staticrypt test.html -p MY_LONG_PASSWORD --share https://example.com/encrypted.h
staticrypt --salt
# or hardcode the salt in the CI script command:
staticrypt test.html -p MY_LONG_PASSWORD --salt 12345678901234567890123456789012
staticrypt test.html -p <long-password> --salt 12345678901234567890123456789012
```
### CLI Reference
@ -83,9 +95,12 @@ The password argument is optional if `STATICRYPT_PASSWORD` is set in the environ
--version Show version number [boolean]
-c, --config Path to the config file. Set to "false" to
disable.[string] [default: ".staticrypt.json"]
-d, --directory Name of the directory where the encrypted
files will be saved.
[string] [default: "encrypted/"]
-d, --directory Name of the directory where the generated
files will be saved. If the '--decrypt' flag
is set, default will be 'decrypted'.
[string] [default: "encrypted"]
--decrypt Include this flag to decrypt files instead of
encrypt. [boolean] [default: false]
-p, --password The password to encrypt your file with. Leave
empty to be prompted for it. If
STATICRYPT_PASSWORD is set in the env, we'll

Wyświetl plik

@ -1,4 +1,4 @@
const path = require("path");
const pathModule = require("path");
const fs = require("fs");
const readline = require("readline");
@ -6,7 +6,9 @@ const { generateRandomSalt, generateRandomString } = require("../lib/cryptoEngin
const { renderTemplate } = require("../lib/formater.js");
const Yargs = require("yargs");
const PASSWORD_TEMPLATE_DEFAULT_PATH = path.join(__dirname, "..", "lib", "password_template.html");
const PASSWORD_TEMPLATE_DEFAULT_PATH = pathModule.join(__dirname, "..", "lib", "password_template.html");
const OUTPUT_DIRECTORY_DEFAULT_PATH = "encrypted";
exports.OUTPUT_DIRECTORY_DEFAULT_PATH = OUTPUT_DIRECTORY_DEFAULT_PATH;
/**
* @param {string} message
@ -71,16 +73,19 @@ function prompt(question) {
});
}
async function getValidatedPassword(passwordArgument, isShortAllowed) {
const password = await getPassword(passwordArgument);
/**
* @param {string} password
* @param {boolean} isShortAllowed
* @returns {Promise<void>}
*/
async function validatePassword(password, isShortAllowed) {
if (password.length < 14 && !isShortAllowed) {
const shouldUseShort = await prompt(
`WARNING: Your password is less than 14 characters (length: ${password.length})` +
" and it's easy to try brute-forcing on public files. For better security we recommend using a longer one, for example: " +
" and it's easy to try brute-forcing on public files, so we recommend using a longer one. Here's a generated one: " +
generateRandomString(21) +
"\nYou can hide this warning by increasing your password length or adding the '--short' flag." +
" Do you want to use the short password? [y/N] "
"\nDo you want to still want to use the shorter password? [y/N] "
);
if (!shouldUseShort.match(/^\s*(y|yes)\s*$/i)) {
@ -88,10 +93,8 @@ async function getValidatedPassword(passwordArgument, isShortAllowed) {
process.exit(0);
}
}
return password;
}
exports.getValidatedPassword = getValidatedPassword;
exports.validatePassword = validatePassword;
/**
* Get the config from the config file.
@ -137,6 +140,7 @@ async function getPassword(passwordArgument) {
// prompt the user for their password
return prompt("Enter your long, unusual password: ");
}
exports.getPassword = getPassword;
/**
* @param {string} filepath
@ -200,8 +204,8 @@ function getSalt(namedArgs, config) {
* @param {string} modulePath - path from staticrypt root directory
*/
function convertCommonJSToBrowserJS(modulePath) {
const rootDirectory = path.join(__dirname, "..");
const resolvedPath = path.join(rootDirectory, ...modulePath.split("/")) + ".js";
const rootDirectory = pathModule.join(__dirname, "..");
const resolvedPath = pathModule.join(rootDirectory, ...modulePath.split("/")) + ".js";
if (!fs.existsSync(resolvedPath)) {
exitWithError(`could not find module to convert at path "${resolvedPath}"`);
@ -262,20 +266,29 @@ function genFile(data, outputFilePath, templateFilePath) {
const renderedTemplate = renderTemplate(templateContents, data);
writeFile(outputFilePath, renderedTemplate);
}
exports.genFile = genFile;
/**
* @param {string} filePath
* @param {string} contents
*/
function writeFile(filePath, contents) {
// create output directory if it does not exist
const dirname = path.dirname(outputFilePath);
const dirname = pathModule.dirname(filePath);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
try {
fs.writeFileSync(outputFilePath, renderedTemplate);
fs.writeFileSync(filePath, contents);
} catch (e) {
console.error(e);
exitWithError("could not generate output file");
exitWithError(`could not write file at path "${filePath}"`);
}
}
exports.genFile = genFile;
exports.writeFile = writeFile;
/**
* @param {string} templatePathParameter
@ -287,6 +300,28 @@ function isCustomPasswordTemplateDefault(templatePathParameter) {
}
exports.isCustomPasswordTemplateDefault = isCustomPasswordTemplateDefault;
/**
* @param {string} path
* @param {string} rootDirectory
* @param {(fullPath: string, rootDirectoryFromArgument: string) => void} callback
*/
function recursivelyApplyCallbackToFiles(callback, path, rootDirectory = "") {
const fullPath = pathModule.resolve(path);
const fullRootDirectory = rootDirectory || pathModule.dirname(fullPath);
if (fs.statSync(fullPath).isDirectory()) {
fs.readdirSync(fullPath).forEach((filePath) => {
const fullFilePath = `${fullPath}/${filePath}`;
recursivelyApplyCallbackToFiles(callback, fullFilePath, fullRootDirectory);
});
return;
}
callback(fullPath, fullRootDirectory);
}
exports.recursivelyApplyCallbackToFiles = recursivelyApplyCallbackToFiles;
function parseCommandLineArguments() {
return (
Yargs.usage("Usage: staticrypt <filename> [<filename> ...] [options]")
@ -299,8 +334,15 @@ function parseCommandLineArguments() {
.option("d", {
alias: "directory",
type: "string",
describe: "Name of the directory where the encrypted files will be saved.",
default: "encrypted/",
describe:
"Name of the directory where the generated files will be saved. If the '--decrypt' flag is " +
"set, default will be 'decrypted'.",
default: OUTPUT_DIRECTORY_DEFAULT_PATH,
})
.option("decrypt", {
type: "boolean",
describe: "Include this flag to decrypt files instead of encrypt.",
default: false,
})
.option("p", {
alias: "password",

Wyświetl plik

@ -12,22 +12,28 @@ if (nodeVersion[0] < 16) {
// parse .env file into process.env
require("dotenv").config();
const pathModule = require("path");
const fs = require("fs");
const cryptoEngine = require("../lib/cryptoEngine.js");
const codec = require("../lib/codec.js");
const { generateRandomSalt } = cryptoEngine;
const { encodeWithHashedPassword } = codec.init(cryptoEngine);
const { decode, encodeWithHashedPassword } = codec.init(cryptoEngine);
const {
parseCommandLineArguments,
OUTPUT_DIRECTORY_DEFAULT_PATH,
buildStaticryptJS,
isOptionSetByUser,
exitWithError,
genFile,
getFileContent,
getValidatedSalt,
getValidatedPassword,
getConfig,
getFileContent,
getPassword,
getValidatedSalt,
isOptionSetByUser,
parseCommandLineArguments,
recursivelyApplyCallbackToFiles,
validatePassword,
writeConfig,
writeFile,
} = require("./helpers.js");
// parse arguments
@ -67,23 +73,49 @@ async function runStatiCrypt() {
writeConfig(configPath, config);
}
process.exit(0);
return;
}
// get the salt & password
const salt = getValidatedSalt(namedArgs, config);
const password = await getValidatedPassword(namedArgs.password, namedArgs.short);
const password = await getPassword(namedArgs.password);
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
// display the share link with the hashed password if the --share flag is set
if (hasShareFlag) {
await validatePassword(password, namedArgs.short);
const url = namedArgs.share || "";
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
console.log(url + "#staticrypt_pwd=" + hashedPassword);
process.exit(0);
return;
}
// only process a directory if the --recursive flag is set
const directoriesInArguments = positionalArguments.filter((path) => fs.statSync(path).isDirectory());
if (directoriesInArguments.length > 0 && !namedArgs.recursive) {
exitWithError(
`'${directoriesInArguments[0].toString()}' is a directory. Use the -r|--recursive flag to process directories.`
);
}
// if asking for decryption, decrypt all the files
if (namedArgs.decrypt) {
const isOutputDirectoryDefault =
namedArgs.directory === OUTPUT_DIRECTORY_DEFAULT_PATH && !isOptionSetByUser("d", yargs);
const outputDirectory = isOutputDirectoryDefault ? "decrypted" : namedArgs.directory;
positionalArguments.forEach((path) => {
recursivelyApplyCallbackToFiles((fullPath, fullRootDirectory) => {
decodeAndGenerateFile(fullPath, fullRootDirectory, hashedPassword, salt, outputDirectory);
}, path);
});
return;
}
await validatePassword(password, namedArgs.short);
// write salt to config file
if (config.salt !== salt) {
config.salt = salt;
@ -105,35 +137,55 @@ async function runStatiCrypt() {
template_color_secondary: namedArgs.templateColorSecondary,
};
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
positionalArguments.forEach((path) =>
encodeAndGenerateFile(path.toString(), hashedPassword, salt, baseTemplateData, isRememberEnabled, namedArgs)
);
// encode all the files
positionalArguments.forEach((path) => {
recursivelyApplyCallbackToFiles((fullPath, fullRootDirectory) => {
encodeAndGenerateFile(
fullPath,
fullRootDirectory,
hashedPassword,
salt,
baseTemplateData,
isRememberEnabled,
namedArgs
);
}, path);
});
}
async function encodeAndGenerateFile(path, hashedPassword, salt, baseTemplateData, isRememberEnabled, namedArgs) {
// if the path is a directory, get into it and process all files
if (fs.statSync(path).isDirectory()) {
if (!namedArgs.recursive) {
console.log(
"ERROR: The path '" +
path +
"' is a directory. Use the -r|--recursive flag to process all files in the directory."
);
async function decodeAndGenerateFile(path, rootDirectoryFromArguments, hashedPassword, salt, outputDirectory) {
// get the file content
const encryptedFileContent = getFileContent(path);
// just return instead of exiting the process, that way all other files can be processed
return;
}
// extract the cipher text from the encrypted file
const cipherTextMatch = encryptedFileContent.match(/"staticryptEncryptedMsgUniqueVariableName":\s*"([^"]+)"/);
fs.readdirSync(path).forEach((filePath) => {
const fullPath = `${path}/${filePath}`;
encodeAndGenerateFile(fullPath, hashedPassword, salt, baseTemplateData, isRememberEnabled, namedArgs);
});
return;
if (!cipherTextMatch) {
return console.log(`ERROR: could not extract cipher text from ${path}`);
}
// decrypt input
const { success, decoded } = await decode(cipherTextMatch[1], hashedPassword, salt);
if (!success) {
return console.log(`ERROR: could not decrypt ${path}`);
}
const relativePath = pathModule.relative(rootDirectoryFromArguments, path);
const outputFilepath = outputDirectory + "/" + relativePath;
writeFile(outputFilepath, decoded);
}
async function encodeAndGenerateFile(
path,
rootDirectoryFromArguments,
hashedPassword,
salt,
baseTemplateData,
isRememberEnabled,
namedArgs
) {
// get the file content
const contents = getFileContent(path);
@ -141,7 +193,7 @@ async function encodeAndGenerateFile(path, hashedPassword, salt, baseTemplateDat
const encryptedMsg = await encodeWithHashedPassword(contents, hashedPassword);
const staticryptConfig = {
encryptedMsg,
staticryptEncryptedMsgUniqueVariableName: encryptedMsg,
isRememberEnabled,
rememberDurationInDays: namedArgs.remember,
salt,
@ -151,7 +203,9 @@ async function encodeAndGenerateFile(path, hashedPassword, salt, baseTemplateDat
staticrypt_config: staticryptConfig,
};
const outputFilepath = namedArgs.directory.replace(/\/+$/, "") + "/" + path;
// remove the base path so that the actual output path is relative to the base path
const relativePath = pathModule.relative(rootDirectoryFromArguments, path);
const outputFilepath = namedArgs.directory + "/" + relativePath;
genFile(templateData, outputFilepath, namedArgs.template);
}

Wyświetl plik

@ -6,7 +6,7 @@ const decode = codec.init(cryptoEngine).decode;
* Initialize the staticrypt module, that exposes functions callbable by the password_template.
*
* @param {{
* encryptedMsg: string,
* staticryptEncryptedMsgUniqueVariableName: string,
* isRememberEnabled: boolean,
* rememberDurationInDays: number,
* salt: string,
@ -29,10 +29,10 @@ function init(staticryptConfig, templateConfig) {
* @returns {Promise<boolean>}
*/
async function decryptAndReplaceHtml(hashedPassword) {
const { encryptedMsg, salt } = staticryptConfig;
const { staticryptEncryptedMsgUniqueVariableName, salt } = staticryptConfig;
const { replaceHtmlCallback } = templateConfig;
const result = await decode(encryptedMsg, hashedPassword, salt);
const result = await decode(staticryptEncryptedMsgUniqueVariableName, hashedPassword, salt);
if (!result.success) {
return false;
}

28
package-lock.json wygenerowano
Wyświetl plik

@ -1,12 +1,12 @@
{
"name": "staticrypt",
"version": "3.2.0",
"version": "3.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "staticrypt",
"version": "3.2.0",
"version": "3.3.0",
"license": "MIT",
"dependencies": {
"dotenv": "^16.0.3",
@ -15,6 +15,9 @@
"bin": {
"staticrypt": "cli/index.js"
},
"devDependencies": {
"prettier": "^2.8.7"
},
"engines": {
"node": ">=16.0.0"
}
@ -104,6 +107,21 @@
"node": ">=8"
}
},
"node_modules/prettier": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -248,6 +266,12 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"prettier": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"dev": true
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "staticrypt",
"version": "3.2.0",
"version": "3.3.0",
"description": "Based on the [crypto-js](https://github.com/brix/crypto-js) library, StatiCrypt uses AES-256 to encrypt your input with your long password and put it in a HTML file with a password prompt that can decrypted in-browser (client side).",
"main": "index.js",
"files": [
@ -43,5 +43,8 @@
"bugs": {
"url": "https://github.com/robinmoisson/staticrypt/issues"
},
"homepage": "https://github.com/robinmoisson/staticrypt"
"homepage": "https://github.com/robinmoisson/staticrypt",
"devDependencies": {
"prettier": "^2.8.7"
}
}