Files
encryptx/REVIEW.md
T
2026-09-30 15:49:12 +08:00

29 KiB
Raw Blame History

encryptx 代码评审报告

修复状态:全部 P0 / P1 / P2 问题已处理完毕

本报告记录的是修复前的状态,保留作为审计依据。修复后的验证结果:

检查项 修复前 修复后
go build ./... 通过 通过
go vet ./... 通过 通过
gofmt -l . 3 个文件未格式化 干净
go test ./... 失败(2 个包 panic) 27 个包全部 ok
govulncheck ./... GO-2026-5932(无修复版本) 0 个可达漏洞
测试包数为 0 的包 4 个 0 个
算法种类 25 个 EncryptType 50 个 EncryptType + 6 个 SignType

逐条对应

编号 问题 处理方式
P0-1 BLOWFISH_* 枚举空实现 新建 blowfishx 包,补齐 ECB/CBC/CFB/OFB/CTR 与密钥生成
P0-2 GenerateKey 输出无法用作 key 密钥契约统一为 []byte,全库不再有字符串密钥
P0-3 ECB 解密 panic(可远程 DoS) 抽取 cipherx,所有模式先校验长度再切片,并新增回归测试
P0-4 PBKDF2 盐值被丢弃 DeriveKey 同时返回密钥与盐值;Encrypt 路径要求显式给盐
P0-5 rsax 以 panic 代替 error 全量重写:0 处 panic、OAEP、PSS、长明文分块、受检类型断言
P0-6 go test ./... 失败 全绿
P1-1 无完整性保护 + padding oracle 新增 ChaCha20-Poly1305 / XChaCha20;填充错误统一为单一 ErrInvalidPadding;不推荐模式全部加安全警告
P1-2 utilsx SHA-1 口令哈希 删除 HashPassword/VerifyPassword/CalculateHash,改用 bcryptx 与新建的 argon2x
P1-3 废弃的 openpgp gpgx 迁移到 github.com/ProtonMail/go-crypto,govulncheck 归零
P1-4 弱算法无风险标注 新增 SecurityLevel() / IsDeprecated() / IsAEAD() / Note() 元数据,覆盖全部算法
P1-5 缺现代密码学原语 新增 ChaCha20、Ed25519、ECDSA、RSA-OAEP、HKDF、scrypt、Argon2id、常量时间比较、SHA-224/384、SHA3-224、BLAKE2b/s、HMAC-SHA1/384、hexx
P1-6 GCM 密文含换行、靠吞错误工作 输出改为干净的 密文:nonce,无空白字符,并加回归测试
P2-1 三个对称包 95% 重复 抽取 cipherx.Suite:777 行重复代码压缩为各包约 60 行包装
P2-2 两套行为不同的 CBC 实现 删除 AESCBCEncrypt / AESCBCDecrypter
P2-3 OFB 未暴露 已暴露到门面(AES_OFB 等),并补齐 CTR
P2-4 map[string]interface{} 静默降级 Options 严格校验:未知键、类型不符一律报错
P2-5 EncryptType 是裸 int 显式赋值(原有 0–24 不变)、String() / Valid() / 元数据
P2-6 错误无法判别 全库 %w 包装,导出哨兵错误(ErrInvalidKeySize、ErrAuthenticationFailed 等)
P2-7 PKCS7 校验过宽 PKCS7Unpad 接收 blockSize 并校验 padding <= blockSize
测试 覆盖率为 0 的包有 4 个、断言缺失 27 个包全部有测试,覆盖率 65%–100%,含 RFC 向量与错误路径
工程化 无 README/LICENSE/.gitignore/CI 全部补齐;go.mod 升级到 go 1.26 + x/crypto v0.57.0

仍待决策的一项

国密算法(SM2/SM3/SM4)未实现:Go 标准库与 x/crypto 均不提供,需要引入第三方依赖(如 github.com/tjfoc/gmsm)。是否引入属于依赖策略决策,未擅自添加。若模块路径 code.yun.ink 意味着有国密合规要求,请告知,我可以按现有包的风格补齐。

