Files
2026-09-30 15:49:12 +08:00

14 KiB
Raw Permalink Blame History

encryptx

Go 加密工具库:哈希、对称加密、AEAD、消息认证码、密钥派生、口令哈希、非对称加密与数字签名。

模块路径:code.yun.ink/pkg/encryptx


快速开始

package main

import (
	"fmt"

	"code.yun.ink/pkg/encryptx"
	"code.yun.ink/pkg/encryptx/aesx"
)

func main() {
	// 推荐路径:直接使用子包,参数强类型、错误明确
	key, err := aesx.GenerateKey(aesx.KeySize256)
	if err != nil {
		panic(err)
	}

	ciphertext, err := aesx.SealGCM([]byte("要保护的数据"), key, nil)
	if err != nil {
		panic(err)
	}

	plaintext, err := aesx.OpenGCM(ciphertext, key, nil)
	if err != nil {
		panic(err) // 认证失败:密文被篡改、nonce 不对或密钥错误
	}
	fmt.Println(string(plaintext))
}

配置驱动(算法名由配置/管理后台决定)时使用顶层门面:

key, _ := encryptx.GenerateKey(encryptx.AES_GCM, nil)

ciphertext, err := encryptx.Encrypt(encryptx.AES_GCM, "要保护的数据", key, encryptx.Options{
	"aad": "v1|user=42", // 附加认证数据,参与完整性校验但不加密
})
if err != nil {
	panic(err)
}

plaintext, err := encryptx.Decrypt(encryptx.AES_GCM, ciphertext, key, encryptx.Options{
	"aad": "v1|user=42",
})

门面的 Options 采用严格校验:键名写错、类型不符都会返回错误,不再静默回退到默认值。这是刻意设计——加密参数被静默忽略属于安全隐患。


算法选型建议

如果你只想知道该用什么,看这张表就够了。

需求 推荐 不要用
对称加密(有 AES 硬件加速) AES-GCM(aesx.SealGCM) ECB、CBC、CFB、OFB、CTR
对称加密(无 AES 加速 / 移动端) ChaCha20-Poly1305、XChaCha20-Poly1305(chachax) 同上
口令存储 Argon2id(argon2x)、其次 scrypt、bcrypt MD5、SHA-1、SHA-256 裸哈希
从高熵密钥派生多个子密钥 HKDF(hkdfx) —
从口令派生密钥 Argon2id / scrypt / PBKDF2(迭代 ≥ 600000) 无盐、单次哈希
数字签名 Ed25519(ed25519x)、ECDSA P-256、RSA-PSS RSA PKCS#1 v1.5 签名
非对称加密 RSA-OAEP(rsax) RSA PKCS#1 v1.5 加密
通用哈希 SHA-256、SHA-512、SHA3-256、BLAKE2b MD5、SHA-1
消息认证 HMAC-SHA256(hmacx),校验务必用 Verify* 用 == 比较 MAC
密钥/口令比较 hmacx.SecureCompare、utilsx.SecureCompare ==、bytes.Equal

已攻破或不推荐的算法

以下算法仍然保留,只为与无法改动的既有系统互操作(老银行接口、老终端协议、遗留数据库字段)。门面通过 SecurityLevel() 与 IsDeprecated() 暴露这一信息,管理后台可以据此提示或禁选:

算法 状态 原因
MD5、SHA1 已攻破 碰撞攻击已实用化(SHAttered)
DES(全部模式) 已攻破 56 位密钥可被暴力破解
TDES(3DES) 遗留 64 位分组,Sweet32;NIST 已弃用
Blowfish 遗留 64 位分组,Sweet32
ECB(任意分组密码) 遗留 无语义安全性:相同明文块产生相同密文块
RIPEMD-160 遗留 上游已标记 deprecated;仅用于如比特币地址等场景
AES-CBC/CFB/OFB/CTR、DES-* 无完整性保护 密文可被篡改而不被发现;必须另行配合 HMAC

安全设计要点

