# 项目加解密使用说明 本指南介绍了 PPanel Server 项目中使用的加解密机制,主要用于设备端(Device)通信的安全保障。 ## 1. 核心算法 项目使用 **AES-256-CBC** 加密算法。 - **填充方式**:PKCS7 Padding。 - **数据编码**:Base64。 ## 2. 密钥(Key)与初始化向量(IV)生成逻辑 ### 2.1 密钥生成 (Key Generation) 密钥由一个预定义的 `SecuritySecret`(简称 Secret)生成: 1. 对 Secret 进行 **SHA-256** 哈希。 2. 取哈希结果的前 **32 字节** 作为 AES-256 的密钥。 ### 2.2 初始化向量生成 (IV Generation) IV 是动态生成的,以增强安全性: 1. 客户端或服务端生成一个随机字符串(Nonce,通常是纳秒级时间戳)。 2. 对 Nonce 进行 **MD5** 哈希。 3. 将 MD5 结果(十六进制字符串)与 Secret 拼接。 4. 对拼接后的字符串按 2.1 节的方式生成密钥逻辑处理,取结果的前 **16 字节** 作为 IV。 > [!NOTE] > 在 API 通信中,Nonce 字符串通常通过请求参数中的 `time` 字段传递。 ## 3. 身份识别与优先顺序 服务端通过 `Login-Type` 来识别是否需要进行加解密逻辑(值为 `device` 时触发)。 ### 3.1 识别途径 1. **Token 负载 (JWT Claims)**:Token 中包含 `LoginType` (值为 `device`) 和 `DeviceId`。 2. **请求头 (Header)**:`Login-Type: device`。 ### 3.2 优先顺序与场景 - **已登录场景**:服务端优先从 **Token** 负载中读取 `LoginType`。如果 Token 合法且包含 `LoginType: device`,则启用加解密。 - **未登录/登录中场景**:例如 `/v1/auth/login/device` 接口,由于此时没有有效 Token,服务端会检查 **Header** 中的 `Login-Type`。 > [!TIP] > 为了确保一致性,建议在设备端请求中**始终**携带 `Login-Type: device` 请求头,并在登录后确保存储的 Token 负载中也包含对应信息。 ## 4. 中间件应用 (DeviceMiddleware) `DeviceMiddleware` 处理 `Login-Type: device` 的请求: - **请求解密**: - 检查 URL 参数或 JSON Body 中的 `data`(加密数据)和 `time`(Nonce)。 - 使用配置的 Secret 和 Nonce 解密 `data`。 - 将解密后的 JSON 重新注入到请求上下文中。 - **响应加密**: - 拦截响应 Body。 - 加密 Body 中的 `data` 字段。 - 将响应格式化为: ```json { "data": "ENCRYPTED_BASE64_STRING", "time": "NONCE_STRING" } ``` ## 5. Token 负载详情 (JWT Payload) 当 `Login-Type` 为 `device` 时,JWT Token 会包含以下自定义字段: - `LoginType`: `"device"` - `DeviceId`: 设备的数据库唯一 ID。 ## 6. 代码示例 ### Go 语言 (服务端) 参考 [pkg/aes/aes.go](file:///Users/Apple/vpn/ppanel-server/pkg/aes/aes.go) ```go import pkgaes "github.com/perfect-panel/server/pkg/aes" // 加密 encrypt, nonce, err := pkgaes.Encrypt([]byte("plain text"), secret) // 解密 decrypt, err := pkgaes.Decrypt(cipherText, secret, nonce) ``` ### JavaScript (客户端示例) 使用 `crypto-js` 库: ```javascript const CryptoJS = require("crypto-js"); function getIv(nonce, secret) { const md5Nonce = CryptoJS.MD5(nonce).toString(); const ivStr = md5Nonce + secret; const key = CryptoJS.SHA256(ivStr); return CryptoJS.enc.Hex.parse(key.toString().substring(0, 32)); } function getKey(secret) { const key = CryptoJS.SHA256(secret); return CryptoJS.enc.Hex.parse(key.toString().substring(0, 64)); } // 加密示例 const key = getKey(secret); const iv = getIv(nonce, secret); const encrypted = CryptoJS.AES.encrypt("plain text", key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); console.log(encrypted.toString()); // Base64 ``` ## 5. 安全建议 - 请务必在配置文件中修改默认的 `SecuritySecret`。 - 确保 `time` (Nonce) 在每次请求时都是唯一的,以防止重放攻击和频率分析。