Metadata-Version: 2.4
Name: pyencode-protector
Version: 0.5.0
Summary: Protect CPython applications with authenticated encrypted module bundles
Author: PyEncode contributors
License: MIT
Project-URL: Homepage, https://github.com/VanThanBK/pyencode
Project-URL: Repository, https://github.com/VanThanBK/pyencode
Project-URL: Issues, https://github.com/VanThanBK/pyencode/issues
Keywords: python,code-protection,obfuscation,encryption
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: cryptography>=42
Provides-Extra: native
Requires-Dist: Cython==3.3.0; platform_system == "Windows" and extra == "native"
Requires-Dist: ziglang==0.16.0; platform_system == "Windows" and extra == "native"

# PyEncode

PyEncode protects CPython applications on Windows, Linux, and macOS. It compiles
Python modules to code objects, serializes them with `marshal` for the portable
runtime or a tagged primitive codec for the Windows native runtime, compresses
them, and authenticated-encrypts them with AES-256-GCM. The generated runtime
decrypts and executes code in memory; neither plaintext source nor plaintext
bytecode is written to a temporary file.

Version 0.5 retains the PYE2/manifest-format-3 compatibility contract and adds two
independent hardening controls:

- `--obf-code 0` decrypts each module once when it is imported. `--obf-code 1`
  additionally puts the original code of module-level functions, methods, and
  lambdas in build-time authenticated capsules. The visible functions expose
  signature-preserving dispatch stubs while idle and restore their real `CodeType`
  only for a call. Nested definitions remain protected inside their
  encrypted parent and are sealed when that parent creates them.
- `--runtime-backend python` emits the portable Python runtime. On Windows x64,
  `--runtime-backend native` instead builds one per-build
  `pyencode_runtime._runtime` extension and embeds the masked master-key material
  in that binary. Application modules still remain encrypted `.pye` artifacts.
- `--native-module` is a separate application AOT option. Use it only when selected
  application modules themselves should become Cython/Zig `.pyd` extensions; it is
  not implied by a native runtime.

The existing protection contract also includes:

- Every artifact receives a random 128-bit filename, for example
  `myapp/7b82a40e9aa14e60db7094f2de3fbc33.pye`.
- PYE2 has no plaintext JSON header; an artifact contains only its magic value,
  nonce, and ciphertext/authentication tag.
- Module names, the entry point, and package flags are stored in an encrypted module
  index.
- Manifest format 3 is signed with Ed25519 and contains the SHA-256 digest of every
  artifact.
- The runtime, launcher, support trees, and resources copied by default are signed,
  key-bound, and verified again during `install()`. Explicit `--unsigned-data`
  resources are isolated from that trusted inventory for host-managed mutable data.
- The loader exposes only a small trampoline to `runpy`, rather than returning the
  application's real code object. Module keys are derived on demand and are not
  retained by the finder.
- Application modules explicitly selected with `--native-module` are removed from
  the encrypted index, emitted as signed
  ABI-tagged `.pyd` files, and recorded in signed `.pyencode-native.json` coverage.
- A dedicated finder rejects preloaded/shadowed native modules and rechecks the
  exact extension hash immediately before CPython loads it.

The goal is to make static analysis substantially more expensive than it is for
`.pyc` files. PyEncode does not claim to make reverse engineering impossible.

## Supported Python versions

PyEncode 0.5 requires **standard CPython 3.10 or later**. The production release
gate currently targets CPython 3.14 on Windows, Linux, and macOS, matching the
embedded interpreter used by the Fusion add-in integration. CI also monitors
Python 3.10 through 3.13 and 3.15 as compatibility lanes, but those versions are
not release-blocking until their compiler-specific capsule mappings are fully
validated. PyPy and other Python implementations are not currently supported.

The portable runtime source is Python-only. The default `cryptography` backend
uses that package's native extension; `--runtime-crypto pure-python` removes the
runtime dependency and is intended for restricted embedded hosts such as Fusion
on macOS.
However, a built artifact is tied to the exact CPython major/minor version used to
build it:

- an artifact built with CPython 3.11 runs on CPython 3.11;
- that artifact does not run on CPython 3.10, 3.12, or 3.14;
- to support several minor versions, build a separate output with each interpreter.

If a bundle includes `.pyd`, `.so`, `.dll`, or other native dependencies through
`--support`, the whole bundle is also constrained by the operating system,
architecture, and ABI of those files. Current testing targets the standard GIL
build of CPython; free-threaded and debug ABIs require separate builds and testing.

