完善不同加密方法的使用
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package cipherx_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/des"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/encryptx/cipherx"
|
||||
)
|
||||
|
||||
// newBlock 按分组长度需要构造 AES(16 字节分组)或 DES(8 字节分组)分组密码。
|
||||
func newBlock(useDES bool, key []byte) (cipher.Block, error) {
|
||||
if useDES {
|
||||
return des.NewCipher(key)
|
||||
}
|
||||
return aes.NewCipher(key)
|
||||
}
|
||||
|
||||
func mustBlock(t *testing.T, useDES bool, key []byte) cipher.Block {
|
||||
t.Helper()
|
||||
b, err := newBlock(useDES, key)
|
||||
if err != nil {
|
||||
t.Fatalf("构造分组失败: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestModeMetadata(t *testing.T) {
|
||||
cases := []struct {
|
||||
mode cipherx.Mode
|
||||
name string
|
||||
usesIV bool
|
||||
needsPad bool
|
||||
}{
|
||||
{cipherx.ECB, "ECB", false, true},
|
||||
{cipherx.CBC, "CBC", true, true},
|
||||
{cipherx.CFB, "CFB", true, false},
|
||||
{cipherx.OFB, "OFB", true, false},
|
||||
{cipherx.CTR, "CTR", true, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.mode.String(); got != c.name {
|
||||
t.Errorf("Mode(%d).String() = %q, 期望 %q", int(c.mode), got, c.name)
|
||||
}
|
||||
if !c.mode.Valid() {
|
||||
t.Errorf("%s.Valid() = false, 期望 true", c.name)
|
||||
}
|
||||
if got := c.mode.UsesIV(); got != c.usesIV {
|
||||
t.Errorf("%s.UsesIV() = %v, 期望 %v", c.name, got, c.usesIV)
|
||||
}
|
||||
if got := c.mode.RequiresPadding(); got != c.needsPad {
|
||||
t.Errorf("%s.RequiresPadding() = %v, 期望 %v", c.name, got, c.needsPad)
|
||||
}
|
||||
}
|
||||
|
||||
if cipherx.Mode(99).Valid() {
|
||||
t.Error("Mode(99).Valid() = true, 期望 false")
|
||||
}
|
||||
if got := cipherx.Mode(99).String(); got != "Mode(99)" {
|
||||
t.Errorf("Mode(99).String() = %q, 期望 %q", got, "Mode(99)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPKCS7PadUnpadRoundTrip(t *testing.T) {
|
||||
for _, blockSize := range []int{8, 16} {
|
||||
for length := 0; length <= 2*blockSize+1; length++ {
|
||||
original := bytes.Repeat([]byte{0xAB}, length)
|
||||
padded, err := cipherx.PKCS7Pad(original, blockSize)
|
||||
if err != nil {
|
||||
t.Fatalf("PKCS7Pad(len=%d, bs=%d) 失败: %v", length, blockSize, err)
|
||||
}
|
||||
if len(padded)%blockSize != 0 {
|
||||
t.Fatalf("填充后长度 %d 不是 %d 的整数倍", len(padded), blockSize)
|
||||
}
|
||||
if len(padded) <= len(original) {
|
||||
if length%blockSize == 0 {
|
||||
// 恰好对齐时必须额外填充一整个分组
|
||||
t.Errorf("len=%d bs=%d 对齐时未补充完整分组", length, blockSize)
|
||||
}
|
||||
}
|
||||
// 入参不得被修改
|
||||
if !bytes.Equal(original, bytes.Repeat([]byte{0xAB}, length)) {
|
||||
t.Fatalf("PKCS7Pad 修改了入参")
|
||||
}
|
||||
|
||||
unpadded, err := cipherx.PKCS7Unpad(padded, blockSize)
|
||||
if err != nil {
|
||||
t.Fatalf("PKCS7Unpad(len=%d, bs=%d) 失败: %v", length, blockSize, err)
|
||||
}
|
||||
if !bytes.Equal(unpadded, original) {
|
||||
t.Errorf("往返不一致: 得到 %x, 期望 %x", unpadded, original)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPKCS7PadInvalidBlockSize(t *testing.T) {
|
||||
for _, blockSize := range []int{-1, 0, 256, 1000} {
|
||||
if _, err := cipherx.PKCS7Pad([]byte("x"), blockSize); err == nil {
|
||||
t.Errorf("PKCS7Pad(bs=%d) 期望错误, 得到 nil", blockSize)
|
||||
} else if !errors.Is(err, cipherx.ErrInvalidBlockSize) {
|
||||
t.Errorf("PKCS7Pad(bs=%d) 错误类型不对: %v", blockSize, err)
|
||||
}
|
||||
if _, err := cipherx.PKCS7Unpad([]byte("12345678"), blockSize); err == nil {
|
||||
t.Errorf("PKCS7Unpad(bs=%d) 期望错误, 得到 nil", blockSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPKCS7UnpadUniformError 验证所有填充失败原因都返回同一个错误,
|
||||
// 以免调用方通过错误信息区分失败原因(padding oracle)。
|
||||
func TestPKCS7UnpadUniformError(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"空数据", nil},
|
||||
{"长度不是分组整数倍", []byte{1, 2, 3}},
|
||||
{"填充字节为 0", append(bytes.Repeat([]byte{0x00}, 15), 0x00)},
|
||||
{"填充长度超出分组", append(bytes.Repeat([]byte{0x00}, 15), 0x11)},
|
||||
{"填充字节不一致", append(bytes.Repeat([]byte{0x00}, 13), 0x03, 0x03, 0x04)},
|
||||
{"全零块", bytes.Repeat([]byte{0x00}, 16)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := cipherx.PKCS7Unpad(c.data, 16)
|
||||
if err == nil {
|
||||
t.Errorf("%s: 期望错误, 得到 nil", c.name)
|
||||
continue
|
||||
}
|
||||
if !errors.Is(err, cipherx.ErrInvalidPadding) {
|
||||
t.Errorf("%s: 期望 ErrInvalidPadding, 得到 %v", c.name, err)
|
||||
}
|
||||
if err.Error() != cipherx.ErrInvalidPadding.Error() {
|
||||
t.Errorf("%s: 错误信息泄漏了失败原因: %q", c.name, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPKCS7UnpadReturnsCopy 验证去填充返回的切片不与入参共享底层数组。
|
||||
func TestPKCS7UnpadReturnsCopy(t *testing.T) {
|
||||
data := append(bytes.Repeat([]byte{0x41}, 15), 0x01)
|
||||
out, err := cipherx.PKCS7Unpad(data, 16)
|
||||
if err != nil {
|
||||
t.Fatalf("PKCS7Unpad 失败: %v", err)
|
||||
}
|
||||
out[0] = 0xFF
|
||||
if data[0] != 0x41 {
|
||||
t.Error("PKCS7Unpad 返回的切片与入参共享底层数组")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllModesRoundTrip 覆盖所有模式 × 两种分组长度 × 多种明文长度。
|
||||
func TestAllModesRoundTrip(t *testing.T) {
|
||||
modes := []cipherx.Mode{cipherx.ECB, cipherx.CBC, cipherx.CFB, cipherx.OFB, cipherx.CTR}
|
||||
|
||||
plaintexts := [][]byte{
|
||||
{},
|
||||
[]byte("a"),
|
||||
[]byte("0123456789abcde"), // 15 字节
|
||||
[]byte("0123456789abcdef"), // 16 字节
|
||||
[]byte("0123456789abcdefg"), // 17 字节
|
||||
bytes.Repeat([]byte("x"), 1000),
|
||||
}
|
||||
|
||||
for _, useDES := range []bool{false, true} {
|
||||
var key []byte
|
||||
if useDES {
|
||||
key = bytes.Repeat([]byte{0x22}, 8)
|
||||
} else {
|
||||
key = bytes.Repeat([]byte{0x11}, 16)
|
||||
}
|
||||
algName := "AES"
|
||||
if useDES {
|
||||
algName = "DES"
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
for i, plaintext := range plaintexts {
|
||||
block, err := newBlock(useDES, key)
|
||||
if err != nil {
|
||||
t.Fatalf("构造分组失败: %v", err)
|
||||
}
|
||||
ciphertext, err := cipherx.Encrypt(block, mode, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("%s-%s 明文#%d 加密失败: %v", algName, mode, i, err)
|
||||
}
|
||||
|
||||
block2, err := newBlock(useDES, key)
|
||||
if err != nil {
|
||||
t.Fatalf("构造分组失败: %v", err)
|
||||
}
|
||||
decrypted, err := cipherx.Decrypt(block2, mode, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("%s-%s 明文#%d 解密失败: %v", algName, mode, i, err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Errorf("%s-%s 明文#%d 往返不一致: 得到 %q, 期望 %q",
|
||||
algName, mode, i, decrypted, plaintext)
|
||||
}
|
||||
|
||||
// IV 前置格式:除 ECB 外,密文前缀必须等于 IV
|
||||
if mode.UsesIV() {
|
||||
bs := block.BlockSize()
|
||||
if len(ciphertext) < bs {
|
||||
t.Fatalf("%s-%s 密文短于分组长度", algName, mode)
|
||||
}
|
||||
iv := ciphertext[:bs]
|
||||
body := ciphertext[bs:]
|
||||
block3, _ := newBlock(useDES, key)
|
||||
decrypted2, err := cipherx.DecryptWithIV(block3, mode, body, iv)
|
||||
if err != nil {
|
||||
t.Fatalf("%s-%s WithIV 解密失败: %v", algName, mode, err)
|
||||
}
|
||||
if !bytes.Equal(decrypted2, plaintext) {
|
||||
t.Errorf("%s-%s WithIV 往返不一致", algName, mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMalformedCiphertextNeverPanics 是 P0-3 的回归测试。
|
||||
//
|
||||
// 旧实现在 ECB 解密时直接用 len(密文) 循环并按分组切片,密文长度不是分组
|
||||
// 整数倍时会触发 "slice bounds out of range" panic,攻击者用一段畸形密文即可
|
||||
// 让服务崩溃。现在必须返回 ErrNotBlockAligned。
|
||||
func TestMalformedCiphertextNeverPanics(t *testing.T) {
|
||||
modes := []cipherx.Mode{cipherx.ECB, cipherx.CBC, cipherx.CFB, cipherx.OFB, cipherx.CTR}
|
||||
|
||||
for _, useDES := range []bool{false, true} {
|
||||
key := bytes.Repeat([]byte{0x33}, 16)
|
||||
bs := 16
|
||||
if useDES {
|
||||
key = bytes.Repeat([]byte{0x33}, 8)
|
||||
bs = 8
|
||||
}
|
||||
block := mustBlock(t, useDES, key)
|
||||
|
||||
for _, mode := range modes {
|
||||
// 长度 0 到 3 个分组 + 半个分组,全部枚举
|
||||
for n := 0; n <= 3*bs+bs/2; n++ {
|
||||
input := bytes.Repeat([]byte{0x7F}, n)
|
||||
// 不应 panic;返回什么都行,但必须安全返回
|
||||
_, _ = cipherx.Decrypt(block, mode, input)
|
||||
_, _ = cipherx.DecryptWithIV(block, mode, input, bytes.Repeat([]byte{0x01}, bs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestECBAndCBCRejectNonAligned 精确断言非对齐密文返回 ErrNotBlockAligned。
|
||||
func TestECBAndCBCRejectNonAligned(t *testing.T) {
|
||||
block := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
|
||||
// 20 字节:不是 16 的整数倍,正是旧实现 panic 的输入
|
||||
nonAligned := bytes.Repeat([]byte{0xAB}, 20)
|
||||
|
||||
if _, err := cipherx.Decrypt(block, cipherx.ECB, nonAligned); !errors.Is(err, cipherx.ErrNotBlockAligned) {
|
||||
t.Errorf("ECB 非对齐密文: 期望 ErrNotBlockAligned, 得到 %v", err)
|
||||
}
|
||||
|
||||
// CBC 的 IV 前置格式:20 字节去掉 16 字节 IV 后剩 4 字节,同样非对齐
|
||||
if _, err := cipherx.Decrypt(block, cipherx.CBC, nonAligned); !errors.Is(err, cipherx.ErrNotBlockAligned) {
|
||||
t.Errorf("CBC 非对齐密文: 期望 ErrNotBlockAligned, 得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyCiphertext(t *testing.T) {
|
||||
block := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
for _, mode := range []cipherx.Mode{cipherx.ECB, cipherx.CBC} {
|
||||
if _, err := cipherx.Decrypt(block, mode, nil); err == nil {
|
||||
t.Errorf("%s 空密文: 期望错误, 得到 nil", mode)
|
||||
}
|
||||
}
|
||||
// IV 模式:空密文不足以容纳 IV
|
||||
for _, mode := range []cipherx.Mode{cipherx.CFB, cipherx.OFB, cipherx.CTR} {
|
||||
if _, err := cipherx.Decrypt(block, mode, nil); !errors.Is(err, cipherx.ErrCiphertextTooShort) {
|
||||
t.Errorf("%s 空密文: 期望 ErrCiphertextTooShort, 得到 %v", mode, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilBlockAndInvalidMode(t *testing.T) {
|
||||
if _, err := cipherx.Encrypt(nil, cipherx.CBC, []byte("x")); !errors.Is(err, cipherx.ErrNilBlock) {
|
||||
t.Errorf("nil block: 期望 ErrNilBlock, 得到 %v", err)
|
||||
}
|
||||
if _, err := cipherx.Decrypt(nil, cipherx.CBC, []byte("x")); !errors.Is(err, cipherx.ErrNilBlock) {
|
||||
t.Errorf("nil block: 期望 ErrNilBlock, 得到 %v", err)
|
||||
}
|
||||
|
||||
block := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
if _, err := cipherx.Encrypt(block, cipherx.Mode(42), []byte("x")); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("非法模式: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
if _, err := cipherx.Decrypt(block, cipherx.Mode(-1), []byte("x")); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("非法模式: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestECBRejectsIV(t *testing.T) {
|
||||
block := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
iv := bytes.Repeat([]byte{0x02}, 16)
|
||||
if _, err := cipherx.EncryptWithIV(block, cipherx.ECB, []byte("x"), iv); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("ECB WithIV: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
if _, err := cipherx.DecryptWithIV(block, cipherx.ECB, []byte("x"), iv); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("ECB WithIV 解密: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidIVSize(t *testing.T) {
|
||||
block := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
for _, ivLen := range []int{0, 8, 15, 17, 32} {
|
||||
iv := bytes.Repeat([]byte{0x02}, ivLen)
|
||||
if _, err := cipherx.EncryptWithIV(block, cipherx.CBC, []byte("x"), iv); !errors.Is(err, cipherx.ErrInvalidIVSize) {
|
||||
t.Errorf("IV 长度 %d: 期望 ErrInvalidIVSize, 得到 %v", ivLen, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRandomIVPerEncryption 验证相同明文两次加密得到不同密文(IV 随机)。
|
||||
func TestRandomIVPerEncryption(t *testing.T) {
|
||||
block := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
plaintext := []byte("same plaintext")
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for i := 0; i < 32; i++ {
|
||||
ciphertext, err := cipherx.Encrypt(block, cipherx.CBC, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("加密失败: %v", err)
|
||||
}
|
||||
if seen[string(ciphertext)] {
|
||||
t.Fatal("相同明文产生了完全相同的密文,IV 可能未随机化")
|
||||
}
|
||||
seen[string(ciphertext)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrongKeyDoesNotPanic 验证错误密钥不会 panic(CBC 通常会因填充校验失败报错)。
|
||||
func TestWrongKeyDoesNotPanic(t *testing.T) {
|
||||
right := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
wrong := mustBlock(t, false, bytes.Repeat([]byte{0x99}, 16))
|
||||
|
||||
ciphertext, err := cipherx.Encrypt(right, cipherx.CBC, []byte("secret data"))
|
||||
if err != nil {
|
||||
t.Fatalf("加密失败: %v", err)
|
||||
}
|
||||
// 允许返回错误(填充校验失败)或返回不同的明文,但绝不能 panic 或返回原文
|
||||
decrypted, err := cipherx.Decrypt(wrong, cipherx.CBC, ciphertext)
|
||||
if err == nil && bytes.Equal(decrypted, []byte("secret data")) {
|
||||
t.Error("错误密钥解出了正确明文")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexHelpers(t *testing.T) {
|
||||
block := mustBlock(t, false, bytes.Repeat([]byte{0x11}, 16))
|
||||
plaintext := []byte("hex round trip 中文")
|
||||
|
||||
for _, mode := range []cipherx.Mode{cipherx.ECB, cipherx.CBC, cipherx.CFB, cipherx.OFB, cipherx.CTR} {
|
||||
hexStr, err := cipherx.EncryptHex(block, mode, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("%s EncryptHex 失败: %v", mode, err)
|
||||
}
|
||||
// 必须是大写、无换行
|
||||
for _, r := range hexStr {
|
||||
if (r < '0' || r > '9') && (r < 'A' || r > 'F') {
|
||||
t.Fatalf("%s EncryptHex 输出含非大写十六进制字符: %q", mode, hexStr)
|
||||
}
|
||||
}
|
||||
|
||||
decrypted, err := cipherx.DecryptHex(block, mode, hexStr)
|
||||
if err != nil {
|
||||
t.Fatalf("%s DecryptHex 失败: %v", mode, err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Errorf("%s Hex 往返不一致", mode)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := cipherx.DecryptHex(block, cipherx.CBC, "这不是十六进制"); err == nil {
|
||||
t.Error("非法十六进制: 期望错误, 得到 nil")
|
||||
}
|
||||
if _, err := cipherx.DecryptHex(block, cipherx.CBC, "ABC"); err == nil {
|
||||
t.Error("奇数长度十六进制: 期望错误, 得到 nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIV(t *testing.T) {
|
||||
iv, err := cipherx.NewIV(16)
|
||||
if err != nil {
|
||||
t.Fatalf("NewIV 失败: %v", err)
|
||||
}
|
||||
if len(iv) != 16 {
|
||||
t.Errorf("NewIV(16) 长度 = %d, 期望 16", len(iv))
|
||||
}
|
||||
if _, err := cipherx.NewIV(0); err == nil {
|
||||
t.Error("NewIV(0): 期望错误, 得到 nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package cipherx
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrInvalidKeySize 表示密钥长度不被算法接受。
|
||||
var ErrInvalidKeySize = errors.New("cipherx: invalid key size")
|
||||
|
||||
// Suite 把一个具体的分组密码算法(分组构造方式 + 允许的密钥长度)与通用模式
|
||||
// 操作绑定在一起。
|
||||
//
|
||||
// 引入 Suite 的目的是消除各算法包之间重复的模式实现:aesx / desx / tdesx /
|
||||
// blowfishx 只需声明「如何用密钥构造分组」,ECB/CBC/CFB/OFB/CTR 的加解密、
|
||||
// 填充、IV 处理与长度校验全部由 Suite 统一提供。
|
||||
type Suite struct {
|
||||
name string
|
||||
newBlock func(key []byte) (cipher.Block, error)
|
||||
keySizes []int
|
||||
blockSize int
|
||||
}
|
||||
|
||||
// NewSuite 创建一个算法套件。
|
||||
//
|
||||
// name 用于错误信息(如 "AES");newBlock 通常是标准库的 aes.NewCipher /
|
||||
// des.NewCipher / des.NewTripleDESCipher 或 x/crypto 的 blowfish.NewCipher;
|
||||
// blockSize 是该算法的分组长度;keySizes 为允许的密钥长度(字节)。
|
||||
// keySizes 留空表示密钥长度由 newBlock 自行校验(适用于 Blowfish 这类
|
||||
// 密钥长度可变且非离散取值的算法)。
|
||||
func NewSuite(name string, newBlock func(key []byte) (cipher.Block, error), blockSize int, keySizes ...int) *Suite {
|
||||
sizes := make([]int, len(keySizes))
|
||||
copy(sizes, keySizes)
|
||||
return &Suite{
|
||||
name: name,
|
||||
newBlock: newBlock,
|
||||
keySizes: sizes,
|
||||
blockSize: blockSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回算法名称。
|
||||
func (s *Suite) Name() string { return s.name }
|
||||
|
||||
// BlockSize 返回分组长度(字节)。
|
||||
func (s *Suite) BlockSize() int { return s.blockSize }
|
||||
|
||||
// KeySizes 返回允许的密钥长度(字节)。若为空,表示不限制为离散取值。
|
||||
func (s *Suite) KeySizes() []int {
|
||||
out := make([]int, len(s.keySizes))
|
||||
copy(out, s.keySizes)
|
||||
return out
|
||||
}
|
||||
|
||||
// NewBlock 校验密钥长度后构造分组密码。
|
||||
//
|
||||
// 所有算法包都应通过本方法构造分组,以便统一错误语义:密钥长度非法时返回的
|
||||
// 错误可以用 errors.Is(err, ErrInvalidKeySize) 判断。
|
||||
func (s *Suite) NewBlock(key []byte) (cipher.Block, error) {
|
||||
if len(s.keySizes) > 0 {
|
||||
ok := false
|
||||
for _, size := range s.keySizes {
|
||||
if len(key) == size {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: 密钥长度 %d 非法, 允许 %v: %w",
|
||||
s.name, len(key), s.keySizes, ErrInvalidKeySize)
|
||||
}
|
||||
}
|
||||
|
||||
block, err := s.newBlock(key)
|
||||
if err != nil {
|
||||
// 底层实现(如 blowfish)也会校验密钥长度,统一归类。
|
||||
return nil, fmt.Errorf("%s: 构造分组密码失败: %w", s.name, errors.Join(ErrInvalidKeySize, err))
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
||||
// Encrypt 用随机 IV 加密(IV 前置,格式为 iv||ciphertext;ECB 无 IV)。
|
||||
func (s *Suite) Encrypt(mode Mode, plaintext, key []byte) ([]byte, error) {
|
||||
block, err := s.NewBlock(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Encrypt(block, mode, plaintext)
|
||||
}
|
||||
|
||||
// Decrypt 解密 Encrypt 产生的数据(IV 前置格式)。
|
||||
func (s *Suite) Decrypt(mode Mode, ciphertext, key []byte) ([]byte, error) {
|
||||
block, err := s.NewBlock(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Decrypt(block, mode, ciphertext)
|
||||
}
|
||||
|
||||
// EncryptWithIV 使用调用方提供的 IV 加密,返回值不含 IV。
|
||||
func (s *Suite) EncryptWithIV(mode Mode, plaintext, key, iv []byte) ([]byte, error) {
|
||||
block, err := s.NewBlock(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return EncryptWithIV(block, mode, plaintext, iv)
|
||||
}
|
||||
|
||||
// DecryptWithIV 使用调用方提供的 IV 解密。
|
||||
func (s *Suite) DecryptWithIV(mode Mode, ciphertext, key, iv []byte) ([]byte, error) {
|
||||
block, err := s.NewBlock(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DecryptWithIV(block, mode, ciphertext, iv)
|
||||
}
|
||||
|
||||
// EncryptHex 与 Encrypt 相同,返回大写十六进制字符串。
|
||||
func (s *Suite) EncryptHex(mode Mode, plaintext, key []byte) (string, error) {
|
||||
ciphertext, err := s.Encrypt(mode, plaintext, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.ToUpper(hex.EncodeToString(ciphertext)), nil
|
||||
}
|
||||
|
||||
// DecryptHex 解密十六进制密文,大小写均可。
|
||||
func (s *Suite) DecryptHex(mode Mode, ciphertextHex string, key []byte) ([]byte, error) {
|
||||
ciphertext, err := decodeHex(ciphertextHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Decrypt(mode, ciphertext, key)
|
||||
}
|
||||
|
||||
// EncryptWithIVHex 与 EncryptWithIV 相同,返回大写十六进制字符串。
|
||||
func (s *Suite) EncryptWithIVHex(mode Mode, plaintext, key, iv []byte) (string, error) {
|
||||
ciphertext, err := s.EncryptWithIV(mode, plaintext, key, iv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.ToUpper(hex.EncodeToString(ciphertext)), nil
|
||||
}
|
||||
|
||||
// DecryptWithIVHex 解密十六进制密文,使用调用方提供的 IV。
|
||||
func (s *Suite) DecryptWithIVHex(mode Mode, ciphertextHex string, key, iv []byte) ([]byte, error) {
|
||||
ciphertext, err := decodeHex(ciphertextHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.DecryptWithIV(mode, ciphertext, key, iv)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package cipherx_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/encryptx/cipherx"
|
||||
)
|
||||
|
||||
// testSuite 返回一个以 AES 为实现、覆盖两种密钥长度的套件。
|
||||
func testSuite() *cipherx.Suite {
|
||||
return cipherx.NewSuite("TEST", aes.NewCipher, 16, 16, 32)
|
||||
}
|
||||
|
||||
func TestSuiteMetadata(t *testing.T) {
|
||||
suite := testSuite()
|
||||
|
||||
if got := suite.Name(); got != "TEST" {
|
||||
t.Errorf("Name() = %q, 期望 TEST", got)
|
||||
}
|
||||
if got := suite.BlockSize(); got != 16 {
|
||||
t.Errorf("BlockSize() = %d, 期望 16", got)
|
||||
}
|
||||
sizes := suite.KeySizes()
|
||||
if len(sizes) != 2 || sizes[0] != 16 || sizes[1] != 32 {
|
||||
t.Errorf("KeySizes() = %v, 期望 [16 32]", sizes)
|
||||
}
|
||||
|
||||
// KeySizes 必须返回副本,改动它不应影响套件本身
|
||||
sizes[0] = 999
|
||||
if again := suite.KeySizes(); again[0] != 16 {
|
||||
t.Error("KeySizes 返回了内部切片,调用方可意外篡改套件配置")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuiteNewBlockValidatesKeyLength(t *testing.T) {
|
||||
suite := testSuite()
|
||||
|
||||
for _, size := range []int{16, 32} {
|
||||
key := bytes.Repeat([]byte{0x11}, size)
|
||||
if _, err := suite.NewBlock(key); err != nil {
|
||||
t.Errorf("NewBlock(密钥长度 %d) 失败: %v", size, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, size := range []int{0, 8, 15, 17, 24, 31, 64} {
|
||||
key := bytes.Repeat([]byte{0x11}, size)
|
||||
if _, err := suite.NewBlock(key); !errors.Is(err, cipherx.ErrInvalidKeySize) {
|
||||
t.Errorf("NewBlock(密钥长度 %d): 期望 ErrInvalidKeySize, 得到 %v", size, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSuiteWithoutKeySizes 验证未声明密钥长度时由底层实现校验,
|
||||
// 且失败仍被统一归类为 ErrInvalidKeySize(Blowfish 走的就是这条路径)。
|
||||
func TestSuiteWithoutKeySizes(t *testing.T) {
|
||||
suite := cipherx.NewSuite("NOCHECK", aes.NewCipher, 16)
|
||||
|
||||
if _, err := suite.NewBlock(bytes.Repeat([]byte{0x11}, 16)); err != nil {
|
||||
t.Fatalf("合法的 16 字节密钥被拒绝: %v", err)
|
||||
}
|
||||
if _, err := suite.NewBlock(bytes.Repeat([]byte{0x11}, 17)); !errors.Is(err, cipherx.ErrInvalidKeySize) {
|
||||
t.Errorf("非法密钥长度: 期望 ErrInvalidKeySize, 得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuiteEncryptDecryptAllModes(t *testing.T) {
|
||||
suite := testSuite()
|
||||
key := bytes.Repeat([]byte{0x42}, 32)
|
||||
plaintext := []byte("suite 往返测试 payload")
|
||||
|
||||
modes := []cipherx.Mode{cipherx.ECB, cipherx.CBC, cipherx.CFB, cipherx.OFB, cipherx.CTR}
|
||||
for _, mode := range modes {
|
||||
ciphertext, err := suite.Encrypt(mode, plaintext, key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Encrypt 失败: %v", mode, err)
|
||||
}
|
||||
decrypted, err := suite.Decrypt(mode, ciphertext, key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Decrypt 失败: %v", mode, err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Errorf("%s: 往返不一致", mode)
|
||||
}
|
||||
}
|
||||
|
||||
// 密钥长度非法时必须在构造阶段就失败
|
||||
if _, err := suite.Encrypt(cipherx.CBC, plaintext, []byte("short")); !errors.Is(err, cipherx.ErrInvalidKeySize) {
|
||||
t.Errorf("Encrypt 非法密钥: 期望 ErrInvalidKeySize, 得到 %v", err)
|
||||
}
|
||||
if _, err := suite.Decrypt(cipherx.CBC, []byte("x"), []byte("short")); !errors.Is(err, cipherx.ErrInvalidKeySize) {
|
||||
t.Errorf("Decrypt 非法密钥: 期望 ErrInvalidKeySize, 得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuiteWithIV(t *testing.T) {
|
||||
suite := testSuite()
|
||||
key := bytes.Repeat([]byte{0x42}, 16)
|
||||
iv := bytes.Repeat([]byte{0x7E}, 16)
|
||||
plaintext := []byte("explicit iv via suite")
|
||||
|
||||
for _, mode := range []cipherx.Mode{cipherx.CBC, cipherx.CFB, cipherx.OFB, cipherx.CTR} {
|
||||
ciphertext, err := suite.EncryptWithIV(mode, plaintext, key, iv)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: EncryptWithIV 失败: %v", mode, err)
|
||||
}
|
||||
decrypted, err := suite.DecryptWithIV(mode, ciphertext, key, iv)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: DecryptWithIV 失败: %v", mode, err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Errorf("%s: WithIV 往返不一致", mode)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := suite.EncryptWithIV(cipherx.CBC, plaintext, key, []byte("bad")); !errors.Is(err, cipherx.ErrInvalidIVSize) {
|
||||
t.Errorf("非法 IV 长度: 期望 ErrInvalidIVSize, 得到 %v", err)
|
||||
}
|
||||
if _, err := suite.EncryptWithIV(cipherx.ECB, plaintext, key, iv); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("ECB 传 IV: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuiteHexHelpers(t *testing.T) {
|
||||
suite := testSuite()
|
||||
key := bytes.Repeat([]byte{0x42}, 16)
|
||||
iv := bytes.Repeat([]byte{0x7E}, 16)
|
||||
plaintext := []byte("hex 辅助函数")
|
||||
|
||||
for _, mode := range []cipherx.Mode{cipherx.ECB, cipherx.CBC, cipherx.CFB, cipherx.OFB, cipherx.CTR} {
|
||||
hexStr, err := suite.EncryptHex(mode, plaintext, key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: EncryptHex 失败: %v", mode, err)
|
||||
}
|
||||
decrypted, err := suite.DecryptHex(mode, hexStr, key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: DecryptHex 失败: %v", mode, err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Errorf("%s: Hex 往返不一致", mode)
|
||||
}
|
||||
}
|
||||
|
||||
// 带显式 IV 的十六进制辅助函数
|
||||
for _, mode := range []cipherx.Mode{cipherx.CBC, cipherx.CFB, cipherx.OFB, cipherx.CTR} {
|
||||
hexStr, err := suite.EncryptWithIVHex(mode, plaintext, key, iv)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: EncryptWithIVHex 失败: %v", mode, err)
|
||||
}
|
||||
decrypted, err := suite.DecryptWithIVHex(mode, hexStr, key, iv)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: DecryptWithIVHex 失败: %v", mode, err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Errorf("%s: WithIVHex 往返不一致", mode)
|
||||
}
|
||||
}
|
||||
|
||||
// 非法十六进制
|
||||
if _, err := suite.DecryptHex(cipherx.CBC, "zz", key); err == nil {
|
||||
t.Error("非法十六进制期望错误")
|
||||
}
|
||||
if _, err := suite.EncryptWithIVHex(cipherx.CBC, plaintext, []byte("short"), iv); !errors.Is(err, cipherx.ErrInvalidKeySize) {
|
||||
t.Errorf("非法密钥: 期望 ErrInvalidKeySize, 得到 %v", err)
|
||||
}
|
||||
if _, err := suite.DecryptWithIVHex(cipherx.CBC, "zz", key, iv); err == nil {
|
||||
t.Error("非法十六进制期望错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSuiteMemberVariants 覆盖 Suite 各方法的失败分支,确保错误在
|
||||
// 构造阶段就被发现,而不是留到更难排查的位置。
|
||||
func TestSuiteMemberVariants(t *testing.T) {
|
||||
suite := testSuite()
|
||||
key := bytes.Repeat([]byte{0x42}, 16)
|
||||
iv := bytes.Repeat([]byte{0x7E}, 16)
|
||||
|
||||
if _, err := suite.Encrypt(cipherx.Mode(99), []byte("x"), key); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("Encrypt 非法模式: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
if _, err := suite.Decrypt(cipherx.Mode(99), []byte("x"), key); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("Decrypt 非法模式: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
if _, err := suite.EncryptWithIV(cipherx.Mode(99), []byte("x"), key, iv); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("EncryptWithIV 非法模式: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
if _, err := suite.DecryptWithIV(cipherx.Mode(99), []byte("x"), key, iv); !errors.Is(err, cipherx.ErrInvalidMode) {
|
||||
t.Errorf("DecryptWithIV 非法模式: 期望 ErrInvalidMode, 得到 %v", err)
|
||||
}
|
||||
|
||||
if _, err := suite.EncryptHex(cipherx.CBC, []byte("x"), []byte("short")); !errors.Is(err, cipherx.ErrInvalidKeySize) {
|
||||
t.Errorf("EncryptHex 非法密钥: 期望 ErrInvalidKeySize, 得到 %v", err)
|
||||
}
|
||||
if _, err := suite.DecryptWithIV(cipherx.CBC, []byte("x"), key, []byte("bad")); !errors.Is(err, cipherx.ErrInvalidIVSize) {
|
||||
t.Errorf("DecryptWithIV 非法 IV: 期望 ErrInvalidIVSize, 得到 %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user