完善不同加密方法的使用

This commit is contained in:
yun
2026-09-30 15:49:12 +08:00
parent 034315ddbd
commit 7db722f8b9
69 changed files with 21362 additions and 2563 deletions
+277
View File
@@ -0,0 +1,277 @@
# 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)。