mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 23:38:56 +00:00
261 lines
8.6 KiB
JavaScript
261 lines
8.6 KiB
JavaScript
class FixItDecryptor {
|
|
/**
|
|
* FixIt decryptor for encrypted pages and fixit-encryptor shortcode
|
|
* @param {Object} options
|
|
* @param {Function} [options.decrypted] [Lifecycle Hooks] handler after decrypting
|
|
* @param {Function} [options.reset] [Lifecycle Hooks] handler after encrypting again
|
|
* @param {Number} [options.duration=86400] number of seconds to cache decryption statistics. unit: s
|
|
*/
|
|
constructor(options = {}) {
|
|
this.options = options || {};
|
|
this.options.duration = this.options.duration || 24 * 60 * 60; // default cache one day
|
|
this.decryptedEventSet = new Set();
|
|
this.partialDecryptedEventSet = new Set();
|
|
this.resetEventSet = new Set();
|
|
customElements.get('fixit-encryptor') || customElements.define('fixit-encryptor', class extends HTMLElement {});
|
|
customElements.get('cipher-text') || customElements.define('cipher-text', class extends HTMLElement {});
|
|
}
|
|
|
|
/**
|
|
* decrypt content
|
|
* @param {Element} $cipherText cipher text element
|
|
* @param {Element} $target target content element
|
|
* @param {String} salt salt string
|
|
*/
|
|
#decryptContent($cipherText, $target, salt) {
|
|
try {
|
|
$target.innerHTML = CryptoJS.enc.Base64
|
|
.parse($cipherText.innerText.replace(salt, ''))
|
|
.toString(CryptoJS.enc.Utf8);
|
|
$cipherText.parentElement.classList.add('decrypted');
|
|
} catch (err) {
|
|
return console.error(err);
|
|
}
|
|
// decrypted hook
|
|
const eventSet = $target.id === 'content' ? this.decryptedEventSet : this.partialDecryptedEventSet;
|
|
for (const event of eventSet) {
|
|
event($target);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* validate password
|
|
* @param {Element} $encryptor fixit-encryptor element
|
|
* @param {Function} callback callback function after password validation
|
|
* @returns
|
|
*/
|
|
async #validatePassword($encryptor, callback) {
|
|
const $cipherText = $encryptor.querySelector('cipher-text');
|
|
const password = $cipherText.dataset.password;
|
|
const inputEl = $encryptor.querySelector('.fixit-decryptor-input');
|
|
const input = inputEl.value.trim();
|
|
// Warning: insufficient-password-hash Weak hashing algorithms for passwords poses security risks.
|
|
const { h64ToString } = await xxhash();
|
|
const inputHash = h64ToString(input);
|
|
const inputSha256 = CryptoJS.SHA256(input).toString();
|
|
const saltLen = input.length % 2 ? input.length : input.length + 1;
|
|
|
|
inputEl.value = '';
|
|
inputEl.blur();
|
|
if (!input) {
|
|
alert('Please enter the correct password!');
|
|
return console.warn('Please enter the correct password!');
|
|
}
|
|
if (inputHash !== password) {
|
|
alert(`Password error: ${input} not the correct password!`);
|
|
return console.warn(`Password error: ${input} not the correct password!`);
|
|
}
|
|
callback($cipherText, inputHash, inputSha256.slice(saltLen));
|
|
}
|
|
|
|
/**
|
|
* initialize FixIt decryptor
|
|
* @param {Object} options
|
|
* @param {Boolean} options.all whether to decrypt all content
|
|
* @param {String} options.shortcode whether to decrypt fixit-encryptor shortcode
|
|
*/
|
|
init({ all, shortcode }) {
|
|
this.addEventListener('decrypted', this.options?.decrypted);
|
|
this.addEventListener('partial-decrypted', this.options?.partialDecrypted);
|
|
this.addEventListener('reset', this.options?.reset);
|
|
const $content = document.querySelector('#content');
|
|
if (shortcode) {
|
|
this.addEventListener('decrypted', () => {
|
|
this.initShortcodes($content);
|
|
});
|
|
this.addEventListener('partial-decrypted', ($parent) => {
|
|
this.initShortcodes($parent);
|
|
});
|
|
}
|
|
if (all) {
|
|
this.initPage();
|
|
} else if (shortcode) {
|
|
this.initShortcodes($content);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* initialize FixIt decryptor for the encrypted pages
|
|
*/
|
|
initPage() {
|
|
this.validateCache();
|
|
const $encryptor = document.querySelector('article > fixit-encryptor');
|
|
const $content = document.querySelector('#content');
|
|
|
|
const decryptorHandler = () => {
|
|
this.#validatePassword($encryptor, ($cipherText, passwordHash, salt) => {
|
|
// cache decryption statistics
|
|
window.localStorage?.setItem(
|
|
`fixit-decryptor/#${location.pathname}`,
|
|
JSON.stringify({
|
|
expiration: Math.ceil(Date.now() / 1000) + this.options.duration,
|
|
password: passwordHash,
|
|
salt,
|
|
})
|
|
);
|
|
this.#decryptContent($cipherText, $content, salt);
|
|
});
|
|
};
|
|
|
|
// bind decryptor input enter keydown event
|
|
$encryptor.querySelector('.fixit-decryptor-input')?.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
decryptorHandler();
|
|
}
|
|
});
|
|
|
|
// bind decryptor button click event
|
|
$encryptor.querySelector('.fixit-decryptor-btn')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
decryptorHandler();
|
|
});
|
|
|
|
// bind encryptor button click event
|
|
$encryptor.querySelector('.fixit-encryptor-btn')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
$encryptor.classList.remove('decrypted');
|
|
$content.innerHTML = '';
|
|
window.localStorage?.removeItem(`fixit-decryptor/#${location.pathname}`);
|
|
// reset hook
|
|
for (const event of this.resetEventSet) {
|
|
event();
|
|
}
|
|
});
|
|
|
|
$encryptor.classList.add('initialized');
|
|
}
|
|
|
|
/**
|
|
* initialize FixIt decryptor for fixit-encryptor shortcodes
|
|
* @param {Element} $parent parent element
|
|
*/
|
|
initShortcodes($parent) {
|
|
const $shortcodes = $parent.querySelectorAll('fixit-encryptor:not(.initialized)');
|
|
|
|
$shortcodes.forEach($shortcode => {
|
|
const decryptorHandler = () => {
|
|
const $content = $shortcode.querySelector('.decryptor-content');
|
|
this.#validatePassword($shortcode, ($cipherText, passwordHash, salt) => {
|
|
this.#decryptContent($cipherText, $content, salt);
|
|
});
|
|
};
|
|
|
|
// bind decryptor input enter keydown event
|
|
$shortcode.querySelector('.fixit-decryptor-input')?.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
decryptorHandler();
|
|
}
|
|
});
|
|
|
|
// bind decryptor button click event
|
|
$shortcode.querySelector('.fixit-decryptor-btn')?.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
decryptorHandler();
|
|
});
|
|
|
|
$shortcode.classList.add('initialized');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* validate the cached decryption statistics in localStorage
|
|
* @returns {FixItDecryptor}
|
|
*/
|
|
validateCache() {
|
|
const $content = document.querySelector('#content');
|
|
const $encryptor = document.querySelector('article > fixit-encryptor');
|
|
const $cipherText = $encryptor.querySelector('cipher-text');
|
|
const password = $cipherText.dataset.password;
|
|
const cachedStat = JSON.parse(window.localStorage?.getItem(`fixit-decryptor/#${location.pathname}`));
|
|
|
|
if (!cachedStat || cachedStat?.password !== password || Number(cachedStat?.expiration) < Math.ceil(Date.now() / 1000)) {
|
|
if (cachedStat) {
|
|
window.localStorage?.removeItem(`fixit-decryptor/#${location.pathname}`);
|
|
console.warn('The password has expired, please re-enter!');
|
|
}
|
|
return this;
|
|
}
|
|
this.#decryptContent($cipherText, $content, cachedStat.salt);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* add event listener for FixIt Decryptor
|
|
* @param {String} event event name
|
|
* @param {Function} listener event handler
|
|
* @returns {FixItDecryptor}
|
|
*/
|
|
addEventListener(event, listener) {
|
|
if (typeof listener !== 'function') {
|
|
return this;
|
|
}
|
|
switch (event) {
|
|
case 'decrypted':
|
|
this.decryptedEventSet.add(listener);
|
|
break;
|
|
case 'partial-decrypted':
|
|
this.partialDecryptedEventSet.add(listener);
|
|
break;
|
|
case 'reset':
|
|
this.resetEventSet.add(listener);
|
|
break;
|
|
default:
|
|
console.warn(`Event ${event} not found in FixIt Decryptor!`);
|
|
break;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* remove event listener for FixIt Decryptor
|
|
* @param {String} event event name
|
|
* @param {Function} listener event handler
|
|
* @returns {FixItDecryptor}
|
|
*/
|
|
removeEventListener(event, listener) {
|
|
if (typeof listener !== 'function') {
|
|
return this;
|
|
}
|
|
switch (event) {
|
|
case 'decrypted':
|
|
this.decryptedEventSet.delete(listener);
|
|
break;
|
|
case 'partial-decrypted':
|
|
this.partialDecryptedEventSet.delete(listener);
|
|
break;
|
|
case 'reset':
|
|
this.resetEventSet.delete(listener);
|
|
break;
|
|
default:
|
|
console.warn(`Event ${event} not found in FixIt Decryptor!`);
|
|
break;
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
window.FixItDecryptor = FixItDecryptor;
|
|
|