Skill: asaotomo/zipcracker (version 2.0.1)

--- name: zipcracker description: CTF-oriented ZIP cracking and recovery with the bundled ZipCracker engine. Use when Codex or OpenClaw needs to analyze or recover an encrypted ZIP in authorized contexts, including pseudo-encryption repair, default dictionary attacks, custom wordlists, mask attacks, short-plaintext CRC32 recovery, known-plaintext attacks, bkcrack workflows, template KPA, WinZip AES triage, or large-dictionary handling. Trigger on requests mentioning zip password, encrypted zip, ZIP challenge, 压缩包破解, ZIP 爆破, 伪加密, 掩码, 四位数字密码, 字典跑一下, 已知明文, 明文攻击, bkcrack, CRC32, AES ZIP, 看起来像 png/exe/pcapng/zip 模板, 这个压缩包打不开, or ClawHub/OpenClaw ZIP solving. --- # ZipCracker Use this skill as a self-contained ZIP cracking package. Always prefer the bundled wrapper in `scripts/openclaw_zipcracker.py` over assuming the original repository still exists somewhere else. Only use it for CTF, self-owned archives, or authorized security work. If the request sounds like unauthorized access to third-party data, refuse. ## Quick Start 1. Collect the minimum inputs before running anything: - Target ZIP path. - Whether the user already has a dictionary, a password pattern, a known plaintext file, a passwordless reference ZIP, or only a file signature guess. - Whether the user wants the original ZIP password itself, or only wants extraction/recovery. - Whether the archive is clearly ZIP-specific; do not force this skill onto `rar` or `7z`. 2. In ambiguous cases, inspect first: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip> ``` Use the profile mode to surface pseudo-encryption, AES vs ZipCrypto mix, short-plaintext candidates, template KPA candidates, and recommended next commands. 3. Run the bundled wrapper: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> ... ``` 4. Prefer the wrapper flags over ad-hoc environment variables: - `--auto-crc` for short-plaintext CRC32 prompts. - `--auto-template-kpa` to let the bundled engine follow up on template-KPA suggestions automatically. - `--auto-large-mask` only when the user explicitly accepts a very large mask search. - `--skip-dict-count` for huge wordlists. - `--skip-orig-password-recovery` when the user only cares about extraction speed after a `bkcrack`-based recovery. - `--allow-install-prompts` only when the user explicitly wants interactive dependency installation attempts. 5. Keep the current working directory as the project directory that contains the target ZIP. The bundled engine resolves its own built-in dictionary relative to the skill, so custom relative paths for the target, plaintext, or dictionary still behave naturally. ## Decision Tree ### 1. Start with the least-assumption path When the user only says "crack this ZIP" or "analyze this archive", inspect first, then begin with the default flow: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip> ``` Then: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py --auto-template-kpa <zip> ``` This preserves the original ZipCracker mindset: - Try pseudo-encryption repair before brute force. - Warn about AES and missing `pyzipper`. - Use the built-in dictionary first. - Fall back to the generated 1-6 digit numeric dictionary. - Offer template-based KPA when the archive structure strongly suggests it. Add `--auto-crc` only when short-plaintext recovery is likely relevant or when the user explicitly asks to try CRC32-style recovery. ### 2. Choose the main attack based on the best clue - If the user has a custom dictionary file or dictionary directory: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> <dict-or-dir> ``` - If the user knows the password shape, use a mask: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -m '?u?l?l?l?d?d' ``` - If the user has a full known plaintext file or a passwordless reference ZIP, use `-kpa`: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <plain-file-or-zip> ``` - If the known plaintext is partial, add offset and extra known bytes: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <part.bin> --kpa-offset 78 -x 0 4d5a ``` - If the user only knows the file type or magic header, use a built-in template: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> --kpa-template png -c image.png ``` - If the user wants pure `bkcrack` recovery and does not want fallback methods: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <plain-file> --bkcrack ``` ### 3. Prefer the strongest clue instead of stacking random tactics - Full known plaintext beats mask guessing. - Partial known plaintext plus `bkcrack` usually beats blind dictionary work when at least some bytes are reliable. - A realistic custom wordlist beats the built-in defaults. - A tight mask beats a giant generic wordlist. - Template KPA is worth trying when the encrypted member looks like `png`, `zip`, `exe`, or `pcapng`. ## Solving Heuristics ### Pseudo-encryption first Do not jump directly into brute force when the request is vague. The bundled engine already attempts pseudo-encryption repair by clearing the encryption bit and validating extraction. Keep that behavior because many CTF ZIP tasks are fake-encrypted rather than truly protected. ### Short plaintext CRC32 recovery Use CRC32 recovery only for entries whose plaintext size is 1 to 6 bytes. This is not a generic password attack; it is a content recovery trick for tiny stored plaintexts. In OpenClaw, opt in with `--auto-crc` when the challenge obviously contains tiny files or the user asks to try CRC-based recovery. ### KPA matching rules When using `-kpa`, the engine reproduces the original matching strategy: - Prefer an encrypted member with the same full inner path. - Otherwise prefer the same basename. - If the plaintext ZIP contains exactly one usable file, use it automatically. - If the encrypted ZIP contains exactly one encrypted regular file, use it automatically. - If matching is ambiguous, supply `-c <inner-name>` explicitly. ### Partial KPA strength Treat partial KPA as high-value only when the hints are meaningful. The original tool prints a warning when the known bytes are weak. In practice: - Aim for at least 12 known bytes total. - Aim for at least 8 contiguous bytes. - Add `-x` byte fragments when you know fixed values like `MZ`, `PE`, or chunk markers. ### Template KPA strategy The bundled engine carries the original built-in templates: - `png` - `zip` - `exe` - `pcapng` These are strongest when: - The encrypted member extension matches the template family. - The file is `ZIP_STORED`, or at least size-compatible with a known header pattern. - The user has no full plaintext but the file type is obvious. If the user says "run the full default workflow", include `--auto-template-kpa` so OpenClaw does not stall at the follow-up prompt. ### Dictionary and mask strategy - Start with the bundled defaults only when the user has no better clue. - Use the user's dictionary immediately when they provide one. - Use `--skip-dict-count` for very large wordlists to avoid expensive upfront line counting. - Use `--auto-large-mask` only after the user explicitly accepts the cost of a huge mask search. - Remember that the built-in default sequence is: bundled `password_list.txt` then 1-6 digit numeric passwords. ### AES and bkcrack caveats - WinZip AES is supported for dictionary and mask workflows when `pyzipper` is available, but it is slower. - Fast in-memory known-plaintext validation only applies to legacy ZipCrypto, not WinZip AES. - `bkcrack` is the preferred path for full or partial KPA on ZipCrypto. - Without `bkcrack`, partial/template KPA should be explained as unavailable rather than pretending it was tried. ## Execution Rules for OpenClaw - Default to `--profile` before cracking when the user has not already provided a strong clue. - Use `scripts/openclaw_zipcracker.py` as the command entrypoint. - Quote and show the exact command you ran in your response. - Explain why the selected attack path matches the available clues. - If a run fails, choose the next tactic based on evidence, not by blindly enumerating every flag. - If dependencies are missing in a restricted environment, explain the blocker and the next best path. Do not imply AES KPA succeeded when it was skipped. - If the user only wants the decrypted contents, prefer `--skip-orig-password-recovery` after successful `bkcrack` extraction. ## Command Patterns - Profile first: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip> ``` - Default triage: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py --auto-template-kpa <zip> ``` - Huge custom dictionary: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py --skip-dict-count <zip> <huge-dict.txt> ``` - Tight mask: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -m '?l?l?l?l?d?d' ``` - Known plaintext ZIP: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <plain.zip> ``` - Partial known plaintext plus extra bytes: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <part.bin> --kpa-offset 78 -x 0 4d5a -x 128 50450000 ``` - Template KPA: ```bash python3 <skill-dir>/scripts/openclaw_zipcracker.py --auto-template-kpa <zip> --kpa-template exe -c app.exe ``` ## References - Read `references/clawhub-final-submission.md` when you need the final recommended Chinese and English storefront copy, tags, and default prompt for direct submission. - Read `references/clawhub-publishing-copy.md` when you need polished listing copy, tags, and a prompt pack for ClawHub. - Read `references/clawhub-bilingual-copy.md` when you need Chinese and English storefront copy with stronger marketing positioning. - Read `references/competitive-ctf-prompts.md` when you want a sharper, more player-like default prompt or demo prompt. - Read `references/natural-language-command-examples.md` when the user request is vague but contains clues that should map to a specific command. - Read `references/forward-test-report.md` for the latest local pressure-test findings and wording adjustments. - Read `references/release-checklist.md` before publishing or updating the skill on ClawHub. - Read `references/openclaw-workflow.md` for the preflight-to-execution flow optimized for OpenClaw. - Read `references/attack-playbook.md` for concrete user-intent-to-command mappings. - Read `references/ctf-techniques.md` for the full reproduction of the tool's solving logic, clue prioritization, and troubleshooting heuristics.

path: scripts/zipcracker_core.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import binascii
import builtins
import bz2
import copy
import hashlib
import importlib
import itertools as its
import json
import multiprocessing
import os
import platform
import queue
import re
import shlex
import shutil
import ssl
import string
import struct
import subprocess
import sys
import tarfile
import tempfile
import threading
import time
import urllib.error
import urllib.request
import zipfile
import lzma
import zlib
from dataclasses import dataclass, field
from typing import Iterable, Iterator, Optional, Sequence, Tuple
from zipfile import _ZipDecrypter

try:
    from importlib import metadata as importlib_metadata
except ImportError:
    try:
        import importlib_metadata
    except ImportError:
        importlib_metadata = None

try:
    import pyzipper

    HAS_PYZIPPER = True
except ImportError:
    pyzipper = None
    HAS_PYZIPPER = False

CHARSET_DIGITS = string.digits
CHARSET_LOWER = string.ascii_lowercase
CHARSET_UPPER = string.ascii_uppercase
CHARSET_SYMBOLS = string.punctuation
OUT_DIR_DEFAULT = "unzipped_files"
ZIPCRACKER_VERSION = "2.1.0"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PASSWORD_LIST_PATH = os.path.join(SCRIPT_DIR, "password_list.txt")
BKCRACK_REPO_URL = "https://github.com/kimci86/bkcrack"
BKCRACK_RELEASES_API = "https://api.github.com/repos/kimci86/bkcrack/releases/latest"
MSVC_REDIST_URL = "https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist"
MSVC_REDIST_X86_URL = "https://aka.ms/vc14/vc_redist.x86.exe"
MSVC_REDIST_X64_URL = "https://aka.ms/vc14/vc_redist.x64.exe"
MSVC_REDIST_ARM64_URL = "https://aka.ms/vc14/vc_redist.arm64.exe"
BKCRACK_AUTO_INSTALL_ENV = "ZIPCRACKER_AUTO_INSTALL_BKCRACK"
BKCRACK_SKIP_ORIG_PW_RECOVERY_ENV = "ZIPCRACKER_SKIP_ORIG_PW_RECOVERY"
ZIPCRACKER_SKIP_DICT_COUNT_ENV = "ZIPCRACKER_SKIP_DICT_COUNT"
PYZIPPER_AUTO_INSTALL_ENV = "ZIPCRACKER_AUTO_INSTALL_PYZIPPER"
ZIPCRACKER_ASSUME_CRC_RECOVERY_ENV = "ZIPCRACKER_ASSUME_CRC_RECOVERY"
ZIPCRACKER_ASSUME_LARGE_MASK_ENV = "ZIPCRACKER_ASSUME_LARGE_MASK"
ZIPCRACKER_ASSUME_TEMPLATE_KPA_ENV = "ZIPCRACKER_ASSUME_TEMPLATE_KPA"
STREAM_READ_CHUNK_SIZE = 1024 * 1024
PYPI_TUNA_SIMPLE_URL = "https://pypi.tuna.tsinghua.edu.cn/simple"
LOG_TIME_FORMAT = "%H:%M:%S"
LOG_PREFIX_MARKERS = ("[+]", "[*]", "[!]", "[-]", "[?]")

COMMON_EXTRA_PASSWORDS = (
    "password",
    "123456",
    "12345678",
    "qwerty",
    "secret",
    "admin",
    "root",
    "666666",
    "111111",
    "5211314",
    "password123",
    "P@ssw0rd",
    "iloveyou",
)

KPA_TEMPLATE_CHOICES = ("png", "zip", "exe", "pcapng")
KPA_TEMPLATE_ALIASES = {
    "pe": "exe",
    "windows-exe": "exe",
}
KPA_TEMPLATE_ENTRY_EXTENSIONS = {
    "png": (".png",),
    "zip": (".zip",),
    "exe": (".exe", ".dll", ".scr", ".sys", ".ocx", ".cpl"),
    "pcapng": (".pcapng",),
}
AUTO_TEMPLATE_CANDIDATE_NAMES = {
    "png": {"png-header", "png-rgb-idat", "png-rgba-idat", "png-gray-idat"},
    "pcapng": {"pcapng-shb"},
    "exe": {"exe-dos-stub", "exe-dos-stub-pe80"},
    "zip": set(),
}


@dataclass
class KnownPlaintextTemplate:
    name: str
    plain_offset: int
    plaintext_bytes: bytes
    extra_specs: list[tuple[int, bytes]] = field(default_factory=list)


@dataclass
class KnownPlaintextAttempt:
    inner_name: str
    plain_source: Optional[dict]
    plain_offset: int = 0
    extra_specs: list[tuple[int, bytes]] = field(default_factory=list)
    label: str = ""
    cleanup_paths: list[str] = field(default_factory=list)
    template_name: Optional[str] = None


@dataclass
class TemplateKpaSuggestion:
    inner_name: str
    template_name: str
    file_size: int
    compress_type: int
    confidence: str = "high"
    reason: str = ""


def loc(locale: str, zh: str, en: str) -> str:
    return en if locale == "en" else zh


def current_log_timestamp() -> str:
    return time.strftime(LOG_TIME_FORMAT, time.localtime())


def prefix_output_lines(text: str, *, timestamp: Optional[str] = None) -> str:
    if not text:
        return text
    prefix = f"[{timestamp or current_log_timestamp()}] "
    parts = text.splitlines(True)
    if not parts:
        return prefix + text
    rendered: list[str] = []
    for part in parts:
        if part in ("\n", "\r\n", "\r"):
            rendered.append(part)
            continue
        if part.lstrip().startswith(LOG_PREFIX_MARKERS):
            rendered.append(prefix + part)
        else:
            rendered.append(part)
    if not text.endswith(("\n", "\r")) and parts and parts[-1] == "":
        rendered.append(prefix)
    return "".join(rendered)


def timestamped_prompt(text: str) -> str:
    return prefix_output_lines(text)


def timestamped_print(*args, **kwargs) -> None:
    sep = kwargs.pop("sep", " ")
    end = kwargs.pop("end", "\n")
    file = kwargs.pop("file", sys.stdout)
    flush = kwargs.pop("flush", False)
    text = sep.join(str(arg) for arg in args) + end
    builtins.print(
        prefix_output_lines(text),
        end="",
        file=file,
        flush=flush,
        **kwargs,
    )


def raw_print(*args, **kwargs) -> None:
    builtins.print(*args, **kwargs)


print = timestamped_print


def normalize_password_line(line: str) -> str:
    return line.rstrip("\r\n")


def bundled_password_list_path() -> Optional[str]:
    if os.path.isfile(DEFAULT_PASSWORD_LIST_PATH):
        return DEFAULT_PASSWORD_LIST_PATH
    return None


def normalize_kpa_template_name(name: str) -> str:
    normalized = name.strip().lower()
    return KPA_TEMPLATE_ALIASES.get(normalized, normalized)


def parse_kpa_offset(value: str, locale: str) -> int:
    try:
        offset = int(value, 0)
    except ValueError as exc:
        raise ValueError(
            loc(
                locale,
                f"KPA 偏移量无效: {value}",
                f"Invalid KPA offset: {value}",
            )
        ) from exc
    if offset < 0:
        raise ValueError(
            loc(
                locale,
                f"KPA 偏移量不能为负数: {value}",
                f"KPA offset cannot be negative: {value}",
            )
        )
    return offset


def parse_kpa_extra_spec(offset_text: str, hex_text: str, locale: str) -> tuple[int, bytes]:
    offset = parse_kpa_offset(offset_text, locale)
    normalized = re.sub(r"\s+", "", hex_text)
    if normalized.lower().startswith("0x"):
        normalized = normalized[2:]
    if not normalized:
        raise ValueError(
            loc(
                locale,
                "KPA 附加字节不能为空。",
                "KPA extra bytes cannot be empty.",
            )
        )
    if len(normalized) % 2 != 0:
        raise ValueError(
            loc(
                locale,
                f"KPA 附加字节必须是偶数个十六进制字符: {hex_text}",
                f"KPA extra bytes must contain an even number of hex characters: {hex_text}",
            )
        )
    try:
        return offset, bytes.fromhex(normalized)
    except ValueError as exc:
        raise ValueError(
            loc(
                locale,
                f"KPA 附加字节不是合法十六进制: {hex_text}",
                f"KPA extra bytes are not valid hexadecimal: {hex_text}",
            )
        ) from exc


def format_hex_bytes(data: bytes, *, limit: int = 16) -> str:
    text = data.hex()
    if len(data) <= limit:
        return text
    return f"{text[: limit * 2]}..."


def merge_known_plaintext_ranges(
    plain_offset: int,
    plain_length: int,
    extra_specs: Sequence[tuple[int, bytes]],
) -> tuple[int, int]:
    ranges: list[tuple[int, int]] = []
    if plain_length > 0:
        ranges.append((plain_offset, plain_offset + plain_length))
    for extra_offset, extra_bytes in extra_specs:
        if extra_bytes:
            ranges.append((extra_offset, extra_offset + len(extra_bytes)))
    if not ranges:
        return 0, 0
    ranges.sort()
    merged: list[list[int]] = []
    for start, end in ranges:
        if not merged or start > merged[-1][1]:
            merged.append([start, end])
        else:
            merged[-1][1] = max(merged[-1][1], end)
    total_known = sum(end - start for start, end in merged)
    max_contiguous = max(end - start for start, end in merged)
    return total_known, max_contiguous


def write_temp_known_plaintext_fragment(data: bytes, suffix: str = ".bin") -> str:
    fd, path = tempfile.mkstemp(prefix="zipcracker_kpa_", suffix=suffix)
    os.close(fd)
    with open(path, "wb") as fp:
        fp.write(data)
    return path


def refresh_pyzipper_state() -> bool:
    global pyzipper, HAS_PYZIPPER
    try:
        pyzipper = importlib.import_module("pyzipper")
        HAS_PYZIPPER = True
        return True
    except ImportError:
        pyzipper = None
        HAS_PYZIPPER = False
        return False


def get_pyzipper_version() -> Optional[str]:
    if importlib_metadata is None:
        return None
    try:
        return importlib_metadata.version("pyzipper")
    except Exception as exc:
        package_not_found = getattr(importlib_metadata, "PackageNotFoundError", None)
        if package_not_found and isinstance(exc, package_not_found):
            return None
        return None


