#!/usr/bin/env python3
"""TP-Link AX20 login — reverse of tpEncrypt.js"""
import hashlib, random, string, json, ssl, urllib.request, urllib.parse, sys

ROUTER = "https://192.168.1.1"
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE

def http(method, path, body=None, headers={}):
    req = urllib.request.Request(ROUTER+path, data=body, headers=headers, method=method)
    req.add_header("Referer", ROUTER+"/webpages/index.html")
    try:
        with urllib.request.urlopen(req, context=ctx, timeout=8) as r:
            return r.status, r.read()
    except Exception as e:
        return 0, str(e).encode()

def rsa_encrypt_nopad(n_hex, e_hex, plaintext):
    n = int(n_hex, 16); e = int(e_hex, 16)
    m = int.from_bytes(plaintext.encode(), 'big')
    c = pow(m, e, n)
    return format(c, 'x').zfill(len(n_hex))

def rsa_encrypt_chunks(n_hex, e_hex, text, chunk=53):
    out = ""
    for i in range(0, len(text), chunk):
        out += rsa_encrypt_nopad(n_hex, e_hex, text[i:i+chunk])
    return out

def gen_aes_key():
    d = string.digits
    return ''.join(random.choice(d) for _ in range(16))

def aes_encrypt(key_str, iv_str, data):
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    from cryptography.hazmat.primitives import padding
    k = key_str.encode(); iv = iv_str.encode()
    padder = padding.PKCS7(128).padder()
    padded = padder.update(data.encode()) + padder.finalize()
    c = Cipher(algorithms.AES(k), modes.CBC(iv))
    enc = c.encryptor()
    ct = enc.update(padded) + enc.finalize()
    import base64; return base64.b64encode(ct).decode()

# Step 1: GET form=auth for RSA key + seq
print("[*] Fetching RSA key via form=auth...")
body = b"form=auth&operation=read"
status, data = http("POST", "/cgi-bin/luci", body=body,
    headers={"Content-Type":"application/x-www-form-urlencoded"})
print(f"  status={status} data={data[:300]}")

n, e, seq = "", "", "0"
if status == 200 and data:
    try:
        j = json.loads(data)
        rsa = j.get("data",{}).get("rsa_key",{}) or j.get("rsa_key",{})
        n = rsa.get("n",""); e = rsa.get("e",""); seq = str(j.get("data",{}).get("seq",0) or j.get("seq",0))
    except: pass

if not n:
    print("[!] form=auth failed, trying JSON RPC for seq...")
    status2, data2 = http("POST", "/cgi-bin/luci/;stok=/login",
        body=b'{"method":"do","login":{}}',
        headers={"Content-Type":"application/json"})
    print(f"  JSON RPC: {status2} {data2[:200]}")
    # Try to get RSA key from frame.js stored vars or hardcode from previous session
    # From RESUME: n=D1E79FF... was found before
    n = input("Enter RSA n (hex): ").strip() if sys.stdin.isatty() else ""
    e = "010001"
    seq = "0"

if not n:
    print("[!] No RSA key. Cannot login.")
    sys.exit(1)

print(f"[+] RSA n={n[:16]}... e={e} seq={seq}")

# Step 2: AES key
aes_key = gen_aes_key(); aes_iv = gen_aes_key()
aes_key_str = f"k={aes_key}&i={aes_iv}"
print(f"[*] AES: {aes_key_str}")

# Step 3: hash = MD5(username + password)
for pw in ["99157425", "admin", "44935153", ""]:
    username = "admin"
    md5_hash = hashlib.md5((username + pw).encode()).hexdigest()
    print(f"\n[*] Trying pw={pw!r} hash={md5_hash[:8]}...")

    # Step 4: data = AES(login JSON payload)
    login_json = json.dumps({"method":"do","login":{"username":username,"password":pw}})
    try:
        data_enc = aes_encrypt(aes_key, aes_iv, login_json)
    except Exception as ex:
        print(f"  AES failed: {ex} — trying plaintext")
        data_enc = login_json

    # Step 5: sign = RSA(aesKeyStr + "&h=" + hash + "&s=" + (seq + len(data_enc)))
    seq_len = str(int(seq) + len(data_enc))
    sign_plain = f"{aes_key_str}&h={md5_hash}&s={seq_len}"
    sign = rsa_encrypt_chunks(n, e, sign_plain)
    print(f"  sign_plain={sign_plain[:40]}... sign={sign[:16]}...")

    # Step 6: POST {sign, data}
    payload = json.dumps({"sign":sign,"data":data_enc}).encode()
    status3, resp3 = http("POST", "/cgi-bin/luci/;stok=/login",
        body=payload, headers={"Content-Type":"application/json"})
    print(f"  RESP [{status3}]: {resp3[:300]}")

    try:
        j3 = json.loads(resp3)
        stok = j3.get("stok") or j3.get("data",{}).get("stok","")
        if stok:
            print(f"\n[!!!] STOK: {stok}")
            break
    except: pass

print("\n[*] Done")