理解以下几条可以避免绝大多数误用:

  1. 只有 AEAD 提供完整性保护。 AES-GCM、ChaCha20-Poly1305、XChaCha20-Poly1305 会检测篡改;其余分组密码模式不会。需要它们时必须自行采用「先加密后认证」(Encrypt-then-MAC)。
  2. Nonce 绝不能在同一密钥下重复。 GCM 一旦重用 nonce,攻击者可恢复认证密钥并伪造密文。aesx.EncryptGCM 每次都生成随机 nonce 并单独返回;SealGCM 把 nonce 前置在密文中,单字段存储最省事。XChaCha20 的 24 字节 nonce 允许安全地随机生成,不需要计数器。
  3. 密钥是 []byte,不是字符串。 不要传入十六进制或 Base64 文本——那是编码,不是密钥。用各包的 GenerateKey 拿到密钥,需要落库时再自行编码。
  4. 口令哈希不要自己加盐或再做一次哈希。 bcryptx、argon2x 的输出已经把算法、参数、随机盐编码在内,直接入库即可。
  5. 比较密钥与 MAC 一律用常量时间函数。 本库提供 hmacx.SecureCompare、utilsx.SecureCompare,以及各 Verify* 函数。
  6. 所有函数都可并发调用,不共享可变状态。
  7. 密钥强度策略区分「生成」与「使用」。 rsax.GenerateKey 拒绝低于 2048 位的密钥;但解析与使用既有的 1024 位密钥是被允许的(否则持有旧密钥的调用方将无法解密自己的历史数据),此时可用 rsax.IsWeakPrivateKey / rsax.IsWeakPublicKey 得知它已不达标。低于 rsax.MinUsableKeyBits(1024,与 Go 标准库下限一致)的密钥一律拒绝。
  8. bcrypt 只使用口令的前 72 字节。 本库的哈希与校验两条路径都会拒绝超过 72 字节的口令并返回 bcryptx.ErrPasswordTooLong,绝不静默截断——静默截断会让共享前 72 字节的不同口令互相通过校验。需要支持更长口令时请改用 argon2x。

包一览

包 内容
aesx AES:ECB/CBC/CFB/OFB/CTR + GCM
desx / tdesx / blowfishx DES / 3DES / Blowfish 的同一套模式(仅兼容用)
chachax ChaCha20-Poly1305 与 XChaCha20-Poly1305(AEAD)
cipherx 分组密码通用层:PKCS#7、模式实现、Suite、统一错误与长度校验
rsax RSA:密钥生成/编解码、OAEP(含长明文分块)、PKCS#1 v1.5、PSS 签名
ed25519x Ed25519 签名(首选)
ecdsax ECDSA P-256/P-384/P-521 签名
hmacx HMAC-SHA1/256/384/512 + 常量时间校验
md5x sha1x sha256x sha512x sha3x blake2x ripemd160x 哈希(含 SHA-224/384、SHA3-224、BLAKE2b/s、SHAKE)
pbkdf2x hkdfx scryptx argon2x 密钥派生与口令哈希(Argon2id 带 PHC 编码)
bcryptx bcrypt 口令哈希
gpgx OpenPGP 加解密与签名(基于 ProtonMail/go-crypto)
base64x hexx Base64 四种变体、十六进制编解码
utilsx 安全随机数、UUID、API Key、令牌、常量时间比较
encryptx(根包) 配置驱动的统一门面

顶层门面

适合「算法由配置决定」的场景。若在写业务代码,优先直接用子包。

// 生成密钥(返回值可直接用作 key 参数,无需任何编码转换)
key, err := encryptx.GenerateKey(encryptx.AES_GCM, encryptx.Options{"keyLength": 32})

// 加密 / 解密
ciphertext, err := encryptx.Encrypt(encryptx.AES_GCM, data, key, options)
plaintext, err := encryptx.Decrypt(encryptx.AES_GCM, ciphertext, key, options)

