Files
encryptx/pbkdf2x/pbkdf2x.go
T

197 lines
8.7 KiB
Go
Raw Normal View History

2026-09-30 15:49:12 +08:00
// 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,且错误文本中不含
// 口令、盐或密钥材料。
2025-11-03 23:03:17 +08:00
package pbkdf2x
import (
2026-09-30 15:49:12 +08:00
"crypto/pbkdf2"
2025-11-03 23:03:17 +08:00
"crypto/rand"
"crypto/sha256"
2026-09-30 15:49:12 +08:00
"crypto/subtle"
2025-11-03 23:03:17 +08:00
"fmt"
2026-09-30 15:49:12 +08:00
"hash"
"io"
2025-11-03 23:03:17 +08:00
)
2026-09-30 15:49:12 +08:00
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)
2025-11-03 23:03:17 +08:00
}
salt := make([]byte, length)
2026-09-30 15:49:12 +08:00
// 使用 io.ReadFull(rand.Reader) 而不是 rand.Read:后者在随机源故障时
// 会直接终止进程,而本包要求把失败作为 error 返回给调用方。
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return nil, fmt.Errorf("pbkdf2x: 读取随机盐失败: %w", err)
2025-11-03 23:03:17 +08:00
}
2026-09-30 15:49:12 +08:00
return salt, nil
2025-11-03 23:03:17 +08:00
}
2026-09-30 15:49:12 +08:00
// 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)
2025-11-03 23:03:17 +08:00
}
2026-09-30 15:49:12 +08:00
// 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")
2025-11-03 23:03:17 +08:00
}
2026-09-30 15:49:12 +08:00
// 使用标准库 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)
2025-11-03 23:03:17 +08:00
if err != nil {
2026-09-30 15:49:12 +08:00
return nil, fmt.Errorf("pbkdf2x: 派生密钥失败: %w", err)
2025-11-03 23:03:17 +08:00
}
2026-09-30 15:49:12 +08:00
return key, nil
}
2025-11-03 23:03:17 +08:00
2026-09-30 15:49:12 +08:00
// 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
2025-11-03 23:03:17 +08:00
}