86 lines
3.9 KiB
Go
86 lines
3.9 KiB
Go
// 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
|
||
}
|