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