完善不同加密方法的使用
This commit is contained in:
+265
-10
@@ -1,22 +1,277 @@
|
||||
// Package hmacx 提供基于 crypto/hmac 的消息认证码(MAC)计算与校验能力。
|
||||
//
|
||||
// 设计要点与安全说明:
|
||||
//
|
||||
// 1. 本包所有计算函数都返回原始摘要字节([]byte),不返回可打印字符串,
|
||||
// 以免调用方把密钥材料或摘要误写入日志。
|
||||
// 2. 校验必须使用本包提供的 Verify* 函数,它们内部调用 crypto/hmac 的
|
||||
// hmac.Equal 做常量时间比较;**绝不要**用 == 或 bytes.Equal 比较 MAC,
|
||||
// 那会泄漏时序信息(timing side channel),使攻击者可以逐字节猜测正确 MAC。
|
||||
// 3. 密钥长度短于哈希分组大小(SHA-1/SHA-256 为 64 字节,SHA-384/SHA-512 为
|
||||
// 128 字节)时,HMAC 会先用 0x00 把密钥填充到分组长度,这是 RFC 2104 规定
|
||||
// 的正常行为,不会降低安全性;安全性取决于密钥的**熵**而非长度。
|
||||
// 但过短的密钥(少于 MinKeyLength 字节)会显著降低抗暴力破解能力,
|
||||
// 应使用 crypto/rand 生成的随机密钥,长度建议为 DefaultKeyLength。
|
||||
// 4. SHA-1 仅用于兼容既有协议(如 TLS 1.0、旧版 OAuth 1.0 签名)。
|
||||
// SHA-1 的抗碰撞性已被攻破,新设计请使用 SHA-256 及以上。
|
||||
package hmacx
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
)
|
||||
|
||||
// HMAC-SHA256 计算消息认证码
|
||||
func HMACSHA256(message, key string) string {
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(message))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
const (
|
||||
// MinKeyLength 是建议的最小 HMAC 密钥长度(字节)。
|
||||
// 短于该长度的密钥熵不足,容易被暴力枚举,安全性显著下降。
|
||||
MinKeyLength = 16
|
||||
|
||||
// DefaultKeyLength 是推荐的 HMAC 密钥长度(字节),
|
||||
// 与 SHA-256 的摘要长度一致,可提供 256 位安全强度。
|
||||
DefaultKeyLength = 32
|
||||
)
|
||||
|
||||
// errNilReader 表示调用方传入了 nil 的 io.Reader。
|
||||
var errNilReader = errors.New("reader 不能为 nil")
|
||||
|
||||
// Sum 使用给定的哈希构造函数计算 HMAC 摘要,返回原始摘要字节。
|
||||
//
|
||||
// 参数:
|
||||
// - h: 哈希构造函数,例如 sha256.New;不能为 nil,否则返回 nil。
|
||||
// - message: 待认证的消息,可以为空。
|
||||
// - key: 认证密钥,可以为空(但空密钥等同于公开密钥,**没有认证意义**)。
|
||||
//
|
||||
// 安全注意:
|
||||
// - 密钥应来自 crypto/rand,长度不小于 MinKeyLength。
|
||||
// - 返回的摘要是敏感数据的完整性凭证,不要写进日志。
|
||||
// - 计算本身不会失败,因此没有 error 返回值;h 为 nil 时返回 nil 以便安全降级。
|
||||
func Sum(h func() hash.Hash, message, key []byte) []byte {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
mac := hmac.New(h, key)
|
||||
_, _ = mac.Write(message)
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
// HMAC-SHA512 计算消息认证码
|
||||
func HMACSHA512(message, key string) string {
|
||||
mac := hmac.New(sha512.New, []byte(key))
|
||||
mac.Write([]byte(message))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
// Verify 使用给定的哈希构造函数校验 HMAC 摘要是否匹配。
|
||||
//
|
||||
// 参数:
|
||||
// - h: 哈希构造函数,例如 sha256.New;为 nil 时直接返回 false。
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
// - expectedMAC: 期望的摘要字节,长度必须等于 h() 的摘要长度。
|
||||
//
|
||||
// 返回值:仅在 expectedMAC 长度合法且常量时间比较相等时返回 true。
|
||||
//
|
||||
// 安全注意:
|
||||
// - 内部使用 hmac.Equal(常量时间比较),不会泄漏时序信息。
|
||||
// - expectedMAC 长度与摘要长度不符时立即返回 false,不会 panic。
|
||||
// - 绝不能先用 == 比较 MAC 再调用本函数,那样时序漏洞依然存在。
|
||||
func Verify(h func() hash.Hash, message, key, expectedMAC []byte) bool {
|
||||
if h == nil {
|
||||
return false
|
||||
}
|
||||
expectedLen := h().Size()
|
||||
if len(expectedMAC) != expectedLen {
|
||||
return false
|
||||
}
|
||||
mac := hmac.New(h, key)
|
||||
_, _ = mac.Write(message)
|
||||
return hmac.Equal(mac.Sum(nil), expectedMAC)
|
||||
}
|
||||
|
||||
// SecureCompare 以常量时间比较两个字节串是否相等。
|
||||
//
|
||||
// 参数:
|
||||
// - a, b: 待比较的字节串,长度不同时直接判定为不相等。
|
||||
//
|
||||
// 返回值:相等返回 true,否则返回 false。
|
||||
//
|
||||
// 安全注意:
|
||||
// - 长度本身会通过返回结果与执行时间被区分出来,因此本函数只适用于比较
|
||||
// 长度固定的秘密(如摘要、MAC);长度不固定的秘密应先做定长处理。
|
||||
// - 底层使用 crypto/subtle.ConstantTimeCompare,比较耗时只与长度相关,
|
||||
// 与内容无关,可抵御逐字节猜测攻击。
|
||||
func SecureCompare(a, b []byte) bool {
|
||||
return hmac.Equal(a, b)
|
||||
}
|
||||
|
||||
// SumSHA1 计算 HMAC-SHA1 摘要(20 字节)。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥,建议长度不小于 MinKeyLength。
|
||||
//
|
||||
// 安全注意:SHA-1 抗碰撞性已被攻破,仅用于兼容既有协议;
|
||||
// 新设计请使用 SumSHA256 或 SumSHA512。密钥过短会降低抗暴力破解能力。
|
||||
func SumSHA1(message, key []byte) []byte {
|
||||
return Sum(sha1.New, message, key)
|
||||
}
|
||||
|
||||
// SumSHA256 计算 HMAC-SHA256 摘要(32 字节),是推荐的默认选择。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥,建议长度不小于 MinKeyLength,推荐 DefaultKeyLength。
|
||||
//
|
||||
// 安全注意:密钥必须保密且具有足够熵;本函数不校验密钥长度,
|
||||
// 过短密钥不会报错但会降低安全性,调用方应自行确保密钥质量。
|
||||
func SumSHA256(message, key []byte) []byte {
|
||||
return Sum(sha256.New, message, key)
|
||||
}
|
||||
|
||||
// SumSHA384 计算 HMAC-SHA384 摘要(48 字节)。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥,建议长度不小于 MinKeyLength。
|
||||
//
|
||||
// 安全注意:SHA-384 的分组大小为 128 字节,短于该长度的密钥会被 0x00 填充,
|
||||
// 这是正常行为;密钥熵不足才是真正的风险。
|
||||
func SumSHA384(message, key []byte) []byte {
|
||||
return Sum(sha512.New384, message, key)
|
||||
}
|
||||
|
||||
// SumSHA512 计算 HMAC-SHA512 摘要(64 字节)。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥,建议长度不小于 MinKeyLength。
|
||||
//
|
||||
// 安全注意:摘要更长但计算开销也更高;多数场景 SHA-256 已足够。
|
||||
func SumSHA512(message, key []byte) []byte {
|
||||
return Sum(sha512.New, message, key)
|
||||
}
|
||||
|
||||
// HexSHA1 计算 HMAC-SHA1 并返回小写十六进制字符串(40 个字符)。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
//
|
||||
// 安全注意:十六进制字符串便于传输与比较,但**校验时不要用 == 比较**,
|
||||
// 请使用 VerifySHA1;另注意该字符串是完整性凭证,不宜写入日志。
|
||||
func HexSHA1(message, key []byte) string {
|
||||
return hex.EncodeToString(SumSHA1(message, key))
|
||||
}
|
||||
|
||||
// HexSHA256 计算 HMAC-SHA256 并返回小写十六进制字符串(64 个字符)。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
//
|
||||
// 安全注意:**校验时不要用 == 比较**返回的字符串,请使用 VerifySHA256
|
||||
// 以避免时序侧信道。
|
||||
func HexSHA256(message, key []byte) string {
|
||||
return hex.EncodeToString(SumSHA256(message, key))
|
||||
}
|
||||
|
||||
// HexSHA384 计算 HMAC-SHA384 并返回小写十六进制字符串(96 个字符)。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
//
|
||||
// 安全注意:**校验时不要用 == 比较**返回的字符串,请使用 VerifySHA384。
|
||||
func HexSHA384(message, key []byte) string {
|
||||
return hex.EncodeToString(SumSHA384(message, key))
|
||||
}
|
||||
|
||||
// HexSHA512 计算 HMAC-SHA512 并返回小写十六进制字符串(128 个字符)。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
//
|
||||
// 安全注意:**校验时不要用 == 比较**返回的字符串,请使用 VerifySHA512。
|
||||
func HexSHA512(message, key []byte) string {
|
||||
return hex.EncodeToString(SumSHA512(message, key))
|
||||
}
|
||||
|
||||
// SumReaderSHA256 从 io.Reader 流式读取消息并计算 HMAC-SHA256 摘要。
|
||||
//
|
||||
// 参数:
|
||||
// - r: 消息来源,不能为 nil。
|
||||
// - key: 认证密钥,建议长度不小于 MinKeyLength。
|
||||
//
|
||||
// 返回值:32 字节的原始摘要;读取失败时返回 nil 与包装后的 error。
|
||||
//
|
||||
// 安全注意:流式读取适合大消息,可避免整个消息驻留内存;
|
||||
// 但 MAC 只保证完整性,不提供机密性,敏感数据仍需先加密。
|
||||
func SumReaderSHA256(r io.Reader, key []byte) ([]byte, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("计算 HMAC-SHA256: %w", errNilReader)
|
||||
}
|
||||
mac := hmac.New(sha256.New, key)
|
||||
if _, err := io.Copy(mac, r); err != nil {
|
||||
return nil, fmt.Errorf("读取消息以计算 HMAC-SHA256: %w", err)
|
||||
}
|
||||
return mac.Sum(nil), nil
|
||||
}
|
||||
|
||||
// VerifySHA1 校验 HMAC-SHA1 摘要是否匹配,使用常量时间比较。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
// - expectedMAC: 期望的 20 字节摘要。
|
||||
//
|
||||
// 返回值:长度合法且常量时间比较相等时返回 true,否则返回 false。
|
||||
//
|
||||
// 安全注意:expectedMAC 长度不是 20 时立即返回 false(不会 panic);
|
||||
// 本函数不会因为比较失败而泄漏任何时序信息。
|
||||
func VerifySHA1(message, key, expectedMAC []byte) bool {
|
||||
return Verify(sha1.New, message, key, expectedMAC)
|
||||
}
|
||||
|
||||
// VerifySHA256 校验 HMAC-SHA256 摘要是否匹配,使用常量时间比较。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
// - expectedMAC: 期望的 32 字节摘要。
|
||||
//
|
||||
// 返回值:长度合法且常量时间比较相等时返回 true,否则返回 false。
|
||||
//
|
||||
// 安全注意:expectedMAC 长度不是 32 时立即返回 false(不会 panic);
|
||||
// 本函数是替代 == 比较的正确做法。
|
||||
func VerifySHA256(message, key, expectedMAC []byte) bool {
|
||||
return Verify(sha256.New, message, key, expectedMAC)
|
||||
}
|
||||
|
||||
// VerifySHA384 校验 HMAC-SHA384 摘要是否匹配,使用常量时间比较。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
// - expectedMAC: 期望的 48 字节摘要。
|
||||
//
|
||||
// 返回值:长度合法且常量时间比较相等时返回 true,否则返回 false。
|
||||
//
|
||||
// 安全注意:expectedMAC 长度不是 48 时立即返回 false(不会 panic)。
|
||||
func VerifySHA384(message, key, expectedMAC []byte) bool {
|
||||
return Verify(sha512.New384, message, key, expectedMAC)
|
||||
}
|
||||
|
||||
// VerifySHA512 校验 HMAC-SHA512 摘要是否匹配,使用常量时间比较。
|
||||
//
|
||||
// 参数:
|
||||
// - message: 待认证的消息。
|
||||
// - key: 认证密钥。
|
||||
// - expectedMAC: 期望的 64 字节摘要。
|
||||
//
|
||||
// 返回值:长度合法且常量时间比较相等时返回 true,否则返回 false。
|
||||
//
|
||||
// 安全注意:expectedMAC 长度不是 64 时立即返回 false(不会 panic)。
|
||||
func VerifySHA512(message, key, expectedMAC []byte) bool {
|
||||
return Verify(sha512.New, message, key, expectedMAC)
|
||||
}
|
||||
|
||||
+392
-54
@@ -1,75 +1,413 @@
|
||||
package hmacx
|
||||
package hmacx_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/encryptx/hmacx"
|
||||
)
|
||||
|
||||
func TestHMACSHA256(t *testing.T) {
|
||||
message := "hello world"
|
||||
key := "secret_key"
|
||||
// rfc4231Case 表示 RFC 4231 中的一个测试用例。
|
||||
// 来源:RFC 4231 "Identifiers and Test Vectors for HMAC-SHA-224, HMAC-SHA-256,
|
||||
// HMAC-SHA-384, and HMAC-SHA-512"(https://www.rfc-editor.org/rfc/rfc4231.txt)。
|
||||
type rfc4231Case struct {
|
||||
name string
|
||||
key []byte
|
||||
data []byte
|
||||
sha256 string
|
||||
sha384 string
|
||||
sha512 string
|
||||
}
|
||||
|
||||
// 计算HMAC-SHA256
|
||||
hmac1 := HMACSHA256(message, key)
|
||||
// rfc4231Cases 为 RFC 4231 第 4.2~4.5 节(Test Case 1~4)的 SHA-256/384/512 摘要,
|
||||
// 以及 RFC 2202 Test Case 1~4 的 HMAC-SHA1 摘要(两组定义的 key/data 相同)。
|
||||
var rfc4231Cases = []rfc4231Case{
|
||||
{
|
||||
name: "RFC4231-TestCase1",
|
||||
key: bytes.Repeat([]byte{0x0b}, 20),
|
||||
data: []byte("Hi There"),
|
||||
sha256: "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
|
||||
sha384: "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6",
|
||||
sha512: "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854",
|
||||
},
|
||||
{
|
||||
name: "RFC4231-TestCase2",
|
||||
key: []byte("Jefe"),
|
||||
data: []byte("what do ya want for nothing?"),
|
||||
sha256: "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843",
|
||||
sha384: "af45d2e376484031617f78d2b58a6b1b9c7ef464f5a01b47e42ec3736322445e8e2240ca5e69e2c78b3239ecfab21649",
|
||||
sha512: "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737",
|
||||
},
|
||||
{
|
||||
name: "RFC4231-TestCase3",
|
||||
key: bytes.Repeat([]byte{0xaa}, 20),
|
||||
data: bytes.Repeat([]byte{0xdd}, 50),
|
||||
sha256: "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe",
|
||||
sha384: "88062608d3e6ad8a0aa2ace014c8a86f0aa635d947ac9febe83ef4e55966144b2a5ab39dc13814b94e3ab6e101a34f27",
|
||||
sha512: "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb",
|
||||
},
|
||||
{
|
||||
name: "RFC4231-TestCase4",
|
||||
key: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19},
|
||||
data: bytes.Repeat([]byte{0xcd}, 50),
|
||||
sha256: "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b",
|
||||
sha384: "3e8a69b7783c25851933ab6290af6ca77a9981480850009cc5577c6e1f573b4e6801dd23c4a7d679ccf8a386c674cffb",
|
||||
sha512: "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd",
|
||||
},
|
||||
}
|
||||
|
||||
// 再次计算相同的消息和密钥
|
||||
hmac2 := HMACSHA256(message, key)
|
||||
// rfc2202SHA1Cases 为 RFC 2202(HMAC-MD5/HMAC-SHA-1 测试向量)Test Case 1~4 的
|
||||
// HMAC-SHA1 期望值,key/data 与 rfc4231Cases 的前四项完全一致。
|
||||
var rfc2202SHA1Cases = []string{
|
||||
"b617318655057264e28bc0b6fb378c8ef146be00",
|
||||
"effcdf6ae5eb2fa2d27416d5f184df9c259a7c79",
|
||||
"125d7342b9ac11cd91a39af48aa17b4f63f175d3",
|
||||
"4c9007f4026250c6bc8414f9bf50c86c2d7235da",
|
||||
}
|
||||
|
||||
// 相同的消息和密钥应该产生相同的HMAC
|
||||
if hmac1 != hmac2 {
|
||||
t.Errorf("相同输入产生了不同的HMAC: %s vs %s", hmac1, hmac2)
|
||||
}
|
||||
|
||||
// 不同的消息应该产生不同的HMAC
|
||||
hmac3 := HMACSHA256("different message", key)
|
||||
if hmac1 == hmac3 {
|
||||
t.Errorf("不同消息产生了相同的HMAC")
|
||||
}
|
||||
|
||||
// 不同的密钥应该产生不同的HMAC
|
||||
hmac4 := HMACSHA256(message, "different_key")
|
||||
if hmac1 == hmac4 {
|
||||
t.Errorf("不同密钥产生了相同的HMAC")
|
||||
// TestSumKnownVectors 用 RFC 4231 与 RFC 2202 的官方测试向量核对摘要计算。
|
||||
func TestSumKnownVectors(t *testing.T) {
|
||||
for i, tc := range rfc4231Cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := hex.EncodeToString(hmacx.SumSHA256(tc.data, tc.key)); got != tc.sha256 {
|
||||
t.Errorf("HMAC-SHA256 不匹配:\n got %s\nwant %s", got, tc.sha256)
|
||||
}
|
||||
if got := hex.EncodeToString(hmacx.SumSHA384(tc.data, tc.key)); got != tc.sha384 {
|
||||
t.Errorf("HMAC-SHA384 不匹配:\n got %s\nwant %s", got, tc.sha384)
|
||||
}
|
||||
if got := hex.EncodeToString(hmacx.SumSHA512(tc.data, tc.key)); got != tc.sha512 {
|
||||
t.Errorf("HMAC-SHA512 不匹配:\n got %s\nwant %s", got, tc.sha512)
|
||||
}
|
||||
if got := hex.EncodeToString(hmacx.SumSHA1(tc.data, tc.key)); got != rfc2202SHA1Cases[i] {
|
||||
t.Errorf("HMAC-SHA1 不匹配:\n got %s\nwant %s", got, rfc2202SHA1Cases[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHMACSHA512(t *testing.T) {
|
||||
message := "hello world"
|
||||
key := "secret_key"
|
||||
// TestSumGenericMatchesSpecialized 校验通用入口 Sum 与各专用函数一致。
|
||||
func TestSumGenericMatchesSpecialized(t *testing.T) {
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
msg := []byte("generic entry point")
|
||||
|
||||
// 计算HMAC-SHA512
|
||||
hmac1 := HMACSHA512(message, key)
|
||||
|
||||
// 再次计算相同的消息和密钥
|
||||
hmac2 := HMACSHA512(message, key)
|
||||
|
||||
// 相同的消息和密钥应该产生相同的HMAC
|
||||
if hmac1 != hmac2 {
|
||||
t.Errorf("相同输入产生了不同的HMAC: %s vs %s", hmac1, hmac2)
|
||||
// Sum + sha256.New 必须与 SumSHA256 完全一致。
|
||||
if got, want := hmacx.Sum(sha256.New, msg, key), hmacx.SumSHA256(msg, key); !bytes.Equal(got, want) {
|
||||
t.Errorf("Sum(sha256.New) 与 SumSHA256 不一致: %x vs %x", got, want)
|
||||
}
|
||||
|
||||
// 不同的消息应该产生不同的HMAC
|
||||
hmac3 := HMACSHA512("different message", key)
|
||||
if hmac1 == hmac3 {
|
||||
t.Errorf("不同消息产生了相同的HMAC")
|
||||
// 摘要长度必须正确,否则 Verify* 的长度校验会失效。
|
||||
if got := len(hmacx.SumSHA1(msg, key)); got != 20 {
|
||||
t.Errorf("HMAC-SHA1 摘要长度 = %d, 期望 20", got)
|
||||
}
|
||||
|
||||
// 不同的密钥应该产生不同的HMAC
|
||||
hmac4 := HMACSHA512(message, "different_key")
|
||||
if hmac1 == hmac4 {
|
||||
t.Errorf("不同密钥产生了相同的HMAC")
|
||||
if got := len(hmacx.SumSHA256(msg, key)); got != 32 {
|
||||
t.Errorf("HMAC-SHA256 摘要长度 = %d, 期望 32", got)
|
||||
}
|
||||
if got := len(hmacx.SumSHA384(msg, key)); got != 48 {
|
||||
t.Errorf("HMAC-SHA384 摘要长度 = %d, 期望 48", got)
|
||||
}
|
||||
if got := len(hmacx.SumSHA512(msg, key)); got != 64 {
|
||||
t.Errorf("HMAC-SHA512 摘要长度 = %d, 期望 64", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHMACSHA256VsSHA512(t *testing.T) {
|
||||
message := "hello world"
|
||||
key := "secret_key"
|
||||
// TestHexFunctions 校验十六进制便利函数与原始摘要一致且格式正确。
|
||||
func TestHexFunctions(t *testing.T) {
|
||||
key := []byte("a-key-with-enough-entropy")
|
||||
msg := []byte("hex convenience")
|
||||
|
||||
// 计算HMAC-SHA256和HMAC-SHA512
|
||||
hmac256 := HMACSHA256(message, key)
|
||||
hmac512 := HMACSHA512(message, key)
|
||||
cases := []struct {
|
||||
name string
|
||||
got string
|
||||
raw []byte
|
||||
}{
|
||||
{"HexSHA1", hmacx.HexSHA1(msg, key), hmacx.SumSHA1(msg, key)},
|
||||
{"HexSHA256", hmacx.HexSHA256(msg, key), hmacx.SumSHA256(msg, key)},
|
||||
{"HexSHA384", hmacx.HexSHA384(msg, key), hmacx.SumSHA384(msg, key)},
|
||||
{"HexSHA512", hmacx.HexSHA512(msg, key), hmacx.SumSHA512(msg, key)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if c.got != hex.EncodeToString(c.raw) {
|
||||
t.Errorf("%s 与原始摘要不一致: %s", c.name, c.got)
|
||||
}
|
||||
if c.got != strings.ToLower(c.got) {
|
||||
t.Errorf("%s 应为小写十六进制: %s", c.name, c.got)
|
||||
}
|
||||
if len(c.got) != len(c.raw)*2 {
|
||||
t.Errorf("%s 长度 = %d, 期望 %d", c.name, len(c.got), len(c.raw)*2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SHA256和SHA512应该产生不同的HMAC
|
||||
if hmac256 == hmac512 {
|
||||
t.Errorf("SHA256和SHA512产生了相同的HMAC")
|
||||
// TestVerifyAcceptsValidMAC 校验 Verify* 对正确 MAC 返回 true。
|
||||
func TestVerifyAcceptsValidMAC(t *testing.T) {
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
msg := []byte("verify the good mac")
|
||||
|
||||
if !hmacx.VerifySHA1(msg, key, hmacx.SumSHA1(msg, key)) {
|
||||
t.Error("VerifySHA1 对正确 MAC 返回了 false")
|
||||
}
|
||||
if !hmacx.VerifySHA256(msg, key, hmacx.SumSHA256(msg, key)) {
|
||||
t.Error("VerifySHA256 对正确 MAC 返回了 false")
|
||||
}
|
||||
if !hmacx.VerifySHA384(msg, key, hmacx.SumSHA384(msg, key)) {
|
||||
t.Error("VerifySHA384 对正确 MAC 返回了 false")
|
||||
}
|
||||
if !hmacx.VerifySHA512(msg, key, hmacx.SumSHA512(msg, key)) {
|
||||
t.Error("VerifySHA512 对正确 MAC 返回了 false")
|
||||
}
|
||||
if !hmacx.Verify(sha256.New, msg, key, hmacx.Sum(sha256.New, msg, key)) {
|
||||
t.Error("Verify(sha256.New) 对正确 MAC 返回了 false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyRejectsTamperedMAC 校验单比特翻转会被拒绝(且不 panic)。
|
||||
func TestVerifyRejectsTamperedMAC(t *testing.T) {
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
msg := []byte("verify the tampered mac")
|
||||
|
||||
tamper := func(src []byte) []byte {
|
||||
out := append([]byte(nil), src...)
|
||||
out[0] ^= 0x01 // 翻转 1 bit
|
||||
return out
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
good []byte
|
||||
ver func(message, key, expectedMAC []byte) bool
|
||||
}{
|
||||
{"SHA1", hmacx.SumSHA1(msg, key), hmacx.VerifySHA1},
|
||||
{"SHA256", hmacx.SumSHA256(msg, key), hmacx.VerifySHA256},
|
||||
{"SHA384", hmacx.SumSHA384(msg, key), hmacx.VerifySHA384},
|
||||
{"SHA512", hmacx.SumSHA512(msg, key), hmacx.VerifySHA512},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if tc.ver(msg, key, tamper(tc.good)) {
|
||||
t.Errorf("Verify%s 接受了被篡改 1 bit 的 MAC", tc.name)
|
||||
}
|
||||
// 错误的密钥同样必须被拒绝。
|
||||
if tc.ver(msg, []byte("another-key-with-32-bytes-len!!"), tc.good) {
|
||||
t.Errorf("Verify%s 接受了错误密钥下的 MAC", tc.name)
|
||||
}
|
||||
// 错误的消息同样必须被拒绝。
|
||||
if tc.ver([]byte("another message"), key, tc.good) {
|
||||
t.Errorf("Verify%s 接受了错误消息下的 MAC", tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyRejectsWrongLengthMAC 校验长度不符时返回 false 而不是 panic。
|
||||
func TestVerifyRejectsWrongLengthMAC(t *testing.T) {
|
||||
key := []byte("0123456789abcdef")
|
||||
msg := []byte("length check")
|
||||
|
||||
good := hmacx.SumSHA256(msg, key)
|
||||
|
||||
// 长度不足、超长、空切片、nil 都必须安全返回 false。
|
||||
lengths := [][]byte{
|
||||
nil,
|
||||
{},
|
||||
good[:len(good)-1],
|
||||
append(append([]byte(nil), good...), 0x00),
|
||||
make([]byte, 64),
|
||||
make([]byte, 1),
|
||||
}
|
||||
for _, mac := range lengths {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("VerifySHA256 对长度 %d 的 MAC 发生了 panic: %v", len(mac), r)
|
||||
}
|
||||
}()
|
||||
if hmacx.VerifySHA256(msg, key, mac) {
|
||||
t.Errorf("VerifySHA256 接受了长度 %d(期望 32)的 MAC", len(mac))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 其余算法的长度校验同样必须生效。
|
||||
if hmacx.VerifySHA1(msg, key, good) {
|
||||
t.Error("VerifySHA1 接受了 32 字节的 MAC(期望 20)")
|
||||
}
|
||||
if hmacx.VerifySHA384(msg, key, good) {
|
||||
t.Error("VerifySHA384 接受了 32 字节的 MAC(期望 48)")
|
||||
}
|
||||
if hmacx.VerifySHA512(msg, key, good) {
|
||||
t.Error("VerifySHA512 接受了 32 字节的 MAC(期望 64)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyEdgeCases 覆盖 nil 哈希构造函数等边界输入,确保不 panic。
|
||||
func TestVerifyEdgeCases(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("边界输入导致 panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
var nilHash func() hash.Hash
|
||||
if hmacx.Verify(nilHash, []byte("msg"), []byte("key"), []byte("mac")) {
|
||||
t.Error("Verify(nil) 应返回 false")
|
||||
}
|
||||
if hmacx.Sum(nilHash, []byte("msg"), []byte("key")) != nil {
|
||||
t.Error("Sum(nil) 应返回 nil")
|
||||
}
|
||||
|
||||
// 空消息与空密钥是合法输入:HMAC 仍然可以计算,只是空密钥没有认证意义。
|
||||
if got := len(hmacx.SumSHA256(nil, nil)); got != 32 {
|
||||
t.Errorf("空消息/空密钥的 HMAC-SHA256 长度 = %d, 期望 32", got)
|
||||
}
|
||||
if !hmacx.VerifySHA256(nil, nil, hmacx.SumSHA256(nil, nil)) {
|
||||
t.Error("VerifySHA256 对空消息/空密钥的正确 MAC 返回了 false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecureCompare 校验常量时间比较函数的行为。
|
||||
func TestSecureCompare(t *testing.T) {
|
||||
a := []byte("constant-time-comparison")
|
||||
|
||||
if !hmacx.SecureCompare(a, append([]byte(nil), a...)) {
|
||||
t.Error("SecureCompare 对相同内容返回了 false")
|
||||
}
|
||||
if hmacx.SecureCompare(a, []byte("constant-time-comparisou")) {
|
||||
t.Error("SecureCompare 对仅末字节不同的内容返回了 true")
|
||||
}
|
||||
if hmacx.SecureCompare(a, a[:len(a)-1]) {
|
||||
t.Error("SecureCompare 对长度不同的内容返回了 true")
|
||||
}
|
||||
if !hmacx.SecureCompare(nil, nil) {
|
||||
t.Error("SecureCompare(nil, nil) 应为 true")
|
||||
}
|
||||
if hmacx.SecureCompare(nil, []byte{}) != true {
|
||||
// nil 与空切片长度都为 0,应视为相等。
|
||||
t.Error("SecureCompare(nil, []byte{}) 应为 true")
|
||||
}
|
||||
// 与 Verify 的结果必须一致。
|
||||
key := []byte("0123456789abcdef")
|
||||
msg := []byte("consistency")
|
||||
mac := hmacx.SumSHA256(msg, key)
|
||||
if !hmacx.SecureCompare(mac, hmacx.SumSHA256(msg, key)) {
|
||||
t.Error("SecureCompare 对相同 MAC 返回了 false")
|
||||
}
|
||||
if !hmacx.VerifySHA256(msg, key, mac) {
|
||||
t.Error("VerifySHA256 与 SecureCompare 结果不一致")
|
||||
}
|
||||
}
|
||||
|
||||
// errReader 是一个总是返回错误的 io.Reader,用于测试流式读取的错误路径。
|
||||
type errReader struct{}
|
||||
|
||||
func (errReader) Read([]byte) (int, error) {
|
||||
return 0, errors.New("模拟的读取失败")
|
||||
}
|
||||
|
||||
// shortReader 在返回全部数据后返回错误,模拟"读一半失败"的场景。
|
||||
type shortReader struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (s *shortReader) Read(p []byte) (int, error) {
|
||||
if len(s.data) == 0 {
|
||||
return 0, errors.New("模拟的读取中断")
|
||||
}
|
||||
n := copy(p, s.data)
|
||||
s.data = s.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// TestSumReaderSHA256 校验流式计算与整体计算一致,以及错误路径。
|
||||
func TestSumReaderSHA256(t *testing.T) {
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
|
||||
payloads := [][]byte{
|
||||
nil,
|
||||
{},
|
||||
[]byte("a"),
|
||||
[]byte("streaming payload that is longer than one internal block boundary 0123456789"),
|
||||
bytes.Repeat([]byte("block"), 1000),
|
||||
}
|
||||
for i, payload := range payloads {
|
||||
got, err := hmacx.SumReaderSHA256(bytes.NewReader(payload), key)
|
||||
if err != nil {
|
||||
t.Fatalf("用例 %d: SumReaderSHA256 返回错误: %v", i, err)
|
||||
}
|
||||
want := hmacx.SumSHA256(payload, key)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("用例 %d: SumReaderSHA256 与 SumSHA256 不一致: %x vs %x", i, got, want)
|
||||
}
|
||||
if !hmacx.VerifySHA256(payload, key, got) {
|
||||
t.Errorf("用例 %d: 流式摘要未能通过 VerifySHA256", i)
|
||||
}
|
||||
}
|
||||
|
||||
// 读取失败必须返回 error 与 nil 摘要,绝不返回部分结果。
|
||||
got, err := hmacx.SumReaderSHA256(errReader{}, key)
|
||||
if err == nil {
|
||||
t.Error("读取失败时 SumReaderSHA256 未返回错误")
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("读取失败时 SumReaderSHA256 应返回 nil,实际 %x", got)
|
||||
}
|
||||
|
||||
got, err = hmacx.SumReaderSHA256(&shortReader{data: []byte("partial data")}, key)
|
||||
if err == nil {
|
||||
t.Error("读取中断时 SumReaderSHA256 未返回错误")
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("读取中断时 SumReaderSHA256 应返回 nil,实际 %x", got)
|
||||
}
|
||||
|
||||
// nil reader 不能 panic。
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("nil reader 导致 panic: %v", r)
|
||||
}
|
||||
}()
|
||||
got, err := hmacx.SumReaderSHA256(nil, key)
|
||||
if err == nil {
|
||||
t.Error("nil reader 应返回错误")
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("nil reader 时应返回 nil,实际 %x", got)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// TestConstants 校验导出的长度常量为文档约定的值。
|
||||
func TestConstants(t *testing.T) {
|
||||
if hmacx.MinKeyLength != 16 {
|
||||
t.Errorf("MinKeyLength = %d, 期望 16", hmacx.MinKeyLength)
|
||||
}
|
||||
if hmacx.DefaultKeyLength != 32 {
|
||||
t.Errorf("DefaultKeyLength = %d, 期望 32", hmacx.DefaultKeyLength)
|
||||
}
|
||||
if hmacx.DefaultKeyLength < hmacx.MinKeyLength {
|
||||
t.Error("DefaultKeyLength 不应小于 MinKeyLength")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoPanicOnAnyInput 用一批畸形输入整体验证不 panic。
|
||||
func TestNoPanicOnAnyInput(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("畸形输入导致 panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
msgs := [][]byte{nil, {}, []byte("x"), bytes.Repeat([]byte{0xff}, 1<<10)}
|
||||
keys := [][]byte{nil, {}, []byte("k"), bytes.Repeat([]byte{0x00}, 200)}
|
||||
for _, m := range msgs {
|
||||
for _, k := range keys {
|
||||
_ = hmacx.SumSHA1(m, k)
|
||||
_ = hmacx.SumSHA384(m, k)
|
||||
_ = hmacx.HexSHA512(m, k)
|
||||
_ = hmacx.VerifySHA256(m, k, nil)
|
||||
_ = hmacx.SecureCompare(m, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user