Jim Harkins 2025-07-20 14:37:31 -07:00 zatwierdzone przez GitHub
commit c3d7e92aa8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
6 zmienionych plików z 380 dodań i 178 usunięć

Wyświetl plik

@ -2,7 +2,7 @@ const pathModule = require("path");
const fs = require("fs");
const readline = require("readline");
const { generateRandomSalt, generateRandomString } = require("../lib/cryptoEngine.js");
const { generateRandomSaltString, generateRandomString } = require("../lib/cryptoEngine.js");
const { renderTemplate } = require("../lib/formater.js");
const Yargs = require("yargs");
@ -74,14 +74,14 @@ function prompt(question) {
}
/**
* @param {string} password
* @param {string} passwordString
* @param {boolean} isShortAllowed
* @returns {Promise<void>}
*/
async function validatePassword(password, isShortAllowed) {
if (password.length < 14 && !isShortAllowed) {
async function validatePasswordString(passwordString, isShortAllowed) {
if (passwordString.length < 14 && !isShortAllowed) {
const shouldUseShort = await prompt(
`WARNING: Your password is less than 14 characters (length: ${password.length})` +
`WARNING: Your password is less than 14 characters (length: ${passwordString.length})` +
" 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." +
@ -94,7 +94,7 @@ async function validatePassword(password, isShortAllowed) {
}
}
}
exports.validatePassword = validatePassword;
exports.validatePasswordString = validatePasswordString;
/**
* Get the config from the config file.
@ -124,7 +124,7 @@ exports.writeConfig = writeConfig;
* @param {string} passwordArgument - password from the command line
* @returns {Promise<string>}
*/
async function getPassword(passwordArgument) {
async function getPasswordString(passwordArgument) {
// try to get the password from the environment variable
const envPassword = process.env.STATICRYPT_PASSWORD;
const hasEnvPassword = envPassword !== undefined && envPassword !== "";
@ -140,7 +140,7 @@ async function getPassword(passwordArgument) {
// prompt the user for their password
return prompt("Enter your long, unusual password: ");
}
exports.getPassword = getPassword;
exports.getPasswordString = getPasswordString;
/**
* @param {string} filepath
@ -155,13 +155,26 @@ function getFileContent(filepath) {
}
exports.getFileContent = getFileContent;
/**
* @param {string} filepath
* @returns {Uint8Array}
*/
function getFileContentBytes(filepath) {
try {
return new Uint8Array(fs.readFileSync(filepath));
} catch (e) {
exitWithError(`input file '${filepath}' does not exist!`);
}
}
exports.getFileContentBytes = getFileContentBytes;
/**
* @param {object} namedArgs
* @param {object} config
* @returns {string}
*/
function getValidatedSalt(namedArgs, config) {
const salt = getSalt(namedArgs, config);
function getValidatedSaltString(namedArgs, config) {
const salt = getSaltString(namedArgs, config);
// validate the salt
if (salt.length !== 32 || /[^a-f0-9]/.test(salt)) {
@ -174,16 +187,16 @@ function getValidatedSalt(namedArgs, config) {
return salt;
}
exports.getValidatedSalt = getValidatedSalt;
exports.getValidatedSaltString = getValidatedSaltString;
/**
* @param {object} namedArgs
* @param {object} config
* @returns {string}
*/
function getSalt(namedArgs, config) {
function getSaltString(namedArgs, config) {
// either a salt was provided by the user through the flag --salt
if (!!namedArgs.salt) {
if (namedArgs.salt) {
return String(namedArgs.salt).toLowerCase();
}
@ -192,7 +205,7 @@ function getSalt(namedArgs, config) {
return config.salt;
}
return generateRandomSalt();
return generateRandomSaltString();
}
/**

Wyświetl plik

@ -17,7 +17,7 @@ const fs = require("fs");
const cryptoEngine = require("../lib/cryptoEngine.js");
const codec = require("../lib/codec.js");
const { generateRandomSalt } = cryptoEngine;
const { generateRandomSaltString } = cryptoEngine;
const { decode, encodeWithHashedPassword } = codec.init(cryptoEngine);
const {
OUTPUT_DIRECTORY_DEFAULT_PATH,
@ -26,12 +26,13 @@ const {
genFile,
getConfig,
getFileContent,
getPassword,
getValidatedSalt,
getFileContentBytes,
getPasswordString,
getValidatedSaltString,
isOptionSetByUser,
parseCommandLineArguments,
recursivelyApplyCallbackToHtmlFiles,
validatePassword,
validatePasswordString,
writeConfig,
writeFile,
getFullOutputPath,
@ -63,14 +64,14 @@ async function runStatiCrypt() {
// if the 's' flag is passed without parameter, generate a salt, display & exit
if (hasSaltFlag && !namedArgs.salt) {
const generatedSalt = generateRandomSalt();
const generatedSaltString = generateRandomSaltString();
// show salt
console.log(generatedSalt);
console.log(generatedSaltstring);
// write to config file if it doesn't exist
if (!config.salt) {
config.salt = generatedSalt;
config.salt = generatedSaltstring;
writeConfig(configPath, config);
}
@ -78,16 +79,21 @@ async function runStatiCrypt() {
}
// get the salt & password
const salt = getValidatedSalt(namedArgs, config);
const password = await getPassword(namedArgs.password);
const saltString = getValidatedSaltString(namedArgs, config);
const salt = cryptoEngine.HexEncoder.parse(saltString);
const passwordString = await getPasswordString(namedArgs.password);
const password = cryptoEngine.UTF8Encoder.parse(passwordString);
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
const hashedPasswordString = cryptoEngine.HexEncoder.stringify(hashedPassword);
// display the share link with the hashed password if the --share flag is set
if (hasShareFlag) {
await validatePassword(password, namedArgs.short);
await validatePasswordString(passwordString, namedArgs.short);
let url = namedArgs.share || "";
url += "#staticrypt_pwd=" + hashedPassword;
url += "#staticrypt_pwd=" + hashedPasswordString;
if (namedArgs.shareRemember) {
url += `&remember_me`;
@ -124,11 +130,11 @@ async function runStatiCrypt() {
return;
}
await validatePassword(password, namedArgs.short);
await validatePasswordString(passwordString, namedArgs.short);
// write salt to config file
if (config.salt !== salt) {
config.salt = salt;
if (config.salt !== saltString) {
config.salt = saltString;
writeConfig(configPath, config);
}
@ -157,7 +163,7 @@ async function runStatiCrypt() {
fullPath,
fullRootDirectory,
hashedPassword,
salt,
saltString,
baseTemplateData,
isRememberEnabled,
namedArgs
@ -174,15 +180,24 @@ async function decodeAndGenerateFile(path, fullRootDirectory, hashedPassword, ou
const encryptedFileContent = getFileContent(path);
// extract the cipher text from the encrypted file
const cipherTextMatch = encryptedFileContent.match(/"staticryptEncryptedMsgUniqueVariableName":\s*"([^"]+)"/);
let encryptedMatch = encryptedFileContent.match(/"staticryptEncryptedUniqueVariableName":\s*"([^"]+)"/);
const ivMatch = encryptedFileContent.match(/"staticryptIvUniqueVariableName":\s*"([^"]+)"/);
const hmacMatch = encryptedFileContent.match(/"staticryptHmacUniqueVariableName":\s*"([^"]+)"/);
const saltMatch = encryptedFileContent.match(/"staticryptSaltUniqueVariableName":\s*"([^"]+)"/);
if (!cipherTextMatch || !saltMatch) {
return console.log(`ERROR: could not extract cipher text or salt from ${path}`);
if (!encryptedMatch || !ivMatch || !hmacMatch || !saltMatch) {
return console.log(`ERROR: could not extract cipher text, iv, hmac, or salt from ${path}`);
}
const encrypted = cryptoEngine.HexEncoder.parse(encryptedMatch[1]);
encryptedMatch = null;
const iv = cryptoEngine.HexEncoder.parse(ivMatch[1]);
const hmac = cryptoEngine.HexEncoder.parse(hmacMatch[1]);
const salt = cryptoEngine.HexEncoder.parse(saltMatch[1]);
// decrypt input
const { success, decoded } = await decode(cipherTextMatch[1], hashedPassword, saltMatch[1]);
const { success, decoded } = await decode(iv, encrypted, hmac, hashedPassword, salt);
if (!success) {
return console.log(`ERROR: could not decrypt ${path}`);
@ -197,25 +212,25 @@ async function encodeAndGenerateFile(
path,
rootDirectoryFromArguments,
hashedPassword,
salt,
saltString,
baseTemplateData,
isRememberEnabled,
namedArgs
) {
// get the file content
const contents = getFileContent(path);
// encrypt input
const encryptedMsg = await encodeWithHashedPassword(contents, hashedPassword);
const encryptedMsg = await encodeWithHashedPassword(getFileContentBytes(path), hashedPassword);
let rememberDurationInDays = parseInt(namedArgs.remember);
rememberDurationInDays = isNaN(rememberDurationInDays) ? 0 : rememberDurationInDays;
const staticryptConfig = {
staticryptEncryptedMsgUniqueVariableName: encryptedMsg,
staticryptIvUniqueVariableName: cryptoEngine.HexEncoder.stringify(encryptedMsg.iv),
staticryptEncryptedUniqueVariableName: cryptoEngine.HexEncoder.stringify(encryptedMsg.encrypted),
staticryptHmacUniqueVariableName: cryptoEngine.HexEncoder.stringify(encryptedMsg.hmac),
isRememberEnabled,
rememberDurationInDays,
staticryptSaltUniqueVariableName: salt,
staticryptSaltUniqueVariableName: saltString,
};
const templateData = {
...baseTemplateData,

Wyświetl plik

@ -6,26 +6,52 @@
function init(cryptoEngine) {
const exports = {};
/**
* Implement digest signing:
*
* hmac = sign( hashedPassword, iv + digest(encrypted) )
*
* To avoid having to make copy of encrypted.
*
* @param {Uint8Array} iv
* @param {Uint8Array|null} encrypted
* @param {Uint8Array} hashedPassword
* @param {Uint8Array|null} encryptedDataHash
*
* @returns {Promise<Uint8Array>} The calculated hmac
*/
async function signDigest(iv, encrypted, hashedPassword, encryptedDataHash = null) {
// we use a hash of the encrypted bytes as a proxy for the actual bytes
// when generating the HMAC to avoid making a copy of the encrypted bytes
if (!encryptedDataHash) {
encryptedDataHash = await cryptoEngine.digestMessage(encrypted);
}
const messageBuffer = new Uint8Array(iv.length + encryptedDataHash.length);
messageBuffer.set(iv);
messageBuffer.set(encryptedDataHash, iv.length);
const hmac = await cryptoEngine.signMessage(hashedPassword, messageBuffer);
return hmac;
}
/**
* Top-level function for encoding a message.
* Includes password hashing, encryption, and signing.
*
* @param {string} msg
* @param {string} password
* @param {string} salt
* @param {Uint8Array} msg
* @param {Uint8Array} password
* @param {Uint8Array} salt
*
* @returns {string} The encoded text
* @returns {Promise<Uint8Array>} The encoded text
*/
async function encode(msg, password, salt) {
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
const authEncryptionData = await encodeWithHashedPassword(msg, hashedPassword);
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
// 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 = await cryptoEngine.signMessage(hashedPassword, encrypted);
return hmac + encrypted;
return authEncryptionData;
}
exports.encode = encode;
@ -33,19 +59,23 @@ function init(cryptoEngine) {
* Encode using a password that has already been hashed. This is useful to encode multiple messages in a row, that way
* we don't need to hash the password multiple times.
*
* @param {string} msg
* @param {string} hashedPassword
* @param {Uint8Array} msg
* @param {Uint8Array} hashedPassword
*
* @returns {string} The encoded text
* @returns { Promise<{iv: Uint8Array, encrypted: Uint8Array, hmac: Uint8Array}> }
*/
async function encodeWithHashedPassword(msg, hashedPassword) {
const encrypted = await cryptoEngine.encrypt(msg, hashedPassword);
const encryptionData = await cryptoEngine.encrypt(msg, hashedPassword);
// 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 = await cryptoEngine.signMessage(hashedPassword, encrypted);
return hmac + encrypted;
const hmac = await signDigest(encryptionData.iv, encryptionData.encrypted, hashedPassword);
return {
...encryptionData,
hmac: hmac,
};
}
exports.encodeWithHashedPassword = encodeWithHashedPassword;
@ -53,33 +83,60 @@ function init(cryptoEngine) {
* Top-level function for decoding a message.
* Includes signature check and decryption.
*
* @param {string} signedMsg
* @param {string} hashedPassword
* @param {string} salt
* @param {Uint8Array} iv
* @param {Uint8Array} encrypted
* @param {Uint8Array} hmac
* @param {Uint8Array} hashedPassword
* @param {Uint8Array} salt
* @param {int} backwardCompatibleAttempt
* @param {string} originalPassword
* @param {Uint8Array} originalPassword
*
* @returns {Object} {success: true, decoded: string} | {success: false, message: string}
* @returns { Promise<{success: true, decoded: Uint8Array} | {success: false, message: string}> }
*/
async function decode(signedMsg, hashedPassword, salt, backwardCompatibleAttempt = 0, originalPassword = "") {
const encryptedHMAC = signedMsg.substring(0, 64);
const encryptedMsg = signedMsg.substring(64);
const decryptedHMAC = await cryptoEngine.signMessage(hashedPassword, encryptedMsg);
async function decode(
iv,
encrypted,
hmac,
hashedPassword,
salt,
backwardCompatibleAttempt = 0,
originalPassword = null
) {
const encryptedDataHash = await cryptoEngine.digestMessage(encrypted);
if (decryptedHMAC !== encryptedHMAC) {
const calculatedHMAC = await signDigest(iv, null, hashedPassword, encryptedDataHash);
if (!cryptoEngine.isArrayEqual(calculatedHMAC, hmac)) {
// we have been raising the number of iterations in the hashing algorithm multiple times, so to support the old
// remember-me/autodecrypt links we need to try bringing the old hashes up to speed.
originalPassword = originalPassword || hashedPassword;
if (backwardCompatibleAttempt === 0) {
const updatedHashedPassword = await cryptoEngine.hashThirdRound(originalPassword, salt);
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
return decode(
iv,
encrypted,
hmac,
updatedHashedPassword,
salt,
backwardCompatibleAttempt + 1,
originalPassword
);
}
if (backwardCompatibleAttempt === 1) {
let updatedHashedPassword = await cryptoEngine.hashSecondRound(originalPassword, salt);
updatedHashedPassword = await cryptoEngine.hashThirdRound(updatedHashedPassword, salt);
return decode(signedMsg, updatedHashedPassword, salt, backwardCompatibleAttempt + 1, originalPassword);
return decode(
iv,
encrypted,
hmac,
updatedHashedPassword,
salt,
backwardCompatibleAttempt + 1,
originalPassword
);
}
return { success: false, message: "Signature mismatch" };
@ -87,7 +144,7 @@ function init(cryptoEngine) {
return {
success: true,
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassword),
decoded: await cryptoEngine.decrypt(iv, encrypted, hashedPassword),
};
}
exports.decode = decode;

Wyświetl plik

@ -1,21 +1,74 @@
const crypto = typeof window === "undefined" ? require("node:crypto").webcrypto : window.crypto;
const isNode = typeof window === "undefined";
const crypto = isNode ? require("node:crypto").webcrypto : window.crypto;
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.
* Compare 2 arrays and return true if they are equal.
*
* @param {Uint8Array} a
* @param {Uint8Array} b
* @returns {boolean}
*/
const HexEncoder = {
function isArrayEqual(a, b) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
exports.isArrayEqual = isArrayEqual;
/**
* Translates between utf8 encoded hexadecimal strings
* and Uint8Array.
*/
const HexEncoder = isNode
? {
// Node version
/**
* hex string -> bytes
* hex string -> Uint8Array
* @param {string|null} hexString
* @returns {Uint8Array|null}
*/
parse: function (hexString) {
const bytes = Buffer.from(hexString, "hex");
return bytes;
},
/**
* Uint8Array -> hex string
* @param {Uint8Array|null} bytes
* @returns {string|null}
*/
stringify: function (bytes) {
const buffer = Buffer.from(bytes);
const hexString = buffer.toString("hex");
return hexString;
},
}
: {
// Browser version
/**
* hex string -> Uint8Array
* @param {string} hexString
* @returns {Uint8Array}
*/
parse: function (hexString) {
if (!hexString) {
return null;
}
if (hexString.length % 2 !== 0) throw "Invalid hexString";
const arrayBuffer = new Uint8Array(hexString.length / 2);
@ -30,11 +83,14 @@ const HexEncoder = {
},
/**
* bytes -> hex string
* Uint8Array -> hex string
* @param {Uint8Array} bytes
* @returns {string}
*/
stringify: function (bytes) {
if (!bytes) {
return null;
}
const hexBytes = [];
for (let i = 0; i < bytes.length; ++i) {
@ -46,30 +102,54 @@ const HexEncoder = {
}
return hexBytes.join("");
},
};
};
exports.HexEncoder = HexEncoder;
/**
* Translates between utf8 strings and Uint8Array bytes.
* Translates between utf8 string and Uint8Array.
*/
const UTF8Encoder = {
/**
* string -> Uint8Array
* @param {string|null} str
* @returns {Uint8Array|null}
*/
parse: function (str) {
if (!str) {
return null;
}
return new TextEncoder().encode(str);
},
/**
* Uint8Array -> string
* @param {Uint8Array|null} bytes
* @returns {string|null}
*/
stringify: function (bytes) {
if (!bytes) {
return null;
}
return new TextDecoder().decode(bytes);
},
};
exports.UTF8Encoder = UTF8Encoder;
/**
* Salt and encrypt a msg with a password.
*
* @param {Uint8Array} msg
* @param {Uint8Array} hashedPassword
*
* @returns { Promise<{iv: Uint8Array, encrypted: Uint8Array}> }
*/
async function encrypt(msg, hashedPassword) {
// 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));
const key = await subtle.importKey("raw", HexEncoder.parse(hashedPassword), ENCRYPTION_ALGO, false, ["encrypt"]);
const key = await subtle.importKey("raw", hashedPassword, ENCRYPTION_ALGO, false, ["encrypt"]);
const encrypted = await subtle.encrypt(
{
@ -77,47 +157,47 @@ async function encrypt(msg, hashedPassword) {
iv: iv,
},
key,
UTF8Encoder.parse(msg)
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));
// return iv with the ciphertext for use in decryption
return {
iv: iv,
encrypted: new Uint8Array(encrypted),
};
}
exports.encrypt = encrypt;
/**
* Decrypt a salted msg using a password.
*
* @param {string} encryptedMsg
* @param {string} hashedPassword
* @returns {Promise<string>}
* @param {Uint8Array} iv
* @param {Uint8Array} encrypted
* @param {Uint8Array} hashedPassword
* @returns { Promise<Uint8Array> }
*/
async function decrypt(encryptedMsg, hashedPassword) {
const ivLength = IV_BITS / HEX_BITS;
const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
const encrypted = encryptedMsg.substring(ivLength);
async function decrypt(iv, encrypted, hashedPassword) {
const key = await subtle.importKey("raw", hashedPassword, ENCRYPTION_ALGO, false, ["decrypt"]);
const key = await subtle.importKey("raw", HexEncoder.parse(hashedPassword), ENCRYPTION_ALGO, false, ["decrypt"]);
const outBuffer = await subtle.decrypt(
const decryptedBuffer = await subtle.decrypt(
{
name: ENCRYPTION_ALGO,
iv: iv,
},
key,
HexEncoder.parse(encrypted)
encrypted
);
return UTF8Encoder.stringify(new Uint8Array(outBuffer));
return new Uint8Array(decryptedBuffer);
}
exports.decrypt = decrypt;
/**
* Salt and hash the password so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} password
* @param {string} salt
* @returns {Promise<string>}
* @param {Uint8Array} password
* @param {Uint8Array} salt
* @returns { Promise<Uint8Array> }
*/
async function hashPassword(password, salt) {
// we hash the password in multiple steps, each adding more iterations. This is because we used to allow less
@ -134,12 +214,12 @@ exports.hashPassword = hashPassword;
* This hashes the password with 1k iterations. This is a low number, we need this function to support backwards
* compatibility.
*
* @param {string} password
* @param {string} salt
* @returns {Promise<string>}
* @param {Uint8Array} password
* @param {Uint8Array} salt
* @returns { Promise<Uint8Array> }
*/
function hashLegacyRound(password, salt) {
return pbkdf2(password, salt, 1000, "SHA-1");
async function hashLegacyRound(password, salt) {
return await pbkdf2(password, salt, 1000, "SHA-1");
}
exports.hashLegacyRound = hashLegacyRound;
@ -147,12 +227,12 @@ 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 hashedPassword
* @param salt
* @returns {Promise<string>}
* @param {Uint8Array} hashedPassword
* @param {Uint8Array} salt
* @returns { Promise<Uint8Array> }
*/
function hashSecondRound(hashedPassword, salt) {
return pbkdf2(hashedPassword, salt, 14000, "SHA-256");
async function hashSecondRound(hashedPassword, salt) {
return await pbkdf2(hashedPassword, salt, 14000, "SHA-256");
}
exports.hashSecondRound = hashSecondRound;
@ -160,52 +240,52 @@ exports.hashSecondRound = hashSecondRound;
* Add a third round of iterations to bring total number to 600k. This is because we used to use 1k, then 15k, so for
* backwards compatibility with remember-me/autodecrypt links, we need to support going from that to more iterations.
*
* @param hashedPassword
* @param salt
* @returns {Promise<string>}
* @param {Uint8Array} hashedPassword
* @param {Uint8Array} salt
* @returns { Promise<Uint8Array> }
*/
function hashThirdRound(hashedPassword, salt) {
return pbkdf2(hashedPassword, salt, 585000, "SHA-256");
async function hashThirdRound(hashedPassword, salt) {
return await pbkdf2(hashedPassword, salt, 585000, "SHA-256");
}
exports.hashThirdRound = hashThirdRound;
/**
* Salt and hash the password so it can be stored in localStorage without opening a password reuse vulnerability.
*
* @param {string} password
* @param {string} salt
* @param {Uint8Array} password
* @param {Uint8Array} salt
* @param {int} iterations
* @param {string} hashAlgorithm
* @returns {Promise<string>}
* @returns { Promise<Uint8Array> }
*/
async function pbkdf2(password, salt, iterations, hashAlgorithm) {
const key = await subtle.importKey("raw", UTF8Encoder.parse(password), "PBKDF2", false, ["deriveBits"]);
const key = await subtle.importKey("raw", password, "PBKDF2", false, ["deriveBits"]);
const keyBytes = await subtle.deriveBits(
const derivedKey = await subtle.deriveBits(
{
name: "PBKDF2",
hash: hashAlgorithm,
iterations,
salt: UTF8Encoder.parse(salt),
salt: salt,
},
key,
256
);
return HexEncoder.stringify(new Uint8Array(keyBytes));
return new Uint8Array(derivedKey);
}
function generateRandomSalt() {
function generateRandomSaltString() {
const bytes = crypto.getRandomValues(new Uint8Array(128 / 8));
return HexEncoder.stringify(new Uint8Array(bytes));
}
exports.generateRandomSalt = generateRandomSalt;
exports.generateRandomSaltString = generateRandomSaltString;
async function signMessage(hashedPassword, message) {
const key = await subtle.importKey(
"raw",
HexEncoder.parse(hashedPassword),
hashedPassword,
{
name: "HMAC",
hash: "SHA-256",
@ -213,12 +293,19 @@ async function signMessage(hashedPassword, message) {
false,
["sign"]
);
const signature = await subtle.sign("HMAC", key, UTF8Encoder.parse(message));
const signature = await subtle.sign("HMAC", key, message);
return HexEncoder.stringify(new Uint8Array(signature));
return new Uint8Array(signature);
}
exports.signMessage = signMessage;
async function digestMessage(message) {
const digest = await subtle.digest("SHA-256", message);
const digestBytes = new Uint8Array(digest);
return digestBytes;
}
exports.digestMessage = digestMessage;
function getRandomAlphanum() {
const possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@ -227,6 +314,7 @@ function getRandomAlphanum() {
// Keep generating new random bytes until we get a value that falls
// within a range that can be evenly divided by possibleCharacters.length
// to ensure each character is selected without bias
do {
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)

Wyświetl plik

@ -4,9 +4,10 @@ const decode = codec.init(cryptoEngine).decode;
/**
* Initialize the staticrypt module, that exposes functions callbable by the password_template.
*
* @param {{
* staticryptEncryptedMsgUniqueVariableName: string,
* staticryptIvUniqueVariableName: string,
* staticryptEncryptedUniqueVariableName: string,
* staticryptHmacUniqueVariableName: string,
* isRememberEnabled: boolean,
* rememberDurationInDays: number,
* staticryptSaltUniqueVariableName: string,
@ -25,22 +26,33 @@ function init(staticryptConfig, templateConfig) {
/**
* Decrypt our encrypted page, replace the whole HTML.
*
* @param {string} hashedPassword
* @param {Uint8Array} hashedPassword
* @returns {Promise<boolean>}
*/
async function decryptAndReplaceHtml(hashedPassword) {
const { staticryptEncryptedMsgUniqueVariableName, staticryptSaltUniqueVariableName } = staticryptConfig;
const iv = cryptoEngine.HexEncoder.parse(staticryptConfig.staticryptIvUniqueVariableName);
const hmac = cryptoEngine.HexEncoder.parse(staticryptConfig.staticryptHmacUniqueVariableName);
const salt = cryptoEngine.HexEncoder.parse(staticryptConfig.staticryptSaltUniqueVariableName);
const { replaceHtmlCallback } = templateConfig;
const result = await decode(
staticryptEncryptedMsgUniqueVariableName,
// Encrypted may be very large, we avoid creating a named variable
// for the Uint8Array so it will immediately be eligible for garbage
// collection.
let result = await decode(
iv,
cryptoEngine.HexEncoder.parse(staticryptConfig.staticryptEncryptedUniqueVariableName),
hmac,
hashedPassword,
staticryptSaltUniqueVariableName
salt
);
if (!result.success) {
return false;
}
const plainHTML = result.decoded;
const plainHTML = cryptoEngine.UTF8Encoder.stringify(result.decoded);
// no further use for result and result.decoded can be vary large
result = null;
// if the user configured a callback call it, otherwise just replace the whole HTML
if (typeof replaceHtmlCallback === "function") {
@ -59,14 +71,15 @@ function init(staticryptConfig, templateConfig) {
* @param {string} password
* @param {boolean} isRememberChecked
*
* @returns {Promise<{isSuccessful: boolean, hashedPassword?: string}>} - we return an object, so that if we want to
* @returns {Promise<{isSuccessful: boolean, hashedPassword?: Uint8Array}>} - we return an object, so that if we want to
* expose more information in the future we can do it without breaking the password_template
*/
async function handleDecryptionOfPage(password, isRememberChecked) {
const { staticryptSaltUniqueVariableName } = staticryptConfig;
async function handleDecryptionOfPage(passwordString, isRememberChecked) {
const password = cryptoEngine.UTF8Encoder.parse(passwordString);
const salt = cryptoEngine.HexEncoder.parse(staticryptConfig.staticryptSaltUniqueVariableName);
// decrypt and replace the whole page
const hashedPassword = await cryptoEngine.hashPassword(password, staticryptSaltUniqueVariableName);
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
return handleDecryptionOfPageFromHash(hashedPassword, isRememberChecked);
}
exports.handleDecryptionOfPage = handleDecryptionOfPage;
@ -86,7 +99,8 @@ function init(staticryptConfig, templateConfig) {
// remember the hashedPassword and set its expiration if necessary
if (isRememberEnabled && isRememberChecked) {
window.localStorage.setItem(rememberPassphraseKey, hashedPassword);
const hashedPasswordString = cryptoEngine.HexEncoder.stringify(hashedPassword);
window.localStorage.setItem(rememberPassphraseKey, hashedPasswordString);
// set the expiration if the duration isn't 0 (meaning no expiration)
if (rememberDurationInDays > 0) {
@ -164,7 +178,7 @@ function init(staticryptConfig, templateConfig) {
const { rememberDurationInDays } = staticryptConfig;
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
// if we are login out, terminate
// if we are loging out, terminate
if (logoutIfNeeded()) {
return false;
}
@ -180,9 +194,11 @@ function init(staticryptConfig, templateConfig) {
}
}
const hashedPassword = localStorage.getItem(rememberPassphraseKey);
const hashedPasswordString = localStorage.getItem(rememberPassphraseKey);
if (hashedPasswordString) {
const hashedPassword = cryptoEngine.HexEncoder.parse(hashedPasswordString);
if (hashedPassword) {
// try to decrypt
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassword);
@ -215,7 +231,8 @@ function init(staticryptConfig, templateConfig) {
const hashedPasswordFragment = hashedPasswordRegexMatch ? hashedPasswordRegexMatch[1] : null;
const rememberMeFragment = urlFragment.includes(rememberMeKey);
const hashedPassword = hashedPasswordFragment || hashedPasswordQuery;
const hashedPasswordString = hashedPasswordFragment || hashedPasswordQuery;
const hashedPassword = cryptoEngine.HexEncoder.parse(hashedPasswordString);
const rememberMe = rememberMeFragment || rememberMeQuery;
if (hashedPassword) {

Wyświetl plik

@ -381,10 +381,14 @@
trackEvent("generate_encrypted");
const unencrypted = document.getElementById("unencrypted_html").value,
password = document.getElementById("password").value;
const unencrypted = cryptoEngine.UTF8Encoder.parse(document.getElementById("unencrypted_html").value);
const passwordString = document.getElementById("password").value;
const password = cryptoEngine.HexEncoder.parse(passwordString);
const saltString = cryptoEngine.generateRandomSaltString();
const salt = cryptoEngine.HexEncoder.parse(saltString);
const salt = cryptoEngine.generateRandomSalt();
const encryptedMsg = await encode(unencrypted, password, salt);
const templateButton = document.getElementById("template_button").value,
@ -399,7 +403,11 @@
const data = {
staticrypt_config: {
staticryptEncryptedMsgUniqueVariableName: encryptedMsg,
staticryptIvUniqueVariableName: cryptoEngine.HexEncoder.stringify(encryptedMsg.iv),
staticryptEncryptedUniqueVariableName: cryptoEngine.HexEncoder.stringify(
encryptedMsg.encrypted
),
staticryptHmacUniqueVariableName: cryptoEngine.HexEncoder.stringify(encryptedMsg.hmac),
isRememberEnabled,
rememberDurationInDays,
staticryptSaltUniqueVariableName: salt,
@ -415,7 +423,11 @@
template_title: templateTitle || "Protected Page",
};
document.getElementById("encrypted_html_display").textContent = encryptedMsg;
// TODO: Displaying the old encryptedMsg hex string is problematic for large
// TODO: pages. Maybe implement a way to allow user to request that (button?)
// TODO: maybe we need to create a new format to support copy/past,
// TODO: (possibly JSON)?
document.getElementById("encrypted_html_display").textContent = "HTML is encrypted";
setFileToDownload(data);
});