Skip to content

Commit

Permalink
dedupe common crypto functions
Browse files Browse the repository at this point in the history
  • Loading branch information
hurrymaplelad committed Nov 9, 2022
1 parent bdc831d commit 225d7b2
Show file tree
Hide file tree
Showing 8 changed files with 911 additions and 159 deletions.
87 changes: 25 additions & 62 deletions cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

"use strict";

const CryptoJS = require("crypto-js");
const fs = require("fs");
const path = require("path");
const Yargs = require("yargs");
const impl = require("../lib/impl-cryptojs");
const codec = require("../lib/codec");
const { generateRandomSalt } = impl;
const { encode } = codec.init(impl);

const SCRIPT_URL =
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js";
Expand All @@ -15,41 +18,24 @@ const SCRIPT_TAG =
'" integrity="sha384-lp4k1VRKPU9eBnPePjnJ9M2RF3i7PC30gXs70+elCVfgwLwx1tv5+ctxdtwxqZa7" crossorigin="anonymous"></script>';

/**
* Salt and encrypt a msg with a password.
* Inspired by https://github.com/adonespitogo
* A dead-simple alternative to webpack or rollup for inlining simple
* CommonJS modules in a browser <script>.
* - Removes all lines containing require().
* - Wraps the module in an immediately invoked function that returns `exports`.
*/
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();
}

