# Copyright (C) 2026 CoelhoFZ
# SPDX-License-Identifier: GPL-3.0-only
# This file is part of unlocker-CoelhoFZ (GPLv3, see LICENSE).

"""pack_payload.py - 2-STAGE packer (stub + mapper + payload).

Usage (build.sh --harden):
  # payload (BLOB2 = the shim) -> payload_key.h + payload.bin
  python3 tools/pack_payload.py gen --shim build/payload.dll \\
      --key build/payload_key.h --blob build/payload.bin

  # mapper (BLOB1 = decrypts the payload) -> stub_cfg.h + mapper.bin
  python3 tools/pack_payload.py gen-mapper --shim build/mapper.dll \\
      --key build/stub_cfg.h --blob build/mapper.bin --magic build/magic.bin

  # embeds both as an overlay in winmm
  python3 tools/pack_payload.py append --winmm dist/winmm.dll \\
      --blob2 build/payload.bin --blob1 build/mapper.bin --magic build/magic.bin

  # two-pass of the stub self-CRC (after pass 1 and the append)
  python3 tools/pack_payload.py set-crc --key build/stub_cfg.h --crc <u32>

Final overlay in winmm.dll (fixed 12-byte trailer):
  [BLOB2 payload][BLOB1 mapper][u32 size_blob2][u32 size_blob1][u32 magic]

gen:       random PER-BUILD key for the payload (payload_key.h, RAW - the
           consumer is the mapper, which is encrypted) + encrypted blob
           (ChaCha20).
gen-mapper:random PER-BUILD key for the mapper (stub_cfg.h, words MASKED
           with 0x9E3779B9 - the stub unmasks at runtime) + random trailer
           magic (build/magic.bin) + encrypted blob (ChaCha20).

ChaCha20 (RFC 8439): 8-word key + 3-word nonce generated PER BUILD. The
blob plaintext ('MZ' of the PEs) recovers nothing: without the key there is
no decryption (unlike the old LCG, broken by known plaintext). Byte-for-byte
contract identical to common/chacha20.h.
set-crc:   writes the self-check constant (.text CRC) into stub_cfg.h,
           already masked (2-pass build: pass1 -> crc -> pass2 identical).

The plaintext of the two PEs only exists HERE in build/ (not versioned);
the final winmm.dll only contains the encrypted blobs + the trailer.
"""

import argparse
import random
import re
import struct
import zlib

MASK = 0x9E3779B9  # mask of the constants in the stub (same as stub_loader.cpp)


def _rotl(v, c):
    return ((v << c) | (v >> (32 - c))) & 0xFFFFFFFF


def _qr(x, a, b, c, d):
    """One ChaCha20 quarter-round (order identical to common/chacha20.h)."""
    x[a] = (x[a] + x[b]) & 0xFFFFFFFF
    x[d] ^= x[a]
    x[d] = _rotl(x[d], 16)
    x[c] = (x[c] + x[d]) & 0xFFFFFFFF
    x[b] ^= x[c]
    x[b] = _rotl(x[b], 12)
    x[a] = (x[a] + x[b]) & 0xFFFFFFFF
    x[d] ^= x[a]
    x[d] = _rotl(x[d], 8)
    x[c] = (x[c] + x[d]) & 0xFFFFFFFF
    x[b] ^= x[c]
    x[b] = _rotl(x[b], 7)