其他明确未纳入的算法:BLAKE3、AES-GCM-SIV、Twofish、CAST5。它们都不属于「现代默认选项」,且同样需要额外依赖。


二次复查(修复完成后再做的一轮对抗性检查)

第一轮修复后,用「不看注释、只看行为」的方式重新检查了一遍:静态通读 + 行为探针实测 + staticcheck 全量静态分析 + Go 原生 fuzz。又发现并修复了 9 个问题。

二次复查发现的问题

# 问题 严重性 处理
1 bcryptx.VerifyPassword 静默截断超长口令:实测一个 100 字节口令通过了「前 72 字节相同」的哈希校验,而 HashPassword 却拒绝——两条路径不对称,等于凭空扩大了可接受的口令集合 高(认证缺陷) 校验路径同样拒绝 >72 字节并返回新增的 ErrPasswordTooLong;补回归测试
2 rsax 在解析与使用既有密钥时也强制 2048 位下限:持有旧 1024 位密钥的调用方连解析都失败,无法解密自己的历史数据——把安全策略变成了功能性故障 高(可用性/回归) 区分「生成」与「使用」:生成仍要求 ≥2048;解析/加解密/签名允许 ≥MinUsableKeyBits(1024);新增 IsWeakPrivateKey/IsWeakPublicKey 供告警
3 chachax 没有导出任何哨兵错误,调用方无法用 errors.Is 区分「被篡改」与「参数错误」,只能解析错误文本 中 新增 ErrInvalidKeySize/ErrInvalidNonceSize/ErrCiphertextTooShort/ErrAuthenticationFailed 并全面接线
4 门面错误分类不自洽:畸形密文(长度不足、非分组对齐、非法十六进制)不能通过 errors.Is(err, ErrInvalidInput) 命中,调用方没有统一的「输入不合法」判定标准 中 新增 classifyDecryptError;同时刻意让认证失败保持独立,避免「被篡改」被淹没为普通格式错误
5 给不使用密钥的算法(BASE64、哈希、口令哈希)传密钥会被静默忽略——调用方以为数据受了密钥保护,实际完全没有 中 新增 ErrUnexpectedKey 并报错
6 GenerateKey(ARGON2ID) 报错,而 GenerateKey(PBKDF2) 返回盐值——同样是需要盐的 KDF,行为不一致 低 ARGON2ID 现在也返回盐值;bcrypt(盐内嵌在结果中)仍拒绝
7 Encrypt(HKDF) 强制要求 salt,而 DeriveKey(HKDF) 允许缺省(HKDF 空盐在标准中等价于全零盐)——同一算法两条入口行为矛盾 低 两条路径统一允许空盐
8 bcryptx.ErrPasswordMismatch 是从未被返回的死导出 API;hmacx.errNilHashFunc、hmacx_test.mustHex、sha3x_test.hashLike 是死代码 低 全部删除
9 包级可变全局 readFileFn/writeFileFn(注释称「供测试替换」但无任何测试使用)——加密库里的无必要可变状态 低 改为直接调用 os.ReadFile/os.WriteFile

此外还发现并修正了一处真实隐患:aesx/desx/tdesx/blowfishx 的 GenerateKeyHex 系列函数文档没有说明返回值必须先解码才能当密钥使用——这正是第一轮 P0-2 缺陷的诱因。已在四个包的文档中写明。

