package main import ( "bytes" "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strings" "time" ) const clientVersion = "2.0.1" type appConfig struct { DisplayName string AllowedAPIs []string } var appRegistry = map[string]appConfig{ "order_site": { DisplayName: "订单后台", AllowedAPIs: []string{ "https://ks.khhao.com", "https://order.khhao.com", "http://localhost", "http://127.0.0.1", "http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:3000", "http://127.0.0.1:3000", "http://192.168.10.6", "http://192.168.10.6:5173", }, }, "live_hub": { DisplayName: "直播运营后台", AllowedAPIs: []string{ "https://live.khhao.com", "http://localhost:5174", "http://127.0.0.1:5174", "http://localhost:8800", "http://127.0.0.1:8800", }, }, "affiliate_dash": { DisplayName: "皮肤分销后台", AllowedAPIs: []string{ "https://skin.khhao.com", "http://localhost:15173", "http://127.0.0.1:15173", }, }, } type identity struct { privateKey ed25519.PrivateKey publicKey ed25519.PublicKey deviceID string } type authorizeRequest struct { App string `json:"app"` ChallengeID string `json:"challengeId"` Challenge string `json:"challenge"` DeviceID string `json:"deviceId"` PublicKey string `json:"publicKey"` Signature string `json:"signature"` Timestamp int64 `json:"timestamp"` ClientVersion string `json:"clientVersion"` } func main() { runClient(findProtocolArg(os.Args[1:])) } type authorizeTarget struct { App string ChallengeID string Challenge string APIBase string } func findProtocolArg(args []string) string { for _, arg := range args { if strings.HasPrefix(strings.ToLower(strings.TrimSpace(arg)), "dcauth://") { return strings.TrimSpace(arg) } } return "" } func parseAuthorizeURL(raw string) (authorizeTarget, error) { parsed, err := url.Parse(raw) if err != nil { return authorizeTarget{}, fmt.Errorf("无法解析协议地址") } if !strings.EqualFold(parsed.Scheme, "dcauth") { return authorizeTarget{}, fmt.Errorf("协议 scheme 不是 dcauth") } if !strings.EqualFold(parsed.Host, "authorize") && !strings.EqualFold(strings.Trim(parsed.Path, "/"), "authorize") { return authorizeTarget{}, fmt.Errorf("不支持的协议动作") } query := parsed.Query() appID := strings.TrimSpace(query.Get("app")) challengeID := strings.TrimSpace(query.Get("challenge_id")) challenge := strings.TrimSpace(query.Get("challenge")) apiBase := strings.TrimRight(strings.TrimSpace(query.Get("api")), "/") if appID == "" || challengeID == "" || challenge == "" || apiBase == "" { return authorizeTarget{}, fmt.Errorf("缺少 app、challenge_id、challenge 或 api 参数") } if len(challengeID) > 128 || len(challenge) > 512 { return authorizeTarget{}, fmt.Errorf("challenge 参数过长") } config, ok := appRegistry[appID] if !ok { return authorizeTarget{}, fmt.Errorf("未知应用:%s", appID) } origin, err := normalizeOrigin(apiBase) if err != nil { return authorizeTarget{}, err } if !containsOrigin(config.AllowedAPIs, origin) { return authorizeTarget{}, fmt.Errorf("应用 %s 不允许访问 %s", appID, origin) } return authorizeTarget{ App: appID, ChallengeID: challengeID, Challenge: challenge, APIBase: origin, }, nil } func normalizeOrigin(raw string) (string, error) { parsed, err := url.Parse(raw) if err != nil || parsed.Host == "" { return "", fmt.Errorf("api 地址无效") } if parsed.Scheme != "https" && parsed.Scheme != "http" { return "", fmt.Errorf("api 地址必须使用 http 或 https") } if parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" { return "", fmt.Errorf("api 地址不允许携带用户信息、查询串或片段") } path := strings.TrimRight(parsed.EscapedPath(), "/") if path != "" { return "", fmt.Errorf("api 必须是不带路径的 origin") } return strings.ToLower(parsed.Scheme) + "://" + strings.ToLower(parsed.Host), nil } func containsOrigin(allowed []string, origin string) bool { for _, item := range allowed { if strings.EqualFold(strings.TrimRight(item, "/"), origin) { return true } } return false } func submitAuthorization(id identity, target authorizeTarget) error { timestamp := time.Now().Unix() canonical := buildAuthorizationCanonical(id, target, timestamp) signature := ed25519.Sign(id.privateKey, []byte(canonical)) payload := authorizeRequest{ App: target.App, ChallengeID: target.ChallengeID, Challenge: target.Challenge, DeviceID: id.deviceID, PublicKey: base64.RawURLEncoding.EncodeToString(id.publicKey), Signature: base64.RawURLEncoding.EncodeToString(signature), Timestamp: timestamp, ClientVersion: clientVersion, } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("生成请求失败") } request, err := http.NewRequest(http.MethodPost, target.APIBase+"/api/dcauth/authorize", bytes.NewReader(body)) if err != nil { return fmt.Errorf("生成请求失败") } request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "DCAuth/"+clientVersion) client := &http.Client{Timeout: 15 * time.Second} response, err := client.Do(request) if err != nil { return fmt.Errorf("无法连接服务器:%w", err) } defer response.Body.Close() responseBody, _ := io.ReadAll(io.LimitReader(response.Body, 64*1024)) if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("服务器拒绝本机验证(HTTP %d):%s", response.StatusCode, summarizeResponse(responseBody)) } return nil } func buildAuthorizationCanonical(id identity, target authorizeTarget, timestamp int64) string { return strings.Join([]string{ "dcauth.v2", target.App, target.ChallengeID, target.Challenge, id.deviceID, fmt.Sprintf("%d", timestamp), }, "\n") } func summarizeResponse(body []byte) string { var payload map[string]any if json.Unmarshal(body, &payload) == nil { for _, key := range []string{"msg", "message", "detail", "error"} { if value, ok := payload[key].(string); ok && strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } } text := strings.TrimSpace(string(body)) if len(text) > 200 { text = text[:200] + "..." } if text == "" { return "无响应内容" } return text } func deviceKeyPath() (string, error) { base := strings.TrimSpace(os.Getenv("LOCALAPPDATA")) if base == "" { configDir, err := os.UserConfigDir() if err != nil { return "", fmt.Errorf("无法定位用户配置目录") } base = configDir } dir := filepath.Join(base, "DCAuth") if err := os.MkdirAll(dir, 0o700); err != nil { return "", fmt.Errorf("无法创建设备密钥目录:%w", err) } return filepath.Join(dir, "device.key"), nil } func loadOrCreateIdentity() (identity, error) { keyPath, err := deviceKeyPath() if err != nil { return identity{}, err } var privateKey ed25519.PrivateKey encrypted, readErr := os.ReadFile(keyPath) if readErr == nil { raw, err := unprotectData(encrypted) if err != nil { return identity{}, fmt.Errorf("设备密钥无法解密,请确认当前 Windows 用户未变化") } if len(raw) != ed25519.PrivateKeySize { return identity{}, fmt.Errorf("设备密钥文件损坏,请删除 %s 后重试", keyPath) } privateKey = ed25519.PrivateKey(raw) } else if os.IsNotExist(readErr) { _, generated, err := ed25519.GenerateKey(nil) if err != nil { return identity{}, fmt.Errorf("生成设备密钥失败") } privateKey = generated protected, err := protectData([]byte(privateKey)) if err != nil { return identity{}, fmt.Errorf("保护设备密钥失败:%w", err) } if err := os.WriteFile(keyPath, protected, 0o600); err != nil { return identity{}, fmt.Errorf("保存设备密钥失败:%w", err) } } else { return identity{}, fmt.Errorf("读取设备密钥失败:%w", readErr) } publicKey := privateKey.Public().(ed25519.PublicKey) return identity{ privateKey: privateKey, publicKey: publicKey, deviceID: buildDeviceID(publicKey), }, nil }