# encryptx Go 加密工具库:哈希、对称加密、AEAD、消息认证码、密钥派生、口令哈希、非对称加密与数字签名。 模块路径:`code.yun.ink/pkg/encryptx` --- ## 快速开始 ```go 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)) } ``` 配置驱动(算法名由配置/管理后台决定)时使用顶层门面: ```go 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`(根包) | 配置驱动的统一门面 | --- ## 顶层门面 适合「算法由配置决定」的场景。若在写业务代码,优先直接用子包。 ```go // 生成密钥(返回值可直接用作 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`。 ```go // 旧 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`。 --- ## 开发 ```bash 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](LICENSE)。