二次复查新增的验证手段

  • staticcheck 全量静态分析:首次运行报出 29 条告警,逐条处理后现为 0 告警,已加入 CI。

    • 其中数条是真实死代码(上表 #8)。
    • 其余是「有意保留遗留算法」导致的 SA1019:Blowfish、RIPEMD-160、CFB/OFB、RSA PKCS#1 v1.5。调用点已逐一用 //lint:ignore 标注保留理由,而非全局关闭该检查。
    • ecdsax 的 SA1019 用包级 staticcheck.conf 豁免,理由写在该文件里:标准库的 ECDH()/VerifyASN1 在坐标缺失时会空指针 panic(已用探针实测),因此必须读取(只读、不修改)D/X/Y 做判空。删掉这些判空会重新引入 panic——这一点是实测确认的,不是推断。
  • Go 原生 fuzz:新增 4 个 fuzz 目标,聚焦解密路径。实跑结果:

    目标 执行次数 结果
    FuzzDecryptNeverPanics 1,009,552 PASS
    FuzzDecryptWithIVNeverPanics 4,317,330 PASS
    FuzzSymmetricRoundTrip 89,506 PASS
    FuzzAEADTamperDetection 4,621,708 PASS
    合计 约 1004 万次 0 崩溃、0 断言失败

    这些目标在普通 go test 下以种子语料运行,等同常规回归测试;CI 中额外跑 30 秒冒烟。

二次复查后的最终状态

检查项 结果
go build ./... 通过
go vet ./... 干净
staticcheck ./... 0 告警
gofmt -l . 干净
go test ./... 27 个包全部 ok
go test -fuzz(4 目标,各 20s) 约 1004 万次执行,全部通过
govulncheck ./... 0 个可达漏洞

第二轮仍未纳入的算法与第一轮相同(国密 SM2/SM3/SM4、BLAKE3、AES-GCM-SIV、Twofish、CAST5),均需引入额外依赖,属待决策事项。


评审对象:code.yun.ink/pkg/encryptx(29 个受版本控制的文件,commit 034315d) 评审方式:静态通读 + 编译/go vet/go test -cover 实测 + 独立 PoC 复现 + govulncheck


一、总体结论

当前实现既不完善也不严谨,且存在多处会让调用方直接踩坑甚至崩溃的缺陷。不建议在未修复前用于生产。

具体判断:

维度 结论
可编译性 ✅ go build ./... 通过,go vet ./... 干净
可测试性 ❌ go test ./... 失败:2 个包因 panic 直接失败
功能完整性 ❌ 枚举声明了 Blowfish 但无实现;GenerateKey 的返回值无法使用;RSA 全链路不可用
安全性 ❌ 无完整性保护的块模式当一等公民、SHA1 口令哈希、废弃的 openpgp、无常量时间比较、依赖有未修复漏洞
简洁性 ❌ 三个对称算法包约 95% 代码重复(777 行可压缩到约 150 行);同一功能有两套不同实现的 API
文档/工程化 ❌ 无 README、LICENSE、.gitignore、CI;go 1.19 已 EOL

严重性汇总

级别 数量 说明
P0 功能性缺陷 / 崩溃 6 含 1 个可被远程触发的 panic(DoS)
P1 安全设计缺陷 6 含 1 个已知未修复的依赖漏洞
P2 简洁性 / 可维护性 7
算法覆盖缺口 约 20 项 无签名、无 AEAD 之外的现代算法、无 HKDF/Argon2

二、P0:功能性缺陷与崩溃

P0-1 BLOWFISH_* 枚举已声明,但没有实现

encryptx.go:50-52 声明了 BLOWFISH_ECB/CBC/CFB,encryptx.go:86-88 注册了名称,encryptx.go:318-331 甚至实现了密钥生成,但 Encrypt(:119-207)和 Decrypt(:216-274)的 switch 里根本没有对应 case。

实测:

Encrypt(BLOWFISH_ECB) = "", err=不支持的加密类型: 17
Decrypt(BLOWFISH_ECB) = "", err=不支持的加密类型: 17
GenerateKey(BLOWFISH_ECB) = "b6ce0b2bb163993d4a4d91f8e0a3804c", err=<nil>   <-- 只有它能用

用户能生成密钥、能在 GetAllEncryptTypes() 里看到它、能在 UI 上选中它,但一调用就报错。

P0-2 GenerateKey 的输出不能作为加密函数的 key 使用(最严重的易用性缺陷)

GenerateKey 把随机字节 hex.EncodeToString 后返回(encryptx.go:293 DES、:301 3DES、:316 AES、:331 Blowfish),而所有对称加密函数把入参当原始字节直接用:aes.NewCipher([]byte(key))(aesx.go:18)。

于是"生成密钥 → 加密"这条最基本的路径必然失败:

AES_CBC   key=ee85f726...89a5f94f (len=64) -> Encrypt err=crypto/aes: invalid key size 64
DES_CBC   key=4986a395d640e510    (len=16) -> Encrypt err=crypto/des: invalid key size 16
TDES_CBC  key=484ef477...dcc66b9  (len=48) -> Encrypt err=crypto/des: invalid key size 48

AES-256 的 32 字节密钥变成 64 字符 → 64 字节 → 非法。DES/3DES/AES 三条路径全部不可用,只有 HMAC(不限长度)恰好能忍。

值得注意的是 encryptx_test.go:207-240 的 TestGenerateKey 只断言字符串长度,从未把生成的密钥拿去加密,所以这个 bug 一直没被发现。

P0-3 ECB 解密对畸形密文 panic(可远程触发的 DoS)

aesx.go:50-53:

decrypted := make([]byte, len(encrypted))
for i := 0; i < len(encrypted); i += aes.BlockSize {
    block.Decrypt(decrypted[i:i+aes.BlockSize], encrypted[i:i+aes.BlockSize])
}

没有校验 len(encrypted) % BlockSize == 0(CBC 路径在 aesx.go:110 有校验,ECB 没有)。同样问题存在于 desx.go:50-53 和 tdesx.go:50-53。

实测(20 字节非整块密文):

[PANIC] aesx.DecryptECBHex(20 字节非整块) -> runtime error: slice bounds out of range [:32] with capacity 20

密文通常来自网络或数据库等不可信来源,DecryptECBHex 又是导出函数,这是一个攻击者用一串精心构造的十六进制就能让服务进程崩溃的漏洞。

P0-4 PBKDF2 自动生成的盐被丢弃,派生结果无法复现

encryptx.go:179-199:未提供 salt 时生成随机盐,但只返回派生出的 key,盐值被丢弃。

实测:同一密码两次调用得到不同结果,且调用方无从复原。

同一密码两次派生: k1==k2 ? false   (盐值未返回给调用方)

密码派生最核心的语义就是"同样的密码+盐 → 同样的密钥"。现在这个函数只能用来"考一次试",无法用于真正的密钥派生与验证。

P0-5 RSA 全链路不可用:以 panic 代替 error

rsax/rsax.go 是包内质量最差的一块:

行号 问题
:18 GenerateRSAKey(bits) 无返回值、不返回路径,调用方无法知道密钥生成在哪
:23,32,46,52,68,81,88,101,113 9 处 panic(err),库代码绝不应 panic
:30,50 文件名硬编码 private.pem/public.pem,写当前工作目录;并发调用互相覆盖,且私钥文件权限未收紧
:74,107 file.Read(buf) 忽略返回的错误和 n
:76,109 pem.Decode 返回的 block 未判空,非法 PEM 文件会 nil 解引用
:84 publicKeyInterface.(*rsa.PublicKey) 非受检类型断言,传入 ECDSA 公钥即 panic
:86 仅支持 PKCS#1 v1.5 加密,未提供 OAEP(现代首选)
:116 rsa.DecryptPKCS1v15 的 error 被 _ 吞掉,解密失败会静默返回空/垃圾
:18 未拒绝 1024 位等弱密钥长度

encryptx.go:339-340 的 GenerateKey(RSA) 返回的是字符串 "RSA密钥已生成并保存到文件",而不是任何密钥材料——通过统一门面根本无法获得密钥,且实测 1024 位未被拒绝。

项目自己的测试就因此崩溃:

--- FAIL: TestRsa
panic: open public.pem: The system cannot find the file specified.
    rsax/rsax.go:68
--- FAIL: TestEncryptAsymmetricAlgorithms
panic: open public.pem: The system cannot find the file specified.
    encryptx.go:168

encryptx_test.go:140 的注释写着"不应该崩溃",结果它崩溃了。

P0-6 go test ./... 整体失败

FAIL	code.yun.ink/pkg/encryptx	1.084s
FAIL	code.yun.ink/pkg/encryptx/rsax	0.904s

一个连自己的测试都跑不过的加密库,无法建立任何信任基础。


三、P1:安全设计缺陷

P1-1 大量"只加密不认证"的模式,且填充错误可区分(padding oracle)

库把 ECB/CBC/CFB/OFB 作为一等公民暴露,但没有任何完整性保护,也没有提供 encrypt-then-MAC 的封装:

  • CBC/ECB 密文可被篡改,解密端不会发现;
  • PKCS7UnPadding(aesx.go:352-371)对"填充长度非法""填充字节不一致""密文为空"返回不同的错误信息,配合 CBC 模式即构成经典的 padding oracle,攻击者可逐字节还原明文(BEAST/POODLE 类攻击)。

现代 Go 加密代码的默认答案应是 AES-GCM 或 ChaCha20-Poly1305,其他模式若要保留必须显式标注为"仅用于兼容旧系统、不具备完整性"。

P1-2 utilsx 的口令哈希是不安全的 SHA1 + 非常量时间比较

utilsx.go:174-184:

// HashPassword 哈希密码(简单示例,实际应用中应使用更安全的算法如bcrypt)
func HashPassword(password, salt string) string {
	h := sha1.New()
	io.WriteString(h, password+salt)
	return hex.EncodeToString(h.Sum(nil))
}
func VerifyPassword(password, salt, hash string) bool {
	return HashPassword(password, salt) == hash
}

三个问题:

  1. SHA1 单次迭代——GPU 每秒可尝试数十亿次,任何弱口令瞬间爆破。注释自己都承认"应使用 bcrypt",但代码没改;
  2. == 字符串比较非常量时间——可用于时序侧信道;
  3. 同一个包里已经有正确实现 bcryptx,却没有被使用,也没有接入顶层门面。

另外 GeneratePassword(utilsx.go:136-172)注释声称"确保密码包含所有选择的字符类型",但实现只是从合并字符集中纯随机取字符,不保证每类字符都出现,注释与行为不符。

P1-3 gpgx 依赖已废弃且被判定"设计上不安全"的 openpgp

gpgx.go:7 导入 golang.org/x/crypto/openpgp。govulncheck 实测结论:

Vulnerability #1: GO-2026-5932
    The golang.org/x/crypto/openpgp package is unmaintained, unsafe by design,
    and has known security issues
  Module: golang.org/x/crypto
    Found in: golang.org/x/crypto@v0.15.0
    Fixed in: N/A                       <-- 没有修复版本
Your code is affected by 1 vulnerability from 1 module.

该漏洞无修复版本,唯一出路是迁移到 github.com/ProtonMail/go-crypto/openpgp 或删除该包。

此外 gpgx.Demo()(gpgx.go:10)根本不是可用 API:文件名硬编码(signer-pubkey.asc 等)、错误用 fmt.Println 打到 stdout 而不是返回、os.Open 的三个 reader 从未 Close()。它没有接入顶层门面,也没有任何测试(覆盖率 0%)。

P1-4 弱算法未做任何风险标注

MD5、SHA1、DES、3DES、ECB 全部作为正常选项暴露,GetAllEncryptTypes() 会把它们平等地返回给调用方(很可能直接渲染成前端的下拉框),没有任何"已不推荐/不安全"的标记或 API 层面的区分。MD5/DES 甚至被放在枚举首位(encryptx.go:31,40)。

P1-5 缺少现代密码学基础设施(详见第四节)

最突出的是完全没有签名/验签能力(无 RSA-PSS、无 ECDSA、无 Ed25519),以及没有任何常量时间比较函数(hmac.Equal/subtle.ConstantTimeCompare 都没有封装)。

P1-6 AES-GCM 的密文格式脆弱且不可移植

aesx.go:207-208 用 fmt.Sprintf("%x\n", ...) 输出,带尾随换行符;encryptx.go:160 再用 ciphertext + ":" + nonce 拼接,得到:

"3e40b62ea1668b57cff415b5c691066d8ffeaf2f4dc1a61b95c4dee0\n:849734e5172e94786ea108b3\n"

往返之所以"能work",是因为 aesx.go:217-218 的 hex.DecodeString 忽略了 error,恰好返回了解码成功的前缀:

cipherbyte, _ := hex.DecodeString(ciphertext)   // 忽略 error
nonce, _ := hex.DecodeString(noncetext)         // 忽略 error

这是靠"错误被吞掉"维持的脆弱平衡,任何一次重构(比如把 _ 改成正常的错误处理)都会立刻打断兼容性。同时 GCM 也不支持 AAD,无法绑定协议头等上下文。


四、加密方法覆盖缺口

回答"加密的方法是否都齐全了":核心常用算法覆盖了,但缺的都是现代默认选项。

已有的(✓)

哈希 MD5 SHA1 SHA256 SHA512 SHA3-256/384/512 SHAKE128/256; 对称 DES 3DES AES(ECB/CBC/CFB/OFB) AES-GCM; 非对称 RSA(仅加密,PKCS#1 v1.5); MAC HMAC-SHA256/512;KDF PBKDF2-SHA256 bcrypt;编码 Base64(4 种变体);随机数 随机串/字节/Token/密码/UUID/APIKey。

缺失的(✗)

分类 缺失 重要度
AEAD ChaCha20-Poly1305、XChaCha20-Poly1305 高(无 AES 硬件加速场景的默认选择)
签名/验签 RSA-PSS、RSA PKCS#1v1.5 Sign、ECDSA、Ed25519 高(整个库没有任何签名能力)
非对称加密 RSA-OAEP、ECDH/X25519 高(OAEP 是现代 RSA 加密默认)
KDF HKDF、scrypt、Argon2id 高(HKDF 是密钥派生的现代标准;口令存储应选 Argon2id/scrypt)
常量时间比较 SecureCompare / HMAC 校验 高(安全 API 的必要件)
口令哈希 将 bcrypt 接入顶层门面、Argon2id 中(bcryptx 已存在但不可达)
哈希 SHA-224、SHA-384、SHA3-224、BLAKE2b/BLAKE2s、RIPEMD-160 中
MAC HMAC-SHA1、HMAC-SHA384、CMAC、Poly1305 中
对称 Blowfish(枚举已声明未实现)、Twofish、AES-CTR、SM4 中
非对称 SM2、国密 SM3 视需求(模块路径为 code.yun.ink,若有国密合规要求则重要)
编码 Hex 编解码、Base32、URL 转义 低
其他 流式/大文件加解密接口(md5x 有 Md5Stream,其余算法无) 中

结论:覆盖面"看起来"不少,但缺的都是 2024 年之后的默认推荐——现代 Go 加密代码的典型组合 AES-GCM / ChaCha20-Poly1305 + HKDF + Ed25519,这里只有 AES-GCM。


五、P2:简洁性与可维护性

P2-1 三个对称算法包 95% 代码重复

aesx.go(371) + desx.go(203) + tdesx.go(203) = 777 行,其中 EncryptECBHex/DecryptECBHex/EncryptCBCHex/DecryptCBCHex/EncryptCFBHex/DecryptCFBHex/PKCS7Padding/PKCS7UnPadding 是逐行复制,唯一差别是 aes.NewCipher vs des.NewCipher vs des.NewTripleDESCipher、aes.BlockSize(16) vs des.BlockSize(8)。

PKCS7Padding/PKCS7UnPadding 被复制了三遍(aesx.go:345,352;desx.go:177,184;tdesx.go:177,184)——P0-3 的 panic 之所以要修三处,根因就在这里。

抽象成 cipherx 通用层后,三个包可以缩到各约 20 行、总计约 150 行。

P2-2 同一功能存在两套行为不同的实现(陷阱)

aesx.go 里 CBC 有两套:

输出格式 解密去填充 hex 解码错误
EncryptCBCHex/DecryptCBCHex(:64,:93) 大写 hex,无换行 ✅ 去除 ✅ 处理
AESCBCEncrypt/AESCBCDecrypter(:130,:158) 小写 hex + 尾随 \n ❌ 不去除 ❌ 忽略

实测第二套:

AESCBCEncrypt 返回含换行符: "c49a4807...932dca\n"
AESCBCDecrypter 返回: "hello\v\v\v\v\v\v\v\v\v\v\v" err=<nil>   <-- 填充未去除

调用方拿到带 11 个 \v 的"明文",且不报错。aesx_test.go:10-29 的 TestGCM 甚至就是拿这套 API 把 fmt.Println 的结果又喂回去,没有任何断言,所以从未暴露。

这类"看起来能用、实际有毒"的重复 API 比缺 API 更危险,应直接删除。

P2-3 OFB 实现未暴露到顶层

aesx.EncryptOFBHex/DecryptOFBHex(aesx.go:293,318)已实现且有测试,但 EncryptType 枚举里没有 AES_OFB,顶层门面调不到。要么暴露,要么删除。

P2-4 顶层门面靠 map[string]interface{} 传参,无编译期检查

Encrypt/Decrypt/GenerateKey(encryptx.go:118,215,280)用 options map[string]interface{} + 字符串 key 取参:

iterations, ok := options["iterations"].(int)
if !ok { iterations = 10000 }   // 传 int64 / float64 会被静默忽略,回退默认值

后果:拼错 key 名、传错类型都静默降级为默认值(比如 iterations 传成了 "10000" 字符串,PBKDF2 就用 10000 而不是用户以为的值;传 1 也一样接受)。加密参数静默失效是安全敏感的。

更好的形态是每个算法一个强类型 Options 结构体 + 构造函数,或 WithIterations(n) 之类的函数式选项。

P2-5 EncryptType 是裸 int

  • 没有 String() 方法(需外部函数 GetEncryptTypeName);
  • 没有 Valid() 校验;
  • iota 位置敏感:encryptx.go:29-66 里任何中间插入都会让已有配置/数据库里的数值整体错位;
  • 错误信息用 %d 输出数字而非名称(:206,:273,:369),排障困难。

P2-6 错误信息未统一包装

encryptx.go 里错误信息是中文("哈希算法不可逆"),子包 aesx/desx 里是混合中英文("hex解码失败: %v" + "ciphertext too short")。同时用 fmt.Errorf 而非 %w 包装,调用方无法 errors.Is/errors.As 判断错误类型——无法区分"密钥错误"和"密文损坏"。

P2-7 PKCS7UnPadding 校验过宽

aesx.go:359 只检查 unpadding > length,没检查 unpadding > blockSize。实测一个 32 字节全 0x20 的块在 blockSize=16 下被当作合法填充接受,返回空串:

32 字节 0x20 块 -> 去填充="" err=<nil> (blockSize=16, 却接受了 padding=32)

(顺带:该函数签名不接收 blockSize,本身就无从校验。)


六、测试与工程化问题

测试覆盖

gpgx        0.0%     <- 无测试文件
md5x        0.0%     <- 无测试文件
pbkdf2x     0.0%     <- 无测试文件
sha256x     0.0%     <- 无测试文件
rsax        FAIL     <- panic
encryptx    FAIL     <- panic

覆盖率数字(aesx 85.5%、desx 85.3%)具有误导性,因为:

  • aesx_test.go:10-29、rsax_test.go:10-19 等只 fmt.Println,不写任何断言,跑通不等于正确;
  • 完全没有错误路径/畸形输入测试——所以 P0-3 的 panic 从未被触发;
  • TestGenerateKey 只查长度不用密钥——所以 P0-2 从未被发现;
  • 测试普遍硬编码弱密钥("example key 1234"、"test_key"、"test_key_1234567")。

工程化

  • 无 README、无 API 文档、无使用示例;
  • 无 LICENSE;
  • 无 .gitignore(本次评审产生的 .gocache/.gomodcache 立刻就显示为未跟踪目录);
  • 无 CI(有 CI 的话 P0-6 不可能存在);
  • go.mod:3 声明 go 1.19——已 EOL,且 crypto/ecdh 等需要 1.20+;
  • go.mod:5 锁定 golang.org/x/crypto v0.15.0——早已过时,govulncheck 报告:"22 vulnerabilities in modules you require"(当前未被调用,但升级是零成本的收益);
  • commit 信息缺失上下文("提交"、"aes"、"添加更多的方法")。

七、建议的修复路线

第一批(必须,阻塞发布)

  1. 修 P0-3 panic:在 aesx/desx/tdesx 的 ECB 解密入口加 len(ct)%BlockSize != 0 校验。(三处;抽象后可只修一处)
  2. 修 P0-2 密钥编码:让 GenerateKey 与 Encrypt/Decrypt 对 key 的编码约定一致。推荐 key 统一为原始字节/标准 Base64,并在每个入口做长度校验;或保留 hex 但在加密入口显式 hex.DecodeString。这是破坏性变更,需同步 README 与调用方。
  3. 修 P0-5 RSA:全部 panic 改为返回 error;pem.Decode 判空;受检类型断言;os.ReadFile 替代手写读;GenerateRSAKey 返回路径或 *rsa.PrivateKey;拒绝 <2048 位;补 rsa.DecryptPKCS1v15 的错误处理。
  4. 修 P0-1:实现 Blowfish(golang.org/x/crypto/blowfish)或从枚举/名称表/GenerateKey 中移除。
  5. 修 P0-4 PBKDF2:返回值改为 (key, salt, error),或强制调用方传入 salt 并在文档中明确。
  6. 让 go test ./... 通过,并修 encryptx_test.go:133-144(它自己就 panic)。

第二批(安全)

  1. 删除或标注不安全模式;新增 ChaCha20-Poly1305,把 AEAD 作为推荐路径。
  2. 新增签名能力:Ed25519(首选)+ RSA-PSS。
  3. 新增 SecureCompare(hmac.Equal / subtle.ConstantTimeCompare),并在 hmacx/pbkdf2x 提供 Verify*;把 utilsx.VerifyPassword 改为常量时间比较。
  4. 用 bcryptx 替换 utilsx.HashPassword/VerifyPassword(或直接删除后者);补充 Argon2id。
  5. 移除 gpgx 或迁移到 github.com/ProtonMail/go-crypto(GO-2026-5932 无修复版本)。
  6. 统一 PKCS7UnPadding 的错误信息,避免 padding oracle。

第三批(简洁与工程化)

  1. 抽 cipherx 通用层,消除 aesx/desx/tdesx 的 777 行重复与三份 PKCS7 副本;删除 AESCBCEncrypt/AESCBCDecrypter 这套有毒的重复 API。
  2. 给 EncryptType 加 String()/Valid();考虑显式赋值而非 iota 以保证数值稳定。
  3. 用强类型 Options 替换 map[string]interface{}。
  4. 补 README(含"算法选型建议表":什么场景用什么)、LICENSE、.gitignore、GitHub Actions(build + vet + test + govulncheck)。
  5. 升级 go.mod(Go 版本与 x/crypto),并加 govulncheck 到 CI。
  6. 补错误路径测试与畸形输入测试(fuzz),把覆盖率目标定在有断言的用例上而非行数上。

附:本次评审的验证命令与原始证据

$env:GOCACHE="<ws>\.gocache"; $env:GOMODCACHE="<ws>\.gomodcache"
go build ./...          # BUILD_EXIT=0
go vet ./...            # VET_EXIT=0
go test ./...           # TEST_EXIT=1  (encryptx / rsax panic)
go test -cover ./...    # gpgx 0%, md5x 0%, pbkdf2x 0%, sha256x 0%
$env:GOSUMDB="off"
go run golang.org/x/vuln/cmd/govulncheck@latest ./...   # GO-2026-5932

PoC 输出(12 项断言,全部复现)见本报告第二、三节内嵌的实测片段。评审用的临时 PoC 与构建缓存已清理,工作区已恢复为干净状态(git status --short 无输出)。