sentinel_pow.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. """Sentinel proof-of-work helpers for OpenAI auth flows."""
  2. from __future__ import annotations
  3. import base64
  4. import json
  5. import random
  6. import time
  7. import uuid
  8. from .constants import OPENAI_USER_AGENT
  9. class SentinelTokenGenerator:
  10. MAX_ATTEMPTS = 500000
  11. ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D"
  12. def __init__(self, *, device_id: str | None = None, user_agent: str | None = None) -> None:
  13. self.device_id = device_id or str(uuid.uuid4())
  14. self.user_agent = user_agent or OPENAI_USER_AGENT
  15. self.requirements_seed = str(random.random())
  16. self.sid = str(uuid.uuid4())
  17. @staticmethod
  18. def _fnv1a_32(text: str) -> str:
  19. h = 2166136261
  20. for ch in text:
  21. h ^= ord(ch)
  22. h = (h * 16777619) & 0xFFFFFFFF
  23. h ^= h >> 16
  24. h = (h * 2246822507) & 0xFFFFFFFF
  25. h ^= h >> 13
  26. h = (h * 3266489909) & 0xFFFFFFFF
  27. h ^= h >> 16
  28. return format(h & 0xFFFFFFFF, "08x")
  29. def _get_config(self) -> list[object]:
  30. now_str = time.strftime(
  31. "%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)",
  32. time.gmtime(),
  33. )
  34. perf_now = random.uniform(1000, 50000)
  35. time_origin = time.time() * 1000 - perf_now
  36. nav_prop = random.choice(
  37. [
  38. "vendorSub",
  39. "productSub",
  40. "vendor",
  41. "maxTouchPoints",
  42. "scheduling",
  43. "userActivation",
  44. "doNotTrack",
  45. "geolocation",
  46. "connection",
  47. "plugins",
  48. "mimeTypes",
  49. "pdfViewerEnabled",
  50. "webkitTemporaryStorage",
  51. "webkitPersistentStorage",
  52. "hardwareConcurrency",
  53. "cookieEnabled",
  54. "credentials",
  55. "mediaDevices",
  56. "permissions",
  57. "locks",
  58. "ink",
  59. ]
  60. )
  61. nav_val = f"{nav_prop}-undefined"
  62. return [
  63. "1920x1080",
  64. now_str,
  65. 4294705152,
  66. random.random(),
  67. self.user_agent,
  68. "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js",
  69. None,
  70. None,
  71. "en-US",
  72. "en-US,en",
  73. random.random(),
  74. nav_val,
  75. random.choice(["location", "implementation", "URL", "documentURI", "compatMode"]),
  76. random.choice(["Object", "Function", "Array", "Number", "parseFloat", "undefined"]),
  77. perf_now,
  78. self.sid,
  79. "",
  80. random.choice([4, 8, 12, 16]),
  81. time_origin,
  82. ]
  83. @staticmethod
  84. def _base64_encode(data: object) -> str:
  85. raw = json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
  86. return base64.b64encode(raw).decode("ascii")
  87. def _run_check(
  88. self,
  89. *,
  90. start_time: float,
  91. seed: str,
  92. difficulty: str,
  93. config: list[object],
  94. nonce: int,
  95. ) -> str | None:
  96. config[3] = nonce
  97. config[9] = round((time.time() - start_time) * 1000)
  98. data = self._base64_encode(config)
  99. hash_hex = self._fnv1a_32(seed + data)
  100. diff_len = len(difficulty)
  101. if hash_hex[:diff_len] <= difficulty:
  102. return data + "~S"
  103. return None
  104. def generate_token(self, *, seed: str | None = None, difficulty: str | None = None) -> str:
  105. start_time = time.time()
  106. config = self._get_config()
  107. resolved_seed = seed if seed is not None else self.requirements_seed
  108. resolved_difficulty = str(difficulty or "0")
  109. for nonce in range(self.MAX_ATTEMPTS):
  110. result = self._run_check(
  111. start_time=start_time,
  112. seed=resolved_seed,
  113. difficulty=resolved_difficulty,
  114. config=config,
  115. nonce=nonce,
  116. )
  117. if result:
  118. return "gAAAAAB" + result
  119. return "gAAAAAB" + self.ERROR_PREFIX + self._base64_encode(str(None))
  120. def generate_requirements_token(self) -> str:
  121. config = self._get_config()
  122. config[3] = 1
  123. config[9] = round(random.uniform(5, 50))
  124. return "gAAAAAC" + self._base64_encode(config)