/**
* 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) {
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
keySize: 256 / 32,
iterations: 1000,
});

return hashedPassphrase.toString();
}

function generateRandomSalt() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
function transcludeModule(modulePath) {
const resolvedPath = path.join(__dirname, ...modulePath.split("/")) + ".js";
const moduleText = fs
.readFileSync(resolvedPath, "utf8")
.replaceAll(/^.*\brequire\(.*$\n/gm, "");

return `
((function(){
const exports = {};
${moduleText}
return exports;
})())
`.trim();
}

/**
Expand Down Expand Up @@ -228,15 +214,7 @@ try {
}

// encrypt input
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 encryptedMessage = hmac + encrypted;
const encryptedMessage = encode(contents, passphrase, salt);

// create crypto-js tag (embedded or not)
let cryptoTag = SCRIPT_TAG;
Expand All @@ -255,10 +233,12 @@ if (namedArgs.embed) {
}

const data = {
codec_iif: transcludeModule("../lib/codec"),
crypto_tag: cryptoTag,
decrypt_button: namedArgs.decryptButton,
embed: namedArgs.embed,
encrypted: encryptedMessage,
impl_iif: transcludeModule("../lib/impl-cryptojs"),
instructions: namedArgs.instructions,
is_remember_enabled: namedArgs.noremember ? "false" : "true",
output_file_path:
Expand Down Expand Up @@ -289,7 +269,7 @@ function genFile(data) {
process.exit(1);
}

const renderedTemplate = render(templateContents, data);
const renderedTemplate = codec.render(templateContents, data);

try {
fs.writeFileSync(data.output_file_path, renderedTemplate);
Expand All @@ -298,20 +278,3 @@ function genFile(data) {
process.exit(1);
}
}

/**
* Replace the placeholder tags (between '{tag}') in 'tpl' string with provided data.
*
* @param tpl
* @param data
* @returns string
*/
function render(tpl, data) {
return tpl.replace(/{(.*?)}/g, function (_, key) {
if (data && data[key] !== undefined) {
return data[key];
}

return "";
});
}
197 changes: 155 additions & 42 deletions example/example_encrypted.html
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,158 @@
<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>
var impl = ((function(){
const exports = {};

/**
* 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 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.
* Inspired by https://github.com/adonespitogo
*
* @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) {
var hashedPassphrase = CryptoJS.PBKDF2(passphrase, salt, {
keySize: 256 / 32,
iterations: 1000,
});

return hashedPassphrase.toString();
}
exports.hashPassphrase = hashPassphrase;

function generateRandomSalt() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
}
exports.generateRandomSalt = generateRandomSalt;

function signMessage(hashedPassphrase, message) {
return CryptoJS.HmacSHA256(
message,
CryptoJS.SHA256(hashedPassphrase).toString()
).toString();
}
exports.signMessage = signMessage;

return exports;
})())
var codec = ((function(){
const exports = {};
/**
* This file includes functions shared across all crytography implementations in lib/
* as well as cli and www clients.
*/
function init(impl) {
const exports = {};
/**
* Top-level function for encoding a message.
* Includes passphrase hashing, encryption, and signing.
*
* @param {string} msg
* @param {string} passphase
* @param {sting} salt
* @returns {string} The encoded text
*/
function encode(msg, passphrase, salt) {
const hashedPassphrase = impl.hashPassphrase(passphrase, salt);
const encrypted = impl.encrypt(msg, 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 = impl.signMessage(hashedPassphrase, encrypted);
return hmac + encrypted;
}
exports.encode = encode;

/**
* Top-level function for decoding a message.
* Includes signature check, an decryption.
*
* @param {*} encoded
* @param {*} hashedPassphrase
* @returns {Object} {success: true, decoded: string} | {succss: false, message: string}
*/
function decode(signedMsg, hashedPassphrase) {
const encryptedHMAC = signedMsg.substring(0, 64);
const encryptedMsg = signedMsg.substring(64);
const decryptedHMAC = impl.signMessage(hashedPassphrase, encryptedMsg);

if (decryptedHMAC !== encryptedHMAC) {
return { success: false, message: "Signature mismatch" };
}
return {
success: true,
decoded: impl.decrypt(encryptedMsg, hashedPassphrase),
};
}
exports.decode = decode;

return exports;
}
exports.init = init;

/**
* Replace the placeholder tags (between '{tag}') in 'tpl' string with provided data.
*
* @param tpl
* @param data
* @returns string
*/
function render(tpl, data) {
return tpl.replace(/{(.*?)}/g, function (_, key) {
if (data && data[key] !== undefined) {
return data[key];
}

return "";
});
}
exports.render = render;

return exports;
})())
var decode = codec.init(impl).decode;

// variables to be filled when generating the file
var encryptedMsg = '65a0577162396cc1bddae60b8f435291ff7a69644825b98bfc636a29089a28efbc9417689b983f2048bb776ca66eb25fU2FsdGVkX19n36H4ocM7GbaeFVganWX86ZTHEZk2w12z3z7rqWDW8OESK8MmGtbnPJetgyWi3jpz3iI+rE/gSilJkhQ2YR/4yCBintGLeh1hCgX+XPBEDT0w+ri4uqUWCxDUIvzyUhbnf1ZD2WsK9wmDHwRwF9YcucHXuyS7/GlUcVsYERzxxDd9frN6DbubNNbdY/QtG+vtmLSwHGZtwQ==',
var encryptedMsg = 'b95277ccf1e06625c90a837647563f9a11a5a47c3d861af72bebfc18a73c58f0922e1bd59a6f8a209711ea9852710f57U2FsdGVkX1+jQJWwwAq6XYWLTlrJL+U9JmFyiQ81/qIb44gnibQJ2SVzUbbCDlnAYxNoVm3ky2CoYGyrqTxBVWg550CATCFWGBpeFIMxIPj0IsN5MVHwhZO+DnerhHcV9OwEdryI7J43XQ1ZX8MjsS9/f04lt9pjrNmIw+8aksAys5oQ7Uv65iJdbiHJCHrzLckfM4Al01053nA0RyfcNQ==',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
isRememberEnabled = true,
rememberDurationInDays = 0; // 0 means forever
Expand All @@ -175,61 +325,24 @@
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) {
var result = decode(encryptedMsg, hashedPassphrase);
if (!result.success) {
return false;
}

var plainHTML = decryptMsg(encryptedHTML, hashedPassphrase);
var plainHTML = result.decoded;

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
*/
Expand Down Expand Up @@ -284,7 +397,7 @@
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;

// decrypt and replace the whole page
var hashedPassphrase = hashPassphrase(passphrase);
var hashedPassphrase = impl.hashPassphrase(passphrase, salt);
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);

if (isDecryptionSuccessful) {
Expand Down
Loading

0 comments on commit 225d7b2

Please sign in to comment.