kopia lustrzana https://github.com/robinmoisson/staticrypt
Merge f2ef582f3a
into f7266b0740
commit
c3d7e92aa8
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
67
cli/index.js
67
cli/index.js
|
@ -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,
|
||||
|
|
117
lib/codec.js
117
lib/codec.js
|
@ -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;
|
||||
|
|
|
@ -1,75 +1,155 @@
|
|||
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 = {
|
||||
/**
|
||||
* 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);
|
||||
function isArrayEqual(a, b) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
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("");
|
||||
},
|
||||
};
|
||||
return true;
|
||||
}
|
||||
exports.isArrayEqual = isArrayEqual;
|
||||
|
||||
/**
|
||||
* Translates between utf8 strings and Uint8Array bytes.
|
||||
* Translates between utf8 encoded hexadecimal strings
|
||||
* and Uint8Array.
|
||||
*/
|
||||
const HexEncoder = isNode
|
||||
? {
|
||||
// Node version
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
let byteString = bytes[i].toString(16);
|
||||
if (byteString.length < 2) {
|
||||
byteString = "0" + byteString;
|
||||
}
|
||||
hexBytes.push(byteString);
|
||||
}
|
||||
return hexBytes.join("");
|
||||
},
|
||||
};
|
||||
exports.HexEncoder = HexEncoder;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Ładowanie…
Reference in New Issue