package auth import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "log" "math" "math/big" "net/http" "strings" "time" "gpt-plus/pkg/httpclient" ) const ( sentinelReqURL = "https://sentinel.openai.com/backend-api/sentinel/req" maxPowAttempts = 500000 errorPrefix = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" defaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36" ) // SentinelGenerator generates openai-sentinel-token values. type SentinelGenerator struct { DeviceID string SID string // session UUID client *httpclient.Client } // NewSentinelGenerator creates a new SentinelGenerator with a random DeviceID. func NewSentinelGenerator(client *httpclient.Client) *SentinelGenerator { return &SentinelGenerator{ DeviceID: generateUUID(), SID: generateUUID(), client: client, } } // NewSentinelGeneratorWithDeviceID creates a SentinelGenerator reusing an existing DeviceID. // Use this to keep sentinel.DeviceID consistent with oai-device-id in request headers. func NewSentinelGeneratorWithDeviceID(client *httpclient.Client, deviceID string) *SentinelGenerator { return &SentinelGenerator{ DeviceID: deviceID, SID: generateUUID(), client: client, } } // sentinelRequirements is the response from the sentinel /req endpoint. type sentinelRequirements struct { Token string `json:"token"` ProofOfWork struct { Required bool `json:"required"` Seed string `json:"seed"` Difficulty string `json:"difficulty"` } `json:"proofofwork"` } // fnv1a32 computes FNV-1a 32-bit hash with murmurhash3 finalizer mixing, // matching the sentinel SDK's JavaScript implementation. func fnv1a32(text string) string { h := uint32(2166136261) // FNV offset basis for _, c := range []byte(text) { h ^= uint32(c) h *= 16777619 // FNV prime } // xorshift mixing (murmurhash3 finalizer) h ^= h >> 16 h *= 2246822507 h ^= h >> 13 h *= 3266489909 h ^= h >> 16 return fmt.Sprintf("%08x", h) } // buildConfig constructs the 19-element browser environment config array. func (sg *SentinelGenerator) buildConfig() []interface{} { now := time.Now().UTC() dateStr := now.Format("Mon Jan 02 2006 15:04:05 GMT+0000 (Coordinated Universal Time)") navRandom := cryptoRandomFloat() perfNow := 100.5 + cryptoRandomFloat()*50 timeOrigin := float64(now.UnixMilli()) - perfNow config := []interface{}{ "1920x1080", // [0] screen dateStr, // [1] date 4294705152, // [2] jsHeapSizeLimit 0, // [3] nonce placeholder defaultUA, // [4] userAgent "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js", // [5] script src nil, // [6] script version nil, // [7] data build DefaultLanguage, // [8] language 0, // [9] elapsed_ms placeholder navRandom, // [10] random float 0, // [11] nav val 0, // [12] doc key 10, // [13] win key perfNow, // [14] performance.now sg.SID, // [15] session UUID "", // [16] URL params 16, // [17] hardwareConcurrency timeOrigin, // [18] timeOrigin } return config } // base64EncodeConfig JSON-encodes the config array with compact separators, then base64 encodes it. func base64EncodeConfig(config []interface{}) string { jsonBytes, _ := json.Marshal(config) return base64.StdEncoding.EncodeToString(jsonBytes) } // runCheck performs a single PoW check iteration. func (sg *SentinelGenerator) runCheck(startTime time.Time, seed, difficulty string, config []interface{}, nonce int) string { config[3] = nonce config[9] = int(time.Since(startTime).Milliseconds()) data := base64EncodeConfig(config) hashHex := fnv1a32(seed + data) diffLen := len(difficulty) if diffLen > 0 && hashHex[:diffLen] <= difficulty { return data + "~S" } return "" } // solvePoW runs the PoW loop and returns the solution token. func (sg *SentinelGenerator) solvePoW(seed, difficulty string) string { startTime := time.Now() config := sg.buildConfig() for i := 0; i < maxPowAttempts; i++ { result := sg.runCheck(startTime, seed, difficulty, config, i) if result != "" { return "gAAAAAB" + result } } // PoW failed after max attempts, return error token errData := base64EncodeConfig([]interface{}{nil}) return "gAAAAAB" + errorPrefix + errData } // generateRequirementsToken creates a requirements token (no server seed needed). // This is the SDK's getRequirementsToken() equivalent. func (sg *SentinelGenerator) generateRequirementsToken() string { config := sg.buildConfig() config[3] = 1 config[9] = 5 + int(cryptoRandomFloat()*45) // simulate small delay 5-50ms data := base64EncodeConfig(config) return "gAAAAAC" + data // note: prefix is C, not B } // fetchChallenge calls the sentinel backend to get challenge data. // Retries on 403/network errors since the proxy rotates IP every ~30s. func (sg *SentinelGenerator) fetchChallenge(ctx context.Context, client *httpclient.Client, flow string) (*sentinelRequirements, error) { const maxRetries = 5 var lastErr error for attempt := 1; attempt <= maxRetries; attempt++ { result, err := sg.doFetchChallenge(ctx, client, flow) if err == nil { return result, nil } lastErr = err log.Printf("[sentinel] attempt %d/%d failed: %v", attempt, maxRetries, err) if attempt < maxRetries { // Wait for proxy IP rotation, use shorter waits for early attempts wait := time.Duration(3*attempt) * time.Second log.Printf("[sentinel] waiting %v for proxy IP rotation before retry...", wait) select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(wait): } } } return nil, fmt.Errorf("sentinel failed after %d attempts: %w", maxRetries, lastErr) } func (sg *SentinelGenerator) doFetchChallenge(ctx context.Context, client *httpclient.Client, flow string) (*sentinelRequirements, error) { pToken := sg.generateRequirementsToken() reqBody := map[string]string{ "p": pToken, "id": sg.DeviceID, "flow": flow, } bodyBytes, _ := json.Marshal(reqBody) req, err := http.NewRequestWithContext(ctx, http.MethodPost, sentinelReqURL, strings.NewReader(string(bodyBytes))) if err != nil { return nil, fmt.Errorf("create sentinel request: %w", err) } req.Header.Set("Content-Type", "text/plain;charset=UTF-8") req.Header.Set("Origin", "https://sentinel.openai.com") req.Header.Set("Referer", "https://sentinel.openai.com/backend-api/sentinel/frame.html?sv=20260219f9f6") req.Header.Set("sec-ch-ua", `"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"`) req.Header.Set("sec-ch-ua-mobile", "?0") req.Header.Set("sec-ch-ua-platform", `"Windows"`) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("sentinel request: %w", err) } body, err := httpclient.ReadBody(resp) if err != nil { return nil, fmt.Errorf("read sentinel response: %w", err) } if resp.StatusCode != 200 { return nil, fmt.Errorf("sentinel returned %d", resp.StatusCode) } var result sentinelRequirements if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("parse sentinel response: %w", err) } return &result, nil } // GenerateToken builds the full openai-sentinel-token JSON string for a given flow. func (sg *SentinelGenerator) GenerateToken(ctx context.Context, client *httpclient.Client, flow string) (string, error) { if client == nil { client = sg.client } challenge, err := sg.fetchChallenge(ctx, client, flow) if err != nil { return "", fmt.Errorf("fetch sentinel challenge: %w", err) } cValue := challenge.Token var pValue string if challenge.ProofOfWork.Required && challenge.ProofOfWork.Seed != "" { pValue = sg.solvePoW(challenge.ProofOfWork.Seed, challenge.ProofOfWork.Difficulty) } else { pValue = sg.generateRequirementsToken() } token := map[string]string{ "p": pValue, "t": "", "c": cValue, "id": sg.DeviceID, "flow": flow, } tokenBytes, _ := json.Marshal(token) return string(tokenBytes), nil } // cryptoRandomFloat returns a cryptographically random float64 in [0, 1). func cryptoRandomFloat() float64 { n, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) return float64(n.Int64()) / float64(math.MaxInt64) }