前言
由于备案到期外加服务器也即将到期,故将博客迁移到静态pages上,具体的迁移过程也足够写一篇blog来介绍了
最终选用的框架是zola + cloudflare pages
折腾zola的过程中发现,好像在代码层面并没有实现加密功能,最多只有一个draft
,草稿功能。
于是决定自己动手写一个静态的加密,具体的实现效果,下面就是一个例子,密码114514
:
下面分享一下具体实现这个功能的过程
加解密算法
之前在GitHub page上使用过一段时间的hexo静态博客框架,这个框架由于是使用前端统一构建,所以可以基于js写一堆plugins,其中加密插件我见过写的比较好的就是hexo-blog-encrypt,所以使用同样的AES-CBC算法,iv使用sha256后的key
具体的实现方式就是在前端js里面实现解密
function aesDecrypt(data, key) {
if (key.length > 32) key = key.slice(0, 32)
const cypherKey = CryptoJS.enc.Utf8.parse(key)
CryptoJS.pad.ZeroPadding.pad(cypherKey, 4)
const iv = CryptoJS.SHA256(key).toString()
const cfg = { iv: CryptoJS.enc.Utf8.parse(iv) }
const decrypted = CryptoJS.AES.decrypt(data, cypherKey, cfg)
return decrypted.toString(CryptoJS.enc.Utf8)
}
但是,怎么做到在zola进行模板渲染的时候,就能够获取AES加密后的文章数据呢?
Cloudflare Pages Build
在cloudflare pages的config页面,可以自定义构建启动命令,比如我的启动命令目前就是zola build
,会将页面构建到public
文件夹下,Cloudflare Page就会自动渲染这个文件夹下的页面。
具体的构建环境,也可以配置,目前官方默认使用的v2版本就自带各种语言
让我们回到正题,如何在zola build的前后任意一个时间完成加密文章的加密处理
我这里的操作是,在zola项目的根目录写上一个golang的源码init.go
,具体的逻辑就是对需要加密的文件进行aes加密,文件的第一行默认是aes的key,然后对之后的数据进行加密处理,并写会原文件。
在执行go run init.go
之后,再执行zola build
,就可以实现在zola运行之前就将文章进行加密了,这样操作直接使用go run init.go && zola build
的构建命令就可以完成了。具体的golang代码如下
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
type PaddingMode string
const PKCS5 PaddingMode = "PKCS5"
const PKCS7 PaddingMode = "PKCS7"
const ZEROS PaddingMode = "ZEROS"
func Padding(padding PaddingMode, src []byte, blockSize int) []byte {
switch padding {
case PKCS5:
src = PKCS5Padding(src, blockSize)
case PKCS7:
src = PKCS7Padding(src, blockSize)
case ZEROS:
src = ZerosPadding(src, blockSize)
}
return src
}
func UnPadding(padding PaddingMode, src []byte) ([]byte, error) {
switch padding {
case PKCS5:
return PKCS5UnPadding(src)
case PKCS7:
return PKCS7UnPadding(src)
case ZEROS:
return ZerosUnPadding(src)
}
return src, nil
}
func PKCS5Padding(src []byte, blockSize int) []byte {
return PKCS7Padding(src, blockSize)
}
func PKCS5UnPadding(src []byte) ([]byte, error) {
return PKCS7UnPadding(src)
}
func PKCS7Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
func PKCS7UnPadding(src []byte) ([]byte, error) {
length := len(src)
if length == 0 {
return src, fmt.Errorf("src length is 0")
}
unpadding := int(src[length-1])
if length < unpadding {
return src, fmt.Errorf("src length is less than unpadding")
}
return src[:(length - unpadding)], nil
}
func ZerosPadding(src []byte, blockSize int) []byte {
rem := len(src) % blockSize
if rem == 0 {
return src
}
return append(src, bytes.Repeat([]byte{0}, blockSize-rem)...)
}
func ZerosUnPadding(src []byte) ([]byte, error) {
for i := len(src) - 1; ; i-- {
if src[i] != 0 {
return src[:i+1], nil
}
}
}
func AesSimpleEncrypt(data, key string) string {
key = trimByMaxKeySize(key)
keyBytes := ZerosPadding([]byte(key), aes.BlockSize)
return AesCBCEncrypt(data, string(keyBytes), GenIVFromKey(key), PKCS7)
}
func AesCBCEncrypt(data, key, iv string, paddingMode PaddingMode) string {
block, err := aes.NewCipher([]byte(key))
if err != nil {
return ""
}
src := Padding(paddingMode, []byte(data), block.BlockSize())
encryptData := make([]byte, len(src))
mode := cipher.NewCBCEncrypter(block, []byte(iv))
mode.CryptBlocks(encryptData, src)
return base64.StdEncoding.EncodeToString(encryptData)
}
func AesSimpleDecrypt(data, key string) string {
key = trimByMaxKeySize(key)
keyBytes := ZerosPadding([]byte(key), aes.BlockSize)
return AesCBCDecrypt(data, string(keyBytes), GenIVFromKey(key), PKCS7)
}
func AesCBCDecrypt(data, key, iv string, paddingMode PaddingMode) string {
block, err := aes.NewCipher([]byte(key))
if err != nil {
return ""
}
decodeData, _ := base64.StdEncoding.DecodeString(data)
decryptData := make([]byte, len(decodeData))
mode := cipher.NewCBCDecrypter(block, []byte(iv))
mode.CryptBlocks(decryptData, decodeData)
original, _ := UnPadding(paddingMode, decryptData)
return string(original)
}
func GenIVFromKey(key string) (iv string) {
hashedKey := sha256.Sum256([]byte(key))
return trimByBlockSize(hex.EncodeToString(hashedKey[:]))
}
func trimByBlockSize(key string) string {
if len(key) > aes.BlockSize {
return key[:aes.BlockSize]
}
return key
}
func trimByMaxKeySize(key string) string {
if len(key) > 32 {
return key[:32]
}
return key
}
func Enc(folderPath string) {
if folderPath == "" {
return
}
if err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return err
}
cont := string(content)
conts := strings.SplitN(cont, "\n", 2)
key := conts[0]
cont = conts[1]
aesedContent := AesSimpleEncrypt(cont, key)
err = os.WriteFile(path, []byte(aesedContent), info.Mode())
if err != nil {
return err
}
}
return nil
}); err != nil {
panic(err)
}
}
func main() {
Enc("encrypt")
}
来一段可以运行演示的代码:(至于这段代码我是如何做到能够动态运行的,之后再用一篇文章详细介绍)
上述代码可以实现AES-CBC的加解密操作,这里只用到了加密操作。
运行又encrypt
文件夹下的所有文件都被AES加密了一次,但是如何使用这些加密后的数据呢?
shortcode
zola有shortcode这样的功能,于是就可以配合golang加密后的文件进行操作
在zola的内置function中,有一个load-data,可以访问远程资源或本地资源,于是可以通过这个函数去获取加密后的AES数据,之后通过JS的方式进行数据的解密。
最终的shortcode
{% if src %}
{% set data = load_data(path="encrypt/" ~ src) %}
{% set hash = get_hash(literal=src, sha_type=256) %}
<div id="secret-content-{{ hash }}">
<p><code>{% if msg %}{{ msg }}{% else -%} 以下内容已被加密,请输入密码后查看 {%- endif %}</code></p>
<input id="pwd-{{hash}}" class="mo-input" name="password" type="password" autocomplete="new-password"
placeholder="password" aria-label="password">
<input id="data-{{ hash }}" name="data" type="hidden" value="{{ data }}">
<button type="submit" style="display: inline-flex;" onclick="decryptContent('{{hash}}')">解密</button>
</div>
<div id="decrypted-content-{{ hash }}"></div>
{% endif %}
这里的decryptContent函数的具体实现写在全局js中了
function decryptContent(hash) {
const pwd = document.getElementById(`pwd-${hash}`).value
const data = document.getElementById(`data-${hash}`).value
if (!pwd || !data) return
const res = aesDecrypt(data, pwd)
if (!res) {
document.getElementById(`pwd-${hash}`).value = ''
return
}
document.getElementById(`secret-content-${hash}`).remove()
document.getElementById(`decrypted-content-${hash}`).innerHTML += marked.parse(res)
}
至于为什么需要用到get-hash这样的函数,是因为如果一篇文章中需要加密多段数据,需要唯一的id去让js顺利找到标签,随后将解密的数据给写上去。
同时,由于本地的数据基本都是用md编写的,需要在前端进行md解析的操作,这里用到的是marked
具体的使用方式,例如本文第一段实现的加密,在md文件中的使用方式就是
\{\{ encrypt(src="test.md", msg="Encrypt Demo") \}\}
而test.md
中具体的内容就是
114514
`简单的加密测试...`
第一行为加密使用的key,第二行就是内容。
至此,一个简单的加密shortcode
就完成了,可以在任意文章中加密任意次数的数据