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