def sha256_file(file_path: str) -> str:
    digest = hashlib.sha256()
    with open(file_path, "rb") as fp:
        for chunk in iter(lambda: fp.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _http_get_json(url: str) -> dict:
    request = urllib.request.Request(
        url,
        headers={
            "User-Agent": f"ZipCracker/{ZIPCRACKER_VERSION}",
            "Accept": "application/vnd.github+json",
        },
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            return json.loads(response.read().decode("utf-8"))
    except urllib.error.URLError as exc:
        if platform.system() == "Windows" and is_ssl_cert_verify_error(exc):
            return json.loads(
                _windows_fetch_url_text(
                    url,
                    accept_header="application/vnd.github+json",
                )
            )
        raise


def _http_download_file(url: str, dest_path: str) -> None:
    request = urllib.request.Request(
        url,
        headers={"User-Agent": f"ZipCracker/{ZIPCRACKER_VERSION}"},
    )
    try:
        with urllib.request.urlopen(request, timeout=60) as response, open(
            dest_path, "wb"
        ) as output:
            shutil.copyfileobj(response, output)
    except urllib.error.URLError as exc:
        if platform.system() == "Windows" and is_ssl_cert_verify_error(exc):
            _windows_download_file(url, dest_path)
            return
        raise


def is_ssl_cert_verify_error(exc: BaseException) -> bool:
    if isinstance(exc, ssl.SSLCertVerificationError):
        return True
    reason = getattr(exc, "reason", None)
    if isinstance(reason, ssl.SSLCertVerificationError):
        return True
    detail = f"{exc}\n{reason or ''}".lower()
    return "certificate verify failed" in detail or "certificate_verify_failed" in detail


def _windows_curl_executable() -> Optional[str]:
    for name in ("curl.exe", "curl"):
        found = shutil.which(name)
        if found:
            return found
    return None


def _windows_fetch_url_text(url: str, *, accept_header: str = "") -> str:
    curl_exe = _windows_curl_executable()
    if curl_exe:
        cmd = [
            curl_exe,
            "-fsSL",
            "--retry",
            "2",
            "--connect-timeout",
            "20",
            "-H",
            f"User-Agent: ZipCracker/{ZIPCRACKER_VERSION}",
        ]
        if accept_header:
            cmd.extend(["-H", f"Accept: {accept_header}"])
        cmd.append(url)
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=180,
        )
        if proc.returncode == 0:
            return proc.stdout

    powershell = shutil.which("powershell") or shutil.which("powershell.exe")
    if not powershell:
        raise urllib.error.URLError(
            "Windows HTTPS fallback is unavailable because neither curl.exe nor PowerShell was found."
        )

    ps_script = [
        "$ProgressPreference='SilentlyContinue'",
        "$headers=@{'User-Agent'='ZipCracker/%s'}" % ZIPCRACKER_VERSION,
    ]
    if accept_header:
        ps_script.append("$headers['Accept']='%s'" % accept_header)
    ps_script.append("(Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri '%s').Content" % url.replace("'", "''"))
    proc = subprocess.run(
        [powershell, "-NoProfile", "-Command", "; ".join(ps_script)],
        capture_output=True,
        text=True,
        timeout=180,
    )
    if proc.returncode != 0:
        raise urllib.error.URLError((proc.stderr or proc.stdout or "").strip())
    return proc.stdout


def _windows_download_file(url: str, dest_path: str) -> None:
    curl_exe = _windows_curl_executable()
    if curl_exe:
        proc = subprocess.run(
            [
                curl_exe,
                "-fL",
                "--retry",
                "2",
                "--connect-timeout",
                "20",
                "-H",
                f"User-Agent: ZipCracker/{ZIPCRACKER_VERSION}",
                "-o",
                dest_path,
                url,
            ],
            capture_output=True,
            text=True,
            timeout=1800,
        )
        if proc.returncode == 0:
            return

    powershell = shutil.which("powershell") or shutil.which("powershell.exe")
    if not powershell:
        raise urllib.error.URLError(
            "Windows HTTPS fallback is unavailable because neither curl.exe nor PowerShell was found."
        )
    ps_script = "; ".join(
        [
            "$ProgressPreference='SilentlyContinue'",
            "$headers=@{'User-Agent'='ZipCracker/%s'}" % ZIPCRACKER_VERSION,
            "Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri '%s' -OutFile '%s'"
            % (url.replace("'", "''"), dest_path.replace("'", "''")),
        ]
    )
    proc = subprocess.run(
        [powershell, "-NoProfile", "-Command", ps_script],
        capture_output=True,
        text=True,
        timeout=1800,
    )
    if proc.returncode != 0:
        raise urllib.error.URLError((proc.stderr or proc.stdout or "").strip())


def get_release_asset_sha256(asset: dict) -> Optional[str]:
    digest = (asset.get("digest") or "").strip()
    if not digest:
        return None
    if ":" in digest:
        algo, value = digest.split(":", 1)
        if algo.lower() != "sha256":
            return None
        return value.lower()
    return digest.lower()


def _read_os_release() -> dict:
    info = {}
    os_release_path = "/etc/os-release"
    if not os.path.isfile(os_release_path):
        return info
    with open(os_release_path, "r", encoding="utf-8", errors="ignore") as fp:
        for line in fp:
            line = line.strip()
            if not line or "=" not in line:
                continue
            key, value = line.split("=", 1)
            info[key] = value.strip().strip('"')
    return info


def detect_os_info() -> dict:
    system = platform.system()
    machine = (platform.machine() or "").lower()
    info = {
        "system": system,
        "machine": machine,
        "pretty_name": system or "Unknown OS",
        "distro_id": "",
        "distro_like": "",
        "version_id": "",
    }
    if system == "Darwin":
        version = platform.mac_ver()[0]
        info["pretty_name"] = f"macOS {version}".strip()
    elif system == "Windows":
        release = platform.release()
        version = platform.version()
        info["pretty_name"] = f"Windows {release} ({version})".strip()
    elif system == "Linux":
        os_release = _read_os_release()
        info["distro_id"] = os_release.get("ID", "").lower()
        info["distro_like"] = os_release.get("ID_LIKE", "").lower()
        info["version_id"] = os_release.get("VERSION_ID", "")
        info["pretty_name"] = (
            os_release.get("PRETTY_NAME")
            or os_release.get("NAME")
            or "Linux"
        )
    return info


def zip_has_aes_members(zip_file: str) -> bool:
    try:
        with zipfile.ZipFile(zip_file, "r") as zf:
            return any(_has_winzip_aes_extra(info) for info in zf.infolist())
    except Exception:
        return False


def collect_archive_encryption_profile(zip_file: str) -> dict:
    profile = {
        "total_entries": 0,
        "encrypted_entries": 0,
        "unencrypted_entries": 0,
        "aes_entries": 0,
        "zipcrypto_entries": 0,
    }
    try:
        with zipfile.ZipFile(zip_file, "r") as zf:
            for info in zf.infolist():
                if info.is_dir():
                    continue
                profile["total_entries"] += 1
                if info.flag_bits & 0x1:
                    profile["encrypted_entries"] += 1
                    if _has_winzip_aes_extra(info):
                        profile["aes_entries"] += 1
                    else:
                        profile["zipcrypto_entries"] += 1
                else:
                    profile["unencrypted_entries"] += 1
    except Exception:
        pass
    return profile


def archive_encryption_notice_lines(
    locale: str, profile: dict, *, pyzipper_ready: bool
) -> list[str]:
    aes_entries = int(profile.get("aes_entries", 0) or 0)
    zipcrypto_entries = int(profile.get("zipcrypto_entries", 0) or 0)
    if aes_entries <= 0:
        return []

    lines = []
    if pyzipper_ready or HAS_PYZIPPER:
        lines.append(
            loc(
                locale,
                f"[!] 检测到该压缩包包含 {aes_entries} 个 AES 加密条目。AES 的密码校验与解压通常明显慢于传统 ZipCrypto，这是正常现象，请耐心等待。",
                f"[!] This archive contains {aes_entries} AES-encrypted entr{'y' if aes_entries == 1 else 'ies'}. AES password verification and extraction are usually much slower than legacy ZipCrypto. This is normal, so please be patient.",
            )
        )
    else:
        lines.append(
            loc(
                locale,
                f"[!] 检测到该压缩包包含 {aes_entries} 个 AES 加密条目，但当前环境未启用 pyzipper。",
                f"[!] This archive contains {aes_entries} AES-encrypted entr{'y' if aes_entries == 1 else 'ies'}, but pyzipper is not enabled in the current environment.",
            )
        )
        lines.append(
            loc(
                locale,
                "[*] ZipCracker 本身支持 AES；当前风险主要来自缺少 pyzipper 依赖，并不代表工具不支持 AES。",
                "[*] ZipCracker itself supports AES. The current risk comes from the missing pyzipper dependency and does not mean the tool lacks AES support.",
            )
        )
        lines.append(
            loc(
                locale,
                "[*] 如果继续，AES 条目的密码验证或解压可能失败，也可能看起来更慢。建议先安装 pyzipper 后再重试。",
                "[*] If you continue, AES entry verification or extraction may fail and may also appear slower. Installing pyzipper before retrying is strongly recommended.",
            )
        )

    if zipcrypto_entries > 0:
        lines.append(
            loc(
                locale,
                f"[*] 该压缩包同时还包含 {zipcrypto_entries} 个传统 ZipCrypto 条目，不同条目的破解速度可能差异明显。",
                f"[*] This archive also contains {zipcrypto_entries} legacy ZipCrypto entr{'y' if zipcrypto_entries == 1 else 'ies'}, so cracking speed may vary noticeably between entries.",
            )
        )
    return lines


def normalize_arch(machine: str) -> str:
    value = (machine or "").lower()
    if value in ("x86_64", "amd64", "x64"):
        return "x86_64"
    if value in ("arm64", "aarch64"):
        return "arm64"
    return value or "unknown"


def arch_keywords(machine: str) -> tuple[str, ...]:
    normalized = normalize_arch(machine)
    if normalized == "x86_64":
        return ("x86_64", "amd64", "x64")
    if normalized == "arm64":
        return ("arm64", "aarch64")
    return (normalized,)


def windows_vc_redist_download_url(os_info: dict) -> str:
    arch = normalize_arch(os_info.get("machine", ""))
    if arch == "arm64":
        return MSVC_REDIST_ARM64_URL
    if arch in ("x86", "i386", "i686"):
        return MSVC_REDIST_X86_URL
    return MSVC_REDIST_X64_URL


def is_ubuntu_like(os_info: dict) -> bool:
    token_blob = " ".join(
        filter(
            None,
            (
                os_info.get("distro_id", ""),
                os_info.get("distro_like", ""),
                os_info.get("pretty_name", ""),
            ),
        )
    ).lower()
    return any(
        token in token_blob
        for token in (
            "ubuntu",
            "linuxmint",
            "pop",
            "elementary",
            "zorin",
            "neon",
            "kali",
            "deepin",
        )
    )


def format_os_label(locale: str, os_info: dict) -> str:
    pretty = os_info.get("pretty_name") or os_info.get("system") or "Unknown OS"
    arch = normalize_arch(os_info.get("machine", ""))
    return loc(locale, f"{pretty} / {arch}", f"{pretty} / {arch}")


def parse_version_tuple(value: str) -> tuple[int, ...]:
    parts = re.findall(r"\d+", value or "")
    if not parts:
        return ()
    return tuple(int(part) for part in parts[:3])


def is_old_ubuntu_lts(os_info: dict) -> bool:
    if os_info.get("system") != "Linux" or not is_ubuntu_like(os_info):
        return False
    version = parse_version_tuple(os_info.get("version_id", ""))
    return bool(version) and version <= (22, 4)


def system_package_install_prefix() -> str:
    if platform.system() != "Windows" and hasattr(os, "geteuid"):
        try:
            if os.geteuid() == 0:
                return ""
        except OSError:
            pass
    return "sudo " if shutil.which("sudo") else ""


def detect_package_manager(os_info: dict) -> str:
    for candidate in ("apt", "dnf", "yum", "pacman", "zypper", "apk"):
        if shutil.which(candidate):
            return candidate

    token_blob = " ".join(
        filter(
            None,
            (
                os_info.get("distro_id", ""),
                os_info.get("distro_like", ""),
                os_info.get("pretty_name", ""),
            ),
        )
    ).lower()
    if any(token in token_blob for token in ("debian", "ubuntu", "mint")):
        return "apt"
    if any(token in token_blob for token in ("rhel", "fedora", "rocky", "alma", "centos")):
        return "dnf"
    if "suse" in token_blob or "opensuse" in token_blob:
        return "zypper"
    if "arch" in token_blob:
        return "pacman"
    if "alpine" in token_blob:
        return "apk"
    return ""


def linux_package_install_command(package_manager: str, packages: Sequence[str]) -> str:
    prefix = system_package_install_prefix()
    package_list = " ".join(packages)
    if package_manager == "apt":
        return f"{prefix}apt update && {prefix}apt install -y {package_list}"
    if package_manager == "dnf":
        return f"{prefix}dnf install -y {package_list}"
    if package_manager == "yum":
        return f"{prefix}yum install -y {package_list}"
    if package_manager == "pacman":
        return f"{prefix}pacman -Sy --noconfirm {package_list}"
    if package_manager == "zypper":
        return f"{prefix}zypper install -y {package_list}"
    if package_manager == "apk":
        return f"{prefix}apk add --no-cache {package_list}"
    return ""


def linux_bkcrack_build_dependency_command(os_info: dict) -> str:
    package_manager = detect_package_manager(os_info)
    packages_map = {
        "apt": ("build-essential", "cmake", "zlib1g-dev", "pkg-config"),
        "dnf": ("gcc-c++", "make", "cmake", "zlib-devel"),
        "yum": ("gcc-c++", "make", "cmake", "zlib-devel"),
        "pacman": ("base-devel", "cmake", "zlib"),
        "zypper": ("gcc-c++", "make", "cmake", "zlib-devel"),
        "apk": ("build-base", "cmake", "zlib-dev", "linux-headers"),
    }
    packages = packages_map.get(package_manager)
    if not packages:
        return ""
    return linux_package_install_command(package_manager, packages)


def linux_python_packaging_command(os_info: dict) -> str:
    package_manager = detect_package_manager(os_info)
    packages_map = {
        "apt": ("python3-pip", "python3-venv"),
        "dnf": ("python3-pip", "python3-virtualenv"),
        "yum": ("python3-pip", "python3-virtualenv"),
        "pacman": ("python-pip", "python-virtualenv"),
        "zypper": ("python3-pip", "python3-virtualenv"),
        "apk": ("py3-pip", "py3-virtualenv"),
    }
    packages = packages_map.get(package_manager)
    if not packages:
        return ""
    return linux_package_install_command(package_manager, packages)


def summarize_command_output(text: str, *, max_lines: int = 10, max_chars: int = 1200) -> str:
    cleaned = (text or "").strip()
    if not cleaned:
        return ""
    lines = cleaned.splitlines()
    if len(lines) > max_lines:
        lines = lines[-max_lines:]
    joined = "\n".join(lines)
    if len(joined) > max_chars:
        joined = joined[-max_chars:]
    return joined.strip()


def format_bytes(size: float) -> str:
    value = float(max(size, 0.0))
    for unit in ("B", "KB", "MB", "GB", "TB"):
        if value < 1024.0 or unit == "TB":
            if unit == "B":
                return f"{int(value)}{unit}"
            return f"{value:.2f}{unit}"
        value /= 1024.0
    return f"{value:.2f}TB"


def is_running_in_venv() -> bool:
    return (
        getattr(sys, "base_prefix", sys.prefix) != sys.prefix
        or hasattr(sys, "real_prefix")
    )


def pip_should_use_user_flag() -> bool:
    if is_running_in_venv():
        return False
    if platform.system() != "Windows" and hasattr(os, "geteuid"):
        try:
            if os.geteuid() == 0:
                return False
        except OSError:
            pass
    return True


def pyzipper_manual_methods(locale: str, os_info: dict) -> list[str]:
    python_cmd = "py -m pip install pyzipper" if os_info.get("system") == "Windows" else "python3 -m pip install pyzipper"
    exact_cmd = f"{sys.executable} -m pip install pyzipper"
    methods = []
    if locale != "en":
        methods.append(
            f"当前 Python 环境安装（推荐，中文模式优先清华源）: {sys.executable} -m pip install pyzipper -i {PYPI_TUNA_SIMPLE_URL}"
        )
    else:
        methods.append(
            loc(locale, f"Install into the current Python environment (recommended): {exact_cmd}", f"Install into the current Python environment (recommended): {exact_cmd}")
        )
    methods.append(
        loc(locale, f"当前 Python 环境安装（官方源）: {exact_cmd}", f"Install into the current Python environment (recommended): {exact_cmd}")
    )
    methods.append(loc(locale, f"通用命令: {python_cmd}", f"Generic command: {python_cmd}"))
    if is_running_in_venv():
        methods.append(
            loc(
                locale,
                "当前看起来处于虚拟环境中，建议直接安装到这个环境。",
                "A virtual environment appears to be active, so installing into it directly is recommended.",
            )
        )
    elif pip_should_use_user_flag():
        methods.append(
            loc(
                locale,
                f"若无管理员权限，可尝试用户级安装: {sys.executable} -m pip install --user pyzipper",
                f"If you do not have admin rights, try a user install: {sys.executable} -m pip install --user pyzipper",
            )
        )
    if os_info.get("system") == "Linux":
        pip_cmd = linux_python_packaging_command(os_info)
        if pip_cmd:
            methods.append(
                loc(
                    locale,
                    f"若系统缺少 pip / venv，可先安装 Python 打包组件: {pip_cmd}",
                    f"If pip / venv is missing on Linux, install the Python packaging tools first: {pip_cmd}",
                )
            )
    return methods


def build_pyzipper_pip_install_commands(locale: str) -> list[tuple[str, list[str]]]:
    command = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check"]
    if pip_should_use_user_flag():
        command.append("--user")
    official = command + ["pyzipper"]
    if locale != "en":
        tsinghua = command + ["-i", PYPI_TUNA_SIMPLE_URL, "pyzipper"]
        return [("tsinghua", tsinghua), ("official", official)]
    return [("official", official)]


def describe_pyzipper_install_failure(locale: str, detail: str, os_info: dict) -> str:
    summary = summarize_command_output(detail)
    lower_detail = (detail or "").lower()
    hints = []
    if "externally-managed-environment" in lower_detail:
        hints.append(
            loc(
                locale,
                "当前 Python 环境受发行版保护。建议先创建虚拟环境，再执行 `python -m pip install pyzipper`。",
                "This Python environment is distro-managed. Create a virtual environment first, then run `python -m pip install pyzipper` inside it.",
            )
        )
    if (
        "no module named pip" in lower_detail
        or "ensurepip is unavailable" in lower_detail
        or "pip/ensurepip is unavailable" in lower_detail
    ):
        packaging_cmd = linux_python_packaging_command(os_info)
        if packaging_cmd:
            hints.append(
                loc(
                    locale,
                    f"当前系统缺少 pip/venv 组件，可先执行: {packaging_cmd}",
                    f"This system appears to be missing pip/venv components. Install them first with: {packaging_cmd}",
                )
            )
    if "temporary failure in name resolution" in lower_detail or "failed to establish a new connection" in lower_detail:
        hints.append(
            loc(
                locale,
                "看起来是网络或镜像源访问失败，请检查网络、DNS 或 pip 源配置。",
                "This looks like a network or package index access failure. Check connectivity, DNS, or your pip index configuration.",
            )
        )
    if not hints:
        return summary or detail.strip()
    pieces = [summary] if summary else []
    pieces.extend(hints)
    return "\n".join(piece for piece in pieces if piece)


def ensure_pip_available() -> tuple[bool, str]:
    try:
        proc = subprocess.run(
            [sys.executable, "-m", "pip", "--version"],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if proc.returncode == 0:
            return True, (proc.stdout or proc.stderr or "").strip()
    except Exception:
        pass

    try:
        proc = subprocess.run(
            [sys.executable, "-m", "ensurepip", "--upgrade"],
            capture_output=True,
            text=True,
            timeout=180,
        )
        if proc.returncode == 0:
            return True, (proc.stdout or proc.stderr or "").strip()
        return False, (proc.stderr or proc.stdout or "").strip()
    except Exception as exc:
        return False, str(exc)


def auto_install_pyzipper(locale: str) -> tuple[bool, str]:
    os_info = detect_os_info()
    ok, info = ensure_pip_available()
    if not ok:
        detail = loc(
            locale,
            f"无法使用 pip/ensurepip: {info}",
            f"pip/ensurepip is unavailable: {info}",
        )
        return False, describe_pyzipper_install_failure(locale, detail, os_info)

    if locale != "en":
        print(
            loc(
                locale,
                f"[*] 中文模式优先使用清华 PyPI 镜像安装 pyzipper: {PYPI_TUNA_SIMPLE_URL}",
                f"[*] Chinese locale detected. Preferring the Tsinghua PyPI mirror for pyzipper: {PYPI_TUNA_SIMPLE_URL}",
            )
        )

    last_error = ""
    for source_name, cmd in build_pyzipper_pip_install_commands(locale):
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=1800,
        )
        if proc.returncode == 0:
            break
        last_error = proc.stderr or proc.stdout or ""
        if source_name == "tsinghua":
            print(
                loc(
                    locale,
                    "[!] 通过清华 PyPI 镜像安装 pyzipper 失败，正在自动回退到官方 PyPI...",
                    "[!] Installing pyzipper through the Tsinghua PyPI mirror failed. Falling back to the official PyPI index...",
                )
            )
    else:
        return False, describe_pyzipper_install_failure(
            locale,
            last_error,
            os_info,
        )

    if not refresh_pyzipper_state():
        return False, loc(locale, "安装完成，但当前进程未能导入 pyzipper。", "Installation finished, but the current process could not import pyzipper.")

    version = get_pyzipper_version() or "unknown"
    return True, version


def get_managed_bkcrack_root() -> str:
    if platform.system() == "Windows":
        base = os.environ.get("LOCALAPPDATA") or os.path.join(
            os.path.expanduser("~"), "AppData", "Local"
        )
        return os.path.join(base, "ZipCracker", "tools", "bkcrack")
    return os.path.join(os.path.expanduser("~"), ".zipcracker", "tools", "bkcrack")


def _find_bkcrack_in_tree(root_dir: str) -> Optional[str]:
    if not os.path.isdir(root_dir):
        return None
    exe_name = "bkcrack.exe" if platform.system() == "Windows" else "bkcrack"
    for dirpath, _, filenames in os.walk(root_dir):
        if exe_name in filenames:
            candidate = os.path.join(dirpath, exe_name)
            if platform.system() != "Windows":
                try:
                    os.chmod(candidate, os.stat(candidate).st_mode | 0o111)
                except OSError:
                    pass
            return candidate
    return None


def find_bkcrack_executable() -> Optional[str]:
    found = shutil.which("bkcrack")
    if found and probe_bkcrack_executable(found)["usable"]:
        return found
    managed_root = get_managed_bkcrack_root()
    current_dir = os.path.join(managed_root, "current")
    for candidate in (
        _find_bkcrack_in_tree(current_dir),
        _find_bkcrack_in_tree(managed_root),
    ):
        if candidate and probe_bkcrack_executable(candidate)["usable"]:
            return candidate
    return None


def probe_bkcrack_executable(exec_path: str) -> dict:
    result = {
        "path": exec_path,
        "usable": False,
        "version": None,
        "error": "",
    }
    if not exec_path:
        result["error"] = "empty path"
        return result

    outputs = []
    for command in ([exec_path, "--version"], [exec_path, "-V"]):
        try:
            proc = subprocess.run(
                command,
                capture_output=True,
                text=True,
                timeout=10,
            )
        except OSError as exc:
            result["error"] = str(exc)
            return result
        except subprocess.SubprocessError as exc:
            outputs.append(str(exc))
            continue
        merged = (proc.stdout or "").strip()
        merged_err = (proc.stderr or "").strip()
        outputs.extend(filter(None, (merged, merged_err)))
        if proc.returncode == 0:
            line = (merged or merged_err or "").splitlines()
            if line:
                result["usable"] = True
                result["version"] = line[0].strip()
                result["error"] = ""
                return result

    result["error"] = outputs[0].splitlines()[0] if outputs else "unknown error"
    return result


def get_bkcrack_version(exec_path: str) -> Optional[str]:
    return probe_bkcrack_executable(exec_path).get("version")


def _is_supported_archive_name(filename: str) -> bool:
    lowered = filename.lower()
    return lowered.endswith((".zip", ".tar.gz", ".tgz", ".tar.xz", ".tar"))


def _extract_archive(archive_path: str, dest_dir: str) -> None:
    lowered = archive_path.lower()
    if lowered.endswith(".zip"):
        with zipfile.ZipFile(archive_path, "r") as zf:
            zf.extractall(dest_dir)
        return
    if lowered.endswith((".tar.gz", ".tgz", ".tar.xz", ".tar")):
        with tarfile.open(archive_path, "r:*") as tf:
            tf.extractall(dest_dir)
        return
    os.makedirs(dest_dir, exist_ok=True)
    shutil.copy2(archive_path, os.path.join(dest_dir, os.path.basename(archive_path)))


def _find_source_root(root_dir: str) -> Optional[str]:
    for dirpath, _, filenames in os.walk(root_dir):
        if "CMakeLists.txt" in filenames:
            return dirpath
    return None


def linux_bkcrack_source_build_command(os_info: dict) -> str:
    compile_cmd = (
        "cmake -S . -B build -DCMAKE_INSTALL_PREFIX=install && "
        "cmake --build build --config Release && "
        "cmake --build build --config Release --target install"
    )
    dep_cmd = linux_bkcrack_build_dependency_command(os_info)
    if dep_cmd:
        return f"{dep_cmd} && {compile_cmd}"
    return compile_cmd


def describe_bkcrack_build_failure(locale: str, detail: str, os_info: dict) -> str:
    summary = summarize_command_output(detail)
    lower_detail = (detail or "").lower()
    hints = []

    if "could not find zlib" in lower_detail or "zlib" in lower_detail:
        hints.append(
            loc(
                locale,
                "看起来缺少 zlib 开发头文件，bkcrack 无法完成链接。",
                "This looks like a missing zlib development header/library dependency required for linking bkcrack.",
            )
        )
    if "cmake_cxx_compiler" in lower_detail or "no c++ compiler" in lower_detail:
        hints.append(
            loc(
                locale,
                "当前系统缺少可用的 C++ 编译器。",
                "No usable C++ compiler was detected on this system.",
            )
        )
    if "ninja" in lower_detail or "make" in lower_detail:
        hints.append(
            loc(
                locale,
                "当前系统可能缺少构建工具（make / ninja）。",
                "This system may be missing a build backend such as make or ninja.",
            )
        )

    dep_cmd = linux_bkcrack_build_dependency_command(os_info)
    if dep_cmd:
        hints.append(
            loc(
                locale,
                f"可先安装 Linux 编译依赖后重试: {dep_cmd}",
                f"Install the Linux build dependencies first, then retry: {dep_cmd}",
            )
        )

    if not hints:
        return summary or detail.strip()
    pieces = [summary] if summary else []
    pieces.extend(hints)
    return "\n".join(piece for piece in pieces if piece)


def describe_bkcrack_install_failure(locale: str, detail: str, os_info: dict) -> str:
    summary = summarize_command_output(detail)
    lower_detail = (detail or "").lower()
    hints = []

    if "certificate verify failed" in lower_detail or "certificate_verify_failed" in lower_detail:
        hints.append(
            loc(
                locale,
                "这通常不是单纯的“国内无法访问 GitHub”，而是当前 Python HTTPS 证书链校验失败。常见原因包括系统根证书缺失、代理/安全软件拦截 HTTPS、或 Python 自身证书环境异常。",
                "This is usually not just 'GitHub is unreachable'. It means HTTPS certificate-chain verification failed in the current Python environment. Common causes include missing system root certificates, HTTPS interception by a proxy/security product, or a broken Python certificate setup.",
            )
        )
        if os_info.get("system") == "Windows":
            hints.append(
                loc(
                    locale,
                    "建议先尝试：1. 校准系统时间 2. 更新系统根证书 3. 关闭会拦截 HTTPS 的代理/杀软 4. 用浏览器手动打开 GitHub release 页面测试。",
                    "Try these first: 1. verify the system clock 2. update Windows root certificates 3. disable any HTTPS-intercepting proxy/antivirus 4. open the GitHub release page in a browser to test.",
                )
            )
    if "timed out" in lower_detail or "name resolution" in lower_detail:
        hints.append(
            loc(
                locale,
                "这更像是网络超时或 DNS 解析问题，请检查网络出口、DNS 或代理设置。",
                "This looks more like a network timeout or DNS resolution problem. Check your outbound network, DNS, or proxy settings.",
            )
        )
    if not hints:
        return summary or detail.strip()
    pieces = [summary] if summary else []
    pieces.extend(hints)
    return "\n".join(piece for piece in pieces if piece)


def windows_exception_code_from_returncode(returncode: int) -> Optional[int]:
    if platform.system() != "Windows":
        return None
    if returncode < 0:
        return None
    if returncode >= 0x80000000:
        return returncode
    return None


def describe_bkcrack_runtime_failure(
    locale: str, returncode: int, output: str = ""
) -> list[str]:
    os_info = detect_os_info()
    lines = [
        loc(
            locale,
            f"[!] bkcrack 退出码: {returncode}",
            f"[!] bkcrack exit code: {returncode}",
        )
    ]
    win_code = windows_exception_code_from_returncode(returncode)
    if win_code == 0xC0000005:
        vc_redist_direct = windows_vc_redist_download_url(os_info)
        lines.append(
            loc(
                locale,
                "[!] 该退出码对应 Windows 异常 0xC0000005（Access Violation，访问冲突）。这不是“密码不对”，而是 bkcrack 进程在运行中崩溃了。",
                "[!] This maps to Windows exception 0xC0000005 (Access Violation). This does not mean 'wrong password'; it means the bkcrack process itself crashed while running.",
            )
        )
        lines.append(
            loc(
                locale,
                "[*] 常见原因：Windows 版 bkcrack 本身崩溃、运行库/安全软件干扰、或多线程运行时异常。",
                "[*] Common causes include a crash inside the Windows bkcrack build, runtime/security-software interference, or a multithreading issue.",
            )
        )
        lines.append(
            loc(
                locale,
                f"[*] 建议先尝试：1. 安装/修复 Microsoft Visual C++ Redistributable（文档 {MSVC_REDIST_URL} ，直链 {vc_redist_direct}） 2. 设 BKCRACK_JOBS=1 再试 3. 临时关闭杀软或给 bkcrack.exe 加白名单 4. 若仍崩溃，优先在 WSL / Linux 下使用 bkcrack。",
                f"[*] Recommended next steps: 1. install/repair Microsoft Visual C++ Redistributable (docs {MSVC_REDIST_URL} , direct download {vc_redist_direct}) 2. retry with BKCRACK_JOBS=1 3. temporarily disable antivirus or whitelist bkcrack.exe 4. if it still crashes, prefer running bkcrack under WSL/Linux.",
            )
        )
    elif win_code == 0xC000001D:
        lines.append(
            loc(
                locale,
                "[!] 该退出码对应 Windows 异常 0xC000001D（Illegal Instruction，非法指令），通常意味着可执行文件与当前 CPU/指令集不兼容。",
                "[!] This maps to Windows exception 0xC000001D (Illegal Instruction), which usually means the executable is incompatible with the current CPU/instruction set.",
            )
        )
    elif win_code == 0xC0000135:
        lines.append(
            loc(
                locale,
                "[!] 该退出码对应 Windows 异常 0xC0000135，通常表示缺少运行时依赖或 DLL。",
                "[!] This maps to Windows exception 0xC0000135, which usually means a required runtime or DLL is missing.",
            )
        )
    elif win_code is not None:
        lines.append(
            loc(
                locale,
                f"[*] 该退出码对应 Windows 异常码 0x{win_code:08X}。",
                f"[*] This return code corresponds to Windows exception 0x{win_code:08X}.",
            )
        )

    lower_output = (output or "").lower()
    if "z reduction" in lower_output and win_code == 0xC0000005:
        lines.append(
            loc(
                locale,
                "[*] 从当前日志看，bkcrack 已经开始处理已知明文（Z reduction 阶段）后才崩溃，说明下载、启动、参数传递都基本正常，问题更像是 bkcrack Windows 运行时崩溃。",
                "[*] Based on the current log, bkcrack started processing the known plaintext (during the Z reduction stage) and then crashed. That means download/startup/argument passing are basically fine; the issue looks more like a runtime crash inside the Windows bkcrack path.",
            )
        )
    return lines


def _choose_bkcrack_release_asset(release: dict, os_info: dict) -> Optional[dict]:
    system = os_info.get("system", "")
    if system == "Darwin":
        os_keywords = ("macos", "darwin")
    elif system == "Windows":
        os_keywords = ("windows", "win")
    elif system == "Linux":
        os_keywords = ("ubuntu", "linux") if is_ubuntu_like(os_info) else ("linux",)
    else:
        return None

    candidates = []
    for asset in release.get("assets", []):
        name = asset.get("name", "")
        lowered = name.lower()
        if not _is_supported_archive_name(lowered):
            continue
        if not any(keyword in lowered for keyword in os_keywords):
            continue
        arch_score = 1 if any(keyword in lowered for keyword in arch_keywords(os_info.get("machine", ""))) else 0
        os_score = 2 if any(keyword in lowered for keyword in os_keywords[:-1] or os_keywords) else 1
        candidates.append((os_score, arch_score, lowered, asset))
    if not candidates:
        return None
    candidates.sort(key=lambda item: (item[0], item[1], item[2]), reverse=True)
    return candidates[0][3]


def bkcrack_manual_methods(locale: str, os_info: dict) -> list[str]:
    compile_cmd = linux_bkcrack_source_build_command(os_info)
    dep_cmd = linux_bkcrack_build_dependency_command(os_info)
    system = os_info.get("system", "")
    if system == "Darwin":
        return [
            loc(locale, "Homebrew（推荐）: brew install bkcrack", "Homebrew (recommended): brew install bkcrack"),
            loc(locale, f"官方预编译包: 打开 {BKCRACK_REPO_URL}/releases 下载适用于 macOS 的压缩包并解压", f"Official precompiled package: open {BKCRACK_REPO_URL}/releases and download the macOS archive"),
            loc(locale, "源码编译: cmake -S . -B build -DCMAKE_INSTALL_PREFIX=install && cmake --build build --config Release && cmake --build build --config Release --target install", "Build from source: cmake -S . -B build -DCMAKE_INSTALL_PREFIX=install && cmake --build build --config Release && cmake --build build --config Release --target install"),
        ]
    if system == "Windows":
        vc_redist_direct = windows_vc_redist_download_url(os_info)
        return [
            loc(locale, f"官方预编译包（推荐）: 打开 {BKCRACK_REPO_URL}/releases 下载适用于 Windows 的压缩包并解压", f"Official precompiled package (recommended): open {BKCRACK_REPO_URL}/releases and download the Windows archive"),
            loc(locale, f"如果运行时报缺少运行库，请安装 Microsoft Visual C++ Redistributable: 文档 {MSVC_REDIST_URL} ，直链 {vc_redist_direct}", f"If bkcrack reports missing runtime libraries, install Microsoft Visual C++ Redistributable: docs {MSVC_REDIST_URL} , direct download {vc_redist_direct}"),
        ]
    if system == "Linux" and is_ubuntu_like(os_info):
        methods = []
        if dep_cmd:
            methods.append(
                loc(
                    locale,
                    f"Linux 构建依赖（apt/Ubuntu 系）: {dep_cmd}",
                    f"Linux build dependencies (apt / Ubuntu family): {dep_cmd}",
                )
            )
        methods.extend(
            [
                loc(locale, f"源码编译（推荐，兼容性最好）: {compile_cmd}", f"Build from source (recommended for compatibility): {compile_cmd}"),
                loc(locale, f"官方预编译包（更适合较新的 Ubuntu / glibc）: 打开 {BKCRACK_REPO_URL}/releases 下载适用于 Ubuntu 的压缩包并解压", f"Official precompiled package (better for newer Ubuntu/glibc): open {BKCRACK_REPO_URL}/releases and download the Ubuntu archive"),
            ]
        )
        return methods
    if system == "Linux":
        methods = []
        if dep_cmd:
            methods.append(
                loc(
                    locale,
                    f"Linux 构建依赖: {dep_cmd}",
                    f"Linux build dependencies: {dep_cmd}",
                )
            )
        methods.extend(
            [
                loc(locale, f"源码编译（推荐）: {compile_cmd}", f"Build from source (recommended): {compile_cmd}"),
                loc(locale, f"如果你的发行版有第三方维护的 bkcrack 包，也可以使用系统包管理器安装；官方说明见 {BKCRACK_REPO_URL}", f"If your distribution provides a third-party bkcrack package, you can also use the system package manager; see {BKCRACK_REPO_URL}"),
            ]
        )
        return methods
    return [
        loc(locale, f"请从官方仓库查看安装说明: {BKCRACK_REPO_URL}", f"Please check the official repository for installation instructions: {BKCRACK_REPO_URL}")
    ]


def bkcrack_auto_install_mode(os_info: dict) -> Optional[str]:
    system = os_info.get("system", "")
    if system in ("Darwin", "Windows"):
        return "release"
    if system == "Linux":
        return "source"
    return None


def _install_bkcrack_from_release(locale: str, os_info: dict) -> tuple[bool, str]:
    release = _http_get_json(BKCRACK_RELEASES_API)
    asset = _choose_bkcrack_release_asset(release, os_info)
    if not asset:
        return False, loc(locale, "未找到与你当前系统匹配的官方预编译包。", "No matching official precompiled package was found for your OS.")

    managed_root = get_managed_bkcrack_root()
    current_dir = os.path.join(managed_root, "current")
    temp_dir = tempfile.mkdtemp(prefix="zipcracker_bkcrack_install_")
    try:
        archive_path = os.path.join(temp_dir, asset["name"])
        print(
            loc(
                locale,
                f"[*] 正在下载 bkcrack 官方预编译包: {asset['name']}（版本 {release.get('tag_name') or 'unknown'}）",
                f"[*] Downloading official bkcrack package: {asset['name']} (version {release.get('tag_name') or 'unknown'})",
            )
        )
        _http_download_file(asset["browser_download_url"], archive_path)
        actual_sha256 = sha256_file(archive_path)
        expected_sha256 = get_release_asset_sha256(asset)
        if expected_sha256:
            if actual_sha256.lower() != expected_sha256.lower():
                return (
                    False,
                    loc(
                        locale,
                        f"下载文件 SHA256 校验失败。期望 {expected_sha256}，实际 {actual_sha256}",
                        f"Downloaded file SHA256 mismatch. Expected {expected_sha256}, got {actual_sha256}",
                    ),
                )
            print(
                loc(
                    locale,
                    f"[+] 已验证下载文件 SHA256: {actual_sha256}",
                    f"[+] Verified downloaded file SHA256: {actual_sha256}",
                )
            )
        else:
            print(
                loc(
                    locale,
                    f"[*] 发布元数据未提供摘要，已本地计算 SHA256: {actual_sha256}",
                    f"[*] Release metadata did not expose a digest, computed local SHA256: {actual_sha256}",
                )
            )
        extract_dir = os.path.join(temp_dir, "extract")
        _extract_archive(archive_path, extract_dir)

        if os.path.isdir(current_dir):
            shutil.rmtree(current_dir, ignore_errors=True)
        os.makedirs(managed_root, exist_ok=True)
        shutil.copytree(extract_dir, current_dir)

        installed = _find_bkcrack_in_tree(current_dir)
        if not installed:
            return False, loc(locale, "已下载并解压，但未找到 bkcrack 可执行文件。", "Downloaded and extracted, but the bkcrack executable was not found.")
        probe = probe_bkcrack_executable(installed)
        if not probe["usable"]:
            return (
                False,
                loc(
                    locale,
                    f"下载的 bkcrack 与当前系统不兼容: {probe['error']}",
                    f"The downloaded bkcrack binary is incompatible with this system: {probe['error']}",
                ),
            )
        version = probe["version"] or release.get("tag_name") or "unknown"
        return True, json.dumps(
            {
                "path": installed,
                "version": version,
                "release_tag": release.get("tag_name") or "",
                "sha256": actual_sha256,
            },
            ensure_ascii=False,
        )
    finally:
        shutil.rmtree(temp_dir, ignore_errors=True)


def _install_bkcrack_from_source(locale: str) -> tuple[bool, str]:
    os_info = detect_os_info()
    missing_requirements = []
    if not shutil.which("cmake"):
        missing_requirements.append("cmake")
    if not any(shutil.which(name) for name in ("c++", "g++", "clang++")):
        missing_requirements.append("C++ compiler")
    if not any(shutil.which(name) for name in ("make", "ninja")):
        missing_requirements.append("make/ninja")
    if missing_requirements:
        dep_cmd = linux_bkcrack_build_dependency_command(os_info)
        missing_text = ", ".join(missing_requirements)
        if dep_cmd:
            return (
                False,
                loc(
                    locale,
                    f"源码一键安装缺少必要构建组件: {missing_text}。可先执行: {dep_cmd}",
                    f"Source-based auto-install is missing required build tools: {missing_text}. Install them first with: {dep_cmd}",
                ),
            )
        return (
            False,
            loc(
                locale,
                f"源码一键安装缺少必要构建组件: {missing_text}",
                f"Source-based auto-install is missing required build tools: {missing_text}",
            ),
        )

    release = _http_get_json(BKCRACK_RELEASES_API)
    tarball_url = release.get("tarball_url")
    if not tarball_url:
        return False, loc(locale, "未获取到 bkcrack 源码下载地址。", "Could not resolve the bkcrack source download URL.")

    managed_root = get_managed_bkcrack_root()
    current_dir = os.path.join(managed_root, "current")
    temp_dir = tempfile.mkdtemp(prefix="zipcracker_bkcrack_src_")
    try:
        archive_path = os.path.join(temp_dir, "bkcrack-source.tar.gz")
        print(
            loc(
                locale,
                "[*] 正在下载 bkcrack 源码并准备编译安装...",
                "[*] Downloading bkcrack source and preparing a local build...",
            )
        )
        _http_download_file(tarball_url, archive_path)
        source_extract_dir = os.path.join(temp_dir, "src")
        _extract_archive(archive_path, source_extract_dir)
        source_root = _find_source_root(source_extract_dir)
        if not source_root:
            return False, loc(locale, "未在源码包中找到 CMakeLists.txt。", "CMakeLists.txt was not found in the downloaded source tree.")

        build_dir = os.path.join(temp_dir, "build")
        if os.path.isdir(current_dir):
            shutil.rmtree(current_dir, ignore_errors=True)
        os.makedirs(managed_root, exist_ok=True)

        for command in (
            ["cmake", "-S", source_root, "-B", build_dir, f"-DCMAKE_INSTALL_PREFIX={current_dir}"],
            ["cmake", "--build", build_dir, "--config", "Release"],
            ["cmake", "--build", build_dir, "--config", "Release", "--target", "install"],
        ):
            proc = subprocess.run(
                command,
                capture_output=True,
                text=True,
            )
            if proc.returncode != 0:
                return (
                    False,
                    describe_bkcrack_build_failure(
                        locale,
                        proc.stderr or proc.stdout or str(command),
                        os_info,
                    ),
                )

        installed = _find_bkcrack_in_tree(current_dir)
        if not installed:
            return False, loc(locale, "源码编译已完成，但未找到 bkcrack 可执行文件。", "The source build completed, but the bkcrack executable was not found.")
        probe = probe_bkcrack_executable(installed)
        if not probe["usable"]:
            return (
                False,
                loc(
                    locale,
                    f"源码编译后的 bkcrack 无法正常执行: {probe['error']}",
                    f"The source-built bkcrack could not be executed: {probe['error']}",
                ),
            )
        version = probe["version"] or release.get("tag_name") or "unknown"
        return True, json.dumps(
            {
                "path": installed,
                "version": version,
                "release_tag": release.get("tag_name") or "",
                "sha256": "",
            },
            ensure_ascii=False,
        )
    finally:
        shutil.rmtree(temp_dir, ignore_errors=True)


def prompt_yes_no(
    locale: str,
    zh_prompt: str,
    en_prompt: str,
    *,
    env_name: Optional[str] = None,
) -> bool:
    if env_name:
        env_value = os.environ.get(env_name, "").strip().lower()
        if env_value in ("1", "true", "yes", "y", "on"):
            return True
        if env_value in ("0", "false", "no", "n", "off"):
            return False
    if not sys.stdin.isatty():
        return False

    while True:
        answer = input(timestamped_prompt(loc(locale, zh_prompt, en_prompt))).strip().lower()
        if answer in ("y", "yes"):
            return True
        if answer in ("n", "no", ""):
            return False
        print(loc(locale, "[!] 请输入 y 或 n。", "[!] Please answer with y or n."))


def offer_pyzipper_install(locale: str, zip_file: str) -> bool:
    if refresh_pyzipper_state():
        return True

    os_info = detect_os_info()
    archive_has_aes = zip_has_aes_members(zip_file)
    version = get_pyzipper_version()
    if version:
        print(
            loc(
                locale,
                f"[+] 检测到您的系统已安装 pyzipper {version}，自动开启 AES 加密破解支持。",
                f"[+] pyzipper {version} detected. AES-encrypted ZIP support is enabled.",
            )
        )
        return True

    print(
        loc(
            locale,
            f"[!] 未检测到 pyzipper。当前系统: {format_os_label(locale, os_info)}",
            f"[!] pyzipper was not detected. Current OS: {format_os_label(locale, os_info)}",
        )
    )
    if archive_has_aes:
        print(
            loc(
                locale,
                "[*] 当前压缩包检测到 AES 扩展字段；若跳过安装，AES 条目可能无法正确验证或解压。",
                "[*] AES extra fields were detected in this archive. If you skip installation, AES entries may not be verified or extracted correctly.",
            )
        )
    else:
        print(
            loc(
                locale,
                "[*] 当前压缩包未检测到明显的 AES 标记；如果只是传统 ZipCrypto，可以选择跳过安装。",
                "[*] No obvious AES marker was detected in this archive. If you only need legacy ZipCrypto support, you can skip installation.",
            )
        )

    methods = pyzipper_manual_methods(locale, os_info)
    for index, method in enumerate(methods, start=1):
        prefix = "安装方式" if locale != "en" else "Install option"
        print(f"[*] {prefix}{index}: {method}")

    env_value = os.environ.get(PYZIPPER_AUTO_INSTALL_ENV, "").strip().lower()
    if env_value in ("1", "true", "yes", "y", "on"):
        should_install = True
    elif env_value in ("0", "false", "no", "n", "off"):
        should_install = False
    else:
        should_install = prompt_yes_no(
            locale,
            "[?] 是否现在执行 pyzipper 一键自动安装？输入 n 可跳过继续当前任务。 (y/n): ",
            "[?] Install pyzipper now? Enter n to skip and continue the current task. (y/n): ",
        )

    if not should_install:
        print(
            loc(
                locale,
                "[*] 已跳过 pyzipper 安装，继续当前任务。",
                "[*] Skipping pyzipper installation and continuing the current task.",
            )
        )
        return False

    print(
        loc(
            locale,
            "[*] 开始执行 pyzipper 一键自动安装...",
            "[*] Starting one-click pyzipper installation...",
        )
    )
    success, result = auto_install_pyzipper(locale)
    if success:
        print(
            loc(
                locale,
                f"[+] pyzipper 安装成功，当前版本: {result}",
                f"[+] pyzipper installed successfully. Current version: {result}",
            )
        )
        return True

    print(
        loc(
            locale,
            f"[!] pyzipper 一键自动安装失败: {result}",
            f"[!] One-click pyzipper installation failed: {result}",
        )
    )
    print(
        loc(
            locale,
            "[*] 已跳过 pyzipper 安装，继续当前任务。",
            "[*] Skipping pyzipper installation and continuing the current task.",
        )
    )
    return False


def offer_bkcrack_install(locale: str, *, required: bool) -> Optional[str]:
    existing = find_bkcrack_executable()
    if existing:
        return existing

    os_info = detect_os_info()
    raw_existing = shutil.which("bkcrack")
    managed_existing = _find_bkcrack_in_tree(os.path.join(get_managed_bkcrack_root(), "current"))
    incompatible_probe = None
    if raw_existing:
        probe = probe_bkcrack_executable(raw_existing)
        if not probe["usable"]:
            incompatible_probe = probe
    elif managed_existing:
        probe = probe_bkcrack_executable(managed_existing)
        if not probe["usable"]:
            incompatible_probe = probe

    auto_mode = bkcrack_auto_install_mode(os_info)
    managed_root = get_managed_bkcrack_root()
    print(
        loc(
            locale,
            f"[!] 未检测到 bkcrack。当前系统: {format_os_label(locale, os_info)}",
            f"[!] bkcrack was not detected. Current OS: {format_os_label(locale, os_info)}",
        )
    )
    if incompatible_probe:
        print(
            loc(
                locale,
                f"[!] 已发现现有 bkcrack 但不可用: {incompatible_probe['path']}",
                f"[!] Found an existing bkcrack executable, but it is unusable: {incompatible_probe['path']}",
            )
        )
        print(
            loc(
                locale,
                f"[*] 原因: {incompatible_probe['error']}",
                f"[*] Reason: {incompatible_probe['error']}",
            )
        )
    for index, method in enumerate(bkcrack_manual_methods(locale, os_info), start=1):
        print(loc(locale, f"[*] 安装方式{index}: {method}", f"[*] Install option {index}: {method}"))

    if not auto_mode:
        print(
            loc(
                locale,
                "[*] 当前系统暂未提供内置一键安装能力，请按上面的手动方式安装。",
                "[*] A built-in one-click installer is not available for this OS yet. Please use one of the manual installation methods above.",
            )
        )
        return None

    print(
        loc(
            locale,
            f"[*] 一键自动安装会把 bkcrack 安装到本地目录: {managed_root}",
            f"[*] One-click install will place bkcrack under: {managed_root}",
        )
    )
    should_install = prompt_yes_no(
        locale,
        "[?] 是否现在执行一键自动安装并继续当前任务？ (y/n): ",
        "[?] Install bkcrack now and continue the current task? (y/n): ",
        env_name=BKCRACK_AUTO_INSTALL_ENV,
    )
    if not should_install:
        if required:
            print(
                loc(
                    locale,
                    "[!] 当前命令必须依赖 bkcrack。请先完成安装后重试。",
                    "[!] This command requires bkcrack. Please install it and retry.",
                )
            )
        return None

    print(
        loc(
            locale,
            "[*] 开始执行 bkcrack 一键安装...",
            "[*] Starting one-click bkcrack installation...",
        )
    )
    try:
        if auto_mode == "release":
            success, result = _install_bkcrack_from_release(locale, os_info)
        else:
            success, result = _install_bkcrack_from_source(locale)
            if not success and incompatible_probe and os_info.get("system") == "Linux" and not shutil.which("cmake"):
                fallback_ok, fallback_result = _install_bkcrack_from_release(locale, os_info)
                success, result = fallback_ok, fallback_result
    except urllib.error.URLError as exc:
        success = False
        result = str(exc)
    except Exception as exc:
        success = False
        result = str(exc)

    if success:
        try:
            install_meta = json.loads(result)
        except json.JSONDecodeError:
            install_meta = {"path": result, "version": "unknown", "release_tag": "", "sha256": ""}
        version = install_meta.get("version") or install_meta.get("release_tag") or "unknown"
        path = install_meta.get("path") or result
        print(
            loc(
                locale,
                f"[+] bkcrack 安装成功: {path}",
                f"[+] bkcrack installed successfully: {path}",
            )
        )
        print(
            loc(
                locale,
                f"[+] 已安装版本: {version}",
                f"[+] Installed version: {version}",
            )
        )
        if install_meta.get("sha256"):
            print(
                loc(
                    locale,
                    f"[+] 安装包 SHA256 已校验通过: {install_meta['sha256']}",
                    f"[+] Package SHA256 verified: {install_meta['sha256']}",
                )
            )
        if os_info.get("system") == "Windows":
            vc_redist_direct = windows_vc_redist_download_url(os_info)
            print(
                loc(
                    locale,
                    f"[*] 若后续运行提示缺少运行库，请安装 Microsoft Visual C++ Redistributable: 文档 {MSVC_REDIST_URL} ，直链 {vc_redist_direct}",
                    f"[*] If a runtime library error appears later, install Microsoft Visual C++ Redistributable: docs {MSVC_REDIST_URL} , direct download {vc_redist_direct}",
                )
            )
        return path

    print(
        loc(
            locale,
            f"[!] 一键自动安装失败: {describe_bkcrack_install_failure(locale, result, os_info)}",
            f"[!] One-click installation failed: {describe_bkcrack_install_failure(locale, result, os_info)}",
        )
    )
    if required:
        print(
            loc(
                locale,
                "[!] 当前命令必须依赖 bkcrack，请按上面的手动方式安装后重试。",
                "[!] This command requires bkcrack. Please use one of the manual installation methods above and retry.",
            )
        )
    return None


def is_regular_member(info: zipfile.ZipInfo) -> bool:
    return not info.is_dir()


def _has_winzip_aes_extra(info: zipfile.ZipInfo) -> bool:
    extra = info.extra
    i = 0
    while i + 4 <= len(extra):
        tid, ln = struct.unpack("<HH", extra[i : i + 4])
        if tid == 0x9901:
            return True
        i += 4 + ln
    return False


def is_zip_archive(path: str) -> bool:
    try:
        return zipfile.is_zipfile(path)
    except OSError:
        return False


def build_kpa_template_candidates(template_name: str, locale: str) -> list[KnownPlaintextTemplate]:
    template = normalize_kpa_template_name(template_name)
    if template == "png":
        return [
            KnownPlaintextTemplate(
                name="png-header",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("89504e470d0a1a0a0000000d49484452"),
            ),
            KnownPlaintextTemplate(
                name="png-rgb-idat",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("89504e470d0a1a0a0000000d49484452"),
                extra_specs=[
                    (24, bytes.fromhex("0802000000")),
                    (37, b"IDAT"),
                ],
            ),
            KnownPlaintextTemplate(
                name="png-rgba-idat",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("89504e470d0a1a0a0000000d49484452"),
                extra_specs=[
                    (24, bytes.fromhex("0806000000")),
                    (37, b"IDAT"),
                ],
            ),
            KnownPlaintextTemplate(
                name="png-gray-idat",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("89504e470d0a1a0a0000000d49484452"),
                extra_specs=[
                    (24, bytes.fromhex("0800000000")),
                    (37, b"IDAT"),
                ],
            ),
        ]
    if template == "pcapng":
        return [
            KnownPlaintextTemplate(
                name="pcapng-shb",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("0a0d0d0a1c0000004d3c2b1a01000000"),
            )
        ]
    if template == "exe":
        return [
            KnownPlaintextTemplate(
                name="exe-mz-prefix",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("4d5a90000300000004000000ffff0000"),
            ),
            KnownPlaintextTemplate(
                name="exe-dos-stub",
                plain_offset=78,
                plaintext_bytes=b"This program cannot be run in DOS mode.",
                extra_specs=[(0, b"MZ")],
            ),
            KnownPlaintextTemplate(
                name="exe-dos-stub-pe80",
                plain_offset=78,
                plaintext_bytes=b"This program cannot be run in DOS mode.",
                extra_specs=[
                    (0, b"MZ"),
                    (60, bytes.fromhex("80000000")),
                    (128, b"PE\x00\x00"),
                ],
            ),
        ]
    if template == "zip":
        return [
            KnownPlaintextTemplate(
                name="zip-local-header-stored-v20",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("504b03041400000000000000"),
            ),
            KnownPlaintextTemplate(
                name="zip-local-header-deflated-v20",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("504b03041400000008000000"),
            ),
            KnownPlaintextTemplate(
                name="zip-local-header-stored-v10",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("504b03040a00000000000000"),
            ),
            KnownPlaintextTemplate(
                name="zip-local-header-deflated-v10",
                plain_offset=0,
                plaintext_bytes=bytes.fromhex("504b03040a00000008000000"),
            ),
        ]
    raise ValueError(
        loc(
            locale,
            f"不支持的 KPA 模板: {template_name}。可用模板: {', '.join(KPA_TEMPLATE_CHOICES)}",
            f"Unsupported KPA template: {template_name}. Available templates: {', '.join(KPA_TEMPLATE_CHOICES)}",
        )
    )


def guess_kpa_template_for_entry_name(entry_name: str) -> Optional[str]:
    ext = os.path.splitext(entry_name)[1].lower()
    for template_name, extensions in KPA_TEMPLATE_ENTRY_EXTENSIONS.items():
        if ext in extensions:
            return template_name
    return None


def template_candidates_fit_entry_size(
    template_name: str,
    file_size: int,
    locale: str,
) -> bool:
    for candidate in build_kpa_template_candidates(template_name, locale):
        required_size = candidate.plain_offset + len(candidate.plaintext_bytes)
        for extra_offset, extra_bytes in candidate.extra_specs:
            required_size = max(required_size, extra_offset + len(extra_bytes))
        if file_size >= required_size:
            return True
    return False


def template_is_high_confidence_auto_suggestion(
    template_name: str,
    file_size: int,
    locale: str,
) -> bool:
    allowed_names = AUTO_TEMPLATE_CANDIDATE_NAMES.get(template_name, set())
    if not allowed_names:
        return False
    for candidate in build_kpa_template_candidates(template_name, locale):
        if candidate.name not in allowed_names:
            continue
        required_size = candidate.plain_offset + len(candidate.plaintext_bytes)
        for extra_offset, extra_bytes in candidate.extra_specs:
            required_size = max(required_size, extra_offset + len(extra_bytes))
        if file_size >= required_size:
            return True
    return False


def compress_type_label(compress_type: int) -> str:
    mapping = {
        zipfile.ZIP_STORED: "ZIP_STORED",
        zipfile.ZIP_DEFLATED: "ZIP_DEFLATED",
        zipfile.ZIP_BZIP2: "ZIP_BZIP2",
        zipfile.ZIP_LZMA: "ZIP_LZMA",
    }
    return mapping.get(compress_type, str(compress_type))


def shell_quote_for_display(value: str) -> str:
    if platform.system() == "Windows":
        if not value or any(ch.isspace() for ch in value) or '"' in value:
            return '"' + value.replace('"', '\\"') + '"'
        return value
    return shlex.quote(value)


def recommended_python_command() -> str:
    return "python" if platform.system() == "Windows" else "python3"


def format_template_kpa_recommendation_command(
    zip_path: str,
    suggestion: TemplateKpaSuggestion,
) -> str:
    script_name = os.path.basename(sys.argv[0]) or "ZipCracker.py"
    return " ".join(
        [
            recommended_python_command(),
            shell_quote_for_display(script_name),
            shell_quote_for_display(zip_path),
            "--kpa-template",
            suggestion.template_name,
            "-c",
            shell_quote_for_display(suggestion.inner_name),
        ]
    )


def template_is_worth_trying_auto_suggestion(
    template_name: str,
    file_size: int,
    locale: str,
) -> bool:
    if template_name not in ("png", "pcapng", "exe"):
        return False
    if template_name == "exe" and file_size < 128:
        return False
    return template_candidates_fit_entry_size(template_name, file_size, locale)


def detect_template_kpa_suggestions(
    zip_path: str,
    locale: str,
) -> list[TemplateKpaSuggestion]:
    suggestions: list[TemplateKpaSuggestion] = []
    with zipfile.ZipFile(zip_path, "r") as zf:
        infos = [info for info in zf.infolist() if is_regular_member(info)]
        for info in infos:
            if info.flag_bits & 0x1 == 0:
                continue
            if _has_winzip_aes_extra(info):
                continue
            template_name = guess_kpa_template_for_entry_name(info.filename)
            if not template_name:
                continue
            is_high_confidence = (
                info.compress_type == zipfile.ZIP_STORED
                and template_is_high_confidence_auto_suggestion(
                    template_name,
                    info.file_size,
                    locale,
                )
            )
            is_worth_trying = template_is_worth_trying_auto_suggestion(
                template_name,
                info.file_size,
                locale,
            )
            if not is_high_confidence and not is_worth_trying:
                continue
            if not template_candidates_fit_entry_size(template_name, info.file_size, locale):
                continue
            reason = (
                loc(
                    locale,
                    "文件后缀、大小与模板特征都比较吻合",
                    "The file extension, size, and template characteristics match well",
                )
                if is_high_confidence
                else loc(
                    locale,
                    "文件类型很像模板题型，虽然不是 ZIP_STORED，但仍值得试一次",
                    "The file type strongly resembles a template KPA case. It is not ZIP_STORED, but it is still worth trying once",
                )
            )
            suggestions.append(
                TemplateKpaSuggestion(
                    inner_name=info.filename,
                    template_name=template_name,
                    file_size=info.file_size,
                    compress_type=info.compress_type,
                    confidence="high" if is_high_confidence else "medium",
                    reason=reason,
                )
            )
    return suggestions


def offer_template_kpa_after_standard_failures(
    zip_path: str,
    out_dir: str,
    locale: str,
) -> bool:
    suggestions = detect_template_kpa_suggestions(zip_path, locale)
    if not suggestions:
        return False

    print(
        loc(
            locale,
            "[*] 常规路径未命中，但检测到压缩包里有条目很像模板化已知明文攻击场景。",
            "[*] Standard attacks did not succeed, but this archive contains entries that look suitable for template-based known-plaintext attacks.",
        )
    )
    for suggestion in suggestions:
        print(
            loc(
                locale,
                f"[*] 候选条目: {suggestion.inner_name} -> 模板 {suggestion.template_name}（文件大小 {suggestion.file_size} 字节，{compress_type_label(suggestion.compress_type)}，置信度 {suggestion.confidence}）",
                f"[*] Candidate entry: {suggestion.inner_name} -> template {suggestion.template_name} (size {suggestion.file_size} bytes, {compress_type_label(suggestion.compress_type)}, confidence {suggestion.confidence})",
            )
        )
        print(loc(locale, f"[*] 原因: {suggestion.reason}", f"[*] Reason: {suggestion.reason}"))
        print(
            loc(
                locale,
                f"[*] 检测到 {suggestion.inner_name} 很像 {suggestion.template_name} 模板题型，推荐尝试 --kpa-template {suggestion.template_name}",
                f"[*] {suggestion.inner_name} looks like a {suggestion.template_name} template KPA case. Recommended option: --kpa-template {suggestion.template_name}",
            )
        )
        print(
            loc(
                locale,
                f"[*] 推荐命令: {format_template_kpa_recommendation_command(zip_path, suggestion)}",
                f"[*] Recommended command: {format_template_kpa_recommendation_command(zip_path, suggestion)}",
            )
        )

    should_try = prompt_yes_no(
        locale,
        "[?] 是否现在自动尝试这些内置 KPA 模板？(y/n): ",
        "[?] Try these built-in KPA templates now? (y/n): ",
        env_name=ZIPCRACKER_ASSUME_TEMPLATE_KPA_ENV,
    )
    if not should_try:
        print(
            loc(
                locale,
                "[*] 已跳过模板化已知明文攻击。",
                "[*] Skipping template-based known-plaintext attacks.",
            )
        )
        return False

    bk_tool = find_bkcrack_executable()
    if not bk_tool:
        bk_tool = offer_bkcrack_install(locale, required=True)
        if not bk_tool:
            return False

    for index, suggestion in enumerate(suggestions, start=1):
        print(
            loc(
                locale,
                f"[*] 正在尝试模板候选 {index}/{len(suggestions)}: {suggestion.inner_name} -> {suggestion.template_name}",
                f"[*] Trying template candidate {index}/{len(suggestions)}: {suggestion.inner_name} -> {suggestion.template_name}",
            )
        )
        attempts: list[KnownPlaintextAttempt] = []
        try:
            _, attempts, _ = build_known_plaintext_attempts(
                zip_path,
                None,
                locale,
                preferred_entry=suggestion.inner_name,
                template_name=suggestion.template_name,
            )
            if run_bkcrack_known_plaintext_attempts(
                zip_path,
                attempts,
                out_dir,
                locale,
                bk_tool,
            ):
                return True
        finally:
            cleanup_known_plaintext_attempts(attempts)

    print(
        loc(
            locale,
            "[*] 内置 KPA 模板已全部尝试完毕，仍未恢复成功。",
            "[*] All built-in KPA templates were tried, but none recovered the archive.",
        )
    )
    return False


def read_zip_entry_raw_data(zip_path: str, inner_name: str) -> bytes:
    with zipfile.ZipFile(zip_path, "r") as zf:
        info = zf.getinfo(inner_name)
        off = getattr(info, "header_offset", None)
        if off is None:
            raise ValueError("ZIP 条目缺少 header_offset，请使用 Python 3.7+")
        with open(zip_path, "rb") as fp:
            fp.seek(off)
            local = fp.read(30)
            if len(local) < 30 or local[:4] != b"PK\x03\x04":
                raise ValueError("本地文件头损坏或无效")
            fn_len = struct.unpack("<H", local[26:28])[0]
            ex_len = struct.unpack("<H", local[28:30])[0]
            fp.seek(off + 30 + fn_len + ex_len)
            return fp.read(info.compress_size)


def read_zip_entry_ciphertext(zip_path: str, inner_name: str) -> bytes:
    with zipfile.ZipFile(zip_path, "r") as zf:
        info = zf.getinfo(inner_name)
        if info.flag_bits & 0x1 == 0:
            raise ValueError(f"条目 '{inner_name}' 未设置加密标志")
    return read_zip_entry_raw_data(zip_path, inner_name)


def resolve_plaintext_zip_entry_name(
    plain_zip_path: str,
    target_inner_name: str,
    locale: str,
) -> str:
    with zipfile.ZipFile(plain_zip_path, "r") as zf:
        infos = [info for info in zf.infolist() if is_regular_member(info)]
        if not infos:
            raise ValueError(
                loc(
                    locale,
                    f"明文 ZIP '{plain_zip_path}' 中未找到可用文件。",
                    f"No usable file entries were found in plaintext ZIP '{plain_zip_path}'.",
                )
            )
        encrypted_infos = [info.filename for info in infos if info.flag_bits & 0x1]
        if encrypted_infos:
            raise ValueError(
                loc(
                    locale,
                    f"明文 ZIP '{plain_zip_path}' 中的条目仍然是加密的，无法作为已知明文源：{encrypted_infos[0]}",
                    f"Plaintext ZIP '{plain_zip_path}' still contains encrypted entries and cannot be used as a known-plaintext source: {encrypted_infos[0]}",
                )
            )
        names = [info.filename for info in infos]
        if target_inner_name in names:
            return target_inner_name
        if len(names) == 1:
            return names[0]
        basename_matches = [
            name for name in names if os.path.basename(name) == os.path.basename(target_inner_name)
        ]
        if len(basename_matches) == 1:
            return basename_matches[0]
    raise ValueError(
        loc(
            locale,
            f"明文 ZIP '{plain_zip_path}' 中未找到与目标条目 '{target_inner_name}' 对应的文件。若 ZIP 内有多个文件，请确保存在同名条目，或改用单独明文文件。",
            f"Plaintext ZIP '{plain_zip_path}' does not contain a file that matches target entry '{target_inner_name}'. If the ZIP contains multiple files, make sure a matching entry exists or provide a standalone plaintext file instead.",
        )
    )


def list_encrypted_regular_entries(zip_path: str, locale: str) -> list[str]:
    with zipfile.ZipFile(zip_path, "r") as zf:
        infos = [info for info in zf.infolist() if is_regular_member(info)]
        encrypted_infos = [info for info in infos if info.flag_bits & 0x1]
    if not encrypted_infos:
        raise ValueError(
            loc(
                locale,
                f"压缩包 '{zip_path}' 中未找到可用于已知明文攻击的加密文件。",
                f"No encrypted file entry suitable for known-plaintext attack was found in '{zip_path}'.",
            )
        )
    return [info.filename for info in encrypted_infos]


def resolve_kpa_cipher_entry_from_template(
    zip_path: str,
    template_name: str,
    locale: str,
    *,
    preferred_entry: Optional[str] = None,
) -> str:
    encrypted_names = list_encrypted_regular_entries(zip_path, locale)
    if preferred_entry:
        if preferred_entry not in encrypted_names:
            raise ValueError(
                loc(
                    locale,
                    f"指定的 ZIP 条目 '{preferred_entry}' 不存在，或该条目未加密。",
                    f"The specified ZIP entry '{preferred_entry}' does not exist, or it is not encrypted.",
                )
            )
        return preferred_entry
    template = normalize_kpa_template_name(template_name)
    expected_extensions = KPA_TEMPLATE_ENTRY_EXTENSIONS.get(template, ())
    matches = [
        name
        for name in encrypted_names
        if os.path.splitext(name)[1].lower() in expected_extensions
    ]
    if len(matches) == 1:
        return matches[0]
    if len(encrypted_names) == 1:
        return encrypted_names[0]
    raise ValueError(
        loc(
            locale,
            f"无法根据模板 '{template}' 自动判断对应的加密条目。请使用 -c 显式指定 ZIP 内文件名。",
            f"Could not automatically determine which encrypted ZIP entry matches template '{template}'. Use -c to specify the entry name explicitly.",
        )
    )


def resolve_kpa_cipher_entry_without_source(
    zip_path: str,
    locale: str,
    *,
    preferred_entry: Optional[str] = None,
) -> str:
    encrypted_names = list_encrypted_regular_entries(zip_path, locale)
    if preferred_entry:
        if preferred_entry not in encrypted_names:
            raise ValueError(
                loc(
                    locale,
                    f"指定的 ZIP 条目 '{preferred_entry}' 不存在，或该条目未加密。",
                    f"The specified ZIP entry '{preferred_entry}' does not exist, or it is not encrypted.",
                )
            )
        return preferred_entry
    if len(encrypted_names) == 1:
        return encrypted_names[0]
    raise ValueError(
        loc(
            locale,
            "未提供明文文件时，无法自动判断对应的加密条目。请使用 -c 指定 ZIP 内文件名。",
            "Without a plaintext file, the script cannot determine the encrypted target entry automatically. Use -c to specify the ZIP entry.",
        )
    )


def resolve_kpa_cipher_entry_name(
    zip_path: str,
    plaintext_path: str,
    locale: str,
    *,
    preferred_entry: Optional[str] = None,
) -> str:
    encrypted_names = list_encrypted_regular_entries(zip_path, locale)

    if preferred_entry:
        if preferred_entry not in encrypted_names:
            raise ValueError(
                loc(
                    locale,
                    f"指定的 ZIP 条目 '{preferred_entry}' 不存在，或该条目未加密。",
                    f"The specified ZIP entry '{preferred_entry}' does not exist, or it is not encrypted.",
                )
            )
        return preferred_entry

    if is_zip_archive(plaintext_path):
        with zipfile.ZipFile(plaintext_path, "r") as zf:
            plain_infos = [info for info in zf.infolist() if is_regular_member(info)]
            plain_names = [info.filename for info in plain_infos if not (info.flag_bits & 0x1)]
        exact_matches = [name for name in plain_names if name in encrypted_names]
        if len(exact_matches) == 1:
            return exact_matches[0]
        if len(plain_names) == 1:
            plain_basename = os.path.basename(plain_names[0])
            basename_matches = [
                name for name in encrypted_names if os.path.basename(name) == plain_basename
            ]
            if len(basename_matches) == 1:
                return basename_matches[0]

    plain_basename = os.path.basename(plaintext_path)
    if plain_basename in encrypted_names:
        return plain_basename
    basename_matches = [
        name for name in encrypted_names if os.path.basename(name) == plain_basename
    ]
    if len(basename_matches) == 1:
        return basename_matches[0]
    if len(encrypted_names) == 1:
        return encrypted_names[0]
    raise ValueError(
        loc(
            locale,
            f"无法根据明文源 '{plaintext_path}' 自动判断对应的加密条目。请使用 -c 显式指定 ZIP 内文件名。",
            f"Could not automatically determine which encrypted ZIP entry matches plaintext source '{plaintext_path}'. Use -c to specify the entry name explicitly.",
        )
    )


def load_known_plaintext_source(
    target_zip_path: str,
    target_inner_name: str,
    plaintext_path: str,
    locale: str,
) -> dict:
    del target_zip_path
    if is_zip_archive(plaintext_path):
        plain_entry = resolve_plaintext_zip_entry_name(
            plaintext_path,
            target_inner_name,
            locale,
        )
        plaintext_bytes = read_zip_entry_raw_data(plaintext_path, plain_entry)
        return {
            "plaintext_bytes": plaintext_bytes,
            "source_kind": "zip",
            "source_path": plaintext_path,
            "source_entry": plain_entry,
            "display_path": f"{plaintext_path}:{plain_entry}",
        }

    with open(plaintext_path, "rb") as fp:
        plaintext_bytes = fp.read()
    return {
        "plaintext_bytes": plaintext_bytes,
        "source_kind": "file",
        "source_path": plaintext_path,
        "source_entry": "",
        "display_path": plaintext_path,
    }


def build_known_plaintext_attempts(
    zip_path: str,
    plaintext_path: Optional[str],
    locale: str,
    *,
    preferred_entry: Optional[str] = None,
    plain_offset: Optional[int] = None,
    extra_specs: Optional[Sequence[tuple[int, bytes]]] = None,
    template_name: Optional[str] = None,
) -> tuple[str, list[KnownPlaintextAttempt], bool]:
    extra_specs = list(extra_specs or [])
    partial_mode = plain_offset is not None or bool(extra_specs) or bool(template_name)

    if plaintext_path:
        inner_name = resolve_kpa_cipher_entry_name(
            zip_path,
            plaintext_path,
            locale,
            preferred_entry=preferred_entry,
        )
    elif template_name:
        inner_name = resolve_kpa_cipher_entry_from_template(
            zip_path,
            template_name,
            locale,
            preferred_entry=preferred_entry,
        )
    else:
        inner_name = resolve_kpa_cipher_entry_without_source(
            zip_path,
            locale,
            preferred_entry=preferred_entry,
        )

    if not partial_mode and plaintext_path:
        plain_source = load_known_plaintext_source(
            zip_path,
            inner_name,
            plaintext_path,
            locale,
        )
        return (
            inner_name,
            [
                KnownPlaintextAttempt(
                    inner_name=inner_name,
                    plain_source=plain_source,
                    plain_offset=0,
                    extra_specs=[],
                    label=plain_source["display_path"],
                )
            ],
            False,
        )

    attempts: list[KnownPlaintextAttempt] = []
    template_candidates = (
        build_kpa_template_candidates(template_name, locale)
        if template_name
        else [None]
    )

    if plaintext_path:
        base_source = load_known_plaintext_source(
            zip_path,
            inner_name,
            plaintext_path,
            locale,
        )
        for candidate in template_candidates:
            attempt_offset = plain_offset if plain_offset is not None else (
                candidate.plain_offset if candidate else 0
            )
            candidate_extras = list(candidate.extra_specs) if candidate else []
            attempt_extras = candidate_extras + extra_specs
            label = base_source["display_path"]
            if candidate:
                label = f"{label} + template:{candidate.name}"
            attempts.append(
                KnownPlaintextAttempt(
                    inner_name=inner_name,
                    plain_source=base_source,
                    plain_offset=attempt_offset,
                    extra_specs=attempt_extras,
                    label=label,
                    template_name=template_name,
                )
            )
    elif template_name:
        for candidate in template_candidates:
            temp_path = write_temp_known_plaintext_fragment(
                candidate.plaintext_bytes,
                suffix=f"_{candidate.name}.bin",
            )
            temp_source = {
                "plaintext_bytes": candidate.plaintext_bytes,
                "source_kind": "file",
                "source_path": temp_path,
                "source_entry": "",
                "display_path": f"template:{candidate.name}",
            }
            attempt_offset = plain_offset if plain_offset is not None else candidate.plain_offset
            attempt_extras = list(candidate.extra_specs) + extra_specs
            attempts.append(
                KnownPlaintextAttempt(
                    inner_name=inner_name,
                    plain_source=temp_source,
                    plain_offset=attempt_offset,
                    extra_specs=attempt_extras,
                    label=temp_source["display_path"],
                    cleanup_paths=[temp_path],
                    template_name=template_name,
                )
            )
    else:
        attempts.append(
            KnownPlaintextAttempt(
                inner_name=inner_name,
                plain_source=None,
                plain_offset=plain_offset or 0,
                extra_specs=extra_specs,
                label="extra-bytes-only",
            )
        )

    return inner_name, attempts, True


def cleanup_known_plaintext_attempts(attempts: Sequence[KnownPlaintextAttempt]) -> None:
    for attempt in attempts:
        for path in attempt.cleanup_paths:
            if os.path.exists(path):
                try:
                    os.remove(path)
                except OSError:
                    pass


def zipcrypto_plaintext_matches_password(
    password: str, ciphertext: bytes, expected_plaintext: bytes
) -> bool:
    decrypter = _ZipDecrypter(password.encode("utf-8"))
    plain = decrypter(ciphertext)
    return plain[12:] == expected_plaintext


def parse_bkcrack_keys_from_output(text: str) -> Optional[Tuple[str, str, str]]:
    triples = []
    for line in text.splitlines():
        if "Internal representation for password" in line:
            continue
        match = re.search(
            r"\b([0-9a-f]{8})\s+([0-9a-f]{8})\s+([0-9a-f]{8})\b",
            line,
            re.IGNORECASE,
        )
        if match:
            triples.append(
                (
                    match.group(1).lower(),
                    match.group(2).lower(),
                    match.group(3).lower(),
                )
            )
    return triples[-1] if triples else None


def parse_bkcrack_recovered_password(text: str) -> Optional[str]:
    for line in text.splitlines():
        stripped = line.strip()
        if stripped.lower().startswith("as text:"):
            return stripped.split(":", 1)[1].strip()
    match = re.search(r"^[REDACTED_SECRET])$", text, re.MULTILINE)
    if match:
        return match.group(1).strip()
    return None


def decompress_zip_member_data(info: zipfile.ZipInfo, data: bytes) -> bytes:
    if info.compress_type == zipfile.ZIP_STORED:
        return data
    if info.compress_type == zipfile.ZIP_DEFLATED:
        return zlib.decompress(data, -15)
    if info.compress_type == zipfile.ZIP_BZIP2:
        return bz2.decompress(data)
    if info.compress_type == zipfile.ZIP_LZMA:
        return lzma.decompress(data)
    raise NotImplementedError(
        f"Unsupported compression method for '{info.filename}': {info.compress_type}"
    )


def extract_with_bkcrack_keys(
    bk: str,
    keys: tuple[str, str, str],
    zip_path: str,
    out_dir: str,
    locale: str,
) -> tuple[bool, list[str] | str]:
    k0, k1, k2 = keys
    _clean_and_create_outdir(out_dir)
    extracted_names: list[str] = []

    try:
        with zipfile.ZipFile(zip_path, "r") as zf:
            infos = [info for info in zf.infolist() if is_regular_member(info)]
            for info in infos:
                dest_path = os.path.join(out_dir, info.filename)
                parent_dir = os.path.dirname(dest_path)
                if parent_dir:
                    os.makedirs(parent_dir, exist_ok=True)

                if info.flag_bits & 0x1:
                    with tempfile.NamedTemporaryFile(
                        prefix="zipcracker_bkcrack_entry_",
                        suffix=".bin",
                        delete=False,
                    ) as tmp:
                        tmp_path = tmp.name
                    try:
                        proc = subprocess.run(
                            [
                                bk,
                                "-k",
                                k0,
                                k1,
                                k2,
                                "-C",
                                zip_path,
                                "-c",
                                info.filename,
                                "-d",
                                tmp_path,
                            ],
                            capture_output=True,
                            text=True,
                            timeout=None,
                        )
                        if proc.returncode != 0:
                            detail = (proc.stderr or proc.stdout or "").strip()
                            return (
                                False,
                                loc(
                                    locale,
                                    f"bkcrack 无法导出条目 '{info.filename}' 的解密数据: {detail}",
                                    f"bkcrack could not export deciphered data for entry '{info.filename}': {detail}",
                                ),
                            )
                        with open(tmp_path, "rb") as fp:
                            compressed_data = fp.read()
                    finally:
                        if os.path.exists(tmp_path):
                            try:
                                os.remove(tmp_path)
                            except OSError:
                                pass
                    payload = decompress_zip_member_data(info, compressed_data)
                else:
                    payload = zf.read(info.filename)

                with open(dest_path, "wb") as output:
                    output.write(payload)
                extracted_names.append(info.filename)
    except Exception as exc:
        return False, str(exc)

    return True, extracted_names


def _find_first_file_in_zip(zf) -> Optional[str]:
    try:
        for info in zf.infolist():
            if is_regular_member(info):
                return info.filename
    except Exception:
        try:
            for name in zf.namelist():
                if not name.endswith("/"):
                    return name
        except Exception:
            return None
    return None


def find_best_verification_entry(zf) -> Optional[str]:
    try:
        infos = [info for info in zf.infolist() if is_regular_member(info)]
    except Exception:
        return _find_first_file_in_zip(zf)
    if not infos:
        return None

    encrypted_infos = [info for info in infos if info.flag_bits & 0x1]
    candidates = encrypted_infos or infos

    def score(info: zipfile.ZipInfo) -> Tuple[int, int, int, str]:
        non_empty_rank = 0 if info.file_size > 0 else 1
        return (
            non_empty_rank,
            info.file_size,
            info.compress_size,
            info.filename,
        )

    return min(candidates, key=score).filename


def _clean_and_create_outdir(out_dir: str) -> None:
    if os.path.exists(out_dir):
        try:
            shutil.rmtree(out_dir)
        except Exception:
            pass
    os.makedirs(out_dir, exist_ok=True)


def is_zip_encrypted(file_path: str) -> bool:
    with zipfile.ZipFile(file_path) as zf:
        for info in zf.infolist():
            if info.flag_bits & 0x1:
                return True
    return False


def fix_zip_encrypted(file_path: str, temp_path: str) -> None:
    with zipfile.ZipFile(file_path) as zf, zipfile.ZipFile(temp_path, "w") as temp_zf:
        for info in zf.infolist():
            original_flag_bits = info.flag_bits
            try:
                if info.flag_bits & 0x1:
                    info.flag_bits ^= 0x1
                clean_info = copy.copy(info)
                temp_zf.writestr(clean_info, zf.read(info.filename))
            finally:
                info.flag_bits = original_flag_bits


def crack_crc(filename: str, crc: int, size: int, locale: str) -> None:
    """短明文 CRC32 枚举恢复：穷举可打印明文，直至 binascii.crc32 与 ZIP 条目记录一致。"""
    candidates = its.product(string.printable, repeat=size)
    print(
        loc(
            locale,
            "[+] 开始进行短明文 CRC32 枚举恢复······",
            "[+] Running short-plaintext CRC32 enumeration recovery...",
        )
    )
    for item in candidates:
        raw = "".join(item).encode()
        if crc == binascii.crc32(raw):
            print(
                loc(
                    locale,
                    f"[*] 短明文 CRC32 枚举恢复成功。\n[*] {filename} 的内容为：{raw.decode()}",
                    f"[*] Short-plaintext CRC32 enumeration recovery succeeded.\n[*] Content of {filename}: {raw.decode()}",
                )
            )
            break


def get_crc(zip_file: str, zf: zipfile.ZipFile, locale: str) -> bool:
    """若存在 1～6 字节条目，询问是否执行短明文 CRC32 枚举恢复；全部条目均由此完成时跳过字典爆破。"""
    cracked_all = 0
    file_list = [name for name in zf.namelist() if not name.endswith("/")]
    if not file_list:
        return False

    for filename in file_list:
        info = zf.getinfo(filename)
        if 0 < info.file_size <= 6:
            should_try = prompt_yes_no(
                locale,
                f'[!] 压缩包 {zip_file} 中的 {filename} 为短明文（{info.file_size} 字节），是否进行短明文 CRC32 枚举恢复？（y/n）',
                f'[!] "{filename}" in "{zip_file}" is short plaintext ({info.file_size} bytes). Run short-plaintext CRC32 enumeration recovery? (y/n) ',
                env_name=ZIPCRACKER_ASSUME_CRC_RECOVERY_ENV,
            )
            if should_try:
                print(
                    loc(
                        locale,
                        f"[+] {filename} 在 ZIP 中记录的 CRC32：{info.CRC}",
                        f"[+] CRC32 stored in ZIP for {filename}: {info.CRC}",
                    )
                )
                crack_crc(filename, info.CRC, info.file_size, locale)
                cracked_all += 1

    if cracked_all >= len(file_list):
        print(
            loc(
                locale,
                f"[*] {zip_file} 内全部条目均已通过短明文 CRC32 枚举恢复，将跳过字典暴力破解。",
                f"[*] All entries in {zip_file} were recovered via short-plaintext CRC32 enumeration; skipping dictionary attack.",
            )
        )
        return True
    return False


def adjust_thread_count(max_limit: int = 128) -> int:
    raw = os.environ.get("ZIPCRACKER_THREADS", "").strip()
    if raw.isdigit():
        return max(1, min(int(raw), max_limit))
    try:
        cpu_count = multiprocessing.cpu_count() or 4
    except NotImplementedError:
        cpu_count = 4
    return max(1, min(max_limit, cpu_count * 4))


def bkcrack_job_count() -> int:
    raw = os.environ.get("BKCRACK_JOBS", "").strip()
    if raw.isdigit():
        return max(1, min(int(raw), 256))
    try:
        cpu_count = multiprocessing.cpu_count() or 4
    except NotImplementedError:
        cpu_count = 4
    return max(1, min(cpu_count * 2, 64))


def resolve_batch_size(max_workers: int) -> int:
    raw = os.environ.get("ZIPCRACKER_BATCH_SIZE", "").strip()
    if raw.isdigit():
        return max(128, min(int(raw), 65536))
    return max(1024, min(8192, max_workers * 128))


def resolve_prefetch_batches(max_workers: int) -> int:
    raw = os.environ.get("ZIPCRACKER_PREFETCH_BATCHES", "").strip()
    if raw.isdigit():
        return max(1, min(int(raw), 64))
    return max(2, min(16, max_workers))


def should_skip_dict_count() -> bool:
    raw = os.environ.get(ZIPCRACKER_SKIP_DICT_COUNT_ENV, "").strip().lower()
    return raw in ("1", "true", "yes", "on")


@dataclass(frozen=True)
class PasswordBatch:
    passwords: Sequence[str]
    source_bytes_read: int = 0
    source_bytes_total: int = 0
    source_label: str = ""


def count_passwords(file_path: str) -> int:
    total = 0
    last_byte = b""
    with open(file_path, "rb") as fp:
        while True:
            chunk = fp.read(STREAM_READ_CHUNK_SIZE)
            if not chunk:
                break
            total += chunk.count(b"\n")
            last_byte = chunk[-1:]
    if last_byte and last_byte != b"\n":
        total += 1
    return total


def iter_password_file_batches_with_progress(
    file_path: str, batch_size: int
) -> Iterator[PasswordBatch]:
    total_bytes = os.path.getsize(file_path)
    source_label = os.path.basename(file_path) or file_path
    with open(file_path, "rb") as fp:
        batch: list[str] = []
        pending = b""

        while True:
            chunk = fp.read(STREAM_READ_CHUNK_SIZE)
            if not chunk:
                break
            pending += chunk
            lines = pending.split(b"\n")
            pending = lines.pop() if lines else b""
            consumed_bytes = fp.tell() - len(pending)
            for raw_line in lines:
                batch.append(normalize_password_line(raw_line.decode("utf-8", errors="ignore")))
                if len(batch) >= batch_size:
                    yield PasswordBatch(
                        passwords=batch,
                        source_bytes_read=consumed_bytes,
                        source_bytes_total=total_bytes,
                        source_label=source_label,
                    )
                    batch = []

        if pending:
            batch.append(normalize_password_line(pending.decode("utf-8", errors="ignore")))
        if batch:
            yield PasswordBatch(
                passwords=batch,
                source_bytes_read=total_bytes,
                source_bytes_total=total_bytes,
                source_label=source_label,
            )


def iter_password_file_batches(file_path: str, batch_size: int) -> Iterator[list[str]]:
    for batch in iter_password_file_batches_with_progress(file_path, batch_size):
        yield list(batch.passwords)


def batched_iterable(values: Iterable[str], batch_size: int) -> Iterator[list[str]]:
    batch: list[str] = []
    for value in values:
        batch.append(value)
        if len(batch) >= batch_size:
            yield batch
            batch = []
    if batch:
        yield batch


def generate_numeric_passwords() -> Iterator[str]:
    for length in range(1, 7):
        for item in its.product(CHARSET_DIGITS, repeat=length):
            yield "".join(item)


def count_numeric_passwords() -> int:
    return sum(10**length for length in range(1, 7))


def parse_mask(mask: str) -> Tuple[list[Tuple[str, ...]], int]:
    tokens: list[Tuple[str, ...]] = []
    i = 0
    while i < len(mask):
        current = mask[i]
        if current == "?":
            if i + 1 < len(mask):
                placeholder = mask[i + 1]
                if placeholder == "d":
                    tokens.append(tuple(CHARSET_DIGITS))
                elif placeholder == "l":
                    tokens.append(tuple(CHARSET_LOWER))
                elif placeholder == "u":
                    tokens.append(tuple(CHARSET_UPPER))
                elif placeholder == "s":
                    tokens.append(tuple(CHARSET_SYMBOLS))
                elif placeholder == "?":
                    tokens.append(("?",))
                else:
                    tokens.append((mask[i : i + 2],))
                i += 2
            else:
                tokens.append(("?",))
                i += 1
        else:
            tokens.append((current,))
            i += 1

    total = 1
    for token_group in tokens:
        total *= max(1, len(token_group))
    if total == 0:
        total = 1
    return tokens, total


def prepare_kpa_context(
    zip_file: str, inner_name: str, plaintext_path: str, locale: str
) -> dict:
    with zipfile.ZipFile(zip_file, "r") as zf:
        info = zf.getinfo(inner_name)
        if info.flag_bits & 0x1 == 0:
            raise ValueError(
                loc(
                    locale,
                    "该条目未加密，无需已知明文攻击",
                    "This entry is not encrypted; known plaintext is unnecessary.",
                )
            )
        if _has_winzip_aes_extra(info):
            raise ValueError(
                loc(
                    locale,
                    "检测到 WinZip AES 加密；已知明文快速校验仅支持传统 ZipCrypto，请使用 AES 字典/掩码爆破或专用工具",
                    "WinZip AES detected; fast known-plaintext validation only supports legacy ZipCrypto. Use AES dictionary/mask attacks or a dedicated tool.",
                )
            )
        ciphertext = read_zip_entry_ciphertext(zip_file, inner_name)
        payload_len = len(ciphertext) - 12
        plain_source = load_known_plaintext_source(
            zip_file,
            inner_name,
            plaintext_path,
            locale,
        )
        plaintext_bytes = plain_source["plaintext_bytes"]
        if len(plaintext_bytes) != payload_len:
            raise ValueError(
                loc(
                    locale,
                    f"明文长度 ({len(plaintext_bytes)}) 与密文载荷长度 ({payload_len}) 不一致。当前明文来源: {plain_source['display_path']}。若传入的是 ZIP，请确保其中存在与目标条目对应的未加密文件；若传入的是普通文件，请确保它与加密头之后的压缩载荷等长。若你手里只有部分明文，可改用 --kpa-offset、-x/--kpa-extra 或 --kpa-template。",
                    f"Plaintext length ({len(plaintext_bytes)}) does not match ciphertext payload length ({payload_len}). Current plaintext source: {plain_source['display_path']}. If you supplied a ZIP, make sure it contains the matching unencrypted entry; if you supplied a regular file, it must match the encrypted payload after the 12-byte header. If you only have partial plaintext, try --kpa-offset, -x/--kpa-extra, or --kpa-template instead.",
                )
            )
    print(
        loc(
            locale,
            f"[+] 已知明文模式: 条目 '{inner_name}'，明文来源 '{plain_source['display_path']}'，明文载荷 {payload_len} 字节；将使用内存内 ZipCrypto 校验（比反复打开 ZIP 更快）。",
            f"[+] Known-plaintext mode: entry '{inner_name}', plaintext source '{plain_source['display_path']}', payload {payload_len} bytes; using in-memory ZipCrypto validation for faster checks.",
        )
    )
    return {
        "kpa_plaintext_bytes": plaintext_bytes,
        "kpa_ciphertext": ciphertext,
        "kpa_inner_name": inner_name,
    }


def try_fast_password_from_plaintext(
    ciphertext: bytes, plaintext_bytes: bytes
) -> Optional[str]:
    seen: set[str] = set()

    def try_one(password: str) -> Optional[str]:
        if password in seen:
            return None
        seen.add(password)
        if zipcrypto_plaintext_matches_password(password, ciphertext, plaintext_bytes):
            return password
        return None

    default_password_list = bundled_password_list_path()
    if default_password_list:
        try:
            for batch in iter_password_file_batches(default_password_list, 4096):
                for password in batch:
                    hit = try_one(password)
                    if hit is not None:
                        return hit
        except OSError:
            pass

    for password in COMMON_EXTRA_PASSWORDS:
        hit = try_one(password)
        if hit is not None:
            return hit

    for password in generate_numeric_passwords():
        hit = try_one(password)
        if hit is not None:
            return hit
    return None


def bkcrack_recover_original_password(
    bk: str,
    jobs: int,
    k0: str,
    k1: str,
    k2: str,
    zip_path: str,
    inner_name: str,
    locale: str,
) -> Optional[str]:
    skip = os.environ.get("ZIPCRACKER_SKIP_BKCRACK_PW", "").strip().lower()
    if skip in ("1", "true", "yes", "on"):
        return None

    timeout_cfg = os.environ.get("BKCRACK_PW_TIMEOUT_SEC", "300").strip().lower()
    if timeout_cfg in ("skip", "no"):
        return None
    if timeout_cfg in ("0", "inf", "none", ""):
        timeout = None
    else:
        try:
            timeout = float(timeout_cfg)
        except ValueError:
            timeout = 300.0

    range_spec = os.environ.get("BKCRACK_PW_RANGE", "4..24").strip() or "4..24"
    charset = os.environ.get("BKCRACK_PW_CHARSET", "?a").strip() or "?a"

    print(
        loc(
            locale,
            f"[*] 尝试用 bkcrack 从内部密钥反推原始 ZIP口令（-r {range_spec} {charset}，限时 {timeout or '无限制'} 秒；跳过设 ZIPCRACKER_SKIP_BKCRACK_PW=1，不限时设 BKCRACK_PW_TIMEOUT_SEC=0）…",
            f"[*] Trying bkcrack password recovery from internal keys (-r {range_spec} {charset}, timeout {timeout or 'unlimited'}s; skip with ZIPCRACKER_SKIP_BKCRACK_PW=1, unlimited with BKCRACK_PW_TIMEOUT_SEC=0)...",
        )
    )

    cmd = [
        bk,
        "-j",
        str(jobs),
        "-k",
        k0,
        k1,
        k2,
        "-r",
        range_spec,
        charset,
        "-C",
        zip_path,
        "-c",
        inner_name,
    ]
    log_path = None
    proc = None
    log = ""
    try:
        fd, log_path = tempfile.mkstemp(prefix="zipcracker_bkcrack_pw_", suffix=".log")
        os.close(fd)
        with open(log_path, "w", encoding="utf-8", errors="replace") as log_fp:
            proc = subprocess.run(
                cmd,
                stdout=log_fp,
                stderr=subprocess.STDOUT,
                timeout=timeout,
            )
        with open(log_path, "r", encoding="utf-8", errors="replace") as log_fp:
            log = log_fp.read()
    except subprocess.TimeoutExpired:
        print(
            loc(
                locale,
                "[!] bkcrack 反推口令超时。可调大 BKCRACK_PW_TIMEOUT_SEC、缩小 BKCRACK_PW_RANGE，或设 BKCRACK_PW_TIMEOUT_SEC=0 不限时。",
                "[!] bkcrack password recovery timed out. Increase BKCRACK_PW_TIMEOUT_SEC, shrink BKCRACK_PW_RANGE, or set BKCRACK_PW_TIMEOUT_SEC=0 for no timeout.",
            )
        )
        return None
    finally:
        if log_path and os.path.exists(log_path):
            try:
                os.remove(log_path)
            except OSError:
                pass

    if proc is None or proc.returncode != 0:
        return None
    return parse_bkcrack_recovered_password(log)


def report_original_zip_password_after_bkcrack(
    bk: str,
    jobs: int,
    k0: str,
    k1: str,
    k2: str,
    zip_path: str,
    inner_name: str,
    plaintext_path: str,
    locale: str,
) -> None:
    skip = os.environ.get(BKCRACK_SKIP_ORIG_PW_RECOVERY_ENV, "").strip().lower()
    if skip in ("1", "true", "yes", "on"):
        print(
            loc(
                locale,
                "[*] 已按环境变量跳过原始 ZIP 密码反推。",
                "[*] Skipping original ZIP password recovery because the environment variable requested it.",
            )
        )
        return

    try:
        ciphertext = read_zip_entry_ciphertext(zip_path, inner_name)
        plain_source = load_known_plaintext_source(
            zip_path,
            inner_name,
            plaintext_path,
            locale,
        )
        plaintext_bytes = plain_source["plaintext_bytes"]
    except (OSError, ValueError) as exc:
        print(
            loc(
                locale,
                f"[*] 无法读取密文/明文以反推口令: {exc}",
                f"[*] Unable to read ciphertext/plaintext for password recovery: {exc}",
            )
        )
        return

    print(
        loc(
            locale,
            f"[*] 已完成解压，接下来会继续尝试反推出原始 ZIP 密码；如只关心解压结果，可先设 {BKCRACK_SKIP_ORIG_PW_RECOVERY_ENV}=1 跳过这一步。",
            f"[*] Extraction is done. The script will now keep trying to recover the original ZIP password; if you only care about extraction, set {BKCRACK_SKIP_ORIG_PW_RECOVERY_ENV}=1 to skip this step.",
        )
    )
    print(
        loc(
            locale,
            "[*] 正在用已知明文快速验证常见口令（字典+弱口令+6位内数字）…",
            "[*] Validating common passwords with known plaintext (dictionary + weak passwords + up to 6 digits)...",
        )
    )
    [REDACTED_SECRET], plaintext_bytes)
    if [REDACTED_SECRET], f"\n[+] 原始 ZIP 密码为：{password}", f"\n[+] Original ZIP [REDACTED_SECRET]"))
        return

    recovered = bkcrack_recover_original_password(
        bk,
        jobs,
        k0,
        k1,
        k2,
        zip_path,
        inner_name,
        locale,
    )
    if recovered:
        print(loc(locale, f"\n[+] 原始 ZIP 密码为：{recovered}", f"\n[+] Original ZIP [REDACTED_SECRET]"))
        return

    zip_abs = os.path.abspath(zip_path)
    print(
        loc(
            locale,
            f"\n[*] 未能自动反推出原始口令（文件已用内部密钥解压）。可手动执行：\n    bkcrack -k {k0} {k1} {k2} -r 4..32 '?p' -j {jobs} -C \"{zip_abs}\" -c \"{inner_name}\"",
            f"\n[*] Could not automatically recover the original password (the archive has already been decrypted with the recovered keys). You can run:\n    bkcrack -k {k0} {k1} {k2} -r 4..32 '?p' -j {jobs} -C \"{zip_abs}\" -c \"{inner_name}\"",
        )
    )


