完善不同加密方法的使用
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package hkdfx_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/encryptx/hkdfx"
|
||||
)
|
||||
|
||||
// seq 生成从 start 开始、长度为 n 的连续字节,用于构造 RFC 5869 的测试输入。
|
||||
func seq(start byte, n int) []byte {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = start + byte(i)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func mustHexDecode(t *testing.T, s string) []byte {
|
||||
t.Helper()
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
t.Fatalf("测试向量不是合法十六进制: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestDeriveKeyRFC5869Vectors 使用 RFC 5869 附录 A 的官方向量做已知答案测试。
|
||||
// 这些向量同时覆盖了 SHA-256 与 SHA-1、空盐以及 82 字节的长输出。
|
||||
func TestDeriveKeyRFC5869Vectors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
secret []byte
|
||||
salt []byte
|
||||
info []byte
|
||||
keyLength int
|
||||
useSHA1 bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "RFC5869-A.1 SHA-256 基本向量",
|
||||
secret: bytes.Repeat([]byte{0x0b}, 22),
|
||||
salt: seq(0x00, 13),
|
||||
info: seq(0xf0, 10),
|
||||
keyLength: 42,
|
||||
want: "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
|
||||
},
|
||||
{
|
||||
name: "RFC5869-A.2 SHA-256 长输入长输出",
|
||||
secret: seq(0x00, 80),
|
||||
salt: seq(0x60, 80),
|
||||
info: seq(0xb0, 80),
|
||||
keyLength: 82,
|
||||
want: "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87",
|
||||
},
|
||||
{
|
||||
name: "RFC5869-A.3 SHA-256 空盐空 info",
|
||||
secret: bytes.Repeat([]byte{0x0b}, 22),
|
||||
salt: nil,
|
||||
info: nil,
|
||||
keyLength: 42,
|
||||
want: "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8",
|
||||
},
|
||||
{
|
||||
name: "RFC5869-A.4 SHA-1 基本向量",
|
||||
secret: bytes.Repeat([]byte{0x0b}, 11),
|
||||
salt: seq(0x00, 13),
|
||||
info: seq(0xf0, 10),
|
||||
keyLength: 42,
|
||||
useSHA1: true,
|
||||
want: "085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896",
|
||||
},
|
||||
{
|
||||
name: "RFC5869-A.5 SHA-1 长输入长输出",
|
||||
secret: seq(0x00, 80),
|
||||
salt: seq(0x60, 80),
|
||||
info: seq(0xb0, 80),
|
||||
keyLength: 82,
|
||||
useSHA1: true,
|
||||
want: "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
got []byte
|
||||
err error
|
||||
)
|
||||
if tc.useSHA1 {
|
||||
got, err = hkdfx.DeriveKeyWithHash(tc.secret, tc.salt, tc.info, tc.keyLength, sha1.New)
|
||||
} else {
|
||||
got, err = hkdfx.DeriveKey(tc.secret, tc.salt, tc.info, tc.keyLength)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if hex.EncodeToString(got) != tc.want {
|
||||
t.Errorf("与 RFC 5869 向量不一致\n got: %s\nwant: %s", hex.EncodeToString(got), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveKeyProperties(t *testing.T) {
|
||||
secret := mustHexDecode(t, "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||
salt := []byte("unique-salt")
|
||||
info := []byte("aes-key")
|
||||
|
||||
k1, err := hkdfx.DeriveKey(secret, salt, info, hkdfx.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
k2, err := hkdfx.DeriveKey(secret, salt, info, hkdfx.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(k1, k2) {
|
||||
t.Error("相同输入两次派生结果不一致")
|
||||
}
|
||||
if len(k1) != hkdfx.DefaultKeyLength {
|
||||
t.Errorf("派生长度 = %d, 期望 %d", len(k1), hkdfx.DefaultKeyLength)
|
||||
}
|
||||
|
||||
// 默认入口必须等价于显式传 SHA-256。
|
||||
explicit, err := hkdfx.DeriveKeyWithHash(secret, salt, info, hkdfx.DefaultKeyLength, sha256.New)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(k1, explicit) {
|
||||
t.Error("DeriveKey 与 DeriveKeyWithHash(sha256) 结果不一致")
|
||||
}
|
||||
|
||||
otherInfo, err := hkdfx.DeriveKey(secret, salt, []byte("hmac-key"), hkdfx.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherInfo) {
|
||||
t.Error("不同 info 得到了相同密钥(密钥分离失效)")
|
||||
}
|
||||
|
||||
otherSalt, err := hkdfx.DeriveKey(secret, []byte("another-salt"), info, hkdfx.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherSalt) {
|
||||
t.Error("不同 salt 得到了相同密钥")
|
||||
}
|
||||
|
||||
otherSecret, err := hkdfx.DeriveKey(append([]byte{0x00}, secret...), salt, info, hkdfx.DefaultKeyLength)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherSecret) {
|
||||
t.Error("不同 secret 得到了相同密钥")
|
||||
}
|
||||
|
||||
// 前缀性质:先派生 64 字节,再取前 32 字节应与直接派生 32 字节一致。
|
||||
long, err := hkdfx.DeriveKey(secret, salt, info, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(long[:32], k1) {
|
||||
t.Error("HKDF 输出不满足前缀一致性")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveKeyNilSaltEqualsZeroSalt 验证 nil 盐按 RFC 5869 等价于 HashLen 个零字节。
|
||||
func TestDeriveKeyNilSaltEqualsZeroSalt(t *testing.T) {
|
||||
secret := []byte("high-entropy-secret-material")
|
||||
info := []byte("context")
|
||||
|
||||
withNil, err := hkdfx.DeriveKey(secret, nil, info, 32)
|
||||
if err != nil {
|
||||
t.Fatalf("nil 盐派生失败: %v", err)
|
||||
}
|
||||
withEmpty, err := hkdfx.DeriveKey(secret, []byte{}, info, 32)
|
||||
if err != nil {
|
||||
t.Fatalf("空盐派生失败: %v", err)
|
||||
}
|
||||
withZeros, err := hkdfx.DeriveKey(secret, make([]byte, sha256.Size), info, 32)
|
||||
if err != nil {
|
||||
t.Fatalf("零盐派生失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(withNil, withEmpty) || !bytes.Equal(withNil, withZeros) {
|
||||
t.Error("nil 盐、空盐与 HashLen 个零字节盐的派生结果不一致")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameterValidation(t *testing.T) {
|
||||
secret := []byte("high-entropy-secret-material")
|
||||
|
||||
// secret 必须非空。
|
||||
for _, s := range [][]byte{nil, {}} {
|
||||
if _, err := hkdfx.DeriveKey(s, nil, nil, 32); err == nil {
|
||||
t.Error("空 secret 未返回错误")
|
||||
}
|
||||
if _, err := hkdfx.DeriveKeyWithHash(s, nil, nil, 32, sha256.New); err == nil {
|
||||
t.Error("空 secret 未返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// 长度非法。
|
||||
for _, length := range []int{0, -1, hkdfx.MaxKeyLength + 1, math.MaxInt} {
|
||||
if _, err := hkdfx.DeriveKey(secret, nil, nil, length); err == nil {
|
||||
t.Errorf("keyLength=%d 未返回错误", length)
|
||||
}
|
||||
}
|
||||
|
||||
// 哈希为 nil。
|
||||
if _, err := hkdfx.DeriveKeyWithHash(secret, nil, nil, 32, nil); err == nil {
|
||||
t.Error("哈希构造函数为 nil 时未返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeyLengthLimits 验证上限按 255*HashLen 计算,并允许等于上限。
|
||||
func TestKeyLengthLimits(t *testing.T) {
|
||||
secret := []byte("high-entropy-secret-material")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
h func() hash.Hash
|
||||
maxLength int
|
||||
}{
|
||||
{"sha256", sha256.New, 255 * sha256.Size},
|
||||
{"sha512", sha512.New, 255 * sha512.Size},
|
||||
{"sha1", sha1.New, 255 * sha1.Size},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
key, err := hkdfx.DeriveKeyWithHash(secret, []byte("salt"), []byte("info"), tc.maxLength, tc.h)
|
||||
if err != nil {
|
||||
t.Fatalf("取上限 %d 时不应报错: %v", tc.maxLength, err)
|
||||
}
|
||||
if len(key) != tc.maxLength {
|
||||
t.Errorf("派生长度 = %d, 期望 %d", len(key), tc.maxLength)
|
||||
}
|
||||
if _, err := hkdfx.DeriveKeyWithHash(secret, []byte("salt"), []byte("info"), tc.maxLength+1, tc.h); err == nil {
|
||||
t.Errorf("超过哈希上限 %d 时未返回错误", tc.maxLength)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 默认 SHA-256 入口的常量上限必须与 255*32 一致。
|
||||
if hkdfx.MaxKeyLength != 255*sha256.Size {
|
||||
t.Errorf("MaxKeyLength = %d, 期望 %d", hkdfx.MaxKeyLength, 255*sha256.Size)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user