修改博客加密插件hexo-blog-encrypt 3.1.9,适配移动端

hexo-blog-encrypt 3.1.9是一个用于给博客文章加密的插件,它可以让你在发布文章之前对文章内容进行加密,只有加密后的文章才可以被阅读。但官方的有部分问题,这里提供一些解决方法,希望能帮助到大家。

1. 移动端输入法无法输入enter

由于输入法无法输入enter,导致在手机端无法输入密码解锁博客:
alt text
这里增加一个confirm按钮,用户点击确认按钮后,和enter一样也能触发解锁逻辑,分别修改三个文件:

  • node_modules/hexo-blog-encrypt/lib/hbe.blink.html
html
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="hbe hbe-container" id="hexo-blog-encrypt" data-wpm="{{hbeWrongPassMessage}}" data-whm="{{hbeWrongHashMessage}}">
<script id="hbeData" type="hbeData" data-hmacdigest="{{hbeHmacDigest}}">{{hbeEncryptedData}}</script>
<div class="hbe hbe-content">
<div class="hbe hbe-input hbe-input-blink">
<input class="hbe hbe-input-field hbe-input-field-blink" type="password" id="hbePass">
<label class="hbe hbe-input-label hbe-input-label-blink" for="hbePass">
<span class="hbe hbe-input-label-content hbe-input-label-content-blink" data-content="{{hbeMessage}}">{{hbeMessage}}</span>
</label>
</div>
<!-- 添加确认按钮 -->
<button id="hbeConfirmBtn" class="hbe hbe-confirm-btn">Confirm</button>
</div>
</div>
  • node_modules/hexo-blog-encrypt/lib/hbe.js
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
(() => {
'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));
});
});
}
});
}
});
// 监听确认按钮点击
const confirmButton = document.getElementById('hbeConfirmBtn');
confirmButton.addEventListener('click', async () => {
const password = document.getElementById('hbePass').value;

try {
const keyMaterial = await getKeyMaterial(password);
const hmacKey = await getHmacKey(keyMaterial);
const decryptKey = await getDecryptKey(keyMaterial);
const iv = await getIv(keyMaterial);

const result = await decrypt(decryptKey, iv, hmacKey);
console.log(`Decrypt result: ${result}`);

if (result) {
const dk = await cryptoObj.subtle.exportKey('jwk', decryptKey);
const hmk = await cryptoObj.subtle.exportKey('jwk', hmacKey);

const newStorageData = {
'dk': dk,
'iv': arrayBufferToHex(iv),
'hmk': hmk,
};
storage.setItem(storageName, JSON.stringify(newStorageData));
}
} catch (error) {
console.error('Error during decryption process:', error);
}
});

}

hbeLoader();

})();
  • node_modules/hexo-blog-encrypt/lib/hbe.style.css
    修改添加以下内容
css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* .hbe-content {
text-align: center;
font-size: 150%;
padding: 1em 0;
} */

/* 自行添加和修改的样式,增加confirm按钮,开始 */
.hbe-content {
text-align: center;
font-size: 150%;
padding: 1em 0;
display: flex;/* 使用 flex 布局 */
justify-content: center;/* 水平居中输入框和按钮 */
align-items: center;/* 垂直居中输入框和按钮 */
gap: 10px;/* 输入框和按钮之间的间距 */
}
.hbe-confirm-btn {
background-color: #009FF5;
color: green;
border: none;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-top: 10px;
}
.hbe-confirm-btn:hover {
background-color: #007bb5;
}
/* 自行添加和修改的样式,增加confirm按钮,结束 */

2.输入法候选字选择期持续触发检测弹窗

输入法候选字选择期持续触发检测函数,体验不好,这是一个代码上的逻辑bug,修改:

  • node_modules/hexo-blog-encrypt/lib/hbe.js
    修改enter监听函数
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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));
});
});
}
});
}
});

3. 最终效果

alt text

4. 补充更新

通过修改nodemoudle的内容,本地运行成功,但远程部署时,npm会重新安装并覆盖nodemoudle的内容,导致修改不能生效,对原项目提了issue但不知道多久更新,因此临时发布了一个npm包(hexo-blog-encrypt-plus,第一次发布npm包,算是练手),用于解决这个问题,后期官方更新后我再删除。
只需:

plaintext
1
npm install hexo-blog-encrypt-plus --save

然后在_config.yml中配置:

plaintext
1
2
encrypt:
enable: true

再参照原作者教程配置即可使用