def run_bkcrack_known_plaintext_attack(
    zip_path: str,
    inner_name: str,
    plaintext_path: str,
    out_dir: str,
    locale: str,
    bk_path: Optional[str] = None,
    *,
    plain_offset: int = 0,
    extra_specs: Optional[Sequence[tuple[int, bytes]]] = None,
    plain_source_override: Optional[dict] = None,
    attempt_label: str = "",
) -> bool:
    bk = bk_path or find_bkcrack_executable()
    if not bk:
        print(
            loc(
                locale,
                "[!] 未找到 bkcrack 可执行文件。已知明文密钥恢复请安装: https://github.com/kimci86/bkcrack",
                "[!] bkcrack executable not found. Install it for known-plaintext key recovery: https://github.com/kimci86/bkcrack",
            )
        )
        return False

    jobs = bkcrack_job_count()
    extra_specs = list(extra_specs or [])
    try:
        if plain_source_override is not None:
            plain_source = plain_source_override
        elif plaintext_path:
            plain_source = load_known_plaintext_source(
                zip_path,
                inner_name,
                plaintext_path,
                locale,
            )
        else:
            plain_source = None
    except (OSError, ValueError) as exc:
        print(
            loc(
                locale,
                f"[!] 无法准备 bkcrack 已知明文源: {exc}",
                f"[!] Failed to prepare the bkcrack known-plaintext source: {exc}",
            )
        )
        return False
    known_total, max_contig = merge_known_plaintext_ranges(
        plain_offset,
        len(plain_source["plaintext_bytes"]) if plain_source else 0,
        extra_specs,
    )
    if attempt_label:
        print(
            loc(
                locale,
                f"[*] 正在尝试 KPA 方案: {attempt_label}",
                f"[*] Trying KPA strategy: {attempt_label}",
            )
        )
    if plain_source:
        print(
            loc(
                locale,
                f"[*] bkcrack 已知明文源: {plain_source['display_path']}，偏移 {plain_offset}，连续已知 {len(plain_source['plaintext_bytes'])} 字节，累计已知 {known_total} 字节。",
                f"[*] bkcrack plaintext source: {plain_source['display_path']}, offset {plain_offset}, contiguous known bytes {len(plain_source['plaintext_bytes'])}, total known bytes {known_total}.",
            )
        )
    else:
        print(
            loc(
                locale,
                f"[*] 当前使用纯附加字节模式，累计已知 {known_total} 字节，最大连续片段 {max_contig} 字节。",
                f"[*] Using extra-bytes-only mode, with {known_total} known bytes total and a longest contiguous fragment of {max_contig} bytes.",
            )
        )
    if known_total < 12 or max_contig < 8:
        print(
            loc(
                locale,
                "[*] 提示: 已知字节通常至少需要 12 字节、且至少 8 字节连续；当前条件较弱，攻击可能直接失败。",
                "[*] Tip: known-plaintext attacks usually need at least 12 known bytes, including 8 contiguous bytes. The current hints are weak, so the attack may fail immediately.",
            )
        )
    print(
        loc(
            locale,
            f"[+] 正在调用 bkcrack 已知明文攻击（-j {jobs} 线程；大量进度输出写入临时文件，避免管道阻塞拖慢攻击）…",
            f"[+] Running bkcrack known-plaintext attack (-j {jobs}; verbose output goes to a temp file to avoid pipe backpressure)...",
        )
    )
    cmd_attack = [
        bk,
        "-j",
        str(jobs),
        "-C",
        zip_path,
        "-c",
        inner_name,
    ]
    if plain_source:
        if plain_source["source_kind"] == "zip":
            cmd_attack.extend(
                [
                    "-P",
                    plain_source["source_path"],
                    "-p",
                    plain_source["source_entry"],
                ]
            )
        else:
            cmd_attack.extend(["-p", plain_source["source_path"]])
        if plain_offset:
            cmd_attack.extend(["-o", str(plain_offset)])
    for extra_offset, extra_bytes in extra_specs:
        cmd_attack.extend(["-x", str(extra_offset), extra_bytes.hex()])
    log_path = None
    proc = None
    combined = ""
    try:
        fd, log_path = tempfile.mkstemp(prefix="zipcracker_bkcrack_", suffix=".log")
        os.close(fd)
        with open(log_path, "w", encoding="utf-8", errors="replace") as log_fp:
            proc = subprocess.run(
                cmd_attack,
                stdout=log_fp,
                stderr=subprocess.STDOUT,
                timeout=None,
            )
        with open(log_path, "r", encoding="utf-8", errors="replace") as log_fp:
            combined = log_fp.read()
    except FileNotFoundError:
        print(loc(locale, "[!] 无法执行 bkcrack。", "[!] Unable to execute bkcrack."))
        return False
    finally:
        if log_path and os.path.exists(log_path):
            try:
                os.remove(log_path)
            except OSError:
                pass

    if proc is None:
        print(
            loc(
                locale,
                "[!] bkcrack 子进程未正常结束。",
                "[!] bkcrack subprocess did not finish correctly.",
            )
        )
        return False
    if proc.returncode != 0:
        print(combined)
        for line in describe_bkcrack_runtime_failure(
            locale,
            proc.returncode,
            combined,
        ):
            print(line)
        return False

    keys = parse_bkcrack_keys_from_output(combined)
    if not keys:
        print(combined)
        print(
            loc(
                locale,
                "[!] 未能从 bkcrack 输出中解析出三组密钥。",
                "[!] Failed to parse the recovered internal keys from bkcrack output.",
            )
        )
        return False

    k0, k1, k2 = keys
    key_tuple = (k0, k1, k2)
    print(
        loc(
            locale,
            f"[+] 已从已知明文恢复内部密钥: {k0} {k1} {k2}",
            f"[+] Recovered internal keys from known plaintext: {k0} {k1} {k2}",
        )
    )
    decrypted_zip = zip_path + ".bkcrack_decrypted.zip"
    try:
        subprocess.run(
            [
                bk,
                "-j",
                str(jobs),
                "-k",
                k0,
                k1,
                k2,
                "-D",
                decrypted_zip,
                "-C",
                zip_path,
            ],
            capture_output=True,
            text=True,
                check=True,
        )
    except subprocess.CalledProcessError as exc:
        print(exc.stderr or exc.stdout or str(exc))
        print(
            loc(
                locale,
                "[!] 使用 bkcrack 写出整包解密副本失败，正在尝试逐条目解密并直接提取文件...",
                "[!] Failed to write a fully decrypted archive with bkcrack. Falling back to per-entry decryption and direct extraction...",
            )
        )
        ok, result = extract_with_bkcrack_keys(
            bk,
            key_tuple,
            zip_path,
            out_dir,
            locale,
        )
        if not ok:
            print(
                loc(
                    locale,
                    f"[!] 逐条目解密提取也失败了: {result}",
                    f"[!] Per-entry decryption and extraction also failed: {result}",
                )
            )
            return False
        names = result
        print(
            loc(
                locale,
                f"\n[*] 已知明文攻击成功，已通过逐条目解密提取到 '{out_dir}': {names}",
                f"\n[*] Known-plaintext attack succeeded. Files were extracted via per-entry decryption to '{out_dir}': {names}",
            )
        )
    else:
        _clean_and_create_outdir(out_dir)
        try:
            with zipfile.ZipFile(decrypted_zip) as zf:
                zf.extractall(path=out_dir)
                names = zf.namelist()
            print(
                loc(
                    locale,
                    f"\n[*] 已知明文攻击成功，已解压到 '{out_dir}': {names}",
                    f"\n[*] Known-plaintext attack succeeded. Extracted to '{out_dir}': {names}",
                )
            )
        except Exception as exc:
            print(
                loc(
                    locale,
                    f"[!] 解压 bkcrack 解密副本失败: {exc}",
                    f"[!] Failed to extract the bkcrack-decrypted copy: {exc}",
                )
            )
            return False
    finally:
        if os.path.exists(decrypted_zip):
            try:
                os.remove(decrypted_zip)
            except OSError:
                pass
    if plain_source and plain_offset == 0 and not extra_specs:
        report_original_zip_password_after_bkcrack(
            bk,
            jobs,
            k0,
            k1,
            k2,
            zip_path,
            inner_name,
            plaintext_path,
            locale,
        )
    else:
        print(
            loc(
                locale,
                "[*] 当前恢复使用了部分明文 / 模板 / 附加字节，跳过原始 ZIP 密码反推。",
                "[*] The current recovery used partial plaintext, templates, or extra bytes, so original ZIP password recovery is being skipped.",
            )
        )
    return True