Both native features--the per-build runtime and application modules selected with
`--native-module`--currently support only standard 64-bit Windows CPython and emit
`.cpXY-win_amd64.pyd`. They deliberately fail instead of silently falling back when
the pinned toolchain cannot be validated. Build a separate native output for every
CPython ABI used by a target host. The per-build native runtime's direct `CodeType`
codec requires CPython 3.11 or later; CPython 3.10 remains supported by the portable
runtime.

On macOS and Linux, use `--runtime-backend python`. For an embedded macOS host such
as Fusion, `--runtime-crypto pure-python` avoids shipping the `cryptography` native
extension and its code-signing/library-validation constraints. The portable runtime
does not emit a native library of its own.

## Installation

Install the published package from PyPI in a virtual environment:

```powershell
py -3.11 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pyencode-protector
```

The CLI command and import namespace are both named `pyencode`.

For source development, use an editable installation in a virtual environment.

Windows PowerShell:

```powershell
py -3.11 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e .
```

Linux:

```bash
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
```

The builder depends on `cryptography>=42`. Portable generated applications use it
at runtime by default. Pass `--runtime-crypto pure-python` to emit a dependency-free
portable runtime that verifies Ed25519 signatures and opens the existing
AES-256-GCM PYE2 format using signed Python code. This compatibility backend is
slower and is not constant-time; it is intended for small protected bundles in
embedded hosts, not as a general-purpose cryptography library. The Windows native
runtime has no generated-application dependency on `cryptography` or
`_pure_crypto.py`.

For native build tools on Windows, the convenience extra installs the versions
used by the backend's current test contract:

```powershell
python -m pip install "pyencode-protector[native]==0.5.0"
```

Production builds should place those tools in an isolated directory and create the
hash-pinned descriptor shown below.

## Usage

Built-in command-line help is available without opening the documentation:

```bash
pyencode --help
pyencode build --help
python -m pyencode --help
```

Protect a single file:

```bash
pyencode build hello.py -o dist/hello
python dist/hello/run.py
```

Protect a package containing `__main__.py`:

```bash
pyencode build src/myapp -o dist/myapp
python dist/myapp/run.py
```

Specify the entry module explicitly:

```bash
pyencode build src -o dist/app --entry myapp.__main__
```

Create and verify a Windows native toolchain descriptor with the **target CPython
interpreter** (the interpreter embedded by Fusion in an add-in build):

```powershell
pyencode native-toolchain create -o build/native-toolchain.json `
  --builder-python C:\Python311\python.exe `
  --tool-root build\native-tools `
  --python-include build\python-dev\include `
  --python-library build\python-dev\libs\python314.lib `
  --target-python-dll C:\path\to\Fusion\python314.dll `
  --cython-version 3.3.0 --zig-version 0.16.0

pyencode native-toolchain verify build/native-toolchain.json
```

Use the descriptor to build only the generated runtime as a per-build native
extension while leaving every application module encrypted as `.pye`:

```powershell
pyencode build src -o dist/app --entry myapp.main `
  --obf-code 1 `
  --runtime-backend native `
  --runtime-crypto pure-python `
  --native-toolchain build/native-toolchain.json
```

For a native runtime, `--runtime-crypto` is retained as compatibility metadata but
does not select a Python crypto implementation. The generated `.pyd` performs
AES-256-GCM through Windows CNG and keeps HKDF/Ed25519 helpers inside compiled
`cdef` code. It neither ships `_pure_crypto.py` nor imports the separately installed
or vendored `cryptography` package. `bcrypt.dll` is a Windows system dependency,
not a bundled library.

Application AOT is an independent option. Select exact, non-entry package
implementation modules with `--native-module`; it may be repeated:

```powershell
pyencode build src -o dist/app --entry myapp.main `
  --runtime-backend native `
  --native-toolchain build/native-toolchain.json `
  --native-module myapp.license_client `
  --native-module myapp.structural.geometry
```

The example above uses both native axes, but either can be used without the other.
`--native-toolchain` is required whenever `--runtime-backend native` or at least one
`--native-module` is requested.

The descriptor pins the target executable/runtime DLL/ABI, builder Python, full
isolated tool tree, Zig executable, CPython headers, and import library by SHA-256
before any compiler process is launched. For an embedded host, pass the actual
`pythonXY.dll` loaded by that host with `--target-python-dll`; otherwise PyEncode
uses the DLL associated with the target interpreter. Creating a descriptor
establishes repeatability; it does not independently prove that the initially
provisioned tools were trustworthy.

The DLL hash is build-provenance metadata: descriptor verification re-hashes it
before compilation. Generated applications enforce the exact Windows/64-bit/
`EXT_SUFFIX` ABI, but do not re-hash the host's loaded Python DLL at runtime.

