初始化

This commit is contained in:
2026-08-14 21:50:48 +08:00
commit d7e382f2e7
114 changed files with 24123 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
package xkaes
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"errors"
"strings"
)
// ========================================================================
// 萧康云医字段加密复刻包(xkaes)
// ========================================================================
// 目的:1:1 复刻 PHP App\Core\DatabaseEncryptor 的 AES-256-CBC 解密逻辑,
// 让 Go Agent 直连 z_xk 库时能正确解出 xk_ai_api_key.api_key 字段。
//
// PHP 端算法(App\Core\DatabaseEncryptor.php):
// 1. 算法:AES-256-CBC
// 2. 密钥派生:key = SHA-256(ENCRYPT_KEY) → 32 字节 raw(不是 hex 字符串!)
// 3. IV:每次加密随机生成 16 字节,前置于密文一起 base64
// 4. padding:PKCS#7(OPENSSL_RAW_DATA 即默认 PKCS#7)
// 5. 编码:标准 base64(带 = padding)
// 6. 前缀:明文用 "xk_ase_256_" 标记,便于识别密文
//
// 密文格式:xk_ase_256_<base64( IV[16] || 密文[PKCS7] )>
//
// 易错点(任一条不对都会解密失败):
// - 密钥不是直接用 ENCRYPT_KEY,而是 SHA-256 取 32 raw 字节
// - IV 不是固定值,base64 解码后前 16 字节切出来当 IV
// - Go 的 CBC Decrypter 不会自动去 padding,要手动去 PKCS#7
// - 不含前缀的串视为未加密旧数据,原样返回(PHP decrypt 第一步就判断)
// ========================================================================
// Prefix 加密字段前缀(与 PHP DatabaseEncryptor 保持一致)
const Prefix = "xk_ase_256_"
// Decrypt 解密用 AES-256-CBC 加密的字符串
//
// 参数:
// cipherText - 形如 "xk_ase_256_<base64(iv||ciphertext)>" 的密文;
// 若不含前缀则视为明文,原样返回(兼容历史未加密数据)
// encryptKey - PHP 端 env('ENCRYPT_KEY') 的原始值(任意长度字符串)
//
// 返回:
// 解密后的明文;任何一步出错返回 error
//
// 注意:
// PHP 端 decryptApiKey 在异常时吞错返回空串,Go 端保留 error 便于排查
func Decrypt(cipherText, encryptKey string) (string, error) {
cipherText = strings.TrimSpace(cipherText)
if cipherText == "" {
// 空串原样返回,与 PHP 一致
return "", nil
}
// 不含前缀 = 未加密的旧数据,直接当明文返回
if !strings.HasPrefix(cipherText, Prefix) {
return cipherText, nil
}
// 去前缀后做标准 base64 解码
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(cipherText, Prefix))
if err != nil {
return "", errors.New("xkaes: base64 解码失败: " + err.Error())
}
// IV 固定 16 字节(AES block size)
// 这里的 16 与 PHP openssl_cipher_iv_length('AES-256-CBC') 一致
if len(raw) < aes.BlockSize {
return "", errors.New("xkaes: 密文长度不足,无法切出 IV")
}
iv := raw[:aes.BlockSize]
body := raw[aes.BlockSize:]
// 密钥派生:SHA-256(encryptKey) 取 32 字节 raw 二进制
digest := sha256.Sum256([]byte(encryptKey))
block, err := aes.NewCipher(digest[:])
if err != nil {
return "", errors.New("xkaes: 创建 AES cipher 失败: " + err.Error())
}
// 密文长度必须是 block size 的整数倍,否则说明密文损坏
if len(body) == 0 || len(body)%block.BlockSize() != 0 {
return "", errors.New("xkaes: 密文不是 block 对齐的,可能已损坏")
}
// CBC 解密(在原 buffer 上就地解密,节省一次拷贝)
dst := make([]byte, len(body))
cipher.NewCBCDecrypter(block, iv).CryptBlocks(dst, body)
// 去 PKCS#7 padding
// Go 的 CBC Decrypter 不会自动去 padding,必须手动去
// PHP 的 OPENSSL_RAW_DATA flag 会自动去 padding,行为差异点在这里
pad := int(dst[len(dst)-1])
if pad <= 0 || pad > block.BlockSize() || pad > len(dst) {
return "", errors.New("xkaes: 无效的 PKCS#7 padding,密钥可能不对或密文损坏")
}
// 校验 padding 字节(PKCS#7 要求末尾 pad 个字节都等于 pad)
for i := len(dst) - pad; i < len(dst); i++ {
if int(dst[i]) != pad {
return "", errors.New("xkaes: PKCS#7 padding 字节不一致")
}
}
return string(dst[:len(dst)-pad]), nil
}