睡觉做着美梦半途吵醒,精神状态感人

于是起来开发了几个小时的 AES 前端加密 Hugo 页面

故而分享简略教程
新建 python 文件

首先 pip install pycryptodome

文件内容:

import os
import re
import base64
import json
import shutil
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
from Crypto.Hash import HMAC, SHA256

# --- 配置 ---
TARGET_DIR = "./public/"
FILE_EXTENSIONS = ('.html', '.htm')

# --- 加密并附带 HMAC 的函数 ---
def encrypt_and_sign_aes(plaintext: str, key: str) -> str:
    """
    使用AES加密文本,并使用HMAC签名。
    密钥会被分成两部分:一部分用于AES,另一部分用于HMAC。
    返回Base64编码的 {iv, ciphertext, hmac} JSON对象。
    """
    # 1. 处理主密钥:使用SHA-256派生出两个不同的子密钥
    master_key = key.encode('utf-8')
    key_hash = SHA256.new(master_key).digest() # 32 bytes
    aes_key = key_hash[:16]  # AES-128 key
    hmac_key = key_hash[16:] # HMAC key

    # 2. AES 加密
    cipher = AES.new(aes_key, AES.MODE_CBC)
    padded_text = pad(plaintext.encode('utf-8'), AES.block_size)
    ciphertext = cipher.encrypt(padded_text)
    
    # 3. 准备要签名的数据 (IV + ciphertext)
    data_to_sign = cipher.iv + ciphertext
    
    # 4. 生成 HMAC-SHA256 签名
    h = HMAC.new(hmac_key, digestmod=SHA256)
    h.update(data_to_sign)
    signature = h.hexdigest()

    # 5. 将 IV, ciphertext, signature 打包成 JSON 并 Base64 编码
    package = {
        "iv": base64.b64encode(cipher.iv).decode('utf-8'),
        "ciphertext": base64.b64encode(ciphertext).decode('utf-8'),
        "hmac": signature
    }
    return base64.b64encode(json.dumps(package).encode('utf-8')).decode('utf-8')

# --- 主处理逻辑 ---
def process_file(file_path: str):
    print(f"Processing file: {file_path}")
    # backup_path = file_path + ".bak"
    # if not os.path.exists(backup_path):
    #     shutil.copy2(file_path, backup_path)
    #     print(f"  Backup created: {backup_path}")
    # else:
    #     print(f"  Backup already exists: {backup_path}")

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
    except UnicodeDecodeError:
        print(f"  Warning: Could not read {file_path} as UTF-8. Skipping.")
        return

    pattern = re.compile(
        r'<PYTHONENCRYPT\s+key="([^"]*?)">\s*(.*?)\s*</PYTHONENCRYPT>',
        re.DOTALL | re.IGNORECASE
    )

    def replace_match(match):
        key = match.group(1)
        html_content = match.group(2)
        if not key:
            print(f"  Warning: Found block without key in {file_path}. Skipping block.")
            return match.group(0)
        try:
            # 使用新的带签名的加密函数
            encrypted_package_b64 = encrypt_and_sign_aes(html_content, key)
            return f'<script>let aesed="{encrypted_package_b64}"</script>'
        except Exception as e:
            print(f"  Error encrypting block in {file_path}: {e}")
            return match.group(0)

    new_content = pattern.sub(replace_match, content)

    if new_content != content:
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(new_content)
        print(f"  File updated: {file_path}")
    else:
        print(f"  No matching blocks found or no changes made.")

if __name__ == "__main__":
    original_cwd = os.getcwd()
    target_path = os.path.join(original_cwd, TARGET_DIR)
    if not os.path.isdir(target_path):
        print(f"Error: Target directory '{target_path}' does not exist.")
        exit(1)
    os.chdir(target_path)
    print(f"Changed working directory to: {os.getcwd()}")
    for root, dirs, files in os.walk('.'):
        for file in files:
            if file.endswith(FILE_EXTENSIONS):
                full_path = os.path.join(root, file)
                display_path = full_path[2:] if full_path.startswith('./') else full_path
                process_file(display_path)
    print("\nAll files processed with HMAC.")