def run_bkcrack_known_plaintext_attempts(
    zip_path: str,
    attempts: Sequence[KnownPlaintextAttempt],
    out_dir: str,
    locale: str,
    bk_path: Optional[str] = None,
) -> bool:
    bk = bk_path or find_bkcrack_executable()
    if not bk:
        print(
            loc(
                locale,
                "[!] 未找到 bkcrack 可执行文件。已知明文密钥恢复请安装: https://github.com/kimci86/bkcrack",
                "[!] bkcrack executable not found. Install it for known-plaintext key recovery: https://github.com/kimci86/bkcrack",
            )
        )
        return False

    total = len(attempts)
    for index, attempt in enumerate(attempts, start=1):
        if total > 1:
            print(
                loc(
                    locale,
                    f"[*] 已知明文模板正在尝试第 {index}/{total} 组候选...",
                    f"[*] Trying known-plaintext template candidate {index}/{total}...",
                )
            )
        if run_bkcrack_known_plaintext_attack(
            zip_path,
            attempt.inner_name,
            attempt.plain_source["source_path"] if attempt.plain_source else "",
            out_dir,
            locale,
            bk,
            plain_offset=attempt.plain_offset,
            extra_specs=attempt.extra_specs,
            plain_source_override=attempt.plain_source,
            attempt_label=attempt.label,
        ):
            return True
        if total > 1 and index < total:
            print(
                loc(
                    locale,
                    "[*] 当前候选未恢复成功，继续尝试下一组已知明文参数。",
                    "[*] The current candidate did not recover the archive. Trying the next known-plaintext candidate.",
                )
            )
    return False