Sign and key-bind additional dependencies or support trees:

```bash
pyencode build src -o dist/app \
  --entry myapp.main \
  --support build/vendor _vendor \
  --support public/config.json config.json
```

`--support SOURCE DEST` may be repeated. If `SOURCE` is a directory, its tree is
copied below `DEST`; if it is a file, `DEST` is the output file path. Symlinks,
reparse points, paths that escape the root, and case-insensitive path collisions are
rejected.

The exact destination `_vendor` is treated as a bootstrap dependency path for hosts
that do not already have `cryptography` installed, such as Fusion. Only the exact
`_vendor` entry remains on `sys.path` while the verifier loads; every other bundle
subdirectory is isolated. Because the vendored cryptography library must run before
Ed25519 can verify the bundle itself, `_vendor` is a trusted bootstrap component by
design in the default `cryptography` runtime. Vendor only wheels obtained from trusted
sources.

For an embedded host that disallows the native extension in `cryptography`, do not
vendor it. Build instead with the wire-compatible dependency-free backend:

```bash
pyencode build src -o dist/app --entry myapp.main \
  --runtime-crypto pure-python
```

This writes an empty generated `requirements.txt`. Both cryptographic
implementations consume the same signed manifest and PYE2 AES-256-GCM artifacts,
so the choice does not change the authentication algorithms or container format.
Pure-Python AES and big-integer Ed25519 operations are not constant-time and decrypt
more slowly.

For Fusion on macOS, use the portable combination:

```bash
pyencode build src -o dist/app --entry myapp.main \
  --obf-code 1 \
  --runtime-backend python \
  --runtime-crypto pure-python
```

Main options:

- `-o`, `--output`: output directory; it must not exist or must be completely empty.
- `-e`, `--entry`: dotted name of the entry module or package.
- `--exclude GLOB`: exclude paths matching a glob; may be repeated.
- `--no-resources`: do not copy non-Python files from the source tree.
- `--keep-docstrings`: preserve docstrings.
- `--optimize {0,1,2}`: CPython optimization level.
- `--expires YYYY-MM-DD`: refuse to run after the specified UTC date.
- `--launcher PATH`: use a custom `.py` launcher.
- `--support SOURCE DEST`: copy, sign, and key-bind a support file or tree.
- `--rename-locals`: rename metadata for local variables that are not parameters.
- `--obf-code {0,1}`: choose module-only decryption (`0`, the default) or add
  authenticated per-function capsules and dispatch stubs (`1`).
- `--allow-extra-data`: allow the host to create additional unsigned data files and
  directories; signed files and recognized Python/native artifact types remain
  protected. The application must not execute or trust the added data.
- `--unsigned-data PATTERN`: copy matching discovered source resources only after
  the signed/key-bound inventory is sealed; repeat for additional globs. This
  requires `--allow-extra-data`.
- `--runtime-crypto {cryptography,pure-python}`: select the portable Python
  runtime's cryptographic implementation. The default is `cryptography`; the
  Windows native runtime always uses its compiled/CNG boundary.
- `--runtime-backend {python,native}`: emit the portable Python runtime (the
  default), or one per-build Windows x64 runtime extension containing the master-key
  material.
- `--native-toolchain FILE`: use a canonical, hash-pinned Windows toolchain
  descriptor for the native runtime, application AOT, or both.
- `--native-module MODULE`: compile this exact dotted implementation module as a
  native extension; repeat for additional modules. Package `__init__`, entry,
  `__main__`, top-level, duplicate, missing, globbed, and non-ASCII names fail.

`--rename-locals` is opt-in because code that uses a dynamic alias of `locals()`,
reads `frame.f_locals`, or depends on debugger/framework introspection may require
the original local names. Arguments, closures, line tables, and exception tables are
always preserved.

`--obf-code`, `--optimize`, `--rename-locals`, and `--keep-docstrings` govern
encrypted `.pye` modules. Native-selected application modules use the fixed
directives recorded in
`.pyencode-native.json` (`binding=False`, no embedded signatures/code comments,
no tracing/profile hooks, and docstrings disabled) so coverage is auditable.

## Custom launchers and embedded hosts

A custom launcher is copied byte-for-byte, signed, and included in key derivation.
It must set:

```python
import sys
sys.dont_write_bytecode = True
```

before any non-bootstrap import. The builder enforces this minimum requirement. A
secure production launcher must also verify that `pyencode_runtime` contains no
`__pycache__`, `.pyc`, or unexpected files **before** importing the package. The
allowed set must name the selected backend exactly: `_runtime.py` for the Python
backend or the generated ABI-tagged `_runtime.cpXY-win_amd64.pyd` for the native
backend. The default launcher and the Weldments pipeline include this complete
preflight check.