修改 baseof.html 模板

找到 {{ block "main" . }} 的位置:

(根据里面的注释仔细修改):

{{/*  加密判断  */}}
{{ if {{/*  判断需要加密的页面  */}}
}}

{{ $key := .Site.Params.ssap }}
<PYTHONENCRYPT key="{{ $key }}">
{{ block "main" . }} {{ end }}
</PYTHONENCRYPT>
<div id="protect-box" class="post">
<noscript>
    <p class="error">请启用JavaScript以查看内容。</p>
</noscript>
<style>
    .key-form {
        padding-top: 2rem;
        margin-top: 2em;
        position: relative;
        overflow: hidden;
    }
    .key-form input {
        height: 2rem;
        width: calc(100% - 4rem);
        border: none;
        border-bottom: 0.1rem solid var(--color-secondary);
        outline: none;
        vertical-align: middle;
        font-size: 1rem;
        background-color: unset !important;
        color: var(--color);
        transition: all 0.3s ease;
    }

    .key-form label {
        position: absolute;
        top: 0;
        left: 0;
        pointer-events: none;
        width: calc(100% - 4rem);
    }

    .key-form label span {
        position: absolute;
        bottom: -4.2rem;
        left: 0;
        transition: all 0.3s ease;
    }

    .key-form input:focus+label span,
    .key-form input:valid+label span {
        transform: translateY(-100%);
        font-size: 1.5rem;
        color: #5984d9;
    }

    .key-form input:valid,
    .key-form input:focus {
        border-color: #5984d9;
    }
</style>
<form class="key-form">
    <input id="key-query" required />
    <label for="key-query">
        <span>Words from the bottom of your heart.</span>
    </label>
    <button style="padding: 0.7rem; display: inline-flex; font-size: 1rem; font-family: unset;color: var(--color);" id="submit-button">🧨</button>
