Site updated: 2026-05-13 16:50:34
This commit is contained in:
297
lib/hbe.js
Normal file
297
lib/hbe.js
Normal file
@@ -0,0 +1,297 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const cryptoObj = window.crypto || window.msCrypto;
|
||||
const storage = window.localStorage;
|
||||
|
||||
const storageName = 'hexo-blog-encrypt:#' + window.location.pathname;
|
||||
const keySalt = textToArray('hexo-blog-encrypt的作者们都是大帅比!');
|
||||
const ivSalt = textToArray('hexo-blog-encrypt是地表最强Hexo加密插件!');
|
||||
|
||||
// As we can't detect the wrong password with AES-CBC,
|
||||
// so adding an empty div and check it when decrption.
|
||||
const knownPrefix = "<hbe-prefix></hbe-prefix>";
|
||||
|
||||
const mainElement = document.getElementById('hexo-blog-encrypt');
|
||||
const wrongPassMessage = mainElement.dataset['wpm'];
|
||||
const wrongHashMessage = mainElement.dataset['whm'];
|
||||
const dataElement = mainElement.getElementsByTagName('script')['hbeData'];
|
||||
const encryptedData = dataElement.innerText;
|
||||
const HmacDigist = dataElement.dataset['hmacdigest'];
|
||||
|
||||
function hexToArray(s) {
|
||||
return new Uint8Array(s.match(/[\da-f]{2}/gi).map((h => {
|
||||
return parseInt(h, 16);
|
||||
})));
|
||||
}
|
||||
|
||||
function textToArray(s) {
|
||||
var i = s.length;
|
||||
var n = 0;
|
||||
var ba = new Array()
|
||||
|
||||
for (var j = 0; j < i;) {
|
||||
var c = s.codePointAt(j);
|
||||
if (c < 128) {
|
||||
ba[n++] = c;
|
||||
j++;
|
||||
} else if ((c > 127) && (c < 2048)) {
|
||||
ba[n++] = (c >> 6) | 192;
|
||||
ba[n++] = (c & 63) | 128;
|
||||
j++;
|
||||
} else if ((c > 2047) && (c < 65536)) {
|
||||
ba[n++] = (c >> 12) | 224;
|
||||
ba[n++] = ((c >> 6) & 63) | 128;
|
||||
ba[n++] = (c & 63) | 128;
|
||||
j++;
|
||||
} else {
|
||||
ba[n++] = (c >> 18) | 240;
|
||||
ba[n++] = ((c >> 12) & 63) | 128;
|
||||
ba[n++] = ((c >> 6) & 63) | 128;
|
||||
ba[n++] = (c & 63) | 128;
|
||||
j += 2;
|
||||
}
|
||||
}
|
||||
return new Uint8Array(ba);
|
||||
}
|
||||
|
||||
function arrayBufferToHex(arrayBuffer) {
|
||||
if (typeof arrayBuffer !== 'object' || arrayBuffer === null || typeof arrayBuffer.byteLength !== 'number') {
|
||||
throw new TypeError('Expected input to be an ArrayBuffer')
|
||||
}
|
||||
|
||||
var view = new Uint8Array(arrayBuffer)
|
||||
var result = ''
|
||||
var value
|
||||
|
||||
for (var i = 0; i < view.length; i++) {
|
||||
value = view[i].toString(16)
|
||||
result += (value.length === 1 ? '0' + value : value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function getExecutableScript(oldElem) {
|
||||
let out = document.createElement('script');
|
||||
const attList = ['type', 'text', 'src', 'crossorigin', 'defer', 'referrerpolicy'];
|
||||
attList.forEach((att) => {
|
||||
if (oldElem[att])
|
||||
out[att] = oldElem[att];
|
||||
})
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async function convertHTMLToElement(content) {
|
||||
let out = document.createElement('div');
|
||||
out.innerHTML = content;
|
||||
out.querySelectorAll('script').forEach(async (elem) => {
|
||||
elem.replaceWith(await getExecutableScript(elem));
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getKeyMaterial(password) {
|
||||
let encoder = new TextEncoder();
|
||||
return cryptoObj.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
{
|
||||
'name': 'PBKDF2',
|
||||
},
|
||||
false,
|
||||
[
|
||||
'deriveKey',
|
||||
'deriveBits',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function getHmacKey(keyMaterial) {
|
||||
return cryptoObj.subtle.deriveKey({
|
||||
'name': 'PBKDF2',
|
||||
'hash': 'SHA-256',
|
||||
'salt': keySalt.buffer,
|
||||
'iterations': 1024
|
||||
}, keyMaterial, {
|
||||
'name': 'HMAC',
|
||||
'hash': 'SHA-256',
|
||||
'length': 256,
|
||||
}, true, [
|
||||
'verify',
|
||||
]);
|
||||
}
|
||||
|
||||
function getDecryptKey(keyMaterial) {
|
||||
return cryptoObj.subtle.deriveKey({
|
||||
'name': 'PBKDF2',
|
||||
'hash': 'SHA-256',
|
||||
'salt': keySalt.buffer,
|
||||
'iterations': 1024,
|
||||
}, keyMaterial, {
|
||||
'name': 'AES-CBC',
|
||||
'length': 256,
|
||||
}, true, [
|
||||
'decrypt',
|
||||
]);
|
||||
}
|
||||
|
||||
function getIv(keyMaterial) {
|
||||
return cryptoObj.subtle.deriveBits({
|
||||
'name': 'PBKDF2',
|
||||
'hash': 'SHA-256',
|
||||
'salt': ivSalt.buffer,
|
||||
'iterations': 512,
|
||||
}, keyMaterial, 16 * 8);
|
||||
}
|
||||
|
||||
async function verifyContent(key, content) {
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(content);
|
||||
|
||||
let signature = hexToArray(HmacDigist);
|
||||
|
||||
const result = await cryptoObj.subtle.verify({
|
||||
'name': 'HMAC',
|
||||
'hash': 'SHA-256',
|
||||
}, key, signature, encoded);
|
||||
console.log(`Verification result: ${result}`);
|
||||
if (!result) {
|
||||
alert(wrongHashMessage);
|
||||
console.log(`${wrongHashMessage}, got `, signature, ` but proved wrong.`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function decrypt(decryptKey, iv, hmacKey) {
|
||||
let typedArray = hexToArray(encryptedData);
|
||||
|
||||
const result = await cryptoObj.subtle.decrypt({
|
||||
'name': 'AES-CBC',
|
||||
'iv': iv,
|
||||
}, decryptKey, typedArray.buffer).then(async (result) => {
|
||||
const decoder = new TextDecoder();
|
||||
const decoded = decoder.decode(result);
|
||||
|
||||
// check the prefix, if not then we can sure here is wrong password.
|
||||
if (!decoded.startsWith(knownPrefix)) {
|
||||
throw "Decode successfully but not start with KnownPrefix.";
|
||||
}
|
||||
|
||||
const hideButton = document.createElement('button');
|
||||
hideButton.textContent = 'Encrypt again';
|
||||
hideButton.type = 'button';
|
||||
hideButton.classList.add("hbe-button");
|
||||
hideButton.addEventListener('click', () => {
|
||||
window.localStorage.removeItem(storageName);
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
document.getElementById('hexo-blog-encrypt').style.display = 'inline';
|
||||
document.getElementById('hexo-blog-encrypt').innerHTML = '';
|
||||
document.getElementById('hexo-blog-encrypt').appendChild(await convertHTMLToElement(decoded));
|
||||
document.getElementById('hexo-blog-encrypt').appendChild(hideButton);
|
||||
|
||||
// support html5 lazyload functionality.
|
||||
document.querySelectorAll('img').forEach((elem) => {
|
||||
if (elem.getAttribute("data-src") && !elem.src) {
|
||||
elem.src = elem.getAttribute('data-src');
|
||||
}
|
||||
});
|
||||
|
||||
// support theme-next refresh
|
||||
window.NexT && NexT.boot && typeof NexT.boot.refresh === 'function' && NexT.boot.refresh();
|
||||
|
||||
// TOC part
|
||||
var tocDiv = document.getElementById("toc-div");
|
||||
if (tocDiv) {
|
||||
tocDiv.style.display = 'inline';
|
||||
}
|
||||
|
||||
var tocDivs = document.getElementsByClassName('toc-div-class');
|
||||
if (tocDivs && tocDivs.length > 0) {
|
||||
for (var idx = 0; idx < tocDivs.length; idx++) {
|
||||
tocDivs[idx].style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
// trigger event
|
||||
var event = new Event('hexo-blog-decrypt');
|
||||
window.dispatchEvent(event);
|
||||
|
||||
return await verifyContent(hmacKey, decoded);
|
||||
}).catch((e) => {
|
||||
alert(wrongPassMessage);
|
||||
console.log(e);
|
||||
return false;
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
function hbeLoader() {
|
||||
|
||||
const oldStorageData = JSON.parse(storage.getItem(storageName));
|
||||
|
||||
if (oldStorageData) {
|
||||
console.log(`Password got from localStorage(${storageName}): `, oldStorageData);
|
||||
|
||||
const sIv = hexToArray(oldStorageData.iv).buffer;
|
||||
const sDk = oldStorageData.dk;
|
||||
const sHmk = oldStorageData.hmk;
|
||||
|
||||
cryptoObj.subtle.importKey('jwk', sDk, {
|
||||
'name': 'AES-CBC',
|
||||
'length': 256,
|
||||
}, true, [
|
||||
'decrypt',
|
||||
]).then((dkCK) => {
|
||||
cryptoObj.subtle.importKey('jwk', sHmk, {
|
||||
'name': 'HMAC',
|
||||
'hash': 'SHA-256',
|
||||
'length': 256,
|
||||
}, true, [
|
||||
'verify',
|
||||
]).then((hmkCK) => {
|
||||
decrypt(dkCK, sIv, hmkCK).then((result) => {
|
||||
if (!result) {
|
||||
storage.removeItem(storageName);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
mainElement.addEventListener('keydown', async (event) => {
|
||||
if (event.isComposing || event.keyCode === 13) {
|
||||
const password = document.getElementById('hbePass').value;
|
||||
const keyMaterial = await getKeyMaterial(password);
|
||||
const hmacKey = await getHmacKey(keyMaterial);
|
||||
const decryptKey = await getDecryptKey(keyMaterial);
|
||||
const iv = await getIv(keyMaterial);
|
||||
|
||||
decrypt(decryptKey, iv, hmacKey).then((result) => {
|
||||
console.log(`Decrypt result: ${result}`);
|
||||
if (result) {
|
||||
cryptoObj.subtle.exportKey('jwk', decryptKey).then((dk) => {
|
||||
cryptoObj.subtle.exportKey('jwk', hmacKey).then((hmk) => {
|
||||
const newStorageData = {
|
||||
'dk': dk,
|
||||
'iv': arrayBufferToHex(iv),
|
||||
'hmk': hmk,
|
||||
};
|
||||
storage.setItem(storageName, JSON.stringify(newStorageData));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hbeLoader();
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user