Skip to content

Commit

Permalink
add webcrypto as an option
Browse files Browse the repository at this point in the history
  • Loading branch information
hurrymaplelad committed Oct 24, 2022
1 parent 8345508 commit c7d17fa
Show file tree
Hide file tree
Showing 10 changed files with 927 additions and 177 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
.vscode/
node_modules
.staticrypt.json
/www/password_template.html
Expand Down
64 changes: 38 additions & 26 deletions cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
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 Down Expand Up @@ -145,9 +142,23 @@ const yargs = Yargs.usage("Usage: staticrypt <filename> <passphrase> [options]")
type: "string",
describe: "Title for output HTML page.",
default: "Protected Page",
})
.option("implementation", {
alias: "impl",
describe:
'Choose between library-based CryptoJS and native WebCrypto. ' +
'Most browsers only support WebCrypto in secure contexts like localhost or HTTPS',
type: "string",
choices: ["cryptojs", "webcrypto"],
default: "cryptojs",
});
const namedArgs = yargs.argv;

const impl = (namedArgs.impl === "cryptojs") ?
require("../lib/impl-cryptojs") : require("../lib/impl-webcrypto");
const {generateRandomSalt} = impl;
const {encode} = codec.init(impl);

// if the 's' flag is passed without parameter, generate a salt, display & exit
if (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
console.log(generateRandomSalt());
Expand Down Expand Up @@ -213,9 +224,6 @@ try {
process.exit(1);
}

// encrypt input
const encryptedMessage = encode(contents, passphrase, salt);

// create crypto-js tag (embedded or not)
let cryptoTag = SCRIPT_TAG;
if (namedArgs.embed) {
Expand All @@ -232,27 +240,31 @@ 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:
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,
};
// encrypt input
encode(contents, passphrase, salt).then((encryptedMessage) => {
const implModulePath = (namedArgs.impl === "cryptojs") ? "../lib/impl-cryptojs" : "../lib/impl-webcrypto";
const data = {
codec_iif: transcludeModule("../lib/codec"),
crypto_tag: cryptoTag,
decrypt_button: namedArgs.decryptButton,
embed: namedArgs.embed,
encrypted: encryptedMessage,
impl_iif: transcludeModule(implModulePath),
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);
genFile(data);
});

/**
* Fill the template with provided data and writes it to output file.
Expand Down
108 changes: 61 additions & 47 deletions example/example_encrypted.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,15 @@
var impl = ((function(){
const exports = {};

var IV_BITS = 16 * 8;
var HEX_BITS = 4;

/**
* 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(IV_BITS / 8);

var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, {
iv: iv,
Expand All @@ -183,7 +186,8 @@

// iv will be hex 16 in length (32 characters)
// we prepend it to the ciphertext for use in decryption
return iv.toString() + encrypted.toString();
var encoded = iv.toString() + encrypted.toString();
return Promise.resolve(encoded);
}
exports.encrypt = encrypt;

Expand All @@ -196,14 +200,17 @@
* @returns {string}
*/
function decrypt(encryptedMsg, hashedPassphrase) {
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, 32));
var encrypted = encryptedMsg.substring(32);
var ivLength = IV_BITS / HEX_BITS;
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, ivLength));
var encrypted = encryptedMsg.substring(ivLength);

return CryptoJS.AES.decrypt(encrypted, hashedPassphrase, {
const decrypted = CryptoJS.AES.decrypt(encrypted, hashedPassphrase, {
iv: iv,
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC,
}).toString(CryptoJS.enc.Utf8);

return Promise.resolve(decrypted);
}
exports.decrypt = decrypt;

Expand All @@ -220,7 +227,7 @@
iterations: 1000,
});

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

Expand All @@ -230,10 +237,11 @@
exports.generateRandomSalt = generateRandomSalt;

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