class PasswordVerifier:
    def __init__(
        self,
        zip_file: str,
        *,
        kpa_ciphertext: Optional[bytes] = None,
        kpa_plaintext_bytes: Optional[bytes] = None,
        kpa_inner_name: Optional[str] = None,
    ) -> None:
        self.zip_file = zip_file
        self.kpa_ciphertext = kpa_ciphertext
        self.kpa_plaintext_bytes = kpa_plaintext_bytes
        self.kpa_inner_name = kpa_inner_name
        self._thread_local = threading.local()

        with zipfile.ZipFile(zip_file, "r") as zf:
            self.verification_entry = find_best_verification_entry(zf)
            self.archive_names = zf.namelist()

    def _open_archive(self):
        if HAS_PYZIPPER:
            return pyzipper.AESZipFile(self.zip_file, "r")
        return zipfile.ZipFile(self.zip_file, "r")

    def _get_thread_archive(self):
        archive = getattr(self._thread_local, "archive", None)
        if archive is None:
            archive = self._open_archive()
            self._thread_local.archive = archive
        return archive

    def close_thread_archive(self) -> None:
        archive = getattr(self._thread_local, "archive", None)
        if archive is not None:
            try:
                archive.close()
            except Exception:
                pass
            self._thread_local.archive = None

    def reset_thread_archive(self) -> None:
        self.close_thread_archive()

    def verify_password(self, password: str) -> bool:
        if self.kpa_ciphertext is not None and self.kpa_plaintext_bytes is not None:
            return zipcrypto_plaintext_matches_password(
                password, self.kpa_ciphertext, self.kpa_plaintext_bytes
            )

        password_bytes = password.encode("utf-8")
        try:
            archive = self._get_thread_archive()
            if self.verification_entry:
                archive.read(self.verification_entry, [REDACTED_SECRET])
            else:
                archive.testzip([REDACTED_SECRET])
            return True
        except RuntimeError:
            return False
        except KeyboardInterrupt:
            raise
        except Exception:
            self.reset_thread_archive()
            return False

    def extract(self, password: str, out_dir: str) -> list[str]:
        _clean_and_create_outdir(out_dir)
        password_bytes = password.encode("utf-8")
        with self._open_archive() as zf:
            zf.extractall(path=out_dir, [REDACTED_SECRET])
            return zf.namelist()


@dataclass
class ProgressState:
    total_passwords: int = 0
    attempted_passwords: int = 0
    current_password: str = ""
    found_password: Optional[str] = None
    source_label: str = ""
    source_bytes_total: int = 0
    source_bytes_read: int = 0
    stop_event: threading.Event = field(default_factory=threading.Event)
    finished_event: threading.Event = field(default_factory=threading.Event)
    lock: threading.Lock = field(default_factory=threading.Lock)

    def reset(
        self,
        total_passwords: int,
        *,
        source_label: str = "",
        source_bytes_total: int = 0,
    ) -> None:
        with self.lock:
            self.total_passwords = total_passwords
            self.attempted_passwords = 0
            self.current_password = ""
            self.found_password = None
            self.source_label = source_label
            self.source_bytes_total = source_bytes_total
            self.source_bytes_read = 0
        self.stop_event.clear()
        self.finished_event.clear()

    def record_attempts(self, attempted: int, last_password: str) -> None:
        with self.lock:
            self.attempted_passwords += attempted
            self.current_password = last_password

    def update_source_progress(
        self,
        *,
        source_bytes_read: int,
        source_bytes_total: int = 0,
        source_label: str = "",
    ) -> None:
        with self.lock:
            if source_label:
                self.source_label = source_label
            if source_bytes_total > 0:
                self.source_bytes_total = source_bytes_total
            if source_bytes_read >= self.source_bytes_read:
                self.source_bytes_read = source_bytes_read

    def mark_success(self, password: str) -> bool:
        with self.lock:
            if self.found_password is not None:
                return False
            self.found_password = password
            self.current_password = password
        self.stop_event.set()
        return True


def format_current_password(password: str, max_chars: int = 48) -> str:
    if len(password) <= max_chars:
        return password
    return password[: max_chars - 3] + "..."


def display_progress(locale: str, state: ProgressState, start_time: float) -> None:
    while not state.finished_event.is_set():
        time.sleep(0.1)
        with state.lock:
            attempted = state.attempted_passwords
            total = state.total_passwords
            current_password = format_current_password(state.current_password)
            source_label = state.source_label
            source_bytes_total = state.source_bytes_total
            source_bytes_read = state.source_bytes_read

        elapsed = max(time.time() - start_time, 1e-9)
        speed = int(attempted / elapsed)
        if total > 0:
            progress = min(100.0, attempted / total * 100)
            remaining = max(total - attempted, 0)
            remain_seconds = remaining / speed if speed > 0 else 0
            remain_text = time.strftime("%H:%M:%S", time.gmtime(remain_seconds))
            line = loc(
                locale,
                f"\r[-] 当前破解进度：{progress:.2f}%，剩余时间：{remain_text}，当前时速：{speed}个/s，正在尝试密码:{current_password:<20}",
                f"\r[-] Progress: {progress:.2f}%, Time Left: {remain_text}, Speed: {speed} pass/s, Trying: {current_password:<20}",
            )
        elif source_bytes_total > 0:
            progress = min(100.0, source_bytes_read / max(source_bytes_total, 1) * 100)
            byte_speed = source_bytes_read / elapsed
            remain_seconds = (
                max(source_bytes_total - source_bytes_read, 0) / byte_speed
                if byte_speed > 0
                else 0
            )
            remain_text = time.strftime("%H:%M:%S", time.gmtime(remain_seconds))
            label_text = source_label or loc(locale, "字典", "dictionary")
            line = loc(
                locale,
                f"\r[-] 流式进度：{progress:.2f}%（{format_bytes(source_bytes_read)}/{format_bytes(source_bytes_total)}，{label_text}），剩余时间：{remain_text}，当前时速：{speed}个/s，文件吞吐：{format_bytes(byte_speed)}/s，正在尝试密码:{current_password:<20}",
                f"\r[-] Streaming progress: {progress:.2f}% ({format_bytes(source_bytes_read)}/{format_bytes(source_bytes_total)}, {label_text}), Time Left: {remain_text}, Speed: {speed} pass/s, File Throughput: {format_bytes(byte_speed)}/s, Trying: {current_password:<20}",
            )
        else:
            line = loc(
                locale,
                f"\r[-] 流式模式：已尝试 {attempted} 个密码，当前时速：{speed}个/s，正在尝试密码:{current_password:<20}",
                f"\r[-] Streaming mode: {attempted} passwords tried, Speed: {speed} pass/s, Trying: {current_password:<20}",
            )

        print(line, end="", flush=True)


def run_parallel_passwords(
    verifier: PasswordVerifier,
    password_batches: Iterable[Sequence[str] | PasswordBatch],
    total_passwords: int,
    out_dir: str,
    locale: str,
    max_workers: int,
    *,
    source_label: str = "",
    source_bytes_total: int = 0,
) -> bool:
    progress = ProgressState()
    progress.reset(
        total_passwords,
        source_label=source_label,
        source_bytes_total=source_bytes_total,
    )
    start_time = time.time()
    display_thread = threading.Thread(
        target=display_progress,
        args=(locale, progress, start_time),
        daemon=True,
    )
    display_thread.start()

    task_queue: queue.Queue[Optional[Sequence[str]]] = queue.Queue(
        maxsize=resolve_prefetch_batches(max_workers)
    )
    workers: list[threading.Thread] = []

    def worker() -> None:
        try:
            while True:
                batch = task_queue.get()
                try:
                    if batch is None:
                        break
                    passwords = batch.passwords if isinstance(batch, PasswordBatch) else batch
                    attempted = 0
                    last_password = ""
                    for password in passwords:
                        if progress.stop_event.is_set():
                            break
                        attempted += 1
                        last_password = password
                        if verifier.verify_password(password):
                            progress.mark_success(password)
                            break
                        if attempted >= 256:
                            progress.record_attempts(attempted, last_password)
                            attempted = 0
                    if attempted:
                        progress.record_attempts(attempted, last_password)
                finally:
                    task_queue.task_done()
        finally:
            verifier.close_thread_archive()

    for _ in range(max_workers):
        thread = threading.Thread(target=worker, daemon=True)
        thread.start()
        workers.append(thread)

    producer_error: Optional[BaseException] = None
    try:
        for batch in password_batches:
            if progress.stop_event.is_set():
                break
            passwords = batch.passwords if isinstance(batch, PasswordBatch) else batch
            if isinstance(batch, PasswordBatch):
                progress.update_source_progress(
                    source_bytes_read=batch.source_bytes_read,
                    source_bytes_total=batch.source_bytes_total,
                    source_label=batch.source_label,
                )
            if passwords:
                task_queue.put(batch)
    except BaseException as exc:
        producer_error = exc
    finally:
        if source_bytes_total > 0:
            progress.update_source_progress(
                source_bytes_read=source_bytes_total,
                source_bytes_total=source_bytes_total,
                source_label=source_label,
            )
        for _ in workers:
            task_queue.put(None)
        task_queue.join()
        for thread in workers:
            thread.join()
        progress.finished_event.set()
        display_thread.join(timeout=0.2)

    print()

    if producer_error is not None:
        raise producer_error

    if progress.found_password:
        print(
            loc(
                locale,
                f"\n[+] 恭喜您！密码破解成功, 该压缩包的密码为：{progress.found_password}",
                f"\n[+] Success! The password is: {progress.found_password}",
            )
        )
        try:
            names = verifier.extract(progress.found_password, out_dir)
            print(
                loc(
                    locale,
                    f"\n[*] 系统已为您自动提取出 {len(names)} 个文件到 '{out_dir}' 文件夹中: {names}",
                    f"\n[*] Successfully extracted {len(names)} file(s) to '{out_dir}': {names}",
                )
            )
        except Exception as exc:
            print(
                loc(
                    locale,
                    f"\n[!] 密码正确，但解压文件时发生错误: {exc}",
                    f"\n[!] Password is correct, but extraction failed: {exc}",
                )
            )
        return True
    return False


def crack_password_with_mask(
    zip_file: str,
    mask: str,
    verifier: PasswordVerifier,
    locale: str,
    out_dir: str,
) -> bool:
    token_groups, total_passwords = parse_mask(mask)
    if total_passwords > 100_000_000_000:
        should_continue = prompt_yes_no(
            locale,
            f"[!]警告：掩码 '{mask}' 将生成 {total_passwords:,} 种组合，可能需要极长时间。是否继续？ (y/n): ",
            f"[!] Warning: The mask '{mask}' will generate {total_passwords:,} combinations, which may take a very long time. Continue? (y/n): ",
            env_name=ZIPCRACKER_ASSUME_LARGE_MASK_ENV,
        )
        if not should_continue:
            print(loc(locale, "[-] 用户已中止攻击。", "[-] Attack aborted by user."))
            return False

    print(
        loc(
            locale,
            f"\n[+] 开始使用掩码 '{mask}' 进行攻击。",
            f"\n[+] Starting attack with mask '{mask}'.",
        )
    )
    print(
        loc(
            locale,
            f"[+] 需要尝试的密码总数组合为: {total_passwords:,}",
            f"[+] Total password combinations to try: {total_passwords:,}",
        )
    )
    max_threads = adjust_thread_count()
    print(
        loc(
            locale,
            f"[+] 动态调整线程数为: {max_threads}个",
            f"[+] Dynamically adjusted thread count to: {max_threads}",
        )
    )

    password_iter = ("".join(parts) for parts in its.product(*token_groups))
    found = run_parallel_passwords(
        verifier,
        batched_iterable(password_iter, resolve_batch_size(max_threads)),
        total_passwords,
        out_dir,
        locale,
        max_threads,
    )
    if not found:
        print(
            loc(
                locale,
                "\n[-] 非常抱歉，掩码生成的所有密码均已尝试，请检查您的掩码或尝试其他方法！",
                "\n[-] Sorry, all passwords generated by the mask have been tried. Please check your mask or try another method.",
            )
        )
    return found


def crack_password_with_file(
    zip_file: str,
    dict_file: str,
    verifier: PasswordVerifier,
    locale: str,
    out_dir: str,
) -> bool:
    total_passwords = 0
    try:
        dict_size_bytes = os.path.getsize(dict_file)
    except OSError:
        dict_size_bytes = 0
    if not should_skip_dict_count():
        try:
            total_passwords = count_passwords(dict_file)
        except OSError as exc:
            print(
                loc(
                    locale,
                    f"[!] 加载字典文件失败，原因：{exc}",
                    f"[!] Failed to load dictionary file: {exc}",
                )
            )
            return False

    dict_type = (
        loc(locale, "内置字典", "Built-in Dictionary")
        if os.path.basename(dict_file) == "password_list.txt"
        else loc(locale, "用户自定义字典", "Custom Dictionary")
    )
    print(
        loc(
            locale,
            f"\n[+] 加载{dict_type}[{dict_file}]成功！",
            f"\n[+] Successfully loaded {dict_type} [{dict_file}]!",
        )
    )
    if total_passwords > 0:
        print(
            loc(
                locale,
                f"[+] 当前字典总密码数: {total_passwords}",
                f"[+] Total passwords in current dictionary: {total_passwords}",
            )
        )
    else:
        print(
            loc(
                locale,
                f"[*] 已跳过字典总数预统计（设置 {ZIPCRACKER_SKIP_DICT_COUNT_ENV}=1），将直接开始流式爆破。",
                f"[*] Skipping the upfront dictionary line count ({ZIPCRACKER_SKIP_DICT_COUNT_ENV}=1). Starting the streaming attack immediately.",
            )
        )
        if dict_size_bytes > 0:
            print(
                loc(
                    locale,
                    f"[*] 当前字典文件大小: {format_bytes(dict_size_bytes)}；进度条将按文件读取进度实时显示。",
                    f"[*] Current dictionary size: {format_bytes(dict_size_bytes)}. Progress will be shown by streamed file read position.",
                )
            )

    max_threads = adjust_thread_count()
    print(
        loc(
            locale,
            f"[+] 动态调整线程数为: {max_threads}个",
            f"[+] Dynamically adjusted thread count to: {max_threads}",
        )
    )
    try:
        found = run_parallel_passwords(
            verifier,
            iter_password_file_batches_with_progress(
                dict_file,
                resolve_batch_size(max_threads),
            ),
            total_passwords,
            out_dir,
            locale,
            max_threads,
            source_label=os.path.basename(dict_file) or dict_file,
            source_bytes_total=dict_size_bytes,
        )
    except OSError as exc:
        print(
            loc(
                locale,
                f"[!] 加载字典文件失败，原因：{exc}",
                f"[!] Failed to load dictionary file: {exc}",
            )
        )
        return False

    if not found:
        print(
            loc(
                locale,
                f"\n[-] 非常抱歉，字典 {dict_file} 中的所有密码均已尝试完毕。",
                f"\n[-] Sorry, all passwords in the dictionary {dict_file} have been tried.",
            )
        )
    return found


def crack_password_with_file_or_dir(
    zip_file: str,
    dict_file_or_dir: str,
    verifier: PasswordVerifier,
    locale: str,
    out_dir: str,
) -> bool:
    if os.path.isdir(dict_file_or_dir):
        for filename in sorted(os.listdir(dict_file_or_dir)):
            file_path = os.path.join(dict_file_or_dir, filename)
            if crack_password_with_file_or_dir(
                zip_file, file_path, verifier, locale, out_dir
            ):
                return True
        return False

    if os.path.isfile(dict_file_or_dir):
        return crack_password_with_file(
            zip_file,
            dict_file_or_dir,
            verifier,
            locale,
            out_dir,
        )

    print(
        loc(
            locale,
            f"[!] 字典路径无效或文件不存在: {dict_file_or_dir!r}，请检查路径。",
            f"[!] Invalid dictionary path or file not found: {dict_file_or_dir!r}.",
        )
    )
    return False


def crack_with_generated_numeric_dict(
    zip_file: str,
    verifier: PasswordVerifier,
    locale: str,
    out_dir: str,
) -> bool:
    print(
        loc(
            locale,
            "\n[-] 内置字典破解失败或未找到，开始尝试1-6位纯数字字典...",
            "\n[-] Built-in dictionary failed or was not found. Trying the 1-6 digit numeric dictionary...",
        )
    )
    total_passwords = count_numeric_passwords()
    print(
        loc(
            locale,
            f"\n[+] 加载1-6位纯数字字典成功！总密码数: {total_passwords}",
            f"\n[+] Loaded the 1-6 digit numeric dictionary successfully! Total passwords: {total_passwords}",
        )
    )

    max_threads = adjust_thread_count()
    print(
        loc(
            locale,
            f"[+] 动态调整线程数为: {max_threads}个",
            f"[+] Dynamically adjusted thread count to: {max_threads}",
        )
    )

    found = run_parallel_passwords(
        verifier,
        batched_iterable(generate_numeric_passwords(), resolve_batch_size(max_threads)),
        total_passwords,
        out_dir,
        locale,
        max_threads,
    )
    if not found:
        print(
            loc(
                locale,
                "\n[-] 非常抱歉，1-6位纯数字字典中的所有密码均已尝试完毕。",
                "\n[-] Sorry, all passwords from the 1-6 digit numeric dictionary have been tried.",
            )
        )
    return found


def print_banner(locale: str) -> None:
    raw_print(
        loc(
            locale,
            r"""                          
     ______          ____                _   [*]Hx0战队      
    |__  (_)_ __    / ___|_ __ __ _  ___| | _____ _ __ 
      / /| | '_ \  | |   | '__/ _ |/ __| |/ / _ \ '__|
     / /_| | |_) | | |___| | | (_| | (__|   <  __/ |   
    /____|_| .__/___\____|_|  \__,_|\___|_|\_\___|_|   
           |_| |_____|                                 
    #Coded By Asaotomo         Update:2026.04.10 (Core Engine Refactor)
            """,
            r"""                          
     ______          ____                _   [*]Hx0 Team
    |__  (_)_ __    / ___|_ __ __ _  ___| | _____ _ __ 
      / /| | '_ \  | |   | '__/ _ |/ __| |/ / _ \ '__|
     / /_| | |_) | | |___| | | (_| | (__|   <  __/ |   
    /____|_| .__/___\____|_|  \__,_|\___|_|\_\___|_|   
           |_| |_____|                                 
    #Coded By Asaotomo         Update:2026.04.10 (Core Engine Refactor)
            """,
        )
    )


def print_version() -> None:
    raw_print(f"ZipCracker {ZIPCRACKER_VERSION}")


def print_usage(locale: str, script_name: str) -> None:
    if locale == "en":
        raw_print("\n--- Dictionary Attack ---")
        raw_print(f"[*] Usage 1 (Default Sequence): python {script_name} YourZipFile.zip")
        raw_print("         └─ Default order: Tries 'password_list.txt' first, then 1-6 digit numbers.")
        raw_print(f"[*] Usage 2 (Custom Dictionary): python {script_name} YourZipFile.zip YourDict.txt")
        raw_print(f"[*] Usage 3 (Dictionary Directory): python {script_name} YourZipFile.zip YourDictDirectory")
        raw_print("\n--- Mask Attack ---")
        raw_print(f"[*] Usage 4 (Mask): python {script_name} YourZipFile.zip -m 'your?dmask?l'")
        raw_print("[*]  ?d: digits, ?l: lowercase, ?u: uppercase, ?s: symbols, ??: literal '?'")
        raw_print("\n--- Known Plaintext (ZipCrypto) ---")
        raw_print(f"[*] Usage 5 (KPA): python {script_name} enc.zip -kpa plain.txt [ -c path/in/zip ] [dictionary or -m mask]")
        raw_print("         └─ If bkcrack is on PATH, the script first tries dictionary-free recovery automatically.")
        raw_print("         └─ If bkcrack is missing, the script will show OS-specific installation guidance and can offer a one-click local install.")
        raw_print(f"[*] Usage 6 (Partial KPA): python {script_name} enc.zip -kpa part.bin --kpa-offset 78 -x 0 4d5a")
        raw_print(f"[*] Usage 7 (Template KPA): python {script_name} enc.zip --kpa-template png [ -c image.png ]")
        raw_print(f"[*] Usage 8 (bkcrack only): python {script_name} enc.zip -kpa plain.txt [ -c inner ] --bkcrack")
        raw_print("\n--- Optional Arguments ---")
        raw_print(f"[*] Specify Output Directory: python {script_name} ... -o YourOutDir")
        raw_print("[*] KPA offset: --kpa-offset 78")
        raw_print("[*] KPA extra bytes: -x 0 4d5a  (repeatable; also accepts 0:4d5a)")
        raw_print(f"[*] KPA templates: --kpa-template {' | '.join(KPA_TEMPLATE_CHOICES)}")
        raw_print(f"[*] Help / Version: python {script_name} --help | --version")
        return

    raw_print("\n--- 字典攻击 ---")
    raw_print(f"[*] 用法1(内置序列): python {script_name} YourZipFile.zip")
    raw_print("         └─ 默认顺序: 先尝试 password_list.txt 文件, 再尝试1-6位纯数字。")
    raw_print(f"[*] 用法2(自定义字典): python {script_name} YourZipFile.zip YourDict.txt")
    raw_print(f"[*] 用法3(字典目录):   python {script_name} YourZipFile.zip YourDictDirectory")
    raw_print("\n--- 掩码攻击 ---")
    raw_print(f"[*] 用法4(掩码):      python {script_name} YourZipFile.zip -m 'your?dmask?l'")
    raw_print("[*]  ?d: 数字, ?l: 小写字母, ?u: 大写字母, ?s: 特殊符号, ??: 问号自身")
    raw_print("\n--- 已知明文 (ZipCrypto) ---")
    raw_print(f"[*] 用法5(KPA):  python {script_name} enc.zip -kpa plain.txt [ -c path/in/zip ] [字典或 -m 掩码]")
    raw_print("         └─ 若系统 PATH 中有 bkcrack，会先自动尝试无字典恢复；失败则继续字典/掩码（无交互）。")
    raw_print("         └─ 若未检测到 bkcrack，脚本会按当前操作系统给出安装方式，并可交互式执行一键本地安装。")
    raw_print("         └─ 明文须与加密流中 12 字节头之后的载荷一致（STORED 时常与解压后内容相同）。")
    raw_print(f"[*] 用法6(部分明文): python {script_name} enc.zip -kpa part.bin --kpa-offset 78 -x 0 4d5a")
    raw_print(f"[*] 用法7(模板KPA):  python {script_name} enc.zip --kpa-template png [ -c image.png ]")
    raw_print(f"[*] 用法8(仅bkcrack): python {script_name} enc.zip -kpa plain.txt [ -c inner ] --bkcrack")
    raw_print("\n--- 可选参数 ---")
    raw_print(f"[*] 指定输出目录:  python {script_name} ... -o YourOutDir")
    raw_print("[*] KPA 偏移量:    --kpa-offset 78")
    raw_print("[*] KPA 附加字节:  -x 0 4d5a  (可重复；也支持 0:4d5a)")
    raw_print(f"[*] KPA 模板:      --kpa-template {' | '.join(KPA_TEMPLATE_CHOICES)}")
    raw_print(f"[*] 帮助 / 版本:    python {script_name} --help | --version")


def run_cli(locale: str = "zh") -> int:
    try:
        print_banner(locale)

        if len(sys.argv) < 2:
            print_usage(locale, sys.argv[0])
            return 0
        if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"):
            print_usage(locale, sys.argv[0])
            return 0
        if len(sys.argv) == 2 and sys.argv[1] in ("-v", "--version"):
            print_version()
            return 0

        zip_file = sys.argv[1]
        basic_default_mode = len(sys.argv) == 2
        out_dir = OUT_DIR_DEFAULT
        dict_path_or_mask_flag = None
        mask_value = None
        kpa_plain_path = None
        kpa_inner_name = None
        kpa_offset = None
        kpa_extra_specs: list[tuple[int, bytes]] = []
        kpa_template_name = None
        use_bkcrack_recover = False

        index = 2
        while index < len(sys.argv):
            arg = sys.argv[index]
            if arg in ("-o", "--out"):
                if index + 1 >= len(sys.argv):
                    print(
                        loc(
                            locale,
                            "[!] 错误: -o 参数后未提供目录名。",
                            "[!] Error: No directory name provided after -o.",
                        )
                    )
                    return 1
                out_dir = sys.argv[index + 1]
                index += 2
            elif arg in ("-m", "--mask"):
                if index + 1 >= len(sys.argv):
                    print(
                        loc(
                            locale,
                            "[!] 错误: -m 参数后未提供掩码字符串。",
                            "[!] Error: No mask string provided after -m.",
                        )
                    )
                    return 1
                dict_path_or_mask_flag = "-m"
                mask_value = sys.argv[index + 1]
                index += 2
            elif arg in ("-kpa", "--kpa"):
                if index + 1 >= len(sys.argv):
                    print(
                        loc(
                            locale,
                            "[!] 错误: -kpa 后应提供已知明文文件路径。",
                            "[!] Error: -kpa requires a known plaintext file path.",
                        )
                    )
                    return 1
                kpa_plain_path = sys.argv[index + 1]
                index += 2
            elif arg in ("-c", "--cipher-entry"):
                if index + 1 >= len(sys.argv):
                    print(
                        loc(
                            locale,
                            "[!] 错误: -c 后应提供 ZIP 内条目路径名。",
                            "[!] Error: -c requires an entry name inside the ZIP.",
                        )
                    )
                    return 1
                kpa_inner_name = sys.argv[index + 1]
                index += 2
            elif arg == "--kpa-offset":
                if index + 1 >= len(sys.argv):
                    print(
                        loc(
                            locale,
                            "[!] 错误: --kpa-offset 后应提供一个非负整数。",
                            "[!] Error: --kpa-offset requires a non-negative integer.",
                        )
                    )
                    return 1
                try:
                    kpa_offset = parse_kpa_offset(sys.argv[index + 1], locale)
                except ValueError as exc:
                    print(loc(locale, f"[!] 错误: {exc}", f"[!] Error: {exc}"))
                    return 1
                index += 2
            elif arg in ("-x", "--kpa-extra"):
                if index + 1 >= len(sys.argv):
                    print(
                        loc(
                            locale,
                            "[!] 错误: -x/--kpa-extra 后应提供 offset 和 hex，或使用 offset:hex。",
                            "[!] Error: -x/--kpa-extra requires offset and hex bytes, or offset:hex.",
                        )
                    )
                    return 1
                next_value = sys.argv[index + 1]
                if ":" in next_value and (
                    index + 2 >= len(sys.argv) or sys.argv[index + 2].startswith("-")
                ):
                    offset_text, hex_text = next_value.split(":", 1)
                    step = 2
                else:
                    if index + 2 >= len(sys.argv):
                        print(
                            loc(
                                locale,
                                "[!] 错误: -x/--kpa-extra 需要两个值，例如 -x 0 4d5a。",
                                "[!] Error: -x/--kpa-extra needs two values, for example: -x 0 4d5a.",
                            )
                        )
                        return 1
                    offset_text = sys.argv[index + 1]
                    hex_text = sys.argv[index + 2]
                    step = 3
                try:
                    kpa_extra_specs.append(
                        parse_kpa_extra_spec(offset_text, hex_text, locale)
                    )
                except ValueError as exc:
                    print(loc(locale, f"[!] 错误: {exc}", f"[!] Error: {exc}"))
                    return 1
                index += step
            elif arg == "--kpa-template":
                if index + 1 >= len(sys.argv):
                    print(
                        loc(
                            locale,
                            "[!] 错误: --kpa-template 后应提供模板名。",
                            "[!] Error: --kpa-template requires a template name.",
                        )
                    )
                    return 1
                kpa_template_name = normalize_kpa_template_name(sys.argv[index + 1])
                if kpa_template_name not in KPA_TEMPLATE_CHOICES:
                    print(
                        loc(
                            locale,
                            f"[!] 错误: 不支持的 KPA 模板 '{sys.argv[index + 1]}'。可用模板: {', '.join(KPA_TEMPLATE_CHOICES)}",
                            f"[!] Error: Unsupported KPA template '{sys.argv[index + 1]}'. Available templates: {', '.join(KPA_TEMPLATE_CHOICES)}",
                        )
                    )
                    return 1
                index += 2
            elif arg == "--bkcrack":
                use_bkcrack_recover = True
                index += 1
            else:
                if dict_path_or_mask_flag is None:
                    dict_path_or_mask_flag = arg
                index += 1

        if not os.path.exists(zip_file):
            print(
                loc(
                    locale,
                    f"[!]错误: 文件 '{zip_file}' 未找到。",
                    f"[!] Error: File '{zip_file}' not found.",
                )
            )
            return 1

        kpa_requested = bool(kpa_plain_path or kpa_template_name or kpa_extra_specs)

        if use_bkcrack_recover and not kpa_requested:
            print(
                loc(
                    locale,
                    "[!] --bkcrack 需与已知明文参数同时使用，例如 -kpa / --kpa-template / -x。",
                    "[!] --bkcrack must be used together with known-plaintext arguments such as -kpa / --kpa-template / -x.",
                )
            )
            return 1

        if not kpa_requested and kpa_offset is not None:
            print(
                loc(
                    locale,
                    "[!] --kpa-offset 需要与 -kpa 或 --kpa-template 一起使用。",
                    "[!] --kpa-offset must be used together with -kpa or --kpa-template.",
                )
            )
            return 1

        if kpa_plain_path and not os.path.isfile(kpa_plain_path):
            print(
                loc(
                    locale,
                    f"[!] 已知明文文件不存在: {kpa_plain_path}",
                    f"[!] Known plaintext file does not exist: {kpa_plain_path}",
                )
            )
            return 1

        archive_profile = collect_archive_encryption_profile(zip_file)
        pyzipper_ready = offer_pyzipper_install(locale, zip_file)
        for line in archive_encryption_notice_lines(
            locale,
            archive_profile,
            pyzipper_ready=pyzipper_ready,
        ):
            print(line)
        if not pyzipper_ready and not HAS_PYZIPPER:
            print(
                loc(
                    locale,
                    "[*] 当前仍未启用 pyzipper，因此如果目标条目使用 AES，加密验证或解压可能无法正常完成。",
                    "[*] pyzipper is still unavailable, so AES-encrypted target entries may not verify or extract correctly.",
                )
            )

        is_truly_encrypted = False
        if is_zip_encrypted(zip_file):
            print(
                loc(
                    locale,
                    f"[!] 系统检测到 {zip_file} 的加密标志位已开启，正在尝试进行伪加密修复...",
                    f"[!] Encryption flag detected in {zip_file}. Attempting pseudo-encryption repair...",
                )
            )
            fixed_zip_name = zip_file + ".fixed.tmp"
            try:
                fix_zip_encrypted(zip_file, fixed_zip_name)
                with zipfile.ZipFile(fixed_zip_name) as fixed_zf:
                    fixed_zf.testzip()

                print(
                    loc(
                        locale,
                        f"[*] 伪加密修复成功！文件 '{zip_file}' 无需密码。",
                        f"[*] Pseudo-encryption fixed successfully. File '{zip_file}' does not require a password.",
                    )
                )
                _clean_and_create_outdir(out_dir)
                with zipfile.ZipFile(fixed_zip_name) as fixed_zf:
                    fixed_zf.extractall(path=out_dir)
                    names = fixed_zf.namelist()
                print(
                    loc(
                        locale,
                        f"[*] 系统已为您自动提取出 {len(names)} 个文件到 '{out_dir}' 文件夹中: {names}",
                        f"[*] Successfully extracted {len(names)} file(s) to '{out_dir}': {names}",
                    )
                )
                return 0
            except Exception:
                is_truly_encrypted = True
                print(
                    loc(
                        locale,
                        "[+] 修复尝试失败，该文件为真加密，准备进行暴力破解。",
                        "[+] Repair attempt failed. This is a truly encrypted ZIP. Preparing brute-force attack.",
                    )
                )
            finally:
                if os.path.exists(fixed_zip_name):
                    try:
                        os.remove(fixed_zip_name)
                    except OSError:
                        pass

        if not is_zip_encrypted(zip_file):
            print(
                loc(
                    locale,
                    f"[!] 系统检测到 {zip_file} 不是一个加密的ZIP文件，您可以直接解压！",
                    f"[!] {zip_file} is not an encrypted ZIP file. You can extract it directly.",
                )
            )
            return 0

        if is_truly_encrypted:
            print(
                loc(
                    locale,
                    "[+] 开始对真加密文件进行破解...",
                    "[+] Starting the cracking workflow for the encrypted ZIP...",
                )
            )
            try:
                with zipfile.ZipFile(zip_file) as zf:
                    if not kpa_plain_path and not use_bkcrack_recover:
                        # 短明文 CRC32 枚举恢复；若包内条目全部由此完成则直接结束流程
                        if get_crc(zip_file, zf, locale):
                            return 0
            except zipfile.BadZipFile:
                print(
                    loc(
                        locale,
                        f"[!] '{zip_file}' 可能不是一个有效的 ZIP 文件或已损坏。",
                        f"[!] '{zip_file}' may not be a valid ZIP file or it may be corrupted.",
                    )
                )
                return 1

            kpa_ctx = None
            kpa_inner_resolved = None
            kpa_attempts: list[KnownPlaintextAttempt] = []
            partial_kpa_mode = False
            if kpa_requested:
                try:
                    kpa_inner_resolved, kpa_attempts, partial_kpa_mode = (
                        build_known_plaintext_attempts(
                            zip_file,
                            kpa_plain_path,
                            locale,
                            preferred_entry=kpa_inner_name,
                            plain_offset=kpa_offset,
                            extra_specs=kpa_extra_specs,
                            template_name=kpa_template_name,
                        )
                    )
                    if not partial_kpa_mode and kpa_plain_path:
                        kpa_ctx = prepare_kpa_context(
                            zip_file,
                            kpa_inner_resolved,
                            kpa_plain_path,
                            locale,
                        )
                    elif kpa_attempts:
                        first_attempt = kpa_attempts[0]
                        total_known, max_contig = merge_known_plaintext_ranges(
                            first_attempt.plain_offset,
                            len(first_attempt.plain_source["plaintext_bytes"])
                            if first_attempt.plain_source
                            else 0,
                            first_attempt.extra_specs,
                        )
                        print(
                            loc(
                                locale,
                                f"[+] 已知明文部分模式: 条目 '{kpa_inner_resolved}'，偏移 {first_attempt.plain_offset}，附加字节 {len(first_attempt.extra_specs)} 组，累计已知 {total_known} 字节，最大连续片段 {max_contig} 字节。",
                                f"[+] Partial known-plaintext mode: entry '{kpa_inner_resolved}', offset {first_attempt.plain_offset}, {len(first_attempt.extra_specs)} extra-byte group(s), {total_known} known bytes total, longest contiguous fragment {max_contig} bytes.",
                            )
                        )
                        if kpa_template_name:
                            print(
                                loc(
                                    locale,
                                    f"[*] 当前已启用模板: {kpa_template_name}（将自动尝试常见头部/偏移候选）。",
                                    f"[*] Enabled KPA template: {kpa_template_name} (common header/offset candidates will be tried automatically).",
                                )
                            )
                except Exception as exc:
                    print(
                        loc(
                            locale,
                            f"[!] 已知明文模式初始化失败: {exc}",
                            f"[!] Failed to initialize known-plaintext mode: {exc}",
                        )
                    )
                    return 1

                try:
                    bk_tool = find_bkcrack_executable()
                    if not bk_tool:
                        bk_tool = offer_bkcrack_install(
                            locale,
                            required=bool(use_bkcrack_recover or partial_kpa_mode),
                        )

                    if use_bkcrack_recover:
                        return (
                            0
                            if run_bkcrack_known_plaintext_attempts(
                                zip_file,
                                kpa_attempts,
                                out_dir,
                                locale,
                                bk_tool,
                            )
                            else 1
                        )

                    if bk_tool:
                        version = get_bkcrack_version(bk_tool)
                        print(
                            loc(
                                locale,
                                f"[*] 已检测到 bkcrack{f'（{version}）' if version else ''}，尝试无字典已知明文恢复；失败将自动继续字典/掩码。",
                                f"[*] bkcrack detected{f' ({version})' if version else ''}. Trying dictionary-free known-plaintext recovery first; dictionary/mask attacks will continue on failure.",
                            )
                        )
                        if run_bkcrack_known_plaintext_attempts(
                            zip_file,
                            kpa_attempts,
                            out_dir,
                            locale,
                            bk_tool,
                        ):
                            return 0
                        print(
                            loc(
                                locale,
                                "[*] bkcrack 未恢复成功，继续字典/掩码。",
                                "[*] bkcrack did not recover the archive. Continuing with dictionary/mask attacks.",
                            )
                        )
                    elif partial_kpa_mode:
                        print(
                            loc(
                                locale,
                                "[*] 当前部分明文 / 模板 KPA 依赖 bkcrack；已跳过这一步，继续字典/掩码。",
                                "[*] Partial/template known-plaintext mode requires bkcrack. Skipping it and continuing with dictionary/mask attacks.",
                            )
                        )
                    else:
                        print(
                            loc(
                                locale,
                                "[*] 未检测到 bkcrack，跳过无字典恢复；继续字典/掩码。",
                                "[*] bkcrack not found. Skipping dictionary-free recovery and continuing with dictionary/mask attacks.",
                            )
                        )
                finally:
                    cleanup_known_plaintext_attempts(kpa_attempts)

            verifier = PasswordVerifier(zip_file, **(kpa_ctx or {}))

            if dict_path_or_mask_flag == "-m":
                if not mask_value:
                    print(
                        loc(
                            locale,
                            "[!] 错误: 未提供掩码字符串。",
                            "[!] Error: No mask string was provided.",
                        )
                    )
                    return 1
                return (
                    0
                    if crack_password_with_mask(
                        zip_file, mask_value, verifier, locale, out_dir
                    )
                    else 1
                )

            print(
                loc(
                    locale,
                    "[+] 系统开始进行字典暴力破解······",
                    "[+] Starting dictionary brute-force attack...",
                )
            )

            if dict_path_or_mask_flag:
                return (
                    0
                    if crack_password_with_file_or_dir(
                        zip_file,
                        dict_path_or_mask_flag,
                        verifier,
                        locale,
                        out_dir,
                    )
                    else 1
                )

            found = False
            default_password_list = bundled_password_list_path()
            if default_password_list:
                found = crack_password_with_file(
                    zip_file,
                    default_password_list,
                    verifier,
                    locale,
                    out_dir,
                )
            else:
                print(
                    loc(
                        locale,
                        "[!] 未找到内置字典 password_list.txt，将直接尝试纯数字字典。",
                        "[!] Built-in dictionary 'password_list.txt' was not found. Proceeding directly to the numeric dictionary.",
                    )
                )

            if found:
                return 0
            numeric_found = crack_with_generated_numeric_dict(
                zip_file, verifier, locale, out_dir
            )
            if numeric_found:
                return 0
            if basic_default_mode and offer_template_kpa_after_standard_failures(
                zip_file,
                out_dir,
                locale,
            ):
                return 0
            return 1

        return 0

    except FileNotFoundError:
        target = sys.argv[1] if len(sys.argv) > 1 else ""
        print(
            loc(
                locale,
                f"[!] 错误: 文件 '{target}' 未找到。",
                f"[!] Error: File '{target}' not found.",
            )
        )
        return 1
    except KeyboardInterrupt:
        print(loc(locale, "\n[!] 用户已中止操作。", "\n[!] Operation interrupted by user."))
        return 130
    except Exception as exc:
        print(
            loc(
                locale,
                f"\n[!] 发生未知错误: {exc}",
                f"\n[!] An unknown error occurred: {exc}",
            )
        )
        import traceback

        traceback.print_exc()
        return 1


