Files
superidou 9501751792 0.10.1: 安全与业务逻辑加固、新品牌与部署加固
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
  与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
  撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
  全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
  >4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
  后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
  nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
2026-08-13 10:50:51 +08:00

128 lines
3.5 KiB
Go

package identity
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
"encoding/base32"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"net/url"
"strings"
"time"
)
const (
totpPeriod = int64(30)
totpDigits = 6
totpWindow = int64(1)
backupCodeCount = 10
backupCodeLength = 8
)
var backupAlphabet = []byte("23456789ABCDEFGHJKLMNPQRSTUVWXYZ")
type BackupCodeRecord struct {
Hash string `json:"hash"`
UsedAt *time.Time `json:"used_at,omitempty"`
}
func GenerateTOTPSecret() (string, error) {
secret := make([]byte, 20)
if _, err := rand.Read(secret); err != nil {
return "", err
}
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(secret), nil
}
func TOTPProvisioningURI(secret string, kind Kind, login string) string {
issuer := "AI Gateway"
label := issuer + ":" + string(kind) + ":" + login
query := url.Values{
"secret": []string{secret},
"issuer": []string{issuer},
"algorithm": []string{"SHA1"},
"digits": []string{fmt.Sprint(totpDigits)},
"period": []string{fmt.Sprint(totpPeriod)},
}
return "otpauth://totp/" + url.PathEscape(label) + "?" + query.Encode()
}
func VerifyTOTP(secret, code string, now time.Time) (int64, bool) {
code = strings.TrimSpace(code)
if len(code) != totpDigits {
return 0, false
}
step := now.Unix() / totpPeriod
for offset := -totpWindow; offset <= totpWindow; offset++ {
candidate, err := hotp(secret, step+offset, totpDigits)
if err == nil && subtle.ConstantTimeCompare([]byte(candidate), []byte(code)) == 1 {
return step + offset, true
}
}
return 0, false
}
func hotp(secret string, counter int64, digits int) (string, error) {
if counter < 0 || digits < 6 || digits > 8 {
return "", errors.New("invalid HOTP parameters")
}
decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(strings.TrimSpace(secret)))
if err != nil {
return "", err
}
message := make([]byte, 8)
binary.BigEndian.PutUint64(message, uint64(counter))
mac := hmac.New(sha1.New, decoded)
_, _ = mac.Write(message)
digest := mac.Sum(nil)
offset := digest[len(digest)-1] & 0x0f
value := (uint32(digest[offset])&0x7f)<<24 |
uint32(digest[offset+1])<<16 |
uint32(digest[offset+2])<<8 |
uint32(digest[offset+3])
modulus := uint32(1)
for i := 0; i < digits; i++ {
modulus *= 10
}
return fmt.Sprintf("%0*d", digits, value%modulus), nil
}
func GenerateBackupCodes() ([]string, []BackupCodeRecord, error) {
codes := make([]string, 0, backupCodeCount)
records := make([]BackupCodeRecord, 0, backupCodeCount)
for range backupCodeCount {
random := make([]byte, backupCodeLength)
if _, err := rand.Read(random); err != nil {
return nil, nil, err
}
for index := range random {
// 拒绝采样消除取模偏差:256 % 31 = 8,直接取模会让前 8 个字符
// 的概率略高于其余字符。丢弃 248..255 的取值后分布均匀。
value := random[index]
for value >= 248 {
if _, err := rand.Read(random[index : index+1]); err != nil {
return nil, nil, err
}
value = random[index]
}
random[index] = backupAlphabet[int(value)%len(backupAlphabet)]
}
raw := string(random)
code := raw[:4] + "-" + raw[4:]
codes = append(codes, code)
records = append(records, BackupCodeRecord{Hash: HashBackupCode(code)})
}
return codes, records, nil
}
func HashBackupCode(code string) string {
normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(code), "-", ""))
digest := sha256.Sum256([]byte(normalized))
return hex.EncodeToString(digest[:])
}