Files
tyapi-server/internal/shared/crypto/signature.go
2026-02-12 13:36:24 +08:00

359 lines
9.2 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 crypto
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
"tyapi-server/internal/shared/interfaces"
)
const (
// SignatureTimestampTolerance 签名时间戳容差(秒),防止重放攻击
SignatureTimestampTolerance = 300 // 5分钟
)
// GenerateSignature 生成HMAC-SHA256签名
// params: 需要签名的参数map
// secretKey: 签名密钥
// timestamp: 时间戳(秒)
// nonce: 随机字符串
func GenerateSignature(params map[string]string, secretKey string, timestamp int64, nonce string) string {
// 1. 构建待签名字符串按key排序拼接成 key1=value1&key2=value2 格式
var keys []string
for k := range params {
if k != "signature" { // 排除签名字段本身
keys = append(keys, k)
}
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
}
// 2. 添加时间戳和随机数
parts = append(parts, fmt.Sprintf("timestamp=%d", timestamp))
parts = append(parts, fmt.Sprintf("nonce=%s", nonce))
// 3. 拼接成待签名字符串
signString := strings.Join(parts, "&")
// 4. 使用HMAC-SHA256计算签名
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(signString))
signature := mac.Sum(nil)
// 5. 返回hex编码的签名
return hex.EncodeToString(signature)
}
// VerifySignature 验证HMAC-SHA256签名
// params: 请求参数map包含signature字段
// secretKey: 签名密钥
// timestamp: 时间戳(秒)
// nonce: 随机字符串
func VerifySignature(params map[string]string, secretKey string, timestamp int64, nonce string) error {
// 1. 检查签名字段是否存在
signature, exists := params["signature"]
if !exists || signature == "" {
return errors.New("签名字段缺失")
}
// 2. 验证时间戳(防止重放攻击)
now := time.Now().Unix()
if timestamp <= 0 {
return errors.New("时间戳无效")
}
if abs(now-timestamp) > SignatureTimestampTolerance {
return fmt.Errorf("请求已过期,时间戳超出容差范围(当前时间:%d请求时间%d", now, timestamp)
}
// 3. 重新计算签名
expectedSignature := GenerateSignature(params, secretKey, timestamp, nonce)
// 4. 将hex字符串转换为字节数组进行比较
signatureBytes, err := hex.DecodeString(signature)
if err != nil {
return fmt.Errorf("签名格式错误: %w", err)
}
expectedBytes, err := hex.DecodeString(expectedSignature)
if err != nil {
return fmt.Errorf("签名计算错误: %w", err)
}
// 5. 使用常量时间比较防止时序攻击
if !hmac.Equal(signatureBytes, expectedBytes) {
return errors.New("签名验证失败")
}
return nil
}
// VerifySignatureWithNonceCheck 验证HMAC-SHA256签名并检查nonce唯一性防止重放攻击
// params: 请求参数map包含signature字段
// secretKey: 签名密钥
// timestamp: 时间戳(秒)
// nonce: 随机字符串
// cache: 缓存服务用于存储已使用的nonce
// cacheKeyPrefix: 缓存键前缀
func VerifySignatureWithNonceCheck(
ctx context.Context,
params map[string]string,
secretKey string,
timestamp int64,
nonce string,
cache interfaces.CacheService,
cacheKeyPrefix string,
) error {
// 1. 先进行基础签名验证
if err := VerifySignature(params, secretKey, timestamp, nonce); err != nil {
return err
}
// 2. 检查nonce是否已被使用防止重放攻击
// 使用请求指纹phone+timestamp+nonce 作为唯一标识
phone := params["phone"]
if phone == "" {
return errors.New("手机号不能为空")
}
// 构建nonce唯一性检查的缓存键
nonceKey := fmt.Sprintf("%s:nonce:%s:%d:%s", cacheKeyPrefix, phone, timestamp, nonce)
// 检查nonce是否已被使用
exists, err := cache.Exists(ctx, nonceKey)
if err != nil {
// 缓存查询失败,记录错误但继续验证(避免缓存故障导致服务不可用)
return fmt.Errorf("检查nonce唯一性失败: %w", err)
}
if exists {
return errors.New("请求已被使用,请勿重复提交")
}
// 3. 将nonce标记为已使用TTL设置为时间戳容差+1分钟确保在容差范围内不会重复使用
ttl := time.Duration(SignatureTimestampTolerance+60) * time.Second
if err := cache.Set(ctx, nonceKey, true, ttl); err != nil {
// 记录错误但不影响验证流程(避免缓存故障导致服务不可用)
return fmt.Errorf("标记nonce已使用失败: %w", err)
}
return nil
}
// 自定义编码字符集不使用标准Base64字符集增加破解难度
// 使用自定义字符集:数字+大写字母排除易混淆的I和O+小写字母排除易混淆的i和l+特殊字符
const customEncodeCharset = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"
// EncodeRequest 使用自定义编码方案编码请求参数
// 编码方式类似Base64但使用自定义字符集并加入简单的混淆
func EncodeRequest(data string) string {
// 1. 将字符串转换为字节数组
bytes := []byte(data)
// 2. 使用自定义Base64变种编码
encoded := customBase64Encode(bytes)
// 3. 添加简单的字符混淆(字符偏移)
confused := applyCharShift(encoded, 7) // 偏移7个位置
return confused
}
// DecodeRequest 解码请求参数
func DecodeRequest(encodedData string) (string, error) {
// 1. 先还原字符混淆
unconfused := reverseCharShift(encodedData, 7)
// 2. 使用自定义Base64变种解码
decoded, err := customBase64Decode(unconfused)
if err != nil {
return "", fmt.Errorf("解码失败: %w", err)
}
return string(decoded), nil
}
// customBase64Encode 自定义Base64编码使用自定义字符集
func customBase64Encode(data []byte) string {
if len(data) == 0 {
return ""
}
var result []byte
charset := []byte(customEncodeCharset)
// 将3个字节24位编码为4个字符
for i := 0; i < len(data); i += 3 {
// 读取3个字节
var b1, b2, b3 byte
b1 = data[i]
if i+1 < len(data) {
b2 = data[i+1]
}
if i+2 < len(data) {
b3 = data[i+2]
}
// 组合成24位
combined := uint32(b1)<<16 | uint32(b2)<<8 | uint32(b3)
// 分成4个6位段
result = append(result, charset[(combined>>18)&0x3F])
result = append(result, charset[(combined>>12)&0x3F])
if i+1 < len(data) {
result = append(result, charset[(combined>>6)&0x3F])
} else {
result = append(result, '=') // 填充字符
}
if i+2 < len(data) {
result = append(result, charset[combined&0x3F])
} else {
result = append(result, '=') // 填充字符
}
}
return string(result)
}
// customBase64Decode 自定义Base64解码
func customBase64Decode(encoded string) ([]byte, error) {
if len(encoded) == 0 {
return []byte{}, nil
}
charset := []byte(customEncodeCharset)
charsetMap := make(map[byte]int)
for i, c := range charset {
charsetMap[c] = i
}
var result []byte
data := []byte(encoded)
// 将4个字符解码为3个字节
for i := 0; i < len(data); i += 4 {
if i+3 >= len(data) {
return nil, fmt.Errorf("编码数据长度不正确")
}
// 获取4个字符的索引
var idx [4]int
for j := 0; j < 4; j++ {
if data[i+j] == '=' {
idx[j] = 0 // 填充字符
} else {
val, ok := charsetMap[data[i+j]]
if !ok {
return nil, fmt.Errorf("无效的编码字符: %c", data[i+j])
}
idx[j] = val
}
}
// 组合成24位
combined := uint32(idx[0])<<18 | uint32(idx[1])<<12 | uint32(idx[2])<<6 | uint32(idx[3])
// 提取3个字节
result = append(result, byte((combined>>16)&0xFF))
if data[i+2] != '=' {
result = append(result, byte((combined>>8)&0xFF))
}
if data[i+3] != '=' {
result = append(result, byte(combined&0xFF))
}
}
return result, nil
}
// applyCharShift 应用字符偏移混淆
func applyCharShift(data string, shift int) string {
charset := customEncodeCharset
charsetLen := len(charset)
result := make([]byte, len(data))
for i, c := range []byte(data) {
if c == '=' {
result[i] = c // 填充字符不变
continue
}
// 查找字符在字符集中的位置
idx := -1
for j, ch := range []byte(charset) {
if ch == c {
idx = j
break
}
}
if idx == -1 {
result[i] = c // 不在字符集中,保持不变
} else {
// 应用偏移
newIdx := (idx + shift) % charsetLen
result[i] = charset[newIdx]
}
}
return string(result)
}
// reverseCharShift 还原字符偏移混淆
func reverseCharShift(data string, shift int) string {
charset := customEncodeCharset
charsetLen := len(charset)
result := make([]byte, len(data))
for i, c := range []byte(data) {
if c == '=' {
result[i] = c // 填充字符不变
continue
}
// 查找字符在字符集中的位置
idx := -1
for j, ch := range []byte(charset) {
if ch == c {
idx = j
break
}
}
if idx == -1 {
result[i] = c // 不在字符集中,保持不变
} else {
// 还原偏移
newIdx := (idx - shift + charsetLen) % charsetLen
result[i] = charset[newIdx]
}
}
return string(result)
}
// abs 计算绝对值
func abs(x int64) int64 {
if x < 0 {
return -x
}
return x
}
// ParseTimestamp 从字符串解析时间戳
func ParseTimestamp(ts string) (int64, error) {
return strconv.ParseInt(ts, 10, 64)
}