完善不同加密方法的使用
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
package encryptx_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/encryptx"
|
||||
"code.yun.ink/pkg/encryptx/aesx"
|
||||
"code.yun.ink/pkg/encryptx/chachax"
|
||||
)
|
||||
|
||||
// 本文件固化第二轮复查中发现的问题,防止回归。
|
||||
|
||||
// TestUnexpectedKeyRejected 验证给不使用密钥的算法传密钥会被明确拒绝。
|
||||
//
|
||||
// 旧行为是静默忽略:调用方把 BASE64 或哈希误当成加密算法时,会以为数据已经
|
||||
// 受到密钥保护,实际上完全没有。这类"看起来做了加密"的误用必须报错。
|
||||
func TestUnexpectedKeyRejected(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x01}, 32)
|
||||
|
||||
keyless := []encryptx.EncryptType{
|
||||
encryptx.MD5, encryptx.SHA256, encryptx.SHA3_512, encryptx.BLAKE2B_256,
|
||||
encryptx.BASE64, encryptx.BASE64_URL, encryptx.HEX,
|
||||
encryptx.BCRYPT, encryptx.ARGON2ID,
|
||||
encryptx.PBKDF2,
|
||||
}
|
||||
|
||||
for _, encryptType := range keyless {
|
||||
options := encryptx.Options{}
|
||||
if encryptType == encryptx.PBKDF2 {
|
||||
// PBKDF2 需要盐值,否则会先报缺参,掩盖我们要测的分支
|
||||
options["salt"] = bytes.Repeat([]byte{0x02}, 16)
|
||||
options["iterations"] = 10000
|
||||
}
|
||||
if encryptType == encryptx.ARGON2ID {
|
||||
options["time"] = 1
|
||||
options["memory"] = 8 * 1024
|
||||
options["threads"] = 1
|
||||
}
|
||||
|
||||
_, err := encryptx.Encrypt(encryptType, "payload", key, options)
|
||||
if !errors.Is(err, encryptx.ErrUnexpectedKey) {
|
||||
t.Errorf("%s 传入密钥: 期望 ErrUnexpectedKey, 得到 %v", encryptType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 反向:真正需要密钥的算法仍然照常工作
|
||||
aesKey, err := encryptx.GenerateKey(encryptx.AES_GCM, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey 失败: %v", err)
|
||||
}
|
||||
if _, err := encryptx.Encrypt(encryptx.AES_GCM, "payload", aesKey, nil); err != nil {
|
||||
t.Errorf("AES-GCM 带密钥加密失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMalformedCiphertextClassifiedAsInvalidInput 验证门面的错误分类自洽。
|
||||
//
|
||||
// 密文的编码与长度问题都应该能通过 errors.Is(err, ErrInvalidInput) 命中,
|
||||
// 这样调用方才有一套统一的"输入不合法"判定标准。
|
||||
func TestMalformedCiphertextClassifiedAsInvalidInput(t *testing.T) {
|
||||
aesKey, _ := encryptx.GenerateKey(encryptx.AES_CBC, nil)
|
||||
gcmKey, _ := encryptx.GenerateKey(encryptx.AES_GCM, nil)
|
||||
chachaKey, _ := encryptx.GenerateKey(encryptx.CHACHA20_POLY1305, nil)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
encryptType encryptx.EncryptType
|
||||
key []byte
|
||||
input string
|
||||
}{
|
||||
{"CBC 空密文", encryptx.AES_CBC, aesKey, ""},
|
||||
{"CBC 过短密文", encryptx.AES_CBC, aesKey, "AB"},
|
||||
{"CBC 非分组对齐", encryptx.AES_CBC, aesKey, strings.Repeat("AB", 17)},
|
||||
{"CBC 非法十六进制", encryptx.AES_CBC, aesKey, "zz"},
|
||||
{"ECB 非分组对齐", encryptx.AES_ECB, aesKey, strings.Repeat("AB", 17)},
|
||||
{"GCM 缺少分隔符", encryptx.AES_GCM, gcmKey, "abcd"},
|
||||
{"GCM nonce 长度错误", encryptx.AES_GCM, gcmKey, "abcd:ef"},
|
||||
{"ChaCha 缺少分隔符", encryptx.CHACHA20_POLY1305, chachaKey, "abcd"},
|
||||
{"Base64 非法", encryptx.BASE64, nil, "!!!!"},
|
||||
{"Hex 奇数长度", encryptx.HEX, nil, "ABC"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
_, err := encryptx.Decrypt(c.encryptType, c.input, c.key, nil)
|
||||
if err == nil {
|
||||
t.Errorf("%s: 期望错误, 得到 nil", c.name)
|
||||
continue
|
||||
}
|
||||
if !errors.Is(err, encryptx.ErrInvalidInput) {
|
||||
t.Errorf("%s: 期望 ErrInvalidInput, 得到 %v", c.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAuthenticationFailureStaysDistinguishable 验证认证失败不会被混进
|
||||
// "输入格式错误"里。
|
||||
//
|
||||
// 若把"密文被篡改"也归类为 ErrInvalidInput,调用方就无法把它和参数写错区分开,
|
||||
// 真正的安全告警会被淹没在普通错误中——这比分类不统一更危险。
|
||||
func TestAuthenticationFailureStaysDistinguishable(t *testing.T) {
|
||||
cases := []struct {
|
||||
encryptType encryptx.EncryptType
|
||||
sentinel error
|
||||
}{
|
||||
{encryptx.AES_GCM, aesx.ErrAuthenticationFailed},
|
||||
{encryptx.CHACHA20_POLY1305, chachax.ErrAuthenticationFailed},
|
||||
{encryptx.XCHACHA20_POLY1305, chachax.ErrAuthenticationFailed},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
key, _ := encryptx.GenerateKey(c.encryptType, nil)
|
||||
ciphertext, err := encryptx.Encrypt(c.encryptType, "authenticated payload", key, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.encryptType, err)
|
||||
}
|
||||
|
||||
// 翻转密文中的一个字符,制造认证失败
|
||||
tampered := []byte(ciphertext)
|
||||
for i := range tampered {
|
||||
if tampered[i] != ':' {
|
||||
if tampered[i] == '0' {
|
||||
tampered[i] = '1'
|
||||
} else {
|
||||
tampered[i] = '0'
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_, err = encryptx.Decrypt(c.encryptType, string(tampered), key, nil)
|
||||
if !errors.Is(err, c.sentinel) {
|
||||
t.Errorf("%s 篡改: 期望 %v, 得到 %v", c.encryptType, c.sentinel, err)
|
||||
}
|
||||
if errors.Is(err, encryptx.ErrInvalidInput) {
|
||||
t.Errorf("%s 篡改被错误地归类为 ErrInvalidInput: %v", c.encryptType, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHKDFConsistentBetweenEncryptAndDeriveKey 验证两条 HKDF 路径对空盐的态度一致。
|
||||
//
|
||||
// 复查发现 Encrypt 强制要求 salt 选项,而 DeriveKey 允许缺省(HKDF 的空盐在标准
|
||||
// 中等价于全零盐),同一算法两条入口行为不一致会误导调用方。
|
||||
func TestHKDFConsistentBetweenEncryptAndDeriveKey(t *testing.T) {
|
||||
secret := []byte("high-entropy-secret-material")
|
||||
|
||||
// DeriveKey 允许空盐,并返回 nil 盐
|
||||
key, salt, err := encryptx.DeriveKey(encryptx.HKDF, secret, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey(HKDF) 无盐失败: %v", err)
|
||||
}
|
||||
if salt != nil {
|
||||
t.Errorf("HKDF 空盐应当返回 nil, 得到 %v", salt)
|
||||
}
|
||||
|
||||
// Encrypt 也必须允许空盐,且结果与 DeriveKey 一致
|
||||
derived, err := encryptx.Encrypt(encryptx.HKDF, string(secret), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt(HKDF) 无盐失败: %v", err)
|
||||
}
|
||||
if !strings.EqualFold(derived, hexEncode(key)) {
|
||||
t.Errorf("Encrypt(HKDF) 与 DeriveKey(HKDF) 结果不一致: %q vs %q", derived, hexEncode(key))
|
||||
}
|
||||
|
||||
// 其余 KDF 仍然必须显式给盐
|
||||
for _, encryptType := range []encryptx.EncryptType{encryptx.PBKDF2, encryptx.SCRYPT} {
|
||||
if _, err := encryptx.Encrypt(encryptType, "password", nil,
|
||||
fastKDFOptions(encryptType)); !errors.Is(err, encryptx.ErrMissingOption) {
|
||||
t.Errorf("%s 缺盐: 期望 ErrMissingOption, 得到 %v", encryptType, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateKeyForAllKDFs 验证四种 KDF 都能生成盐值。
|
||||
//
|
||||
// 复查发现 ARGON2ID 被排除在盐值生成之外,而它同样是需要盐的 KDF(DeriveKey
|
||||
// 路径),同一类算法行为不一致。
|
||||
func TestGenerateKeyForAllKDFs(t *testing.T) {
|
||||
for _, encryptType := range kdfTypes {
|
||||
salt, err := encryptx.GenerateKey(encryptType, encryptx.Options{"saltLength": 16})
|
||||
if err != nil {
|
||||
t.Errorf("%s: GenerateKey 失败: %v", encryptType, err)
|
||||
continue
|
||||
}
|
||||
if len(salt) != 16 {
|
||||
t.Errorf("%s: 盐值长度 = %d, 期望 16", encryptType, len(salt))
|
||||
}
|
||||
|
||||
// 生成的盐必须能用于派生
|
||||
options := fastKDFOptions(encryptType)
|
||||
options["salt"] = salt
|
||||
if _, _, err := encryptx.DeriveKey(encryptType, []byte("password"), options); err != nil {
|
||||
t.Errorf("%s: 用生成的盐派生失败: %v", encryptType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// bcrypt 的盐内嵌在哈希结果中,仍然应当拒绝生成
|
||||
if _, err := encryptx.GenerateKey(encryptx.BCRYPT, nil); err == nil {
|
||||
t.Error("GenerateKey(BCRYPT) 应当返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// hexEncode 是测试用的小工具,避免为一行格式化再引入依赖。
|
||||
func hexEncode(data []byte) string {
|
||||
const digits = "0123456789abcdef"
|
||||
out := make([]byte, 0, len(data)*2)
|
||||
for _, b := range data {
|
||||
out = append(out, digits[b>>4], digits[b&0x0f])
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
Reference in New Issue
Block a user