// 密钥派生:同时返回盐值,两者必须一起保存
key, salt, err := encryptx.DeriveKey(encryptx.ARGON2ID, password, encryptx.Options{
	"time": 3, "memory": 64 * 1024, "threads": 4, "keyLength": 32,
})

// 口令哈希
encoded, err := encryptx.HashPassword(encryptx.ARGON2ID, password, nil)
ok, err := encryptx.VerifyPassword(encryptx.ARGON2ID, password, encoded, nil)

// 签名 / 验签
privatePEM, publicPEM, err := encryptx.GenerateKeyPair(encryptx.ED25519, nil)
signature, err := encryptx.Sign(encryptx.ED25519, message, privatePEM, nil)
err = encryptx.Verify(encryptx.ED25519, message, signature, publicPEM, nil)

输出编码

算法族 编码
哈希、HMAC、HEX 小写十六进制
BASE64、BASE64_URL 标准 / URL 安全 Base64
分组密码(ECB/CBC/CFB/OFB/CTR) 大写十六进制,无换行
AEAD(AES-GCM、ChaCha20) 密文十六进制:nonce十六进制(小写)
RSA 加密、数字签名 标准 Base64

门面选项

选项 适用算法 说明
keyLength AES、Blowfish、HMAC、KDF、Argon2 密钥/输出长度(字节)
salt / saltLength PBKDF2、scrypt、Argon2、HKDF 盐值(十六进制字符串或 []byte)/ 随机盐长度
iterations PBKDF2 迭代次数
n r p scrypt CPU/内存代价、块大小、并行度
time memory threads Argon2id 迭代次数、内存(KiB)、并行度
cost bcrypt 代价
iv CBC/CFB/OFB/CTR 显式 IV(十六进制),仅用于兼容固定 IV 的旧系统
aad AES-GCM、ChaCha20 附加认证数据
info HKDF 上下文信息
bits RSA 密钥位数(默认 2048,最小 2048)
publicKeyFile / privateKeyFile RSA、签名 从文件读取密钥

字符串形式的 salt 与 iv 一律按十六进制解析,需要原始字节时请直接传 []byte。


迁移指南(从旧版本升级)

旧版本存在若干严重缺陷,本次为破坏性重构。逐条对照:

1. 密钥类型:string → []byte

旧版 GenerateKey 返回十六进制文本(32 字节密钥变成 64 字符),而加密函数把它当原始密钥字节使用,于是**「生成密钥 → 加密」这条最基本的路径必然失败**,报 crypto/aes: invalid key size 64。

// 旧
key, _ := encryptx.GenerateKey(encryptx.AES_CBC, nil) // "ee85f726...f94f" (64 字符)
ciphertext, err := encryptx.Encrypt(encryptx.AES_CBC, data, key, nil) // 必然失败

// 新
key, _ := encryptx.GenerateKey(encryptx.AES_CBC, nil) // []byte, 32 字节
ciphertext, err := encryptx.Encrypt(encryptx.AES_CBC, data, key, nil)

2. 函数改名

