Files
encryptx/pbkdf2x/pbkdf2x.go
T
2026-09-30 15:49:12 +08:00

197 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
"crypto/subtle"
"fmt"
"hash"
"io"
)
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)
// 使用 io.ReadFull(rand.Reader) 而不是 rand.Read:后者在随机源故障时
// 会直接终止进程,而本包要求把失败作为 error 返回给调用方。
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return nil, fmt.Errorf("pbkdf2x: 读取随机盐失败: %w", err)
}
return salt, 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)
}
// 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")
}
// 使用标准库 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 nil, fmt.Errorf("pbkdf2x: 派生密钥失败: %w", err)
}
return key, 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
}