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

86 lines
3.9 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 hkdfx 基于 HKDF(RFC 5869)实现 "extract-then-expand" 密钥派生。
//
// 适用场景:把已有的高熵密钥材料(ECDH/X25519 共享密钥、KMS 下发的随机
// master key、随机生成的根密钥)扩展成多个用途不同的子密钥,或用 info 做
// 密钥分离。
//
// 安全注意事项:
// - HKDF 不是慢哈希,没有任何迭代与内存硬化环节,绝对不能用于低熵口令。
// 用 HKDF 处理用户口令等于把口令直接当密钥,攻击者可离线高速穷举;
// 口令请使用 argon2x(首选)或 scryptx。
// - secret 必须是非空的高熵密钥材料;空材料被拒绝。
// - salt 可以为 nil:此时按 RFC 5869 使用 HashLen 个零字节作为盐。salt 不必
// 保密,但应当唯一,用于隔离不同会话/不同对端。
// - info 是公开的上下文标签(如 "aes-key" / "hmac-key")。用同一份 secret
// 派生多个子密钥时必须给出互不相同的 info,否则会得到相同的密钥。
// - 一次派生的长度不能超过 255*HashLen 字节(HKDF-SHA256 即 MaxKeyLength),
// 超过该上限在密码学上无意义,本包直接拒绝。
package hkdfx
import (
"crypto/sha256"
"fmt"
"hash"
"io"
"golang.org/x/crypto/hkdf"
)
const (
// DefaultKeyLength 是推荐的派生长度(字节),32 字节可直接作为 AES-256
// 或 HMAC-SHA256 的密钥。
DefaultKeyLength = 32
// MaxKeyLength 是 HKDF-SHA256 单次可派生的最大长度(字节),等于
// 255 * SHA-256 输出长度 32。使用其他哈希时真实上限为 255*HashLen,
// DeriveKeyWithHash 会按传入哈希的实际输出长度校验。
MaxKeyLength = 8160
)
// DeriveKey 使用 HKDF-SHA256 从高熵 secret 派生 keyLength 字节密钥。
//
// secret 为输入密钥材料,必须非空;salt 可以为 nil(按 RFC 5869 使用全零盐);
// info 是用途/上下文标签,可以为 nil。keyLength 必须落在 [1, MaxKeyLength]。
//
// 派生结果是 secret、salt、info 三者的确定性函数:三者相同则结果相同,任一不同
// 则结果不同,因此调用方只要保存 salt 与 info(它们是公开值)即可复现。
//
// 再次强调:不要用本函数处理用户口令,请改用 argon2x/scryptx。
func DeriveKey(secret, salt, info []byte, keyLength int) ([]byte, error) {
return DeriveKeyWithHash(secret, salt, info, keyLength, sha256.New)
}
// DeriveKeyWithHash 与 DeriveKey 相同,但允许指定底层 HMAC 哈希(如 sha256.New、
// sha512.New、sha1.New)。h 为 nil 时返回 error。
//
// 输出上限按 255*h().Size() 计算:SHA-256 为 8160 字节,SHA-512 为 16320 字节。
// 安全提示:更换哈希会派生完全不同的密钥,属于不兼容变更;SHA-1 仅用于兼容旧协议。
func DeriveKeyWithHash(secret, salt, info []byte, keyLength int, h func() hash.Hash) ([]byte, error) {
if len(secret) == 0 {
return nil, fmt.Errorf("hkdfx: 输入密钥材料 secret 不能为空")
}
if h == nil {
return nil, fmt.Errorf("hkdfx: 哈希构造函数不能为 nil")
}
if keyLength <= 0 {
return nil, fmt.Errorf("hkdfx: 派生长度 %d 必须大于 0", keyLength)
}
hashSize := h().Size()
if hashSize <= 0 {
return nil, fmt.Errorf("hkdfx: 哈希输出长度非法: %d", hashSize)
}
// RFC 5869 的 L 上限为 255*HashLen;超出时底层 Reader 会返回
// "hkdf: entropy limit reached",这里提前给出更明确的错误。
if maxLen := 255 * hashSize; keyLength > maxLen {
return nil, fmt.Errorf("hkdfx: 派生长度 %d 超过该哈希的上限 %d (%d*%d)", keyLength, maxLen, 255, hashSize)
}
out := make([]byte, keyLength)
// hkdf.New 内部执行 Extract + Expand;salt 为 nil 时按标准使用全零盐。
if _, err := io.ReadFull(hkdf.New(h, secret, salt, info), out); err != nil {
return nil, fmt.Errorf("hkdfx: 派生密钥失败: %w", err)
}
return out, nil
}