path: scripts/password_list.txt
content: a123456789
11223344
1qaz2wsx
xiazhili
888888
123456
password
789456123
qwertyuiop
qqqqqqqq
iloveyou
qq123456
87654321
000000000
asdfghjkl
31415926
12344321
1q2w3e4r
0000000000
123456789
admin@123
12345678
11111111
dearbook
00000000
123123123
1234567890
88888888
111111111
147258369
aaaaaaaa
987654321
1111111111
66666666
qazwsxedc
123456abc
abcd1234
0123456789
123654789
12121212
asdasdasd
12341234
110110110
qwertyui
123456123
123456789a
123456aa
asdfasdf
99999999
999999999
123456123456
520520520
963852741
741852963
55555555
33333333
qwer1234
asd123456
qweasdzxc
zzzzzzzz
77777777
code8925
11112222
ms0083jxj
123456qq
qweqweqwe
111222333
asdf1234
3.1415926
asdfghjk
147852369
q1w2e3r4
521521521
12345678a
123qweasd
123698745
1123581321
1234abcd
woaini1314
1qazxsw2
woaiwojia
zxcvbnm123
321321321
05962514787
wwwwwwww
123456987
kingcom5
5845201314
0987654321
11111111111111
123456asd
abc123456
aa123456
a12345678
22222222
1234qwer
a1234567
123321123
1Q2W3E4R5T
12345600
lilylily
11235813
10101010
xiaoxiao
woshishui
qwe123456
12345612
5201314520
1234554321
12301230
100200300
z123456789
qazwsx123
woainima
ssssssss
P@ssw0rd
44444444
aaa123456
q123456789
wojiushiwo
qaz123456
wocaonima
123321aa
25257758
csdncsdn
1357924680
yangyang
woaini520
369258147
zhang123
321654987
9876543210
1233211234567
1234567b
zxczxczxc
dddddddd
google250
5845211314
aaaaaaaaa
abcdefgh
369369369
20082008
123456qwe
goodluck
zxc123456
qwerasdf
135792468
299792458
computer
12qwaszx
a1111111
as123456
123456789.
12345678910
a123456a
888888888
168168168
superman
qq123123
789789789
8888888888
abc12345
xxxxxxxx
zaq12wsx
sunshine
112233445566
a1b2c3d4
qq111111
aptx4869
123321123321
66668888
52013145201314
007007007
wodemima
147896325
1a2b3c4d
123789456
aaaaaaaaaa
jingjing
12345qwert
helloworld
110120119
tiantian
123456ab
13145200
aaaa1111
a123123123
123456..
00001111
a5201314
12312312
zhangwei
w123456789
584131421
123456789q
77585210
abcd123456
qw123456
ab123456
passw0rd
li123456
20102010
666666666
12348765
1234512345
456456456
20080808
newhappy
wangjian
csdn.net
12345687
01234567
01020304
21212121
dongdong
qazqazqaz
123123123123
wiii2dsE
23232323
13141314
74108520
7894561230
5841314520
a11111111
q1w2e3r4t5
aa123123
woaini521
shanghai
77585211
88771024
123qwe123
123456as
19491001
qq000000
wang123456
01010101
qqq11111
zz123456
abc123456789
14789632
111111aa
q1234567
yuanyuan
qazxswedc
3141592653
meiyoumima
77585217758521
liu123456
20092009
12345abcde
1234567890123
zhanglei
110119120
012345678
77889900
mmmmmmmm
123456000
qwert12345
lb851210
justdoit
qq123456789
19841010
7758521521
hahahaHA
aini1314
llllllll
19841020
11111111a
13131313
123456798
10203040
123456zx
23456789
mingming
chenchen
asasasas
worinima
52013141314
123456qaz
123454321
qqqqqqqqqq
1111qqqq
Woailaopo
123456ABCD
1234ASDF
518518518
wangjing
z3255500
88982866
11110000
5201314a
584131420
987456321
258258258
qq5201314
qqqqqqqqq
q1111111
88889999
12365478
00000000000000000000
Q12345678
www123456
123123aa
zx123456
19871010
qweasd123
123123456
45612300
25251325
1234567899
19850603
administrator
a0000000
52013140
nicholas
longlong
120120120
19841028
kkkkkkkk
tzwadmin123
asdf123456
333333333
wangpeng
911911911
jjjjjjjj
admin123
microsoft
66778899
19871024
hyjzstx8
lovelove
78787878
qwqwqwqw
wangwang
16897168
123456qw
cccccccc
z1234567
a1s2d3f4
love1314
456789123
12332100
HHHHHHHH
qqqq1111
xiaolong
a00000000
aaa123123
xingxing
xiaoqiang
zhangjian
zxcv1234
pppppppp
19851019
ds760206
19841012
1qaz1qaz
19851010
1314520520
gggggggg
111111qq
55667788
19871025
wobuzhidao
1111111a
1111aaaa
12300000
13572468
miaomiao
w12345678
19861012
imissyou
qwe123123
abcde12345
19871020
asd12345
nihao123
internet
qwe12345
19881011
123456789abc
zhimakaimen
123456789123
qwerty123
1223334444
19881010
woainiwoaini
19861020
w1234567
yyyyyyyy
84131421
1qaz2wsx3edc
zhangjie
testtest
wangyang
z12345678
zhanghao
456123789
99998888
12131415
shanshan
11111111111
19890309
asdfjkl;
5841314521
5555555555
110120130
hello123
abc12345678
19861015
xiaofeng
12312300
112112112
3141592654
1q1q1q1q
56565656
liangliang
dgdg7234322
159159159
19841023
19841025
19881212
19890306
zhangjing
98765432
00123456
19841018
19861210
19871125
119119119
00112233
19861010
123456456
123123qq
5201314123
12345678q
19830209
98989898
19881120
19851212
19861028
222222222
asd123123
1qaz@wsx
6666666666
147147147
19861123
1029384756
89898989
woshitiancai
19841001
19871212
tingting
oooooooo
19871987
19861018
mm123456
789654123
121121121
19881128
123456ok
zaq1xsw2
58585858
wangchao
123456zz
19881028
19871023
19881020
19841015
19871011
19871021
123456780
19881220
123abc123
19861111
19861016
314159265
asdasd123
19861215
369852147
19861026
19871028
19821010
123456zxc
13145201314520
19841024
beijing2008
1212121212
19861212
dingding
19841026
19871012
19870623
555555555
12332112
dg123456
159753123
Aa111111
Q11111111
19861023
19871015
12345611
19861025
zhangyan
huang123
windowsxp
19841021
handsome
19871001
19881015
19861011
123456...
19851020
19861001
19841014
ASDFZXCV
12345689
woshishei
584201314
zxcvbnma
19851120
1231512315
19871214
zhangyang
16899168
11221122
a1314520
19871016
wang1234
chenjian
19881225
lingling
qwertyuio
963258741
19851218
19871225
19871120
19871228
qwerqwer
19851125
19881024
chinaren
123581321
001001001
huanhuan
19871017
19881022
songguang
tsinghua
100100100
19861021
123456789z
19861216
19881017
zhongguo
19881013
19851023
0.123456
19861013
19860202
19881016
19871026
13324016206
19861218
19841011
134679852
qkvmlia569
560111aa
19861024
19841019
zxcvbnmzxcvbnm
19861220
19861211
19841027
520131400
zxcvbnm1
19881018
123654123
789632145
19861121
12345abc
1237890o0
19821982
19821221
19871022
19881125
19861124
19871122
5200251314
19841017
19861120
19871121
19831010
l123456789
123QWEASDZXC
abcabcabc
........
kingking
19871029
admin888
159357159357
19851025
123456ASDF
jianqiao
18181818
19851013
14141414
qwe123qwe
ZHANGHUI
Haohaoxuexi
19831220
19881123
19861225
19861224
123698741
74107410
88886666
19871210
159357123
19881021
19861122
19881210
19861014
19851216
amuqdedwft
19861022
19881001
woaiwoziji
68686868
19870214
19841122
zxzxzxzx
19851230
19881023
19821024
operation
123456321
19860214
ddzj39cb3
support123
19861112
19831001
301415926
ww123456
11111112
19891010
19881027
12345677
19861214
POIUYTREWQ
msconfig
19881029
19851011
19851012
19871103
19871223
951ljb753
777888999
19821016
09876543
123456788
19810914
19851018
19861125
123654987
19861228
111111111111
19871126
19841022
777777777
10002000
19851001
aaaa0000
19851030
55556666
19871013
zaiwa1124
zhangjun
3366994qaz
200919ab
19871129
1234567q
l12345678
19841984
chen123456
19871002
19831111
tttttttt
19861226
19881228
19821120
19881012
qweqwe123
09090909
19871226
123456qwerty
19881002
19851015
19851121
19870825
19881226
19861103
19870909
19851210
19871127
19871123
19870101
zhangtao
19871218
19851225
19871027
zxcasdqwe
19881025
19851205
wo123456
19821012
19861223
19821128
19861128
yingying
19881129
cs123456
woshizhu
19861017
wangyong
19881127
19851123
1111111q
19861019
cc123456
19851214
bbbbbbbb
19871213
LIUQIANG
19880808
19860713
741258963
19871215
19841013
19881215
xiaoming
19841006
19841005
abc123abc
19860128
19871115
83869247
19851016
19871111
19871211
19861113
woshishen
19841029
19851224
19871221
19881218
wangliang
howareyou
19861030
19881014
FANGFANG
19821023
19841210
19851024
19841002
19860101
a7758521
bugaosuni
19861027
19870815
19861127
19831020
19841120
19851226
12356789
19861108
19881112
19881118
19841123
19851028
19841016
19881026
9638527410
13141516
qingqing
19861029
wangdong
19861002
jack123456
123456bb
333666999
19851102
yy123456
12345678900
19861126
hao123456
200401265
123321456
19851022
zxcvbnmm
19861221
19890101
12345671
19841212
19831120
19861206
jiangnan
19831125
19881214
lxqqqqqq
19871229
19861118
nishishui
19861110
19831012
19871124
19871014
cndkervip
wanggang
19881122
19870626
19851211
19871227
19861986
qwert123
19881126
90909090
78963214
19871019
2222222222
19870604
13149286ab
159753159753
123456789QQ
mac123456
19831024
19891120
19871201
00000001
wangying
19851213
qaz12345
96385274
19851026
19881124
baidu1599
supervisor
19861213
10000000
CAONIMA123
19861102
19831124
123456789+
19871203
qawsedrf
zhangxin
abcdefg123
19871104
19871128
19851219
100200100200
jianjian
19851118
99887766
19881213
19861203
19851113
19871208
19851116
19891026
19851220
zhangzhang
9999999999
s123456789
19871030
19870216
19831118
jiaojiao
19871222
001002003
7758521a
19851111
19871118
7708801314520
125125125
85208520
admin369
wangfeng
19861106
19861104
19871207
yu123456
19880101
csdn123456
19881988
19871216
19851017
51201314
19821025
19870923
19881115
19871004
19871209
19831212
19851215
19881111
19861205
19831013
19871018
19861006
19821116
123456789@
19870621
19891011
maidoumaidou
12345654321
asdffdsa
20052005
1234567891
7758521520
19881116
19870201
19861129
55665566
19861119
19870620
19870125
rongfan66
19821001
19821122
nishizhu
19881121
19871005
19841205
19871220
asdqwe123
19871205
19871102
65432100
19870627
19831218
19821018
zhangpeng
19880214
19820814
19851112
QIANQIAN
19871117
19861003
19831213
zhangchao
test1234
zaqxswcde
19841125
19871110
1qa2ws3ed
192837465
19881114
19851217
01230123
19891020
19880211
19860909
ss123456
1q2w3e4r5t6y
19860126
19851223
19881216
19851126
19821028
19861227
19891024
19851204
19871217
19870606
19870202
windows123
123456789*
mengmeng
19851229
zzzzzzzzz
19841108
woaimama
19871108
19831027
19861217
19831025
19831011
19841127
huangwei
19841124
19891225
xx123456
caonimabi
19831023
19871112
zhouzhou
19831015
qazwsxed
fa1681688
19861208
19841007
zhangqiang
19831129
19861219
19881217
abcd12345
19881229
19881211
19821207
*********
19881224
19871008
19871116
19861230
19870618
19851014
19999999
19881117
19881008
31496081
19831106
19891015
19841004
19870629
19851221
19861222
19870628
china6815
19891012
x123456789
19841225
19851122
19851124
19891023
LKJHGFDSA
19841226
fengfeng
123456qqq
19851985
19861105
52001314
19870102
19851003
19831028
1415926535
19871105
19821212
19841220
19881030
19821022
95279527
19891021
12345666
19821017
19851128
19851005
wonderful
asdfqwer
19891989
19870608
19871224
jiangwei
19831021
19861209
stefanie
31415926535
19851228
19870917
19881208
QQQ123456
adminadmin
19821026
youaifa569
jiushiaini
584211314
19821208
159357456
19851027
19870619
19870911
19831105
19861115
52113145211314
19831126
19880828
wangzhen
19881113
19821021
19881110
19870310
19841030
19831016
5201314.
19881005
19891215
20202020
19831226
19841219
00000000a
19871109
19861101
hhxxttxs
19821224
05413330
19871206
19821019
asdfgh123
19870921
bingbing
19851227
19871204
12345688
7215217758991
25252525
19891028
zhangbin
12345670
000123456
19841213
19860921
10241024
19891221
aaaa1234
19851115
asdfg12345
zhangliang
19881201
ykvpoia569
!QAZ2wsx
nclpf2p4
19881119
19870625
19851105
19831123
19881219
mimamima
weiweiwei
abc123123
asd123asd
19841113
1a2s3d4f
youkofa569
19850101
19851127
19891019
19860210
19891022
19881223
huangjie
19860916
19861117
19851029
19891016
19831026
19831112
19821218
19841111
19831211
asdfg123
19841003
19891224
123456789qaz
19831224
19821123
19861008
19891124
19881221
19891212
19870128
yang123456
19831219
19880901
19841228
36363636
19861229
19851002
19861109
sun123456
download
19821013
19831210
jiang123
19841008
l1234567
19831014
19821014
19891025
19860902
19851006
19831007
19831122
19841217
19881108
19821213
5211314521
19870708
19831018
19851101
19870913
xiaojing
19891006
hellohello
19871101
19880202
19870824
19831208
201314201314
987412365
12346789
19851021
yangchao
19811025
19870912
19891001
19861201
19830920
19841101
19881019
19841126
19831216
xiaojian
19861202
19821020
zhang123456
19870210
19831121
19851110
700629gh
19870925
wangning
000000aa
77585211314
19860911
pingping
19891216
19881227
19841218
19870205
123456789123456789
qqqqwwww
19880218
19870129
19861114
19831022
27105821
19821215
chenchao
19870818
19881103
19861107
abc111111
19870203
315315315
19870103
19871009
zzzzzzzzzz
19880125
19861204
19831215
XUANXUAN
19841224
zhangkai
19820101
19831225
feixiang
19891013
19870212
19851106
19870213
19861007
19851008
16888888
19871006
aabbccdd
19860927
workhard
19870819
19870624
thankyou
19841121
19841230
19871007
19851114
ybnkoia569
19891228
0.123456789
xiaodong
19870615
19890214
wangshuai
19860228
lv80066368
19831101
xj123456
37213721
19841203
19851129
19821030
19880312
WOSHINIBA
19870603
19871119
12332111
19891017
yangjian
wangming
19821225
qingfeng
5i5i5i5i
19890122
21876346a
19831227
wangqiang
csdn1234
yangguang
12345789
19841211
19871106
19840921
19851107
19891226
19821129
19851203
19870209
19870810
19821015
00009999
rilidongl
19861207
chen1234
19881004
19881006
19881003
19861005
1357913579
19881130
19851208
19850809
19841221
19870919
19870601
happy123
52013141
19891213
88488848
ly123456
qq1314520
19880928
19860315
19811010
s2j3l9v5
tangtang
19831983
19870215
19891030
f19841205
123456789w
19881109
11111122
987987987
19870903
ll123456
zhangying
19831128
19860926
wangmeng
19831214
19841119
pengpeng
19870520
19880310
19870808
19841103
19871107
rongrong
19821118
butterfly
53231323
19880428
13145210
19870225
football
19871219
19850923
19870208
19870206
s1234567
19880217
qq123321
19831221
19881202
19851104
xiaoyang
19840101
1010101010
5205201314
19841116
19821102
qazwsx12
22334455
19851201
19880212
19870207
123123123a
paradise
s12345678
19861116
xiaogang
541881452
19861009
7758521123
19821002
123123qwe
19811017
19871114
19880816
19860917
19860217
19880203
19880108
20062006
19891122
whoareyou
laopo521
19870927
19891029
19870916
gujinbiaoaa
19860125
zhendeaini
19831108
19831102
19880809
3.141592653
19851119
19870926
19870220
77582588
19821121
19880120
123456654
19870121
19881102
52105210
963963963
13801001020
19891027
19881106
19860103
1213141516
19860314
19831230
19841105
19891104
19871003
19870915
wiiisa222
iloveyou1314
19821126
19841215
lin123456
zxcvzxcv
tiankong
abc123abc123
19821230
19860923
19880206
19840102
19860303
19891130
Fuyume123
19880124
123456xx
19870305
131452000
19801980
xxwwan1314
qwerty123456
xiangxiang
niaishui
19851109
19870920
19880311
19841208
19870828
19860814
19840129
19870918
19840211
chenliang
19870123
19860816
19891123
okokokok
19870820
19870217
19870707
19870617
19880921
19821216
19880126
19881206
19870616
19870301
19841214
q1q1q1q1
19870801
19821029
adgjmptw
19821220
woshinidie
PASSWORD123
19891128
19871230
123456789aa
gogogogo
19881204
WANGHUAN
456852456852
19841107
19870120
20000000
zhangming
19831008
19851117
19880215
19850102
19870228
19821115
19890228
thinkpad
19821008
19841129
0054444944
password1
a1b2c3d4e5
19841229
19841206
19831103
19861004
19821027
19871202
huanghao
19811026
19841130
19860929
19870928
0147258369
19881107
wu123456
kk123456
19870611
19831201
19840214
147896321
19890818
19840120
123456ww
19831127
19860102
19860625
qq12345678
19860820
haohaohao
19860606
aaaassss
19840420
19851007
19870112
19870609
19870622
19890806
19880123
33445566
19851209
19831228
19850105
19821011
19880209
beautiful
19860924
chenfeng
159753456
19870907
19870614
19841102
apple123
19840823
19831003
yangjing
19870904
19850711
19870303
19880815
19880712
19870605
19831009
19870701
wokaonima
19841204
1a2b3c4d5e
19891121
19870113
19881209
kangkang
19840926
amp12345
19870218
19880307
19811028
19880228
westlife
19870821
19821112
19821101
86868686
19880915
19880606
19831217
20090909
aa123456789
21345678
19870221
25802580
19890301
qazhuang123
19891116
h123456789
woxiangni
19821106
19880118
19891211
19881205
775852100
19880523
52013145
19821226
77778888
19870525
19821104
19891127
987654123
123123321
19870823
7777777777
19830915
songsong
xixihaha
19870211
19860212
19851231
19890201
19880926
19891218
19891201
19870612
19841115
19860925
19860120
19821217
19870728
foreverlove
19891018
19870602
19821211
19821219
yang1234
19850926
19881203
19881101
45454545
19891110
19830214
QQ1234567
19831205
19831113
19831029
Abc112233
19821205
19890206
goo78leeg
19831204
19890205
19840925
19891125
19821006
19821130
zycggytkqb
zy123456
19821210
19870811
ABCabc123
19850921
19870503
20022002
1111122222
19860912
slamdunk
19891219
19851222
19870521
19821124
19891206
19831019
04020323
22223333
19850215
19870325
19831206
19870914
eladnbin1104
19850909
123abc456
19860211
19830202
19880326
qazwsxedcrfv
123123
19830418
123456789987654321
19880601
0000000a
19891002
521224727
zhangyong
19831115
c123456789
19821228
19840203
19860216
19870807
78945612
15151515
19870830
19880927
19840601
19890323
19870705
19891210
zhangfeng
11223300
fighting
19841009
19870522
asd123456789
159357258
19860523
tongtong
19840210
19870922
pok29q6666
12345678.
19831207
asd147258
13800138000
19851207
19870822
19890215
19860808
19851206
19881007
19870610
19860215
8008208820
19891014
software
19880223
19870223
19870418
19880103
5201314qq
19891129
19891220
19870126
wangchen
19811018
19880303
19871113
19880922
19890927
19831116
19880510
19841128
einstein
0.0.0.0.
19850925
19880610
19851004
nishiwode
songaideng
19870404
19870529
aa000000
19880128
chenjing
19860910
dirdirdir
19811001
19840808
19880309
19891113
19881222
19860410
19841209
19870127
19880105
19860810
19860329
windows98
19811225
19880205
19870315
19871231
19891101
19880818
19831110
woshihaoren
yongheng
19850823
19870412
19870419
19890826
19870929
19860725
19880611
123456ll
19830205
19881231
19870715
19820911
19811020
19860208
19850927
19831006
101101101
11211121
12342234
134679258
juventus
19870814
19880909
love123456
19811022
19860312
19821214
19840204
19880102
1472583690
19870324
19860619
linrk520
16161616
19880920
19860906
19860626
fdsafdsa
19870829
xu123456
123456789l
19860918
19191919
19870901
80808080
19841216
19870304
19821103
woaini11
19860205
19860512
19890212
123456789asd
19870402
19891119
chenyang
pass1234
19890910
19860118
19870226
1234560.
19860325
tiantang
a7758258
19870908
19860815
19880701
19880207
19890120
AAAAAAAAAAAAAAAAAAAA
hu123456
19821119
19860219
19821204
zf526546543
19830218
19850124
19890919
19890216
19880302
19821229
19890920
19811216
tomorrow
dangyuan
19880820
19801020
54181452
19820812
19890123
19891109
19811013
130130130
19891231
19850214
19811014
19820808
19860623
19891208
19860203
19870504
19840929
19870817
19870813
19870725
goodgood
19870306
zhu123456
19870110
19870410
19821201
19870910
19890213
cet333333
19841223
19881105
19860824
19880612
19860104
19841112
19870328
19851202
147852963
19821111
19841109
19880916
19891202
19870106
19860823
19841110
19841227
123456pp
19821109
19851108
19870924
19870316
22446688
19880829
69696969
19890828
19890310
19821009
19860226
19840928
19880419
19850917
wei123456
19880628
19820918
19830926
44556677
19870204
19880813
19840909
19870826
19831005
19820909
19850412
19830928
liang123
19870417
19880216
19881207
32323232
19860715
19860318
19821202
hh123456
19870411
china123
19840216
19890314
19870923wex
zxc123123
19830815
19880313
14159265
Password01!
789123456
19880508
19890316
19851103
abcdabcd
qwer123456
19850820
qazxsw123
19880720
741741741
19821127
19860106
19870312
19870613
19870905
19870523
19801212
19831202
19870501
19850825
19870115
19880520
19870421
19880709
19880129
19870320
19901025
19870727
19840207
19880210
19891112
19890312
123456liu
xy123456
19860401
19860724
19870219
19880722
19850815
wanglong
19890103
19860829
19830101
19880427
19891118
19840815
19880201
jisuanji
19890129
19890110
by7704566
19880910
jiangtao
19860919
19881230
chengcheng
123456++
19880204
19840924
19891103
19850808
19860311
19870902
19870906
19880618
19850622
19870524
19850303
19820928
123456li
28845452884545
19891223
123456TT
19841201
hao456250
19820214
19821105
11001100
19890415
19891207
tt123456
cyq721225
19811981
19880104
19821117
19831117
19870122
19831209
19860306
19831109
zhanglin
19890124
19870311
19831203
77887788
19860914
9876543211
aassddff
19841202
19880923
19890912
19860805
19880918
19860712
19850115
19811021
19870416
19880517
19860108
19881104
jiajiajia
19870105
19870719
19860822
19841104
198019803014166
19880911
19890924
19880516
19860122
zc123456
19891230
19821005
19870816
qqwweerr
19891106
19811015
y123456789
19880707
zhangjin
jordan23
maomaomao
19841118
19870721
19890203
wangyuan
zxcvbnmz
19880306
19820502
19840902
1314520a
19860310
19830302
wj123456
19860204
19801225
19840918
19870712
19890618
jiushiwo
123456yy
19860904
123123..
147369258
19880420
chenming
19870124
19820818
19860825
19870313
19881009
19840312
woshiren
jiandanai
19860826
654321654321
windows2000
a1a1a1a1
19820816
19890504
huanghuang
liujianliu
123qazWSX
19880917
19860505
19811108
12345qwe
80238023
19860520
19860812
wangxiao
19850912
MINGTIAN
1234567890a
zhanghua
19891205
19870114
19860116
19811024
19831104
112233112233
19840923
wangping
19880229
19880318
zhenzhen
19880115
19840906
19890125
19880122
19880814
19880723
741236985
19871031
11231123
19880613
yangyong
3333333333
19870716
19880506
66669999
a123456b
123456781
19860209
ningning
19860928
shuaishuai
woainiya
19820915
19870724
19850822
19880324
19880127
15935700
19860605
19880106
19850216
19880801
19880609
19850415
19860818
19890823
19811218
19880624
19860901
zhangfan
19830124
19811121
laopo520
19870827
19850121
19870116
19860124
1314520123
jiok98001
xiaoliang
19850924
19821231
19870804
19870729
********
19821007
19890218
19870714
19811128
19840205
19831017
zhangrui
19880608
19880501
142857142857
26262626
password888
5201314..
19891126
sz123456
19850211
zhouyang
19860420
20012001
19870505
19880708
19880925
19840822
m123456789
19831031
19870803
1234567890.
19811016
19880224
19861231
19831114
19870415
19880715
19820925
azsxdcfv
19870307
19880805
123asd123
19850220
19880421
19811029
shevchenko
19830505
19891117
19870809
happyboy
51515151
19870518
wwwwwwwww
19890308
19811212
42011178
19880711
19891214
19880912
19830215
Lj123456
19841207
19850914
19811115
19840201
19880208
19860320
19890628
19880518
19870130
kobebryant
19860105
19840310
123456qwer
19891229
163163163
19820917
19820926
iamthebest
zl123456
m12315309
woshinibaba
19880325
dangerous
19860615
19890128
19890815
19880617
19880116
19860913
19850212
19890116
vvvvvvvv
19850902
52013144
19820822
110112119
19860218
19850621
happy12345
19821222
zhoujian
19870314
000111222
19880408
19860220
diandian
132465798
19830623
19870302
19890127
19870517
ok123456
19830916
19860922
yongyuan
19880826
jiangjun
happyday
0147896325
19850922
19860601
huangjian
38183818
20082009
19891108
19891227
19880626
zhangqian
213213213
windows1
19880109
19890303
60200946
19821125
19890916
19860302
19831222
19840606
19870607
19831223
19880110
bb123456
19840315
19830629
19890102
19880511
19880220
96321478
19860719
19830102
19850125
123000000
175638080
gameover
xiao123456
19850928
19860223
19890126
19870513
19850410
19880418
19880301
19870322
19880226
womendeai
19850623
zzz123456
19880320
19860123
19880903
19870526
19891111
jiangjiang
19840127
19830624
19860903
19811019
zhaoyang
19860806
chenzhen
19801012
19820930
qq112233
19830210
nnnnnnnn
19831119
19891209
19871130
asdasdasdasd
wangqing
19891107
19870703
19880525
sdfsdfsdf
09308066
178678789
19850510
19880806
19880305
19880321
15975300
19880406
19880907
19890405
sd123456
a147258369
19820413
19870802
19811011
A1234567890
19820809
19870512
19860206
19850701
19880314
zxcvbnm,
19890202
woshi123
12312345
19850310
123456cc
5418854188
19860701
10231023
19811123
chenlong
19880114
19801220
19880213
zhouazhou
19870711
19880629
19860602
19880822
19840915
19890813
Hellokitty
qkvpoia569
19870723
19820913
19840202
19890923
19831229
12345698
19870806
zhongnan
19821209
19831107
19851009
19870408
xiaofang
19840801
19820927
19850601
QQ7758521
19890909
19890221
19831030
19821003
19870516
19860614
19880924
wangting
007008009
aaaabbbb
20042004
19830714
jb85811510
19841117
19840123
79797979
44332211
mima123456
20072007
19891003
19840404
19840218
19801226
19840106
zhuzhuzhu
welcome1
zhangyue
19840220
19860616
19870104
19820124
19880810
19880603
19830925
19890319
19870427
19870329
19880929
19840311
19870530
19870406
19890107
19891102
19850727
19821203
19891222
19880811
19840920
19870323
19860506
19830327
12211221
zhanglong
19831002
19870509
19850725
19860708
19890901
312312312
19860707
19880423
19891115
19830929
19791001
52525252
19820828
135135135
99990000
19880724
00000000000
19880802
19850110
19850929
19850910
19880702
mm108428
19860809
qazxcvbnm
zhanghong
123456mm
19880919
dongfang
334205265
19880315
javascript
123QWERT
19890315
19880625
60729043
19890106
19880119
19830612
19880412
19860624
19850120
19890119
zhangzhen
19830922
19841106
19850204
123456kk
19891005
19890606
19860521
19870318
19890829
19820707
19870118
19870710
19850908
19841231
19820914
19820921
19880721
19830913
19860720
19811125
19801215
999888777
19860213
19860415
19860402
19860607
asdf12345
314159314159
xiaoxiong
jj123456
19870319
1qazzaq1
19880221
feifeifei
19860113
zj123456
19841222
19860201
19850618
19850107
19860416
19890801
19901010
19870713
19870718
yaho982er
19860710
19880902
19850626
19830521
19880728
19840612
21882188
19880605
19850828
19820318
512512512
19830918
19850726
xiaotian
19891105
19870222
wl123456
a5211314
19890612
654654654
123456520
19880219
19890925
19880415
19830213
19850301
19880411
123qwe123qwe
19820910
19830901
WASDWASD
52113141314
19840820
juanjuan
19860527
19860920
wodezuiai
19880825
19891217
nsnsnsns99
WOAIZIJI
19860304
weblogic
19820826
19860501
19811027
55558888
19860408
19850224
ddzj39cd9
11259375
cn835312
19870425
891023hh
19890114
19870519
19820916
19850911
19880529
110120110
huanglei
19880505
19860722
19890825
wq123456
19890601
19880714
12345678z
19890209
19901023
19811012
19850219
52571314
19880729
19830113
19870401
19890928
19880622
19860522
19870131
19881031
19890616
19850502
19870107
76543210
19840302
19870511
19830203
167669123
19890604
19840713
19850501
19850614
19880904
19890404
19850611
19860828
19870108
19880726
19840326
19870502
abwdxwtz
19890808
caonimama
19830201
19890918
630158513
passport
love5201314
19890305
woxihuanni
19880304
19870630
19840901
19890816
19820912
19860326
19820902
x12345678
19860403
19840303
19880615
19850608
chenggong
12365400
19840305
19860107
19890624
nopassword
19880906
19811127
19890520
19890821
19850104
wowangle
19891004
19860612
zhou123456
hongkong
19850202
19850801
guaiguai
19821223
000000000000
19811129
19850805
michelle
10293847
19880227
wangbadan
19880817
19880405
19850123
clens21563sf
666888999
qwertasdfg
19870428
19841031
19890220
19890626
19870309
19860109
19840116
456852123
yangming
19870805
wy123456
19850314
19890526
19870424
19870720
52113140
19860721
1234560123
19811124
bakhn524d
19830923
19860319
admin12345
52101314
19890915
19840829
19880225
19870726
13579246810
19811023
19880819
19860112
19880425
16899199
19831231
19890607
19890812
19840215
chaochao
19840124
19880512
19850126
zaqwsx123
p0o9i8u7
19880718
19891008
19850217
19821031
showmethe
19860207
19820309
142536789
19870812
19860307
19850420
19830919
ms0123456
19860518
19860428
19870409
19860114
19811120
ilovecsdn
19811203
whbs2234
19830310
19890921
zhangmin
19840810
19840229
19880117
19850312
19860801
19811223
19850103
19821114
10201020
19851130
19820208
19890311
19811210
19850903
19860802
19891007
19880528
19820415
jianghui
19860418
19860709
19880524
shengyulan
19890501
19860514
19840728
19860406
19820216
zhangshuai
19850920
19840828
19820620
19820619
19880530
19880908
19820210
5201314aa
19860115
2010comer
19860821
passpass
19890926
weishenme
19830313
19830801
19840212
19840110
19840615
456123123
19870706
546546546
123654123654
19880327
19850721
19870510
19880329
z3261678
mamawoaini
87878787
19890211
19801025
19850129
19880913
19811215
19890113
19840217
19860621
520090025hgb
19890503
zxc12345
19860110
19801208
19890613
19880620
19821110
19860305
19890811
19870413
19850116
46709394
qazwsxqaz
19880107
19840712
menghuan
19830721
51211314
19830601
qaz123wsx
201201201
19860817
19850109
19870224
19890820
19830312
19891031
19811213
19860317
wangcheng
19840424
58451201314
19870317
19840807
19860417
19860813
19890204
135781012
14725836
96969696
0102030405
19801112
qaz123123
1314520.
19890105
19860412
19860728
19860714
19850821
19880526
19830909
19820202
lovemyself
19850326
19890222
123123aaa
19860819
19820103
19820529
zhanshen
19880504
19890413
20002000
19820425
19820618
19801213
19850320
zh123456
19830810
1314521521
wangqian
zhaojing
753951753951
19880308
19840208
19880426
DD123456
youandme
5201314789
19890307
aa123321
19890619
19850322
long123456
19880914
19880725
19820219
19880404
wohenhao
19860611
19861130
19840809
19820217
capslock
lavender
19901020
19840225
19870709
19840209
axs8873w
19830621
210210210
19840813
19890824
19850210
19850918
19830713
19811226
19840501
20032003
ws123456
19850128
19841114
19820801
19880803
19890418
918918918
110119110
12131213
19850723
mypassword
19850218
19870321
70701111
19821004
niaiwoma
19850302
19850916
1887415157
zhaozhao
13093313856
19890320
19820422
19850826
x1234567
19840421
19861031
19840811
xiaoying
shoujiqb
19880616
19860308
pp123456
19850707
19880621
h1234567
0000....
19890208
liuchang
huang123456
19860421
19850209
456123456123
19840526
123456abcdef
19850305
19840911
19890327
08080808
19850807
19831130
19860608
250250250
liuliang
19890625
lilingjie1102
74123698
19860618
19820827
19801123
123456789o
19860127
19811217
19850824
o0o0o0o0
211211211
asdasd33
19890508
19850425
159951159
19850226
19850525
38383838
19880812
19890525
77585200
19870405
19870429
19840610
19840629
verygood
19830921
19850108
19820512
19850613
19880113
19860908
19890605
caonimade
19901022
19830219
ihateyou
19890707
19850112
19801124
19850221
19890412
hld_1209
19821108
zaq123456
19890615
77582587758258
19870702
19840414
19870327
19811116
19880830
19860321
19840824
19840510
1234509876
123321qq
19840504
123123abc
19811228
17051974
zaqwsxZXC
19820626
19811221
19860717
19840725
19860504
fuckfuck
19850625
19860419
523523523
19801010
zhonghua
19820315
mengxiang
19840821
19860422
aaa12345
19811230
19850304
19850315
19890219
19820829
c1234567
samleiming
19811231
19801120
19840711
19840104
19830625
19850127
19771105
19830906
jdheh421
wangbing
52331314
zxcvbnmasd
19890603
19890710
19880804
19860610
19890715
12300123
laopowoaini
19820115
19811104
alexander
19831004
19870422
19891114
19860301
19820325
19820410
19820920
19860414
wangxiang
19820127
19890321
19840721
19880402
19830820
19840720
opendoor
19821107
19850606
19890115
19830103
19860811
19860129
19830303
19890623
19830823
19850919
19890803
19880515
cnforyou
19801230
19830306
19901018
19880604
19850207
19860426
zhangnan
19890411
autoexec
258369147
12345asdfg
19830226
19880401
19781978
CHOCOLATE
19840228
19801014
19830211
19890712
19970701
19880330
19820606
19820723
19820714
19840405
Asdfghjkl;'
19830126
19840818
19890329
10251025
951753951753
19890914
zhangheng
19830307
19840919
19840814
19880319
19880727
19860513
19820119
19880130
19850609
19880513
19840226
19850317
zhangyuan
19860705
19840409
159258357
yskj33333
520123456
19860510
19880710
1008610086
19880602
19830520
19820601
19850724
19860905
gao123456
19820408
19890117
19990125
19830701
19820312
19850612
19801228
jiarenqb
19860224
19850624
17171717
19830911
19880519
19811112
12312311
shenyang
19840614
19870423
19860603
accpaccp
Q123123123
xiaopeng
19840105
5845131421
19890130
19870722
19850523
19820905
19740302
34416912
19840506
xiaoyuer
19840626
19890722
19870227
10011001
19840625
111111aaa
19830110
wang1987
19870109
19820701
19850205
149162536
19840130
honghong
lalalala
19880422
19890420
19860629
19890425
19840723
19830513
ainiyiwannian
lililili
z1231231
123000123
19870119
3344556677
19820203
19811219
19890416
19880627
19870308
110220330
5201314000
19860529
96111111
19850520
12481632
19860723
19890210
19840126
mima1234
19840904
19820720
19840927
19850122
19820821
19801121
19840227
19880112
19870528
11118888
19810101
7410852963
19860405
19890302
19840213
delphi2009
huiyuanai
19860620
seoshiyan
19811105
19870717
19850228
19810323
124578963
19830120
19840416
19840706
aaaAAA111
19820922
19840605
19830510
19820416
19890410
19850628
19830606
asdfasdfasdf
19880824
19840410
19890620
19870515
19821206
19840117
19880623
19811111
19850325
19820711
19820817
19880503
19820802
126126126
19830626
19880317
qiujingni
19820316
wangzheng
551648586
19880905
19890324
19820304
zw123456
1234zxcv
a1a2a3a4
19860327
211314211314
zaqwsxcde
19890318
19820807
zhangning
19900101
19820924
123456az
19840702
19860718
19890104
19880614
19890929
qqqqqqqqqqqqqqqqqqqq
45874587
LUO123456
19850206
19820810
fksdde7039
19880521
19811224
19850816
19830726
wwwwwwwwww
19820418
19890719
19860729
19830106
19840219
19840309
m1234567
117117117
19820123
20090101
19890614
19890709
19820401
19840505
wohenaini
huanying
520fagnsg
19830808
19820417
19850913
19830222
zaq12345
19890817
19830910
19840122
19820919
19811214
19890701
123456789x
19830206
harrypotter
19840917
19850720
951753852
562059487
19830323
19840520
19890108
sunyanzi
19890109
19901015
baobao520
112233123
314314314
19811101
19880730
19821227
19840912
5211314a
19891009
19890304
19860907
19840119
123qweqwe
19880514
zhangwen
sdsdsdsd
gaoxiang
19820813
19890313
19891203
zhangkun
123456sa
19820412
19830817
zhang1987
19820903
guoliang
19880502
aa112233
kingsoft
444444444
19860716
19880823
congcong
19840910
19850818
19820929
19810201
19860706
19850819
19840329
19830515
zsj201006
19900504
19890805
asdfghjkl123
111111121
ingtake1
19820211
19801016
112244abc
mimacuowu
19820906
100200100
h12345678
19850309
19880704
19880827
sasasasa
19870111
q5201314
31415927
19840304
xinxin13d
19820825
19890112
19820201
19840121
123456789m
1236547890
19890326
19880316
19860516
zoo-1573
abcdef123
19860411
19811007
bbscsdnnet
19890317
19830315
19850827
a89400ab
caocaocao
19800101
songyang
19860227
10121012
19850318
19850804
19890830
19890705
19811204
19850106
19880111
320320320
bai18dudu
19811109
zaglylc369
19890325
19850722
remember
19801028
19801210
19840604
19880706
19901016
xz123456
wltg2010
12345ssdlh
19860316
19860324
poiuytre
qwaszx123
07231564
0340412124abcd
19860702
19860515
19820107
19870330
19891204
19901024
19870420
19811126
19820617
liu12345
19820120
19820102
19820721
19890111
19840913
19810909
19860726
138138138
321456987
112233aa
19860117
19840726
19840512
qazwsx123456
19850114
19820823
19820923
19850223
110119110119
19840528
19860915
00001234
19850504
30303030
19830220
51888888
19860830
19840527
19890322
19890510
shenzhen
1h2h2h3h4h
ad123456
superstar
xiaoyong
158158158
19850201
nihaoma123
19880821
chengang
19850513
19811008
19890906
qwer4321
19820306
19840825
02020202
MOTOROLA
19830414
19840908
19850901
113113113
19810929
19820204
a123123a
19820114
19860509
19811122
19830725
19830316
19830114
19860804
chendong
19850506
19890725
19880713
85858585
19860711
19830425
19850421
12151215
19840701
qwerty12
19850529
19830814
19850203
19890518
19870414
19820427
12311231
19890328
happyhappy
19820805
www12345
shangxin
19811003
qunimade
www.csdn.net
19830130
198712821
19840618
19870326
19810202
31313131
19860517
19880407
19821113
19830729
19850328
19890409
19283746
19890718
19890627
19830914
19880716
WANGXING
19820109
19850915
wen123456
19890515
19850509
HY123456
008008008
13243546
123123asd
j123456789
19811031
19830311
891129aaa
19830115
19840922
19840816
zxcvbnm,./
19830718
19840514
33455432
19860627
19811103
19840318
19860424
19830917
19860328
19820722
19840617
19801130
19820121
19870730
19840115
74511940
19890726
19781028
19840108
19880322
ma123456
19901225
10221022
19820815
19820708
112233445
19810918
33336666
19801201
huanghui
19830719
19880607
asd123asd123
123321000
liuliuliu
19830613
19781218
19901019
19830812
19850906
19830811
131421131421
19830421
19890902
changjiang
1234567abc
19880719
19830208
19860628
19860604
19850814
108108108
19860121
19850803
19781010
19850227
f123456789
my123456
19860528
19801007
11111111q
123456dd
19870507
19840826
benjamin
19860525
19850321
19820322
19830512
19820528
wx123456
tianyuan
19890814
45685200
huanjue321
19820428
19820820
china2008
19850605
19850426
19820118
kxtn888cn
19820125
19880121
jkljkljkl
19820623
127127127
123456wang
19811102
19840522
a1314521
azxcvbnm
19890401
19830924
makelove
123ABCDE
19860111
19820506
66886688
19840109
19890802
19880328
19820212
az123456
19810920
19900201
19890225
19890428
19820302
33787943
19890922
12345677654321
19830615
19830614
huangyan
love1234
19840314
19820712
19840707
123456.0
19820726
12301230123
19820301
yangning
aaabbbccc
19860507
19810925
19901012
19860503
19860508
sxg007007
kisskiss
2012comeer
q00000000
baobei520
19890524
19850324
19840804
19870403
t123456789
wozhiaini
zhoufeng
13579000
19810609
66079350
187222455
19890819
19840408
19820328
01233210
19820622
19850905
19880522
1346798520
c120696363520
19820716
19820624
19801214
zxcvbnm123456
frenzy673954
19850419
123789abc
3714261985
nihaonihao
qwaszx12
19890723
68973435779
2582587758
19840325
chenxiao
zhangyun
44448888
13145211
19901990
19840324
19830105
123123000
19820128
19801011
wodeshijie
wwwswcy588
19850323
19820501
19850515
19820423
qqqwwweee
77582580
19840724
19830709
19811130
huaihuai
19811107
19860827
11011011
132132132
csdn2010
qingtian
19850528
794613852
guo123456
19880807
19840308
19830111
19830708
19850505
chenyong
19850423
65656565
zxcvbnml
0000oooo
19880717
19890505
sssssssss
19850616
19810924
19890227
19830806
10181018
19820901
kkk52789
qq520520
wodeai123
19860404
19811206
19791025
314159265358
lovejing
zhang1234
19840523
19890822
19850708
13145211314521
jiushini
zzzz1111
19830816
woxihuan
19811227
19840709
19890913
19801019
zhangchen
19830610
19890207
214214214
19830822
19801216
19890406
huyunqiao
998998998
19890629
19850712
19890729
wang1989
abcdef123456
19880705
19850604
woainimama
19880507
19830605
zhao123456
19880424
zxcvb123
woainilaopo
19850703
19890506
19820228
19810628
19890527
19820126
qwerfdsa
19820719
19820402
19830423
19840128
19801221
sunsunsun
19830112
weiyu371
19890521
19870930
xihuanni
12345678aa
3216732167
19830417
19830104
131415926
13654006821
chenpeng
zxcvbnm0
12340000
19880417
19840607
123321abc
19870117
19850213
woaibaobao
gjxhsgjxhs
881125881125
whosyourdaddy
19890512
19850319
19840703
19880222
19801023
19810916
19890702
19811002
f12345678
19850718
321456789
19820104
19791010
19830309
52013148
lixiaolong
28282828
19830927
lr1028829
19880527
wanghong
77587758
q123456q
114114114
19850729
311311311
19860119
19840412
19830628
19870508
asd123321
19840627
19830813
19820313
3135134162
df000000
19830724
19890911
ht123456
19890621
19860423
19820824
huangtao
19870527
19890426
19901011
24242424
19850409
84821742
82828282
19820426
19890714
19810926
19840502
19791228
19840428
19801027
19850307
19840323
19830318
7758258123
19810812
123.123.
19830703
19890423
19820603
19840817
19850311
3.1415926535
qwe123qwe123
csdnmima
19900125
19830603
19870514
19850329
babamama
68856636
abcde123
19810901
2582525775
1020304050
ilovechina
19901122
kk413200
33221100
19840819
baishikele
1qazxsw23edc
19840224
19850503
jiangfeng
19820218
19860622
19880410
19830127
19850829
19820227
19840812
19830818
19860613
19880323
19820419
19840118
19890908
19840907
19890513
19890121
19850702
zhangjia
19840608
19820830
19820407
19840508
19840107
19801005
cheng123
19850705
19890724
74108520963.
19820226
19901220
19870704
19810928
251314251314
19850619
19840221
13145201
zq123456
19880416
jylk1314
maomao123
c12345678
19781122
19820614
19811009
iloveyou123
xiaozhang
19840621
feng123456
19830227
w5201314
19840716
19890217
19890721
19820414
wobuaini
19890516
19830501
baobei521
19801001
19840401
19880403
19860407
112358132134
sb987654321
19791026
19830608
19890118
52005200
19820628
19840113
0o0o0o0o
nnnnmmmm
19801116
13709394
19830821
19860511
a1s2d3f4g5
19850113
19840521
19801024
19890711
19900102
19820610
19850610
19820213
19830930
963214785
19860309
12152205
19830128
123456jj
abc123321
mrf11277215
19830204
a520520aa
19850706
19830912
19820504
123456888
19820526
lingfeng
19801118
19901230
19870506
19830326
123789123
19801206
654321abc
19840710
12345678W
z1111111
nevergiveup
19901014
19830710
19900126
19901212
19860427
19830707
huahuahua
19850811
comeonbaby
19801217
19830228
qwertyuiop123
19830320
98188729
7758521.
19850511
19810923
19840316
19890429
sunflower
xiaoqian
19850728
19820222
admintus
zzzzxxxx
19811205
123456789aaa
123456xyz
19811202
fuckyou123
19880409
19850620
19811208
19890414
19850422
a123456.
WOSHINIMA
cinderella
tqtwffgcc
19820405
19860322
123qwe456
19820518
chencheng
zhangcheng
y1234567
35353535
826826826
19880630
19900520
19840806
19820516
12345678@
19801128
5201314q
19820206
19880413
b123456789
19890223
19801211
19830522
19850519
19820725
19840322
a123a123
86680101
yangpeng
19820105
19890708
12251225
19811211
19900214
`1234567
19820611
19830404
wangsheng
19850406
19820424
19860530
19840529
19890403
12345qaz
19820710
19840714
19860803
123321456654
19890917
000000123
ddddddddd
123456789000
16881688
19830217
a88888888
19840623
19890519
19830903
xiaoqing
19830223
831101qsl
19850802
19850521
11121314
19860727
19840611
zhang520
19820728
qwe123asd
19810824
19791127
19840517
52771314
19830117
19840722
19840903
19820209
www.163.com
19820215
19850627
19840905
12332145
19830325
1qaz!QAZ
19860330
yesterday
19890609
wushuang
19850717
19901026
1231231230
19840206
2718281828
19830602
11201120
zhang1988
19801129
19850719
tblkspthkr
19840306
19860409
19840103
369874125
rrrrrrrr
eeeeeeee
19850602
19830620
19820621
19860413
jia123456
19840418
CATHERINE
19850812
19860524
19801015
19830225
19810902
2870873859
19820320
woainiaa
19810808
00001107
19801003
19880131
huangxin
evangelion
19840525
flyinsky
19820627
19811229
19801026
19801017
confidence
basketball
wodemingzi
19830528
xianjian
woshiwo123
suzhou@562718
wangfang
19850527
jin123456
19820527
19791101
54545454
19830904
19830301
19890402
520131415
19820612
OOOO0000
19830429
325325325
chenglong
19781226
z123123123
jasajasa
19791220
2wsxcde3
19820907
19820615
19850629
19860222
19840301
7758258a
a123b123
19811220
ff123456
19900206
7418529630
52013143344
19820420
19820129
19830627
19890522
19840802
19781224
19810919
19840613
zero696295
147258
12345123
7758258258
19791030
1234567k
j2mv9jyyq6
19820519
19901218
k1234567
19820908
5845213344
abc147258
19880831
yangfeng
8975789757
aaabcabc
19830825
13245678
19830622
19840916
19850530
19840319
19830406
Love2008
20100101
19840320
000000aaa
hj123456
19820403
102030102030
19781123
19890610
19801107
lixy12345678
thinking
appleapple
woaini00
800800800
19860630
19850617
19830720
dfdfdfdf
haha1234
sh123456
19811114
19830408
19890226
19860225
19801103
lmj12345
110110110110
19860526
19850709
19820715
124578369
hao123.com
19830412
19840622
19830504
19840422
123456zzz
19820308
DINGDANG
hathaway
18273645
19890523
zhangfei
qqqaaazzz
19810823
19830819
19901030
32103210
xiaopang
z0000000
19900624
19801125
19840718
19830308
19781021
19830618
19781128
19820709
19840112
19820904
102102102
qq0000000
19840624
happiness
1234aaaa
19830604
feng19831
19820327
aaaaaa11
AAASSSDDD
646656xz7
anainima
10261026
19890827
asdfghjkl;
19830611
19820514
886886886
19840827
19900526
19880429
19840313
19850615
123456789987
11119999
19830129
boshigangchang
li7xi9bgn
19890903
19801126
19811030
19820106
19890517
19820811
19830305
19850208
19830803
19830829
19810217
198198198
xiaobing
3.141592654
liangwei
javajava
19890810
19830527
yuan1234
19850416
zhoujielun
19791123
19830525
19890704
19850904
19830514
qq7758258
asdf4321
fangyuan
19860502
11qqaazz
19830212
770880520
19890728
19830506
qqqqqq11
19830702
19840513
19830224
qiangqiang
19850306
19820305
xiaohong
11241124
19840519
aspirine
000000QQ
19820117
19840729
19901120
z11111111
wd123456
19850316
789852123
19820307
m12345678
zhou1234
aa5201314
19811201
19840515
zxasqw12
jackjack
19850428
19840914
ilovethisgame
19840620
00008888
LOVEFOREVER
19830619
123456678
liu5201314
19860519
24681012
19820713
wowowowo
19900818
19820116
19830503
qwerrewq
woaini123456
19840704
19830427
lu123456
19830824
19890511
19820409
888888889
````````
19890421
zhangXIANG
sjdf2ghjt
19820314
19771027
19900123
19830207
19850810
618618618
jfjscy8767
19860429
19890611
67676767
19890608
19830402
19890703
19860807
19811207
19850313
45678900
19901227
lp123456
woaini2008
zhanghan
zxcv123456
19870426
gertydrj
qqqq1234
feifei123
19850418
25897758
139139139
napoleon
19820406
19830526
12231223
han123456
hello1234
shenshen
8866810635956388
lx123456
19890905
1234567z
19820705
zhangqing
19840429
abc5201314
19820323
11123456
20070101
118118118
19840518
19890720
1rtj689d
19810214
19830324
asdzxc123
detective
123456ss
19880703
19888888
753951456
QAZ123456789
19820520
qazqaz123
326326326
112358112358
woshinidaye
250976046
896352639
qwer12345
christina
19830407
19820524
zhangyao
19860313
19870407
jianfeng
linxing7778
19830905
19781212
12345678abc
19830328
12345699
19900216
19850327
maomao520
04230423
19901027
19850408
19820110
19801021
wsadwsad
abc1234567
19811006
19820421
19840125
19830317
19811209
19850411
19850514
12593000
lionking
19840426
98741236
1234rewq
19791224
dldldldl
mhh123456
19830422
19830711
12365412
619030475
520131456
19791111
19820303
19801110
yangcheng
19830123
19840403
19890408
nuttertools
66666688
19890809
19901221
wingzero
19820221
cd123456
19840609
19830902
gg123456
guoguoguo
mm5201314
xiaoxiang
131420131420
19901125
aaaaaa123
19840628
123456789s
19840327
zhangdong
19801008
zhaopeng
19820319
415415415
19820523
19810828
0000000.
justlike
zxc123zxc
yan123456
hao12345
1305821983
19801119
jiubugaosuni
19810610
19830712
054821054821
19830705
19810325
19900811
19801111
19791214
19890529
cisco123
310310310
135246789
AAA111111
shuchang
a987654321
19900329
19801223
x3561668
19830108
wodemeng
111111112
27272727
19811222
19890502
19890224
zhaoming
12191219
19900512
wang4040955
13143344
19850730
123456**
19890907
3106028011
123456789!
410651410651
huanxiang
aaaaa11111
qazwsx1234
00000011
19871271987
qaz123qaz
19900104
19811110
19840516
19830607
19880331
19901215
he123456
ac123456
19900118
33334444
19840131
1a1a1a1a
19830827
19830728
20021212
zcwdclqscll
19901211
19850507
pojaablog
hehehehe
princess
liaoliao
19830508
www.baidu.com
fengling
19890622
19851031
19830507
zhouying
19840328
19830807
19840321
19890706
qq123654
13145201314
taotaotao
abcdefghi
19801013
19850524
zhang1986
19850817
huangjing
1230456789
f246865h
731731731
19850704
19830329
19850806
123abcabc
19850429
19801219
19781015
19901210
19830109
19801207
JI394SU3
19791119
19901124
19810521
lxsz60652
19781121
jianqiang
19810927
19830828
19790909
19820703
19801117
19820113
123zxc123
shuijing
19850714
nihaogxfc
846528ydt
12281228
19900110
19840222
19830121
19820625
linlinlin
34567890
choupiwang
19810312
19810912
19840705
19820205
19850222
19810801
wwwuploves
19791028
19890419
19820521
index0088
19850518
88998899
19890904
huanghua
fy641213
fengxiaofeng
19890716
78951230
12111211
jason123
19840715
19890804
0932313521
19820108
19900320
a0123456
19801106
gonzo1982
19900513
19830524
176219444
19890717
19811117
yydq1234
batistuta
showmethemoney
19840223
jiangjie
08290829
19900215
19801029
19830216
19830405
19850522
135521234
51885188
19850414
19840111
19830116
19900510
19791102
zhengwei
3.14159265
19811106
19790630
songjian
19830411
liuxiang
dddddddddd
19890602
34343434
19840511
19820317
19840407
liuzheng
19901224
19781211
19840423
19890417
shuishui
19811113
*963.*963.
123a123a
onepiece
19840603
chenqiang
chenying
csdn2009
123321ww
binbinbin
19840411
19840417
516516516
19901206
19860703
engineer
19820608
547896321
19791128
19941128
19800405
20100000
jianghua
jing006010
11251125
07070707
1r2t3y4u
19830314
wangwei123
zhangsan
19781111
11223311
12241224
sa123456
wangcong
19830716
22221111
admin123456
jiangshan
zhangmeng
123012301230
Jfew3289
19801127
19830722
19801114
19781012
windowss
19801102
19840507
19820819
52111314
82468246
19800819
zhangzhi
19791013
viewsonic
aaaaaa5061313
19901028
nishishei
19850225
19791024
maenbin1234
starstar
19880531
19830519
19830826
19901017
gutengda
19850330
zxcvbn123
YC123456
wangsong
a111111111
789987789
19860617
liudehua
19811005
1236987450
zz123123
19850512
526526526
19840415
19820607
19820602
133133133
19791226
19850117
10020000
shenxian
305488940
19830518
63636363
174292xy
19820605
123456789qwe
19830321
19820429
19900205
123456@@
xingkong
19850111
19840803
shinichi
19830401
218218218
19900801
xiaoguang
19801218
19810921
19830809
19820505
19850424
19840307
19760620
garfield
52000000
411192710
06060606
19850130
showtime
19791020
weiwei123
19840708
19820404
19820515
19781001
19830704
19791021
12141214
19840427
335201314
19840114
19810723
19830805
19850508
zhaoliang
99819981
syncmaster
135790135790
110120110120
19901214
wz123456
19810910
19900113
123qaz123
19820729
asdfgasdfg
1985111802
19850607
19880414
19791022
19801108
10161016
5201314abc
jiejiejie
36987412
11271127
19781225
wang1986
cj123456
19901002
753951123
19810304
wangshuo
12345asd
19790318
19830727
19840602
19850308
19850907
5203344587
19810922
wang1985
748159263
19830122
zhangxiao
81818181
19900124
ffflufei
19810915
61616161
77881230
19901013
ddzj49nb3
zhongguoren
ilikeyou
19791027
200802200802
123456789p
19880509
12345678qq
wodecsdn
19820310
kissalan
lz123456
zmczmc12
50206735020673
19900108
aidejiushini
19850404
19810905
33123456
12031203
19870331
19791104
t839k241
19830715
198710086
19820510
19810911
19840419
Romantic
128128128
p123456789
061112abc
19860425
19801122
45612312
zxcvbnmas
lw123456
19791979
xiaoping
221221221
123abcdef
19900820
987654321a
19801022
wozhidao
19830502
5213713752
729729729
sherlock
24682468
zhaojian
19890713
nihao123!
icecream
19781118
12340987
19820704
19850715
951753123
19810620
43420024
XM123123
19900208
19820220
19810826
12345abcd
19830426
kingkong
tinglove
19830908
131452100
19850630
19860609
qazwsxedc123
19840509
ch123456
xiao1234
19890407
tianfeng
as123123
19900227
19900203
19811118
52113141
19830517
chrdwhdhxt
10111011
19830907
1234567809
19840317
chenxing
19901021
584334421
cj7152821
quanquan
dby123456
windowswindows
99996666
19830419
jingling
wiii2ds1
qwerasdfzxcv
19801109
19781126
lovecsdn
19820130
19810621
19890727
baobaobao
53589793
liwei123
q1q2q3q4
loveyou1314
happy2008
1234567890-=
19901126
icandoit
19830617
19840616
19850417
19801229
19860704
dg123456789
19900105
129129129
19810310
19801009
woshi007
19840413
00000007
116116116
19810930
jiangbin
champion
876543210
24681357
19810825
19820613
10000001
scandisk
19850000
19810618
19810221
19811119
12346899
19830221
520131411
19810917
19830304
2wsx3edc
dadadada
9874563210
zxczxc123
77585201314
19820718
123456ly
01478963
19810606
510406030
birthday
welcome123
19850427
fengjian
xiaozhuzhu
19810203
16899052
19860221
1z2x3c4v
fishfish
zxcvasdf
19860323
19791012
19840717
yy5201314
85411165
19820311
19791015
19781026
521125521125
19850710
wodeweilai
19791023
19781025
jinjinjin
fengpeng
19781023
13128255707
521521521521
19890617
happy2000
19830616
19890430
19810601
110112113
dajiahao
hangzhou
19791008
123321qwe
19900505
trustno1
04061018
cy123456
852741963
19900916
xxxxxxxxx
asddsaasd
19830516
CZ123456
87651234
19900516
19781230
19850402
19810307
456123456
ZXCVBNMMNBVCXZ
19810623
19810720
19890507
159357159
1234567.
19810228
90332773
19801018
000000001
y12345678
023023023
19900523
19850526
194983521
56785678
19900707
ssssssssss
19781210
19801205
13919936479
19901101
wc625799
19820503
justsoso
19830804
123456789..
19820806
19850813
lc123456
01314520
19890509
19890630
12101210
75395100
77885210
254044375
19850413
19830415
19830706
wangkang
95959595
hongjuan
19810527
19850119
19820803
z1x2c3v4
19830330
19901117
00724015
19830410
19820122
0000aaaa
19901130
01234567890
19840503
wwwcsdnnet
sleigh1368
19900525
19781220
12300a00a
19820324
19810301
professional
19810619
19900321
zhangping
aceracer
084119084123
19810815
daydayup
19901105
19900503
19781022
guangguang
lvfeididi
19791218
mlf12345
0p9o8i7u
19890422
yueyue521
19900219
123321aaa
wangling
10211021
19830125
zhuang123
19820111
569874123
19791120
19781018
liuyang123
xiaojiang
chinachina
26511314
d123456789
1314520QQ
19901213
cx123456
19810116
andyandy
316316316
b12345678
zhangting
ruanjian
zhao1234
aa12345678
19820804
19820629
ww111111
456852852
woaideren
19781011
shandong
fy123456
aaaqqq123
12021202
141592653
lbjx9j11
l123l456
19900301
987456123
ericsson
520131488
xingaixing
00112900
xiaochao
19801204
t1234567
19900213
19820329
528528528
weiwei520
a77493618
54185418
88888888A
jianglei
19840805
19810306
xiaomeng
zxcvbnmasdfghjkl
lishuang
19820724
cocacola
cheng428
000000..
19840619
langlang
19820411
19820330
woaini111
ls123456
20042008
zxc123456789
790297648
19820717
19830119
success777
nihaihaoma
19810816
19810510
19791108
19791217
123456111
k12345678
19820525
hx2010seo
liucheng
123456ff
19900109
sky123456
tongyong
66700388
13628792658
k123456789
abcde1234
19850516
123abc123abc
555666777
19810126
19801105
203203203
19890514
13142659
as123456789
wodedipan
XiaoWang
abcdefghijklmn
hibernate
19900228
19801224
19781120
45683968
xm123456
woaitl520
520131499
12347890
19781104
19830529
19781221
19830118
lightning
19771126
19830416
19901008
19781223
14725800
hengheng
rifhv123456
19781101
zhangbing
74185296
852852852
318318318
184866884
jinsheng
19820112
asd00000
liujiang
159753159
19900314
19810121
jianghao
Sina.com
19901123
chouchou
85602222
19791213
66667777
19840524
492357816
abcdefghijk
7758521000
19811004
197912wsr
19820702
19850716
5201314159
chenqian
88880000
19810705
19781215
19830107
huangkai
19790916
FANG123456
huanghai
123456www
shenjian
19771010
19850713
19901102
lz11180607
goodmorning
haha123456
wh123456
19810416
19901029
2999811983
123654aa
woaini110
woaimeinv
yh123456
iiiiiiii
74747474
19801231
19810212
19900824
20072008
YL123456
135791113
tang123456
19791208
1234567895
iloveyou520
19810425
baobao521
system32
xiaolang
19810712
sx123456
19850118
19840930
13456789
124124124
07742216
feifei521
33338888
19820616
lb123456
qasdfrghy1
19810219
123456789999
e65351656
19810311
wangjuan
19870430
xiaobudian
19830523
woaibabamama
19791109
19900312
19810206
456852456
t12345678
19820609
yx12345678
qingshan
19781116
19810707
19810528
programmer
11122233
caonima1
19820530
19810119
19830830
19840730
qazwsxqazwsx
qiaoqiao
19810617
woshiyizhiyu
19810719
19901001
wuyongchong
qsd34678321
19900910
748748748
04200116
wsw800168
19900306
19830723
logitech
19761976
12345678x
7215217758
19791212
0123654789
yangxiao
10191019
19901129
136136136
rootroot
19830717
tianlong
84423196
xiaoyu520
zzxxccvv
19830428
19880619
12171217
yingxiong
19901115
cao123456
19900209
a11223344
19901128
daohaosima
21436587
19840402
19790315
19781113
aaaaaaa1
sunmenga1
19901110
11011101
FAN123456
19810813
19810721
qw123123
jiangyan
30798764681
19801115
pass@word1
56781234
qingsong
12351235
19810124
614614614
19810627
marlboro
yangling
xiaokang
19810314
weiqiang
12261226
jiangjian
19810318
huangjin
19900207
wangyonglang
19830609
19810504
112233qq
6668881204511
jiangxin
13530023404
7894561230.
19830424
hhhhhhhhhh
1a2s3d4f5g
explorer
cai123456
19810320
19771020
wwwyhjd198cn
zhoujing
xxx123456
19901111
tc123456
82537724
517517517
668668668
lh123456
12041204
jennifer
111111qqq
s5201314
5201314584
19790822
huangjun
010203040506
woaiwolaopo
aiwosuoai
545007273
xiongxiong
kongkong
yangqing
Hellocsdn
19781203
c3df32ea
19900218
123asdfg
19900415
19900122
123456789b
789512357
sw123456
A123456789a
yangshuai
19820508
19791105
19870831
yangzhen
52100000
qq5211314
19890427
03030303
19810213
19901112
wangjile
19800312
19781228
1593572468
12349876
w4cuvji2
youloveme
19830420
beckham7
woaijing
19900711
qa123456
19900211
lk123456
5845131420
ai123456
19890528
65432111
7758258520
killer123
19850401
yygjmy333
19900718
shenlong
8012160713
XIONGWEI
eyesonme
question
20092010
319319319
89080620084
qwertyuiop[]
19810629
19791125
624624624
sunupsun
191154120
19791225
19781112
584521584521
19800613
19900202
7758521qq
yuanchao
woshishuaige
16897188
system123
9632587410
redapple
19900217
8630516boss
jianglong
19790203
zhang521
19810817
19810624
2876180com
simileplun
00998877
19820207
19810711
4444444444
javazfj1jie
happy520
100000000
502502502
19820517
999666333
19810412
yangbing
885201314
19850403
20080101
ty123456
19791229
06124308030
19491001abc
zhangchi
19791019
19901003
2269439999
19900210
521125521
19781202
15890193764
19901127
woaiwodejia
012520wadll
qwedsazxc
19820321
122122122
19820604
qazwsx11
58080596547
happy2010
zhangquan
doraemon
19870531
happy1314
19810701
woaini12
19850930
xiang123
woaini99
19810626
shuangshuang
27182818
19800812
xiaotang
0111424dd
liushuai
abc651114
19830511
23242526
19810327
111111222222
panasonic
maikuraki
19781115
xinxinxin
happy2009
zhan1234
19900307
19850517
likelike
19791029
sl123456
19800216
123987456
19971997
zhaofeng
4294967296
123456lj
liverpool
19800916
woaini123
123456654321
ffffffff
1122334455
12369874
1234567a
12345679
123456aaa
buzhidao
weblogic
weblogic123
11111111
weblogic123456
0
1
2
3
4
5
6
7
8
9
！
@
#
¥
%
^
&
*
(
)
-
_
=
+
`
~
·
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
<
>
,
.
?
/
;
'
[
]


