初始化
This commit is contained in:
108
internal/security/xkaes/xkaes.go
Normal file
108
internal/security/xkaes/xkaes.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user