pyencode-protector 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyencode_protector-0.3.1/PKG-INFO +304 -0
- pyencode_protector-0.3.1/README.md +281 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/pyproject.toml +2 -2
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/__init__.py +1 -1
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/cli.py +62 -13
- pyencode_protector-0.3.1/src/pyencode_protector.egg-info/PKG-INFO +304 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/SOURCES.txt +1 -0
- pyencode_protector-0.3.1/tests/test_cli.py +50 -0
- pyencode_protector-0.3.0/PKG-INFO +0 -276
- pyencode_protector-0.3.0/README.md +0 -253
- pyencode_protector-0.3.0/src/pyencode_protector.egg-info/PKG-INFO +0 -276
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/setup.cfg +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/__main__.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/builder.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/code_hardening.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/container.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/crypto.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/discovery.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/errors.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/integrity.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/inventory.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/manifest.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/opaque.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/__init__.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/_build.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/_mp_main.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/_runtime.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/source.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/dependency_links.txt +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/entry_points.txt +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/requires.txt +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/top_level.txt +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_code_hardening.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_core.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_integration.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_inventory.py +0 -0
- {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_opaque.py +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyencode-protector
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Protect CPython applications with authenticated encrypted module bundles
|
|
5
|
+
Author: PyEncode contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/VanThanBK/pyencode
|
|
8
|
+
Project-URL: Repository, https://github.com/VanThanBK/pyencode
|
|
9
|
+
Project-URL: Issues, https://github.com/VanThanBK/pyencode/issues
|
|
10
|
+
Keywords: python,code-protection,obfuscation,encryption
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: cryptography>=42
|
|
23
|
+
|
|
24
|
+
# PyEncode
|
|
25
|
+
|
|
26
|
+
PyEncode is a pure-Python tool for protecting CPython applications on Windows and
|
|
27
|
+
Linux. Each module is compiled to a code object, serialized with `marshal`,
|
|
28
|
+
compressed, and authenticated-encrypted with AES-256-GCM. The runtime decrypts and
|
|
29
|
+
executes the code in memory; neither plaintext source nor plaintext bytecode is
|
|
30
|
+
written to a temporary file.
|
|
31
|
+
|
|
32
|
+
Version 0.3 uses the PYE2 format:
|
|
33
|
+
|
|
34
|
+
- Every artifact receives a random 128-bit filename, for example
|
|
35
|
+
`myapp/7b82a40e9aa14e60db7094f2de3fbc33.pye`.
|
|
36
|
+
- PYE2 has no plaintext JSON header; an artifact contains only its magic value,
|
|
37
|
+
nonce, and ciphertext/authentication tag.
|
|
38
|
+
- Module names, the entry point, and package flags are stored in an encrypted module
|
|
39
|
+
index.
|
|
40
|
+
- Manifest format 3 is signed with Ed25519 and contains the SHA-256 digest of every
|
|
41
|
+
artifact.
|
|
42
|
+
- The runtime, launcher, resources, and support trees copied by the builder are all
|
|
43
|
+
signed, key-bound, and verified again during `install()`.
|
|
44
|
+
- The loader exposes only a small trampoline to `runpy`, rather than returning the
|
|
45
|
+
application's real code object. Module keys are derived on demand and are not
|
|
46
|
+
retained by the finder.
|
|
47
|
+
|
|
48
|
+
The goal is to make static analysis substantially more expensive than it is for
|
|
49
|
+
`.pyc` files. PyEncode does not claim to make reverse engineering impossible.
|
|
50
|
+
|
|
51
|
+
## Supported Python versions
|
|
52
|
+
|
|
53
|
+
PyEncode 0.3 requires **standard CPython 3.10 or later**. CI directly tests Python
|
|
54
|
+
3.10 through 3.15. Newer CPython feature releases may install and build under the
|
|
55
|
+
forward-compatible version policy, and are added to the required test matrix once
|
|
56
|
+
the release and its dependency wheels are available. PyPy and other Python
|
|
57
|
+
implementations are not currently supported.
|
|
58
|
+
|
|
59
|
+
The runtime is pure Python, so the same runtime source works on Windows and Linux.
|
|
60
|
+
However, a built artifact is tied to the exact CPython major/minor version used to
|
|
61
|
+
build it:
|
|
62
|
+
|
|
63
|
+
- an artifact built with CPython 3.11 runs on CPython 3.11;
|
|
64
|
+
- that artifact does not run on CPython 3.10, 3.12, or 3.14;
|
|
65
|
+
- to support several minor versions, build a separate output with each interpreter.
|
|
66
|
+
|
|
67
|
+
If a bundle includes `.pyd`, `.so`, `.dll`, or other native dependencies through
|
|
68
|
+
`--support`, the whole bundle is also constrained by the operating system,
|
|
69
|
+
architecture, and ABI of those files. Current testing targets the standard GIL
|
|
70
|
+
build of CPython; free-threaded and debug ABIs require separate builds and testing.
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
Install the published package from PyPI in a virtual environment:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
py -3.11 -m venv .venv
|
|
78
|
+
.venv\Scripts\Activate.ps1
|
|
79
|
+
python -m pip install --upgrade pip
|
|
80
|
+
python -m pip install pyencode-protector
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The CLI command and import namespace are both named `pyencode`.
|
|
84
|
+
|
|
85
|
+
For source development, use an editable installation in a virtual environment.
|
|
86
|
+
|
|
87
|
+
Windows PowerShell:
|
|
88
|
+
|
|
89
|
+
```powershell
|
|
90
|
+
py -3.11 -m venv .venv
|
|
91
|
+
.venv\Scripts\Activate.ps1
|
|
92
|
+
python -m pip install --upgrade pip
|
|
93
|
+
python -m pip install -e .
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Linux:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python3.11 -m venv .venv
|
|
100
|
+
source .venv/bin/activate
|
|
101
|
+
python -m pip install --upgrade pip
|
|
102
|
+
python -m pip install -e .
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The only runtime dependency is `cryptography>=42`.
|
|
106
|
+
|
|
107
|
+
## Usage
|
|
108
|
+
|
|
109
|
+
Built-in command-line help is available without opening the documentation:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pyencode --help
|
|
113
|
+
pyencode build --help
|
|
114
|
+
python -m pyencode --help
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Protect a single file:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pyencode build hello.py -o dist/hello
|
|
121
|
+
python dist/hello/run.py
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Protect a package containing `__main__.py`:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pyencode build src/myapp -o dist/myapp
|
|
128
|
+
python dist/myapp/run.py
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Specify the entry module explicitly:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pyencode build src -o dist/app --entry myapp.__main__
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Sign and key-bind additional dependencies or support trees:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pyencode build src -o dist/app \
|
|
141
|
+
--entry myapp.main \
|
|
142
|
+
--support build/vendor _vendor \
|
|
143
|
+
--support public/config.json config.json
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`--support SOURCE DEST` may be repeated. If `SOURCE` is a directory, its tree is
|
|
147
|
+
copied below `DEST`; if it is a file, `DEST` is the output file path. Symlinks,
|
|
148
|
+
reparse points, paths that escape the root, and case-insensitive path collisions are
|
|
149
|
+
rejected.
|
|
150
|
+
|
|
151
|
+
The exact destination `_vendor` is treated as a bootstrap dependency path for hosts
|
|
152
|
+
that do not already have `cryptography` installed, such as Fusion. Only the exact
|
|
153
|
+
`_vendor` entry remains on `sys.path` while the verifier loads; every other bundle
|
|
154
|
+
subdirectory is isolated. Because the vendored cryptography library must run before
|
|
155
|
+
Ed25519 can verify the bundle itself, `_vendor` is a trusted bootstrap component by
|
|
156
|
+
design in this pure-Python runtime. Vendor only wheels obtained from trusted
|
|
157
|
+
sources.
|
|
158
|
+
|
|
159
|
+
Main options:
|
|
160
|
+
|
|
161
|
+
- `-o`, `--output`: output directory; it must not exist or must be completely empty.
|
|
162
|
+
- `-e`, `--entry`: dotted name of the entry module or package.
|
|
163
|
+
- `--exclude GLOB`: exclude paths matching a glob; may be repeated.
|
|
164
|
+
- `--no-resources`: do not copy non-Python files from the source tree.
|
|
165
|
+
- `--keep-docstrings`: preserve docstrings.
|
|
166
|
+
- `--optimize {0,1,2}`: CPython optimization level.
|
|
167
|
+
- `--expires YYYY-MM-DD`: refuse to run after the specified UTC date.
|
|
168
|
+
- `--launcher PATH`: use a custom `.py` launcher.
|
|
169
|
+
- `--support SOURCE DEST`: copy, sign, and key-bind a support file or tree.
|
|
170
|
+
- `--rename-locals`: rename metadata for local variables that are not parameters.
|
|
171
|
+
- `--allow-extra-data`: allow the host to create additional unsigned data files and
|
|
172
|
+
directories; signed files and recognized Python/native artifact types remain
|
|
173
|
+
protected. The application must not execute or trust the added data.
|
|
174
|
+
|
|
175
|
+
`--rename-locals` is opt-in because code that uses a dynamic alias of `locals()`,
|
|
176
|
+
reads `frame.f_locals`, or depends on debugger/framework introspection may require
|
|
177
|
+
the original local names. Arguments, closures, line tables, and exception tables are
|
|
178
|
+
always preserved.
|
|
179
|
+
|
|
180
|
+
## Custom launchers and embedded hosts
|
|
181
|
+
|
|
182
|
+
A custom launcher is copied byte-for-byte, signed, and included in key derivation.
|
|
183
|
+
It must set:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
import sys
|
|
187
|
+
sys.dont_write_bytecode = True
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
before any non-bootstrap import. The builder enforces this minimum requirement. A
|
|
191
|
+
secure production launcher must also verify that `pyencode_runtime` contains no
|
|
192
|
+
`__pycache__`, `.pyc`, or unexpected files **before** importing the package. The
|
|
193
|
+
default launcher and the Weldments pipeline include the complete preflight check.
|
|
194
|
+
|
|
195
|
+
A host such as Fusion can load an entry point without placing its plaintext module
|
|
196
|
+
name in the stub:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
import sys
|
|
200
|
+
sys.dont_write_bytecode = True
|
|
201
|
+
|
|
202
|
+
# Perform the pyencode_runtime preflight here.
|
|
203
|
+
from pyencode_runtime import load_entry
|
|
204
|
+
|
|
205
|
+
implementation = load_entry()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`install()` intentionally returns `None`. `load_entry()` returns the entry module to
|
|
209
|
+
an embedding host, while `run()` executes the entry point with `__main__` semantics.
|
|
210
|
+
|
|
211
|
+
## Output layout and runtime behavior
|
|
212
|
+
|
|
213
|
+
```text
|
|
214
|
+
dist/app/
|
|
215
|
+
├── .pyencode-manifest.json
|
|
216
|
+
├── myapp/
|
|
217
|
+
│ ├── 14a0...f91c.pye
|
|
218
|
+
│ ├── 7b82...bc33.pye
|
|
219
|
+
│ └── assets/config.json
|
|
220
|
+
├── pyencode_runtime/
|
|
221
|
+
│ ├── __init__.py
|
|
222
|
+
│ ├── _build.py
|
|
223
|
+
│ ├── _mp_main.py
|
|
224
|
+
│ └── _runtime.py
|
|
225
|
+
├── requirements.txt
|
|
226
|
+
└── run.py
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Resources are not encrypted, but their build-time bytes and paths are signed and
|
|
230
|
+
key-bound. By default, the entire output tree is immutable: the runtime rejects any
|
|
231
|
+
file or directory that is added, removed, or modified.
|
|
232
|
+
|
|
233
|
+
For hosts that create metadata or caches beside an add-in, `--allow-extra-data`
|
|
234
|
+
permits additional data files and directories without invalidating the bundle. This
|
|
235
|
+
mode still rejects modifications or removal of signed files, unexpected `.py`,
|
|
236
|
+
`.pyc`, native libraries, or `.pye` files, and all symlinks or reparse points. Do not
|
|
237
|
+
use this extra data as trusted input for licensing or security decisions. The
|
|
238
|
+
Weldments pipeline enables this compatibility mode so Fusion can create `.vscode`,
|
|
239
|
+
logs, or machine-specific caches.
|
|
240
|
+
|
|
241
|
+
The `__file__` value of a protected module is the real path to its randomly named
|
|
242
|
+
artifact. Consequently, `Path(__file__).parent`, `pkgutil.get_data()`, and
|
|
243
|
+
`importlib.resources`, including nested directory resources, continue to work. Code
|
|
244
|
+
that depends on the basename or stem of `__file__` sees the random token.
|
|
245
|
+
|
|
246
|
+
Integration tests cover imports by full name, relative imports, circular imports,
|
|
247
|
+
namespace packages, Unicode module names, reloads, `runpy.run_module()`, and
|
|
248
|
+
`multiprocessing` spawn for applications launched through the default launcher.
|
|
249
|
+
`pkgutil.iter_modules()` and `walk_packages()` cannot automatically discover
|
|
250
|
+
protected child names because those names intentionally reside in the encrypted
|
|
251
|
+
index; importing a known full name still works.
|
|
252
|
+
|
|
253
|
+
For an embedding host that calls `load_entry()`, spawning works if the child process
|
|
254
|
+
also runs the host bootstrap. A host that places its bootstrap only inside
|
|
255
|
+
`if __name__ == '__main__'` and then directly spawns a protected target requires a
|
|
256
|
+
dedicated launcher. Automatic spawn bootstrapping currently focuses on applications
|
|
257
|
+
executed through `run()`.
|
|
258
|
+
|
|
259
|
+
Version 0.3 uses one fixed runtime namespace per process. Do not load two independent
|
|
260
|
+
PyEncode distributions into the same interpreter; run them in separate processes.
|
|
261
|
+
Randomized or multi-bundle runtime namespaces are a compatibility item to resolve
|
|
262
|
+
before declaring a stable 1.0 API.
|
|
263
|
+
|
|
264
|
+
## Security limitations
|
|
265
|
+
|
|
266
|
+
PyEncode makes static analysis harder, but it cannot guarantee secrecy on a machine
|
|
267
|
+
fully controlled by an attacker:
|
|
268
|
+
|
|
269
|
+
- The default mode is offline, so key material must remain in the bundle. An
|
|
270
|
+
experienced analyst can inspect the runtime and reproduce the key-derivation
|
|
271
|
+
process.
|
|
272
|
+
- Code objects must exist in memory while they execute. Tracing, debuggers,
|
|
273
|
+
monkeypatching, or native hooks may still observe code and runtime state.
|
|
274
|
+
- The integrity interlock prevents straightforward file modification, artifact
|
|
275
|
+
substitution, code injection, and re-signing. It does not turn a pure-Python
|
|
276
|
+
runtime into a native trust anchor.
|
|
277
|
+
- Module and entry-point names are hidden at rest, but appear during import in
|
|
278
|
+
`sys.modules`, tracebacks, and runtime state. Package and resource directories may
|
|
279
|
+
still reveal part of the application structure.
|
|
280
|
+
- `--expires` relies on the system clock and is not a replacement for a licensing
|
|
281
|
+
system.
|
|
282
|
+
|
|
283
|
+
Never store long-lived API keys or private keys in a client. For high-value products,
|
|
284
|
+
the security upgrade that makes the greatest practical difference is an external or
|
|
285
|
+
envelope key obtained from a license server or keyring. A native runtime primarily
|
|
286
|
+
adds further reverse-engineering cost.
|
|
287
|
+
|
|
288
|
+
## Testing
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
python -m unittest discover -v
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
CI runs on Windows and Ubuntu with CPython 3.10 through 3.15. Tests cover PYE2, the
|
|
295
|
+
encrypted module index, KDF vectors, tampering and re-signing, resource/support
|
|
296
|
+
integrity, opaque filenames, imports, packages, namespaces, Unicode, `runpy`,
|
|
297
|
+
`multiprocessing` spawn, ordinary launchers without `-B`, expiration policy, and
|
|
298
|
+
atomic output creation.
|
|
299
|
+
|
|
300
|
+
## Releasing
|
|
301
|
+
|
|
302
|
+
The production release process using GitHub OIDC is documented in
|
|
303
|
+
[RELEASING.md](RELEASING.md). The workflow uploads to PyPI only when a pushed tag
|
|
304
|
+
exactly matches the project version, for example `v0.3.1`.
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# PyEncode
|
|
2
|
+
|
|
3
|
+
PyEncode is a pure-Python tool for protecting CPython applications on Windows and
|
|
4
|
+
Linux. Each module is compiled to a code object, serialized with `marshal`,
|
|
5
|
+
compressed, and authenticated-encrypted with AES-256-GCM. The runtime decrypts and
|
|
6
|
+
executes the code in memory; neither plaintext source nor plaintext bytecode is
|
|
7
|
+
written to a temporary file.
|
|
8
|
+
|
|
9
|
+
Version 0.3 uses the PYE2 format:
|
|
10
|
+
|
|
11
|
+
- Every artifact receives a random 128-bit filename, for example
|
|
12
|
+
`myapp/7b82a40e9aa14e60db7094f2de3fbc33.pye`.
|
|
13
|
+
- PYE2 has no plaintext JSON header; an artifact contains only its magic value,
|
|
14
|
+
nonce, and ciphertext/authentication tag.
|
|
15
|
+
- Module names, the entry point, and package flags are stored in an encrypted module
|
|
16
|
+
index.
|
|
17
|
+
- Manifest format 3 is signed with Ed25519 and contains the SHA-256 digest of every
|
|
18
|
+
artifact.
|
|
19
|
+
- The runtime, launcher, resources, and support trees copied by the builder are all
|
|
20
|
+
signed, key-bound, and verified again during `install()`.
|
|
21
|
+
- The loader exposes only a small trampoline to `runpy`, rather than returning the
|
|
22
|
+
application's real code object. Module keys are derived on demand and are not
|
|
23
|
+
retained by the finder.
|
|
24
|
+
|
|
25
|
+
The goal is to make static analysis substantially more expensive than it is for
|
|
26
|
+
`.pyc` files. PyEncode does not claim to make reverse engineering impossible.
|
|
27
|
+
|
|
28
|
+
## Supported Python versions
|
|
29
|
+
|
|
30
|
+
PyEncode 0.3 requires **standard CPython 3.10 or later**. CI directly tests Python
|
|
31
|
+
3.10 through 3.15. Newer CPython feature releases may install and build under the
|
|
32
|
+
forward-compatible version policy, and are added to the required test matrix once
|
|
33
|
+
the release and its dependency wheels are available. PyPy and other Python
|
|
34
|
+
implementations are not currently supported.
|
|
35
|
+
|
|
36
|
+
The runtime is pure Python, so the same runtime source works on Windows and Linux.
|
|
37
|
+
However, a built artifact is tied to the exact CPython major/minor version used to
|
|
38
|
+
build it:
|
|
39
|
+
|
|
40
|
+
- an artifact built with CPython 3.11 runs on CPython 3.11;
|
|
41
|
+
- that artifact does not run on CPython 3.10, 3.12, or 3.14;
|
|
42
|
+
- to support several minor versions, build a separate output with each interpreter.
|
|
43
|
+
|
|
44
|
+
If a bundle includes `.pyd`, `.so`, `.dll`, or other native dependencies through
|
|
45
|
+
`--support`, the whole bundle is also constrained by the operating system,
|
|
46
|
+
architecture, and ABI of those files. Current testing targets the standard GIL
|
|
47
|
+
build of CPython; free-threaded and debug ABIs require separate builds and testing.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
Install the published package from PyPI in a virtual environment:
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
py -3.11 -m venv .venv
|
|
55
|
+
.venv\Scripts\Activate.ps1
|
|
56
|
+
python -m pip install --upgrade pip
|
|
57
|
+
python -m pip install pyencode-protector
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The CLI command and import namespace are both named `pyencode`.
|
|
61
|
+
|
|
62
|
+
For source development, use an editable installation in a virtual environment.
|
|
63
|
+
|
|
64
|
+
Windows PowerShell:
|
|
65
|
+
|
|
66
|
+
```powershell
|
|
67
|
+
py -3.11 -m venv .venv
|
|
68
|
+
.venv\Scripts\Activate.ps1
|
|
69
|
+
python -m pip install --upgrade pip
|
|
70
|
+
python -m pip install -e .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Linux:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3.11 -m venv .venv
|
|
77
|
+
source .venv/bin/activate
|
|
78
|
+
python -m pip install --upgrade pip
|
|
79
|
+
python -m pip install -e .
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The only runtime dependency is `cryptography>=42`.
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
Built-in command-line help is available without opening the documentation:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pyencode --help
|
|
90
|
+
pyencode build --help
|
|
91
|
+
python -m pyencode --help
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Protect a single file:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pyencode build hello.py -o dist/hello
|
|
98
|
+
python dist/hello/run.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Protect a package containing `__main__.py`:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pyencode build src/myapp -o dist/myapp
|
|
105
|
+
python dist/myapp/run.py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Specify the entry module explicitly:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pyencode build src -o dist/app --entry myapp.__main__
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Sign and key-bind additional dependencies or support trees:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pyencode build src -o dist/app \
|
|
118
|
+
--entry myapp.main \
|
|
119
|
+
--support build/vendor _vendor \
|
|
120
|
+
--support public/config.json config.json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--support SOURCE DEST` may be repeated. If `SOURCE` is a directory, its tree is
|
|
124
|
+
copied below `DEST`; if it is a file, `DEST` is the output file path. Symlinks,
|
|
125
|
+
reparse points, paths that escape the root, and case-insensitive path collisions are
|
|
126
|
+
rejected.
|
|
127
|
+
|
|
128
|
+
The exact destination `_vendor` is treated as a bootstrap dependency path for hosts
|
|
129
|
+
that do not already have `cryptography` installed, such as Fusion. Only the exact
|
|
130
|
+
`_vendor` entry remains on `sys.path` while the verifier loads; every other bundle
|
|
131
|
+
subdirectory is isolated. Because the vendored cryptography library must run before
|
|
132
|
+
Ed25519 can verify the bundle itself, `_vendor` is a trusted bootstrap component by
|
|
133
|
+
design in this pure-Python runtime. Vendor only wheels obtained from trusted
|
|
134
|
+
sources.
|
|
135
|
+
|
|
136
|
+
Main options:
|
|
137
|
+
|
|
138
|
+
- `-o`, `--output`: output directory; it must not exist or must be completely empty.
|
|
139
|
+
- `-e`, `--entry`: dotted name of the entry module or package.
|
|
140
|
+
- `--exclude GLOB`: exclude paths matching a glob; may be repeated.
|
|
141
|
+
- `--no-resources`: do not copy non-Python files from the source tree.
|
|
142
|
+
- `--keep-docstrings`: preserve docstrings.
|
|
143
|
+
- `--optimize {0,1,2}`: CPython optimization level.
|
|
144
|
+
- `--expires YYYY-MM-DD`: refuse to run after the specified UTC date.
|
|
145
|
+
- `--launcher PATH`: use a custom `.py` launcher.
|
|
146
|
+
- `--support SOURCE DEST`: copy, sign, and key-bind a support file or tree.
|
|
147
|
+
- `--rename-locals`: rename metadata for local variables that are not parameters.
|
|
148
|
+
- `--allow-extra-data`: allow the host to create additional unsigned data files and
|
|
149
|
+
directories; signed files and recognized Python/native artifact types remain
|
|
150
|
+
protected. The application must not execute or trust the added data.
|
|
151
|
+
|
|
152
|
+
`--rename-locals` is opt-in because code that uses a dynamic alias of `locals()`,
|
|
153
|
+
reads `frame.f_locals`, or depends on debugger/framework introspection may require
|
|
154
|
+
the original local names. Arguments, closures, line tables, and exception tables are
|
|
155
|
+
always preserved.
|
|
156
|
+
|
|
157
|
+
## Custom launchers and embedded hosts
|
|
158
|
+
|
|
159
|
+
A custom launcher is copied byte-for-byte, signed, and included in key derivation.
|
|
160
|
+
It must set:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import sys
|
|
164
|
+
sys.dont_write_bytecode = True
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
before any non-bootstrap import. The builder enforces this minimum requirement. A
|
|
168
|
+
secure production launcher must also verify that `pyencode_runtime` contains no
|
|
169
|
+
`__pycache__`, `.pyc`, or unexpected files **before** importing the package. The
|
|
170
|
+
default launcher and the Weldments pipeline include the complete preflight check.
|
|
171
|
+
|
|
172
|
+
A host such as Fusion can load an entry point without placing its plaintext module
|
|
173
|
+
name in the stub:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
import sys
|
|
177
|
+
sys.dont_write_bytecode = True
|
|
178
|
+
|
|
179
|
+
# Perform the pyencode_runtime preflight here.
|
|
180
|
+
from pyencode_runtime import load_entry
|
|
181
|
+
|
|
182
|
+
implementation = load_entry()
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`install()` intentionally returns `None`. `load_entry()` returns the entry module to
|
|
186
|
+
an embedding host, while `run()` executes the entry point with `__main__` semantics.
|
|
187
|
+
|
|
188
|
+
## Output layout and runtime behavior
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
dist/app/
|
|
192
|
+
├── .pyencode-manifest.json
|
|
193
|
+
├── myapp/
|
|
194
|
+
│ ├── 14a0...f91c.pye
|
|
195
|
+
│ ├── 7b82...bc33.pye
|
|
196
|
+
│ └── assets/config.json
|
|
197
|
+
├── pyencode_runtime/
|
|
198
|
+
│ ├── __init__.py
|
|
199
|
+
│ ├── _build.py
|
|
200
|
+
│ ├── _mp_main.py
|
|
201
|
+
│ └── _runtime.py
|
|
202
|
+
├── requirements.txt
|
|
203
|
+
└── run.py
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Resources are not encrypted, but their build-time bytes and paths are signed and
|
|
207
|
+
key-bound. By default, the entire output tree is immutable: the runtime rejects any
|
|
208
|
+
file or directory that is added, removed, or modified.
|
|
209
|
+
|
|
210
|
+
For hosts that create metadata or caches beside an add-in, `--allow-extra-data`
|
|
211
|
+
permits additional data files and directories without invalidating the bundle. This
|
|
212
|
+
mode still rejects modifications or removal of signed files, unexpected `.py`,
|
|
213
|
+
`.pyc`, native libraries, or `.pye` files, and all symlinks or reparse points. Do not
|
|
214
|
+
use this extra data as trusted input for licensing or security decisions. The
|
|
215
|
+
Weldments pipeline enables this compatibility mode so Fusion can create `.vscode`,
|
|
216
|
+
logs, or machine-specific caches.
|
|
217
|
+
|
|
218
|
+
The `__file__` value of a protected module is the real path to its randomly named
|
|
219
|
+
artifact. Consequently, `Path(__file__).parent`, `pkgutil.get_data()`, and
|
|
220
|
+
`importlib.resources`, including nested directory resources, continue to work. Code
|
|
221
|
+
that depends on the basename or stem of `__file__` sees the random token.
|
|
222
|
+
|
|
223
|
+
Integration tests cover imports by full name, relative imports, circular imports,
|
|
224
|
+
namespace packages, Unicode module names, reloads, `runpy.run_module()`, and
|
|
225
|
+
`multiprocessing` spawn for applications launched through the default launcher.
|
|
226
|
+
`pkgutil.iter_modules()` and `walk_packages()` cannot automatically discover
|
|
227
|
+
protected child names because those names intentionally reside in the encrypted
|
|
228
|
+
index; importing a known full name still works.
|
|
229
|
+
|
|
230
|
+
For an embedding host that calls `load_entry()`, spawning works if the child process
|
|
231
|
+
also runs the host bootstrap. A host that places its bootstrap only inside
|
|
232
|
+
`if __name__ == '__main__'` and then directly spawns a protected target requires a
|
|
233
|
+
dedicated launcher. Automatic spawn bootstrapping currently focuses on applications
|
|
234
|
+
executed through `run()`.
|
|
235
|
+
|
|
236
|
+
Version 0.3 uses one fixed runtime namespace per process. Do not load two independent
|
|
237
|
+
PyEncode distributions into the same interpreter; run them in separate processes.
|
|
238
|
+
Randomized or multi-bundle runtime namespaces are a compatibility item to resolve
|
|
239
|
+
before declaring a stable 1.0 API.
|
|
240
|
+
|
|
241
|
+
## Security limitations
|
|
242
|
+
|
|
243
|
+
PyEncode makes static analysis harder, but it cannot guarantee secrecy on a machine
|
|
244
|
+
fully controlled by an attacker:
|
|
245
|
+
|
|
246
|
+
- The default mode is offline, so key material must remain in the bundle. An
|
|
247
|
+
experienced analyst can inspect the runtime and reproduce the key-derivation
|
|
248
|
+
process.
|
|
249
|
+
- Code objects must exist in memory while they execute. Tracing, debuggers,
|
|
250
|
+
monkeypatching, or native hooks may still observe code and runtime state.
|
|
251
|
+
- The integrity interlock prevents straightforward file modification, artifact
|
|
252
|
+
substitution, code injection, and re-signing. It does not turn a pure-Python
|
|
253
|
+
runtime into a native trust anchor.
|
|
254
|
+
- Module and entry-point names are hidden at rest, but appear during import in
|
|
255
|
+
`sys.modules`, tracebacks, and runtime state. Package and resource directories may
|
|
256
|
+
still reveal part of the application structure.
|
|
257
|
+
- `--expires` relies on the system clock and is not a replacement for a licensing
|
|
258
|
+
system.
|
|
259
|
+
|
|
260
|
+
Never store long-lived API keys or private keys in a client. For high-value products,
|
|
261
|
+
the security upgrade that makes the greatest practical difference is an external or
|
|
262
|
+
envelope key obtained from a license server or keyring. A native runtime primarily
|
|
263
|
+
adds further reverse-engineering cost.
|
|
264
|
+
|
|
265
|
+
## Testing
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
python -m unittest discover -v
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
CI runs on Windows and Ubuntu with CPython 3.10 through 3.15. Tests cover PYE2, the
|
|
272
|
+
encrypted module index, KDF vectors, tampering and re-signing, resource/support
|
|
273
|
+
integrity, opaque filenames, imports, packages, namespaces, Unicode, `runpy`,
|
|
274
|
+
`multiprocessing` spawn, ordinary launchers without `-B`, expiration policy, and
|
|
275
|
+
atomic output creation.
|
|
276
|
+
|
|
277
|
+
## Releasing
|
|
278
|
+
|
|
279
|
+
The production release process using GitHub OIDC is documented in
|
|
280
|
+
[RELEASING.md](RELEASING.md). The workflow uploads to PyPI only when a pushed tag
|
|
281
|
+
exactly matches the project version, for example `v0.3.1`.
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyencode-protector"
|
|
7
|
-
version = "0.3.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.3.1"
|
|
8
|
+
description = "Protect CPython applications with authenticated encrypted module bundles"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|