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