aurora / internal /prooftoken /prooftoken.go
qwen2api's picture
Upload 113 files
6bc074c verified
Raw
History Blame Contribute Delete
11.2 kB
package prooftoken
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
mathrand "math/rand"
"strconv"
"time"
"aurora/internal/browserfp"
"aurora/internal/fingerprint"
"aurora/util"
)
// mathRandNew 是 math/rand.New 的本地别名 (避免类型混淆)。
type mathRand = mathrand.Rand
func mathRandNew(seed int64) *mathRand {
return mathrand.New(mathrand.NewSource(seed))
}
const (
// PrefixRequirements RequirementsToken 前缀
PrefixRequirements = "gAAAAAC"
// PrefixProof ProofToken 前缀
PrefixProof = "gAAAAAB"
// Suffix 附加在 base64(config) 后面的分隔符
Suffix = "~S"
// ErrorPrefix PoW 失败时的占位指纹前缀 (对齐 sdk.deob.pretty.js:292 /
// 4813494d-lryin3horwb01cb5.js class constructor)。
// 最终 token 格式: PrefixProof + ErrorPrefix + base64(JSON.stringify("e" or error)) + Suffix
ErrorPrefix = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D"
// DefaultErrorPayload 失败 fallback 默认 base64 内容 (= base64(JSON.stringify("e")) = "ImUi")。
// 对齐 buildGenerateFailMessage(e) 里 String(e ?? "e") → NM("e")。
DefaultErrorPayload = "ImUi"
)
// DefaultFlow 是 sentinel prepare/finalize 流程标识。
const DefaultFlow = "chatgpt"
// fingerprintSize 25 元素 config, 对齐新版 SDK 算法 (conversation.txt 2026-06 样本)。
const fingerprintSize = 25
// windowKeys 候选 [13] (Object.getOwnPropertyNames(window) 随机键)
var windowKeys = []string{
"requestIdleCallback", "webkitRequestAnimationFrame", "onfocus", "onblur",
}
// reactLetters 是 [12] 随机 suffix 字符表
var reactLetters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
// Config 持有 p 字段生成所需的全部上下文 (对齐新版 BrowserSession 字段)。
type Config struct {
DeviceID string
UserAgent string
Language string
Languages string
// Timezone IANA 时区名(如 "America/Los_Angeles"),传给
// fingerprint.Options.Timezone。如果为空,fingerprint 用 Go 本地时区。
Timezone string
ScreenWidth int
ScreenHeight int
HardwareConcurrency int
SentinelSV string // SDK 版本, e.g. "20260423af3c"
BuildID string // 来自 chatgpt.com 页面的 data-build
// 可选:固定 Math.random (用于测试)
FixedRandom *float64
}
// NewConfig 构造默认配置。指纹随机化由 fingerprint.Build25 内部完成。
// userAgent 为空时使用 util.FixedUserAgent。
func NewConfig(userAgent string) *Config {
if userAgent == "" {
userAgent = util.FixedUserAgent
}
fp := browserfp.Get()
return &Config{
DeviceID: randomUUID(),
UserAgent: userAgent,
Language: fp.Language,
Languages: browserfp.LanguageJoin(fp.Language),
Timezone: "America/Los_Angeles",
ScreenWidth: fp.ScreenWidth,
ScreenHeight: fp.ScreenHeight,
HardwareConcurrency: fp.HardwareConcurrency,
SentinelSV: "20260423af3c",
BuildID: fp.BuildID,
}
}
// randomUUID 生成 v4 UUID (与 crypto/rand 区分)。
func randomUUID() string {
var b [16]byte
_, _ = rand.Read(b[:])
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// EncodeConfig 把 config 数组编码为 base64 字符串 (对齐 base64.b64encode(json_str.encode('utf-8')))。
func EncodeConfig(config []any) string {
b, err := json.Marshal(config)
if err != nil {
return ""
}
return base64.StdEncoding.EncodeToString(b)
}
// EncodeString 把任意字符串 JSON 序列化后 base64 编码(对齐 SDK 的
// NM(String(e)) 行为:先把字符串当 JSON 值序列化,再 btoa + UTF-8)。
// 用在 PoW 失败 fallback 拼 base64(error)。
func EncodeString(s string) string {
b, err := json.Marshal(s)
if err != nil {
return ""
}
return base64.StdEncoding.EncodeToString(b)
}
// FNV1aHash FNV-1a 32 位哈希, 返回 8 位 hex (小写)。
// 对齐 Python fnv1a_hash + _imul。
func FNV1aHash(text string) string {
const (
fnvOffset = 2166136261
fnvPrime = 16777619
)
h := uint32(fnvOffset)
for _, ch := range text {
h ^= uint32(ch)
h = imul32(h, fnvPrime)
}
h ^= h >> 16
h = imul32(h, 2246822507)
h ^= h >> 13
h = imul32(h, 3266489909)
h ^= h >> 16
return fmt.Sprintf("%08x", h)
}
// imul32 模拟 JavaScript Math.imul (32 位整数乘法)。
func imul32(a, b uint32) uint32 {
return (a * b) & 0xFFFFFFFF
}
// rngFloat 返回 Math.random() (若 FixedRandom 非空则返回 fixed 值)。
func (c *Config) rngFloat(rng *mathRand) float64 {
if c.FixedRandom != nil {
return *c.FixedRandom
}
return rng.Float64()
}
// envFlags 返回 [19-22] 的环境探测值 (Number("X" in window))。
// 当前 aurora 不是真浏览器,默认全部 0;后续如需模拟,可在 Config 里加字段。
func (c *Config) envFlags() [4]int {
return [4]int{0, 0, 0, 0}
}
// buildConfig 构造 25 元素 fingerprint config (覆盖 [3]/[9]/[14])。
//
// 走 internal/fingerprint.Build25 拿到真实浏览器形态(对齐 2026-06-24 chatgpt.com 抓包),
// 然后覆盖 PoW 的 [3] nonce、[9] elapsed, 以及 [14] device_id。
func (c *Config) buildConfig(rng *mathRand, attempt *int, elapsedMs *int64) []any {
// 把 c 的字段映射成 fingerprint.Options;rng 注入保持 deterministic
opts := fingerprint.Options{
UserAgent: c.UserAgent,
Platform: "Win32",
ScreenWidth: c.ScreenWidth,
ScreenHeight: c.ScreenHeight,
HardwareConcurrency: c.HardwareConcurrency,
JSHeapSizeLimit: 4294967296,
BuildID: c.BuildID,
Timezone: c.Timezone,
Rand: rng,
}
// 如果 Languages 是 "en-US,en" 字符串形式,拆成 []string
if c.Languages != "" {
opts.Languages = splitLangList(c.Languages)
} else {
opts.Languages = []string{c.Language, "en"}
}
config := fingerprint.Build25(opts)
// [3] / [9] 覆盖:PoW 阶段用 nonce(int) 和 elapsedMs(int64)
if attempt != nil {
config[3] = *attempt
} else {
// requirements 阶段:对齐 sdk.deob.pretty.js:413 `n[3] = 1`(固定 int 1,
// 不是 Math.random)。requirements token 不跑 PoW,只是固定一个
// 标记让服务端验设备形态。
config[3] = 1
}
if elapsedMs != nil {
config[9] = *elapsedMs
} else {
// requirements 阶段:[9] 是 performance.now() - t0 的一次性测时;
// 这里不传 elapsedMs 时沿用 fingerprint 的 Math.random()(float),
// 跟 SDK 行为一致(SDK 也不传)。
config[9] = c.rngFloat(rng)
}
// [14] device_id — 浏览器 SDK 用 localStorage 中存储的 device_id,
// 必须是非空 UUID, 否则服务器识别为空设备 → mini 池。
if c.DeviceID != "" {
config[14] = c.DeviceID
}
return config
}
// splitLangList 把 "en-US,en" 拆成 []string。
func splitLangList(s string) []string {
out := []string{}
cur := ""
for _, r := range s {
if r == ',' {
if cur != "" {
out = append(out, cur)
cur = ""
}
continue
}
cur += string(r)
}
if cur != "" {
out = append(out, cur)
}
return out
}
// GenerateRequirementsToken 生成首次 sentinel/req 的 p 字段值 (gAAAAAC 前缀)。
//
// 对齐 sdk.deob.pretty.js:407-418 _generateRequirementsTokenAnswerBlocking:
// 1. 拿 fingerprint config(本包 buildConfig)
// 2. [3] = 1 (固定)
// 3. [9] = performance.now() - t0 (一次性测时,不循环)
// 4. base64(JSON.stringify(config)) → 返回
// 5. 失败 → errorPrefix + base64(error) (errorPrefix = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D")
//
// 注意:requirements token **不跑 PoW**(proof token 才跑);这里只是
// 一次性拼一份带时间戳的 config 让服务端验设备形态。
func (c *Config) GenerateRequirementsToken() string {
rng := mathRandNew(time.Now().UnixNano())
// 对齐浏览器: requirements 阶段 [9] = Math.random(), 不是 elapsed
// (elapsed 只在 PoW 迭代阶段使用)
config := c.buildConfig(rng, nil, nil)
config[3] = 1
encoded := EncodeConfig(config)
return PrefixRequirements + encoded + Suffix
}
// SolveProofOfWork 按服务端挑战求解 proof token (gAAAAAB 前缀 + FNV-1a 哈希)。
// [13]/[17] 时间自洽由 fingerprint.Build25 内部保证,这里不用再覆盖。
//
// 失败 fallback 格式(对齐 sdk.deob.pretty.js:329 + buildGenerateFailMessage:364-366):
//
// "gAAAAAB" + ErrorPrefix + base64(JSON.stringify("e" or error)) + "~S"
//
// 500k 次未命中时 err=nil → 用 DefaultErrorPayload ("ImUi" = base64('"e"'))。
func (c *Config) SolveProofOfWork(seed, difficulty string) string {
if seed == "" || difficulty == "" {
return PrefixProof + Suffix
}
startTime := time.Now()
rng := mathRandNew(time.Now().UnixNano())
diffLen := len(difficulty)
const maxIter = 500_000
for i := 0; i < maxIter; i++ {
nonce := i
elapsed := time.Since(startTime).Milliseconds()
config := c.buildConfig(rng, &nonce, &elapsed)
encoded := EncodeConfig(config)
hashInput := seed + encoded
hashResult := FNV1aHash(hashInput)
if hashResult[:diffLen] <= difficulty {
return PrefixProof + encoded + Suffix
}
}
// 达到最大次数仍未找到,返回 fallback (error=nil → 默认 "e")
return PrefixProof + ErrorPrefix + DefaultErrorPayload + Suffix
}
// BuildFailToken 构造 PoW 失败 fallback token。
// errMessage 为空时使用 SDK 默认 "e"。
func BuildFailToken(errMessage string) string {
if errMessage == "" {
errMessage = "e"
}
return PrefixProof + ErrorPrefix + EncodeString(errMessage) + Suffix
}
// String 辅助:float64 → string (避免重复写 strconv.FormatFloat)。
func String(v float64) string {
return strconv.FormatFloat(v, 'f', -1, 64)
}
// BackwardCompat 旧 API 兼容 (client-go 旧版代码依赖这些)。
// (c *Config).RequirementsToken 兼容 client-go 旧名 (返回 gAAAAAC + base64 + ~S)。
func (c *Config) RequirementsToken() string {
return c.GenerateRequirementsToken()
}
// SolveProofToken 兼容 client-go 旧名 (接受 userAgent 参数)。
func SolveProofToken(seed, difficulty, userAgent string) string {
c := NewConfig(userAgent)
return c.SolveProofOfWork(seed, difficulty)
}
// BuildSentinelRequestBody 构造 sentinel/req 请求体 (JSON 字符串)。
func BuildSentinelRequestBody(p, deviceID, flow string) string {
if flow == "" {
flow = DefaultFlow
}
body := map[string]string{"p": p, "id": deviceID, "flow": flow}
b, _ := json.Marshal(body)
return string(b)
}
// SentinelTokenHeader 是 f/conversation 头里 openai-sentinel-token 的值 (JSON)。
// 字段含义:
//
// p: prepare 阶段用的 p (config 编码)
// t: turnstile token (无则空)
// c: sentinel token (服务端返回)
// id: deviceID
// flow: 流程标识
type SentinelTokenHeader struct {
P string `json:"p"`
T string `json:"t"`
C string `json:"c"`
ID string `json:"id"`
Flow string `json:"flow"`
}
// BuildSentinelTokenHeader 构造 openai-sentinel-token 请求头值 (JSON 字符串)。
func BuildSentinelTokenHeader(p, turnstileToken, sentinelToken, deviceID, flow string) string {
if flow == "" {
flow = DefaultFlow
}
h := SentinelTokenHeader{P: p, T: turnstileToken, C: sentinelToken, ID: deviceID, Flow: flow}
b, _ := json.Marshal(h)
return string(b)
}