// 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 }