A host such as Fusion can load an entry point without placing its plaintext module
name in the stub:

```python
import sys
sys.dont_write_bytecode = True

# Perform the pyencode_runtime preflight here.
from pyencode_runtime import load_entry

implementation = load_entry()
```

`install()` intentionally returns `None`. `load_entry()` returns the entry module to
an embedding host, while `run()` executes the entry point with `__main__` semantics.

## Output layout and runtime behavior

```text
dist/app/
├── .pyencode-manifest.json
├── .pyencode-native.json               # only with application AOT
├── .pyencode-runtime-native.json       # only with a native runtime
├── myapp/
│   ├── 14a0...f91c.pye
│   ├── 7b82...bc33.pye
│   ├── core.cp314-win_amd64.pyd        # only if selected by --native-module
│   └── assets/config.json
├── pyencode_runtime/
│   ├── __init__.py
│   ├── _build.py
│   ├── _mp_main.py
│   ├── _pure_crypto.py                 # Python backend only
│   └── _runtime.py                     # Python backend; native builds contain
│       _runtime.cp314-win_amd64.pyd    # this ABI file instead (exactly one,
│                                       # with no _pure_crypto.py)
├── requirements.txt
└── run.py
```

The native runtime report records its pinned toolchain and generated/binary hashes.
It is distinct from `.pyencode-native.json`, which records only application modules
compiled through `--native-module`. Both reports are signed when present.

Resources are not encrypted, but their build-time bytes and paths are signed and
key-bound. By default, the entire output tree is immutable: the runtime rejects any
file or directory that is added, removed, or modified.

For hosts that create metadata or caches beside an add-in, `--allow-extra-data`
permits additional data files and directories without invalidating the bundle. This
mode still rejects modifications or removal of signed files, unexpected `.py`,
`.pyc`, native libraries, or `.pye` files, and all symlinks or reparse points. Do not
use this extra data as trusted input for licensing or security decisions. The
Weldments pipeline enables this compatibility mode so Fusion can create `.vscode`,
logs, or machine-specific caches.

Files that the application itself updates, such as user settings, host metadata, or
profile libraries, must not be part of the signed source inventory. Use repeatable
`--unsigned-data` patterns to copy their initial values safely without a separate
post-build step:

```bash
pyencode build src -o dist/app --entry myapp.main \
  --allow-extra-data \
  --unsigned-data '*.png' \
  --unsigned-data 'myapp/data/settings.json'
```

Patterns are matched case-sensitively against the POSIX path relative to the output
root and, for convenience, against the basename. Thus `*.png` selects PNG resources
at any depth; `**/*.png` is also accepted and includes root-level matches. Every
pattern must match at least one resource discovered in the main source tree.
`--unsigned-data` does not apply to `--support` inputs, and `--no-resources` leaves
nothing eligible for selection.

Selected files are copied into the atomic temporary output only after the signed
manifest and key-bound inventory have been sealed. Their paths and bytes are absent
from `integrity.files`, are not inputs to key derivation, and may be modified or
removed by the host. The signed manifest records only the existing
`allow_extra_data=true` compatibility policy; it deliberately does not present the
patterns or selected files as trusted inputs.

The builder rejects a pattern if it selects Python source/bytecode, `.pye`, native
libraries, executables or executable scripts, symlinks/reparse points, non-regular
entries, the launcher, `pyencode_runtime`, either generated manifest/report, or
`requirements.txt`. Added unsigned files are still scanned at runtime: recognized
code/native artifacts and every link/reparse point remain forbidden. Never execute
or use unsigned data for licensing, integrity, or other security decisions.

The `__file__` value of a protected module is the real path to its randomly named
artifact. Consequently, `Path(__file__).parent`, `pkgutil.get_data()`, and
`importlib.resources`, including nested directory resources, continue to work. Code
that depends on the basename or stem of `__file__` sees the random token.

Integration tests cover imports by full name, relative imports, circular imports,
namespace packages, Unicode module names, reloads, `runpy.run_module()`, and
`multiprocessing` spawn for applications launched through the default launcher.
`pkgutil.iter_modules()` and `walk_packages()` cannot automatically discover
protected child names because those names intentionally reside in the encrypted
index; importing a known full name still works.

For an embedding host that calls `load_entry()`, spawning works if the child process
also runs the host bootstrap. A host that places its bootstrap only inside
`if __name__ == '__main__'` and then directly spawns a protected target requires a
dedicated launcher. Automatic spawn bootstrapping currently focuses on applications
executed through `run()`.