</form>
<p id="res-box"></p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script>
    async function decryptAndVerifyAES(encryptedPackageB64, password) {
        try {
            // 1. 解析 Base64 -> JSON -> 各个组件
            const jsonStr = atob(encryptedPackageB64);
            const packageObj = JSON.parse(jsonStr);
            
            const ivWA = CryptoJS.enc.Base64.parse(packageObj.iv);
            const ciphertextWA = CryptoJS.enc.Base64.parse(packageObj.ciphertext);
            const receivedHmacHex = packageObj.hmac;

            // 2. 处理密钥:派生 AES key 和 HMAC key
            const encoder = new TextEncoder();
            const masterKeyU8 = encoder.encode(password);
            const masterKeyWA = CryptoJS.lib.WordArray.create(masterKeyU8);
            const keyHashWA = CryptoJS.SHA256(masterKeyWA);
            const keyHashHex = keyHashWA.toString(CryptoJS.enc.Hex);
            
            const aesKeyHex = keyHashHex.substring(0, 32); // 16 bytes -> 32 hex chars
            const hmacKeyHex = keyHashHex.substring(32);  // 16 bytes -> 32 hex chars
            
            const aesKeyWA = CryptoJS.enc.Hex.parse(aesKeyHex);
            const hmacKeyWA = CryptoJS.enc.Hex.parse(hmacKeyHex);

            // 3. 验证 HMAC
            const dataToSignWA = CryptoJS.lib.WordArray.create(
                ivWA.words.concat(ciphertextWA.words),
                ivWA.sigBytes + ciphertextWA.sigBytes
            );
            const computedHmac = CryptoJS.HmacSHA256(dataToSignWA, hmacKeyWA);
            const computedHmacHex = computedHmac.toString(CryptoJS.enc.Hex);

            if (computedHmacHex !== receivedHmacHex) {
                throw new Error("HMAC verification failed! Data may be tampered or key is wrong.");
            }

            // 4. HMAC 验证通过,开始解密
            const decryptor = CryptoJS.AES.decrypt(
                { ciphertext: ciphertextWA },
                aesKeyWA,
                {
                    iv: ivWA,
                    mode: CryptoJS.mode.CBC,
                    padding: CryptoJS.pad.Pkcs7
                }
            );

            const decryptedText = decryptor.toString(CryptoJS.enc.Utf8);
            
            // 5. 额外检查:解密后的内容是否是有效的 UTF-8 (可选)
            if (!decryptedText || decryptedText.includes('\uFFFD')) {
                throw new Error("Decryption produced invalid UTF-8. Key might be wrong.");
            }

            return decryptedText;

        } catch (error) {
            console.error("Decryption/Verification failed:", error);
            return `Decryption failed: ${error.message}`;
        }
    }

    const btne = document.getElementById('submit-button');
    const resb = document.getElementById('res-box');
    let decryptedHtml = '';
    async function renderThis(secretKey) {
    // 1. 获取加密后的数据
    const encryptedData = aesed; // 这是从你的 <script> 标签中读取的变量

    decryptedHtml = await decryptAndVerifyAES(encryptedData, secretKey);
    if (decryptedHtml.startsWith("Decryption failed:")) {
        resb.innerText = decryptedHtml;
    } else {
        localStorage.setItem("keytime", Date.now());
        document.querySelector('.content').innerHTML = decryptedHtml;
        {{/*  Array.from(document.querySelector('.content').querySelectorAll('script')).forEach(oldScriptEl => {
        // 创建一个新的<script>节点:
        const newScriptEl = document.createElement('script');
        // 复制attributes:
        Array.from(oldScriptEl.attributes).forEach(attr => {
            newScriptEl.setAttribute(attr.name, attr.value);
        });
        // 复制text:
        const scriptText = document.createTextNode(oldScriptEl.innerHTML);
        // 在原始位置替换原始<script>节点:
        newScriptEl.appendChild(scriptText);
        oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);
        });  */}}
        const scripts = Array.from(document.querySelector('.content').querySelectorAll('script'));

        // 排序:带 src 的放前面
        scripts.sort((a, b) => {
            const aHasSrc = a.hasAttribute('src');
            const bHasSrc = b.hasAttribute('src');

            // 如果 a 有 src 而 b 没有,则 a 应该排在前面
            if (aHasSrc && !bHasSrc) return -1;
            // 如果 b 有 src 而 a 没有,则 b 应该排在前面
            if (!aHasSrc && bHasSrc) return 1;
            // 如果两者都有或都没有 src,则保持原有顺序(稳定排序)
            return 0;
        });

        function loadScript(index) {
            if (index >= scripts.length) return; // 所有脚本已处理完毕

            const oldScriptEl = scripts[index];
            const newScriptEl = document.createElement('script');

            // 复制 attributes
            Array.from(oldScriptEl.attributes).forEach(attr => {
                newScriptEl.setAttribute(attr.name, attr.value);
            });

            if (oldScriptEl.src) {
                // 对于外部脚本,等待加载完成后再处理下一个
                newScriptEl.onload = newScriptEl.onerror = () => {
                    loadScript(index + 1); // 加载下一个脚本
                };
            } else {
                // 对于内联脚本,直接设置内容并立即处理下一个
                newScriptEl.text = oldScriptEl.textContent;
            }

            // 替换旧的 script 标签
            oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);

            // 如果是内联脚本,手动触发下一个加载
            if (!oldScriptEl.src) {
                loadScript(index + 1);
            }
        }

        // 开始加载第一个脚本
        loadScript(0);
    }
    
    }
    if (localStorage.getItem("keytime")) {
    if (Date.now() - localStorage.getItem("keytime") >= 604800000) { //过期
        localStorage.removeItem("pagekey");
        localStorage.removeItem("keytime");
    } else { //没过期
        var keykey=localStorage.getItem("pagekey");
        renderThis(keykey);
    }
    } else {
    localStorage.removeItem("pagekey");
    btne.addEventListener('click', function(e) {
        localStorage.setItem("pagekey", document.getElementById('key-query').value);
        var keykey=localStorage.getItem("pagekey");
        renderThis(keykey);
        e.preventDefault();
    });
    }
</script>
</div>

{{ else }}
{{ block "main" . }} {{ end }}
{{ end }}
测试

生成后运行 python 该 python 文件名.py 测试

完成!

- Total words: 1573 -