Expand All @@ -254,15 +262,18 @@
* @param {string} msg
* @param {string} passphase
* @param {sting} salt
* @returns {string} The encoded text
* @returns {Promise<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;
return impl.hashPassphrase(passphrase, salt).then(function(hashedPassphrase) {
return impl.encrypt(msg, hashedPassphrase).then(function(encrypted) {
return impl.signMessage(hashedPassphrase, encrypted).then(function(hmac) {
// 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)
return hmac + encrypted;
});
});
});
}
exports.encode = encode;

Expand All @@ -272,20 +283,22 @@
*
* @param {*} encoded
* @param {*} hashedPassphrase
* @returns {Object} {success: true, decoded: string} | {succss: false, message: string}
* @returns {Promise<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),
};
return impl.signMessage(hashedPassphrase, encryptedMsg).then(function(decryptedHMAC) {
if (decryptedHMAC !== encryptedHMAC) {
return { success: false, message: "Signature mismatch" };
}
return impl.decrypt(encryptedMsg, hashedPassphrase).then(function(decoded) {
return {
success: true,
decoded: decoded,
};
});
});
}
exports.decode = decode;

Expand Down Expand Up @@ -316,7 +329,7 @@
var decode = codec.init(impl).decode;

// variables to be filled when generating the file
var encryptedMsg = '5b8ddda06f7972f55040c4e53ac58581408b427bb14d0856b3e8fa275836489fa2195f5c68889a2c9b81f3bad4d84683U2FsdGVkX1+ZRQKlbRyks/JsZcQ+tKL24t+BOrwQilIyt5/uiwboh274a5/NfJs+iJG7meL6uQlKNUKgVPNW7pL00Wvsj2PTZbve2YuB7Qq1QqZZ/jAM9IXPWAMKs9VoGBEvDw3BUhgiCVOq0ibTYFBz9LO5xcuI1Gwdnw7DNYE2/aadml4/eo4D3yQI7U61vbBo1KYjEGr9rBgoUHnkSw==',
var encryptedMsg = '8af5413a09610945eaf71ded7559ecaaa93c8414c8e39e3e4951cf8ade51c9a399c664369959d92b91ab900fc4f7f7d6U2FsdGVkX18uzOPNOJ7JLg2NQCum1dvLl7CO+XZCJU6JdN7QwWmx4aSJDqnWfu6lmttosCYmZFfbTg8eT/8NcVoP2O9dCRohqIBzXJpfnFiJ0SRSSxWF/vkImNJUOamBQyd+6ND41LEC7YmkzQAJpu4z7TIrTqXIv37a0wICxwmpFC4AO8SoIYvubRXXVDZipUJQetvK3ZBOCBF6OSZbag==',
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
isRememberEnabled = true,
rememberDurationInDays = 0; // 0 means forever
Expand All @@ -332,15 +345,16 @@
* @returns
*/
function decryptAndReplaceHtml(hashedPassphrase) {
var result = decode(encryptedMsg, hashedPassphrase);
if (!result.success) {
return false;
}
var plainHTML = result.decoded;
return decode(encryptedMsg, hashedPassphrase).then(function(result) {
if (!result.success) {
return false;
}
var plainHTML = result.decoded;

document.write(plainHTML);
document.close();
return true;
document.write(plainHTML);
document.close();
return true;
});
}

/**
Expand Down Expand Up @@ -378,13 +392,13 @@

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();
}
decryptAndReplaceHtml(hashedPassphrase).then(function(isDecryptionSuccessful) {
// 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();
}
});
}
}
}
Expand All @@ -397,10 +411,12 @@
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;

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

if (isDecryptionSuccessful) {
impl.hashPassphrase(passphrase, salt).then(function(hashedPassphrase) {
return decryptAndReplaceHtml(hashedPassphrase);
}).then(function(isDecryptionSuccessful) {
if (!isDecryptionSuccessful) {
return alert('Bad passphrase!');
}
// remember the hashedPassphrase and set its expiration if necessary
if (isRememberEnabled && shouldRememberPassphrase) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassphrase);
Expand All @@ -413,9 +429,7 @@
);
}
}
} else {
alert('Bad passphrase!');
}
});
});
</script>
</body>
Expand Down
Loading

0 comments on commit c7d17fa

Please sign in to comment.