mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 05:07:40 +02:00
231 lines
8.3 KiB
Python
231 lines
8.3 KiB
Python
"""Read the user's own logged-in provider cookies from their real browser, so
|
|
onboarding can harvest their actual chat history at first run without an in-app login.
|
|
|
|
macOS + Chromium only for now (Chrome/Arc/Brave/Edge). We first find WHICH store holds
|
|
the session by counting cookie names in the SQLite (no decryption, no keychain), then
|
|
decrypt only that one store, so the "Safe Storage" keychain is touched at most once per
|
|
browser (cached for the process). Values are v10/v11 AES-CBC. Fails open to {} on
|
|
anything (no browser, app-bound v20 cookies, denied keychain, Safari-only user), so
|
|
prep just falls back to the local scan.
|
|
|
|
Only ever reads the specific provider domain asked for; never a general cookie sweep.
|
|
The values are session secrets: used in-process for the harvest, never logged or stored.
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import sqlite3
|
|
import subprocess
|
|
import tempfile
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from typeguard import typechecked
|
|
|
|
CHROMIUM_ROOTS = {
|
|
"Chrome": "Library/Application Support/Google/Chrome",
|
|
"Arc": "Library/Application Support/Arc/User Data",
|
|
"Brave": "Library/Application Support/BraveSoftware/Brave-Browser",
|
|
"Edge": "Library/Application Support/Microsoft Edge",
|
|
}
|
|
KEYCHAIN_SERVICE = {
|
|
"Chrome": "Chrome Safe Storage",
|
|
"Arc": "Arc Safe Storage",
|
|
"Brave": "Brave Safe Storage",
|
|
"Edge": "Microsoft Edge Safe Storage",
|
|
}
|
|
PROFILES = ["Default"] + [f"Profile {i}" for i in range(1, 12)]
|
|
|
|
# One keychain read per browser per process; "Always Allow" then never re-prompts.
|
|
p_key_cache: Dict[str, Optional[bytes]] = {}
|
|
|
|
|
|
@typechecked
|
|
def p_safe_storage_key(browser: str) -> Optional[bytes]:
|
|
if browser in p_key_cache:
|
|
return p_key_cache[browser]
|
|
key: Optional[bytes] = None
|
|
try:
|
|
r = subprocess.run(
|
|
["security", "find-generic-password", "-w", "-s", KEYCHAIN_SERVICE[browser]],
|
|
capture_output=True, text=True, timeout=20,
|
|
)
|
|
pw = r.stdout.strip()
|
|
if pw:
|
|
key = hashlib.pbkdf2_hmac("sha1", pw.encode(), b"saltysalt", 1003, 16)
|
|
except Exception:
|
|
key = None
|
|
p_key_cache[browser] = key
|
|
return key
|
|
|
|
|
|
@typechecked
|
|
def p_count_domain(db_path: str, domain: str) -> int:
|
|
tmp = tempfile.mktemp()
|
|
try:
|
|
shutil.copy2(db_path, tmp)
|
|
con = sqlite3.connect(f"file:{tmp}?mode=ro", uri=True)
|
|
cur = con.cursor()
|
|
cur.execute("SELECT count(*) FROM cookies WHERE host_key LIKE ?", (f"%{domain}",))
|
|
n = int(cur.fetchone()[0])
|
|
con.close()
|
|
return n
|
|
except Exception:
|
|
return 0
|
|
finally:
|
|
try:
|
|
os.remove(tmp)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
@typechecked
|
|
def p_best_store(domain: str) -> Optional[Tuple[str, str]]:
|
|
"""The (browser, db_path) holding the most cookies for `domain`, found WITHOUT the keychain."""
|
|
home = os.path.expanduser("~")
|
|
best: Optional[Tuple[str, str]] = None
|
|
best_score = (0, -1.0)
|
|
for browser, rel in CHROMIUM_ROOTS.items():
|
|
base = os.path.join(home, rel)
|
|
if not os.path.isdir(base):
|
|
continue
|
|
for prof in PROFILES:
|
|
for sub in ("Cookies", "Network/Cookies"):
|
|
path = os.path.join(base, prof, sub)
|
|
if not os.path.isfile(path):
|
|
continue
|
|
n = p_count_domain(path, domain)
|
|
if n:
|
|
score = (n, os.path.getmtime(path))
|
|
if score > best_score:
|
|
best, best_score = (browser, path), score
|
|
return best
|
|
|
|
|
|
@typechecked
|
|
def p_decrypt(enc: bytes, key: bytes) -> Optional[str]:
|
|
if enc[:3] not in (b"v10", b"v11"):
|
|
return None # v20 = app-bound encryption, out of reach without the browser
|
|
try:
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
c = Cipher(algorithms.AES(key), modes.CBC(b" " * 16), backend=default_backend())
|
|
d = c.decryptor()
|
|
dec = d.update(enc[3:]) + d.finalize()
|
|
dec = dec[: -dec[-1]] # strip PKCS7 padding
|
|
for cut in (0, 32): # newer Chromium prepends a 32-byte domain hash
|
|
try:
|
|
return dec[cut:].decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
continue
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
@typechecked
|
|
def read_provider_cookies(domain: str) -> Dict[str, str]:
|
|
"""Decrypted cookie jar for `domain`, from whichever browser store actually has the session. At most one keychain touch (that store's browser), cached for the process."""
|
|
store = p_best_store(domain)
|
|
if store is None:
|
|
return {}
|
|
browser, db_path = store
|
|
key = p_safe_storage_key(browser)
|
|
if key is None:
|
|
return {}
|
|
jar: Dict[str, str] = {}
|
|
tmp = tempfile.mktemp()
|
|
try:
|
|
shutil.copy2(db_path, tmp)
|
|
con = sqlite3.connect(f"file:{tmp}?mode=ro", uri=True)
|
|
cur = con.cursor()
|
|
cur.execute("SELECT name, encrypted_value FROM cookies WHERE host_key LIKE ?", (f"%{domain}",))
|
|
for name, enc in cur.fetchall():
|
|
if not enc:
|
|
continue
|
|
val = p_decrypt(bytes(enc), key)
|
|
if val:
|
|
jar[str(name)] = val
|
|
con.close()
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
try:
|
|
os.remove(tmp)
|
|
except OSError:
|
|
pass
|
|
return jar
|
|
|
|
|
|
@typechecked
|
|
def read_provider_cookie_records(domain: str) -> List[Dict[str, Any]]:
|
|
"""Full cookie records ({name,value,domain,path,secure,httponly}) for `domain`, so Electron's offscreen browser can re-inject the session faithfully and pass Cloudflare with a real Chrome TLS handshake. Same one-store, one-keychain-touch path as read_provider_cookies."""
|
|
store = p_best_store(domain)
|
|
if store is None:
|
|
return []
|
|
browser, db_path = store
|
|
key = p_safe_storage_key(browser)
|
|
if key is None:
|
|
return []
|
|
records: List[Dict[str, Any]] = []
|
|
tmp = tempfile.mktemp()
|
|
try:
|
|
shutil.copy2(db_path, tmp)
|
|
con = sqlite3.connect(f"file:{tmp}?mode=ro", uri=True)
|
|
cur = con.cursor()
|
|
cur.execute(
|
|
"SELECT name, encrypted_value, host_key, path, is_secure, is_httponly FROM cookies WHERE host_key LIKE ?",
|
|
(f"%{domain}",),
|
|
)
|
|
for name, enc, host_key, path, is_secure, is_httponly in cur.fetchall():
|
|
if not enc:
|
|
continue
|
|
val = p_decrypt(bytes(enc), key)
|
|
if val is None:
|
|
continue
|
|
records.append({
|
|
"name": str(name), "value": val, "domain": str(host_key),
|
|
"path": str(path) or "/", "secure": bool(is_secure), "httponly": bool(is_httponly),
|
|
})
|
|
con.close()
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
try:
|
|
os.remove(tmp)
|
|
except OSError:
|
|
pass
|
|
return records
|
|
|
|
|
|
# Gemini authenticates on the parent .google.com SSO domain, not gemini.google.com, so its
|
|
# session lives in these named cookies. We read ONLY these (never the whole google cookie
|
|
# jar) and only to load a Gemini page offscreen, keeping the ChatGPT/Claude trust frame.
|
|
GOOGLE_AUTH_COOKIE_NAMES = {
|
|
"SID", "HSID", "SSID", "APISID", "SAPISID", "SIDCC", "NID",
|
|
"__Secure-1PSID", "__Secure-3PSID", "__Secure-1PSIDTS", "__Secure-3PSIDTS",
|
|
"__Secure-1PSIDCC", "__Secure-3PSIDCC", "__Secure-1PAPISID", "__Secure-3PAPISID",
|
|
}
|
|
|
|
|
|
@typechecked
|
|
def read_google_session_records() -> List[Dict[str, Any]]:
|
|
"""The named Google SSO cookies from .google.com, so the offscreen browser can load Gemini logged in. Scoped to the auth set by name, never a general google-cookie sweep."""
|
|
return [r for r in read_provider_cookie_records(".google.com") if r.get("name") in GOOGLE_AUTH_COOKIE_NAMES]
|
|
|
|
|
|
@typechecked
|
|
def cookie_header(jar: Dict[str, str]) -> str:
|
|
return "; ".join(f"{k}={v}" for k, v in jar.items())
|
|
|
|
|
|
@typechecked
|
|
def logged_in_providers() -> List[str]:
|
|
"""Which providers have a readable session, WITHOUT decrypting or touching the keychain: safe for a UI presence check."""
|
|
out: List[str] = []
|
|
for provider, domain in (("codex", "chatgpt.com"), ("claude", "claude.ai"), ("gemini", "gemini.google.com")):
|
|
if p_best_store(domain) is not None:
|
|
out.append(provider)
|
|
return out
|