完善不同加密方法的使用
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user