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.
Files changed (37) hide show
  1. pyencode_protector-0.3.1/PKG-INFO +304 -0
  2. pyencode_protector-0.3.1/README.md +281 -0
  3. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/pyproject.toml +2 -2
  4. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/__init__.py +1 -1
  5. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/cli.py +62 -13
  6. pyencode_protector-0.3.1/src/pyencode_protector.egg-info/PKG-INFO +304 -0
  7. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/SOURCES.txt +1 -0
  8. pyencode_protector-0.3.1/tests/test_cli.py +50 -0
  9. pyencode_protector-0.3.0/PKG-INFO +0 -276
  10. pyencode_protector-0.3.0/README.md +0 -253
  11. pyencode_protector-0.3.0/src/pyencode_protector.egg-info/PKG-INFO +0 -276
  12. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/setup.cfg +0 -0
  13. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/__main__.py +0 -0
  14. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/builder.py +0 -0
  15. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/code_hardening.py +0 -0
  16. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/container.py +0 -0
  17. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/crypto.py +0 -0
  18. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/discovery.py +0 -0
  19. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/errors.py +0 -0
  20. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/integrity.py +0 -0
  21. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/inventory.py +0 -0
  22. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/manifest.py +0 -0
  23. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/opaque.py +0 -0
  24. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/__init__.py +0 -0
  25. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/_build.py +0 -0
  26. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/_mp_main.py +0 -0
  27. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/runtime_template/_runtime.py +0 -0
  28. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode/source.py +0 -0
  29. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/dependency_links.txt +0 -0
  30. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/entry_points.txt +0 -0
  31. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/requires.txt +0 -0
  32. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/src/pyencode_protector.egg-info/top_level.txt +0 -0
  33. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_code_hardening.py +0 -0
  34. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_core.py +0 -0
  35. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_integration.py +0 -0
  36. {pyencode_protector-0.3.0 → pyencode_protector-0.3.1}/tests/test_inventory.py +0 -0
  37. {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.0"
8
- description = "A small cross-platform Python module protector"
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" }
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.3.1"