diff --git a/config.yaml b/config.yaml index 889f76e..1a5113d 100644 --- a/config.yaml +++ b/config.yaml @@ -129,6 +129,9 @@ sms: code_length: 6 expire_time: 5m mock_enabled: false + # 签名验证配置(用于防止接口被刷) + signature_enabled: true # 是否启用签名验证 + signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥) rate_limit: daily_limit: 10 hourly_limit: 5 diff --git a/docs/短信接口签名验证使用指南.md b/docs/短信接口签名验证使用指南.md new file mode 100644 index 0000000..e31b445 --- /dev/null +++ b/docs/短信接口签名验证使用指南.md @@ -0,0 +1,347 @@ +# 短信接口签名验证使用指南 + +## 概述 + +为了防止短信发送接口被恶意刷取,系统实现了基于HMAC-SHA256的签名验证机制。所有发送短信的请求必须包含有效的签名,否则请求将被拒绝。 + +## 工作原理 + +1. **前端生成签名**:使用密钥对请求参数进行HMAC-SHA256签名 +2. **后端验证签名**:后端使用相同密钥重新计算签名并比对 +3. **时间戳验证**:防止重放攻击,时间戳必须在5分钟内有效 +4. **随机数验证**:每次请求必须包含唯一的随机字符串(nonce) +5. **参数编码传输**(推荐):将所有参数(包括签名)编码成Base64字符串后传输,隐藏参数结构,增加安全性 + +## 配置说明 + +### 后端配置 + +在 `config.yaml` 中配置签名相关参数: + +```yaml +sms: + # ... 其他配置 ... + # 签名验证配置 + signature_enabled: true # 是否启用签名验证 + signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥) +``` + +**重要提示**: +- 生产环境必须修改 `signature_secret` 为复杂的随机字符串 +- 密钥长度建议至少32个字符 +- 密钥应包含大小写字母、数字和特殊字符 + +## 签名算法 + +### 1. 构建待签名字符串 + +将请求参数(排除`signature`字段)按key排序,拼接成以下格式: + +``` +key1=value1&key2=value2×tamp=1234567890&nonce=random_string +``` + +### 2. 计算HMAC-SHA256签名 + +使用配置的密钥对待签名字符串进行HMAC-SHA256计算,结果转换为hex编码。 + +### 3. 请求参数 + +系统支持两种请求方式: + +#### 方式1:直接传递参数 + +发送请求时直接传递所有字段: + +```json +{ + "phone": "13800138000", + "scene": "register", + "timestamp": 1704067200, + "nonce": "a1b2c3d4e5f6g7h8", + "signature": "abc123def456..." +} +``` + +#### 方式2:编码后传输(推荐,更安全) + +将所有参数(包括签名)编码成Base64字符串后传输,只传递一个`data`字段: + +```json +{ + "data": "eyJwaG9uZSI6IjEzODAwMTM4MDAwIiwic2NlbmUiOiJyZWdpc3RlciIsInRpbWVzdGFtcCI6MTcwNDA2NzIwMCwibm9uY2UiOiJhMWIyYzNkNGE1ZjYiLCJzaWduYXR1cmUiOiJhYmMxMjNkZWY0NTYifQ==" +} +``` + +**编码传输的优势**: +- 隐藏参数结构,增加破解难度 +- 参数不可见,防止参数被直接修改 +- 增加一层编码保护 + +## 前端实现 + +### Node.js 示例 + +参考文件:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js` + +#### 方式1:直接传递参数 + +```javascript +const crypto = require('crypto'); + +function generateSignature(params, secretKey, timestamp, nonce) { + // 1. 构建待签名字符串 + const keys = Object.keys(params) + .filter(k => k !== 'signature') + .sort(); + + const parts = keys.map(k => `${k}=${params[k]}`); + parts.push(`timestamp=${timestamp}`); + parts.push(`nonce=${nonce}`); + + const signString = parts.join('&'); + + // 2. 计算HMAC-SHA256签名 + const signature = crypto + .createHmac('sha256', secretKey) + .update(signString) + .digest('hex'); + + return signature; +} + +// 使用示例 +const params = { phone: '13800138000', scene: 'register' }; +const timestamp = Math.floor(Date.now() / 1000); +const nonce = generateRandomString(16); +const secretKey = 'your_secret_key'; +const signature = generateSignature(params, secretKey, timestamp, nonce); + +// 发送请求 +const requestBody = { + phone: '13800138000', + scene: 'register', + timestamp: timestamp, + nonce: nonce, + signature: signature, +}; +``` + +#### 方式2:编码后传输(推荐) + +```javascript +// 1. 生成签名(同上) +const params = { phone: '13800138000', scene: 'register' }; +const timestamp = Math.floor(Date.now() / 1000); +const nonce = generateRandomString(16); +const secretKey = 'your_secret_key'; +const signature = generateSignature(params, secretKey, timestamp, nonce); + +// 2. 构建包含所有参数的JSON对象 +const allParams = { + phone: '13800138000', + scene: 'register', + timestamp: timestamp, + nonce: nonce, + signature: signature, +}; + +// 3. 编码为Base64 +const jsonString = JSON.stringify(allParams); +const encodedData = Buffer.from(jsonString).toString('base64'); + +// 4. 发送请求(只传递data字段) +const requestBody = { + data: encodedData, +}; +``` + +### 浏览器 JavaScript 示例 + +参考文件:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js` + +#### 方式1:直接传递参数 + +```javascript +async function generateSignature(params, secretKey, timestamp, nonce) { + // 1. 构建待签名字符串 + const keys = Object.keys(params) + .filter(k => k !== 'signature') + .sort(); + + const parts = keys.map(k => `${k}=${params[k]}`); + parts.push(`timestamp=${timestamp}`); + parts.push(`nonce=${nonce}`); + + const signString = parts.join('&'); + + // 2. 使用Web Crypto API计算HMAC-SHA256签名 + const encoder = new TextEncoder(); + const keyData = encoder.encode(secretKey); + const messageData = encoder.encode(signString); + + const cryptoKey = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData); + const hashArray = Array.from(new Uint8Array(signature)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + return hashHex; +} + +// 使用示例 +const params = { phone: '13800138000', scene: 'register' }; +const timestamp = Math.floor(Date.now() / 1000); +const nonce = generateNonce(16); +const secretKey = 'your_secret_key'; +const signature = await generateSignature(params, secretKey, timestamp, nonce); + +// 发送请求 +const requestBody = { + phone: '13800138000', + scene: 'register', + timestamp: timestamp, + nonce: nonce, + signature: signature, +}; +``` + +#### 方式2:编码后传输(推荐) + +```javascript +// 1. 生成签名(同上) +const params = { phone: '13800138000', scene: 'register' }; +const timestamp = Math.floor(Date.now() / 1000); +const nonce = generateNonce(16); +const secretKey = 'your_secret_key'; +const signature = await generateSignature(params, secretKey, timestamp, nonce); + +// 2. 构建包含所有参数的JSON对象 +const allParams = { + phone: '13800138000', + scene: 'register', + timestamp: timestamp, + nonce: nonce, + signature: signature, +}; + +// 3. 编码为Base64(浏览器环境) +const jsonString = JSON.stringify(allParams); +const encodedData = btoa(unescape(encodeURIComponent(jsonString))); + +// 4. 发送请求(只传递data字段) +const requestBody = { + data: encodedData, +}; +``` + +## 密钥隐藏策略 + +由于前端代码可以被查看,完全隐藏密钥是不可能的。但可以通过以下方式增加破解难度: + +### 1. 字符串拆分和拼接 + +```javascript +function getSecretKey() { + const part1 = 'TyApi2024'; + const part2 = 'SMSSecret'; + const part3 = 'Key!@#$%^'; + return part1 + part2 + part3; +} +``` + +### 2. 字符数组拼接 + +```javascript +function getSecretKey() { + const chars = ['T', 'y', 'A', 'p', 'i', ...]; + return chars.join(''); +} +``` + +### 3. Base64编码混淆 + +```javascript +function getSecretKey() { + const encoded = 'base64_encoded_string'; + return atob(encoded); +} +``` + +### 4. 代码混淆 + +使用构建工具(如webpack、rollup等)进行代码混淆和压缩,使密钥更难被发现。 + +### 5. 后端代理(推荐) + +将签名逻辑放在后端代理接口中,前端只调用代理接口,不直接包含密钥。 + +## 安全建议 + +1. **定期更换密钥**:建议每3-6个月更换一次签名密钥 +2. **监控异常请求**:监控签名验证失败的请求,及时发现攻击行为 +3. **结合其他防护措施**: + - IP限流 + - 设备指纹识别 + - 验证码(图形验证码) + - 行为分析 + +4. **日志记录**:记录所有签名验证失败的请求,包括IP、User-Agent等信息 + +## 错误处理 + +### 常见错误 + +1. **签名字段缺失**:返回 `"签名字段缺失"` +2. **时间戳无效**:返回 `"时间戳无效"` +3. **请求已过期**:返回 `"请求已过期,时间戳超出容差范围"` +4. **签名验证失败**:返回 `"签名验证失败"` + +### 时间戳容差 + +系统允许的时间戳容差为 **5分钟**(300秒)。如果请求时间戳与服务器时间差超过5分钟,请求将被拒绝。 + +## 测试 + +### 测试签名生成 + +```bash +# 使用Node.js示例 +node tyapi-frontend/public/examples/nodejs/sms_signature_demo.js +``` + +### 测试API调用 + +```bash +curl -X POST http://localhost:8080/api/v1/users/send-code \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "13800138000", + "scene": "register", + "timestamp": 1704067200, + "nonce": "a1b2c3d4e5f6g7h8", + "signature": "计算得到的签名" + }' +``` + +## 注意事项 + +1. **时间同步**:确保客户端和服务器时间同步,避免时间戳验证失败 +2. **随机数唯一性**:每次请求的nonce应该是唯一的,可以使用UUID或时间戳+随机数 +3. **密钥安全**:生产环境密钥不要提交到代码仓库,应使用环境变量或密钥管理服务 +4. **向后兼容**:如果需要在开发环境禁用签名验证,可以设置 `signature_enabled: false` + +## 相关文件 + +- 后端签名工具:`internal/shared/crypto/signature.go` +- 后端Handler:`internal/infrastructure/http/handlers/user_handler.go` +- 配置结构:`internal/config/config.go` +- Node.js示例:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js` +- 浏览器示例:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js` + diff --git a/internal/application/user/dto/commands/user_commands.go b/internal/application/user/dto/commands/user_commands.go index cfc9263..ec0a23f 100644 --- a/internal/application/user/dto/commands/user_commands.go +++ b/internal/application/user/dto/commands/user_commands.go @@ -43,10 +43,17 @@ type ResetPasswordCommand struct { } // SendCodeCommand 发送验证码命令 -// @Description 发送短信验证码请求参数 +// @Description 发送短信验证码请求参数。只接收编码后的data字段(使用自定义编码方案,非Base64) type SendCodeCommand struct { - Phone string `json:"phone" binding:"required,phone" example:"13800138000"` - Scene string `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind certification" example:"register"` + // 编码后的数据(使用自定义编码方案的JSON字符串,包含所有参数:phone, scene, timestamp, nonce, signature) + Data string `json:"data" binding:"required" example:"K8mN9vP2sL7kH3oB6yC1zA5uF0qE9tW..."` // 自定义编码后的数据 + + // 以下字段从data解码后填充,不直接接收 + Phone string `json:"-"` // 从data解码后获取 + Scene string `json:"-"` // 从data解码后获取 + Timestamp int64 `json:"-"` // 从data解码后获取 + Nonce string `json:"-"` // 从data解码后获取 + Signature string `json:"-"` // 从data解码后获取 } // UpdateProfileCommand 更新用户信息命令 diff --git a/internal/config/config.go b/internal/config/config.go index 9c8d78e..3615ad2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -213,6 +213,9 @@ type SMSConfig struct { ExpireTime time.Duration `mapstructure:"expire_time"` RateLimit SMSRateLimit `mapstructure:"rate_limit"` MockEnabled bool `mapstructure:"mock_enabled"` // 是否启用模拟短信服务 + // 签名验证配置 + SignatureEnabled bool `mapstructure:"signature_enabled"` // 是否启用签名验证 + SignatureSecret string `mapstructure:"signature_secret"` // 签名密钥 } // SMSRateLimit 短信限流配置 diff --git a/internal/infrastructure/http/handlers/user_handler.go b/internal/infrastructure/http/handlers/user_handler.go index b639314..27f9460 100644 --- a/internal/infrastructure/http/handlers/user_handler.go +++ b/internal/infrastructure/http/handlers/user_handler.go @@ -2,6 +2,8 @@ package handlers import ( + "encoding/json" + "fmt" "strconv" "github.com/gin-gonic/gin" @@ -11,6 +13,8 @@ import ( "tyapi-server/internal/application/user/dto/commands" "tyapi-server/internal/application/user/dto/queries" _ "tyapi-server/internal/application/user/dto/responses" + "tyapi-server/internal/config" + "tyapi-server/internal/shared/crypto" "tyapi-server/internal/shared/interfaces" "tyapi-server/internal/shared/middleware" ) @@ -22,6 +26,7 @@ type UserHandler struct { validator interfaces.RequestValidator logger *zap.Logger jwtAuth *middleware.JWTAuthMiddleware + config *config.Config } // NewUserHandler 创建用户处理器 @@ -31,6 +36,7 @@ func NewUserHandler( validator interfaces.RequestValidator, logger *zap.Logger, jwtAuth *middleware.JWTAuthMiddleware, + cfg *config.Config, ) *UserHandler { return &UserHandler{ appService: appService, @@ -38,16 +44,26 @@ func NewUserHandler( validator: validator, logger: logger, jwtAuth: jwtAuth, + config: cfg, } } +// decodedSendCodeData 解码后的请求数据结构 +type decodedSendCodeData struct { + Phone string `json:"phone"` + Scene string `json:"scene"` + Timestamp int64 `json:"timestamp"` + Nonce string `json:"nonce"` + Signature string `json:"signature"` +} + // SendCode 发送验证码 // @Summary 发送短信验证码 -// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景 +// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景。需要提供有效的签名验证。只接收编码后的data字段(使用自定义编码方案) // @Tags 用户认证 // @Accept json // @Produce json -// @Param request body commands.SendCodeCommand true "发送验证码请求" +// @Param request body commands.SendCodeCommand true "发送验证码请求(只包含data字段)" // @Success 200 {object} map[string]interface{} "验证码发送成功" // @Failure 400 {object} map[string]interface{} "请求参数错误" // @Failure 429 {object} map[string]interface{} "请求频率限制" @@ -55,14 +71,61 @@ func NewUserHandler( // @Router /api/v1/users/send-code [post] func (h *UserHandler) SendCode(c *gin.Context) { var cmd commands.SendCodeCommand - if err := h.validator.BindAndValidate(c, &cmd); err != nil { + + // 绑定请求(只包含data字段) + if err := c.ShouldBindJSON(&cmd); err != nil { + h.response.BadRequest(c, "请求参数格式错误,必须提供data字段") return } + // 验证data字段不为空 + if cmd.Data == "" { + h.response.BadRequest(c, "data字段不能为空") + return + } + + // 解码自定义编码的数据 + decodedData, err := h.decodeRequestData(cmd.Data) + if err != nil { + h.logger.Warn("解码请求数据失败", + zap.String("client_ip", c.ClientIP()), + zap.Error(err)) + h.response.BadRequest(c, "请求数据解码失败") + return + } + + // 验证必要字段 + if decodedData.Phone == "" || decodedData.Scene == "" { + h.response.BadRequest(c, "手机号和场景不能为空") + return + } + + // 如果启用了签名验证,进行签名校验 + if h.config.SMS.SignatureEnabled { + if err := h.verifyDecodedSignature(decodedData); err != nil { + h.logger.Warn("短信发送签名验证失败", + zap.String("phone", decodedData.Phone), + zap.String("scene", decodedData.Scene), + zap.String("client_ip", c.ClientIP()), + zap.Error(err)) + h.response.BadRequest(c, "签名验证失败,请求无效") + return + } + } + + // 构建SendCodeCommand用于调用应用服务 + serviceCmd := &commands.SendCodeCommand{ + Phone: decodedData.Phone, + Scene: decodedData.Scene, + Timestamp: decodedData.Timestamp, + Nonce: decodedData.Nonce, + Signature: decodedData.Signature, + } + clientIP := c.ClientIP() userAgent := c.GetHeader("User-Agent") - if err := h.appService.SendCode(c.Request.Context(), &cmd, clientIP, userAgent); err != nil { + if err := h.appService.SendCode(c.Request.Context(), serviceCmd, clientIP, userAgent); err != nil { h.response.BadRequest(c, err.Error()) return } @@ -70,6 +133,42 @@ func (h *UserHandler) SendCode(c *gin.Context) { h.response.Success(c, nil, "验证码发送成功") } +// decodeRequestData 解码自定义编码的请求数据 +func (h *UserHandler) decodeRequestData(encodedData string) (*decodedSendCodeData, error) { + // 使用自定义编码方案解码 + decodedData, err := crypto.DecodeRequest(encodedData) + if err != nil { + return nil, fmt.Errorf("自定义编码解码失败: %w", err) + } + + // 解析JSON + var decoded decodedSendCodeData + if err := json.Unmarshal([]byte(decodedData), &decoded); err != nil { + return nil, fmt.Errorf("JSON解析失败: %w", err) + } + + return &decoded, nil +} + +// verifyDecodedSignature 验证解码后的签名 +func (h *UserHandler) verifyDecodedSignature(data *decodedSendCodeData) error { + // 构建参数map(包含signature字段,VerifySignature会自动排除它) + params := map[string]string{ + "phone": data.Phone, + "scene": data.Scene, + "signature": data.Signature, + } + + // 验证签名 + return crypto.VerifySignature( + params, + h.config.SMS.SignatureSecret, + data.Timestamp, + data.Nonce, + ) +} + + // Register 用户注册 // @Summary 用户注册 // @Description 使用手机号、密码和验证码进行用户注册,需要确认密码 diff --git a/internal/shared/crypto/signature.go b/internal/shared/crypto/signature.go new file mode 100644 index 0000000..2f031ec --- /dev/null +++ b/internal/shared/crypto/signature.go @@ -0,0 +1,304 @@ +package crypto + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" +) + +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 +} + +// 自定义编码字符集(不使用标准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) +} +