// 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 }