Files
encryptx/cipherx/cipherx.go
T
2026-09-30 15:49:12 +08:00

387 lines
13 KiB
Go

// Package cipherx 提供分组密码的通用封装。
//
// 设计目标:
// - 统一 PKCS#7 填充、IV 生成与前缀格式、长度校验与错误语义;
// - 消除 aesx / desx / tdesx / blowfishx 之间约 95% 的重复实现;
// - 对来自外部的密文一律「先校验长度、再切片」,杜绝越界 panic。
//
// # 安全提示
//
// - ECB 模式不具备语义安全性:相同明文块会产生相同密文块,攻击者可据此
// 识别重复内容与结构。该模式仅为兼容既有系统而保留,新代码不应使用。
// - ECB / CBC / CFB / OFB / CTR **均不提供完整性保护**,密文被篡改时解密
// 不会报错(CBC 的填充校验只能概率性发现篡改)。新代码应优先使用 AEAD,
// 例如 AES-GCM 或 ChaCha20-Poly1305。若因兼容性必须使用这些模式,请自行
// 采用「先加密后认证」(Encrypt-then-MAC)的组合方式。
// - CBC 解密时所有填充校验失败均返回同一个 ErrInvalidPadding,且校验过程
// 不提前短路,以免通过错误信息或响应时间区分失败原因(padding oracle)。
package cipherx
import (
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
)
// Mode 表示分组密码的工作模式。
type Mode int
const (
// ECB 电子密码本模式。需要填充;相同明文块产生相同密文块,不推荐使用。
ECB Mode = iota
// CBC 密码分组链接模式。需要填充,并需要随机且不可预测的 IV。
CBC
// CFB 密码反馈模式。不需要填充,按流式处理,解密不具有随机访问能力。
CFB
// OFB 输出反馈模式。不需要填充,按流式处理。
OFB
// CTR 计数器模式。不需要填充,按流式处理,可并行与随机访问。
CTR
)
var modeNames = [...]string{"ECB", "CBC", "CFB", "OFB", "CTR"}
// String 返回模式名称,便于日志与错误信息使用。
func (m Mode) String() string {
if !m.Valid() {
return fmt.Sprintf("Mode(%d)", int(m))
}
return modeNames[m]
}
// Valid 报告模式是否为已支持的模式。
func (m Mode) Valid() bool { return int(m) >= 0 && int(m) < len(modeNames) }
// UsesIV 报告该模式是否需要初始化向量。
func (m Mode) UsesIV() bool { return m != ECB }
// RequiresPadding 报告该模式是否需要 PKCS#7 填充。
func (m Mode) RequiresPadding() bool { return m == ECB || m == CBC }
// 错误定义。调用方可用 errors.Is 精确判断失败类型。
var (
// ErrNilBlock 表示传入的 cipher.Block 为 nil。
ErrNilBlock = errors.New("cipherx: cipher block is nil")
// ErrInvalidBlockSize 表示块大小或填充块大小非法。
ErrInvalidBlockSize = errors.New("cipherx: invalid block size")
// ErrInvalidIVSize 表示 IV 长度与分组长度不一致。
ErrInvalidIVSize = errors.New("cipherx: invalid IV size")
// ErrInvalidMode 表示模式非法,或对该模式使用了不适用的操作(如给 ECB 传 IV)。
ErrInvalidMode = errors.New("cipherx: invalid cipher mode")
// ErrEmptyCiphertext 表示密文为空。
ErrEmptyCiphertext = errors.New("cipherx: ciphertext is empty")
// ErrCiphertextTooShort 表示密文长度不足以容纳 IV 或一个完整分组。
ErrCiphertextTooShort = errors.New("cipherx: ciphertext is too short")
// ErrNotBlockAligned 表示密文长度不是分组长度的整数倍。
ErrNotBlockAligned = errors.New("cipherx: ciphertext is not a multiple of the block size")
// ErrInvalidHex 表示十六进制文本无法解码。
ErrInvalidHex = errors.New("cipherx: invalid hexadecimal input")
// ErrInvalidPadding 统一表示 PKCS#7 填充校验失败。
//
// 刻意不区分「填充长度非法」「填充字节不一致」等具体原因:区分它们会让
// 攻击者借助错误信息实施 padding oracle 攻击。
ErrInvalidPadding = errors.New("cipherx: invalid padding")
)
// PKCS7Pad 按 PKCS#7 规则填充 data,返回新切片(不修改入参)。
//
// blockSize 必须在 [1, 255] 范围内:填充长度需要用一个字节表示。
// 若 len(data) 恰好是 blockSize 的整数倍,会额外填充一个完整分组。
func PKCS7Pad(data []byte, blockSize int) ([]byte, error) {
if blockSize <= 0 || blockSize > 255 {
return nil, fmt.Errorf("cipherx: 填充块大小 %d 超出 [1,255]: %w", blockSize, ErrInvalidBlockSize)
}
padding := blockSize - len(data)%blockSize
out := make([]byte, len(data)+padding)
copy(out, data)
for i := len(data); i < len(out); i++ {
out[i] = byte(padding)
}
return out, nil
}
// PKCS7Unpad 校验并去除 PKCS#7 填充。
//
// 所有失败原因统一返回 ErrInvalidPadding;校验循环不提前中断,以减小时序差异。
// 返回的切片是新分配的,不与入参共享底层数组。
func PKCS7Unpad(data []byte, blockSize int) ([]byte, error) {
if blockSize <= 0 || blockSize > 255 {
return nil, fmt.Errorf("cipherx: 填充块大小 %d 超出 [1,255]: %w", blockSize, ErrInvalidBlockSize)
}
length := len(data)
if length == 0 || length%blockSize != 0 {
return nil, ErrInvalidPadding
}
padding := int(data[length-1])
if padding == 0 || padding > blockSize || padding > length {
return nil, ErrInvalidPadding
}
// 恒定时间风格的整块比较:即使发现不一致也继续遍历完,避免时序泄漏。
var diff byte
for _, b := range data[length-padding:] {
diff |= b ^ byte(padding)
}
if diff != 0 {
return nil, ErrInvalidPadding
}
out := make([]byte, length-padding)
copy(out, data[:length-padding])
return out, nil
}
// NewIV 用 crypto/rand 生成一个长度为 blockSize 的随机 IV。
func NewIV(blockSize int) ([]byte, error) {
if blockSize <= 0 {
return nil, fmt.Errorf("cipherx: 块大小 %d 非法: %w", blockSize, ErrInvalidBlockSize)
}
iv := make([]byte, blockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, fmt.Errorf("cipherx: 生成随机 IV 失败: %w", err)
}
return iv, nil
}
func validateBlock(block cipher.Block) error {
if block == nil {
return ErrNilBlock
}
return nil
}
func validateMode(mode Mode) error {
if !mode.Valid() {
return fmt.Errorf("cipherx: 不支持的模式 %d: %w", int(mode), ErrInvalidMode)
}
return nil
}
// Encrypt 使用给定分组与模式加密明文,IV 由本函数随机生成。
//
// 返回值格式:
// - ECB:密文(已做 PKCS#7 填充)
// - 其余模式:iv || 密文(IV 前置,便于随密文一起存储与传输)
func Encrypt(block cipher.Block, mode Mode, plaintext []byte) ([]byte, error) {
if err := validateBlock(block); err != nil {
return nil, err
}
if err := validateMode(mode); err != nil {
return nil, err
}
if mode == ECB {
return encryptECB(block, plaintext)
}
iv, err := NewIV(block.BlockSize())
if err != nil {
return nil, err
}
body, err := EncryptWithIV(block, mode, plaintext, iv)
if err != nil {
return nil, err
}
out := make([]byte, 0, len(iv)+len(body))
out = append(out, iv...)
out = append(out, body...)
return out, nil
}
// Decrypt 解密由 Encrypt 产生的数据(IV 前置格式)。
func Decrypt(block cipher.Block, mode Mode, ciphertext []byte) ([]byte, error) {
if err := validateBlock(block); err != nil {
return nil, err
}
if err := validateMode(mode); err != nil {
return nil, err
}
if mode == ECB {
return decryptECB(block, ciphertext)
}
bs := block.BlockSize()
if len(ciphertext) < bs {
return nil, fmt.Errorf("cipherx: %s 密文长度 %d 不足以容纳 %d 字节 IV: %w",
mode, len(ciphertext), bs, ErrCiphertextTooShort)
}
iv := ciphertext[:bs]
body := ciphertext[bs:]
return DecryptWithIV(block, mode, body, iv)
}
// EncryptWithIV 使用调用方提供的 IV 加密,返回值**只包含密文本身**(不前置 IV)。
//
// 该函数用于与规定了固定 IV 的既有系统互操作。自行管理 IV 时必须保证:
// - IV 在同一密钥下绝不重复(CBC/CFB 要求不可预测,CTR/OFB 要求唯一);
// - IV 无需保密,但应与密文一同传递。
//
// ECB 模式不接受 IV,调用会返回 ErrInvalidMode。
func EncryptWithIV(block cipher.Block, mode Mode, plaintext, iv []byte) ([]byte, error) {
if err := validateBlock(block); err != nil {
return nil, err
}
if err := validateMode(mode); err != nil {
return nil, err
}
if mode == ECB {
return nil, fmt.Errorf("cipherx: ECB 模式不接受 IV: %w", ErrInvalidMode)
}
bs := block.BlockSize()
if len(iv) != bs {
return nil, fmt.Errorf("cipherx: IV 长度 %d, 期望 %d: %w", len(iv), bs, ErrInvalidIVSize)
}
switch mode {
case CBC:
padded, err := PKCS7Pad(plaintext, bs)
if err != nil {
return nil, err
}
out := make([]byte, len(padded))
cipher.NewCBCEncrypter(block, iv).CryptBlocks(out, padded)
return out, nil
case CFB:
out := make([]byte, len(plaintext))
//lint:ignore SA1019 CFB 自 Go 1.24 起被标准库标记为废弃(无认证);本库保留它是为了与既有系统互操作,包注释已明确警告其不提供完整性保护。
cipher.NewCFBEncrypter(block, iv).XORKeyStream(out, plaintext)
return out, nil
case OFB:
out := make([]byte, len(plaintext))
//lint:ignore SA1019 同上:OFB 仅为兼容既有系统而保留,不提供完整性保护。
cipher.NewOFB(block, iv).XORKeyStream(out, plaintext)
return out, nil
case CTR:
out := make([]byte, len(plaintext))
cipher.NewCTR(block, iv).XORKeyStream(out, plaintext)
return out, nil
}
return nil, fmt.Errorf("cipherx: 不支持的模式 %d: %w", int(mode), ErrInvalidMode)
}
// DecryptWithIV 使用调用方提供的 IV 解密 EncryptWithIV 产生的密文。
func DecryptWithIV(block cipher.Block, mode Mode, ciphertext, iv []byte) ([]byte, error) {
if err := validateBlock(block); err != nil {
return nil, err
}
if err := validateMode(mode); err != nil {
return nil, err
}
if mode == ECB {
return nil, fmt.Errorf("cipherx: ECB 模式不接受 IV: %w", ErrInvalidMode)
}
bs := block.BlockSize()
if len(iv) != bs {
return nil, fmt.Errorf("cipherx: IV 长度 %d, 期望 %d: %w", len(iv), bs, ErrInvalidIVSize)
}
switch mode {
case CBC:
if len(ciphertext) == 0 {
return nil, ErrEmptyCiphertext
}
if len(ciphertext)%bs != 0 {
return nil, fmt.Errorf("cipherx: CBC 密文长度 %d: %w", len(ciphertext), ErrNotBlockAligned)
}
out := make([]byte, len(ciphertext))
cipher.NewCBCDecrypter(block, iv).CryptBlocks(out, ciphertext)
return PKCS7Unpad(out, bs)
case CFB:
out := make([]byte, len(ciphertext))
//lint:ignore SA1019 CFB 自 Go 1.24 起被标准库标记为废弃(无认证);本库保留它是为了与既有系统互操作,包注释已明确警告其不提供完整性保护。
cipher.NewCFBDecrypter(block, iv).XORKeyStream(out, ciphertext)
return out, nil
case OFB:
out := make([]byte, len(ciphertext))
//lint:ignore SA1019 同上:OFB 仅为兼容既有系统而保留,不提供完整性保护。
cipher.NewOFB(block, iv).XORKeyStream(out, ciphertext)
return out, nil
case CTR:
out := make([]byte, len(ciphertext))
cipher.NewCTR(block, iv).XORKeyStream(out, ciphertext)
return out, nil
}
return nil, fmt.Errorf("cipherx: 不支持的模式 %d: %w", int(mode), ErrInvalidMode)
}
func encryptECB(block cipher.Block, plaintext []byte) ([]byte, error) {
bs := block.BlockSize()
padded, err := PKCS7Pad(plaintext, bs)
if err != nil {
return nil, err
}
out := make([]byte, len(padded))
for i := 0; i < len(padded); i += bs {
block.Encrypt(out[i:i+bs], padded[i:i+bs])
}
return out, nil
}
func decryptECB(block cipher.Block, ciphertext []byte) ([]byte, error) {
bs := block.BlockSize()
if len(ciphertext) == 0 {
return nil, ErrEmptyCiphertext
}
// 先校验长度再切片:旧实现缺少这一步,畸形密文会直接触发
// "slice bounds out of range" panic,构成可远程触发的拒绝服务。
if len(ciphertext)%bs != 0 {
return nil, fmt.Errorf("cipherx: ECB 密文长度 %d: %w", len(ciphertext), ErrNotBlockAligned)
}
out := make([]byte, len(ciphertext))
for i := 0; i < len(ciphertext); i += bs {
block.Decrypt(out[i:i+bs], ciphertext[i:i+bs])
}
return PKCS7Unpad(out, bs)
}
// EncryptHex 与 Encrypt 相同,但把结果编码为**大写**十六进制字符串。
func EncryptHex(block cipher.Block, mode Mode, plaintext []byte) (string, error) {
ciphertext, err := Encrypt(block, mode, plaintext)
if err != nil {
return "", err
}
return strings.ToUpper(hex.EncodeToString(ciphertext)), nil
}
// DecryptHex 解密十六进制密文,大小写均可,允许首尾空白。
func DecryptHex(block cipher.Block, mode Mode, ciphertextHex string) ([]byte, error) {
ciphertext, err := decodeHex(ciphertextHex)
if err != nil {
return nil, err
}
return Decrypt(block, mode, ciphertext)
}
// EncryptWithIVHex 与 EncryptWithIV 相同,返回大写十六进制字符串。
func EncryptWithIVHex(block cipher.Block, mode Mode, plaintext, iv []byte) (string, error) {
ciphertext, err := EncryptWithIV(block, mode, plaintext, iv)
if err != nil {
return "", err
}
return strings.ToUpper(hex.EncodeToString(ciphertext)), nil
}
// DecryptWithIVHex 解密十六进制密文,使用调用方提供的 IV。
func DecryptWithIVHex(block cipher.Block, mode Mode, ciphertextHex string, iv []byte) ([]byte, error) {
ciphertext, err := decodeHex(ciphertextHex)
if err != nil {
return nil, err
}
return DecryptWithIV(block, mode, ciphertext, iv)
}
func decodeHex(s string) ([]byte, error) {
decoded, err := hex.DecodeString(strings.TrimSpace(s))
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidHex, err)
}
return decoded, nil
}