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

278 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)。