|
1 | | -# Python-Obfuscator |
| 1 | +# python-obfuscator |
2 | 2 |
|
3 | | -One night I got bored of writing good code, so I made good code to make bad code. |
| 3 | +[](https://github.com/davidteather/python-obfuscator/actions/workflows/package-test.yml) |
| 4 | +[](https://github.com/davidteather/python-obfuscator/releases) |
| 5 | +[](https://pypi.org/project/python-obfuscator/) |
| 6 | +[](https://codecov.io/gh/davidteather/python-obfuscator) |
4 | 7 |
|
5 | | -[](https://github.com/davidteather/python-obfuscator/releases) [](https://pypi.org/project/python-obfuscator/)  [](https://www.linkedin.com/in/david-teather-4400a37a/) |
| 8 | +A Python source-code obfuscator built on the standard-library `ast` module. It applies multiple independent techniques — each individually togglable — to make code harder to read while keeping it fully executable. See [Known limitations](#known-limitations) before use. |
6 | 9 |
|
7 | | -### **DONT USE IN PRODUCTION** |
| 10 | +If this project is useful to you, consider [sponsoring development](https://github.com/sponsors/davidteather). |
8 | 11 |
|
9 | | -**I just made this because it was interesting to me. I do plan on making this more official in the future, but currently don't have the time!** |
10 | | - |
11 | | -Consider sponsoring me [here](https://github.com/sponsors/davidteather) |
| 12 | +--- |
12 | 13 |
|
13 | 14 | ## Installing |
14 | 15 |
|
15 | | -``` |
| 16 | +```bash |
16 | 17 | pip install python-obfuscator |
17 | 18 | ``` |
18 | 19 |
|
19 | | -## Quickstart |
| 20 | +Requires Python ≥ 3.10. |
20 | 21 |
|
21 | | -By default, obfuscated code is written under an **`obfuscated/`** directory (created next to your current working directory). Paths under the current directory are preserved (e.g. `src/app.py` → `obfuscated/src/app.py`). |
| 22 | +--- |
22 | 23 |
|
23 | | -``` |
24 | | -pyobfuscate -i your_file.py |
25 | | -``` |
| 24 | +## Quick start — CLI |
26 | 25 |
|
27 | | -Print to stdout instead (e.g. for piping): |
| 26 | +```bash |
| 27 | +# Writes obfuscated/your_file.py (path structure is preserved) |
| 28 | +pyobfuscate -i your_file.py |
28 | 29 |
|
29 | | -``` |
| 30 | +# Print to stdout |
30 | 31 | pyobfuscate -i your_file.py --stdout |
31 | | -``` |
32 | 32 |
|
33 | | -Print the installed version (from `python_obfuscator.version`): |
| 33 | +# Disable specific techniques |
| 34 | +pyobfuscate -i your_file.py --disable dead_code_injector --disable exec_wrapper |
34 | 35 |
|
35 | | -``` |
| 36 | +# Show version |
36 | 37 | pyobfuscate --version |
37 | 38 | ``` |
38 | 39 |
|
39 | | -## More Detailed Documentation |
| 40 | +--- |
| 41 | + |
| 42 | +## Python API |
| 43 | + |
| 44 | +### One-shot helper |
| 45 | + |
| 46 | +```python |
| 47 | +from python_obfuscator import obfuscate |
| 48 | + |
| 49 | +source = "x = 1\nprint(x + 2)" |
| 50 | +result = obfuscate(source) |
| 51 | +print(result) |
| 52 | +``` |
| 53 | + |
| 54 | +### Selective techniques |
| 55 | + |
| 56 | +```python |
| 57 | +from python_obfuscator import obfuscate, ObfuscationConfig |
| 58 | + |
| 59 | +# All techniques except dead-code injection |
| 60 | +config = ObfuscationConfig.all_enabled().without("dead_code_injector") |
| 61 | +result = obfuscate(source, config=config) |
40 | 62 |
|
41 | | -You can use this as a module if you want |
| 63 | +# Only string encoding |
| 64 | +config = ObfuscationConfig.only("string_hex_encoder") |
| 65 | +result = obfuscate(source, config=config) |
42 | 66 | ``` |
43 | | -import python_obfuscator |
44 | | -obfuscator = python_obfuscator.obfuscator() |
45 | 67 |
|
46 | | -code_to_obfuscate = "print('hello world')" |
| 68 | +### Reusing across multiple files (caches the pipeline) |
| 69 | + |
| 70 | +```python |
| 71 | +from python_obfuscator import Obfuscator, ObfuscationConfig |
| 72 | + |
| 73 | +obf = Obfuscator(ObfuscationConfig.all_enabled()) |
| 74 | +for path in my_files: |
| 75 | + path.write_text(obf.obfuscate(path.read_text())) |
47 | 76 | ``` |
48 | 77 |
|
49 | | -You can also exclude certain techniques applied for obfuscation |
| 78 | +### Config combinators |
| 79 | + |
| 80 | +```python |
| 81 | +cfg = ObfuscationConfig.all_enabled() # every registered technique |
| 82 | +cfg = ObfuscationConfig.only("variable_renamer", "exec_wrapper") |
| 83 | +cfg = cfg.without("exec_wrapper") # returns a new frozen config |
| 84 | +cfg = cfg.with_added("dead_code_injector") |
50 | 85 | ``` |
51 | | -import python_obfuscator |
52 | | -from python_obfuscator.techniques import add_random_variables |
53 | | -obfuscator = python_obfuscator.obfuscator() |
54 | 86 |
|
55 | | -code_to_obfuscate = "print('hello world')" |
56 | | -obfuscated_code = obfuscator.obfuscate(code_to_obfuscate, remove_techniques=[add_random_variables]) |
| 87 | +--- |
| 88 | + |
| 89 | +## Techniques |
| 90 | + |
| 91 | +| Name | Priority | What it does | |
| 92 | +|------|----------|--------------| |
| 93 | +| `variable_renamer` | 10 | Renames local variables, function names, parameters, and class names to visually ambiguous identifiers (`lIIllIlI…`). Excludes builtins, imports, dunders, and attribute-accessed names. | |
| 94 | +| `string_hex_encoder` | 20 | Replaces every string literal `"hi"` with `bytes.fromhex('6869').decode('utf-8')`. Skips f-strings. | |
| 95 | +| `dead_code_injector` | 30 | Injects dead variable assignments at **every scope level** — module body, function bodies, class bodies, if/for/while/try/with branches. Some assignments reference other dead variables to simulate computation. | |
| 96 | +| `exec_wrapper` | 100 | Wraps the entire module in a single `exec("…")` call, reducing the top-level AST to one statement. Runs last. | |
| 97 | + |
| 98 | +Techniques are applied in priority order (lowest first). |
| 99 | + |
| 100 | +--- |
| 101 | + |
| 102 | +## Example |
| 103 | + |
| 104 | +**Input** |
| 105 | + |
| 106 | +```python |
| 107 | +def greet(name): |
| 108 | + msg = "Hello, " + name |
| 109 | + print(msg) |
| 110 | + |
| 111 | +greet("world") |
57 | 112 | ``` |
58 | | -Find a list of all techniques [here](https://github.com/davidteather/python-obfuscator/blob/210da2d3dfb96ab7653fad869a43cb67aeb0fe67/python_obfuscator/techniques.py#L87) |
59 | 113 |
|
60 | | -## Example Obfuscated Code |
| 114 | +**Output** (all techniques enabled — abridged) |
61 | 115 |
|
62 | | -Input |
| 116 | +```python |
| 117 | +exec('def lIlIllI(IIlIlII):\n lIllIlI = bytes.fromhex(\'48656c6c6f2c20\').decode(\'utf-8\') + IIlIlII\n ...\nlIlIllI(bytes.fromhex(\'776f726c64\').decode(\'utf-8\'))') |
63 | 118 | ``` |
64 | | -y = input("what's your favorite number") |
65 | 119 |
|
66 | | -user_value = int(y) |
67 | | -print("{} that's a great number!".format(user_value)) |
| 120 | +--- |
| 121 | + |
| 122 | +## Performance overhead |
| 123 | + |
| 124 | +Benchmarks run on an Apple M-series machine, 20 iterations each. The test programs cover OOP, algorithms, functional patterns, number theory, and string processing. |
| 125 | + |
| 126 | +### Total overhead (all techniques) |
| 127 | + |
| 128 | +| Program | Original | Obfuscated | Overhead | |
| 129 | +|---------|----------|------------|---------| |
| 130 | +| `algorithms.py` | 0.94 ms | 1.98 ms | +112% | |
| 131 | +| `cipher.py` | 1.37 ms | 2.67 ms | +95% | |
| 132 | +| `data_structures.py` | 0.72 ms | 2.20 ms | +207% | |
| 133 | +| `functional.py` | 0.67 ms | 1.71 ms | +155% | |
| 134 | +| `number_theory.py` | 1.84 ms | 3.16 ms | +72% | |
| 135 | +| `oop_simulation.py` | 0.68 ms | 1.66 ms | +144% | |
| 136 | + |
| 137 | +### Per-technique contribution (average across all programs) |
| 138 | + |
| 139 | +| Technique | Avg overhead | Notes | |
| 140 | +|-----------|-------------|-------| |
| 141 | +| `variable_renamer` | ~5% | Pure rename — negligible at runtime | |
| 142 | +| `string_hex_encoder` | ~12% | `bytes.fromhex` call per string literal | |
| 143 | +| `dead_code_injector` | ~85% | Dominant cost — dead assignments execute every iteration | |
| 144 | +| `exec_wrapper` | ~2% | Single extra `exec` layer | |
| 145 | + |
| 146 | +The dead-code injector's overhead scales with the number of scopes and loop iterations in the original program. Programs with tight inner loops see the most overhead. |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## Known limitations |
| 151 | + |
| 152 | +- **Class method names are not renamed.** Attribute-accessed names (`obj.method`) cannot be safely renamed without full type-inference, so the renamer conservatively excludes them. |
| 153 | +- **Keyword argument names are not renamed.** `fn(key=val)` call-site keyword strings are bare AST strings, not `Name` nodes, and are not updated when a parameter is renamed. |
| 154 | +- **No scope-aware renaming.** The same identifier used in two independent function scopes maps to the same obfuscated name (which is semantically correct but less obfuscated than it could be). |
| 155 | +- **No control-flow obfuscation.** Opaque predicates, bogus branches, and integer encoding are not implemented. |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## Running the test suite |
| 160 | + |
| 161 | +```bash |
| 162 | +pip install -e ".[dev]" |
| 163 | +pytest |
68 | 164 | ``` |
69 | 165 |
|
70 | | -[With `pyobfuscate -i file.py`](https://gist.github.com/davidteather/b6ff932140d8c174b9c6f50c9b42fdaf) |
| 166 | +Coverage is enforced at ≥ 95% on every CI run. |
71 | 167 |
|
| 168 | +```bash |
| 169 | +# With coverage report |
| 170 | +coverage run -m pytest && coverage report |
72 | 171 |
|
73 | | -[With `--one-liner`](https://gist.github.com/davidteather/75e48c04bf74f0262fe2919239a74295) |
| 172 | +# E2E tests with benchmark output |
| 173 | +pytest tests/e2e/ -v -s |
| 174 | +``` |
74 | 175 |
|
75 | | -## Authors |
| 176 | +--- |
76 | 177 |
|
77 | | -* **David Teather** - *Initial work* - [davidteather](https://github.com/davidteather) |
| 178 | +## Authors |
78 | 179 |
|
79 | | -See also the list of [contributors](https://github.com/davidteather/python-obfuscator) who participated in this project. |
| 180 | +**David Teather** — [davidteather](https://github.com/davidteather) |
80 | 181 |
|
81 | 182 | ## License |
82 | 183 |
|
83 | | -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details |
| 184 | +MIT — see [LICENSE](LICENSE). |
0 commit comments