Files
encryptx/hmacx/hmacx.go
T
2026-09-30 15:49:12 +08:00

278 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}