path: scripts/openclaw_zipcracker.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import os
import subprocess
import sys
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
CLI_BY_LANG = {
    "zh": SCRIPT_DIR / "ZipCracker.py",
    "en": SCRIPT_DIR / "ZipCracker_en.py",
}
INSPECTOR_PATH = SCRIPT_DIR / "zipcracker_inspect.py"


def default_lang() -> str:
    for env_name in ("ZIPCRACKER_SKILL_LANG", "LC_ALL", "LC_MESSAGES", "LANG"):
        value = os.environ.get(env_name, "").strip().lower()
        if value.startswith("en"):
            return "en"
        if value.startswith("zh"):
            return "zh"
    return "zh"


def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
    parser = argparse.ArgumentParser(
        description="OpenClaw-friendly wrapper around the bundled ZipCracker skill."
    )
    parser.add_argument(
        "--lang",
        choices=("zh", "en"),
        default=default_lang(),
        help="Choose the bundled Chinese or English CLI.",
    )
    parser.add_argument(
        "--profile",
        action="store_true",
        help="Inspect the ZIP first and print a human-readable strategy summary.",
    )
    parser.add_argument(
        "--profile-json",
        action="store_true",
        help="Inspect the ZIP first and emit structured JSON.",
    )
    parser.add_argument(
        "--auto-crc",
        action="store_true",
        help="Auto-confirm short-plaintext CRC32 enumeration prompts.",
    )
    parser.add_argument(
        "--auto-large-mask",
        action="store_true",
        help="Auto-confirm huge mask warnings.",
    )
    parser.add_argument(
        "--auto-template-kpa",
        action="store_true",
        help="Auto-confirm built-in template KPA suggestions after default attacks fail.",
    )
    parser.add_argument(
        "--skip-dict-count",
        action="store_true",
        help="Skip upfront line counting for large dictionaries.",
    )
    parser.add_argument(
        "--skip-orig-password-recovery",
        action="store_true",
        help="Skip original password recovery after bkcrack-based extraction succeeds.",
    )
    parser.add_argument(
        "--allow-install-prompts",
        action="store_true",
        help="Allow pyzipper/bkcrack install prompts instead of forcing non-interactive skip.",
    )
    return parser.parse_known_args(argv)