旧 新
md5x.Md5String(s) md5x.Hex([]byte(s))
md5x.Md5File(p) md5x.HexFile(p)
md5x.Md5Stream(r) md5x.HexReader(r)
sha1x.Sha1(s) sha1x.Hex([]byte(s))
sha256x.Sha256(s) sha256x.Hex256([]byte(s))
sha512x.Sha512(s) sha512x.Hex512([]byte(s))
sha3x.Sha3_256(s) sha3x.Hex256([]byte(s))
sha3x.Shake128(s, n) sha3x.Shake128Hex([]byte(s), n)(现在返回 error)
hmacx.HMACSHA256(m, k) hmacx.HexSHA256([]byte(m), []byte(k))
base64x.StdEncode(s) base64x.Encode([]byte(s))
base64x.StdDecode(s) base64x.Decode(s)(返回 []byte)
pbkdf2x.DeriveKeyFromPassword(pw, saltHex, it, len) pbkdf2x.DeriveKey([]byte(pw), salt, it, len)(盐为 []byte)
utilsx.GenerateRandomHexString(n) utilsx.RandomHex(n)
utilsx.GenerateToken(n) utilsx.RandomToken(n)
utilsx.HashPassword(pw, salt) / VerifyPassword 已删除,改用 bcryptx 或 argon2x
rsax.RSA_Encrypt(data, path) rsax.EncryptOAEP(pub, data) 或 EncryptOAEPLong
rsax.RSA_Decrypt(data, path) rsax.DecryptOAEP(priv, data)
rsax.GenerateRSAKey(bits) rsax.GenerateKeyPairPEM(bits) / rsax.SaveKeyPair(privPath, pubPath, bits)
gpgx.Demo() gpgx.Encrypt / Decrypt / Sign / Verify 等
aesx.AESCBCEncrypt / AESCBCDecrypter aesx.EncryptCBC / aesx.DecryptCBC(旧版带换行且不去填充,已删除)

3. 行为变化

  • 不再 panic。 旧版 rsax 有 9 处 panic,公钥文件缺失会让进程崩溃。现在全部返回 error。
  • 不再有隐藏的文件 I/O。 RSA 加解密改为接收 PEM 内容;需要按路径加载时用 rsax.LoadPublicKey,或门面的 publicKeyFile 选项。
  • RSA 加密默认改为 OAEP(SHA-256)。 旧版是 PKCS#1 v1.5,旧密文不再兼容。
  • 数据被静默忽略的情况已消除。 未知选项名、类型不符的选项一律报错;给不使用密钥的算法(BASE64、哈希、口令哈希)传 key 会返回 ErrUnexpectedKey,而不是像旧版本那样静默忽略——后者会让调用方误以为数据受到了密钥保护。
  • EncryptType 数值保持稳定。 原有 0–24 的取值未变,新算法从 25 起追加。
  • 选项字符串一律按十六进制解析。 旧版把 salt 当任意字符串使用。

4. 依赖与工具链

  • Go 版本要求提升至 go 1.26。
  • golang.org/x/crypto 升级到 v0.57.0。
  • golang.org/x/crypto/openpgp 已废弃且被 govulncheck 判定为「设计上不安全、无修复版本」(GO-2026-5932),gpgx 已迁移到 github.com/ProtonMail/go-crypto。

开发

go build ./...
go vet ./...
gofmt -l .
go test ./...
go test -race ./...          # 需要 cgo
go test -cover ./...
go test -fuzz=FuzzDecryptNeverPanics -fuzztime=30s .   # 模糊测试
staticcheck ./...            # 需要 go install honnef.co/go/tools/cmd/staticcheck@latest
govulncheck ./...

encryptx_test.go 中的四个 Fuzz* 目标在普通 go test 下以种子语料运行,也可用 -fuzz 持续变异。它们集中覆盖解密路径——这些函数的输入直接来自网络或数据库等不可信来源,也是历史上出现过 slice bounds out of range 崩溃的地方。

CI(.github/workflows/ci.yml)在每次推送时执行 gofmt、vet、staticcheck、build、-race 测试、fuzz 冒烟、govulncheck,并校验 go.mod/go.sum 与源码 import 一致。

关于静态检查的豁免

ecdsax/staticcheck.conf 对 SA1019 做了定向豁免。原因是该包必须读取 ecdsa.PrivateKey.D 与 PublicKey.X/Y 做判空:标准库在这些字段缺失时会直接空指针 panic(已实测),而本库承诺「任何畸形密钥都返回 error,绝不 panic」。我们只读取、从不修改这些字段,不构成 Go 1.26 弃用说明所警告的风险。

其余仅存于本库的废弃 API 用法(Blowfish、RIPEMD-160、CFB/OFB、RSA PKCS#1 v1.5)都在调用点用 //lint:ignore 标注了保留理由——它们的存在本身就是「与既有系统互操作」这一需求。


许可

MIT,见 LICENSE。