Metadata-Version: 2.4
Name: obfuscatorx
Version: 0.0.3
Summary: Comprehensive AST-level Python source obfuscation toolkit. Obfuscates a whole project into a new directory.
Author: ObfuscatorX Contributors
License: MIT
Keywords: obfuscation,ast,python,protection,security
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Security
Classifier: License :: OSI Approved :: MIT License
Requires-Python: >=3.10,<3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: rich>=13.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# ObfuscatorX

Python 源码混淆工具包。将整个 Python 项目混淆后输出到新目录,源目录只读不修改。

> **v0.0.3 是一次安全返工**。旧版本存在若干「混淆后代码跑不起来」的语义缺陷,
> 以及「保护可被几十行脚本还原」的设计缺陷。变更清单见文末
> [v0.0.3 变更](#v003-变更与迁移)。

## 安装

需要 Python >= 3.10。

```sh
pip install obfuscatorx
```

从源码开发安装:

```sh
pip install -e ".[dev]"
```

若未安装且不想装(click + rich 已在环境中),可直接运行:

```sh
PYTHONPATH=src python3 -m obfuscatorx.cli.main --help
```

## 命令行使用

### 基本用法

```sh
# 默认配置混淆整个项目
obfuscatorx obfuscate 你的项目目录 -o 输出目录

# 可复现构建(同 seed 产出逐字节一致)
obfuscatorx obfuscate 你的项目目录 -o 输出目录 --seed 42

# 单文件分发模式(每文件变成 AEAD + marshal loader)
obfuscatorx obfuscate 你的项目目录 -o 输出目录 --seed 42 --loader

# 强密钥模式:密钥不随产物分发
obfuscatorx obfuscate 你的项目目录 -o 输出目录 --key-file my.key
OBFUSCATORX_KEY_FILE=my.key python 输出目录/main.py

# 保留某些公开名不被重命名(供外部 import)
obfuscatorx obfuscate 你的项目目录 -o 输出目录 --keep public_api_name

# 查看会处理哪些文件(不实际混淆)
obfuscatorx scan 你的项目目录
```

### 最复杂命令(全选项示例)

```sh
obfuscatorx obfuscate 你的项目目录 -o 输出目录 \
    --seed 31337 \
    --exclude "*.bak" \
    --keep public_api \
    --flatten \
    --strip-annotations \
    --polymorphic \
    --anti-debug \
    --key-file my.key \
    --integrity-strict \
    --compress \
    --max-ratio 1000.0 \
    --no-copy-assets \
    -v
```

> `--loader` 与 `--compress` 互斥(两者都是 post-obfuscation 包装,只能选一)。

### 选项速查

| 选项 | 作用 | 默认 |
|------|------|------|
| `-o, --output DIR` | 输出目录(必填) | — |
| `--seed N` | 可复现构建的种子 | 每次随机 |
| `--keep NAME` | 保留不重命名的标识符(可重复) | 无 |
| `--exclude PAT` | 排除文件模式(可重复) | `__pycache__`/`venv`/`*.egg-info` |
| `--key-file PATH` | 外部密钥文件,产物内**不含**密钥材料 | 无(密钥随产物) |
| `--aggressive-params` | 即使项目使用 `f(**mapping)` 也重命名形参(有风险) | 关 |
| `--copy-excluded-py` | 把被排除的 `.py` 复制进产物(会明文发布源码!) | 关 |
| `--max-ratio FLOAT` | 膨胀比超此值则拒绝输出 | 不限 |
| `--no-copy-assets` | 不复制非 .py 文件到输出 | 复制 |
| `-v, --verbose` | 打印进度 | 关 |

**Pass 开关**(默认开的用 `--no-` 关闭):

| 选项 | 作用 | 默认 |
|------|------|------|
| `--no-rename` | 关闭标识符重命名 | 开 |
| `--no-strings` | 关闭字符串加密 | 开 |
| `--no-encrypt-fstrings` | 不加密 f-string 静态文本(会泄漏字面量) | 加密 |
| `--no-numbers` | 关闭整数混淆 | 开 |
| `--no-mba` | 关闭位运算恒等变换 | 开 |
| `--no-opaque` | 关闭不透明谓词 | 开 |
| `--no-junk` | 关闭死代码注入 | 开 |
| `--no-lower-match` | 不降级 match 语句 | 降级 |
| `--no-integrity` | 关闭 HMAC 完整性校验 | 开 |
| `--integrity-strict` | 源文件不可读时判定为篡改(fail-closed) | 关 |

**可选 Pass**(默认关,用 flag 开启):

| 选项 | 作用 |
|------|------|
| `--flatten` | 控制流扁平化(while + dispatch 循环) |
| `--vm` | 把「单 `return <表达式>`」的顶层函数下沉为**密封栈式虚拟机字节码** + 随机操作码解释器 |
| `--vm-prob FLOAT` / `--vm-functions NAME` | 控制虚拟化覆盖率(默认全部合格的函数) |
| `--lazy` | 把顶层函数编译成 code object、AEAD 密封,首次调用时解密并重绑定 |
| `--lazy-prob FLOAT` / `--lazy-functions NAME` | 控制加密覆盖率(默认全部合格的函数) |
| `--polymorphic` | 运行时别名重绑定(已保留 `__name__`/`__qualname__`/`__wrapped__`,内省基本可用) |
| `--anti-debug` | 反调试检测(⚠️ 会影响 pytest/profiler,仅用于生产部署) |
| `--loader` | AEAD + marshal 单文件 loader(⚠️ 绑定 Python 版本) |
| `--compress` | zlib 压缩 + AEAD 包装每文件 |
| `--strip-annotations` | 剥离类型注解 |

> `--vm` 与 `--lazy` 只处理**顶层** `def`(没有闭包),方法、`async def`、带装饰器的
> 函数一律跳过。`--vm` 的适用面更窄:仅接受「函数体是一句 `return 表达式`」且表达式
> 由常量、名字、`+ - * // % | & ^ << >> **`、`- + ~ not`、单次比较、以及**纯位置参数**
> 的普通函数调用组成;其余一律原样保留。用 `-v` 可以看到每个档位实际保护了多少函数。
> `--lazy` 与 `--loader` 不能同时使用(会双层包装)。
> `--lower-fstrings` 已无必要:f-string 静态文本现在默认就地加密,该 flag 仅为兼容保留。

### 完整帮助

```sh
obfuscatorx obfuscate --help
```

## 安全模型

### 保护了什么

- **字符串/字节字面量**以整池 AEAD(encrypt-then-MAC)形式存储,每个模块独立密钥。
  池在首次使用时解密并缓存,热路径不重复做密码学运算。
- **f-string 静态文本**同样进入加密池(旧版本原样保留,会泄漏 URL、主机名、表名等)。
- **标识符**按作用域重命名为易混淆字符,跨模块导出一致。
- **字节码包装**(`--loader`/`--compress`)同样使用 AEAD,不再是「zlib 一层」。
- **函数级保护**(`--lazy` / `--vm`)把整个函数体或表达式下沉进密封资源,
  源码里只留下桩代码;资源与模块密钥绑定(AAD 含模块路径、资源类型与序号),
  不能跨模块或跨槽位搬运。
- **完整性校验**是密钥化的 HMAC,不再是「答案和考卷放在一起」的裸 SHA-256。
- 生成的运行时标识符**每个模块随机**,产物中没有 `_obf_decode` / `_OBF_POOL` /
  `_obf_runtime` 这类固定指纹,也不再依赖共享运行时模块(每个模块自包含,
  子模块可被直接执行,多个被混淆项目不会互相覆盖)。

### 没有保护什么(务必知悉)

- **密钥随产物分发时,密钥就是可还原的。** 未指定 `--key-file` 时,模块密钥必须以
  某种形式随产物携带;本工具把它拆成算术分片而非明文常量,这只是提高成本,不是保密。
  **需要真正的密钥保密,请使用 `--key-file`**(产物内不含任何密钥材料,运行时从
  `OBFUSCATORX_KEY` / `OBFUSCATORX_KEY_FILE` 读取)。
- **运行时内存中一定有明文。** 字符串最终要以 `str` 出现在进程里,调试器/内存转储
  可以拿到。这是 Python 层面的固有上限。
- **`--loader` 绑定 Python 版本**(marshal 格式),构建与运行必须同一
  `major.minor`。
- **类方法名与类属性名不做重命名**(`instance.attr` 静态无法解析类型)。
- **形参重命名会因关键字调用而受限**:若项目中存在任意 `f(**mapping)` 调用,
  默认**不重命名任何形参**(除非 `--aggressive-params`)。一旦某形参名出现在任何
  `f(name=...)` 调用点,该形参也保持原名。这是为了不破坏调用方。
- 混淆 ≠ 加密:任何能运行的代码最终都可逆向,本工具提供的是「层层递进的障碍」
  而非「保险箱」。

## 库内调用

### 项目级混淆

```python
from obfuscatorx import obfuscate_project
from obfuscatorx.obfuscator import ObfuscationConfig

obfuscate_project(
    src_root="你的项目目录",
    dst_root="输出目录",
    config=ObfuscationConfig(
        seed=42,                        # 可复现
        keep=frozenset({"api_name"}),    # 保留不改的公开名
        max_ratio=500.0,                 # 膨胀比超 500x 拒绝
        # 强密钥模式:产物内不含密钥
        master_key=b"my key material",
        # 可选 Pass(默认全 False,需手动开)
        flatten=True,                    # 控制流扁平化
        polymorphic=True,                # 运行时多态变异
        antidebug=True,                  # 反调试(⚠️ 影响测试框架)
        compress=True,                   # zlib + AEAD
    ),
)
```

### 单文件混淆

```python
from obfuscatorx.obfuscator import obfuscate_source, ObfuscationConfig

obfuscated = obfuscate_source(
    source="x = 42\nprint(x)\n",
    config=ObfuscationConfig(seed=42, integrity=False),
    rel_path="my_module.py",
    # project=None 时无跨模块重命名(见下)
)
exec(obfuscated, {})  # 输出: 42
```

> ⚠️ 单文件模式(`project=None`)下,跨模块导入名无法一致重命名,且**所有形参保持
> 原名**(因为没有项目级的调用点信息)。多模块项目请用 `obfuscate_project`。

## 注意事项

- **源目录只读**,产物写入新目录,不污染原代码。
- **可复现构建**:同 `--seed` 产出逐字节一致。唯一例外:若模块里存在**全字面量
  字符串集合**(`x in {"a", "b"}`)且字符串加密被 `--no-strings` 关掉,CPython 会把它
  折叠成 `frozenset` 常量,其迭代顺序依赖 `PYTHONHASHSEED`;此时需额外设置
  `PYTHONHASHSEED=0` 才能字节一致。默认开启字符串加密会消除这类常量;命中时构建
  会打印提示。
- **`--exclude` 排除的 `.py` 默认不会进入产物**。旧版本会把它们原样复制过去,
  等于把源码明文发布到「已保护」的目录里;如确需复制,显式传 `--copy-excluded-py`。
- **`--anti-debug`** 会检测 `sys.gettrace`/`sys.getprofile`、调试器模块与帧上的
  `f_trace`,可能干扰 pytest/profiler/CI,**仅用于生产部署**。
- **完整性校验**基于源码文本;若部署只保留 `.pyc`(或 frozen/zipapp),
  校验会被跳过。需要「读不到源码即判为篡改」时加 `--integrity-strict`。

## v0.0.3 变更与迁移

修复的语义缺陷(旧版本会产出运行时报错的代码):

- MBA 不再重写无法证明为 `int` 的 `-`/`&`/`|`/`^`(`{a|b}` 集合、`dict` 合并、
  `float`/`Decimal`/`timedelta` 减法此前会直接 `TypeError`)。
- 关键字实参:`f(name=...)`、keyword-only 参数、方法关键字调用不再因形参重命名而断裂。
- `{k: v for k, v in ...}` 的 value 现在会被正确重命名(此前 `NameError`)。
- `from __future__ import ...` 项目现在可以正常混淆(此前直接构建失败)。
- `import a.b`(无 `as`)保持原有绑定语义(此前 `AttributeError`)。
- `from m import *` 会冻结目标的导出名,导入方仍可解析(此前 `NameError`)。
- `--flatten` 不再被默认开启的 `--junk` 静默禁用。
- 数字混淆不再生成 `int('0x..', 16)` / `int.from_bytes(..)`,避免局部 `int` 覆盖内建。

修复的保护缺陷(旧版本的「加密」实为编码):

- 字符串池改为整池 AEAD,池内不再保存每字面量密钥;旧的 30 行静态还原脚本现在
  恢复出 0 条字面量。
- `--loader`/`--compress` 改为 AEAD;密钥不再能从文件自身(seed/长度/版本)直接推导。
- 完整性校验改为密钥化 HMAC;篡改后重算裸 SHA-256 不再通过。
- 反调试删除了永远不可能触发的计时检查,并移除了会把路径含 `trace` 的正常程序
  直接 `SystemExit` 的误杀逻辑。
- 默认 seed 不再悄悄退化为 0(每次构建随机);`--seed` 仍然逐字节可复现。
- 运行时标识符全部随机化,不再有固定指纹,也不再有 `__obf_match_subject__` 这类
  逃过重命名的残留标记。
- 不存在 `_obf_runtime.py` 共享模块,子模块可直接 `python pkg/mod.py` 运行。

新增能力(旧版本接受但静默无效的两个 flag 现在真正工作):

- **`--vm`**:合格的顶层 `return <表达式>` 被编译成随机操作码的栈式字节码并 AEAD 密封,
  运行时由生成的解释器执行。解释器使用 `operator` 的真实运算,**对类型完全透明**
  (集合/字典/浮点/自定义类型语义不变)。
- **`--lazy`**:合格的顶层函数被编译成 code object、marshal + AEAD 密封,首次调用时
  解密、用 `types.FunctionType` 重建并回填模块名;桩函数保留原签名,关键字调用、
  默认值、装饰器(装饰器函数本身不处理)与递归都正常。
- **`--polymorphic`** 保留元数据(`__name__`/`__qualname__`/`__doc__`/`__module__`/
  `__wrapped__`),`inspect.signature` 可用;`async def` 不再被处理。
- 发布链路加固:GitHub Actions 全部固定到 release tag 的 commit SHA,
  `cibuildwheel`/`nuitka` 固定版本,workflow 显式收窄 `permissions: contents: read`。

行为变更(迁移注意):

- 路径含 `**kwargs` 调用的项目默认不重命名形参,可用 `--aggressive-params` 覆盖。
- `--exclude` 的 `.py` 默认不再复制进产物,可用 `--copy-excluded-py` 恢复旧行为。
- `--lazy` 不能与 `--loader` 同用(会双层包装),会直接报错。
- 单文件 API(`obfuscate_source`)默认 `integrity=True`,但 `__file__` 不存在时
  会自动跳过校验,因此 `exec(obfuscated, {})` 仍然可用。

## 发布与凭据

- PyPI 凭据请走 `TWINE_USERNAME=__token__` + `TWINE_PASSWORD`(或 CI secret),
  **不要**把 token 写进仓库根目录的 `.pypirc`。该文件已在 `.gitignore` 中且从未
  进入 git 历史;如本机已存在明文 token,建议在 PyPI 侧吊销后改用 keyring:
  `python -m keyring set https://upload.pypi.org/legacy/ __token__`。
- 升级 GitHub Actions 时,用
  `git ls-remote --tags --refs https://github.com/<owner>/<repo> refs/tags/<tag>`
  取新 SHA 并同步更新 workflow 中的版本注释。