def build_env(args: argparse.Namespace) -> dict[str, str]:
    env = os.environ.copy()
    if not args.allow_install_prompts:
        env.setdefault("ZIPCRACKER_AUTO_INSTALL_PYZIPPER", "0")
        env.setdefault("ZIPCRACKER_AUTO_INSTALL_BKCRACK", "0")
    if args.auto_crc:
        env["ZIPCRACKER_ASSUME_CRC_RECOVERY"] = "1"
    if args.auto_large_mask:
        env["ZIPCRACKER_ASSUME_LARGE_MASK"] = "1"
    if args.auto_template_kpa:
        env["ZIPCRACKER_ASSUME_TEMPLATE_KPA"] = "1"
    if args.skip_dict_count:
        env["ZIPCRACKER_SKIP_DICT_COUNT"] = "1"
    if args.skip_orig_password_recovery:
        env["ZIPCRACKER_SKIP_ORIG_PW_RECOVERY"] = "1"
    return env


def main(argv: list[str] | None = None) -> int:
    args, inner_args = parse_args(argv or sys.argv[1:])
    if inner_args and inner_args[0] == "--":
        inner_args = inner_args[1:]
    if not inner_args:
        inner_args = ["--help"]

    if args.profile or args.profile_json:
        inspect_args = [str(INSPECTOR_PATH), "--lang", args.lang]
        if args.profile_json:
            inspect_args.append("--json")
        inspect_args.extend(inner_args)
        completed = subprocess.run([sys.executable, *inspect_args], check=False)
        return completed.returncode

    cli_path = CLI_BY_LANG[args.lang]
    env = build_env(args)
    command = [sys.executable, str(cli_path), *inner_args]
    completed = subprocess.run(command, env=env, check=False)
    return completed.returncode


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/ZipCracker_en.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

from zipcracker_core import run_cli


if __name__ == "__main__":
    sys.exit(run_cli("en"))


path: scripts/zipcracker_inspect.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import os
import shlex
import sys
import tempfile
import zipfile
from pathlib import Path
from typing import Any

from zipcracker_core import (
    HAS_PYZIPPER,
    collect_archive_encryption_profile,
    compress_type_label,
    detect_template_kpa_suggestions,
    find_best_verification_entry,
    fix_zip_encrypted,
    format_os_label,
    detect_os_info,
    get_bkcrack_version,
    is_regular_member,
    is_zip_encrypted,
    loc,
    _has_winzip_aes_extra,
    find_bkcrack_executable,
)


SCRIPT_DIR = Path(__file__).resolve().parent
WRAPPER_PATH = SCRIPT_DIR / "openclaw_zipcracker.py"


def default_lang() -> str:
    for env_name in ("ZIPCRACKER_SKILL_LANG", "LC_ALL", "LC_MESSAGES", "LANG"):
        value = os.environ.get(env_name, "").strip().lower()
        if value.startswith("en"):
            return "en"
        if value.startswith("zh"):
            return "zh"
    return "zh"


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Inspect a ZIP archive and suggest the best ZipCracker attack path."
    )
    parser.add_argument("zip_path", help="Target ZIP file")
    parser.add_argument(
        "--lang",
        choices=("zh", "en"),
        default=default_lang(),
        help="Output language",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Emit structured JSON instead of the human summary.",
    )
    parser.add_argument(
        "--max-entries",
        type=int,
        default=50,
        help="Maximum number of member entries to include in the report.",
    )
    return parser.parse_args(argv)


def shell_quote(value: str) -> str:
    return shlex.quote(value)


def detect_pseudo_encryption(zip_path: str) -> dict[str, Any]:
    if not os.path.exists(zip_path):
        return {"status": "missing"}
    if not is_zip_encrypted(zip_path):
        return {"status": "not-encrypted"}

    fd, temp_path = tempfile.mkstemp(prefix="zipcracker_inspect_", suffix=".fixed.zip")
    os.close(fd)
    try:
        fix_zip_encrypted(zip_path, temp_path)
        with zipfile.ZipFile(temp_path, "r") as zf:
            bad_member = zf.testzip()
        if bad_member:
            return {"status": "not-confirmed", "detail": f"bad member: {bad_member}"}
        return {"status": "confirmed"}
    except Exception as exc:
        return {"status": "not-confirmed", "detail": str(exc)}
    finally:
        try:
            os.remove(temp_path)
        except OSError:
            pass


def collect_entries(zip_path: str, max_entries: int) -> dict[str, Any]:
    result: dict[str, Any] = {
        "entries": [],
        "total_regular_entries": 0,
        "truncated": False,
        "best_verification_entry": None,
    }
    with zipfile.ZipFile(zip_path, "r") as zf:
        infos = [info for info in zf.infolist() if is_regular_member(info)]
        result["total_regular_entries"] = len(infos)
        result["best_verification_entry"] = find_best_verification_entry(zf)
        for index, info in enumerate(infos):
            if index >= max_entries:
                result["truncated"] = True
                break
            result["entries"].append(
                {
                    "name": info.filename,
                    "file_size": info.file_size,
                    "compress_size": info.compress_size,
                    "compress_type": compress_type_label(info.compress_type),
                    "encrypted": bool(info.flag_bits & 0x1),
                    "aes": bool(_has_winzip_aes_extra(info)),
                    "short_plaintext_candidate": 0 < info.file_size <= 6,
                }
            )
    return result


def build_recommendations(report: dict[str, Any], locale: str) -> list[dict[str, str]]:
    zip_path = report["zip_path"]
    wrapper = str(WRAPPER_PATH)
    quoted_wrapper = shell_quote(wrapper)
    quoted_zip = shell_quote(zip_path)
    recommendations: list[dict[str, str]] = []

    def add(title_zh: str, title_en: str, reason_zh: str, reason_en: str, args: list[str]) -> None:
        command = " ".join([shell_quote(sys.executable), quoted_wrapper, *args, quoted_zip])
        recommendations.append(
            {
                "title": loc(locale, title_zh, title_en),
                "reason": loc(locale, reason_zh, reason_en),
                "command": command,
            }
        )

    if not report["is_valid_zip"]:
        return recommendations

    if not report["is_encrypted"]:
        add(
            "直接解压/校验",
            "Extract directly",
            "当前档案没有发现加密条目，不需要走破解流程。",
            "No encrypted members were detected, so cracking is unnecessary.",
            [],
        )
        return recommendations

    pseudo_status = report["pseudo_encryption"]["status"]
    if pseudo_status == "confirmed":
        add(
            "优先按伪加密处理",
            "Handle as pseudo-encryption first",
            "清除加密位后校验通过，极可能是伪加密题。",
            "Clearing the encryption bit validated successfully, so this strongly resembles pseudo-encryption.",
            [],
        )
        return recommendations

    if report["short_plaintext_entries"]:
        add(
            "先试短明文 CRC32",
            "Try short-plaintext CRC32 first",
            "存在 1-6 字节的条目，适合先走短明文 CRC32 枚举恢复。",
            "The archive contains 1-6 byte entries, so short-plaintext CRC32 enumeration is a strong first move.",
            ["--auto-crc"],
        )

    if report["template_suggestions"]:
        add(
            "默认流程并自动跟进模板 KPA",
            "Run default flow with template KPA follow-up",
            "检测到与 png/zip/exe/pcapng 模板相符的加密条目。",
            "Encrypted members look compatible with built-in png/zip/exe/pcapng template KPA cases.",
            ["--auto-template-kpa"],
        )

    add(
        "先跑标准默认流程",
        "Run the standard default flow",
        "先让引擎按原始策略依次尝试伪加密、默认字典、数字字典，再决定是否切换其他战术。",
        "Let the engine follow its original strategy first: pseudo-encryption check, bundled dictionary, numeric fallback, then escalate if needed.",
        [],
    )

    if report["encryption_profile"]["aes_entries"] > 0 and not report["pyzipper_ready"]:
        recommendations.append(
            {
                "title": loc(locale, "AES 注意事项", "AES caution"),
                "reason": loc(
                    locale,
                    "该包含有 AES 条目，但当前环境未启用 pyzipper。掩码/字典前应优先补齐 AES 依赖。",
                    "This archive contains AES entries, but pyzipper is not enabled. Install/enable AES support before relying on dictionary or mask workflows.",
                ),
                "command": "",
            }
        )

    if report["bkcrack_available"]:
        recommendations.append(
            {
                "title": loc(locale, "已知明文时优先用 KPA", "Prefer KPA when plaintext exists"),
                "reason": loc(
                    locale,
                    "系统已检测到 bkcrack；如果你手里有完整/部分明文、参考 ZIP 或文件头线索，应优先切到 KPA 路线。",
                    "bkcrack is available; if you have full/partial plaintext, a reference ZIP, or strong file-signature clues, prefer the KPA path over blind brute force.",
                ),
                "command": "",
            }
        )

    return recommendations


def build_report(zip_path: str, locale: str, max_entries: int) -> dict[str, Any]:
    report: dict[str, Any] = {
        "zip_path": os.path.abspath(zip_path),
        "is_valid_zip": False,
        "exists": os.path.exists(zip_path),
        "is_encrypted": False,
        "pyzipper_ready": bool(HAS_PYZIPPER),
        "bkcrack_available": False,
        "bkcrack_version": None,
        "system_label": format_os_label(locale, detect_os_info()),
        "encryption_profile": {},
        "pseudo_encryption": {"status": "unknown"},
        "entries": [],
        "total_regular_entries": 0,
        "entries_truncated": False,
        "best_verification_entry": None,
        "short_plaintext_entries": [],
        "template_suggestions": [],
        "recommendations": [],
    }

    if not report["exists"]:
        report["recommendations"] = []
        return report

    try:
        with zipfile.ZipFile(zip_path, "r"):
            pass
    except Exception as exc:
        report["invalid_reason"] = str(exc)
        return report

    report["is_valid_zip"] = True
    report["is_encrypted"] = is_zip_encrypted(zip_path)
    report["encryption_profile"] = collect_archive_encryption_profile(zip_path)
    report["pseudo_encryption"] = detect_pseudo_encryption(zip_path)

    entry_info = collect_entries(zip_path, max_entries)
    report["entries"] = entry_info["entries"]
    report["total_regular_entries"] = entry_info["total_regular_entries"]
    report["entries_truncated"] = entry_info["truncated"]
    report["best_verification_entry"] = entry_info["best_verification_entry"]
    report["short_plaintext_entries"] = [
        entry for entry in report["entries"] if entry["short_plaintext_candidate"]
    ]

    suggestions = detect_template_kpa_suggestions(zip_path, locale)
    report["template_suggestions"] = [
        {
            "inner_name": item.inner_name,
            "template_name": item.template_name,
            "file_size": item.file_size,
            "compress_type": compress_type_label(item.compress_type),
            "confidence": item.confidence,
            "reason": item.reason,
        }
        for item in suggestions
    ]

    bk_path = find_bkcrack_executable()
    if bk_path:
        report["bkcrack_available"] = True
        report["bkcrack_path"] = bk_path
        report["bkcrack_version"] = get_bkcrack_version(bk_path)

    report["recommendations"] = build_recommendations(report, locale)
    return report


def render_summary(report: dict[str, Any], locale: str) -> str:
    lines: list[str] = []
    lines.append(loc(locale, f"[+] 目标 ZIP: {report['zip_path']}", f"[+] Target ZIP: {report['zip_path']}"))
    if not report["exists"]:
        lines.append(loc(locale, "[-] 文件不存在。", "[-] File does not exist."))
        return "\n".join(lines)
    if not report["is_valid_zip"]:
        reason = report.get("invalid_reason", "")
        lines.append(loc(locale, f"[-] 不是有效 ZIP: {reason}", f"[-] Not a valid ZIP: {reason}"))
        return "\n".join(lines)

    lines.append(
        loc(
            locale,
            f"[+] 常规文件条目: {report['total_regular_entries']}，加密条目: {report['encryption_profile'].get('encrypted_entries', 0)}，未加密条目: {report['encryption_profile'].get('unencrypted_entries', 0)}",
            f"[+] Regular members: {report['total_regular_entries']}, encrypted: {report['encryption_profile'].get('encrypted_entries', 0)}, unencrypted: {report['encryption_profile'].get('unencrypted_entries', 0)}",
        )
    )
    lines.append(
        loc(
            locale,
            f"[*] 加密构成: ZipCrypto={report['encryption_profile'].get('zipcrypto_entries', 0)}, AES={report['encryption_profile'].get('aes_entries', 0)}",
            f"[*] Encryption mix: ZipCrypto={report['encryption_profile'].get('zipcrypto_entries', 0)}, AES={report['encryption_profile'].get('aes_entries', 0)}",
        )
    )
    lines.append(
        loc(
            locale,
            f"[*] pyzipper: {'已启用' if report['pyzipper_ready'] else '未启用'}，bkcrack: {'可用' if report['bkcrack_available'] else '不可用'}",
            f"[*] pyzipper: {'enabled' if report['pyzipper_ready'] else 'unavailable'}, bkcrack: {'available' if report['bkcrack_available'] else 'unavailable'}",
        )
    )
    if report["bkcrack_available"] and report.get("bkcrack_version"):
        lines.append(
            loc(
                locale,
                f"[*] bkcrack 版本: {report['bkcrack_version']}",
                f"[*] bkcrack version: {report['bkcrack_version']}",
            )
        )

    pseudo = report["pseudo_encryption"]
    pseudo_status_map = {
        "confirmed": loc(locale, "可直接按伪加密处理", "confirmed pseudo-encryption"),
        "not-confirmed": loc(locale, "未确认伪加密，更像真加密", "not confirmed; likely truly encrypted"),
        "not-encrypted": loc(locale, "未发现加密", "archive is not encrypted"),
        "missing": loc(locale, "文件不存在", "file missing"),
        "unknown": loc(locale, "未知", "unknown"),
    }
    lines.append(
        loc(
            locale,
            f"[*] 伪加密判断: {pseudo_status_map.get(pseudo['status'], pseudo['status'])}",
            f"[*] Pseudo-encryption check: {pseudo_status_map.get(pseudo['status'], pseudo['status'])}",
        )
    )

    if report["best_verification_entry"]:
        lines.append(
            loc(
                locale,
                f"[*] 推荐验证条目: {report['best_verification_entry']}",
                f"[*] Best verification entry: {report['best_verification_entry']}",
            )
        )

    if report["short_plaintext_entries"]:
        names = ", ".join(
            f"{item['name']}({item['file_size']}B)" for item in report["short_plaintext_entries"]
        )
        lines.append(
            loc(
                locale,
                f"[*] 短明文候选: {names}",
                f"[*] Short-plaintext candidates: {names}",
            )
        )

    if report["template_suggestions"]:
        lines.append(loc(locale, "[*] 模板 KPA 候选:", "[*] Template KPA candidates:"))
        for item in report["template_suggestions"]:
            lines.append(
                loc(
                    locale,
                    f"    - {item['inner_name']} -> {item['template_name']} ({item['confidence']}, {item['compress_type']}, {item['file_size']}B)",
                    f"    - {item['inner_name']} -> {item['template_name']} ({item['confidence']}, {item['compress_type']}, {item['file_size']}B)",
                )
            )

    if report["entries"]:
        lines.append(loc(locale, "[*] 成员概览:", "[*] Member overview:"))
        for item in report["entries"]:
            tag_parts = []
            if item["encrypted"]:
                tag_parts.append("enc")
            if item["aes"]:
                tag_parts.append("aes")
            if item["short_plaintext_candidate"]:
                tag_parts.append("short")
            tag_text = ",".join(tag_parts) if tag_parts else "plain"
            lines.append(
                f"    - {item['name']} | {item['file_size']}B | {item['compress_type']} | {tag_text}"
            )
        if report["entries_truncated"]:
            lines.append(
                loc(locale, "    - ... 其余条目已省略", "    - ... additional entries omitted")
            )

    if report["recommendations"]:
        lines.append(loc(locale, "[+] 推荐下一步:", "[+] Recommended next steps:"))
        for index, rec in enumerate(report["recommendations"], start=1):
            lines.append(f"{index}. {rec['title']}")
            lines.append(f"   {rec['reason']}")
            if rec["command"]:
                lines.append(f"   {rec['command']}")

    return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv or sys.argv[1:])
    report = build_report(args.zip_path, args.lang, args.max_entries)
    if args.json:
        print(json.dumps(report, ensure_ascii=False, indent=2))
    else:
        print(render_summary(report, args.lang))
    return 0 if report["exists"] and report["is_valid_zip"] else 1


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/ZipCracker.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

from zipcracker_core import run_cli


if __name__ == "__main__":
    sys.exit(run_cli("zh"))


path: agents/openai.yaml
content: interface:
  display_name: "ZipCracker"
  short_description: "World-class ZIP CTF cracking, KPA, and recovery workflows"
  default_prompt: "Use $zipcracker like a top-tier ZIP CTF solver: profile this archive, choose the strongest clue-driven path, run the bundled workflow, and explain the exact command, rationale, and result."
policy:
  allow_implicit_invocation: true


path: references/natural-language-command-examples.md
content: # Natural Language to Command Examples

## Table of Contents

- Generic triage
- Dictionary-driven requests
- Mask-driven requests
- Known-plaintext requests
- Template-KPA requests
- Extraction-first requests

## Generic triage

| User request | Recommended command | Why |
| --- | --- | --- |
| 帮我分析一下这个 ZIP，先别乱跑。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip>` | Clue set is weak, so profile first. |
| 这个压缩包打不开，先看看是不是伪加密。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip>` | Pseudo-encryption suspicion should be checked before brute force. |
| Crack this ZIP like a CTF challenge. | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip>` then `python3 <skill-dir>/scripts/openclaw_zipcracker.py --auto-template-kpa <zip>` | Default world-class flow: inspect, then run the original engine path. |
| 先告诉我最值得跑的命令。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip>` | The profile output includes recommended next commands. |

## Dictionary-driven requests

| User request | Recommended command | Why |
| --- | --- | --- |
| 用这个字典试一下这个 ZIP。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> <dict.txt>` | User already supplied the strongest clue. |
| 跑一下这个目录里的多个字典。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> <dict-dir>` | The bundled engine supports dictionary directories. |
| 这是个超大字典，别先数行数。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --skip-dict-count <zip> <huge-dict.txt>` | Skip upfront counting for better startup and steadier memory. |
| Just try the usual dictionaries first. | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip>` | Uses the bundled default dictionary sequence. |

## Mask-driven requests

| User request | Recommended command | Why |
| --- | --- | --- |
| 密码应该是四位数字。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -m '?d?d?d?d'` | Natural-language structure maps cleanly to a mask. |
| The password starts with `flag` and ends with two digits. | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -m 'flag?d?d'` | Strong structure clue, tighter than a generic wordlist. |
| 形态像一位大写、三位小写、三位数字。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -m '?u?l?l?l?d?d?d'` | Exact password shape is better than brute-force dictionaries. |
| 这个掩码很大，但我确认要跑。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --auto-large-mask <zip> -m '<mask>'` | Auto-confirms the large-mask safety prompt. |

## Known-plaintext requests

| User request | Recommended command | Why |
| --- | --- | --- |
| 我有一份已知明文，帮我走 KPA。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <plain.txt>` | Full plaintext is a stronger clue than dictionaries or masks. |
| 我有一个无密码 ZIP 参考包。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <plain.zip>` | Reference ZIPs are directly supported by the engine. |
| 我只知道一段明文在偏移 78 的位置。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <part.bin> --kpa-offset 78` | Partial KPA with explicit offset. |
| 除了偏移明文，我还知道开头是 `MZ`。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <part.bin> --kpa-offset 78 -x 0 4d5a` | Extra fixed bytes strengthen partial KPA. |
| 只用 `bkcrack`，不要回退其他方法。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> -kpa <plain.txt> --bkcrack` | Enforces a pure `bkcrack` recovery path. |

## Template-KPA requests

| User request | Recommended command | Why |
| --- | --- | --- |
| 这个压缩包里的文件像 PNG，试试模板明文攻击。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> --kpa-template png -c image.png` | Direct template KPA request. |
| The encrypted member looks like an EXE. | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> --kpa-template exe -c app.exe` | EXE templates are built in. |
| 看起来像 pcapng 包头。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py <zip> --kpa-template pcapng -c capture.pcapng` | Maps directly to a bundled template family. |
| 跑完整默认流，失败了就自动跟模板 KPA。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --auto-template-kpa <zip>` | Lets the engine escalate to template KPA when structure suggests it. |

## Extraction-first requests

| User request | Recommended command | Why |
| --- | --- | --- |
| 我只想把文件先拿出来，不在乎原始密码。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --skip-orig-password-recovery <zip> -kpa <plain.txt>` | Skip post-extraction password recovery. |
| 先恢复内容，原始 ZIP 密码以后再说。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --skip-orig-password-recovery <zip> -kpa <plain.txt>` | Same extraction-first preference. |
| 只要能解出来就行。 | `python3 <skill-dir>/scripts/openclaw_zipcracker.py --profile <zip>` | First inspect, then choose the quickest high-confidence route. |


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)