风云变换#
一直以来,咱认为,咱的博客隐藏层层互联网覆盖下的,
即使咱有提交搜索引擎收录请求想必也是难以被检索到的,
于是咱向来都是有的没得都往博客上水。
但是在某天,咱水群的时候,突然被问起,这个是不是你的博客?
咱顿时汗流浃背,仿佛群友下一秒就要出现在咱家门口了。
同时,近日咱正好打算创作一些可能不太适合完全公开的内容,
遂趁此机会为博客添加一个简单的加密功能。
咱的博客是基于 Hugo 的纯静态页面,这意味着当打开页面时,
被加密的信息已经发送到用户的设备上了,所以解密也需要在用户设备上进行。
这意味着需要在页面上存储加密后的密文,同时还需要存在一个解密逻辑。
那如何加密呢?
Hugo 是从 Markdown 文件生成静态页面的。所以实现加密有如下几个思路:
- 在源 Markdown 里加密。
- 在生成的 Html 里加密。
- 让Hugo在生成的过程中加密。
首先,咱排除了第一种方案,因为这样会导致源文件被加密,这对于后续的修改文章内容会造成困扰。
第三种方案需要编写 Go 模块来实现,而 Hugo 使用 Go 模块需要将博客也转成模块的形式,
将现有的博客转为模块的形式是一个大工程遂放弃。于是只能使用第二种方案来实现加密了。
为了实现第二种方案的效果,需要三个部分:
- Hugo 生成带有的标识的需要加密的 Html 元素和相关的加密信息。
- Hugo 嵌入相关的解密逻辑。
- 使用外部工具加密 Html。
至于加密算法的选择方面,咱选择了 AES-256-CBC-PKCS7,一个常见的对称加密算法。
有了清晰的逻辑实现起来就不难了,只需要找到各个功能的实现方法然后拼凑在一起就好了。
特征 Html 生成#
首先是 Hugo 这边的特征元素生成,咱使用 Hugo 的 Shortcode 来实现。
具体代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| {{ $passphrase := "" }}
{{- if .IsNamedParams }}
{{ $passphrase = (.Get "passphrase") | default "" }}
{{- else}}
{{ $passphrase = (.Get 0) | default ""}}
{{- end}}
{{- if .Page.Draft }}
{{- with .Inner }}
<div class="encrypted-block" data-unencrypted-content="{{ . | markdownify | base64Encode }}" data-passphrase="{{ $passphrase }}">
{{ . | markdownify }}
</div>
{{- end }}
{{- else }}
{{- with .Inner }}
<div class="encrypted-block" data-unencrypted-content="{{ . | markdownify | base64Encode }}" data-passphrase="{{ $passphrase }}">
<div class="decrypt-pad">
<input type="password" class="decrypt-passphrase" placeholder="{{i18n "input_passphrase"}}"/>
<button class="decrypt-button" onclick="decrypt_encryped_content(this)">{{i18n "decrypt"}}</button>
</div>
</div>
{{- end }}
{{- end}}
|
为了防止 Hugo 自带的 Summary 或者主题自带的特性意外将明文暴露,
这里将明文和口令存储在 html 标签的 data-* 区域中用于确保不会被处理。
同时为了方便的在编写时能较好的预览,特殊处理页面是草稿的情况。
解密逻辑引入#
由于现代浏览器并没有内置的 AES 加解密功能,同时由于 Hugo 也没有提供相关的能力,
所以只能导入外部的库来实现解密逻辑,这里选用 CryptoJS 了支持这个功能。
首先是如何引入 CryptoJS,这里涉及到 Hugo 对于外部资源文件的管理逻辑,
由于咱不太希望自己的博客过于依赖外部 CDN 的资源,所以使用了如下的方式来导入 CryptoJS。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| {{- $cryptoJS := resources.Get "js/crypto-js.js" -}}
{{- with try (resources.GetRemote "https://cdn.jsdelivr.net/npm/crypto-js/crypto-js.js") }}
{{ with .Err }}
{{ errorf "%s" .}}
{{ else with .Value }}
{{ $cryptoJS = . | resources.FromString "js/crypto-js.js" }}
{{ end }}
{{- end }}
{{ $cryptoJS = $cryptoJS | fingerprint }}
<script
defer
crossorigin="anonymous"
id="CryptoJS-script"
src="{{ $cryptoJS.RelPermalink }}"
integrity="{{ $cryptoJS.Data.Integrity }}"
></script>
|
然后编写,对应的解密 Javascript 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| <script>
function decrypt_encryped_content(button) {
const block = button.closest('.encrypted-block');
const encrypted_data = block.dataset.encryptedData;
const passphrase = block.querySelector('.decrypt-passphrase').value.trim();
try {
const bytes = CryptoJS.AES.decrypt(encrypted_data, passphrase);
const decrypted_content = bytes.toString(CryptoJS.enc.Utf8);
const html_doc = CryptoJS.enc.Utf8.stringify(CryptoJS.enc.Base64.parse(decrypted_content));
if(decrypted_content) {
block.innerHTML = html_doc;
}
} catch (err) {
alert('Error decrypting content: ' + err.message);
}
}
</script>
|
至此,解密部分的所有组件就完成了,剩下的就是加密的部分的编写了。
加密 Html#
为了方便起见,咱选择使用 Python 脚本来实现 Html 中特征元素的加密。
不过这里有个小困惑点,那就是 CryptoJS 的 AES 默认解密函数只需要传入密文和口令就可以实现解锁,
其对于口令长度并没有限制,而 AES 加密算法中对于密钥的长度是有明确限制的。
同时对于初始化向量的选择没有任何头绪。
于是在一番网上的调查后,
发现这篇文章
[1]
中讲到了如何多语言兼容 CryptoJS 的 AES 的加解密算法。
遂有下面的 Python 实现的加密算法。
顺带展现一下加密功能的实现,口令:ciallo
。
最后将一些新引入的 Html 元素编写相应的 CSS 使其和之前的主题契合,
就完成了这个简单的功能。不过的实现还有一个缺点,那就是无法在运行
hugo server -D
时实时测试加密的内容。
最最后,如果你在咱的博客中看到了加密的但是想看的内容,
可以联系咱,或者尝试自己发现密钥!
References#
[1] 前后端对接,多语言实现 CryptoJS 的 AES 简单加密解密