def _block(key, ctr, nonce):
    """64-byte keystream block. key=8 words, nonce=3 words."""
    st = ([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
          + [w & 0xFFFFFFFF for w in key]
          + [ctr & 0xFFFFFFFF]
          + [w & 0xFFFFFFFF for w in nonce])
    x = list(st)
    cols = [(0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15)]
    diag = [(0, 5, 10, 15), (1, 6, 11, 12), (2, 7, 8, 13), (3, 4, 9, 14)]
    for _ in range(10):
        for g in cols:
            _qr(x, *g)
        for g in diag:
            _qr(x, *g)
    return struct.pack('<16I', *[((x[i] + st[i]) & 0xFFFFFFFF) for i in range(16)])


def encrypt(data, key, nonce):
    """ChaCha20 XOR (encrypt == decrypt). key=8 words, nonce=3 words."""
    out = bytearray(data)
    for off in range(0, len(data), 64):
        ks = _block(key, off // 64, nonce)
        lim = min(64, len(data) - off)
        for i in range(lim):
            out[off + i] ^= ks[i]
    return bytes(out)


def gen_words():
    """8 key words + 3 nonce words, random (odd to avoid zero)."""
    return ([random.getrandbits(32) | 1 for _ in range(8)],
            [random.getrandbits(32) | 1 for _ in range(3)])


def cmd_gen(args):
    """Payload (BLOB2): payload_key.h RAW + payload.bin (ChaCha20)."""
    shim = open(args.shim, 'rb').read()
    key, nonce = gen_words()
    blob = encrypt(shim, key, nonce)
    crc = zlib.crc32(shim) & 0xFFFFFFFF
    with open(args.key, 'w') as f:
        f.write("// payload_key.h - GENERATED by tools/pack_payload.py gen (PER-BUILD key).\n")
        f.write("// Consumed by the mapper (which is encrypted - the values stay invisible).\n")
        f.write("#pragma once\n")
        for i, w in enumerate(key):
            f.write("#define PAYLOAD_KEY_W%d   0x%08Xu\n" % (i, w))
        for i, w in enumerate(nonce):
            f.write("#define PAYLOAD_NONCE_W%d 0x%08Xu\n" % (i, w))
        f.write("#define PAYLOAD_CRC    0x%08Xu\n" % crc)
    with open(args.blob, 'wb') as f:
        f.write(blob)
    print("gen(payload): %d -> blob %d (crc=0x%08X)" % (len(shim), len(blob), crc))


def cmd_gen_mapper(args):
    """Mapper (BLOB1): stub_cfg.h MASKED + random magic + mapper.bin.

    ChaCha20 with random PER-BUILD key+nonce; stub_cfg.h only carries the
    MASKED words (^ 0x9E3779B9), unmasked at runtime by the stub. There are
    no classic LCG literals anywhere.
    """
    shim = open(args.shim, 'rb').read()
    key, nonce = gen_words()
    blob = encrypt(shim, key, nonce)
    crc = zlib.crc32(shim) & 0xFFFFFFFF
    magic = random.getrandbits(32) & 0xFFFFFFFF
    if magic == 0:
        magic = 0xDEADBEEF
    with open(args.key, 'w') as f:
        f.write("// stub_cfg.h - GENERATED by tools/pack_payload.py gen-mapper (PER-BUILD key).\n")
        f.write("// MASKED words (0x9E3779B9) - the stub unmasks them at runtime.\n")
        f.write("// Do NOT version: the key is the secret sauce of each distribution.\n")
        f.write("#pragma once\n")
        for i, w in enumerate(key):
            f.write("#define MAPPER_KEY_W%d_MASKED   0x%08Xu\n" % (i, w ^ MASK))
        for i, w in enumerate(nonce):
            f.write("#define MAPPER_NONCE_W%d_MASKED 0x%08Xu\n" % (i, w ^ MASK))
        f.write("#define MAPPER_CRC_MASKED    0x%08Xu\n" % (crc ^ MASK))
        f.write("#define TRAILER_MAGIC_MASKED 0x%08Xu\n" % (magic ^ MASK))
        f.write("#define EXPECTED_CRC_MASKED  0x9E3779B9u  // placeholder (0^mask) - set-crc updates it\n")
    with open(args.magic, 'wb') as f:
        f.write(struct.pack('<I', magic))
    with open(args.blob, 'wb') as f:
        f.write(blob)
    print("gen(mapper): %d -> blob %d (crc=0x%08X magic=0x%08X)" % (len(shim), len(blob), crc, magic))


def cmd_append(args):
    """[BLOB2 payload][BLOB1 mapper][size2][size1][magic] at the end of winmm."""
    winmm = open(args.winmm, 'rb').read()
    blob2 = open(args.blob2, 'rb').read()
    blob1 = open(args.blob1, 'rb').read()
    magic = struct.unpack('<I', open(args.magic, 'rb').read(4))[0]
    with open(args.winmm, 'wb') as f:
        f.write(winmm)
        f.write(blob2)
        f.write(blob1)
        f.write(struct.pack('<III', len(blob2), len(blob1), magic))
    print("append: winmm %d -> %d bytes (+%d blob2 + %d blob1 + 12 trailer)"
          % (len(winmm), len(winmm) + len(blob2) + len(blob1) + 12,
             len(blob2), len(blob1)))


def cmd_set_crc(args):
    """Writes the self-CRC constant (.text) into stub_cfg.h, masked."""
    text = open(args.key, 'r').read()
    val = (args.crc ^ MASK) & 0xFFFFFFFF
    new, n = re.subn(r"#define EXPECTED_CRC_MASKED\s+0x[0-9A-Fa-f]+u",
                     "#define EXPECTED_CRC_MASKED  0x%08Xu" % val, text)
    if n != 1:
        raise SystemExit("set-crc: EXPECTED_CRC_MASKED not found in %s" % args.key)
    open(args.key, 'w').write(new)
    print("set-crc: EXPECTED_CRC_MASKED = 0x%08X (crc 0x%08X ^ mask)" % (val, args.crc))


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = ap.add_subparsers(dest="cmd", required=True)

    g = sub.add_parser("gen")
    g.add_argument("--shim", required=True)
    g.add_argument("--key", required=True)
    g.add_argument("--blob", required=True)
    g.set_defaults(func=cmd_gen)

    m = sub.add_parser("gen-mapper")
    m.add_argument("--shim", required=True)
    m.add_argument("--key", required=True)
    m.add_argument("--blob", required=True)
    m.add_argument("--magic", required=True)
    m.set_defaults(func=cmd_gen_mapper)

    a = sub.add_parser("append")
    a.add_argument("--winmm", required=True)
    a.add_argument("--blob2", required=True)
    a.add_argument("--blob1", required=True)
    a.add_argument("--magic", required=True)
    a.set_defaults(func=cmd_append)

    s = sub.add_parser("set-crc")
    s.add_argument("--key", required=True)
    s.add_argument("--crc", type=lambda v: int(v, 0), required=True)
    s.set_defaults(func=cmd_set_crc)

    args = ap.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
