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
	
	 robinmoisson
						robinmoisson