kopia lustrzana https://github.com/robinmoisson/staticrypt
				
				
				
			
		
			
				
	
	
		
			709 wiersze
		
	
	
		
			22 KiB
		
	
	
	
		
			HTML
		
	
	
			
		
		
	
	
			709 wiersze
		
	
	
		
			22 KiB
		
	
	
	
		
			HTML
		
	
	
| <!doctype html>
 | |
| <html class="staticrypt-html">
 | |
| <head>
 | |
|     <meta charset="utf-8">
 | |
|     <title>Protected Page</title>
 | |
|     <meta name="viewport" content="width=device-width, initial-scale=1">
 | |
| 
 | |
|     <!-- do not cache this page -->
 | |
|     <meta http-equiv="cache-control" content="max-age=0"/>
 | |
|     <meta http-equiv="cache-control" content="no-cache"/>
 | |
|     <meta http-equiv="expires" content="0"/>
 | |
|     <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT"/>
 | |
|     <meta http-equiv="pragma" content="no-cache"/>
 | |
| 
 | |
|     <style>
 | |
|         .staticrypt-hr {
 | |
|             margin-top: 20px;
 | |
|             margin-bottom: 20px;
 | |
|             border: 0;
 | |
|             border-top: 1px solid #eee;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-page {
 | |
|             width: 360px;
 | |
|             padding: 8% 0 0;
 | |
|             margin: auto;
 | |
|             box-sizing: border-box;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-form {
 | |
|             position: relative;
 | |
|             z-index: 1;
 | |
|             background: #FFFFFF;
 | |
|             max-width: 360px;
 | |
|             margin: 0 auto 100px;
 | |
|             padding: 45px;
 | |
|             text-align: center;
 | |
|             box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
 | |
|         }
 | |
| 
 | |
|         .staticrypt-form input[type="password"] {
 | |
|             outline: 0;
 | |
|             background: #f2f2f2;
 | |
|             width: 100%;
 | |
|             border: 0;
 | |
|             margin: 0 0 15px;
 | |
|             padding: 15px;
 | |
|             box-sizing: border-box;
 | |
|             font-size: 14px;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-form .staticrypt-decrypt-button {
 | |
|             text-transform: uppercase;
 | |
|             outline: 0;
 | |
|             background: #4CAF50;
 | |
|             width: 100%;
 | |
|             border: 0;
 | |
|             padding: 15px;
 | |
|             color: #FFFFFF;
 | |
|             font-size: 14px;
 | |
|             cursor: pointer;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-form .staticrypt-decrypt-button:hover, .staticrypt-form .staticrypt-decrypt-button:active, .staticrypt-form .staticrypt-decrypt-button:focus {
 | |
|             background: #43A047;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-html {
 | |
|             height: 100%;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-body {
 | |
|             height: 100%;
 | |
|             margin: 0;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-content {
 | |
|             height: 100%;
 | |
|             margin-bottom: 1em;
 | |
|             background: #76b852; /* fallback for old browsers */
 | |
|             background: -webkit-linear-gradient(right, #76b852, #8DC26F);
 | |
|             background: -moz-linear-gradient(right, #76b852, #8DC26F);
 | |
|             background: -o-linear-gradient(right, #76b852, #8DC26F);
 | |
|             background: linear-gradient(to left, #76b852, #8DC26F);
 | |
|             font-family: "Arial", sans-serif;
 | |
|             -webkit-font-smoothing: antialiased;
 | |
|             -moz-osx-font-smoothing: grayscale;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-instructions {
 | |
|             margin-top: -1em;
 | |
|             margin-bottom: 1em;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-title {
 | |
|             font-size: 1.5em;
 | |
|         }
 | |
| 
 | |
|         label.staticrypt-remember {
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             margin-bottom: 1em;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-remember input[type=checkbox] {
 | |
|             transform: scale(1.5);
 | |
|             margin-right: 1em;
 | |
|         }
 | |
| 
 | |
|         .hidden {
 | |
|             display: none !important;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-spinner-container {
 | |
|             height: 100%;
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             justify-content: center;
 | |
|         }
 | |
| 
 | |
|         .staticrypt-spinner {
 | |
|             display: inline-block;
 | |
|             width: 2rem;
 | |
|             height: 2rem;
 | |
|             vertical-align: text-bottom;
 | |
|             border: 0.25em solid gray;
 | |
|             border-right-color: transparent;
 | |
|             border-radius: 50%;
 | |
|             -webkit-animation: spinner-border .75s linear infinite;
 | |
|             animation: spinner-border .75s linear infinite;
 | |
|             animation-duration: 0.75s;
 | |
|             animation-timing-function: linear;
 | |
|             animation-delay: 0s;
 | |
|             animation-iteration-count: infinite;
 | |
|             animation-direction: normal;
 | |
|             animation-fill-mode: none;
 | |
|             animation-play-state: running;
 | |
|             animation-name: spinner-border;
 | |
|         }
 | |
| 
 | |
|         @keyframes spinner-border {
 | |
|             100% {
 | |
|                 transform: rotate(360deg);
 | |
|             }
 | |
|         }
 | |
|     </style>
 | |
| </head>
 | |
| 
 | |
| <body class="staticrypt-body">
 | |
| 
 | |
| <div id="staticrypt_loading" class="staticrypt-spinner-container">
 | |
|     <div class="staticrypt-spinner"></div>
 | |
| </div>
 | |
| 
 | |
| <div id="staticrypt_content" class="staticrypt-content hidden">
 | |
|     <div class="staticrypt-page">
 | |
|         <div class="staticrypt-form">
 | |
|             <div class="staticrypt-instructions">
 | |
|                 <p class="staticrypt-title">Protected Page</p>
 | |
|                 <p>Enter "test" to unlock the page</p>
 | |
|             </div>
 | |
| 
 | |
|             <hr class="staticrypt-hr">
 | |
| 
 | |
|             <form id="staticrypt-form" action="#" method="post">
 | |
|                 <input id="staticrypt-password"
 | |
|                        type="password"
 | |
|                        name="password"
 | |
|                        placeholder="Password"
 | |
|                        autofocus/>
 | |
| 
 | |
|                 <label id="staticrypt-remember-label" class="staticrypt-remember hidden">
 | |
|                     <input id="staticrypt-remember"
 | |
|                            type="checkbox"
 | |
|                            name="remember"/>
 | |
|                     Remember me
 | |
|                 </label>
 | |
| 
 | |
|                 <input type="submit" class="staticrypt-decrypt-button" value="DECRYPT"/>
 | |
|             </form>
 | |
|         </div>
 | |
| 
 | |
|     </div>
 | |
| </div>
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| <script>
 | |
|     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.
 | |
|  */
 | |
| 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));
 | |
| 
 | |
|     const key = await subtle.importKey(
 | |
|         "raw",
 | |
|         HexEncoder.parse(hashedPassphrase),
 | |
|         ENCRYPTION_ALGO,
 | |
|         false,
 | |
|         ["encrypt"]
 | |
|     );
 | |
| 
 | |
|     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.
 | |
|  *
 | |
|  * @param {string} encryptedMsg
 | |
|  * @param {string} hashedPassphrase
 | |
|  * @returns {Promise<string>}
 | |
|  */
 | |
| async function decrypt(encryptedMsg, hashedPassphrase) {
 | |
|     const ivLength = IV_BITS / HEX_BITS;
 | |
|     const iv = HexEncoder.parse(encryptedMsg.substring(0, ivLength));
 | |
|     const encrypted = encryptedMsg.substring(ivLength);
 | |
| 
 | |
|     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;
 | |
| 
 | |
| /**
 | |
|  * Salt and hash the passphrase so it can be stored in localStorage without opening a password reuse vulnerability.
 | |
|  *
 | |
|  * @param {string} passphrase
 | |
|  * @param {string} salt
 | |
|  * @returns {Promise<string>}
 | |
|  */
 | |
| 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);
 | |
| }
 | |
| exports.hashPassphrase = hashPassphrase;
 | |
| 
 | |
| /**
 | |
|  * This hashes the passphrase with 1k iterations. This is a low number, we need this function to support backwards
 | |
|  * compatibility.
 | |
|  *
 | |
|  * @param {string} passphrase
 | |
|  * @param {string} salt
 | |
|  * @returns {Promise<string>}
 | |
|  */
 | |
| function hashLegacyRound(passphrase, salt) {
 | |
|     return pbkdf2(passphrase, salt, 1000, "SHA-1");
 | |
| }
 | |
| 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 hashedPassphrase
 | |
|  * @param salt
 | |
|  * @returns {Promise<string>}
 | |
|  */
 | |
| function hashSecondRound(hashedPassphrase, salt) {
 | |
|     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() {
 | |
|     const bytes = crypto.getRandomValues(new Uint8Array(128 / 8));
 | |
| 
 | |
|     return HexEncoder.stringify(new Uint8Array(bytes));
 | |
| }
 | |
| exports.generateRandomSalt = generateRandomSalt;
 | |
| 
 | |
| 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));
 | |
| 
 | |
|     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 = 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[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
 | |
|     const randomIndex = parsedInt % possibleCharacters.length;
 | |
| 
 | |
|     return possibleCharacters[randomIndex];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Generate a random string of a given length.
 | |
|  *
 | |
|  * @param {int} length
 | |
|  * @returns {string}
 | |
|  */
 | |
| function generateRandomString(length) {
 | |
|     let randomString = '';
 | |
| 
 | |
|     for (let i = 0; i < length; i++) {
 | |
|         randomString += getRandomAlphanum();
 | |
|     }
 | |
| 
 | |
|     return randomString;
 | |
| }
 | |
| exports.generateRandomString = generateRandomString;
 | |
| 
 | |
| 
 | |
|   return exports;
 | |
| })())
 | |
|     const codec = ((function(){
 | |
|   const exports = {};
 | |
|   /**
 | |
|  * Initialize the codec with the provided cryptoEngine - this return functions to encode and decode messages.
 | |
|  *
 | |
|  * @param cryptoEngine - the engine to use for encryption / decryption
 | |
|  */
 | |
| function init(cryptoEngine) {
 | |
|   // TODO: remove on next major version bump. This is a hack to make the salt available in all functions here in a
 | |
|   //  backward compatible way (not requiring to  change the password_template).
 | |
|   const backwardCompatibleSalt = 'b93bbaf35459951c47721d1f3eaeb5b9';
 | |
| 
 | |
|   const exports = {};
 | |
| 
 | |
|   /**
 | |
|    * Top-level function for encoding a message.
 | |
|    * Includes password hashing, encryption, and signing.
 | |
|    *
 | |
|    * @param {string} msg
 | |
|    * @param {string} password
 | |
|    * @param {string} salt
 | |
|    * @param {boolean} isLegacy - whether to use the legacy hashing algorithm (1k iterations) or not
 | |
|    *
 | |
|    * @returns {string} The encoded text
 | |
|    */
 | |
|   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
 | |
|         ? 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 = await cryptoEngine.signMessage(hashedPassphrase, encrypted);
 | |
| 
 | |
|     return hmac + encrypted;
 | |
|   }
 | |
|   exports.encode = encode;
 | |
| 
 | |
|   /**
 | |
|    * Top-level function for decoding a message.
 | |
|    * Includes signature check and decryption.
 | |
|    *
 | |
|    * @param {string} signedMsg
 | |
|    * @param {string} hashedPassphrase
 | |
|    * @param {string} backwardCompatibleHashedPassword
 | |
|    *
 | |
|    * @returns {Object} {success: true, decoded: string} | {success: false, message: string}
 | |
|    */
 | |
|   async function decode(signedMsg, hashedPassphrase, backwardCompatibleHashedPassword = '') {
 | |
|     const encryptedHMAC = signedMsg.substring(0, 64);
 | |
|     const encryptedMsg = signedMsg.substring(64);
 | |
|     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
 | |
|       //  iterations in PBKDF2 - if the key we try isn't working, it might be because it's a remember-me/autodecrypt
 | |
|       //  link key, generated with 1k iterations. Try again with the updated iteration count.
 | |
|       if (!backwardCompatibleHashedPassword) {
 | |
|         return decode(
 | |
|             signedMsg,
 | |
|             await cryptoEngine.hashSecondRound(hashedPassphrase, backwardCompatibleSalt),
 | |
|             hashedPassphrase
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       return { success: false, message: "Signature mismatch" };
 | |
|     }
 | |
| 
 | |
|     // TODO: remove in next major version bump. If we're trying to double hash for backward compatibility reasons,
 | |
|     //  and the attempt is successful, we check if we should update the stored password in localStorage. This avoids
 | |
|     //  having to compute the upgrade each time.
 | |
|     if (backwardCompatibleHashedPassword) {
 | |
|       if (window && window.localStorage) {
 | |
|         const storedPassword = window.localStorage.getItem('staticrypt_passphrase');
 | |
| 
 | |
|         // check the stored password is actually the backward compatible one, so we don't save the new one and trigger
 | |
|         // the "remember-me" by mistake, leaking the password
 | |
|         if (storedPassword === backwardCompatibleHashedPassword) {
 | |
|           window.localStorage.setItem('staticrypt_passphrase', hashedPassphrase);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       success: true,
 | |
|       decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassphrase),
 | |
|     };
 | |
|   }
 | |
|   exports.decode = decode;
 | |
| 
 | |
|   return exports;
 | |
| }
 | |
| exports.init = init;
 | |
| 
 | |
|   return exports;
 | |
| })())
 | |
|     const decode = codec.init(cryptoEngine).decode;
 | |
| 
 | |
|     // variables to be filled when generating the file
 | |
|     const encryptedMsg = '69480424a5fcc19909363e39d8434a0f706b32183e0771f2be1e746cdadfef879e9866b997f75e85a60ddfb7483e571eeba89134dea2f77a4b1123fb2825e8d32132045ea5a98b491fb29b20142643c98a2bc24e90d936f58dea6c67a2a692c9066e4d0cce5f01da8b433a5338224327edda47dadf43a6ffc45185f26f4c2438a68af464f3a69cb586fbb71cf4ef353a66d474fafa848bf736c683c986bb86b8e23dfcf6f3dbef170cacaae3591f9004f6f44b4dcc597d5696adfbb0781a4c32',
 | |
|         salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
 | |
|         labelError = 'Bad password!',
 | |
|         isRememberEnabled = true,
 | |
|         rememberDurationInDays = 0; // 0 means forever
 | |
| 
 | |
|     // constants
 | |
|     const rememberPassphraseKey = 'staticrypt_passphrase',
 | |
|         rememberExpirationKey = 'staticrypt_expiration';
 | |
| 
 | |
|     /**
 | |
|      * Decrypt our encrypted page, replace the whole HTML.
 | |
|      *
 | |
|      * @param  hashedPassphrase
 | |
|      * @returns {Promise<boolean>}
 | |
|      */
 | |
|     async function decryptAndReplaceHtml(hashedPassphrase) {
 | |
|         const result = await decode(encryptedMsg, hashedPassphrase);
 | |
|         if (!result.success) {
 | |
|             return false;
 | |
|         }
 | |
|         const plainHTML = result.decoded;
 | |
| 
 | |
|         document.write(plainHTML);
 | |
|         document.close();
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Clear localstorage from staticrypt related values
 | |
|      */
 | |
|     function clearLocalStorage() {
 | |
|         localStorage.removeItem(rememberPassphraseKey);
 | |
|         localStorage.removeItem(rememberExpirationKey);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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 {Promise<boolean>} true if we derypted and replaced the whole page, false otherwise
 | |
|      */
 | |
|     async function decryptOnLoadFromRememberMe() {
 | |
|         if (!isRememberEnabled) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         // show the remember me checkbox
 | |
|         document.getElementById('staticrypt-remember-label').classList.remove('hidden');
 | |
| 
 | |
|         // if we are login out, clear the storage and terminate
 | |
|         const queryParams = new URLSearchParams(window.location.search);
 | |
| 
 | |
|         if (queryParams.has("staticrypt_logout")) {
 | |
|             clearLocalStorage();
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         // if there is expiration configured, check if we're not beyond the expiration
 | |
|         if (rememberDurationInDays && rememberDurationInDays > 0) {
 | |
|             const expiration = localStorage.getItem(rememberExpirationKey),
 | |
|                 isExpired = expiration && new Date().getTime() > parseInt(expiration);
 | |
| 
 | |
|             if (isExpired) {
 | |
|                 clearLocalStorage();
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         const hashedPassphrase = localStorage.getItem(rememberPassphraseKey);
 | |
| 
 | |
|         if (hashedPassphrase) {
 | |
|             // try to decrypt
 | |
|             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
 | |
|             if (!isDecryptionSuccessful) {
 | |
|                 clearLocalStorage();
 | |
|                 return false;
 | |
|             }
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     function decryptOnLoadFromQueryParam() {
 | |
|         const queryParams = new URLSearchParams(window.location.search);
 | |
|         const hashedPassphrase = queryParams.get("staticrypt_pwd");
 | |
| 
 | |
|         if (hashedPassphrase) {
 | |
|             return decryptAndReplaceHtml(hashedPassphrase);
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     // try to automatically decrypt on load if there is a saved password
 | |
|     window.onload = async function () {
 | |
|         let hasDecrypted = await decryptOnLoadFromQueryParam();
 | |
| 
 | |
|         if (!hasDecrypted) {
 | |
|             hasDecrypted = await decryptOnLoadFromRememberMe();
 | |
|         }
 | |
| 
 | |
|         // if we didn't decrypt anything, show the password prompt. Otherwise the content has already been replaced, no
 | |
|         // need to do anything
 | |
|         if (!hasDecrypted) {
 | |
|             document.getElementById("staticrypt_loading").classList.add("hidden");
 | |
|             document.getElementById("staticrypt_content").classList.remove("hidden");
 | |
|             document.getElementById("staticrypt-password").focus();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // handle password form submission
 | |
|     document.getElementById('staticrypt-form').addEventListener('submit', async function (e) {
 | |
|         e.preventDefault();
 | |
| 
 | |
|         const passphrase = document.getElementById('staticrypt-password').value,
 | |
|             shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
 | |
| 
 | |
|         // decrypt and replace the whole page
 | |
|         const hashedPassphrase = await cryptoEngine.hashPassphrase(passphrase, salt);
 | |
|         const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassphrase);
 | |
| 
 | |
|         if (isDecryptionSuccessful) {
 | |
|             // remember the hashedPassphrase and set its expiration if necessary
 | |
|             if (isRememberEnabled && shouldRememberPassphrase) {
 | |
|                 window.localStorage.setItem(rememberPassphraseKey, hashedPassphrase);
 | |
| 
 | |
|                 // set the expiration if the duration isn't 0 (meaning no expiration)
 | |
|                 if (rememberDurationInDays > 0) {
 | |
|                     window.localStorage.setItem(
 | |
|                         rememberExpirationKey,
 | |
|                         (new Date().getTime() + rememberDurationInDays * 24 * 60 * 60 * 1000).toString()
 | |
|                     );
 | |
|                 }
 | |
|             }
 | |
|         } else {
 | |
|             alert(labelError);
 | |
|         }
 | |
|     });
 | |
| </script>
 | |
| </body>
 | |
| </html>
 |