#!/usr/bin/env python3
"""FLARE-On 13 #1 FLAreCAPTCHA - static solver.

Usage: python3 -I solve.py [path/to/flarecaptcha.html]

Reproduces the page's flag computation without running the page:
  1. The bitmap array is read through a Proxy that XORs every element with
     document.characterSet.length ("UTF-8" -> 5).
  2. On a right-click (e.button == 5**0 + 1**1 == 2) held for 2000 ms:
       displayPaddingPx = round(elapsed_ms / 1000)            -> 2
       widgetLength     = len(resetWidget.toString())
       key16            = (displayPaddingPx * (widgetLength + 1115)) & 0xFFFF
     even indices are XORed with key16 >> 8, odd indices with key16 & 0xFF.
The result is cross-checked with a known-plaintext attack on the
"@flare-on.com" suffix, which needs no assumptions about timing or source text.
"""
import os
import re
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_HTML = os.path.join(HERE, "loot", "extracted", "flarecaptcha.html")
SUFFIX = "@flare-on.com"


def decode(bitmap, env_shift, key16):
    hi, lo = (key16 >> 8) & 0xFF, key16 & 0xFF
    return "".join(chr((b ^ env_shift) ^ (hi if i % 2 == 0 else lo))
                   for i, b in enumerate(bitmap))


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_HTML
    with open(path, "r", encoding="utf-8", newline="") as f:
        src = f.read()

    bitmap = [int(x) for x in re.search(
        r"recaptchaLogoBitmap\s*=\s*\[([^\]]*)\]", src).group(1).split(",")]

    # Function.prototype.toString() of an arrow function returns its exact
    # source text from "(" up to and including the closing "}".
    m = re.search(r"const resetWidget = (\(\) => \{.*?\n\s*\})\s*;", src, re.S)
    reset_src = m.group(1)
    widget_len = len(reset_src)  # JS length counts UTF-16 units; source is ASCII

    env_shift = len("UTF-8")       # document.characterSet for <meta charset="UTF-8">
    hold_secs = round(2000 / 1000)  # setTimeout(..., 2000)
    key16 = (hold_secs * (widget_len + 1115)) & 0xFFFF

    print(f"[*] bitmap length      : {len(bitmap)}")
    print(f"[*] resetWidget length : {widget_len}")
    print(f"[*] key16              : {key16:#06x}")
    flag = decode(bitmap, env_shift, key16)
    print(f"[+] computed flag      : {flag}")

    # Independent check: recover both key bytes from the known suffix.
    off = len(bitmap) - len(SUFFIX)
    ks = [(bitmap[off + j] ^ env_shift) ^ ord(c) for j, c in enumerate(SUFFIX)]
    even = {k for j, k in enumerate(ks) if (off + j) % 2 == 0}
    odd = {k for j, k in enumerate(ks) if (off + j) % 2 == 1}
    assert len(even) == 1 and len(odd) == 1, "inconsistent known-plaintext key"
    kpa_key = (even.pop() << 8) | odd.pop()
    kpa_flag = decode(bitmap, env_shift, kpa_key)
    print(f"[+] known-plaintext key: {kpa_key:#06x} -> {kpa_flag}")
    assert kpa_flag == flag, "computed key disagrees with known-plaintext key"

    with open(os.path.join(HERE, "flag.txt"), "w") as f:
        f.write(flag + "\n")
    print("[+] wrote flag.txt")


if __name__ == "__main__":
    main()
