完善不同加密方法的使用
This commit is contained in:
+174
-44
@@ -1,66 +1,196 @@
|
||||
// Package pbkdf2x 基于 PBKDF2(RFC 8018 / PKCS #5 v2.1)提供口令密钥派生与校验。
|
||||
//
|
||||
// 安全注意事项:
|
||||
// - PBKDF2 只有 CPU 代价、没有内存代价,抗 GPU/ASIC 暴力破解的能力弱于
|
||||
// Argon2id 与 scrypt。新系统的口令存储应优先选用 argon2x,其次 scryptx;
|
||||
// 本包主要用于兼容既有系统与 FIPS 场景。
|
||||
// - 迭代次数必须随硬件能力上调。OWASP 2023 对 PBKDF2-HMAC-SHA256 的推荐值为
|
||||
// 600000,即 DefaultIterations;低于 MinIterations 的配置会被拒绝。
|
||||
// - 盐必须是密码学安全随机数、每个口令独立,长度不低于 MinSaltLength 字节,
|
||||
// 推荐 DefaultSaltLength 字节。盐不是秘密,可与派生结果一起明文存储。
|
||||
// - 派生密钥属于敏感数据,调用方负责及时清零,且不得写入日志或错误信息。
|
||||
//
|
||||
// 本包任何情况下都不会 panic:所有非法参数都返回 error,且错误文本中不含
|
||||
// 口令、盐或密钥材料。
|
||||
package pbkdf2x
|
||||
|
||||
import (
|
||||
"crypto/pbkdf2"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"hash"
|
||||
"io"
|
||||
)
|
||||
|
||||
// GenerateSalt 生成随机盐值
|
||||
func GenerateSalt(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", fmt.Errorf("盐值长度必须大于0")
|
||||
const (
|
||||
// DefaultIterations 是 PBKDF2-HMAC-SHA256 的默认迭代次数,取 OWASP 2023 年
|
||||
// 口令存储建议值 600000。迭代次数越高,离线暴力破解成本越高,派生耗时
|
||||
// 也近似线性增长。
|
||||
DefaultIterations = 600000
|
||||
|
||||
// MinIterations 是允许的最小迭代次数。1 万次迭代对现代 GPU 几乎不构成
|
||||
// 防护,低于该值的请求一律拒绝,避免调用方误传 1 或 1000。
|
||||
MinIterations = 10000
|
||||
|
||||
// MaxIterations 是允许的最大迭代次数,用于拦截误把毫秒、字节数当成
|
||||
// 迭代次数传入的调用(例如 0xFFFFFFFF),避免单次请求长时间占用 CPU
|
||||
// 而演变成拒绝服务放大面。
|
||||
MaxIterations = 10000000
|
||||
|
||||
// DefaultSaltLength 是推荐的盐长度(字节)。盐越长,彩虹表与多目标
|
||||
// 批量攻击的收益越低。
|
||||
DefaultSaltLength = 16
|
||||
|
||||
// MinSaltLength 是允许的最小盐长度(字节)。RFC 8018 建议至少 8 字节,
|
||||
// 更短的盐会显著提高碰撞概率。
|
||||
MinSaltLength = 8
|
||||
|
||||
// DefaultKeyLength 是推荐的派生密钥长度(字节),32 字节可直接作为
|
||||
// AES-256 或 HMAC-SHA256 的密钥。
|
||||
DefaultKeyLength = 32
|
||||
|
||||
// MaxKeyLength 是允许的最大派生密钥长度(字节)。该上限用于防止调用方
|
||||
// 把"输出缓冲区大小"误传入,造成不必要的内存分配与计算开销。
|
||||
MaxKeyLength = 1024
|
||||
)
|
||||
|
||||
// maxSaltLength 是允许的最大盐长度(字节)。盐不是秘密,超长盐没有额外收益,
|
||||
// 这里设上限只是为了让明显误传的参数(如 1<<20)尽早失败。
|
||||
const maxSaltLength = 1024
|
||||
|
||||
// GenerateSalt 返回 length 字节的密码学安全随机盐,供 DeriveKey 使用。
|
||||
//
|
||||
// length 必须落在 [MinSaltLength, 1024] 区间内,推荐传 DefaultSaltLength。
|
||||
// 每次注册、改密或加密新数据都必须生成新的盐,并把盐与派生结果一起持久化:
|
||||
// 盐是公开值,缺失它会导致派生结果永远无法复现。
|
||||
//
|
||||
// 随机源不可用时返回 error,绝不退化为可预测的盐。
|
||||
func GenerateSalt(length int) ([]byte, error) {
|
||||
if length < MinSaltLength {
|
||||
return nil, fmt.Errorf("pbkdf2x: 盐长度 %d 小于最小值 %d", length, MinSaltLength)
|
||||
}
|
||||
if length > maxSaltLength {
|
||||
return nil, fmt.Errorf("pbkdf2x: 盐长度 %d 超过最大值 %d", length, maxSaltLength)
|
||||
}
|
||||
|
||||
salt := make([]byte, length)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成盐值失败: %v", err)
|
||||
// 使用 io.ReadFull(rand.Reader) 而不是 rand.Read:后者在随机源故障时
|
||||
// 会直接终止进程,而本包要求把失败作为 error 返回给调用方。
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return nil, fmt.Errorf("pbkdf2x: 读取随机盐失败: %w", err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(salt), nil
|
||||
return salt, nil
|
||||
}
|
||||
|
||||
// DeriveKeyFromPassword 使用PBKDF2从密码派生密钥
|
||||
// password: 原始密码
|
||||
// saltHex: 十六进制格式的盐值
|
||||
// iterations: 迭代次数,推荐至少10000次
|
||||
// keyLength: 派生密钥的长度(字节)
|
||||
func DeriveKeyFromPassword(password, saltHex string, iterations, keyLength int) (string, error) {
|
||||
// 解码盐值
|
||||
salt, err := hex.DecodeString(saltHex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("盐值解码失败: %v", err)
|
||||
}
|
||||
|
||||
// 使用PBKDF2派生密钥
|
||||
key := pbkdf2.Key([]byte(password), salt, iterations, keyLength, sha256.New)
|
||||
|
||||
// 返回十六进制格式的密钥
|
||||
return hex.EncodeToString(key), nil
|
||||
// DeriveKey 使用 PBKDF2-HMAC-SHA256 从 password 与 salt 派生 keyLength 字节密钥。
|
||||
//
|
||||
// password 为原始口令字节(不是十六进制串);salt 为 GenerateSalt 产生的随机盐
|
||||
// 或调用方自行管理的唯一盐。iterations 需落在 [MinIterations, MaxIterations],
|
||||
// keyLength 需落在 [1, MaxKeyLength],推荐 DefaultIterations 与 DefaultKeyLength。
|
||||
//
|
||||
// 相同的 (password, salt, iterations, keyLength) 必然得到相同密钥;只要盐变化,
|
||||
// 结果即完全不同。因此派生结果必须连同盐与参数一起保存才能复现。
|
||||
//
|
||||
// 安全提示:派生结果不可逆推口令,但如果口令熵不足,攻击者仍可离线穷举,
|
||||
// 请配合口令强度策略与足够的迭代次数使用。
|
||||
func DeriveKey(password, salt []byte, iterations, keyLength int) ([]byte, error) {
|
||||
return DeriveKeyWithHash(password, salt, iterations, keyLength, sha256.New)
|
||||
}
|
||||
|
||||
// DeriveKeyFromPasswordWithRandomSalt 使用PBKDF2从密码派生密钥,并自动生成随机盐值
|
||||
// password: 原始密码
|
||||
// saltLength: 盐值长度(字节)
|
||||
// iterations: 迭代次数,推荐至少10000次
|
||||
// keyLength: 派生密钥的长度(字节)
|
||||
// 返回值: 密钥(十六进制), 盐值(十六进制), 错误
|
||||
func DeriveKeyFromPasswordWithRandomSalt(password string, saltLength, iterations, keyLength int) (string, string, error) {
|
||||
// 生成随机盐值
|
||||
saltHex, err := GenerateSalt(saltLength)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
// DeriveKeyWithHash 与 DeriveKey 相同,但允许指定 PBKDF2 的伪随机函数底层哈希,
|
||||
// 例如 sha1.New(仅用于兼容旧系统)、sha512.New。h 为 nil 时返回 error。
|
||||
//
|
||||
// 参数校验与 DeriveKey 完全一致:盐长度、迭代次数、输出长度越界都会返回 error。
|
||||
// 安全提示:SHA-1/MD5 已被证明存在碰撞攻击,新系统不要使用;更换哈希属于不兼容
|
||||
// 变更,同一份存储数据必须固定使用创建时所用的哈希。
|
||||
func DeriveKeyWithHash(password, salt []byte, iterations, keyLength int, h func() hash.Hash) ([]byte, error) {
|
||||
if err := validateParams(salt, iterations, keyLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h == nil {
|
||||
return nil, fmt.Errorf("pbkdf2x: 哈希构造函数不能为 nil")
|
||||
}
|
||||
|
||||
// 派生密钥
|
||||
keyHex, err := DeriveKeyFromPassword(password, saltHex, iterations, keyLength)
|
||||
// 使用标准库 crypto/pbkdf2:它返回 error;而 golang.org/x/crypto/pbkdf2.Key
|
||||
// 在参数非法或 FIPS 140-only 模式下会 panic,与本包"绝不 panic"的约定冲突,
|
||||
// 且 FIPS 限制无法通过入口校验完全规避。
|
||||
key, err := pbkdf2.Key(h, string(password), salt, iterations, keyLength)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return nil, fmt.Errorf("pbkdf2x: 派生密钥失败: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return keyHex, saltHex, nil
|
||||
// DeriveKeyWithRandomSalt 生成 saltLength 字节随机盐并立即派生密钥,一次性返回
|
||||
// (key, salt, error)。这是推荐入口:调用方必须把返回的 salt 与 key 一起保存
|
||||
// (例如 salt 存明文列、key 作为校验值),否则无法复现派生结果。
|
||||
//
|
||||
// saltLength 通常传 DefaultSaltLength;iterations 与 keyLength 的含义同 DeriveKey。
|
||||
// 任何参数非法或随机源失败都会返回 error,此时 key 与 salt 均为 nil。
|
||||
func DeriveKeyWithRandomSalt(password []byte, saltLength, iterations, keyLength int) (key, salt []byte, err error) {
|
||||
salt, err = GenerateSalt(saltLength)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
key, err = DeriveKey(password, salt, iterations, keyLength)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return key, salt, nil
|
||||
}
|
||||
|
||||
// Verify 用相同参数重新派生密钥,并以常量时间比较 expectedKey,返回是否匹配。
|
||||
//
|
||||
// 参数非法(盐过短、迭代次数越界、keyLength 越界)时返回 error;参数合法但密钥
|
||||
// 不匹配时返回 (false, nil)。expectedKey 长度与 keyLength 不一致时直接判定为
|
||||
// 不匹配,不会泄漏比较进度。
|
||||
//
|
||||
// 安全提示:必须使用本函数而不是 bytes.Equal 或 == 比较密钥,后者会因提前返回
|
||||
// 而泄漏前缀匹配长度,构成时序侧信道。
|
||||
func Verify(password, salt, expectedKey []byte, iterations, keyLength int) (bool, error) {
|
||||
derived, err := DeriveKey(password, salt, iterations, keyLength)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// 比较完成后立即清零本地派生结果,缩短密钥在内存中的驻留时间。
|
||||
defer func() {
|
||||
for i := range derived {
|
||||
derived[i] = 0
|
||||
}
|
||||
}()
|
||||
|
||||
if len(expectedKey) != len(derived) {
|
||||
return false, nil
|
||||
}
|
||||
return subtle.ConstantTimeCompare(derived, expectedKey) == 1, nil
|
||||
}
|
||||
|
||||
// validateParams 是所有派生入口共用的参数闸门,保证非法输入在进入底层实现
|
||||
// 之前就变成明确的 error,而不是 panic 或静默截断。
|
||||
func validateParams(salt []byte, iterations, keyLength int) error {
|
||||
if len(salt) == 0 {
|
||||
return fmt.Errorf("pbkdf2x: 盐不能为空")
|
||||
}
|
||||
if len(salt) < MinSaltLength {
|
||||
return fmt.Errorf("pbkdf2x: 盐长度 %d 小于最小值 %d", len(salt), MinSaltLength)
|
||||
}
|
||||
if len(salt) > maxSaltLength {
|
||||
return fmt.Errorf("pbkdf2x: 盐长度 %d 超过最大值 %d", len(salt), maxSaltLength)
|
||||
}
|
||||
if iterations < MinIterations {
|
||||
return fmt.Errorf("pbkdf2x: 迭代次数 %d 小于最小值 %d", iterations, MinIterations)
|
||||
}
|
||||
if iterations > MaxIterations {
|
||||
return fmt.Errorf("pbkdf2x: 迭代次数 %d 超过最大值 %d", iterations, MaxIterations)
|
||||
}
|
||||
if keyLength <= 0 {
|
||||
return fmt.Errorf("pbkdf2x: 密钥长度 %d 必须大于 0", keyLength)
|
||||
}
|
||||
if keyLength > MaxKeyLength {
|
||||
return fmt.Errorf("pbkdf2x: 密钥长度 %d 超过最大值 %d", keyLength, MaxKeyLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
package pbkdf2x_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/encryptx/pbkdf2x"
|
||||
)
|
||||
|
||||
// mustHexDecode 把测试向量里的十六进制串还原为字节。
|
||||
func mustHexDecode(t *testing.T, s string) []byte {
|
||||
t.Helper()
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
t.Fatalf("测试向量不是合法十六进制: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestDeriveKeyKnownVectors 固定本包的已知答案(KAT)。
|
||||
// 期望值由 Python 3.13 的 hashlib.pbkdf2_hmac(独立实现)生成,例如:
|
||||
//
|
||||
// hashlib.pbkdf2_hmac('sha256', b'password', b'saltsaltsalt', 10000, 32).hex()
|
||||
func TestDeriveKeyKnownVectors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
password []byte
|
||||
saltHex string
|
||||
iterations int
|
||||
keyLength int
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "sha256-10000-32",
|
||||
password: []byte("password"),
|
||||
saltHex: hex.EncodeToString([]byte("saltsaltsalt")),
|
||||
iterations: 10000,
|
||||
keyLength: 32,
|
||||
want: "63086a6bd6ba533c492ed8d8453f99d723a9fbc2c427c2b848784308fc88cad3",
|
||||
},
|
||||
{
|
||||
name: "sha256-10000-64",
|
||||
password: []byte("password"),
|
||||
saltHex: hex.EncodeToString([]byte("saltsaltsalt")),
|
||||
iterations: 10000,
|
||||
keyLength: 64,
|
||||
want: "63086a6bd6ba533c492ed8d8453f99d723a9fbc2c427c2b848784308fc88cad33b33c8dad2c6f11785d281ae729f83877daa2b8afe1923791c46a8e226f92f4a",
|
||||
},
|
||||
{
|
||||
// 含 0x00 的二进制盐:验证盐是原始字节而不是十六进制字符串。
|
||||
name: "sha256-100000-32-binary-salt",
|
||||
password: []byte("Correct Horse Battery Staple"),
|
||||
saltHex: "000102030405060708090a0b0c0d0e0f",
|
||||
iterations: 100000,
|
||||
keyLength: 32,
|
||||
want: "26707b0d34414adebe877b7345529a17095c7b56bdea133de943583d1e5e2efd",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := pbkdf2x.DeriveKey(tc.password, mustHexDecode(t, tc.saltHex), tc.iterations, tc.keyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey 返回错误: %v", err)
|
||||
}
|
||||
if hex.EncodeToString(got) != tc.want {
|
||||
t.Errorf("派生结果与已知向量不一致\n got: %s\nwant: %s", hex.EncodeToString(got), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveKeyWithHashKnownVector 验证 PRF 可替换:期望值同样由 Python 的
|
||||
// hashlib.pbkdf2_hmac('sha512', ...) 独立生成。
|
||||
func TestDeriveKeyWithHashKnownVector(t *testing.T) {
|
||||
const want = "c3d64081b5d8986fe81498e5cf0f5ed0c3bf9f011ac699ead49a7b192fe69ef239f970ba2de9827289e036f9d80fc18cc870c518d16f512cb623017df08820f8"
|
||||
|
||||
got, err := pbkdf2x.DeriveKeyWithHash([]byte("password"), []byte("saltsaltsalt"), 10000, 64, sha512.New)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKeyWithHash 返回错误: %v", err)
|
||||
}
|
||||
if hex.EncodeToString(got) != want {
|
||||
t.Errorf("PBKDF2-HMAC-SHA512 结果与已知向量不一致\n got: %s\nwant: %s", hex.EncodeToString(got), want)
|
||||
}
|
||||
|
||||
// 换哈希必须得到不同结果,证明 h 真正生效而不是被写死为 SHA-256。
|
||||
sha256Key, err := pbkdf2x.DeriveKey([]byte("password"), []byte("saltsaltsalt"), 10000, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey 返回错误: %v", err)
|
||||
}
|
||||
if bytes.Equal(sha256Key, got) {
|
||||
t.Error("SHA-512 与 SHA-256 派生结果相同,说明哈希参数未生效")
|
||||
}
|
||||
|
||||
// SHA-1 仅用于兼容旧系统,这里只验证可用性与长度。
|
||||
sha1Key, err := pbkdf2x.DeriveKeyWithHash([]byte("password"), []byte("saltsaltsalt"), 10000, 20, sha1.New)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKeyWithHash(sha1) 返回错误: %v", err)
|
||||
}
|
||||
if len(sha1Key) != 20 {
|
||||
t.Errorf("SHA-1 派生长度 = %d, 期望 20", len(sha1Key))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveKeyReproducible 覆盖最核心的语义:同口令同盐同参数必须可复现。
|
||||
func TestDeriveKeyReproducible(t *testing.T) {
|
||||
password := []byte("正确的马电池订书钉")
|
||||
salt := []byte("0123456789abcdef")
|
||||
|
||||
k1, err := pbkdf2x.DeriveKey(password, salt, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("第一次派生失败: %v", err)
|
||||
}
|
||||
k2, err := pbkdf2x.DeriveKey(password, salt, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("第二次派生失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(k1, k2) {
|
||||
t.Fatal("相同输入两次派生结果不一致,无法复现")
|
||||
}
|
||||
if len(k1) != pbkdf2x.DefaultKeyLength {
|
||||
t.Fatalf("派生长度 = %d, 期望 %d", len(k1), pbkdf2x.DefaultKeyLength)
|
||||
}
|
||||
|
||||
otherSalt, err := pbkdf2x.DeriveKey(password, []byte("fedcba9876543210"), pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("换盐派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherSalt) {
|
||||
t.Error("不同盐得到了相同密钥")
|
||||
}
|
||||
|
||||
otherPassword, err := pbkdf2x.DeriveKey([]byte("错误口令"), salt, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("换口令派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherPassword) {
|
||||
t.Error("不同口令得到了相同密钥")
|
||||
}
|
||||
|
||||
otherIterations, err := pbkdf2x.DeriveKey(password, salt, pbkdf2x.MinIterations*2, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("换迭代次数派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherIterations) {
|
||||
t.Error("不同迭代次数得到了相同密钥")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveKeyWithRandomSalt 针对被修复的核心缺陷:随机盐必须一并返回,
|
||||
// 且该盐必须能复现同一个密钥。
|
||||
func TestDeriveKeyWithRandomSalt(t *testing.T) {
|
||||
password := []byte("hunter2")
|
||||
|
||||
key, salt, err := pbkdf2x.DeriveKeyWithRandomSalt(password, pbkdf2x.DefaultSaltLength, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKeyWithRandomSalt 返回错误: %v", err)
|
||||
}
|
||||
if len(salt) != pbkdf2x.DefaultSaltLength {
|
||||
t.Fatalf("返回的盐长度 = %d, 期望 %d(盐必须是原始字节,不是十六进制字符串)", len(salt), pbkdf2x.DefaultSaltLength)
|
||||
}
|
||||
if len(key) != pbkdf2x.DefaultKeyLength {
|
||||
t.Fatalf("返回的密钥长度 = %d, 期望 %d", len(key), pbkdf2x.DefaultKeyLength)
|
||||
}
|
||||
|
||||
// 用返回的盐复现:这是旧实现丢失盐值后无法做到的事情。
|
||||
reproduced, err := pbkdf2x.DeriveKey(password, salt, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("用返回的盐复现失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(key, reproduced) {
|
||||
t.Fatal("用 DeriveKeyWithRandomSalt 返回的盐无法复现同一密钥")
|
||||
}
|
||||
|
||||
key2, salt2, err := pbkdf2x.DeriveKeyWithRandomSalt(password, pbkdf2x.DefaultSaltLength, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("第二次 DeriveKeyWithRandomSalt 失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(salt, salt2) {
|
||||
t.Error("两次调用返回了相同的盐,随机盐未生效")
|
||||
}
|
||||
if bytes.Equal(key, key2) {
|
||||
t.Error("两次调用返回了相同的密钥")
|
||||
}
|
||||
|
||||
// 非法 saltLength 必须报错而不是 panic。
|
||||
for _, bad := range []int{0, -1, pbkdf2x.MinSaltLength - 1, 1025, math.MaxInt} {
|
||||
if _, _, err := pbkdf2x.DeriveKeyWithRandomSalt(password, bad, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength); err == nil {
|
||||
t.Errorf("saltLength=%d 时未返回错误", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
password := []byte("s3cret-口令")
|
||||
salt := []byte("0123456789abcdef")
|
||||
key, err := pbkdf2x.DeriveKey(password, salt, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
|
||||
ok, err := pbkdf2x.Verify(password, salt, key, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("Verify 返回错误: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("正确口令 + 正确盐未通过校验")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
password []byte
|
||||
salt []byte
|
||||
expected []byte
|
||||
}{
|
||||
{"错误口令", []byte("s3cret-口令x"), salt, key},
|
||||
{"错误盐", password, []byte("fedcba9876543210"), key},
|
||||
{"密钥被篡改", password, salt, append(append([]byte{}, key[:len(key)-1]...), key[len(key)-1]^0x01)},
|
||||
{"密钥被截断", password, salt, key[:len(key)-1]},
|
||||
{"密钥被加长", password, salt, append(append([]byte{}, key...), 0x00)},
|
||||
{"空密钥", password, salt, nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ok, err := pbkdf2x.Verify(tc.password, tc.salt, tc.expected, pbkdf2x.MinIterations, pbkdf2x.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("参数合法时 Verify 不应返回错误: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Error("不匹配的密钥被判为匹配")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParameterValidation 覆盖所有非法参数路径:必须返回 error,且绝不 panic。
|
||||
func TestParameterValidation(t *testing.T) {
|
||||
validSalt := bytes.Repeat([]byte{0xA5}, pbkdf2x.MinSaltLength)
|
||||
password := []byte("pw")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
salt []byte
|
||||
iterations int
|
||||
keyLength int
|
||||
saltInvalid bool
|
||||
}{
|
||||
{"盐为 nil", nil, pbkdf2x.MinIterations, 32, true},
|
||||
{"盐为空切片", []byte{}, pbkdf2x.MinIterations, 32, true},
|
||||
{"盐过短", []byte("1234567"), pbkdf2x.MinIterations, 32, true},
|
||||
{"盐过长", bytes.Repeat([]byte{0x01}, 1025), pbkdf2x.MinIterations, 32, true},
|
||||
{"迭代次数为 0", validSalt, 0, 32, false},
|
||||
{"迭代次数为负", validSalt, -1, 32, false},
|
||||
{"迭代次数低于下限", validSalt, pbkdf2x.MinIterations - 1, 32, false},
|
||||
{"迭代次数超上限", validSalt, pbkdf2x.MaxIterations + 1, 32, false},
|
||||
{"迭代次数为 MaxInt", validSalt, math.MaxInt, 32, false},
|
||||
{"密钥长度为 0", validSalt, pbkdf2x.MinIterations, 0, false},
|
||||
{"密钥长度为负", validSalt, pbkdf2x.MinIterations, -32, false},
|
||||
{"密钥长度超上限", validSalt, pbkdf2x.MinIterations, pbkdf2x.MaxKeyLength + 1, false},
|
||||
{"密钥长度为 MaxInt", validSalt, pbkdf2x.MinIterations, math.MaxInt, false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
key, err := pbkdf2x.DeriveKey(password, tc.salt, tc.iterations, tc.keyLength)
|
||||
if err == nil {
|
||||
t.Fatalf("DeriveKey 未返回错误,得到 %d 字节密钥", len(key))
|
||||
}
|
||||
if key != nil {
|
||||
t.Errorf("出错时密钥应为 nil,实际长度 %d", len(key))
|
||||
}
|
||||
|
||||
if _, err := pbkdf2x.Verify(password, tc.salt, validSalt, tc.iterations, tc.keyLength); err == nil {
|
||||
t.Error("Verify 对非法参数未返回错误")
|
||||
}
|
||||
if _, err := pbkdf2x.DeriveKeyWithHash(password, tc.salt, tc.iterations, tc.keyLength, nil); err == nil {
|
||||
t.Error("DeriveKeyWithHash 对非法参数未返回错误")
|
||||
}
|
||||
// DeriveKeyWithRandomSalt 自行生成盐,因此仅在迭代次数/长度非法时才应报错。
|
||||
if !tc.saltInvalid {
|
||||
if _, _, err := pbkdf2x.DeriveKeyWithRandomSalt(password, pbkdf2x.DefaultSaltLength, tc.iterations, tc.keyLength); err == nil {
|
||||
t.Error("DeriveKeyWithRandomSalt 对非法参数未返回错误")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 合法的盐/迭代次数/长度组合但 h 为 nil:同样必须是 error 而不是 panic。
|
||||
if _, err := pbkdf2x.DeriveKeyWithHash(password, validSalt, pbkdf2x.MinIterations, 32, nil); err == nil {
|
||||
t.Error("哈希构造函数为 nil 时未返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBoundaryValues 验证边界值落在"允许"一侧时确实可用。
|
||||
func TestBoundaryValues(t *testing.T) {
|
||||
salt := bytes.Repeat([]byte{0x5A}, pbkdf2x.MinSaltLength)
|
||||
|
||||
// 最小迭代次数 + 最大密钥长度。
|
||||
key, err := pbkdf2x.DeriveKey([]byte("pw"), salt, pbkdf2x.MinIterations, pbkdf2x.MaxKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("边界参数派生失败: %v", err)
|
||||
}
|
||||
if len(key) != pbkdf2x.MaxKeyLength {
|
||||
t.Errorf("密钥长度 = %d, 期望 %d", len(key), pbkdf2x.MaxKeyLength)
|
||||
}
|
||||
|
||||
// 最小密钥长度。
|
||||
if _, err := pbkdf2x.DeriveKey([]byte("pw"), salt, pbkdf2x.MinIterations, 1); err != nil {
|
||||
t.Errorf("keyLength=1 应被接受: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSalt(t *testing.T) {
|
||||
for _, length := range []int{pbkdf2x.MinSaltLength, pbkdf2x.DefaultSaltLength, 32, 64} {
|
||||
salt, err := pbkdf2x.GenerateSalt(length)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSalt(%d) 返回错误: %v", length, err)
|
||||
}
|
||||
if len(salt) != length {
|
||||
t.Errorf("盐长度 = %d, 期望 %d", len(salt), length)
|
||||
}
|
||||
}
|
||||
|
||||
a, err := pbkdf2x.GenerateSalt(pbkdf2x.DefaultSaltLength)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSalt 返回错误: %v", err)
|
||||
}
|
||||
b, err := pbkdf2x.GenerateSalt(pbkdf2x.DefaultSaltLength)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSalt 返回错误: %v", err)
|
||||
}
|
||||
if bytes.Equal(a, b) {
|
||||
t.Error("两次生成的盐相同,随机性有问题")
|
||||
}
|
||||
|
||||
for _, length := range []int{0, -1, pbkdf2x.MinSaltLength - 1, 1025, math.MaxInt} {
|
||||
if _, err := pbkdf2x.GenerateSalt(length); err == nil {
|
||||
t.Errorf("GenerateSalt(%d) 未返回错误", length)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user