make WebCrypto the only engine available (closes #168)

v3
robinmoisson 2023-03-29 16:12:44 +02:00
rodzic 7021e3ad8d
commit 92691a1994
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 9419716500078583
11 zmienionych plików z 31 dodań i 404 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
const fs = require("fs");
const { generateRandomSalt } = require("../lib/cryptoEngine/webcryptoEngine.js");
const { generateRandomSalt } = require("../lib/cryptoEngine.js");
const path = require("path");
const {renderTemplate} = require("../lib/formater.js");
const Yargs = require("yargs");
@ -168,39 +168,6 @@ function genFile(data, outputFilePath, templateFilePath) {
}
exports.genFile = genFile;
/**
* TODO: remove in next major version
*
* This method checks whether the password template support the security fix increasing PBKDF2 iterations. Users using
* an old custom password_template might have logic that doesn't benefit from the fix.
*
* @param {string} templatePathParameter
* @returns {boolean}
*/
function isCustomPasswordTemplateLegacy(templatePathParameter) {
const customTemplateContent = readFile(templatePathParameter, "template");
// if the template injects the crypto engine, it's up to date
return !customTemplateContent.includes("js_crypto_engine");
}
exports.isCustomPasswordTemplateLegacy = isCustomPasswordTemplateLegacy;
/**
* TODO: remove in next major version
*
* This method checks whether the password template support the async logic.
*
* @param {string} templatePathParameter
* @returns {boolean}
*/
function isPasswordTemplateUsingAsync(templatePathParameter) {
const customTemplateContent = readFile(templatePathParameter, "template");
// if the template includes this comment, it's up to date
return customTemplateContent.includes("// STATICRYPT_VERSION: async");
}
exports.isPasswordTemplateUsingAsync = isPasswordTemplateUsingAsync;
/**
* @param {string} templatePathParameter
* @returns {boolean}
@ -224,18 +191,6 @@ function parseCommandLineArguments() {
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("engine", {
type: "string",
describe: "The crypto engine to use. WebCrypto uses 600k iterations and is more secure, CryptoJS 15k.\n" +
"Possible values: 'cryptojs', 'webcrypto'.",
default: "cryptojs",
})
.option("f", {
alias: "file-template",
type: "string",

Wyświetl plik

@ -3,31 +3,21 @@
"use strict";
const fs = require("fs");
const path = require("path");
// parse .env file into process.env
require('dotenv').config();
const cryptojsEngine = require("../lib/cryptoEngine/cryptojsEngine");
const webcryptoEngine = require("../lib/cryptoEngine/webcryptoEngine");
const cryptoEngine = require("../lib/cryptoEngine.js");
const codec = require("../lib/codec.js");
const { generateRandomSalt, generateRandomString } = cryptoEngine;
const { encode } = codec.init(cryptoEngine);
const { convertCommonJSToBrowserJS, exitWithError, isOptionSetByUser, genFile, getPassword, getFileContent, getSalt} = require("./helpers");
const { isCustomPasswordTemplateLegacy, parseCommandLineArguments, isPasswordTemplateUsingAsync} = require("./helpers.js");
const CRYPTOJS_SCRIPT_TAG =
'<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>';
const { parseCommandLineArguments} = require("./helpers.js");
// parse arguments
const yargs = parseCommandLineArguments();
const namedArgs = yargs.argv;
// set the crypto engine
const isWebcrypto = namedArgs.engine === "webcrypto";
const cryptoEngine = isWebcrypto ? webcryptoEngine : cryptojsEngine;
const { generateRandomSalt, generateRandomString } = cryptoEngine;
const { encode } = codec.init(cryptoEngine);
async function runStatiCrypt() {
// if the 's' flag is passed without parameter, generate a salt, display & exit
if (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
@ -91,94 +81,32 @@ async function runStatiCrypt() {
console.log(url + "#staticrypt_pwd=" + hashedPassword);
}
// TODO: remove in the next major version bump. This is to allow a security update to some versions without breaking
// older ones. If the password template is custom AND created before 2.2.0 we need to use the old hashing algorithm.
const isLegacy = isCustomPasswordTemplateLegacy(namedArgs.f);
if (isLegacy) {
console.log(
"#################################\n\n" +
"SECURITY WARNING [StatiCrypt]: You are using an old version of the password template, which has been found to " +
"be less secure. Please update your custom password_template logic to match the latest version." +
"\nYou can find instructions here: https://github.com/robinmoisson/staticrypt/issues/161" +
"\n\n#################################"
);
}
if (!isWebcrypto) {
console.log(
"WARNING: If you are viewing the file over HTTPS or locally, we recommend " +
(isPasswordTemplateUsingAsync(namedArgs.f) ? "" : "updating your password template to the latest version and ") +
"using the '--engine webcrypto' more secure engine. It will become the default in StatiCrypt next major version."
);
} else if (!isPasswordTemplateUsingAsync(namedArgs.f) && isWebcrypto) {
exitWithError(
"The '--engine webcrypto' engine is only available for password templates that use async/await. Please " +
"update your password template to the latest version or use the '--engine cryptojs' engine."
)
}
// create crypto-js tag (embedded or not)
let cryptoTag = CRYPTOJS_SCRIPT_TAG;
if (isWebcrypto) {
cryptoTag = "";
} else if (namedArgs.embed) {
try {
const embedContents = fs.readFileSync(
path.join(__dirname, "..", "lib", "kryptojs-3.1.9-1.min.js"),
"utf8"
);
cryptoTag = "<script>" + embedContents + "</script>";
} catch (e) {
exitWithError("Embed file does not exist.");
}
}
const cryptoEngineString = isWebcrypto
? convertCommonJSToBrowserJS("lib/cryptoEngine/webcryptoEngine")
: convertCommonJSToBrowserJS("lib/cryptoEngine/cryptojsEngine");
// get the file content
const contents = getFileContent(inputFilepath);
// encrypt input
encode(contents, password, salt, isLegacy).then((encryptedMessage) => {
let codecString;
if (isWebcrypto) {
codecString = convertCommonJSToBrowserJS("lib/codec");
} else {
// TODO: remove on next major version bump. The replace is a hack to pass the salt to the injected js_codec in
// a backward compatible way (not requiring to update the password_template). Same for using a "sync" version
// of the codec.
codecString = convertCommonJSToBrowserJS("lib/codec-sync").replace('##SALT##', salt);
}
const encryptedMessage = await encode(contents, password, salt);
const data = {
crypto_tag: cryptoTag,
decrypt_button: namedArgs.decryptButton,
// TODO: deprecated option here for backward compat, remove on next major version bump
embed: isWebcrypto ? false : namedArgs.embed,
encrypted: encryptedMessage,
instructions: namedArgs.instructions,
is_remember_enabled: namedArgs.noremember ? "false" : "true",
js_codec: codecString,
js_crypto_engine: cryptoEngineString,
label_error: namedArgs.labelError,
passphrase_placeholder: namedArgs.passphrasePlaceholder,
remember_duration_in_days: namedArgs.remember,
remember_me: namedArgs.rememberLabel,
salt: salt,
title: namedArgs.title,
};
const data = {
decrypt_button: namedArgs.decryptButton,
encrypted: encryptedMessage,
instructions: namedArgs.instructions,
is_remember_enabled: namedArgs.noremember ? "false" : "true",
js_codec: convertCommonJSToBrowserJS("lib/codec"),
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
label_error: namedArgs.labelError,
passphrase_placeholder: namedArgs.passphrasePlaceholder,
remember_duration_in_days: namedArgs.remember,
remember_me: namedArgs.rememberLabel,
salt: salt,
title: namedArgs.title,
};
const outputFilepath = namedArgs.output !== null
? namedArgs.output
: inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
const outputFilepath = namedArgs.output !== null
? namedArgs.output
: inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
genFile(data, outputFilepath, namedArgs.f);
});
genFile(data, outputFilepath, namedArgs.f);
}
runStatiCrypt();

Wyświetl plik

@ -201,8 +201,6 @@ const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*
* Mirrors the API of CryptoJS.enc.Hex
*/
const HexEncoder = {
/**
@ -564,7 +562,7 @@ exports.init = init;
const decode = codec.init(cryptoEngine).decode;
// variables to be filled when generating the file
const encryptedMsg = 'df7428ed075decd3c4ff9d8ab6d2bea4410854c863d705789fb22e14b7da7ee20e94c593047abb9ba34d6519eebc879d2097bb918c0af0d4e248959849fb9c6bbb93aba054806c8773d1e4b63ec317185ad5462a9919dda986716c67bb57a89a044de3e25707cded482657c4a0208e9916aaa9d839f090eaaeb95603e05db11fe4bc37c4d98b9170124ce1c7ca18fe39c2f179e23eee61ba7d79cb3145e8833936c62adeffce1f5e129745c89541faa8100bfde4733bfa9c0ecf04768b3d1889',
const encryptedMsg = '2c0a13159934226fa022225a06c64af6dfffe80dd6cb908f0f6ad93311563d78150cfc5465e3c7d70d682194a6d4a0c6082e37aaca8dbd83036d9e9cf629a112132b8fb6004a028b31ea4fb5a9f82d505096fe59e109970261733b4c8f21110b6f365d8b087d0ec15866917341e3cd105c65c9c7542626bae08903cd10675ed7fbd71062a1e87e35d30341c8251ab452352eaeecd6e44ed0a256979b30287032bccefaecf1685e948bf0fc3a3b5ad1f7b70092d9e32ceb60818ee49e821f8933',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
labelError = 'Bad password!',
isRememberEnabled = true,

Wyświetl plik

@ -184,11 +184,6 @@ Your encrypted string</pre>
</div>
</div>
<!--
Filename changed to circumvent adblockers that mistake it for a crypto miner (see https://github.com/robinmoisson/staticrypt/issues/107)
-->
<script src="lib/kryptojs-3.1.9-1.min.js"></script>
<script src="https://cdn.ckeditor.com/4.7.0/standard/ckeditor.js"></script>
<script id="cryptoEngine">
@ -203,8 +198,6 @@ const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*
* Mirrors the API of CryptoJS.enc.Hex
*/
const HexEncoder = {
/**
@ -615,8 +608,8 @@ exports.renderTemplate = renderTemplate;
}
/**
* Register something happened - this uses a simple Supabase function to implement a counter, and allows use to drop
* google analytics.
* Register something happened - this uses a simple Supabase function to implement a counter, and allows to drop
* google analytics. We don't store any personal data or IP.
*
* @param action
*/

Wyświetl plik

@ -1,95 +0,0 @@
/**
* TODO: delete this file in next major version. This is a version of the codec that doesn't use async, so we can use it
* in old custom password_template. It will only be used with the cryptoJS engine.
*
* Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
*
* @param cryptoEngine - the engine to use for encryption / decryption
*/
function init(cryptoEngine) {
// TODO: remove on next major version bump. This is a hack to make the salt available in all functions here in a
// backward compatible way (not requiring to change the password_template).
const backwardCompatibleSalt = '##SALT##';
const exports = {};
/**
* Top-level function for encoding a message.
* Includes password hashing, encryption, and signing.
*
* @param {string} msg
* @param {string} password
* @param {string} salt
* @param {boolean} isLegacy - whether to use the legacy hashing algorithm (1k iterations) or not
*
* @returns {string} The encoded text
*/
function encode(msg, password, salt, isLegacy = false) {
// TODO: remove in the next major version bump. This is to not break backwards compatibility with the old way of hashing
const hashedPassphrase = isLegacy
? cryptoEngine.hashLegacyRound(password, salt)
: cryptoEngine.hashPassphrase(password, salt);
const encrypted = cryptoEngine.encrypt(msg, hashedPassphrase);
// we use the hashed password in the HMAC because this is effectively what will be used a password (so we can store
// it in localStorage safely, we don't use the clear text password)
const hmac = cryptoEngine.signMessage(hashedPassphrase, encrypted);
return hmac + encrypted;
}
exports.encode = encode;
/**
* Top-level function for decoding a message.
* Includes signature check and decryption.
*
* @param {string} signedMsg
* @param {string} hashedPassphrase
* @param {string} backwardCompatibleHashedPassword
*
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
*/
function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
const encryptedHMAC = signedMsg.substring(0, 64);
const encryptedMsg = signedMsg.substring(64);
const decryptedHMAC = cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
if (decryptedHMAC !== encryptedHMAC) {
// TODO: remove in next major version bump. This is to not break backwards compatibility with the old 1k
// iterations in PBKDF2 - if the key we try isn't working, it might be because it's a remember-me/autodecrypt
// link key, generated with 1k iterations. Try again with the updated iteration count.
if (!backwardCompatibleHashedPassword) {
return decode(
signedMsg,
cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
hashedPassphrase
);
}
return { success: false, message: "Signature mismatch" };
}
// TODO: remove in next major version bump. If we're trying to double hash for backward compatibility reasons,
// and the attempt is successful, we check if we should update the stored password in localStorage. This avoids
// having to compute the upgrade each time.
if (backwardCompatibleHashedPassword) {
if (window && window.localStorage) {
const storedPassword = window.localStorage.getItem('staticrypt_passphrase');
// check the stored password is actually the backward compatible one, so we don't save the new one and trigger
// the "remember-me" by mistake, leaking the password
if (storedPassword === backwardCompatibleHashedPassword) {
window.localStorage.setItem('staticrypt_passphrase', hashedPassphrase);
}
}
}
return {
success: true,
decoded: cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
};
}
exports.decode = decode;
return exports;
}
exports.init = init;

Wyświetl plik

@ -8,8 +8,6 @@ const ENCRYPTION_ALGO = "AES-CBC";
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array bytes.
*
* Mirrors the API of CryptoJS.enc.Hex
*/
const HexEncoder = {
/**

Wyświetl plik

@ -1,138 +0,0 @@
const CryptoJS = require("crypto-js");
/**
* Salt and encrypt a msg with a password.
*/
function encrypt(msg, hashedPassphrase) {
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,
});
// iv will be hex 16 in length (32 characters)
// we prepend it to the ciphertext for use in decryption
return iv.toString() + encrypted.toString();
}
exports.encrypt = encrypt;
/**
* Decrypt a salted msg using a password.
*
* @param {string} encryptedMsg
* @param {string} hashedPassphrase
* @returns {string}
*/
function decrypt(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);
}
exports.decrypt = decrypt;
/**
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} passphrase
* @param {string} salt
* @returns string
*/
function hashPassphrase(passphrase, salt) {
// we hash the passphrase in two steps: first 1k iterations, then we add iterations. This is because we used to use 1k,
// so for backwards compatibility with remember-me/autodecrypt links, we need to support going from that to more
// iterations
var hashedPassphrase = hashLegacyRound(passphrase, salt);
return hashSecondRound(hashedPassphrase, salt);
}
exports.hashPassphrase = hashPassphrase;
/**
* This hashes the passphrase with 1k iterations. This is a low number, we need this function to support backwards
* compatibility.
*
* @param {string} passphrase
* @param {string} salt
* @returns {string}
*/
function hashLegacyRound(passphrase, salt) {
return CryptoJS.PBKDF2(passphrase, salt, {
keySize: 256 / 32,
iterations: 1000,
}).toString();
}
exports.hashLegacyRound = hashLegacyRound;
/**
* Add a second round of iterations. This is because we used to use 1k, so for backwards compatibility with
* remember-me/autodecrypt links, we need to support going from that to more iterations.
*
* @param hashedPassphrase
* @param salt
* @returns {string}
*/
function hashSecondRound(hashedPassphrase, salt) {
return CryptoJS.PBKDF2(hashedPassphrase, salt, {
keySize: 256 / 32,
iterations: 14000,
hasher: CryptoJS.algo.SHA256,
}).toString();
}
exports.hashSecondRound = hashSecondRound;
function generateRandomSalt() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
}
exports.generateRandomSalt = generateRandomSalt;
function getRandomAlphanum() {
var possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var byteArray;
var parsedInt;
// Keep generating new random bytes until we get a value that falls
// within a range that can be evenly divided by possibleCharacters.length
do {
byteArray = CryptoJS.lib.WordArray.random(1);
// extract the lowest byte to get an int from 0 to 255 (probably unnecessary, since we're only generating 1 byte)
parsedInt = byteArray.words[0] & 0xff;
} while (parsedInt >= 256 - (256 % possibleCharacters.length));
// Take the modulo of the parsed integer to get a random number between 0 and totalLength - 1
var randomIndex = parsedInt % possibleCharacters.length;
return possibleCharacters[randomIndex];
}
/**
* Generate a random string of a given length.
*
* @param {int} length
* @returns {string}
*/
function generateRandomString(length) {
var randomString = '';
for (var i = 0; i < length; i++) {
randomString += getRandomAlphanum();
}
return randomString;
}
exports.generateRandomString = generateRandomString;
function signMessage(hashedPassphrase, message) {
return CryptoJS.HmacSHA256(
message,
CryptoJS.SHA256(hashedPassphrase).toString()
).toString();
}
exports.signMessage = signMessage;

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "staticrypt",
"version": "2.6.0",
"version": "3.0.0",
"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": [

Wyświetl plik

@ -2,7 +2,7 @@ const { convertCommonJSToBrowserJS, genFile } = require("../cli/helpers.js");
const data = {
js_codec: convertCommonJSToBrowserJS("lib/codec"),
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine/webcryptoEngine"),
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine"),
js_formater: convertCommonJSToBrowserJS("lib/formater"),
};

Wyświetl plik

@ -184,11 +184,6 @@ Your encrypted string</pre>
</div>
</div>
<!--
Filename changed to circumvent adblockers that mistake it for a crypto miner (see https://github.com/robinmoisson/staticrypt/issues/107)
-->
<script src="lib/kryptojs-3.1.9-1.min.js"></script>
<script src="https://cdn.ckeditor.com/4.7.0/standard/ckeditor.js"></script>
<script id="cryptoEngine">
@ -223,8 +218,8 @@ Filename changed to circumvent adblockers that mistake it for a crypto miner (se
}
/**
* Register something happened - this uses a simple Supabase function to implement a counter, and allows use to drop
* google analytics.
* Register something happened - this uses a simple Supabase function to implement a counter, and allows to drop
* google analytics. We don't store any personal data or IP.
*
* @param {string} action
*/