Version 0.5 uses one fixed runtime namespace per process. Do not load two independent
PyEncode distributions into the same interpreter; run them in separate processes.
Randomized or multi-bundle runtime namespaces are a compatibility item to resolve
before declaring a stable 1.0 API.

## Security limitations

PyEncode makes static analysis harder, but it cannot guarantee secrecy on a machine
fully controlled by an attacker:

- The default mode is offline, so key material must remain in the bundle. An
  experienced analyst can inspect the Python runtime and reproduce key derivation.
  The native runtime removes the readable Python key reconstruction and embeds
  masked per-build material in a `.pyd`; its long-lived function key is held in a
  private C buffer rather than a Python attribute. Binary analysis, native memory
  inspection, or a debugger can still recover these values.
- Code objects must exist in memory while they execute. Tracing, debuggers,
  memory inspection, code-object watchers, or native hooks may still observe code
  and runtime state. The native codec avoids the ordinary `marshal`, `code.__new__`,
  and `function.__new__` audit paths, but this does not make it debugger- or
  `PyCode_AddWatcher`-proof.
- At `--obf-code 1`, the original code for protected top-level functions is not
  present in the module code object at rest, and normal `function.__code__`
  inspection sees a generic stub. The real `CodeType` is nevertheless materialized
  for execution. A suspended generator/coroutine retains an executing frame, and an
  attacker who controls the process can inspect or intercept it. This option raises
  extraction cost; it is not a confidentiality boundary.
- The integrity interlock prevents straightforward file modification, artifact
  substitution, code injection, and re-signing. It does not turn a pure-Python
  runtime into a native trust anchor. A native runtime is a stronger obfuscation
  boundary, not a hardware-backed trust anchor.
- Module and entry-point names are hidden at rest, but appear during import in
  `sys.modules`, tracebacks, and runtime state. Package and resource directories may
  still reveal part of the application structure.
- `--expires` relies on the system clock and is not a replacement for a licensing
  system.
- Native-selected modules no longer expose a marshal-loadable application code
  object through the PyEncode runtime, but Cython binaries still reveal useful
  names, strings, metadata, and machine code to a skilled reverse engineer.
- Native runtime and application extensions remain patchable in a process fully
  controlled by an attacker.
  The signed finder raises the cost of preload, path-shadow, and file-tamper attacks;
  it is not an unpatchable hardware trust anchor or a guarantee of equivalence to
  PyArmor level 1 or every PyArmor configuration. The products use different
  runtimes and should be compared with repeatable attacks against the exact builds,
  not by matching option names.
- The native runtime is loaded before it can verify its own signed inventory. Its
  manifest hash detects accidental corruption and inconsistent replacement, but is
  not an operating-system trust anchor. If Authenticode is added, signing must occur
  before PyEncode computes the runtime report and key-bound inventory; signing the
  `.pyd` after a build invalidates that build.
- The native `CodeType` record uses CPython's C pickle implementation only after
  the signed artifact hash and AES-GCM tag have been verified. Its schema checks
  are for trusted build output; it is not a general-purpose decoder for untrusted
  pickle data.
- Native files are re-hashed immediately before CPython loads them, but the hash and
  operating-system loader open are not one atomic operation. An attacker that already
  controls the process or filesystem may still exploit that narrow replacement window.

Never store long-lived API keys or private keys in a client. For high-value products,
the security upgrade that makes the greatest practical difference is an external or
envelope key obtained from a license server or platform key store. A native runtime
and `--obf-code 1` primarily add reverse-engineering cost.

## Testing

```bash
python -m unittest discover -v
```

CI runs on Windows and Ubuntu with CPython 3.10 through 3.15, plus a macOS
CPython 3.14 portable-runtime job. Portable tests cover PYE2, the
encrypted module index, KDF vectors, tampering and re-signing, resource/support
integrity, opaque filenames, imports, packages, namespaces, Unicode, `runpy`,
`multiprocessing` spawn, ordinary launchers without `-B`, expiration policy, and
atomic output creation. Level-1 tests cover build-time function capsules, ordinary
functions and methods, lambdas, closures, generators, coroutines, call behavior,
and the idle dispatch-stub invariant.

The Windows native suite additionally covers the per-build runtime and application
AOT independently, exact-module selection, canonical toolchain descriptors and
tampering, PE architecture/export/import/hardening checks, signed native reports,
extension loading, preload rejection, and runtime tamper.

## Releasing

The production release process using GitHub OIDC is documented in
[RELEASING.md](RELEASING.md). The workflow uploads to PyPI only when a pushed tag
exactly matches the project version, for example `v0.5.0`.
