pyfernet-payload 0.2.4__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.
- pyfernet_payload-0.2.4/LICENSE +21 -0
- pyfernet_payload-0.2.4/PKG-INFO +151 -0
- pyfernet_payload-0.2.4/README.md +120 -0
- pyfernet_payload-0.2.4/pyproject.toml +59 -0
- pyfernet_payload-0.2.4/setup.cfg +4 -0
- pyfernet_payload-0.2.4/setup.py +63 -0
- pyfernet_payload-0.2.4/src/pyfernet/__init__.py +14 -0
- pyfernet_payload-0.2.4/src/pyfernet/__main__.py +8 -0
- pyfernet_payload-0.2.4/src/pyfernet/cli.py +158 -0
- pyfernet_payload-0.2.4/src/pyfernet/constants.py +31 -0
- pyfernet_payload-0.2.4/src/pyfernet/encryptor.py +143 -0
- pyfernet_payload-0.2.4/src/pyfernet/fernet_lite.py +231 -0
- pyfernet_payload-0.2.4/src/pyfernet/loader.py +195 -0
- pyfernet_payload-0.2.4/src/pyfernet/py.typed +1 -0
- pyfernet_payload-0.2.4/src/pyfernet/vfs.py +251 -0
- pyfernet_payload-0.2.4/src/pyfernet_payload.egg-info/PKG-INFO +151 -0
- pyfernet_payload-0.2.4/src/pyfernet_payload.egg-info/SOURCES.txt +18 -0
- pyfernet_payload-0.2.4/src/pyfernet_payload.egg-info/dependency_links.txt +1 -0
- pyfernet_payload-0.2.4/src/pyfernet_payload.egg-info/entry_points.txt +2 -0
- pyfernet_payload-0.2.4/src/pyfernet_payload.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shunyaoyin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyfernet-payload
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Encrypt a Python source directory into one .enc file and run it from memory (Fernet AES, stdlib-only).
|
|
5
|
+
Home-page: https://github.com/yinshunyao/PyFernet
|
|
6
|
+
Author: shunyaoyin
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/yinshunyao/PyFernet
|
|
9
|
+
Project-URL: Issues, https://github.com/yinshunyao/PyFernet/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/yinshunyao/PyFernet#readme
|
|
11
|
+
Keywords: fernet,aes,encrypt,source-protection,in-memory,training
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security :: Cryptography
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
|
|
32
|
+
# PyFernet (`pyfernet-payload`)
|
|
33
|
+
|
|
34
|
+
[中文说明](README.zh-CN.md)
|
|
35
|
+
|
|
36
|
+
Pack a training source directory into a single `.enc` blob; on the client, decrypt and run from an **in-memory VFS**. Only an **empty directory tree** is created on disk (for `Path(__file__)` / `chdir`); `.py` / config **contents are never written**. Removed on exit.
|
|
37
|
+
|
|
38
|
+
- **Zero third-party runtime deps** (stdlib AES-128-CBC + Fernet-compatible format)
|
|
39
|
+
- **PyPI name**: `pyfernet-payload` (`pyfernet` is already taken)
|
|
40
|
+
- **CLI / import**: `pyfernet` / `import pyfernet`
|
|
41
|
+
- **Relative paths work**: `Path(__file__).parent / "train_config.json"`, `open`, `spec_from_file_location` via VFS hooks
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pyfernet-payload
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
From a local clone:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# preferred on older pip / mirrors (no editable hook needed)
|
|
53
|
+
python3 -m pip install .
|
|
54
|
+
|
|
55
|
+
# or editable (dev); if this fails, upgrade pip first:
|
|
56
|
+
python3 -m pip install -U pip setuptools wheel
|
|
57
|
+
python3 -m pip install -e .
|
|
58
|
+
```
|
|
59
|
+
## CLI
|
|
60
|
+
|
|
61
|
+
### Encrypt
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pyfernet encrypt ./my_train -o train_payload.enc -e train.py
|
|
65
|
+
# passphrase via prompt; or:
|
|
66
|
+
export PYFERNET_PASSWORD='your-secret'
|
|
67
|
+
pyfernet encrypt ./my_train -o train_payload.enc -e train.py --password-env PYFERNET_PASSWORD
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Run
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pyfernet run train_payload.enc
|
|
74
|
+
# forward args to the training entry (after --):
|
|
75
|
+
pyfernet run train_payload.enc -- --epochs 50 --batch 8
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Equivalent:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python -m pyfernet encrypt ./my_train -o train_payload.enc -e train.py
|
|
82
|
+
python -m pyfernet run train_payload.enc
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Workflow
|
|
86
|
+
|
|
87
|
+
1. **Local**: write training code → `pyfernet encrypt` → `train_payload.enc`
|
|
88
|
+
2. **SFTP**: upload the ciphertext only (client already has `pip install pyfernet-payload`)
|
|
89
|
+
3. **SSH**: `pyfernet run train_payload.enc`, enter passphrase
|
|
90
|
+
4. Decrypt into memory → empty dir anchors + VFS hooks → start training; write weights to a normal disk path
|
|
91
|
+
5. On exit, remove empty dirs; durable disk still has only ciphertext
|
|
92
|
+
|
|
93
|
+
If the entry uses sibling dirs (e.g. `train_detect_rtdetrv2/` + `train_detect_cfg/` + `train_detect_yolo/`), **encrypt the parent folder** and set `-e train_detect_rtdetrv2/train_insect.py`.
|
|
94
|
+
|
|
95
|
+
Inference code, datasets, and weights can stay plaintext on the client.
|
|
96
|
+
|
|
97
|
+
## IDE variable mode (optional)
|
|
98
|
+
|
|
99
|
+
With no CLI args, modules use the variables at the bottom of the file:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python -m pyfernet.encryptor # no args → SOURCE_DIR / OUTPUT_PATH / ENTRY_POINT
|
|
103
|
+
python -m pyfernet.loader # no args → PAYLOAD_PATH / TRAIN_ARGV
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
With args, they use the CLI, e.g. `python -m pyfernet.encryptor ./src -o out.enc -e train.py`.
|
|
107
|
+
|
|
108
|
+
## Library API
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from pyfernet import encrypt_directory, run_payload
|
|
112
|
+
|
|
113
|
+
encrypt_directory("examples/demo_train", "dist/train_payload.enc", "train.py", "secret")
|
|
114
|
+
run_payload("dist/train_payload.enc", "secret", argv=["train.py", "--epochs", "1"])
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Payload format
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
PYFE1\0 + 16B salt + Fernet(token)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Fernet plaintext is a zip (sources + `_pyfernet_manifest.json`). The passphrase is stretched with PBKDF2-HMAC-SHA256 (390000 iterations).
|
|
124
|
+
|
|
125
|
+
## Notes
|
|
126
|
+
|
|
127
|
+
- Source file **bytes never hit disk**; only empty directories exist briefly (often under `/dev/shm` on Linux)
|
|
128
|
+
- Process memory / debuggers can still see plaintext; a leaked passphrase decrypts the blob
|
|
129
|
+
- By default only `.py` and a few config suffixes are packed—do not put datasets or weights in the blob
|
|
130
|
+
- The client still needs training deps installed (torch, ultralytics, etc.)
|
|
131
|
+
|
|
132
|
+
## Publish to PyPI
|
|
133
|
+
|
|
134
|
+
See [docs/PUBLISH.md](docs/PUBLISH.md) ([中文](docs/PUBLISH.zh-CN.md)).
|
|
135
|
+
|
|
136
|
+
## Layout
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
PyFernet/
|
|
140
|
+
pyproject.toml
|
|
141
|
+
README.md # English (PyPI)
|
|
142
|
+
README.zh-CN.md # Chinese
|
|
143
|
+
src/pyfernet/
|
|
144
|
+
cli.py
|
|
145
|
+
encryptor.py
|
|
146
|
+
loader.py
|
|
147
|
+
fernet_lite.py
|
|
148
|
+
examples/demo_train/
|
|
149
|
+
docs/PUBLISH.md
|
|
150
|
+
docs/PUBLISH.zh-CN.md
|
|
151
|
+
```
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# PyFernet (`pyfernet-payload`)
|
|
2
|
+
|
|
3
|
+
[中文说明](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Pack a training source directory into a single `.enc` blob; on the client, decrypt and run from an **in-memory VFS**. Only an **empty directory tree** is created on disk (for `Path(__file__)` / `chdir`); `.py` / config **contents are never written**. Removed on exit.
|
|
6
|
+
|
|
7
|
+
- **Zero third-party runtime deps** (stdlib AES-128-CBC + Fernet-compatible format)
|
|
8
|
+
- **PyPI name**: `pyfernet-payload` (`pyfernet` is already taken)
|
|
9
|
+
- **CLI / import**: `pyfernet` / `import pyfernet`
|
|
10
|
+
- **Relative paths work**: `Path(__file__).parent / "train_config.json"`, `open`, `spec_from_file_location` via VFS hooks
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install pyfernet-payload
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
From a local clone:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# preferred on older pip / mirrors (no editable hook needed)
|
|
22
|
+
python3 -m pip install .
|
|
23
|
+
|
|
24
|
+
# or editable (dev); if this fails, upgrade pip first:
|
|
25
|
+
python3 -m pip install -U pip setuptools wheel
|
|
26
|
+
python3 -m pip install -e .
|
|
27
|
+
```
|
|
28
|
+
## CLI
|
|
29
|
+
|
|
30
|
+
### Encrypt
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pyfernet encrypt ./my_train -o train_payload.enc -e train.py
|
|
34
|
+
# passphrase via prompt; or:
|
|
35
|
+
export PYFERNET_PASSWORD='your-secret'
|
|
36
|
+
pyfernet encrypt ./my_train -o train_payload.enc -e train.py --password-env PYFERNET_PASSWORD
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Run
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pyfernet run train_payload.enc
|
|
43
|
+
# forward args to the training entry (after --):
|
|
44
|
+
pyfernet run train_payload.enc -- --epochs 50 --batch 8
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Equivalent:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m pyfernet encrypt ./my_train -o train_payload.enc -e train.py
|
|
51
|
+
python -m pyfernet run train_payload.enc
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Workflow
|
|
55
|
+
|
|
56
|
+
1. **Local**: write training code → `pyfernet encrypt` → `train_payload.enc`
|
|
57
|
+
2. **SFTP**: upload the ciphertext only (client already has `pip install pyfernet-payload`)
|
|
58
|
+
3. **SSH**: `pyfernet run train_payload.enc`, enter passphrase
|
|
59
|
+
4. Decrypt into memory → empty dir anchors + VFS hooks → start training; write weights to a normal disk path
|
|
60
|
+
5. On exit, remove empty dirs; durable disk still has only ciphertext
|
|
61
|
+
|
|
62
|
+
If the entry uses sibling dirs (e.g. `train_detect_rtdetrv2/` + `train_detect_cfg/` + `train_detect_yolo/`), **encrypt the parent folder** and set `-e train_detect_rtdetrv2/train_insect.py`.
|
|
63
|
+
|
|
64
|
+
Inference code, datasets, and weights can stay plaintext on the client.
|
|
65
|
+
|
|
66
|
+
## IDE variable mode (optional)
|
|
67
|
+
|
|
68
|
+
With no CLI args, modules use the variables at the bottom of the file:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python -m pyfernet.encryptor # no args → SOURCE_DIR / OUTPUT_PATH / ENTRY_POINT
|
|
72
|
+
python -m pyfernet.loader # no args → PAYLOAD_PATH / TRAIN_ARGV
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
With args, they use the CLI, e.g. `python -m pyfernet.encryptor ./src -o out.enc -e train.py`.
|
|
76
|
+
|
|
77
|
+
## Library API
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pyfernet import encrypt_directory, run_payload
|
|
81
|
+
|
|
82
|
+
encrypt_directory("examples/demo_train", "dist/train_payload.enc", "train.py", "secret")
|
|
83
|
+
run_payload("dist/train_payload.enc", "secret", argv=["train.py", "--epochs", "1"])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Payload format
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
PYFE1\0 + 16B salt + Fernet(token)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Fernet plaintext is a zip (sources + `_pyfernet_manifest.json`). The passphrase is stretched with PBKDF2-HMAC-SHA256 (390000 iterations).
|
|
93
|
+
|
|
94
|
+
## Notes
|
|
95
|
+
|
|
96
|
+
- Source file **bytes never hit disk**; only empty directories exist briefly (often under `/dev/shm` on Linux)
|
|
97
|
+
- Process memory / debuggers can still see plaintext; a leaked passphrase decrypts the blob
|
|
98
|
+
- By default only `.py` and a few config suffixes are packed—do not put datasets or weights in the blob
|
|
99
|
+
- The client still needs training deps installed (torch, ultralytics, etc.)
|
|
100
|
+
|
|
101
|
+
## Publish to PyPI
|
|
102
|
+
|
|
103
|
+
See [docs/PUBLISH.md](docs/PUBLISH.md) ([中文](docs/PUBLISH.zh-CN.md)).
|
|
104
|
+
|
|
105
|
+
## Layout
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
PyFernet/
|
|
109
|
+
pyproject.toml
|
|
110
|
+
README.md # English (PyPI)
|
|
111
|
+
README.zh-CN.md # Chinese
|
|
112
|
+
src/pyfernet/
|
|
113
|
+
cli.py
|
|
114
|
+
encryptor.py
|
|
115
|
+
loader.py
|
|
116
|
+
fernet_lite.py
|
|
117
|
+
examples/demo_train/
|
|
118
|
+
docs/PUBLISH.md
|
|
119
|
+
docs/PUBLISH.zh-CN.md
|
|
120
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# setuptools>=64 才有 PEP 660 editable;旧 pip 会回退读同目录 setup.py
|
|
3
|
+
requires = ["setuptools>=68", "wheel"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "pyfernet-payload"
|
|
8
|
+
version = "0.2.4"
|
|
9
|
+
description = "Encrypt a Python source directory into one .enc file and run it from memory (Fernet AES, stdlib-only)."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "shunyaoyin" },
|
|
16
|
+
]
|
|
17
|
+
keywords = [
|
|
18
|
+
"fernet",
|
|
19
|
+
"aes",
|
|
20
|
+
"encrypt",
|
|
21
|
+
"source-protection",
|
|
22
|
+
"in-memory",
|
|
23
|
+
"training",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Environment :: Console",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Topic :: Security :: Cryptography",
|
|
37
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
38
|
+
"Typing :: Typed",
|
|
39
|
+
]
|
|
40
|
+
# 零运行时第三方依赖(纯标准库)
|
|
41
|
+
dependencies = []
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/yinshunyao/PyFernet"
|
|
45
|
+
Issues = "https://github.com/yinshunyao/PyFernet/issues"
|
|
46
|
+
Documentation = "https://github.com/yinshunyao/PyFernet#readme"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
pyfernet = "pyfernet.cli:main"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
package-dir = { "" = "src" }
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
56
|
+
include = ["pyfernet*"]
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.package-data]
|
|
59
|
+
pyfernet = ["py.typed"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""setuptools entry for older pip that does not fully honor pyproject [project].
|
|
3
|
+
|
|
4
|
+
Keeps name/version/entry points explicit so installs are not UNKNOWN-0.0.0.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from setuptools import find_packages, setup
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parent
|
|
15
|
+
SRC = ROOT / "src"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _version() -> str:
|
|
19
|
+
init = (SRC / "pyfernet" / "__init__.py").read_text(encoding="utf-8")
|
|
20
|
+
m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init, re.M)
|
|
21
|
+
if not m:
|
|
22
|
+
raise RuntimeError("cannot find __version__ in pyfernet/__init__.py")
|
|
23
|
+
return m.group(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
setup(
|
|
27
|
+
name="pyfernet-payload",
|
|
28
|
+
version=_version(),
|
|
29
|
+
description=(
|
|
30
|
+
"Encrypt a Python source directory into one .enc file "
|
|
31
|
+
"and run it from memory (Fernet AES, stdlib-only)."
|
|
32
|
+
),
|
|
33
|
+
long_description=(ROOT / "README.md").read_text(encoding="utf-8"),
|
|
34
|
+
long_description_content_type="text/markdown",
|
|
35
|
+
author="shunyaoyin",
|
|
36
|
+
url="https://github.com/yinshunyao/PyFernet",
|
|
37
|
+
license="MIT",
|
|
38
|
+
package_dir={"": "src"},
|
|
39
|
+
packages=find_packages(where="src"),
|
|
40
|
+
package_data={"pyfernet": ["py.typed"]},
|
|
41
|
+
include_package_data=True,
|
|
42
|
+
python_requires=">=3.10",
|
|
43
|
+
install_requires=[],
|
|
44
|
+
entry_points={
|
|
45
|
+
"console_scripts": [
|
|
46
|
+
"pyfernet=pyfernet.cli:main",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
classifiers=[
|
|
50
|
+
"Development Status :: 4 - Beta",
|
|
51
|
+
"Environment :: Console",
|
|
52
|
+
"Intended Audience :: Developers",
|
|
53
|
+
"License :: OSI Approved :: MIT License",
|
|
54
|
+
"Operating System :: OS Independent",
|
|
55
|
+
"Programming Language :: Python :: 3",
|
|
56
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
57
|
+
"Programming Language :: Python :: 3.10",
|
|
58
|
+
"Programming Language :: Python :: 3.11",
|
|
59
|
+
"Programming Language :: Python :: 3.12",
|
|
60
|
+
"Programming Language :: Python :: 3.13",
|
|
61
|
+
"Topic :: Security :: Cryptography",
|
|
62
|
+
],
|
|
63
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PyFernet: encrypt a source directory into one .enc file and run it from memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.4"
|
|
6
|
+
|
|
7
|
+
from pyfernet.encryptor import encrypt_directory
|
|
8
|
+
from pyfernet.loader import run_payload
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"__version__",
|
|
12
|
+
"encrypt_directory",
|
|
13
|
+
"run_payload",
|
|
14
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""命令行入口:pyfernet encrypt | run"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pyfernet import __version__
|
|
11
|
+
from pyfernet.encryptor import main as encrypt_main
|
|
12
|
+
from pyfernet.loader import list_payload, main as run_main
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_PASSWORD_ENV = "PYFERNET_PASSWORD"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _password_from_args(password: str | None, password_env: str | None) -> str | None:
|
|
19
|
+
"""解析口令:-p > --password-env > 环境变量 PYFERNET_PASSWORD > None(交互)。"""
|
|
20
|
+
if password is not None:
|
|
21
|
+
return password
|
|
22
|
+
env_name = password_env or DEFAULT_PASSWORD_ENV
|
|
23
|
+
# 显式传了 --password-env,或默认名在环境里有值时,走环境变量
|
|
24
|
+
if password_env is not None or env_name in os.environ:
|
|
25
|
+
value = os.environ.get(env_name)
|
|
26
|
+
if not value:
|
|
27
|
+
raise SystemExit(
|
|
28
|
+
f"环境变量未设置或为空: {env_name}\n"
|
|
29
|
+
f" --password-env 后面是「变量名」不是口令本身。\n"
|
|
30
|
+
f" 正确示例:\n"
|
|
31
|
+
f" export {DEFAULT_PASSWORD_ENV}='你的口令'\n"
|
|
32
|
+
f" nohup pyfernet run train.enc --password-env {DEFAULT_PASSWORD_ENV} > d.log 2>&1 &\n"
|
|
33
|
+
f" 或(会进进程列表,仅临时用):\n"
|
|
34
|
+
f" nohup pyfernet run train.enc -p '你的口令' > d.log 2>&1 &"
|
|
35
|
+
)
|
|
36
|
+
return value
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _split_script_args(argv: list[str]) -> tuple[list[str], list[str]]:
|
|
41
|
+
"""将 `run ... -- script_args` 拆开,避免 REMAINDER 吞掉 -p/--password-env。"""
|
|
42
|
+
if argv and argv[0] == "run" and "--" in argv:
|
|
43
|
+
idx = argv.index("--")
|
|
44
|
+
return argv[:idx], argv[idx + 1 :]
|
|
45
|
+
return argv, []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
49
|
+
parser = argparse.ArgumentParser(
|
|
50
|
+
prog="pyfernet",
|
|
51
|
+
description="将源码目录加密为单个 .enc,并在内存中解密运行(磁盘无明文源码)。",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
54
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
55
|
+
|
|
56
|
+
p_enc = sub.add_parser("encrypt", help="打包并加密源码目录")
|
|
57
|
+
p_enc.add_argument("source_dir", type=Path, help="要加密的源码目录")
|
|
58
|
+
p_enc.add_argument(
|
|
59
|
+
"-o",
|
|
60
|
+
"--output",
|
|
61
|
+
type=Path,
|
|
62
|
+
default=Path("train_payload.enc"),
|
|
63
|
+
help="输出密文路径(默认: train_payload.enc)",
|
|
64
|
+
)
|
|
65
|
+
p_enc.add_argument(
|
|
66
|
+
"-e",
|
|
67
|
+
"--entry",
|
|
68
|
+
default="train.py",
|
|
69
|
+
help="相对源目录的入口脚本(默认: train.py)",
|
|
70
|
+
)
|
|
71
|
+
p_enc.add_argument(
|
|
72
|
+
"-p",
|
|
73
|
+
"--password",
|
|
74
|
+
default=None,
|
|
75
|
+
help="口令(不推荐写在命令行;默认交互输入)。也可用 --password-env",
|
|
76
|
+
)
|
|
77
|
+
p_enc.add_argument(
|
|
78
|
+
"--password-env",
|
|
79
|
+
default=None,
|
|
80
|
+
metavar="VAR",
|
|
81
|
+
help="从环境变量读口令(填变量名,如 PYFERNET_PASSWORD;不是口令本身)",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
p_run = sub.add_parser(
|
|
85
|
+
"run",
|
|
86
|
+
help="解密密文包并在内存中执行",
|
|
87
|
+
epilog="传给训练脚本的参数写在 -- 之后,例如: pyfernet run a.enc -- --epochs 10",
|
|
88
|
+
)
|
|
89
|
+
p_run.add_argument("payload", type=Path, help="密文包路径(.enc)")
|
|
90
|
+
p_run.add_argument(
|
|
91
|
+
"-p",
|
|
92
|
+
"--password",
|
|
93
|
+
default=None,
|
|
94
|
+
help="口令(不推荐写在命令行;默认交互输入)。也可用 --password-env",
|
|
95
|
+
)
|
|
96
|
+
p_run.add_argument(
|
|
97
|
+
"--password-env",
|
|
98
|
+
default=None,
|
|
99
|
+
metavar="VAR",
|
|
100
|
+
help="从环境变量读口令(填变量名,如 PYFERNET_PASSWORD;不是口令本身)",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
p_list = sub.add_parser("list", help="列出密文包内文件(需口令)")
|
|
104
|
+
p_list.add_argument("payload", type=Path, help="密文包路径(.enc)")
|
|
105
|
+
p_list.add_argument("-p", "--password", default=None, help="口令(默认交互输入)")
|
|
106
|
+
p_list.add_argument(
|
|
107
|
+
"--password-env",
|
|
108
|
+
default=None,
|
|
109
|
+
metavar="VAR",
|
|
110
|
+
help="从环境变量读口令(填变量名;不是口令本身)",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return parser
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main(argv: list[str] | None = None) -> int:
|
|
117
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
118
|
+
cli_argv, script_args = _split_script_args(raw)
|
|
119
|
+
|
|
120
|
+
parser = build_parser()
|
|
121
|
+
args = parser.parse_args(cli_argv)
|
|
122
|
+
|
|
123
|
+
if args.command == "encrypt":
|
|
124
|
+
password = _password_from_args(args.password, args.password_env)
|
|
125
|
+
encrypt_main(
|
|
126
|
+
source_dir=str(args.source_dir),
|
|
127
|
+
output_path=str(args.output),
|
|
128
|
+
entry_point=args.entry,
|
|
129
|
+
password=password,
|
|
130
|
+
)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
if args.command == "run":
|
|
134
|
+
password = _password_from_args(args.password, args.password_env)
|
|
135
|
+
entry_name = Path(args.payload).name
|
|
136
|
+
train_argv = [entry_name, *script_args]
|
|
137
|
+
run_main(payload_path=str(args.payload), password=password, argv=train_argv)
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
if args.command == "list":
|
|
141
|
+
password = _password_from_args(args.password, args.password_env)
|
|
142
|
+
if password is None:
|
|
143
|
+
import getpass
|
|
144
|
+
|
|
145
|
+
password = getpass.getpass("解密口令: ")
|
|
146
|
+
entry, files = list_payload(args.payload, password)
|
|
147
|
+
print(f"entry: {entry}")
|
|
148
|
+
print(f"files: {len(files)}")
|
|
149
|
+
for name in files:
|
|
150
|
+
print(name)
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
parser.error(f"未知命令: {args.command}")
|
|
154
|
+
return 2
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared payload format constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
MAGIC = b"PYFE1\0"
|
|
6
|
+
SALT_LEN = 16
|
|
7
|
+
KDF_ITERATIONS = 390_000
|
|
8
|
+
MANIFEST_NAME = "_pyfernet_manifest.json"
|
|
9
|
+
|
|
10
|
+
SKIP_DIRS = {
|
|
11
|
+
".git",
|
|
12
|
+
"__pycache__",
|
|
13
|
+
".venv",
|
|
14
|
+
"venv",
|
|
15
|
+
".mypy_cache",
|
|
16
|
+
".pytest_cache",
|
|
17
|
+
"node_modules",
|
|
18
|
+
}
|
|
19
|
+
SKIP_SUFFIXES = {".pyc", ".pyo", ".enc"}
|
|
20
|
+
ALLOW_SUFFIXES = {
|
|
21
|
+
".py",
|
|
22
|
+
".yaml",
|
|
23
|
+
".yml",
|
|
24
|
+
".json",
|
|
25
|
+
".toml",
|
|
26
|
+
".cfg",
|
|
27
|
+
".ini",
|
|
28
|
+
".txt",
|
|
29
|
+
".md",
|
|
30
|
+
}
|
|
31
|
+
ALLOW_NAMES = {"LICENSE", "NOTICE"}
|