kopia lustrzana https://github.com/robinmoisson/staticrypt
working webcrypto
rodzic
f824538bd0
commit
8116020815
|
@ -211,10 +211,14 @@ function parseCommandLineArguments() {
|
|||
.option("e", {
|
||||
alias: "embed",
|
||||
type: "boolean",
|
||||
describe:
|
||||
"Whether or not to embed crypto-js in the page (or use an external CDN).",
|
||||
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. Possible values: 'cryptojs', 'webcrypto'.",
|
||||
default: "cryptojs",
|
||||
})
|
||||
.option("f", {
|
||||
alias: "file-template",
|
||||
type: "string",
|
||||
|
|
84
cli/index.js
84
cli/index.js
|
@ -4,29 +4,30 @@
|
|||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Yargs = require("yargs");
|
||||
|
||||
// parse .env file into process.env
|
||||
require('dotenv').config();
|
||||
|
||||
const cryptoEngine = require("../lib/cryptoEngine/cryptojsEngine");
|
||||
const cryptojsEngine = require("../lib/cryptoEngine/cryptojsEngine");
|
||||
const webcryptoEngine = require("../lib/cryptoEngine/webcryptoEngine");
|
||||
const codec = require("../lib/codec");
|
||||
const { convertCommonJSToBrowserJS, exitEarly, isOptionSetByUser, genFile, getPassword, getFileContent, getSalt} = require("./helpers");
|
||||
const { isCustomPasswordTemplateLegacy, parseCommandLineArguments} = require("./helpers.js");
|
||||
const { generateRandomSalt, generateRandomString } = cryptoEngine;
|
||||
const { encode } = codec.init(cryptoEngine);
|
||||
|
||||
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 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>';
|
||||
|
||||
// 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);
|
||||
|
||||
// if the 's' flag is passed without parameter, generate a salt, display & exit
|
||||
if (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
|
||||
console.log(generateRandomSalt());
|
||||
|
@ -105,12 +106,11 @@ if (isLegacy) {
|
|||
);
|
||||
}
|
||||
|
||||
// encrypt input
|
||||
const encryptedMessage = encode(contents, password, salt, isLegacy);
|
||||
|
||||
// create crypto-js tag (embedded or not)
|
||||
let cryptoTag = SCRIPT_TAG;
|
||||
if (namedArgs.embed) {
|
||||
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"),
|
||||
|
@ -123,27 +123,37 @@ if (namedArgs.embed) {
|
|||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
crypto_tag: cryptoTag,
|
||||
decrypt_button: namedArgs.decryptButton,
|
||||
embed: namedArgs.embed,
|
||||
encrypted: encryptedMessage,
|
||||
instructions: namedArgs.instructions,
|
||||
is_remember_enabled: namedArgs.noremember ? "false" : "true",
|
||||
// TODO: remove on next major version bump. This is a hack to pass the salt to the injected js_codec in a backward
|
||||
// compatible way (not requiring to update the password_template).
|
||||
js_codec: convertCommonJSToBrowserJS("lib/codec").replace('##SALT##', salt),
|
||||
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine/cryptojsEngine"),
|
||||
label_error: namedArgs.labelError,
|
||||
passphrase_placeholder: namedArgs.passphrasePlaceholder,
|
||||
remember_duration_in_days: namedArgs.remember,
|
||||
remember_me: namedArgs.rememberLabel,
|
||||
salt: salt,
|
||||
title: namedArgs.title,
|
||||
};
|
||||
const cryptoEngineString = isWebcrypto
|
||||
? convertCommonJSToBrowserJS("lib/cryptoEngine/webcryptoEngine")
|
||||
: convertCommonJSToBrowserJS("lib/cryptoEngine/cryptojsEngine");
|
||||
|
||||
const outputFilepath = namedArgs.output !== null
|
||||
? namedArgs.output
|
||||
: inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
|
||||
|
||||
genFile(data, outputFilepath, namedArgs.f);
|
||||
// encrypt input
|
||||
encode(contents, password, salt, isLegacy).then((encryptedMessage) => {
|
||||
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",
|
||||
// TODO: remove on next major version bump. This is a hack to pass the salt to the injected js_codec in a backward
|
||||
// compatible way (not requiring to update the password_template).
|
||||
js_codec: convertCommonJSToBrowserJS("lib/codec").replace('##SALT##', salt),
|
||||
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 outputFilepath = namedArgs.output !== null
|
||||
? namedArgs.output
|
||||
: inputFilepath.replace(/\.html$/, "") + "_encrypted.html";
|
||||
|
||||
genFile(data, outputFilepath, namedArgs.f);
|
||||
|
||||
});
|
|
@ -184,48 +184,135 @@
|
|||
</div>
|
||||
|
||||
|
||||
<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 cryptoEngine = ((function(){
|
||||
const cryptoEngine = ((function(){
|
||||
const exports = {};
|
||||
|
||||
const { subtle } = crypto;
|
||||
|
||||
const IV_BITS = 16 * 8;
|
||||
const HEX_BITS = 4;
|
||||
const ENCRYPTION_ALGO = "AES-CBC";
|
||||
|
||||
/**
|
||||
* Translates between utf8 encoded hexadecimal strings
|
||||
* and Uint8Array bytes.
|
||||
*
|
||||
* Mirrors the API of CryptoJS.enc.Hex
|
||||
*/
|
||||
const HexEncoder = {
|
||||
/**
|
||||
* hex string -> bytes
|
||||
* @param {string} hexString
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
parse: function (hexString) {
|
||||
if (hexString.length % 2 !== 0) throw "Invalid hexString";
|
||||
const arrayBuffer = new Uint8Array(hexString.length / 2);
|
||||
|
||||
for (let i = 0; i < hexString.length; i += 2) {
|
||||
const byteValue = parseInt(hexString.substring(i, i + 2), 16);
|
||||
if (isNaN(byteValue)) {
|
||||
throw "Invalid hexString";
|
||||
}
|
||||
arrayBuffer[i / 2] = byteValue;
|
||||
}
|
||||
return arrayBuffer;
|
||||
},
|
||||
|
||||
/**
|
||||
* bytes -> hex string
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
stringify: function (bytes) {
|
||||
const hexBytes = [];
|
||||
|
||||
for (let i = 0; i < bytes.length; ++i) {
|
||||
let byteString = bytes[i].toString(16);
|
||||
if (byteString.length < 2) {
|
||||
byteString = "0" + byteString;
|
||||
}
|
||||
hexBytes.push(byteString);
|
||||
}
|
||||
return hexBytes.join("");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates between utf8 strings and Uint8Array bytes.
|
||||
*/
|
||||
const UTF8Encoder = {
|
||||
parse: function (str) {
|
||||
return new TextEncoder().encode(str);
|
||||
},
|
||||
|
||||
stringify: function (bytes) {
|
||||
return new TextDecoder().decode(bytes);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
async function encrypt(msg, hashedPassphrase) {
|
||||
// Must be 16 bytes, unpredictable, and preferably cryptographically random. However, it need not be secret.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#parameters
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_BITS / 8));
|
||||
|
||||
var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, {
|
||||
iv: iv,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
});
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassphrase),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
|
||||
// iv will be hex 16 in length (32 characters)
|
||||
// we prepend it to the ciphertext for use in decryption
|
||||
return iv.toString() + encrypted.toString();
|
||||
const encrypted = await subtle.encrypt(
|
||||
{
|
||||
name: ENCRYPTION_ALGO,
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
UTF8Encoder.parse(msg)
|
||||
);
|
||||
|
||||
// iv will be 32 hex characters, we prepend it to the ciphertext for use in decryption
|
||||
return HexEncoder.stringify(iv) + HexEncoder.stringify(new Uint8Array(encrypted));
|
||||
}
|
||||
exports.encrypt = encrypt;
|
||||
|
||||
/**
|
||||
* Decrypt a salted msg using a password.
|
||||
* Inspired by https://github.com/adonespitogo
|
||||
*
|
||||
* @param {string} encryptedMsg
|
||||
* @param {string} hashedPassphrase
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function decrypt(encryptedMsg, hashedPassphrase) {
|
||||
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, 32));
|
||||
var encrypted = encryptedMsg.substring(32);
|
||||
async function decrypt(encryptedMsg, hashedPassphrase) {
|
||||
const ivLength = IV_BITS / HEX_BITS;
|
||||
const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
|
||||
const encrypted = encryptedMsg.substring(ivLength);
|
||||
|
||||
return CryptoJS.AES.decrypt(encrypted, hashedPassphrase, {
|
||||
iv: iv,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
}).toString(CryptoJS.enc.Utf8);
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassphrase),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
|
||||
const outBuffer = await subtle.decrypt(
|
||||
{
|
||||
name: ENCRYPTION_ALGO,
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
HexEncoder.parse(encrypted)
|
||||
);
|
||||
|
||||
return UTF8Encoder.stringify(new Uint8Array(outBuffer));
|
||||
}
|
||||
exports.decrypt = decrypt;
|
||||
|
||||
|
@ -234,15 +321,15 @@ exports.decrypt = decrypt;
|
|||
*
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @returns string
|
||||
* @returns {Promise<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);
|
||||
async 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
|
||||
const hashedPassphrase = await hashLegacyRound(passphrase, salt);
|
||||
|
||||
return hashSecondRound(hashedPassphrase, salt);
|
||||
return hashSecondRound(hashedPassphrase, salt);
|
||||
}
|
||||
exports.hashPassphrase = hashPassphrase;
|
||||
|
||||
|
@ -252,13 +339,10 @@ exports.hashPassphrase = hashPassphrase;
|
|||
*
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function hashLegacyRound(passphrase, salt) {
|
||||
return CryptoJS.PBKDF2(passphrase, salt, {
|
||||
keySize: 256 / 32,
|
||||
iterations: 1000,
|
||||
}).toString();
|
||||
return pbkdf2(passphrase, salt, 1000, "SHA-1");
|
||||
}
|
||||
exports.hashLegacyRound = hashLegacyRound;
|
||||
|
||||
|
@ -268,38 +352,87 @@ exports.hashLegacyRound = hashLegacyRound;
|
|||
*
|
||||
* @param hashedPassphrase
|
||||
* @param salt
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function hashSecondRound(hashedPassphrase, salt) {
|
||||
return CryptoJS.PBKDF2(hashedPassphrase, salt, {
|
||||
keySize: 256 / 32,
|
||||
iterations: 14000,
|
||||
hasher: CryptoJS.algo.SHA256,
|
||||
}).toString();
|
||||
return pbkdf2(hashedPassphrase, salt, 14000, "SHA-256");
|
||||
}
|
||||
exports.hashSecondRound = hashSecondRound;
|
||||
|
||||
/**
|
||||
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
|
||||
*
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @param {int} iterations
|
||||
* @param {string} hashAlgorithm
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function pbkdf2(passphrase, salt, iterations, hashAlgorithm) {
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
UTF8Encoder.parse(passphrase),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
|
||||
const keyBytes = await subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
hash: hashAlgorithm,
|
||||
iterations,
|
||||
salt: UTF8Encoder.parse(salt),
|
||||
},
|
||||
key,
|
||||
256
|
||||
);
|
||||
|
||||
return HexEncoder.stringify(new Uint8Array(keyBytes));
|
||||
}
|
||||
exports.hashPassphrase = hashPassphrase;
|
||||
|
||||
function generateRandomSalt() {
|
||||
return CryptoJS.lib.WordArray.random(128 / 8).toString();
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(128 / 8));
|
||||
|
||||
return HexEncoder.stringify(new Uint8Array(bytes));
|
||||
}
|
||||
exports.generateRandomSalt = generateRandomSalt;
|
||||
|
||||
function getRandomAlphanum() {
|
||||
var possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
async function signMessage(hashedPassphrase, message) {
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassphrase),
|
||||
{
|
||||
name: "HMAC",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
const signature = await subtle.sign("HMAC", key, UTF8Encoder.parse(message));
|
||||
|
||||
var byteArray;
|
||||
var parsedInt;
|
||||
return HexEncoder.stringify(new Uint8Array(signature));
|
||||
}
|
||||
exports.signMessage = signMessage;
|
||||
|
||||
|
||||
function getRandomAlphanum() {
|
||||
const possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
let byteArray;
|
||||
let 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);
|
||||
byteArray = crypto.getRandomValues(new Uint8Array(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;
|
||||
parsedInt = byteArray[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;
|
||||
const randomIndex = parsedInt % possibleCharacters.length;
|
||||
|
||||
return possibleCharacters[randomIndex];
|
||||
}
|
||||
|
@ -311,27 +444,20 @@ function getRandomAlphanum() {
|
|||
* @returns {string}
|
||||
*/
|
||||
function generateRandomString(length) {
|
||||
var randomString = '';
|
||||
let randomString = '';
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
randomString += getRandomAlphanum();
|
||||
for (let 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;
|
||||
|
||||
return exports;
|
||||
})())
|
||||
var codec = ((function(){
|
||||
const codec = ((function(){
|
||||
const exports = {};
|
||||
/**
|
||||
* Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
|
||||
|
@ -356,15 +482,17 @@ function init(cryptoEngine) {
|
|||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
function encode(msg, password, salt, isLegacy = false) {
|
||||
async 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);
|
||||
? await cryptoEngine.hashLegacyRound(password, salt)
|
||||
: await cryptoEngine.hashPassphrase(password, salt);
|
||||
|
||||
|
||||
const encrypted = await 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);
|
||||
const hmac = await cryptoEngine.signMessage(hashedPassphrase, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
|
@ -380,10 +508,10 @@ function init(cryptoEngine) {
|
|||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
|
||||
async function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
|
||||
const decryptedHMAC = await 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
|
||||
|
@ -392,7 +520,7 @@ function init(cryptoEngine) {
|
|||
if (!backwardCompatibleHashedPassword) {
|
||||
return decode(
|
||||
signedMsg,
|
||||
cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
|
||||
await cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
|
||||
hashedPassphrase
|
||||
);
|
||||
}
|
||||
|
@ -417,7 +545,7 @@ function init(cryptoEngine) {
|
|||
|
||||
return {
|
||||
success: true,
|
||||
decoded: cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
|
||||
};
|
||||
}
|
||||
exports.decode = decode;
|
||||
|
@ -428,31 +556,31 @@ exports.init = init;
|
|||
|
||||
return exports;
|
||||
})())
|
||||
var decode = codec.init(cryptoEngine).decode;
|
||||
const decode = codec.init(cryptoEngine).decode;
|
||||
|
||||
// variables to be filled when generating the file
|
||||
var encryptedMsg = '060d759ada624841e032ea6cae0702adcbc1d451c01abe34997ed26a84ed4744d441fa6762ebd5a8c57cc5072f604a85U2FsdGVkX1+kxAZ7tF7tajh5NbDHIVUiC4CzLCa9H8BoTKgapvg9mXDGaIDkmRmk3nLDt1I+lMB8vgy/nr2E04CUTvaAJFFua9EZwMzTa6VNWELRJEOrbzESZ8P++2sZyVYUGinfB8ZbBdEZHPErPc6f7ZcksLTmCki+W4cpOEfYF9HBHjsMqu7BVvBCW5NXBajFzasDS327SrLay10VXA==',
|
||||
const encryptedMsg = '69480424a5fcc19909363e39d8434a0f706b32183e0771f2be1e746cdadfef879e9866b997f75e85a60ddfb7483e571eeba89134dea2f77a4b1123fb2825e8d32132045ea5a98b491fb29b20142643c98a2bc24e90d936f58dea6c67a2a692c9066e4d0cce5f01da8b433a5338224327edda47dadf43a6ffc45185f26f4c2438a68af464f3a69cb586fbb71cf4ef353a66d474fafa848bf736c683c986bb86b8e23dfcf6f3dbef170cacaae3591f9004f6f44b4dcc597d5696adfbb0781a4c32',
|
||||
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
|
||||
labelError = 'Bad password!',
|
||||
isRememberEnabled = true,
|
||||
rememberDurationInDays = 0; // 0 means forever
|
||||
|
||||
// constants
|
||||
var rememberPassphraseKey = 'staticrypt_passphrase',
|
||||
const rememberPassphraseKey = 'staticrypt_passphrase',
|
||||
rememberExpirationKey = 'staticrypt_expiration';
|
||||
|
||||
/**
|
||||
* Decrypt our encrypted page, replace the whole HTML.
|
||||
*
|
||||
* @param hashedPassphrase
|
||||
* @returns
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
function decryptAndReplaceHtml(hashedPassphrase) {
|
||||
var result = decode(encryptedMsg, hashedPassphrase);
|
||||
async function decryptAndReplaceHtml(hashedPassphrase) {
|
||||
const result = await decode(encryptedMsg, hashedPassphrase);
|
||||
if (!result.success) {
|
||||
return false;
|
||||
}
|
||||
var plainHTML = result.decoded;
|
||||
const plainHTML = result.decoded;
|
||||
|
||||
document.write(plainHTML);
|
||||
document.close();
|
||||
|
@ -471,9 +599,9 @@ exports.init = init;
|
|||
* To be called on load: check if we want to try to decrypt and replace the HTML with the decrypted content, and
|
||||
* try to do it if needed.
|
||||
*
|
||||
* @returns true if we derypted and replaced the whole page, false otherwise
|
||||
* @returns {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
|
||||
*/
|
||||
function decryptOnLoadFromRememberMe() {
|
||||
async function decryptOnLoadFromRememberMe() {
|
||||
if (!isRememberEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
@ -482,7 +610,7 @@ exports.init = init;
|
|||
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
|
||||
|
||||
// if we are login out, clear the storage and terminate
|
||||
var queryParams = new URLSearchParams(window.location.search);
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (queryParams.has("staticrypt_logout")) {
|
||||
clearLocalStorage();
|
||||
|
@ -491,7 +619,7 @@ exports.init = init;
|
|||
|
||||
// if there is expiration configured, check if we're not beyond the expiration
|
||||
if (rememberDurationInDays && rememberDurationInDays > 0) {
|
||||
var expiration = localStorage.getItem(rememberExpirationKey),
|
||||
const expiration = localStorage.getItem(rememberExpirationKey),
|
||||
isExpired = expiration && new Date().getTime() > parseInt(expiration);
|
||||
|
||||
if (isExpired) {
|
||||
|
@ -500,11 +628,11 @@ exports.init = init;
|
|||
}
|
||||
}
|
||||
|
||||
var hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
|
||||
const hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
|
||||
|
||||
if (hashedPassphrase) {
|
||||
// try to decrypt
|
||||
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);
|
||||
const isDecryptionSuccessful = await 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
|
||||
|
@ -520,8 +648,8 @@ exports.init = init;
|
|||
}
|
||||
|
||||
function decryptOnLoadFromQueryParam() {
|
||||
var queryParams = new URLSearchParams(window.location.search);
|
||||
var hashedPassphrase = queryParams.get("staticrypt_pwd");
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
const hashedPassphrase = queryParams.get("staticrypt_pwd");
|
||||
|
||||
if (hashedPassphrase) {
|
||||
return decryptAndReplaceHtml(hashedPassphrase);
|
||||
|
@ -531,11 +659,11 @@ exports.init = init;
|
|||
}
|
||||
|
||||
// try to automatically decrypt on load if there is a saved password
|
||||
window.onload = function () {
|
||||
var hasDecrypted = decryptOnLoadFromQueryParam();
|
||||
window.onload = async function () {
|
||||
let hasDecrypted = await decryptOnLoadFromQueryParam();
|
||||
|
||||
if (!hasDecrypted) {
|
||||
hasDecrypted = decryptOnLoadFromRememberMe();
|
||||
hasDecrypted = await decryptOnLoadFromRememberMe();
|
||||
}
|
||||
|
||||
// if we didn't decrypt anything, show the password prompt. Otherwise the content has already been replaced, no
|
||||
|
@ -548,15 +676,15 @@ exports.init = init;
|
|||
}
|
||||
|
||||
// handle password form submission
|
||||
document.getElementById('staticrypt-form').addEventListener('submit', function (e) {
|
||||
document.getElementById('staticrypt-form').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var passphrase = document.getElementById('staticrypt-password').value,
|
||||
const passphrase = document.getElementById('staticrypt-password').value,
|
||||
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
|
||||
|
||||
// decrypt and replace the whole page
|
||||
var hashedPassphrase = cryptoEngine.hashPassphrase(passphrase, salt);
|
||||
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);
|
||||
const hashedPassphrase = await cryptoEngine.hashPassphrase(passphrase, salt);
|
||||
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
|
||||
|
||||
if (isDecryptionSuccessful) {
|
||||
// remember the hashedPassphrase and set its expiration if necessary
|
||||
|
|
262
index.html
262
index.html
|
@ -200,43 +200,130 @@ Filename changed to circumvent adblockers that mistake it for a crypto miner (se
|
|||
<script id="cryptoEngine">
|
||||
window.cryptoEngine = ((function(){
|
||||
const exports = {};
|
||||
|
||||
const { subtle } = crypto;
|
||||
|
||||
const IV_BITS = 16 * 8;
|
||||
const HEX_BITS = 4;
|
||||
const ENCRYPTION_ALGO = "AES-CBC";
|
||||
|
||||
/**
|
||||
* Translates between utf8 encoded hexadecimal strings
|
||||
* and Uint8Array bytes.
|
||||
*
|
||||
* Mirrors the API of CryptoJS.enc.Hex
|
||||
*/
|
||||
const HexEncoder = {
|
||||
/**
|
||||
* hex string -> bytes
|
||||
* @param {string} hexString
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
parse: function (hexString) {
|
||||
if (hexString.length % 2 !== 0) throw "Invalid hexString";
|
||||
const arrayBuffer = new Uint8Array(hexString.length / 2);
|
||||
|
||||
for (let i = 0; i < hexString.length; i += 2) {
|
||||
const byteValue = parseInt(hexString.substring(i, i + 2), 16);
|
||||
if (isNaN(byteValue)) {
|
||||
throw "Invalid hexString";
|
||||
}
|
||||
arrayBuffer[i / 2] = byteValue;
|
||||
}
|
||||
return arrayBuffer;
|
||||
},
|
||||
|
||||
/**
|
||||
* bytes -> hex string
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
stringify: function (bytes) {
|
||||
const hexBytes = [];
|
||||
|
||||
for (let i = 0; i < bytes.length; ++i) {
|
||||
let byteString = bytes[i].toString(16);
|
||||
if (byteString.length < 2) {
|
||||
byteString = "0" + byteString;
|
||||
}
|
||||
hexBytes.push(byteString);
|
||||
}
|
||||
return hexBytes.join("");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates between utf8 strings and Uint8Array bytes.
|
||||
*/
|
||||
const UTF8Encoder = {
|
||||
parse: function (str) {
|
||||
return new TextEncoder().encode(str);
|
||||
},
|
||||
|
||||
stringify: function (bytes) {
|
||||
return new TextDecoder().decode(bytes);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
async function encrypt(msg, hashedPassphrase) {
|
||||
// Must be 16 bytes, unpredictable, and preferably cryptographically random. However, it need not be secret.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#parameters
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_BITS / 8));
|
||||
|
||||
var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, {
|
||||
iv: iv,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
});
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassphrase),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
|
||||
// iv will be hex 16 in length (32 characters)
|
||||
// we prepend it to the ciphertext for use in decryption
|
||||
return iv.toString() + encrypted.toString();
|
||||
const encrypted = await subtle.encrypt(
|
||||
{
|
||||
name: ENCRYPTION_ALGO,
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
UTF8Encoder.parse(msg)
|
||||
);
|
||||
|
||||
// iv will be 32 hex characters, we prepend it to the ciphertext for use in decryption
|
||||
return HexEncoder.stringify(iv) + HexEncoder.stringify(new Uint8Array(encrypted));
|
||||
}
|
||||
exports.encrypt = encrypt;
|
||||
|
||||
/**
|
||||
* Decrypt a salted msg using a password.
|
||||
* Inspired by https://github.com/adonespitogo
|
||||
*
|
||||
* @param {string} encryptedMsg
|
||||
* @param {string} hashedPassphrase
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function decrypt(encryptedMsg, hashedPassphrase) {
|
||||
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, 32));
|
||||
var encrypted = encryptedMsg.substring(32);
|
||||
async function decrypt(encryptedMsg, hashedPassphrase) {
|
||||
const ivLength = IV_BITS / HEX_BITS;
|
||||
const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
|
||||
const encrypted = encryptedMsg.substring(ivLength);
|
||||
|
||||
return CryptoJS.AES.decrypt(encrypted, hashedPassphrase, {
|
||||
iv: iv,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
}).toString(CryptoJS.enc.Utf8);
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassphrase),
|
||||
ENCRYPTION_ALGO,
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
|
||||
const outBuffer = await subtle.decrypt(
|
||||
{
|
||||
name: ENCRYPTION_ALGO,
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
HexEncoder.parse(encrypted)
|
||||
);
|
||||
|
||||
return UTF8Encoder.stringify(new Uint8Array(outBuffer));
|
||||
}
|
||||
exports.decrypt = decrypt;
|
||||
|
||||
|
@ -245,15 +332,15 @@ exports.decrypt = decrypt;
|
|||
*
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @returns string
|
||||
* @returns {Promise<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);
|
||||
async 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
|
||||
const hashedPassphrase = await hashLegacyRound(passphrase, salt);
|
||||
|
||||
return hashSecondRound(hashedPassphrase, salt);
|
||||
return hashSecondRound(hashedPassphrase, salt);
|
||||
}
|
||||
exports.hashPassphrase = hashPassphrase;
|
||||
|
||||
|
@ -263,13 +350,10 @@ exports.hashPassphrase = hashPassphrase;
|
|||
*
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function hashLegacyRound(passphrase, salt) {
|
||||
return CryptoJS.PBKDF2(passphrase, salt, {
|
||||
keySize: 256 / 32,
|
||||
iterations: 1000,
|
||||
}).toString();
|
||||
return pbkdf2(passphrase, salt, 1000, "SHA-1");
|
||||
}
|
||||
exports.hashLegacyRound = hashLegacyRound;
|
||||
|
||||
|
@ -279,38 +363,87 @@ exports.hashLegacyRound = hashLegacyRound;
|
|||
*
|
||||
* @param hashedPassphrase
|
||||
* @param salt
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function hashSecondRound(hashedPassphrase, salt) {
|
||||
return CryptoJS.PBKDF2(hashedPassphrase, salt, {
|
||||
keySize: 256 / 32,
|
||||
iterations: 14000,
|
||||
hasher: CryptoJS.algo.SHA256,
|
||||
}).toString();
|
||||
return pbkdf2(hashedPassphrase, salt, 14000, "SHA-256");
|
||||
}
|
||||
exports.hashSecondRound = hashSecondRound;
|
||||
|
||||
/**
|
||||
* Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
|
||||
*
|
||||
* @param {string} passphrase
|
||||
* @param {string} salt
|
||||
* @param {int} iterations
|
||||
* @param {string} hashAlgorithm
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function pbkdf2(passphrase, salt, iterations, hashAlgorithm) {
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
UTF8Encoder.parse(passphrase),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
|
||||
const keyBytes = await subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
hash: hashAlgorithm,
|
||||
iterations,
|
||||
salt: UTF8Encoder.parse(salt),
|
||||
},
|
||||
key,
|
||||
256
|
||||
);
|
||||
|
||||
return HexEncoder.stringify(new Uint8Array(keyBytes));
|
||||
}
|
||||
exports.hashPassphrase = hashPassphrase;
|
||||
|
||||
function generateRandomSalt() {
|
||||
return CryptoJS.lib.WordArray.random(128 / 8).toString();
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(128 / 8));
|
||||
|
||||
return HexEncoder.stringify(new Uint8Array(bytes));
|
||||
}
|
||||
exports.generateRandomSalt = generateRandomSalt;
|
||||
|
||||
function getRandomAlphanum() {
|
||||
var possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
async function signMessage(hashedPassphrase, message) {
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassphrase),
|
||||
{
|
||||
name: "HMAC",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
const signature = await subtle.sign("HMAC", key, UTF8Encoder.parse(message));
|
||||
|
||||
var byteArray;
|
||||
var parsedInt;
|
||||
return HexEncoder.stringify(new Uint8Array(signature));
|
||||
}
|
||||
exports.signMessage = signMessage;
|
||||
|
||||
|
||||
function getRandomAlphanum() {
|
||||
const possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
let byteArray;
|
||||
let 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);
|
||||
byteArray = crypto.getRandomValues(new Uint8Array(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;
|
||||
parsedInt = byteArray[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;
|
||||
const randomIndex = parsedInt % possibleCharacters.length;
|
||||
|
||||
return possibleCharacters[randomIndex];
|
||||
}
|
||||
|
@ -322,23 +455,16 @@ function getRandomAlphanum() {
|
|||
* @returns {string}
|
||||
*/
|
||||
function generateRandomString(length) {
|
||||
var randomString = '';
|
||||
let randomString = '';
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
randomString += getRandomAlphanum();
|
||||
for (let 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;
|
||||
|
||||
return exports;
|
||||
})())
|
||||
|
@ -370,15 +496,17 @@ function init(cryptoEngine) {
|
|||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
function encode(msg, password, salt, isLegacy = false) {
|
||||
async 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);
|
||||
? await cryptoEngine.hashLegacyRound(password, salt)
|
||||
: await cryptoEngine.hashPassphrase(password, salt);
|
||||
|
||||
|
||||
const encrypted = await 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);
|
||||
const hmac = await cryptoEngine.signMessage(hashedPassphrase, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
|
@ -394,10 +522,10 @@ function init(cryptoEngine) {
|
|||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
|
||||
async function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
|
||||
const decryptedHMAC = await 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
|
||||
|
@ -406,7 +534,7 @@ function init(cryptoEngine) {
|
|||
if (!backwardCompatibleHashedPassword) {
|
||||
return decode(
|
||||
signedMsg,
|
||||
cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
|
||||
await cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
|
||||
hashedPassphrase
|
||||
);
|
||||
}
|
||||
|
@ -431,7 +559,7 @@ function init(cryptoEngine) {
|
|||
|
||||
return {
|
||||
success: true,
|
||||
decoded: cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
|
||||
};
|
||||
}
|
||||
exports.decode = decode;
|
||||
|
|
20
lib/codec.js
20
lib/codec.js
|
@ -21,15 +21,17 @@ function init(cryptoEngine) {
|
|||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
function encode(msg, password, salt, isLegacy = false) {
|
||||
async 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);
|
||||
? await cryptoEngine.hashLegacyRound(password, salt)
|
||||
: await cryptoEngine.hashPassphrase(password, salt);
|
||||
|
||||
|
||||
const encrypted = await 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);
|
||||
const hmac = await cryptoEngine.signMessage(hashedPassphrase, encrypted);
|
||||
|
||||
return hmac + encrypted;
|
||||
}
|
||||
|
@ -45,10 +47,10 @@ function init(cryptoEngine) {
|
|||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
|
||||
*/
|
||||
function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
|
||||
async function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
|
||||
const encryptedHMAC = signedMsg.substring(0, 64);
|
||||
const encryptedMsg = signedMsg.substring(64);
|
||||
const decryptedHMAC = cryptoEngine.signMessage(hashedPassphrase, encryptedMsg);
|
||||
const decryptedHMAC = await 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
|
||||
|
@ -57,7 +59,7 @@ function init(cryptoEngine) {
|
|||
if (!backwardCompatibleHashedPassword) {
|
||||
return decode(
|
||||
signedMsg,
|
||||
cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
|
||||
await cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
|
||||
hashedPassphrase
|
||||
);
|
||||
}
|
||||
|
@ -82,7 +84,7 @@ function init(cryptoEngine) {
|
|||
|
||||
return {
|
||||
success: true,
|
||||
decoded: cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
|
||||
};
|
||||
}
|
||||
exports.decode = decode;
|
||||
|
|
|
@ -2,7 +2,6 @@ const CryptoJS = require("crypto-js");
|
|||
|
||||
/**
|
||||
* 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);
|
||||
|
@ -21,7 +20,6 @@ exports.encrypt = encrypt;
|
|||
|
||||
/**
|
||||
* Decrypt a salted msg using a password.
|
||||
* Inspired by https://github.com/adonespitogo
|
||||
*
|
||||
* @param {string} encryptedMsg
|
||||
* @param {string} hashedPassphrase
|
||||
|
|
|
@ -187,33 +187,33 @@
|
|||
{crypto_tag}
|
||||
|
||||
<script>
|
||||
var cryptoEngine = {js_crypto_engine}
|
||||
var codec = {js_codec}
|
||||
var decode = codec.init(cryptoEngine).decode;
|
||||
const cryptoEngine = {js_crypto_engine}
|
||||
const codec = {js_codec}
|
||||
const decode = codec.init(cryptoEngine).decode;
|
||||
|
||||
// variables to be filled when generating the file
|
||||
var encryptedMsg = '{encrypted}',
|
||||
const encryptedMsg = '{encrypted}',
|
||||
salt = '{salt}',
|
||||
labelError = '{label_error}',
|
||||
isRememberEnabled = {is_remember_enabled},
|
||||
rememberDurationInDays = {remember_duration_in_days}; // 0 means forever
|
||||
|
||||
// constants
|
||||
var rememberPassphraseKey = 'staticrypt_passphrase',
|
||||
const rememberPassphraseKey = 'staticrypt_passphrase',
|
||||
rememberExpirationKey = 'staticrypt_expiration';
|
||||
|
||||
/**
|
||||
* Decrypt our encrypted page, replace the whole HTML.
|
||||
*
|
||||
* @param {string} hashedPassphrase
|
||||
* @returns {boolean}
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
function decryptAndReplaceHtml(hashedPassphrase) {
|
||||
var result = decode(encryptedMsg, hashedPassphrase);
|
||||
async function decryptAndReplaceHtml(hashedPassphrase) {
|
||||
const result = await decode(encryptedMsg, hashedPassphrase);
|
||||
if (!result.success) {
|
||||
return false;
|
||||
}
|
||||
var plainHTML = result.decoded;
|
||||
const plainHTML = result.decoded;
|
||||
|
||||
document.write(plainHTML);
|
||||
document.close();
|
||||
|
@ -232,9 +232,9 @@
|
|||
* To be called on load: check if we want to try to decrypt and replace the HTML with the decrypted content, and
|
||||
* try to do it if needed.
|
||||
*
|
||||
* @returns {boolean} true if we derypted and replaced the whole page, false otherwise
|
||||
* @returns {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
|
||||
*/
|
||||
function decryptOnLoadFromRememberMe() {
|
||||
async function decryptOnLoadFromRememberMe() {
|
||||
if (!isRememberEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
@ -243,7 +243,7 @@
|
|||
document.getElementById('staticrypt-remember-label').classList.remove('hidden');
|
||||
|
||||
// if we are login out, clear the storage and terminate
|
||||
var queryParams = new URLSearchParams(window.location.search);
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (queryParams.has("staticrypt_logout")) {
|
||||
clearLocalStorage();
|
||||
|
@ -252,7 +252,7 @@
|
|||
|
||||
// if there is expiration configured, check if we're not beyond the expiration
|
||||
if (rememberDurationInDays && rememberDurationInDays > 0) {
|
||||
var expiration = localStorage.getItem(rememberExpirationKey),
|
||||
const expiration = localStorage.getItem(rememberExpirationKey),
|
||||
isExpired = expiration && new Date().getTime() > parseInt(expiration);
|
||||
|
||||
if (isExpired) {
|
||||
|
@ -261,11 +261,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
var hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
|
||||
const hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
|
||||
|
||||
if (hashedPassphrase) {
|
||||
// try to decrypt
|
||||
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);
|
||||
const isDecryptionSuccessful = await 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
|
||||
|
@ -281,8 +281,8 @@
|
|||
}
|
||||
|
||||
function decryptOnLoadFromQueryParam() {
|
||||
var queryParams = new URLSearchParams(window.location.search);
|
||||
var hashedPassphrase = queryParams.get("staticrypt_pwd");
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
const hashedPassphrase = queryParams.get("staticrypt_pwd");
|
||||
|
||||
if (hashedPassphrase) {
|
||||
return decryptAndReplaceHtml(hashedPassphrase);
|
||||
|
@ -292,11 +292,11 @@
|
|||
}
|
||||
|
||||
// try to automatically decrypt on load if there is a saved password
|
||||
window.onload = function () {
|
||||
var hasDecrypted = decryptOnLoadFromQueryParam();
|
||||
window.onload = async function () {
|
||||
let hasDecrypted = await decryptOnLoadFromQueryParam();
|
||||
|
||||
if (!hasDecrypted) {
|
||||
hasDecrypted = decryptOnLoadFromRememberMe();
|
||||
hasDecrypted = await decryptOnLoadFromRememberMe();
|
||||
}
|
||||
|
||||
// if we didn't decrypt anything, show the password prompt. Otherwise the content has already been replaced, no
|
||||
|
@ -309,15 +309,15 @@
|
|||
}
|
||||
|
||||
// handle password form submission
|
||||
document.getElementById('staticrypt-form').addEventListener('submit', function (e) {
|
||||
document.getElementById('staticrypt-form').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var passphrase = document.getElementById('staticrypt-password').value,
|
||||
const passphrase = document.getElementById('staticrypt-password').value,
|
||||
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
|
||||
|
||||
// decrypt and replace the whole page
|
||||
var hashedPassphrase = cryptoEngine.hashPassphrase(passphrase, salt);
|
||||
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);
|
||||
const hashedPassphrase = await cryptoEngine.hashPassphrase(passphrase, salt);
|
||||
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
|
||||
|
||||
if (isDecryptionSuccessful) {
|
||||
// remember the hashedPassphrase and set its expiration if necessary
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
# encrypt the example file
|
||||
npx . example/example.html test \
|
||||
--no-embed \
|
||||
--engine webcrypto \
|
||||
--short \
|
||||
--salt b93bbaf35459951c47721d1f3eaeb5b9 \
|
||||
--instructions "Enter \"test\" to unlock the page"
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ const { convertCommonJSToBrowserJS, genFile } = require("../cli/helpers.js");
|
|||
|
||||
const data = {
|
||||
js_codec: convertCommonJSToBrowserJS("lib/codec"),
|
||||
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine/cryptojsEngine"),
|
||||
js_crypto_engine: convertCommonJSToBrowserJS("lib/cryptoEngine/webcryptoEngine"),
|
||||
js_formater: convertCommonJSToBrowserJS("lib/formater"),
|
||||
};
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue