Create learn.html
This commit is contained in:
parent
90538f7ebc
commit
f216551673
1 changed files with 894 additions and 0 deletions
894
learn.html
Normal file
894
learn.html
Normal file
|
@ -0,0 +1,894 @@
|
|||
<!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"],
|
||||
input[type="text"] {
|
||||
background: inherit;
|
||||
border: 0;
|
||||
box-sizing: border-box; /* This ensures padding is included in the total width */
|
||||
font-size: 14px;
|
||||
outline: 0;
|
||||
padding: 15px 30px 15px 15px; /* Adjust the padding to ensure there is space for the icon */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.staticrypt-password-container {
|
||||
position: relative;
|
||||
outline: 0;
|
||||
background: #f2f2f2;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.staticrypt-toggle-password-visibility {
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
opacity: 60%;
|
||||
padding: 13px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.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: #4CAF50;
|
||||
filter: brightness(92%);
|
||||
}
|
||||
|
||||
.staticrypt-html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.staticrypt-body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.staticrypt-content {
|
||||
height: 100%;
|
||||
margin-bottom: 1em;
|
||||
background: #76B852;
|
||||
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 0.75s linear infinite;
|
||||
animation: spinner-border 0.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);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
.staticrypt-form input[type="password"],
|
||||
input[type="text"] {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</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">Learn</p>
|
||||
<p></p>
|
||||
</div>
|
||||
|
||||
<hr class="staticrypt-hr" />
|
||||
|
||||
<form id="staticrypt-form" action="#" method="post">
|
||||
<div class="staticrypt-password-container">
|
||||
<input
|
||||
id="staticrypt-password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<img
|
||||
class="staticrypt-toggle-password-visibility"
|
||||
alt="template_toggle_show"
|
||||
title="template_toggle_show"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNS4yIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTM4LjggNS4xQzI4LjQtMy4xIDEzLjMtMS4yIDUuMSA5LjJTLTEuMiAzNC43IDkuMiA0Mi45bDU5MiA0NjRjMTAuNCA4LjIgMjUuNSA2LjMgMzMuNy00LjFzNi4zLTI1LjUtNC4xLTMzLjdMNTI1LjYgMzg2LjdjMzkuNi00MC42IDY2LjQtODYuMSA3OS45LTExOC40YzMuMy03LjkgMy4zLTE2LjcgMC0yNC42Yy0xNC45LTM1LjctNDYuMi04Ny43LTkzLTEzMS4xQzQ2NS41IDY4LjggNDAwLjggMzIgMzIwIDMyYy02OC4yIDAtMTI1IDI2LjMtMTY5LjMgNjAuOEwzOC44IDUuMXpNMjIzLjEgMTQ5LjVDMjQ4LjYgMTI2LjIgMjgyLjcgMTEyIDMyMCAxMTJjNzkuNSAwIDE0NCA2NC41IDE0NCAxNDRjMCAyNC45LTYuMyA0OC4zLTE3LjQgNjguN0w0MDggMjk0LjVjOC40LTE5LjMgMTAuNi00MS40IDQuOC02My4zYy0xMS4xLTQxLjUtNDcuOC02OS40LTg4LjYtNzEuMWMtNS44LS4yLTkuMiA2LjEtNy40IDExLjdjMi4xIDYuNCAzLjMgMTMuMiAzLjMgMjAuM2MwIDEwLjItMi40IDE5LjgtNi42IDI4LjNsLTkwLjMtNzAuOHpNMzczIDM4OS45Yy0xNi40IDYuNS0zNC4zIDEwLjEtNTMgMTAuMWMtNzkuNSAwLTE0NC02NC41LTE0NC0xNDRjMC02LjkgLjUtMTMuNiAxLjQtMjAuMkw4My4xIDE2MS41QzYwLjMgMTkxLjIgNDQgMjIwLjggMzQuNSAyNDMuN2MtMy4zIDcuOS0zLjMgMTYuNyAwIDI0LjZjMTQuOSAzNS43IDQ2LjIgODcuNyA5MyAxMzEuMUMxNzQuNSA0NDMuMiAyMzkuMiA0ODAgMzIwIDQ4MGM0Ny44IDAgODkuOS0xMi45IDEyNi4yLTMyLjVMMzczIDM4OS45eiIvPjwvc3ZnPg=="
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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="Access Flashcards" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// these variables will be filled when generating the file - the template format is 'variable_name'
|
||||
const staticryptInitiator =
|
||||
((function(){
|
||||
const exports = {};
|
||||
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.
|
||||
*/
|
||||
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, 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 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} hashedPassword
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function decrypt(encryptedMsg, hashedPassword) {
|
||||
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(hashedPassword), 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 password so it can be stored in localStorage without opening a password reuse vulnerability.
|
||||
*
|
||||
* @param {string} password
|
||||
* @param {string} salt
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function hashPassword(password, salt) {
|
||||
// we hash the password in multiple steps, each adding more iterations. This is because we used to allow less
|
||||
// iterations, so for backward compatibility reasons, we need to support going from that to more iterations.
|
||||
let hashedPassword = await hashLegacyRound(password, salt);
|
||||
|
||||
hashedPassword = await hashSecondRound(hashedPassword, salt);
|
||||
|
||||
return hashThirdRound(hashedPassword, salt);
|
||||
}
|
||||
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>}
|
||||
*/
|
||||
function hashLegacyRound(password, salt) {
|
||||
return pbkdf2(password, 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 hashedPassword
|
||||
* @param salt
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function hashSecondRound(hashedPassword, salt) {
|
||||
return pbkdf2(hashedPassword, salt, 14000, "SHA-256");
|
||||
}
|
||||
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>}
|
||||
*/
|
||||
function hashThirdRound(hashedPassword, salt) {
|
||||
return 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 {int} iterations
|
||||
* @param {string} hashAlgorithm
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function pbkdf2(password, salt, iterations, hashAlgorithm) {
|
||||
const key = await subtle.importKey("raw", UTF8Encoder.parse(password), "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));
|
||||
}
|
||||
|
||||
function generateRandomSalt() {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(128 / 8));
|
||||
|
||||
return HexEncoder.stringify(new Uint8Array(bytes));
|
||||
}
|
||||
exports.generateRandomSalt = generateRandomSalt;
|
||||
|
||||
async function signMessage(hashedPassword, message) {
|
||||
const key = await subtle.importKey(
|
||||
"raw",
|
||||
HexEncoder.parse(hashedPassword),
|
||||
{
|
||||
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) {
|
||||
const exports = {};
|
||||
|
||||
/**
|
||||
* Top-level function for encoding a message.
|
||||
* Includes password hashing, encryption, and signing.
|
||||
*
|
||||
* @param {string} msg
|
||||
* @param {string} password
|
||||
* @param {string} salt
|
||||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
async function encode(msg, password, salt) {
|
||||
const hashedPassword = await cryptoEngine.hashPassword(password, salt);
|
||||
|
||||
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;
|
||||
}
|
||||
exports.encode = encode;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @returns {string} The encoded text
|
||||
*/
|
||||
async function 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;
|
||||
}
|
||||
exports.encodeWithHashedPassword = encodeWithHashedPassword;
|
||||
|
||||
/**
|
||||
* Top-level function for decoding a message.
|
||||
* Includes signature check and decryption.
|
||||
*
|
||||
* @param {string} signedMsg
|
||||
* @param {string} hashedPassword
|
||||
* @param {string} salt
|
||||
* @param {int} backwardCompatibleAttempt
|
||||
* @param {string} originalPassword
|
||||
*
|
||||
* @returns {Object} {success: true, decoded: string} | {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);
|
||||
|
||||
if (decryptedHMAC !== encryptedHMAC) {
|
||||
// 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);
|
||||
}
|
||||
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 { success: false, message: "Signature mismatch" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
decoded: await cryptoEngine.decrypt(encryptedMsg, hashedPassword),
|
||||
};
|
||||
}
|
||||
exports.decode = decode;
|
||||
|
||||
return exports;
|
||||
}
|
||||
exports.init = init;
|
||||
|
||||
return exports;
|
||||
})());
|
||||
const decode = codec.init(cryptoEngine).decode;
|
||||
|
||||
/**
|
||||
* Initialize the staticrypt module, that exposes functions callbable by the password_template.
|
||||
*
|
||||
* @param {{
|
||||
* staticryptEncryptedMsgUniqueVariableName: string,
|
||||
* isRememberEnabled: boolean,
|
||||
* rememberDurationInDays: number,
|
||||
* staticryptSaltUniqueVariableName: string,
|
||||
* }} staticryptConfig - object of data that is stored on the password_template at encryption time.
|
||||
*
|
||||
* @param {{
|
||||
* rememberExpirationKey: string,
|
||||
* rememberPassphraseKey: string,
|
||||
* replaceHtmlCallback: function,
|
||||
* clearLocalStorageCallback: function,
|
||||
* }} templateConfig - object of data that can be configured by a custom password_template.
|
||||
*/
|
||||
function init(staticryptConfig, templateConfig) {
|
||||
const exports = {};
|
||||
|
||||
/**
|
||||
* Decrypt our encrypted page, replace the whole HTML.
|
||||
*
|
||||
* @param {string} hashedPassword
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function decryptAndReplaceHtml(hashedPassword) {
|
||||
const { staticryptEncryptedMsgUniqueVariableName, staticryptSaltUniqueVariableName } = staticryptConfig;
|
||||
const { replaceHtmlCallback } = templateConfig;
|
||||
|
||||
const result = await decode(
|
||||
staticryptEncryptedMsgUniqueVariableName,
|
||||
hashedPassword,
|
||||
staticryptSaltUniqueVariableName
|
||||
);
|
||||
if (!result.success) {
|
||||
return false;
|
||||
}
|
||||
const plainHTML = result.decoded;
|
||||
|
||||
// if the user configured a callback call it, otherwise just replace the whole HTML
|
||||
if (typeof replaceHtmlCallback === "function") {
|
||||
replaceHtmlCallback(plainHTML);
|
||||
} else {
|
||||
document.write(plainHTML);
|
||||
document.close();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to decrypt the page and replace the whole HTML.
|
||||
*
|
||||
* @param {string} password
|
||||
* @param {boolean} isRememberChecked
|
||||
*
|
||||
* @returns {Promise<{isSuccessful: boolean, hashedPassword?: string}>} - 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;
|
||||
|
||||
// decrypt and replace the whole page
|
||||
const hashedPassword = await cryptoEngine.hashPassword(password, staticryptSaltUniqueVariableName);
|
||||
return handleDecryptionOfPageFromHash(hashedPassword, isRememberChecked);
|
||||
}
|
||||
exports.handleDecryptionOfPage = handleDecryptionOfPage;
|
||||
|
||||
async function handleDecryptionOfPageFromHash(hashedPassword, isRememberChecked) {
|
||||
const { isRememberEnabled, rememberDurationInDays } = staticryptConfig;
|
||||
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
|
||||
|
||||
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassword);
|
||||
|
||||
if (!isDecryptionSuccessful) {
|
||||
return {
|
||||
isSuccessful: false,
|
||||
hashedPassword,
|
||||
};
|
||||
}
|
||||
|
||||
// remember the hashedPassword and set its expiration if necessary
|
||||
if (isRememberEnabled && isRememberChecked) {
|
||||
window.localStorage.setItem(rememberPassphraseKey, hashedPassword);
|
||||
|
||||
// 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isSuccessful: true,
|
||||
hashedPassword,
|
||||
};
|
||||
}
|
||||
exports.handleDecryptionOfPageFromHash = handleDecryptionOfPageFromHash;
|
||||
|
||||
/**
|
||||
* Clear localstorage from staticrypt related values
|
||||
*/
|
||||
function clearLocalStorage() {
|
||||
const { clearLocalStorageCallback, rememberExpirationKey, rememberPassphraseKey } = templateConfig;
|
||||
|
||||
if (typeof clearLocalStorageCallback === "function") {
|
||||
clearLocalStorageCallback();
|
||||
} else {
|
||||
localStorage.removeItem(rememberPassphraseKey);
|
||||
localStorage.removeItem(rememberExpirationKey);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDecryptOnLoad() {
|
||||
let isSuccessful = await decryptOnLoadFromUrl();
|
||||
|
||||
if (!isSuccessful) {
|
||||
isSuccessful = await decryptOnLoadFromRememberMe();
|
||||
}
|
||||
|
||||
return { isSuccessful };
|
||||
}
|
||||
exports.handleDecryptOnLoad = handleDecryptOnLoad;
|
||||
|
||||
/**
|
||||
* Clear storage if we are logging out
|
||||
*
|
||||
* @returns {boolean} - whether we logged out
|
||||
*/
|
||||
function logoutIfNeeded() {
|
||||
const logoutKey = "staticrypt_logout";
|
||||
|
||||
// handle logout through query param
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
if (queryParams.has(logoutKey)) {
|
||||
clearLocalStorage();
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle logout through URL fragment
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash.includes(logoutKey)) {
|
||||
clearLocalStorage();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
const { rememberDurationInDays } = staticryptConfig;
|
||||
const { rememberExpirationKey, rememberPassphraseKey } = templateConfig;
|
||||
|
||||
// if we are login out, terminate
|
||||
if (logoutIfNeeded()) {
|
||||
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 hashedPassword = localStorage.getItem(rememberPassphraseKey);
|
||||
|
||||
if (hashedPassword) {
|
||||
// try to decrypt
|
||||
const isDecryptionSuccessful = await decryptAndReplaceHtml(hashedPassword);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
async function decryptOnLoadFromUrl() {
|
||||
const passwordKey = "staticrypt_pwd";
|
||||
const rememberMeKey = "remember_me";
|
||||
|
||||
// try to get the password from the query param (for backward compatibility - we now want to avoid this method,
|
||||
// since it sends the hashed password to the server which isn't needed)
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
const hashedPasswordQuery = queryParams.get(passwordKey);
|
||||
const rememberMeQuery = queryParams.get(rememberMeKey);
|
||||
|
||||
const urlFragment = window.location.hash.substring(1);
|
||||
// get the password from the url fragment
|
||||
const hashedPasswordRegexMatch = urlFragment.match(new RegExp(passwordKey + "=([^&]*)"));
|
||||
const hashedPasswordFragment = hashedPasswordRegexMatch ? hashedPasswordRegexMatch[1] : null;
|
||||
const rememberMeFragment = urlFragment.includes(rememberMeKey);
|
||||
|
||||
const hashedPassword = hashedPasswordFragment || hashedPasswordQuery;
|
||||
const rememberMe = rememberMeFragment || rememberMeQuery;
|
||||
|
||||
if (hashedPassword) {
|
||||
return handleDecryptionOfPageFromHash(hashedPassword, rememberMe);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return exports;
|
||||
}
|
||||
exports.init = init;
|
||||
|
||||
return exports;
|
||||
})());
|
||||
;
|
||||
const templateError = "template_error",
|
||||
templateToggleAltShow = "template_toggle_show",
|
||||
templateToggleAltHide = "template_toggle_hide",
|
||||
isRememberEnabled = true,
|
||||
staticryptConfig = {"staticryptEncryptedMsgUniqueVariableName":"fcca6085654404ae7426b5cd4e5ff6a76bbf1ff500fa0271f83f8dd6839ae0feb56de74ed54874a8c4d6503b35dad7e4746851ffe5773714420cd598fa05a25af9d2b3d2538bb70d184263593dbeb6c325efddaa1c917f41c8f2eeb5ff3b7e3c948b9088d918a214ed3ddb9e8620b29d2c929b18fa6f30b77a050863d99c4692d27c426ca95bbdfd317dbfa25b66d520eca6572ab9f28c5e2716f3f6c5082f08c6af55eed9bb166ffff5dff81d7471ba70e5720fdbed34a66db26cb2359fdce4b5500d8213a75225b31ba575a1b69c577a86e2cc83eb64fe4a4a389e6b91e9e9d18c58b9fedb5e8fee0ed03675b6d2d88ec4f2581a98bd5ba83399d9aefc71ed8992ee01541cccd31af197784da09c190f55e087d50739944967c9c2b7b27a041dfa3188874b47d9b3ce15c1265cf2a7f3add4c0c053bf85a096d185cee3f25727f07a22b9c37f6e526ca0622fa78f782d34026d3736e5272f8ad702ef7d9b03664018728634332611a35ed0513eed8715807af497a5cbe6acf13ea31954029a95f2ece42198e577e4758eeb42987c237c67929ec372b64075ad02a067bd20c3d659f1e49bc545d31589eee0b1869505974f17a219d2ef6fb81e11b61ba47c3dab6e8929f3c31ad1aabce9ba468355913cc757556c7c9fbb938e28efa9f38d15eb31c23c0a363b8f6fa00d784bb0b8897da9e51fdf976c495a0e6a8a7fbc3b4adf7e9b5e7ea8d3893ed0cf8c9399901f532711370de11946d7e4be6f228649358d859d7b5384d4ad0b8620d5286dd692c6b9929586d603b5c4226b84445c7488","isRememberEnabled":true,"rememberDurationInDays":0,"staticryptSaltUniqueVariableName":"6a10f4d30c6486e6c69e16c5488e06a7"};
|
||||
|
||||
// you can edit these values to customize some of the behavior of StatiCrypt
|
||||
const templateConfig = {
|
||||
rememberExpirationKey: "staticrypt_expiration",
|
||||
rememberPassphraseKey: "staticrypt_passphrase",
|
||||
replaceHtmlCallback: null,
|
||||
clearLocalStorageCallback: null,
|
||||
};
|
||||
|
||||
// init the staticrypt engine
|
||||
const staticrypt = staticryptInitiator.init(staticryptConfig, templateConfig);
|
||||
|
||||
// try to automatically decrypt on load if there is a saved password
|
||||
window.onload = async function () {
|
||||
const { isSuccessful } = await staticrypt.handleDecryptOnLoad();
|
||||
|
||||
// if we didn't decrypt anything on load, show the password prompt. Otherwise the content has already been
|
||||
// replaced, no need to do anything
|
||||
if (!isSuccessful) {
|
||||
// hide loading screen
|
||||
document.getElementById("staticrypt_loading").classList.add("hidden");
|
||||
document.getElementById("staticrypt_content").classList.remove("hidden");
|
||||
document.getElementById("staticrypt-password").focus();
|
||||
|
||||
// show the remember me checkbox
|
||||
if (isRememberEnabled) {
|
||||
document.getElementById("staticrypt-remember-label").classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// toggle password visibility
|
||||
const toggleIcon = document.querySelector(".staticrypt-toggle-password-visibility");
|
||||
// these two icons are coming from FontAwesome
|
||||
const imgSrcEyeClosed =
|
||||
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNS4yIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTM4LjggNS4xQzI4LjQtMy4xIDEzLjMtMS4yIDUuMSA5LjJTLTEuMiAzNC43IDkuMiA0Mi45bDU5MiA0NjRjMTAuNCA4LjIgMjUuNSA2LjMgMzMuNy00LjFzNi4zLTI1LjUtNC4xLTMzLjdMNTI1LjYgMzg2LjdjMzkuNi00MC42IDY2LjQtODYuMSA3OS45LTExOC40YzMuMy03LjkgMy4zLTE2LjcgMC0yNC42Yy0xNC45LTM1LjctNDYuMi04Ny43LTkzLTEzMS4xQzQ2NS41IDY4LjggNDAwLjggMzIgMzIwIDMyYy02OC4yIDAtMTI1IDI2LjMtMTY5LjMgNjAuOEwzOC44IDUuMXpNMjIzLjEgMTQ5LjVDMjQ4LjYgMTI2LjIgMjgyLjcgMTEyIDMyMCAxMTJjNzkuNSAwIDE0NCA2NC41IDE0NCAxNDRjMCAyNC45LTYuMyA0OC4zLTE3LjQgNjguN0w0MDggMjk0LjVjOC40LTE5LjMgMTAuNi00MS40IDQuOC02My4zYy0xMS4xLTQxLjUtNDcuOC02OS40LTg4LjYtNzEuMWMtNS44LS4yLTkuMiA2LjEtNy40IDExLjdjMi4xIDYuNCAzLjMgMTMuMiAzLjMgMjAuM2MwIDEwLjItMi40IDE5LjgtNi42IDI4LjNsLTkwLjMtNzAuOHpNMzczIDM4OS45Yy0xNi40IDYuNS0zNC4zIDEwLjEtNTMgMTAuMWMtNzkuNSAwLTE0NC02NC41LTE0NC0xNDRjMC02LjkgLjUtMTMuNiAxLjQtMjAuMkw4My4xIDE2MS41QzYwLjMgMTkxLjIgNDQgMjIwLjggMzQuNSAyNDMuN2MtMy4zIDcuOS0zLjMgMTYuNyAwIDI0LjZjMTQuOSAzNS43IDQ2LjIgODcuNyA5MyAxMzEuMUMxNzQuNSA0NDMuMiAyMzkuMiA0ODAgMzIwIDQ4MGM0Ny44IDAgODkuOS0xMi45IDEyNi4yLTMyLjVMMzczIDM4OS45eiIvPjwvc3ZnPg==";
|
||||
const imgSrcEyeOpened =
|
||||
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNS4yIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTI4OCAzMmMtODAuOCAwLTE0NS41IDM2LjgtMTkyLjYgODAuNkM0OC42IDE1NiAxNy4zIDIwOCAyLjUgMjQzLjdjLTMuMyA3LjktMy4zIDE2LjcgMCAyNC42QzE3LjMgMzA0IDQ4LjYgMzU2IDk1LjQgMzk5LjRDMTQyLjUgNDQzLjIgMjA3LjIgNDgwIDI4OCA0ODBzMTQ1LjUtMzYuOCAxOTIuNi04MC42YzQ2LjgtNDMuNSA3OC4xLTk1LjQgOTMtMTMxLjFjMy4zLTcuOSAzLjMtMTYuNyAwLTI0LjZjLTE0LjktMzUuNy00Ni4yLTg3LjctOTMtMTMxLjFDNDMzLjUgNjguOCAzNjguOCAzMiAyODggMzJ6TTE0NCAyNTZhMTQ0IDE0NCAwIDEgMSAyODggMCAxNDQgMTQ0IDAgMSAxIC0yODggMHptMTQ0LTY0YzAgMzUuMy0yOC43IDY0LTY0IDY0Yy03LjEgMC0xMy45LTEuMi0yMC4zLTMuM2MtNS41LTEuOC0xMS45IDEuNi0xMS43IDcuNGMuMyA2LjkgMS4zIDEzLjggMy4yIDIwLjdjMTMuNyA1MS4yIDY2LjQgODEuNiAxMTcuNiA2Ny45czgxLjYtNjYuNCA2Ny45LTExNy42Yy0xMS4xLTQxLjUtNDcuOC02OS40LTg4LjYtNzEuMWMtNS44LS4yLTkuMiA2LjEtNy40IDExLjdjMi4xIDYuNCAzLjMgMTMuMiAzLjMgMjAuM3oiLz48L3N2Zz4=";
|
||||
toggleIcon.addEventListener("click", function () {
|
||||
const passwordInput = document.getElementById("staticrypt-password");
|
||||
if (passwordInput.type === "password") {
|
||||
passwordInput.type = "text";
|
||||
toggleIcon.src = imgSrcEyeOpened;
|
||||
toggleIcon.alt = templateToggleAltHide;
|
||||
toggleIcon.title = templateToggleAltHide;
|
||||
} else {
|
||||
passwordInput.type = "password";
|
||||
toggleIcon.src = imgSrcEyeClosed;
|
||||
toggleIcon.alt = templateToggleAltShow;
|
||||
toggleIcon.title = templateToggleAltShow;
|
||||
}
|
||||
});
|
||||
|
||||
// handle password form submission
|
||||
document.getElementById("staticrypt-form").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = document.getElementById("staticrypt-password").value,
|
||||
isRememberChecked = document.getElementById("staticrypt-remember").checked;
|
||||
|
||||
const { isSuccessful } = await staticrypt.handleDecryptionOfPage(password, isRememberChecked);
|
||||
|
||||
if (!isSuccessful) {
|
||||
alert(templateError);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue