> 技术文档 > 【PyArmor】Python 代码加密与授权的终极防线_pyarmor license

【PyArmor】Python 代码加密与授权的终极防线_pyarmor license

PyArmor:Python 代码加密与授权的终极防线

第一章:PyArmor 概览与核心特性

1.1 PyArmor 是什么?

  • 定义与目标 (Definition and Objectives):
    PyArmor 是一款用于加密和保护 Python 脚本的命令行工具。它的核心目标是使得 Python 代码难以被逆向工程,保护源代码不被轻易查看和修改,同时提供强大的授权管理功能,确保软件按照开发者的意愿被使用。PyArmor 不是将 Python 代码编译成本地机器码(像 Nuitka 那样),而是通过对 Python 编译后的字节码进行加密,并依赖一个动态链接库(_pytransform.so, _pytransform.dll, 或 _pytransform.dylib)在运行时动态解密和执行这些字节码。这种机制旨在平衡保护强度和跨平台兼容性。

    PyArmor 的设计哲学可以概括为以下几点:

    1. 高强度保护: 提供多种加密和混淆技术,显著增加逆向工程的难度。
    2. 灵活授权: 支持多种授权方式,如绑定硬件、设置有效期、自定义绑定数据等。
    3. 跨平台兼容: 支持 Windows, Linux, macOS 以及部分嵌入式系统。
    4. 易于集成: 能够与常见的 Python 打包工具(如 PyInstaller, cx_Freeze)良好集成。
    5. 最小化依赖: 加密后的脚本仅依赖核心的 _pytransform 动态库。
  • 与其他代码保护工具的对比(简要)(Comparison with Other Code Protection Tools (Brief)):
    Python 生态中存在多种代码保护或打包方案,各有侧重:

    • .pyc 文件: Python 解释器在加载 .py 文件时会自动生成 .pyc 文件,其中包含预编译的字节码。这能略微提高加载速度,但其字节码格式是公开的,很容易被反编译工具(如 uncompyle6)还原成近似的源代码。保护强度非常低。
    • Cython: Cython 可以将 Python 代码转换为 C 代码,然后编译成本地扩展模块。这可以提供显著的性能提升和一定程度的代码保护,因为 C 代码比 Python 字节码更难逆向。但 Cython 的主要目标是性能优化和与 C/C++库的集成,代码保护是其副产品。转换过程可能需要修改部分 Python 代码以适应静态类型。
    • Nuitka: Nuitka 是一个 Python 编译器,它将 Python 代码编译成 C 代码,然后链接成可执行文件或扩展模块,目标是完全兼容 Python 并提供更好的性能和可执行文件分发。Nuitka 提供了比 .pyc 和 Cython 更强的保护,因为最终产物是本地机器码。然而,编译大型项目可能耗时较长,且对某些动态特性的处理可能比较复杂。
    • PyOxidizer: PyOxidizer 是一个专注于将 Python 应用程序打包成单个可执行文件的工具,它将 Python 解释器、依赖库和代码都嵌入其中。它也提供了一定的保护,因为代码不再以独立的 .py.pyc 文件形式存在。其主要目标是打包和分发,而非极致的加密。
    • PyArmor: PyArmor 专注于字节码加密和授权管理。它不对 Python 代码做语言层面的转换(如转 C),而是在字节码层面进行操作。这使得它对原始 Python 代码的兼容性非常好,几乎不需要修改代码。其核心优势在于提供了运行时的动态解密和细粒度的授权控制。

    简要对比总结:

    特性 .pyc Cython Nuitka PyOxidizer PyArmor 主要目标 缓存 性能优化, C集成 性能优化, 可执行文件 打包, 单文件分发 代码加密, 授权管理 保护机制 无 Python -> C -> 机器码 Python -> C -> 机器码 嵌入解释器和字节码 字节码加密, 运行时动态库解密 保护强度 极低 中等 中高 中等 高 授权管理 无 无 (需自行实现) 无 (需自行实现) 无 (需自行实现) 内建强大授权机制 代码修改 无 可能需要 通常不需要,但复杂特性需注意 不需要 几乎不需要 跨平台 是 是 (需为各平台编译) 是 (需为各平台编译) 是 (需为各平台编译) 是 (动态库需对应平台) 依赖 无 编译后的扩展模块 编译后的可执行文件/扩展模块 单文件 (内部包含解释器) _pytransform 动态库

    PyArmor 在需要强代码保护和灵活授权管理的场景下具有明显优势,特别是当开发者不希望大幅修改现有 Python 代码,并需要快速部署时。

1.2 PyArmor 的核心功能

PyArmor 提供了一系列精心设计的功能,以实现对 Python 应用程序的全面保护。

  • 代码加密 (Code Encryption):
    这是 PyArmor 最核心的功能。

    • .pyc 文件的局限性: 如前所述,标准的 .pyc 文件只是 Python 源代码的字节码表示,其格式是公开的,可以使用现成的工具(如 decompyle3, uncompyle6)相对容易地反编译回可读性较高的 Python 代码。因此,仅仅分发 .pyc 文件并不能有效保护知识产权。
    • PyArmor 的字节码级加密: PyArmor 不仅仅是生成 .pyc 文件,它在生成字节码之后,会对这些字节码进行高强度的加密。这意味着即使有人获取了 PyArmor 处理过的文件(通常扩展名仍为 .pyc.pyo,或者在打包后嵌入到可执行文件中),这些文件中的字节码内容也是加密的,无法直接被标准 Python 解释器执行,也无法被常规的反编译工具识别和处理。
    • 运行时解密: 加密的字节码在程序运行时,由 PyArmor 提供的核心动态库 _pytransform 进行动态解密。这个解密过程是在内存中进行的,解密后的原始字节码不会轻易暴露在文件系统中。
  • 运行授权 (Runtime Authorization):
    PyArmor 提供了强大而灵活的许可证管理系统,允许开发者控制加密脚本的运行权限。

    • 许可证文件 (license.lic): 开发者可以为加密的脚本生成一个或多个许可证文件。这些许可证文件包含了授权信息。
    • 硬件绑定: 许可证可以绑定到特定的硬件特征,例如:
      • 硬盘序列号 (Harddisk Serial Number)
      • 网卡 MAC 地址 (MAC Address)
      • IPv4 地址 (IPv4 Address)
      • 主板序列号等 (Motherboard Serial, etc. - 取决于平台和 PyArmor 版本支持)
        一旦绑定,加密脚本只能在具有匹配硬件信息的机器上运行。
    • 有效期限制 (Expiration Date): 许可证可以设置一个有效期,过期后加密脚本将无法运行。这对于试用版软件或订阅制软件非常有用。
    • 自定义绑定数据 (Binding to Custom Data): 许可证还可以绑定到开发者定义的任意字符串数据。这可以用于实现更复杂的授权逻辑,例如绑定到用户名、客户ID或其他业务相关信息。
    • 多种许可证策略: 可以为不同的客户或不同的产品版本生成具有不同限制的许可证。
  • 混淆 (Obfuscation):
    除了加密,PyArmor 还提供了一些混淆技术,进一步增加逆向工程的难度。混淆的目的是让代码的结构和逻辑变得难以理解,即使部分字节码被解密(理论上极难),混淆后的代码仍然难以分析。

    • 名称混淆 (Name Mangling/Obfuscation): 虽然 PyArmor 的主要混淆手段不是简单的名称替换(因为 Python 的动态性使得这很复杂且容易出错),但其加密和内部处理机制客观上达到了隐藏原始名称的效果。
    • 更深层次的混淆选项: PyArmor 提供如 BCC 模式(将 Python 函数转换为加密的 C 函数表示)和 RFT 模式(运行时函数追踪)等高级选项,这些都属于更深层次的结构性混淆和保护。
      • --obf-mod (模块混淆级别): 控制模块级别的混淆程度。
      • --obf-code (代码对象混淆级别): 控制代码对象(如函数体)的混淆程度。级别越高,保护越强,但可能对性能有轻微影响。
  • 跨平台支持 (Cross-Platform Support):
    PyArmor 致力于支持主流的操作系统:

    • Windows: (32位 和 64位)
    • Linux: (x86_64, ARM, aarch64 等多种架构)
    • macOS: (Intel 和 Apple Silicon M1/M2)
    • 嵌入式系统: 如 Raspberry Pi。
      加密脚本本身是平台无关的(因为是 Python 字节码),但其运行依赖的 _pytransform 动态库是平台相关的。因此,为不同平台分发加密应用时,需要确保包含了对应平台的 _pytransform 库。PyArmor 工具链会自动处理或帮助开发者获取这些平台特定的库。

1.3 PyArmor 的工作原理(高层次)

理解 PyArmor 的工作原理有助于更好地使用它并解决可能遇到的问题。其核心机制可以概括为以下几个步骤:

  1. 加密阶段 (Obfuscation Phase - 使用 pyarmor obfuscate 命令时):

    • 解析 Python 脚本: PyArmor 首先像 Python 解释器一样解析 .py 脚本。
    • 编译为字节码: 将 Python 源代码编译成标准的 Python 字节码。
    • 字节码加密与转换: 这是 PyArmor 的核心步骤。它使用内部的加密算法(可能结合用户的 pytransform.key 或全局密钥)对字节码进行加密。根据选择的混淆选项(如 BCC 模式),字节码可能会被进一步转换或包裹。
    • 生成加密脚本: 加密后的内容通常会替换原始 .py 文件(如果使用 --inplace 模式)或者输出到 dist 目录中,文件名可能保持不变(但内容已加密)或者变为 .pyc
    • 打包 _pytransform 动态库: PyArmor 会将运行加密脚本所必需的 _pytransform 动态库(例如 _pytransform.dll on Windows, _pytransform.so on Linux, _pytransform.dylib on macOS)一并输出到 dist 目录。这个库是平台相关的。
    • 生成引导代码 (Bootstrap Code): 对于被加密的模块,PyArmor 可能会注入一些引导代码(通常在模块的开头),这些代码负责在模块被导入时初始化 PyArmor 的运行时环境,加载 _pytransform 库,并准备解密后续的字节码。
  2. 运行阶段 (Runtime Phase - 执行加密后的脚本时):

    • 加载 _pytransform: 当 Python 解释器尝试导入一个被 PyArmor 加密的模块时,模块头部的引导代码会首先被执行。这个引导代码的主要任务是找到并加载与当前平台匹配的 _pytransform 动态库。
    • 初始化运行时环境: _pytransform 加载后,会进行一些初始化工作,例如设置钩子(hooks)来接管 Python 解释器加载字节码的过程。
    • 许可证校验: 在执行加密代码之前,_pytransform 会查找并校验许可证文件 (license.lic)。
      • 它会检查许可证是否存在、是否过期、硬件绑定信息是否匹配(如果设置了绑定)、自定义数据是否匹配等。
      • 如果许可证无效或校验失败,_pytransform 会阻止脚本继续执行,并通常会抛出一个异常。
    • 动态解密与执行: 如果许可证校验通过,当 Python 解释器需要执行加密模块中的某个函数或代码块时:
      • _pytransform 截获对加密字节码的访问。
      • 在内存中动态解密这部分字节码。
      • 将解密后的字节码(仍然是标准的 Python 字节码)交给 Python 解释器执行。
      • 这个解密过程是按需、小块进行的,而不是一次性解密整个模块,以增强安全性。
    • 保护机制持续生效: 在整个脚本运行期间,_pytransform 会持续监控和保护,例如防止通过某些手段(如调试器、内存转储)直接获取解密后的字节码。
  • 动态库 _pytransform 的角色:
    _pytransform 是 PyArmor 的心脏。它是一个用 C/C++ 编写的动态链接库(DLL/SO/DYLIB),包含了以下关键功能:

    • 解密引擎: 负责解密被 PyArmor 加密的字节码。
    • 许可证校验逻辑: 实现所有与许可证相关的检查。
    • 运行时保护: 提供反调试、反篡改等运行时保护措施。
    • 与 Python 解释器的交互接口: 通过 Python C API 与解释器紧密协作,以便在合适的时机介入字节码的加载和执行流程。
      没有正确版本的 _pytransform 库,或者该库被篡改,加密后的脚本将无法运行。
  • 加密脚本的执行流程 (Execution Flow of an Encrypted Script):

    1. 用户执行主脚本 (e.g., python main_encrypted.py)。
    2. 如果 main_encrypted.py 本身被加密,其头部的引导代码执行。
    3. 引导代码加载 _pytransform 动态库。
    4. _pytransform 初始化,进行许可证校验。
    5. 如果许可证有效,_pytransform 准备按需解密 main_encrypted.py 中的字节码。
    6. Python 解释器开始执行 main_encrypted.py 的(解密后的)字节码。
    7. main_encrypted.py import 其他被加密的模块 (e.g., import utils_encrypted) 时:
      a. utils_encrypted.py 的引导代码执行(如果它还没有被加载和初始化过)。
      b. _pytransform (已经被加载) 准备按需解密 utils_encrypted.py 中的字节码。
      c. 解释器执行 utils_encrypted.py 的(解密后的)字节码。
    8. 如果导入未加密的模块,则按正常流程执行。
  • 许可证的验证机制 (License Verification Mechanism):

    • 查找: _pytransform 默认会在几个预定义的位置查找 license.lic 文件:
      • _pytransform 动态库相同的目录。
      • 与主脚本相同的目录。
      • 当前工作目录。
      • 可以通过环境变量或特定 API 调用来指定许可证路径。
    • 解析: 读取许可证文件内容。许可证文件本身通常也是经过编码或加密的,以防止直接篡改。
    • 校验:
      • 完整性检查: 确保许可证文件未被损坏或篡改。
      • 有效期检查: 获取当前系统日期,与许可证中的有效期进行比较。
      • 硬件绑定检查 (如果启用):
        • _pytransform 会调用系统 API 获取当前机器的硬件信息(如硬盘序列号、MAC 地址)。
        • 将获取到的硬件信息与许可证中记录的绑定信息进行比对。这个比对过程可能不是简单的字符串相等,可能会有一定的容错机制或使用特定的编码方式。
      • 自定义数据检查 (如果启用): 将许可证中绑定的自定义数据与运行时提供的数据(或预设数据)进行比较。
    • 结果: 如果所有检查都通过,则授权成功。任何一步失败,都会导致授权失败,脚本无法执行或功能受限。

PyArmor 的这种工作原理,使得它能够在不改变 Python 语言本身运行方式的前提下,增加一层坚固的保护壳。

1.4 安装与基本配置

在开始使用 PyArmor 之前,首先需要将其安装到你的 Python 环境中。

  • 安装 PyArmor (pip install pyarmor):
    PyArmor 本身是一个 Python 包,可以通过 pip(Python 的包安装器)轻松安装。打开你的命令行终端(如 Windows 的 CMD 或 PowerShell,Linux/macOS 的 Terminal),然后执行以下命令:

    pip install pyarmor

    这条命令会从 PyPI (Python Package Index) 下载最新稳定版的 PyArmor 及其必要的依赖,并将其安装到你当前的 Python 环境中。

    中文解释:

    • pip: 是 Python 用来安装和管理软件包的工具。
    • install: 是 pip 的一个子命令,表示要执行安装操作。
    • pyarmor: 是要安装的包的名称。

    如果你想安装特定版本的 PyArmor,可以使用:

    pip install pyarmor==X.Y.Z

    X.Y.Z 替换为你想要安装的具体版本号。

    如果你在访问 PyPI 时速度较慢,可以考虑使用国内的镜像源,例如清华大学的镜像:

    pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pyarmor

    中文解释:

    • -i https://pypi.tuna.tsinghua.edu.cn/simple: 指定了包的下载源为清华大学的镜像服务器,这通常可以加快国内用户的下载速度。

    安装完成后,pyarmor 命令行工具就会被添加到系统的 PATH 路径中(通常情况下),你可以在任何目录下直接调用它。

  • 获取 PyArmor 版本信息 (pyarmor --version):
    为了验证 PyArmor 是否成功安装,并查看其版本号,可以在命令行中执行:

    pyarmor --version

    如果安装成功,它会输出类似如下的信息:

    PyArmor 8.x.y

    (具体的版本号会根据你安装的版本而变化)

    中文解释:

    • pyarmor: 调用 PyArmor 命令行工具。
    • --version: 是 PyArmor 的一个选项,用于显示其版本信息。

    了解 PyArmor 的版本很重要,因为不同版本可能会有功能上的差异或特定的 bug修复。查阅官方文档时,通常需要对应你正在使用的版本。

  • 基本的项目结构 (Basic Project Structure):
    当你使用 PyArmor 加密一个 Python 项目时,PyArmor 通常会在你的项目目录下或指定的输出目录(默认为 dist)下生成一些特定的文件和文件夹。一个典型的 PyArmor 项目在加密后可能会有如下结构:

    假设你的原始项目结构是:

    my_project/├── main.py├── utils/│ ├── __init__.py│ └── helper.py└── data/ └── config.json

    当你使用 pyarmor obfuscate main.pypyarmor obfuscate -O output_dir main.py 这样的命令后,生成的 dist 目录 (或 output_dir) 可能会包含:

    dist/ (或指定的输出目录)├── main.py # 加密后的主脚本 (文件名可能与原脚本相同,但内容已加密)├── utils/  # 如果 utils 目录下的模块也被递归加密│ ├── __init__.py # 加密后的 __init__.py│ └── helper.py # 加密后的 helper.py├── _pytransform.so # (Linux) PyArmor 核心动态库├── _pytransform.dll # (Windows, 如果在 Windows 上加密)├── _pytransform.dylib # (macOS, 如果在 macOS 上加密)├── license.lic # (如果生成了许可证) 默认的许可证文件└── pytransform.key # (如果生成了项目特定的密钥文件) 项目密钥文件

    关键文件和目录解释:

    • 加密后的 .py 文件: 这些文件的内容是经过 PyArmor 加密和处理的字节码容器。它们通常不能被标准的 Python 解释器直接理解,除非有 _pytransform 的辅助。
    • _pytransform.so/.dll/.dylib: 这是 PyArmor 的核心运行时库,平台相关。加密后的 Python 代码在执行时必须能够找到并加载这个库。它是执行解密、许可证校验等操作的关键。
    • license.lic: 这是默认的许可证文件。当 PyArmor 生成需要许可证的加密脚本时,会创建这个文件。它包含了授权信息。你可以为不同的客户生成不同的 license.lic 文件。
    • pytransform.key: 这是 PyArmor 用于加密项目的一个关键文件(通常在第一次对某个项目执行 obfuscateinit 时生成,或者使用全局密钥)。这个密钥文件对于后续更新加密脚本或生成与此项目兼容的许可证至关重要。请务必妥善备份此文件,如果丢失,你可能无法更新之前用它加密的脚本,或者无法为这些脚本生成新的许可证。 默认情况下,PyArmor 可能会在用户主目录下的 .pyarmor/ 文件夹中存储一个全局的 pytransform.key,或者在项目本地生成。

    理解这个基本的文件结构有助于你部署和分发加密后的应用程序。通常,你需要将 dist 目录下的所有内容(或其子集,取决于你的打包策略)一起分发给最终用户。如果你的项目还依赖其他非 Python 数据文件(如 data/config.json),你需要确保这些文件也被正确地包含在分发包中,PyArmor 的 --manifest 选项可以帮助处理这类文件。

    在实际使用中,PyArmor 提供了多种命令和选项来定制加密过程和输出结构,我们将在后续章节中详细探讨。

第二章:PyArmor 基础使用:加密你的第一个脚本

本章我们将从最基础的用法开始,学习如何使用 PyArmor 来加密单个 Python 脚本以及整个 Python 包。我们会详细解释相关命令、生成的产物以及如何运行加密后的代码。

2.1 单个脚本加密

这是 PyArmor 最直接的应用场景:你有一个独立的 Python 脚本文件,希望对其进行加密保护。

  • pyarmor obfuscate 命令详解:
    obfuscate 是 PyArmor 的核心命令,用于执行加密操作。

    假设你有一个简单的 Python 脚本 hello.py

    # hello.pydef greet(name): message = f\"你好, {  name}! 欢迎使用 PyArmor 加密的应用。\" print(message) return messageif __name__ == \"__main__\": user_name = input(\"请输入你的名字: \") greet(user_name) print(\"脚本执行完毕。\")

    中文代码解释:

    • # hello.py: 这是一个注释,标明文件名。
    • def greet(name):: 定义一个名为 greet 的函数,它接受一个参数 name
    • message = f\"你好, {name}! 欢迎使用 PyArmor 加密的应用。\": 创建一个格式化字符串,包含问候语和传入的 name
    • print(message): 打印这个问候消息到控制台。
    • return message: 函数返回这个消息字符串。
    • if __name__ == \"__main__\":: 这是一个 Python 的常用结构,表示当该脚本作为主程序直接运行时,才执行下面的代码块。
    • user_name = input(\"请输入你的名字: \"): 提示用户输入名字,并将输入内容存储在 user_name 变量中。
    • greet(user_name): 调用 greet 函数,并将用户输入的名字作为参数传递。
    • print(\"脚本执行完毕。\"): 打印一条表示脚本结束的消息。

    现在,我们使用 PyArmor 来加密这个 hello.py 脚本。在命令行中,切换到 hello.py 所在的目录,然后执行:

    pyarmor obfuscate hello.py

    中文命令解释:

    • pyarmor: 调用 PyArmor 工具。
    • obfuscate: 指定执行加密操作。
    • hello.py: 指明要加密的目标脚本文件。

    执行该命令后,PyArmor 会进行一系列操作,并在当前目录下创建一个名为 dist 的子目录。

  • 生成的文件结构 (dist 目录, _pytransform.so/.dll/.dylib):
    命令执行成功后,查看 dist 目录,你会发现类似如下的文件结构(以 Linux 为例,Windows 下为 .dll,macOS 下为 .dylib):

    dist/├── hello.py # 这是加密后的 hello.py└── _pytransform.so # PyArmor 运行时核心库

    如果你是在 Windows 上操作,会看到 _pytransform.dll;在 macOS 上则是 _pytransform.dylib

    文件解释:

    • dist/hello.py: 这个文件虽然名字和原始脚本一样,但其内容已经被 PyArmor 加密。如果你用文本编辑器打开它,会看到一些引导代码和大量不可读的(或看起来像乱码的)数据。
      # PyArmor Script, DO NOT MODIFY!# -*- coding: utf-8 -*-# Source: hello.py# Last Changed: Wed Nov 15 10:00:00 2023# Platform: # Python Version: from pytransform import pyarmor_runtimepyarmor_runtime(__name__, __file__, b\'\\x50\\x59\\x41\\x52\\x4d\\x4f\\x52\\x00\\x00...\') # 这里是大量加密数据# ... 可能还有其他引导代码 ...

      注意开头的注释,它明确指出这是一个 PyArmor 加密脚本,并且警告不要修改它。关键在于 from pytransform import pyarmor_runtimepyarmor_runtime(...) 这一行,它负责加载运行时并处理加密的字节码。

    • dist/_pytransform.so (或 .dll, .dylib): 这是 PyArmor 的核心运行时库。没有这个文件,加密后的 hello.py 将无法执行。它包含了执行解密、许可证校验等所有底层逻辑。这个文件是特定于你执行 pyarmor obfuscate 命令时所在的操作系统和架构的。例如,在 64 位 Linux 上生成的 _pytransform.so 不能直接用于 Windows 或 32 位 Linux。

    默认情况下,PyArmor 在加密单个脚本时,会生成一个不需要额外许可证文件 (license.lic) 即可运行的加密脚本。这种默认的许可证是“试用版”许可证,它通常没有功能限制,但可能会在运行时打印 PyArmor 的相关信息,或者有其他非商业版本的标记。对于商业分发,你需要生成并使用正式的许可证。

  • 运行加密后的脚本:
    要运行加密后的脚本,你需要确保 Python 解释器能够找到 _pytransform 动态库。最简单的方法是直接在 dist 目录下运行脚本。

    切换到 dist 目录:

    cd dist

    然后像运行普通 Python 脚本一样运行它:

    python hello.py

    中文命令解释:

    • cd dist: 切换当前目录到 dist 子目录。
    • python hello.py: 使用 Python 解释器执行当前目录下的 hello.py (这个是加密后的版本)。

    程序会像原始脚本一样提示你输入名字,并打印问候语。

    请输入你的名字: [你输入名字,例如:张三]你好, 张三! 欢迎使用 PyArmor 加密的应用。脚本执行完毕。

    如果一切正常,这意味着 PyArmor 成功地加密了你的脚本,并且运行时库 _pytransform 也被正确加载和执行。

    重要提示:

    • 加密后的脚本依赖于 _pytransform 动态库。在分发你的加密应用时,必须将这个库与加密脚本一起提供给用户,并且它们通常需要在同一目录下,或者 _pytransform 所在的目录需要在 Python 的模块搜索路径中(或者系统的动态库搜索路径中)。
    • 如果你在 dist 目录之外尝试运行加密的 hello.py,例如,在 my_project 目录下执行 python dist/hello.py,只要 dist 目录在 Python 的 sys.path 中(通常会自动添加),或者 _pytransformdist/hello.py 在一起,通常也能工作。但为了确保可靠性,推荐将加密脚本和 _pytransform 保持在同一分发单元内。
  • 代码示例与行级解释 (回顾 hello.py 和 PyArmor 的作用):

    原始 hello.py:

    # hello.pydef greet(name): # 定义 greet 函数 message = f\"你好, {  name}! 欢迎使用 PyArmor 加密的应用。\" # 构造消息 print(message) # 打印消息 return message # 返回消息if __name__ == \"__main__\": # 主程序入口 user_name = input(\"请输入你的名字: \") # 获取用户输入 greet(user_name) # 调用 greet 函数 print(\"脚本执行完毕。\") # 结束语

    PyArmor 的作用:

    1. 分析 hello.py: PyArmor 读取并理解这个脚本的结构。
    2. 编译字节码: 将 greet 函数和 if __name__ == \"__main__\": 块中的代码编译成 Python 字节码。
    3. 加密字节码: 使用其内部算法加密这些字节码。
    4. 生成 dist/hello.py: 创建一个新的 hello.py 文件,其中包含:
      • 一些 PyArmor 的引导代码 (bootstrap code)。
      • 加密后的字节码数据。
    5. 复制 _pytransform: 将对应平台的 _pytransform 动态库复制到 dist 目录。

    当运行 dist/hello.py 时:

    1. Python 解释器执行 dist/hello.py 中的引导代码。
    2. 引导代码加载 _pytransform 库。
    3. _pytransform 库初始化,并准备在需要时解密脚本中的字节码。
    4. 当 Python 解释器尝试执行 greet 函数或主程序块时,_pytransform 在内存中解密相应的字节码,然后交给解释器执行。
    5. 用户看到的行为与原始未加密脚本完全一致。

    这就是 PyArmor 单个脚本加密的基础流程。通过这个简单的例子,你可以看到 PyArmor 如何在不改变脚本原有功能的前提下,为其增加一层保护。

2.2 整个项目(包)加密

通常,一个 Python 应用不仅仅是一个单独的 .py 文件,而是由多个模块和包组成的复杂项目。PyArmor 同样支持对整个 Python 包或项目进行递归加密。

  • pyarmor obfuscate -r :
    pyarmor obfuscate 命令配合 -r (或 --recursive) 选项,可以递归地加密指定目录下的所有 .py 文件。

    假设我们有以下项目结构 my_app/:

    my_app/├── main.py├── calculators/│ ├── __init__.py│ ├── basic_ops.py│ └── advanced_ops.py└── utils/ ├── __init__.py └── logger.py

    文件内容示例:
    my_app/main.py:

    # my_app/main.pyfrom calculators import basic_ops, advanced_opsfrom utils.logger import log_messagedef run_app(): log_message(\"应用程序启动\") a, b = 10, 5 print(f\"{  a} + {  b} = {  basic_ops.add(a, b)}\") print(f\"{  a} - {  b} = {  basic_ops.subtract(a, b)}\") print(f\"{  a} * {  b} = {  advanced_ops.multiply(a, b)}\") print(f\"{  a} / {  b} = {  advanced_ops.divide(a, b)}\") log_message(\"应用程序结束\")if __name__ == \"__main__\": run_app()

    中文代码解释:

    • from calculators import basic_ops, advanced_ops: 从 calculators 包导入 basic_opsadvanced_ops 模块。
    • from utils.logger import log_message: 从 utils 包的 logger 模块导入 log_message 函数。
    • def run_app():: 定义主应用函数。
    • log_message(\"应用程序启动\"): 调用日志函数记录启动信息。
    • a, b = 10, 5: 定义两个数字。
    • 后续 print 语句调用导入模块中的函数进行计算并打印结果。
    • log_message(\"应用程序结束\"): 调用日志函数记录结束信息。
    • if __name__ == \"__main__\": run_app(): 如果作为主脚本运行,则执行 run_app

    my_app/calculators/__init__.py:

    # my_app/calculators/__init__.py# 这个文件使得 \'calculators\' 目录成为一个 Python 包print(\"Calculators包已加载\")

    中文代码解释:

    • # 这个文件使得 \'calculators\' 目录成为一个 Python 包: 注释说明 __init__.py 的作用。
    • print(\"Calculators包已加载\"): 包被导入时打印一条消息。

    my_app/calculators/basic_ops.py:

    # my_app/calculators/basic_ops.pydef add(x, y): return x + ydef subtract(x, y): return x - y

    中文代码解释:

    • def add(x, y): return x + y: 定义加法函数。
    • def subtract(x, y): return x - y: 定义减法函数。

    my_app/calculators/advanced_ops.py:

    # my_app/calculators/advanced_ops.pydef multiply(x, y): return x * ydef divide(x, y): if y == 0: raise ValueError(\"除数不能为零\") return x / y

    中文代码解释:

    • def multiply(x, y): return x * y: 定义乘法函数。
    • def divide(x, y): ...: 定义除法函数,包含对除数为零的检查。

    my_app/utils/__init__.py:

    # my_app/utils/__init__.py# 这个文件使得 \'utils\' 目录成为一个 Python 包print(\"Utils包已加载\")

    my_app/utils/logger.py:

    # my_app/utils/logger.pyimport datetimedef log_message(message): timestamp = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\") print(f\"[{  timestamp}] LOG: {  message}\")

    中文代码解释:

    • import datetime: 导入日期时间模块。
    • def log_message(message): ...: 定义日志函数,打印带时间戳的消息。

    现在,我们要加密整个 my_app 项目。假设 my_app 是你当前工作目录的子目录。你可以执行:

    pyarmor obfuscate --recursive my_app/main.py

    或者,更常见的是,如果你的入口脚本在包的根目录下,并且你想加密整个包,你可以这样做:

    pyarmor obfuscate --recursive --output dist/my_app_obfuscated my_app

    这里我们使用 -O--output 来指定输出目录,并将整个 my_app 目录作为输入。

    一个更简洁的方式,如果你的主入口脚本是 my_app/main.py,并且你想加密它以及它能通过 import 语句找到的所有在 my_app 目录下的其他 .py 文件:

    # 确保当前目录是 my_app 的父目录pyarmor obfuscate --recursive my_app/main.py

    这条命令会:

    1. my_app/main.py 作为入口点。
    2. 递归查找 my_app/main.py 所导入的、且位于 my_app 目录(或其子目录)下的所有 .py 文件,并将它们全部加密。
    3. 结果将输出到 dist/my_app/ 目录中(如果 main.pymy_app 子目录中,PyArmor 会尝试在 dist 下保持相似的结构)。

    让我们使用一个更清晰的命令,将整个 my_app 目录视为一个包进行加密,并指定输出目录:

    # 假设当前目录是 my_app 的父目录pyarmor obfuscate --recursive --output dist_my_app my_app

    中文命令解释:

    • pyarmor obfuscate: 执行加密。
    • --recursive (-r): 告诉 PyArmor 递归地处理输入路径下的 .py 文件。
    • --output dist_my_app (-O dist_my_app): 指定加密后的输出目录为 dist_my_app
    • my_app: 要加密的源目录/包。

    执行后,dist_my_app 目录的结构会是:

    dist_my_app/├── my_app/  # 保持了原始包的结构│ ├── main.py  # 加密后的 main.py│ ├── calculators/│ │ ├── __init__.py # 加密后的 __init__.py│ │ ├── basic_ops.py # 加密后的 basic_ops.py│ │ └── advanced_ops.py # 加密后的 advanced_ops.py│ └── utils/│ ├── __init__.py # 加密后的 __init__.py│ └── logger.py # 加密后的 logger.py├── _pytransform.so # (或 .dll/.dylib) 运行时库└── license.lic # (如果适用) 许可证文件

    注意,_pytransform 库和 license.lic (如果生成) 通常会放在输出目录的顶层,或者与主加密模块同级。PyArmor 的目标是让加密后的包可以直接被 Python 解释器使用。

  • 处理 __init__.py:
    __init__.py 文件在 Python 中用于标识一个目录为包(package)。PyArmor 会像处理其他 .py 文件一样处理 __init__.py 文件,对其进行加密。加密后的 __init__.py 仍然能够正确地初始化包,并允许 Python 导入该包或包中的模块。当 _pytransform 存在并正确加载后,导入包含加密 __init__.py 的包时,其引导代码会执行,然后包的初始化逻辑(如果 __init__.py 中有)会按预期运行。

  • 排除特定文件或目录 (--exclude):
    在加密整个项目时,你可能希望某些文件或子目录不被加密。例如,测试文件、文档、或者一些你确定不需要或不应该被加密的辅助脚本。PyArmor 提供了 --exclude 选项来实现这一点。

    假设在 my_app 中有一个 tests 子目录,我们不希望加密它:

    my_app/├── main.py├── calculators/│ └── ...├── utils/│ └── ...└── tests/ ├── test_basic_ops.py └── test_advanced_ops.py

    你可以这样加密,同时排除 tests 目录:

    pyarmor obfuscate --recursive --output dist_my_app --exclude tests my_app

    中文命令解释:

    • --exclude tests: 告诉 PyArmor 在递归加密 my_app 目录时,跳过名为 tests 的任何子目录。

    你也可以排除特定的文件:

    pyarmor obfuscate --recursive --output dist_my_app --exclude my_app/utils/logger.py my_app

    这将加密 my_app 中的所有其他 .py 文件,但 my_app/utils/logger.py 会被原样复制到输出目录(如果它在 --exact 模式下被包含)或者被忽略。通常,如果一个文件被 --exclude,PyArmor 在构建 dist 目录时可能不会包含它,除非你使用了其他选项(如 --manifest)来显式包含它。

    --exclude 的值可以是一个模式。例如,排除所有名为 test_*.py 的文件:

    pyarmor obfuscate --recursive --output dist_my_app --exclude \"*/test_*.py\" my_app

    (引号的使用可能取决于你的 shell 环境,以防止 shell 扩展通配符)

    重要: 被排除的文件将不会被 PyArmor 加密。如果这些未加密的文件被加密的模块导入,它们将作为普通 Python 模块运行。你需要确保这种混合模式符合你的安全需求。

  • 代码示例与行级解释 (运行加密后的包):
    加密完成后,dist_my_app 目录就包含了你的受保护应用。要运行它:

    1. 确保 _pytransform 动态库位于 Python 可以找到它的地方。通常,如果它和加密的 my_app 包在同一个顶层目录(即 dist_my_app),并且你从 dist_my_app 的父目录来运行,Python 的导入机制应该能处理好。
    2. 你需要调整你的 PYTHONPATH 或者你的执行方式,让 Python 解释器能够找到 dist_my_app/my_app 这个包。

    最简单的方式是,将 dist_my_app 目录视为你的项目根目录之一。
    假设你现在位于 dist_my_app 的父目录。你可以这样运行:

    # 确保 _pytransform.* 在 dist_my_app 目录下# 并且 my_app (加密版) 也在 dist_my_app 目录下PYTHONPATH=./dist_my_app python -m my_app.main

    中文命令解释:

    • PYTHONPATH=./dist_my_app: 临时将 dist_my_app 目录添加到 Python 模块搜索路径中。这样 Python 就能找到里面的 my_app 包和 _pytransform 库。
    • python -m my_app.main: 使用 -m 选项来运行 my_app 包中的 main 模块。Python 会在 PYTHONPATH 和标准库路径中查找 my_app.main

    或者,如果你的 dist_my_app 结构是:

    dist_my_app/├── main.py  # 入口脚本在顶层 (假设它导入 my_app_package)├── my_app_package/ # 包含所有加密模块│ └── ...└── _pytransform.so

    那么你可以进入 dist_my_app 并直接运行 python main.py

    回到我们之前的 dist_my_app/my_app/ 结构:

    dist_my_app/├── my_app/│ └── main.py│ └── ... (其他加密模块)└── _pytransform.so

    你可以:

    1. cd dist_my_app
    2. python my_app/main.py

    my_app/main.py (加密版) 启动时:

    • 它的引导代码会尝试加载同目录(或 sys.path 中)的 _pytransform.so
    • _pytransform 初始化。
    • main.py 执行 from calculators import basic_ops 时:
      • Python 解释器尝试在 my_app 包内(或其他搜索路径)找到 calculators 子包。
      • my_app/calculators/__init__.py (加密版) 被加载,其引导代码执行。
      • my_app/calculators/basic_ops.py (加密版) 被加载,其引导代码执行。
      • _pytransform 动态解密这些模块中的代码,使得 add, subtract 等函数可以被 main.py 调用。
    • 整个应用的逻辑按预期执行,但所有 .py 文件的实际内容都是受保护的。
    • 你会看到 Calculators包已加载Utils包已加载 的打印输出,以及计算结果和日志消息。

    通过 --recursive 选项,PyArmor 使得保护包含多个文件和子包的复杂 Python 项目变得相对简单和直接。

2.3 引入辅助文件和数据文件

Python 项目通常不仅仅包含 .py 脚本,还可能依赖于各种非 Python 文件,例如配置文件(JSON, YAML, INI)、数据文件(CSV, TXT, SQLite数据库)、模板文件(HTML, Jinja2)、图像、证书等等。当使用 PyArmor 加密并准备分发应用时,这些辅助文件也需要被正确地包含在最终的输出中。

  • --manifest 选项的使用:
    PyArmor 提供了 --manifest 选项,允许你指定一个模板字符串(类似于 setup.pyMANIFEST.in 的语法)或者一个 MANIFEST.in 格式的文件,来精确控制哪些文件或目录应该被复制到输出目录。这个功能与 Python 的 distutilssetuptools 中的 MANIFEST.in 文件非常相似。

    --manifest 选项的值可以是一个包含命令的字符串,用逗号分隔,或者是一个指向 MANIFEST.in 文件的路径。

    常用的 MANIFEST.in 命令:

    • include ...: 包含匹配模式的文件。
    • exclude ...: 排除匹配模式的文件(这会覆盖之前的 include)。
    • recursive-include ...: 递归地包含某个目录下匹配模式的文件。
    • recursive-exclude ...: 递归地排除某个目录下匹配模式的文件。
    • graft : 包含整个目录树。
    • prune : 排除整个目录树。
    • global-include : 全局包含。
    • global-exclude : 全局排除。

    模式可以使用通配符,如 * (匹配任何字符,除了路径分隔符) 和 ? (匹配单个字符)。

    使用示例:
    假设我们的项目 my_data_app/ 结构如下:

    my_data_app/├── app_core.py├── configs/│ ├── settings.json│ └── credentials.ini.template # 注意是 .template,我们可能不想直接包含它├── static/│ ├── images/│ │ └── logo.png│ └── styles.css├── templates/│ └── index.html└── data_files/ └── sample_data.csv

    我们希望加密 app_core.py,并包含 configs/settings.json, 整个 static 目录, 整个 templates 目录, 以及 data_files/sample_data.csv。我们不希望包含 credentials.ini.template

    方法一:直接在命令行中使用 --manifest 字符串:

    pyarmor obfuscate --recursive \\  --output dist_my_data_app \\  --manifest \"include configs/settings.json, graft static, graft templates, include data_files/sample_data.csv\" \\  my_data_app/app_core.py

    中文命令解释:

    • pyarmor obfuscate --recursive: 执行递归加密。
    • --output dist_my_data_app: 输出到 dist_my_data_app 目录。
    • --manifest \"...\": 指定清单模板字符串。
      • include configs/settings.json: 包含 my_data_app/configs/settings.json 文件。
      • graft static: 包含 my_data_app/static/ 目录及其所有内容。
      • graft templates: 包含 my_data_app/templates/ 目录及其所有内容。
      • include data_files/sample_data.csv: 包含 my_data_app/data_files/sample_data.csv 文件。
    • my_data_app/app_core.py: 作为加密入口的脚本 (PyArmor 会从这个脚本所在的目录 my_data_app 开始查找清单规则中指定的文件)。

    执行后,dist_my_data_app 目录结构会是:

    dist_my_data_app/├── app_core.py # 加密后的脚本├── configs/│ └── settings.json # 复制过来的 JSON 文件├── static/  # 复制过来的目录│ ├── images/│ │ └── logo.png│ └── styles.css├── templates/  # 复制过来的目录│ └── index.html├── data_files/ # 复制过来的目录│ └── sample_data.csv # 复制过来的 CSV 文件└── _pytransform.so # (或 .dll/.dylib)

    注意:configs/credentials.ini.template 没有被包含进来。

    方法二:使用 MANIFEST.in 文件:
    首先,在 my_data_app 目录下创建一个名为 MANIFEST.in 的文件:
    my_data_app/MANIFEST.in:

    include configs/settings.jsongraft staticgraft templatesinclude data_files/sample_data.csv# 可以添加注释# exclude configs/credentials.ini.template # 如果之前有更广泛的规则包含了它,可以用 exclude

    然后,在 my_data_app 的父目录下执行命令:

    pyarmor obfuscate --recursive \\  --output dist_my_data_app \\  --manifest my_data_app/MANIFEST.in \\  my_data_app/app_core.py

    或者,如果当前目录就是 my_data_app

    # 当前目录: my_data_app/pyarmor obfuscate --recursive \\  --output ../dist_my_data_app \\  --manifest MANIFEST.in \\  app_core.py

    效果与方法一相同。使用 MANIFEST.in 文件对于复杂的包含/排除规则更为清晰和易于管理。

    重要:

    • --manifest 规则中指定的路径是相对于被加密的包或脚本的根目录的。在上面的例子中,如果主脚本是 my_data_app/app_core.py,那么 configs/settings.json 指的就是 my_data_app/configs/settings.json
    • PyArmor 的 --manifest 主要是为了将这些非 Python 文件复制到输出目录,它本身不对这些数据文件进行加密或保护。如果你需要保护这些数据文件的内容,你需要采用其他加密手段,并在你的 Python 代码中实现解密逻辑。
    • --exact 选项:默认情况下,PyArmor 在递归加密时,只会加密 .py 文件,并把它们放到输出目录。如果想让 PyArmor 把源目录下的所有其他文件(非 .py 文件)也原样复制到输出目录的对应位置,可以使用 --exact 标志。例如,pyarmor obfuscate --recursive --exact --output dist_dir src_dir。此时,你可能仍然需要 --manifest 来进行更细致的控制,或者用 --exclude 来排除不需要的文件。--exact 适合于源目录结构和数据文件需要被完整保留到输出目录的场景。
  • 打包非 Python 文件:
    如上所述,--manifest 选项是 PyArmor 推荐的打包非 Python 文件的方式。它提供了足够的灵活性来包含所需的数据。

    思考:你的加密 Python 代码如何访问这些打包进来的数据文件?
    通常,你的 Python 代码会使用相对路径来访问这些文件。例如,在加密后的 app_core.py 中,如果它需要读取 configs/settings.json

    # 在 app_core.py (加密后,位于 dist_my_data_app/app_core.py)import jsonimport os# 获取当前脚本所在的目录# __file__ 在加密脚本中仍然指向其在文件系统中的路径current_script_dir = os.path.dirname(os.path.abspath(__file__))# 构建相对于当前脚本的配置文件路径# 假设 settings.json 与 app_core.py 在 dist 目录中处于相同的相对位置# dist_my_data_app/# |- app_core.py# |- configs/# | |- settings.json## 如果 app_core.py 在 dist_my_data_app/app_core.py# 那么 config_path 应该是 dist_my_data_app/configs/settings.json# 如果你的输出结构是平铺的,例如 app_core.py 和 configs 目录都在 dist_my_data_app 下:config_path = os.path.join(current_script_dir, \"configs\", \"settings.json\") # 这是不正确的,因为 current_script_dir 是 dist_my_data_app/ # 正确的应该是相对于项目的“根”# 更可靠的方式是,如果你的应用有一个已知的“基础目录”结构。# 假设 dist_my_data_app 是你的应用根目录。# 当你从 dist_my_data_app 的父目录运行,例如 python dist_my_data_app/app_core.py# 或者将 dist_my_data_app 加入 PYTHONPATH 并运行模块## 考虑脚本如何被执行。如果 app_core.py 是主入口,并且你期望数据文件相对于它:# 方案1: 假设数据文件相对于脚本所在目录的父目录(如果输出结构是 dist_root/script.py, dist_root/configs/settings.json)# base_dir = os.path.dirname(os.path.abspath(__file__)) # 脚本在 dist_root# config_path = os.path.join(base_dir, \"configs\", \"settings.json\")# 方案2: 如果脚本在子目录中,例如 dist_root/my_package/script.py, 数据文件在 dist_root/configs/settings.json# base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 上两级目录# config_path = os.path.join(base_dir, \"configs\", \"settings.json\")# 最好的做法通常是确定一个应用部署后的根目录。# 如果你使用 PyInstaller 等工具打包,它们有自己的机制来处理数据文件和在运行时定位它们。# 对于纯 PyArmor 分发,你需要设计好你的目录结构和访问逻辑。# 假设 dist_my_data_app 就是我们部署的根目录,而 app_core.py 就在这个根目录下:# dist_my_data_app/# app_core.py# configs/settings.json# _pytransform.so## 则在 app_core.py 中:app_root = os.path.dirname(os.path.abspath(__file__)) # 这就是 dist_my_data_app/config_path = os.path.join(app_root, \"configs\", \"settings.json\")try: with open(config_path, \'r\', encoding=\'utf-8\') as f: settings = json.load(f) print(\"成功加载配置:\", settings)except FileNotFoundError: print(f\"错误: 配置文件未找到于 {  config_path}\")except json.JSONDecodeError: print(f\"错误: 配置文件 {  config_path} 格式无效\")

    你需要确保你的 Python 代码中使用的路径能够正确定位到 dist 目录中这些被复制过来的文件。使用 os.path.abspath(__file__)os.path.dirname() 组合通常是定位与脚本相关的资源的好方法。

  • 代码示例与行级解释 (演示 --manifest):
    让我们创建一个包含数据文件的简单项目并用 PyArmor 处理它。

    项目结构 project_with_data/:

    project_with_data/├── run.py├── settings/│ └── app.conf└── assets/ └── message.txt

    project_with_data/run.py:

    # project_with_data/run.pyimport osdef load_config(): # 假设 run.py 和 settings/ 目录在同一级别 # 在加密后,这个相对关系应该保持在 dist 目录中 script_dir = os.path.dirname(os.path.abspath(__file__)) config_file = os.path.join(script_dir, \"settings\", \"app.conf\") print(f\"尝试加载配置文件: {  config_file}\") # 打印尝试加载的路径 try: with open(config_file, \'r\') as f: content = f.read().strip() print(f\"配置内容: \'{  content}\'\") return content except FileNotFoundError: print(\"错误: app.conf 未找到!\") return Nonedef load_message(): script_dir = os.path.dirname(os.path.abspath(__file__)) message_file = os.path.join(script_dir, \"assets\", \"message.txt\") print(f\"尝试加载消息文件: {  message_file}\") # 打印尝试加载的路径 try: with open(message_file, \'r\') as f: content = f.read().strip() print(f\"消息内容: \'{  content}\'\") return content except FileNotFoundError: print(\"错误: message.txt 未找到!\") return Noneif __name__ == \"__main__\": print(\"--- 应用启动 ---\") app_name = load_config() greeting = load_message() if app_name and greeting: print(f\"\\n欢迎使用 {  app_name}!\") print(f\"今日消息: {  greeting}\") else: print(\"\\n应用初始化失败,缺少必要文件。\") print(\"--- 应用结束 ---\")

    中文代码解释:

    • import os: 导入操作系统接口模块,用于路径操作。
    • def load_config():: 定义加载配置文件的函数。
    • script_dir = os.path.dirname(os.path.abspath(__file__)): 获取当前执行脚本 run.py 所在的绝对目录。
    • config_file = os.path.join(script_dir, \"settings\", \"app.conf\"): 构建配置文件的绝对路径,假设 settings/app.conf 相对于 run.py
    • with open(...): 打开并读取配置文件内容。
    • def load_message():: 类似地定义加载消息文件的函数。
    • message_file = os.path.join(script_dir, \"assets\", \"message.txt\"): 构建消息文件的绝对路径。
    • if __name__ == \"__main__\":: 主程序块。
    • 调用 load_config()load_message() 并打印结果。

    project_with_data/settings/app.conf:

    MyProtectedApp

    project_with_data/assets/message.txt:

    保护代码,从 PyArmor 开始!

    现在,我们使用 PyArmor 加密 run.py 并通过 --manifest 包含数据文件。
    project_with_data 的父目录下执行:

    pyarmor obfuscate --output dist_project_data \\  --manifest \"include settings/app.conf, include assets/message.txt\" \\  project_with_data/run.py

    中文命令解释:

    • pyarmor obfuscate: 执行加密。
    • --output dist_project_data: 输出到 dist_project_data 目录。
    • --manifest \"include settings/app.conf, include assets/message.txt\":
      • include settings/app.conf: 包含相对于 project_with_data/settings/app.conf
      • include assets/message.txt: 包含相对于 project_with_data/assets/message.txt
    • project_with_data/run.py: 要加密的主脚本。

    生成的 dist_project_data 目录结构:

    dist_project_data/├── run.py # 加密后的 run.py├── settings/│ └── app.conf # 复制过来的 app.conf├── assets/│ └── message.txt # 复制过来的 message.txt└── _pytransform.so # (或 .dll/.dylib)

    现在运行加密后的应用:

    cd dist_project_datapython run.py

    输出应该会是:

    --- 应用启动 ---尝试加载配置文件: /path/to/your/dist_project_data/settings/app.conf配置内容: \'MyProtectedApp\'尝试加载消息文件: /path/to/your/dist_project_data/assets/message.txt消息内容: \'保护代码,从 PyArmor 开始!\'欢迎使用 MyProtectedApp!今日消息: 保护代码,从 PyArmor 开始!--- 应用结束 ---

    (路径 /path/to/your/dist_project_data/ 会是你实际的路径)

    这个例子清晰地展示了如何使用 --manifest 将非 Python 文件包含到 PyArmor 的输出中,并如何在加密代码中通过相对路径访问它们。这是确保加密应用能够正确加载其所需资源的常用方法。

2.4 理解加密模式 (Obfuscation Mode)

PyArmor 提供了不同级别的代码混淆和加密策略,允许开发者在保护强度、性能影响和兼容性之间进行权衡。这些主要通过 --obf-mod (模块混淆级别) 和 --obf-code (代码对象混淆级别) 等选项来控制。还有一些更高级的模式如 BCC (Bytecode Code Coverage / C Compiler) 和 RFT (Runtime Function Tracing)。

  • --obf-mod (模块混淆级别 - Obfuscate Module Level):
    这个选项控制对模块级别结构(例如模块的 docstring、模块中定义的全局名称等)的混淆程度。

    • 0 (默认值): 不对模块的 __doc__ (文档字符串) 和 __file__ (指向原始 .py 文件路径的属性) 进行特殊处理。__file__ 属性在加密后会指向加密后的脚本文件。
    • 1: 将模块的 __doc__ 设置为 None。这可以移除模块的文档字符串,减小一点信息泄露。
    • 2: 除了将 __doc__ 设置为 None 外,还会尝试将模块的 __file__ 属性修改或隐藏,使其不直接暴露原始文件名或加密后的脚本路径(具体行为可能随 PyArmor 版本变化,通常会指向一个非特定或内部的标识)。这能增加一点逆向分析时追踪文件来源的难度。

    示例:
    假设有脚本 mod_test.py:

    # mod_test.py\"\"\"这是一个模块级别的文档字符串。\"\"\"GLOBAL_VAR = 100def module_func(): \"\"\"这是一个函数级别的文档字符串。\"\"\" print(\"模块函数被调用\")print(f\"模块名称: {  __name__}\")print(f\"模块文档: {  __doc__}\")print(f\"模块文件: {  __file__}\")print(f\"全局变量: {  GLOBAL_VAR}\")module_func()

    中文代码解释:

    • \"\"\"这是一个模块级别的文档字符串。\"\"\": 定义了模块的文档字符串。
    • GLOBAL_VAR = 100: 定义了一个全局变量。
    • def module_func(): ...: 定义了一个函数及其文档字符串。
    • 后续 print 语句打印模块的各种属性。

    加密并运行:

    1. 默认 (--obf-mod 0,通常不需显式指定):

      pyarmor obfuscate mod_test.pycd distpython mod_test.py

      输出可能类似 (具体 __file__ 路径会不同):

      模块名称: __main__模块文档: 这是一个模块级别的文档字符串。模块文件: /path/to/dist/mod_test.py全局变量: 100模块函数被调用
    2. 使用 --obf-mod 1:

      pyarmor obfuscate --obf-mod 1 mod_test.pycd distpython mod_test.py

      输出可能类似:

      模块名称: __main__模块文档: None模块文件: /path/to/dist/mod_test.py全局变量: 100模块函数被调用

      可以看到模块文档字符串变成了 None

    3. 使用 --obf-mod 2:

      pyarmor obfuscate --obf-mod 2 mod_test.pycd distpython mod_test.py

      输出可能类似 (具体 __file__ 值可能变化,有时可能是 或其他内部表示):

      模块名称: __main__模块文档: None模块文件:  # 或其他非直接路径的值全局变量: 100模块函数被调用

      模块文档为 None,并且 __file__ 的值可能不再是直接的文件系统路径。

    --obf-mod 主要用于减少元数据泄露,对核心代码逻辑的保护作用相对间接。

  • --obf-code (代码对象混淆级别 - Obfuscate Code Object Level):
    这个选项控制对 Python 代码对象(如函数体、类定义体、模块顶层代码)内部字节码的混淆和加密强度。这是 PyArmor 保护的核心。

    • 0: 禁用代码对象的加密。这通常只用于调试或特殊情况,因为它不提供实际的代码保护。生成的脚本仍然需要 _pytransform,但字节码是明文的(或接近明文)。不推荐用于生产环境。
    • 1 (默认值): 对每个代码对象的字节码进行加密。这是标准的保护级别,提供了良好的安全性和性能。
    • 2: 在级别 1 的基础上,对代码对象应用更强的混淆技术(例如,PyArmor 内部可能称为 F Suprema 或类似的增强混淆)。这会使字节码在解密后也更难通过静态分析理解,可能会稍微增加一点点运行时开销,但提供更高的安全性。

    示例:
    我们无法直接“看到”字节码的加密程度,但可以理解其概念。

    # func_test.pydef sensitive_calculation(a, b): \"\"\"这是一个包含一些计算逻辑的函数。\"\"\" x = (a * a + b * b) * 2 y = x // (a + b + 0.01) # 避免除以零 return yresult = sensitive_calculation(10, 5)print(f\"计算结果: {  result}\")print(f\"函数文档: {  sensitive_calculation.__doc__}\")

    中文代码解释:

    • def sensitive_calculation(a, b): ...: 定义一个进行一些计算的函数。
    • 内部执行了平方、相加、乘法和除法运算。
    • result = sensitive_calculation(10, 5): 调用函数。
    • print(f\"函数文档: {sensitive_calculation.__doc__}\"): 打印函数文档字符串。
    1. 默认 (--obf-code 1):

      pyarmor obfuscate func_test.pycd distpython func_test.py

      输出:

      计算结果: 166.0函数文档: 这是一个包含一些计算逻辑的函数。

      字节码被加密。函数文档字符串默认不受 obf-code 影响(它受 wrap 模式或之后提到的 --no-wrap 等影响)。

    2. 使用 --obf-code 2:

      pyarmor obfuscate --obf-code 2 func_test.pycd distpython func_test.py

      输出与级别 1 相同,但内部的字节码受到了更深层次的混淆和加密。

      计算结果: 166.0函数文档: 这是一个包含一些计算逻辑的函数。
    3. 使用 --obf-code 0 (不推荐):

      pyarmor obfuscate --obf-code 0 func_test.pycd distpython func_test.py

      输出仍然相同,但此时 func_test.py 中的 sensitive_calculation 函数的字节码未被加密。

    关于函数/类文档字符串和 obf-code:
    默认情况下(Wrap 模式,稍后讨论),PyArmor 会尝试保留函数和类的文档字符串,因为很多工具(如 help(), Sphinx 文档生成器)依赖它们。obf-code 主要针对的是可执行的字节码指令。如果想移除函数/类的文档字符串,通常需要结合其他选项或模式,例如在“非Wrap模式”下或者使用特定的插件(如果可用)。PyArmor 的一个重要特性是它尽量保持与原始 Python 代码的兼容性,包括对 inspect 模块等的支持,这有时意味着某些元数据需要被保留或模拟。

  • 不同模式的安全性与性能权衡:

    • 安全性 (Security):

      • --obf-code 0: 极低安全性 (几乎无保护)。
      • --obf-code 1: 良好安全性。这是大多数情况下的推荐级别,能有效防止常规反编译。
      • --obf-code 2: 更高安全性。提供了针对更高级分析手段的额外保护层。
      • --obf-mod 12: 轻微提升安全性,通过减少元数据泄露。
      • BCC 模式 (--enable-bcc): 将部分 Python 函数的字节码转换为一种加密的 C 函数表示形式,并在运行时通过 _pytransform 解释执行。这大大增加了逆向工程的难度,因为不再是标准的 Python 字节码。安全性非常高
      • RFT 模式 (--enable-rft): 运行时函数追踪,可能会改变函数调用方式和结构。安全性较高
    • 性能 (Performance):

      • --obf-code 0: 性能影响最小(但无保护)。
      • --obf-code 1: 对性能有轻微影响。主要是启动时 _pytransform 的初始化和首次解密字节码的开销。一旦代码被解密并执行过一次,后续调用相同代码块通常会更快(因为可能有缓存)。
      • --obf-code 2: 可能比级别 1 有稍微大一点的性能开销,因为混淆和解密过程更复杂。影响程度通常不大,但对于性能极度敏感的应用需要测试。
      • --obf-mod: 对运行时性能的影响基本可以忽略不计。
      • BCC 模式 (--enable-bcc): 通常会对性能产生较明显的负面影响,因为涉及到更复杂的运行时转换和解释。不适合性能瓶颈处的代码,除非保护需求极高。
      • RFT 模式 (--enable-rft): 也可能引入一些性能开销。
    • 兼容性 (Compatibility):

      • --obf-code 1--obf-mod (0, 1, 2) 通常具有最好的兼容性,几乎能与所有标准 Python 代码和库一起工作。
      • --obf-code 2 兼容性也很好。
      • BCC 模式 (--enable-bcc): 兼容性相对较低。并非所有 Python 函数都能完美转换为 BCC 模式(例如,依赖非常动态特性或复杂闭包的函数可能不行)。使用 BCC 模式时需要充分测试。
      • RFT 模式 (--enable-rft): 兼容性也需要注意,特别是对于依赖特定函数调用栈或元信息的代码。

    选择策略:

    1. 默认 (--obf-code 1, --obf-mod 0): 对于大多数应用,这是一个很好的起点,提供了良好的安全性和性能平衡。
    2. 增强保护: 如果需要更高的安全性,可以尝试 --obf-code 2。如果还不够,并且可以接受一定的性能损失和潜在的兼容性调试,可以考虑对核心敏感模块启用 BCC 模式。
    3. 性能优先: 如果加密对性能影响过大(不太常见,除非是 BCC/RFT),并且可以接受略低的安全性,理论上可以考虑 --obf-code 1。但通常不建议降低到 --obf-code 0
    4. 元数据: 使用 --obf-mod 12 来移除或修改模块级元数据,如果这对你很重要。

    PyArmor 还引入了 “Wrap 模式”“非 Wrap 模式” 的概念,这与函数/代码对象的封装方式有关,并影响 inspect 模块的行为以及文档字符串等的处理。

    • Wrap 模式 (默认): PyArmor 会尝试用一种特殊的方式封装加密后的代码对象,使得 inspect 模块等工具仍然可以(在一定程度上)获取到函数的签名、文档字符串等信息,从而提高与依赖这些自省功能的库的兼容性。
    • 非 Wrap 模式 (--no-wrap 或特定条件下的旧模式): 不使用这种封装。加密后的函数对于 inspect 来说可能看起来像一个普通的 C 函数或一个无法深入分析的对象。这可能提供略微高一点的“黑盒”程度,但会牺牲与某些库的兼容性。文档字符串也可能在这种模式下丢失。

    通常,保持默认的 Wrap 模式是推荐的,除非遇到特定的兼容性问题或有明确理由不使用它。

  • 代码示例与行级解释 (模式选择的影响):
    由于直接观察加密模式对字节码的影响比较困难,我们通过一个依赖 inspect 模块的例子来间接展示 Wrap 模式(默认)和 --obf-code 的行为。

    inspect_test.py:

    # inspect_test.pyimport inspectdef my_function(a: int, b: str = \"default\") -> bool: \"\"\" 这是一个用于测试的函数。 它接受一个整数和一个可选字符串,返回一个布尔值。 \"\"\" print(f\"参数 a: {  a}, 类型: {  type(a)}\") print(f\"参数 b: {  b}, 类型: {  type(b)}\") return len(b) > aprint(f\"--- 原始函数 ({  my_function.__name__}) ---\")print(f\"文档: {  inspect.getdoc(my_function)}\")print(f\"签名: {  inspect.signature(my_function)}\")print(f\"源码行号: {  inspect.getsourcelines(my_function)[1] if hasattr(inspect, \'getsourcelines\') else \'N/A\'}\") # getsourcelines 可能在加密后失效my_function(5, \"example string\")

    中文代码解释:

    • import inspect: 导入 inspect 模块,用于获取对象的元信息。
    • def my_function(a: int, b: str = \"default\") -> bool:: 定义一个带类型注解、默认参数和返回类型注解的函数。
    • \"\"\"...\"\"\": 函数的文档字符串。
    • print(f\"文档: {inspect.getdoc(my_function)}\"): 使用 inspect 获取并打印函数文档。
    • print(f\"签名: {inspect.signature(my_function)}\"): 获取并打印函数签名(参数、默认值、注解)。
    • inspect.getsourcelines(...)[1]: 尝试获取函数源代码的起始行号(这个在加密后几乎肯定会失败或返回不准确信息)。
    • my_function(5, \"example string\"): 调用函数。

    加密并运行 (默认模式,即 Wrap 模式, --obf-code 1):

    pyarmor obfuscate inspect_test.pycd distpython inspect_test.py

    输出可能如下 (具体行号信息会不同或报错):

    --- 原始函数 (my_function) ---文档: 这是一个用于测试的函数。它接受一个整数和一个可选字符串,返回一个布尔值。签名: (a: int, b: str = \'default\') -> bool源码行号: N/A # 或者抛出错误,或者返回不正确的值参数 a: 5, 类型: 参数 b: example string, 类型: 

    分析:

    • 文档字符串: 在默认的 Wrap 模式下被保留,inspect.getdoc 可以获取到。
    • 函数签名: inspect.signature 仍然可以正确解析出参数名、类型注解和默认值。这是 Wrap 模式的一个重要优点,保证了与很多依赖签名的库(如 FastAPI, Typer, Pydantic)的兼容性。
    • 源码行号: inspect.getsourcelines 通常无法工作,因为源码本身已被加密字节码替换。

    如果使用 --obf-code 2 (仍然是 Wrap 模式):

    pyarmor obfuscate --obf-code 2 inspect_test.py# 然后运行

    输出结果在 inspect 层面通常与 --obf-code 1 (Wrap 模式) 相同。字节码保护更强,但对外的接口元信息(通过 inspect 可见的)保持一致。

    如果 PyArmor 在某种情况下未使用 Wrap 模式 (例如使用 --no-wrap 选项,或者旧版本对于某些结构的默认行为):
    (假设有一个 --no-wrap 选项,具体请查阅你使用的 PyArmor 版本的文档,因为这个行为和选项名称可能演变)

    # 假设有 --no-wrap 选项# pyarmor obfuscate --no-wrap inspect_test.py# cd dist# python inspect_test.py

    在非 Wrap 模式下,输出可能会有显著不同:

    --- 原始函数 (my_function) ---文档: None # 或者其他非原始文档的值签名: # 可能无法获取签名,或得到一个非常通用的签名,如 (*args, **kwargs)源码行号: N/A参数 a: 5, 类型: 参数 b: example string, 类型: 

    分析 (非 Wrap 模式):

    • 文档字符串很可能丢失或变为 None
    • 函数签名可能无法被 inspect 正确解析,导致依赖此功能的库出错。

    因此,除非有特殊需求且充分了解其影响,一般推荐使用 PyArmor 的默认设置(即 Wrap 模式,--obf-code 1),它在保护代码的同时,最大限度地保持了与 Python 生态系统的兼容性。对于需要极致保护的特定函数,可以考虑 BCC 等更高级模式,但要接受其潜在的兼容性和性能代价。

    始终建议在选择了特定的加密模式和选项后,对你的应用程序进行全面的测试,以确保所有功能按预期工作,并且与所有依赖的第三方库兼容。

第三章:PyArmor 许可证管理:控制你的代码访问权限

PyArmor 不仅仅是一个代码加密工具,它还提供了一套强大而灵活的许可证管理系统。通过许可证,开发者可以精确控制谁、在什么条件下、以及多长时间内可以使用加密后的 Python 应用程序。这对于商业软件的分发、试用版本的控制以及订阅模式的实现至关重要。

3.1 许可证的核心概念

在深入学习如何生成和使用许可证之前,我们首先需要理解几个核心概念。

  • 3.1.1 什么是许可证文件 (license.lic)?
    许可证文件(通常命名为 license.lic)是 PyArmor 用来存储授权信息的一个特殊文件。它由 PyArmor 工具链根据开发者的指令生成,并由运行时的 _pytransform 动态库读取和校验。

    这个文件本质上是一个数据容器,其中以安全的方式(通常是加密或编码的)记录了以下类型的信息:

    • 许可证的唯一标识: 用于区分不同的许可证实例。
    • 有效期信息: 规定了许可证的起始和截止日期。如果当前日期超出了这个范围,许可证将失效。
    • 硬件绑定信息: 如果设置了硬件绑定,许可证文件中会包含目标机器的特定硬件特征码(例如硬盘序列号、MAC 地址的哈希值或加密表示)。_pytransform 在运行时会获取当前机器的硬件信息并与许可证中的记录进行比对。
    • 自定义绑定数据: 开发者可以指定任意字符串数据与许可证绑定,例如用户ID、客户名称、特定的功能开关等。
    • 许可证的签发者信息(间接): 许可证的有效性依赖于生成它的 pytransform.key。只有使用正确的密钥生成的许可证才能被对应的加密脚本识别。
    • 校验和或签名: 用于确保许可证文件本身没有被篡改或损坏。

    license.lic 文件通常是文本文件,但其内容对人类来说不是直接可读的,因为它经过了 PyArmor 的特殊处理以防止轻易解读和修改。直接编辑 license.lic 文件通常会导致许可证校验失败。

    当加密后的 Python 脚本启动时,_pytransform 库会自动查找并加载 license.lic 文件(除非许可证信息被直接嵌入到脚本中)。如果找不到有效的许可证,或者许可证校验失败(例如过期、硬件不匹配),脚本将无法正常运行,通常会抛出异常并终止。

  • 3.1.2 许可证的作用与重要性
    许可证机制在 PyArmor 中扮演着至关重要的角色,它为开发者提供了多种控制软件分发和使用的方式:

    1. 商业软件授权 (Commercial Software Licensing):
      对于销售的 Python 软件,开发者可以为每个购买的用户或每个激活的设备生成一个唯一的许可证。这确保了只有付费用户才能使用软件的完整功能。可以根据不同的购买套餐(如基础版、专业版、企业版)生成具有不同功能限制或使用期限的许可证。

    2. 试用版和演示版 (Trial and Demo Versions):
      开发者可以生成具有短期有效期的许可证,用于创建软件的试用版。例如,一个功能齐全但只能使用 30 天的试用许可证。试用期过后,用户需要购买正式许可证才能继续使用。

    3. 订阅模式 (Subscription Models):
      对于按月或按年订阅的软件服务,许可证可以设置相应的有效期。当订阅到期时,用户需要续订并获取新的许可证文件。

    4. 硬件绑定防止滥用 (Hardware Binding to Prevent Misuse):
      通过将许可证绑定到特定的硬件(如服务器的硬盘或特定用户的开发机),可以防止许可证被复制到多台未经授权的机器上使用。这对于限制软件安装实例数量非常有效。

    5. 功能模块控制 (Feature Control via Custom Data):
      通过将自定义数据绑定到许可证,可以实现更细粒度的功能控制。例如,许可证中可以包含一个标志,指示用户是否有权使用某个高级功能模块。加密的 Python 代码可以在运行时读取这些自定义数据,并据此启用或禁用相应功能。

    6. 按需授权与追踪 (On-demand Licensing and Tracking):
      结合自定义数据绑定(如客户ID),开发者可以追踪特定许可证的分发和使用情况(尽管 PyArmor 本身不直接提供追踪服务器,但许可证数据可以为此提供基础)。

    7. 保护知识产权的延伸:
      虽然代码加密本身保护了算法和实现细节,但许可证机制进一步确保了这些受保护的代码只能在授权的条件下运行,防止了即使代码本身难以逆向,但程序却被无限制使用的情况。

    没有许可证管理,即使代码被加密,一旦加密后的程序包泄露,就可能被无限制地复制和运行,这会严重损害开发者的利益。许可证为加密的“锁”增加了一把需要匹配的“钥匙”。

  • 3.1.3 默认许可证与自定义许可证
    PyArmor 在处理许可证时有两种基本情况:

    1. 默认许可证 (Default License):
      当你简单地使用 pyarmor obfuscate a_script.py 命令加密一个脚本时,如果没有指定任何特殊的许可证选项,PyArmor 通常会为加密脚本关联一个“默认许可证”。这个默认许可证的特性可能包括:

      • 无特定硬件绑定: 通常不绑定到任何特定硬件,可以在任何兼容的机器上运行。
      • 无特定有效期限制 (或有很长的有效期): 对于免费版或基础版的 PyArmor,这个默认许可证可能允许加密脚本永久运行。对于 PyArmor 的商业版生成的试用许可证,它可能会有一个默认的试用期限或者在运行时打印一些 PyArmor 的信息。
      • 包含 PyArmor 标记: 运行时,使用默认许可证的脚本可能会在控制台输出一些关于 PyArmor 的信息(例如 “Powered by PyArmor” 或版本信息),尤其是在非付费版本的 PyArmor 中。

      当 PyArmor 生成加密脚本时,它会在输出目录(如 dist/)下放置一个 license.lic 文件,这个就是默认的许可证文件。或者,在某些情况下(如使用 --bootstrap 2 或更高版本的 PyArmor),它可能会将这个默认许可证的信息直接嵌入到加密脚本的引导代码中,这样就不再需要一个外部的 license.lic 文件。

      适用场景:

      • 快速测试加密功能。
      • 分发开源或免费工具,但仍希望通过 PyArmor 的信息来声明其使用了该技术。
      • 在开发阶段,暂时不需要复杂的授权控制。
    2. 自定义许可证 (Custom License):
      自定义许可证是开发者通过 pyarmor licenses 命令明确生成的,具有特定限制条件的许可证。开发者可以完全控制自定义许可证的各项属性:

      • 有效期: 可以精确设置许可证的开始和结束日期。
      • 硬件绑定: 可以将许可证绑定到一个或多个硬件特征码。
      • 自定义数据: 可以嵌入任意对应用有意义的字符串数据。
      • 无 PyArmor 标记 (通常对于商业版): 使用商业版 PyArmor 生成的自定义许可证,在运行时通常不会显示 PyArmor 的水印信息。

      自定义许可证是实现商业化软件授权管理的核心。开发者需要使用 pyarmor licenses 命令,配合各种选项(如 --expired, --bind-disk, --bind-mac, --bind-data 等)来创建这些许可证。

      适用场景:

      • 商业软件分发。
      • 创建具有特定限制(如时间、机器)的试用版。
      • 实现基于订阅的授权。
      • 需要根据客户需求定制授权策略。

    关键区别总结:

    特性 默认许可证 (示例) 自定义许可证 生成方式 pyarmor obfuscate 自动生成 (或嵌入) pyarmor licenses 命令显式生成 有效期 无限制或长效 (可能随 PyArmor 版本和类型变化) 可由开发者精确指定 硬件绑定 通常无 可由开发者指定绑定一个或多个硬件特征 自定义数据 通常无 可由开发者指定任意字符串数据 PyArmor 标记 可能在运行时显示 (尤其非商业版 PyArmor) 通常不显示 (尤其商业版 PyArmor) 用途 测试、简单分发、开发 商业授权、试用版、订阅、精细化控制

    在实际项目中,特别是商业项目中,几乎总是需要使用自定义许可证来满足具体的授权需求。

3.2 生成许可证

PyArmor 提供了 pyarmor licenses 命令专门用于生成和管理许可证文件。这个命令有许多选项,允许开发者创建满足各种需求的许可证。

  • 3.2.1 pyarmor licenses 命令详解:
    pyarmor licenses 是创建自定义许可证文件的主要入口。其基本语法结构如下:

    pyarmor licenses [options] <registration_code_or_license_file_for_renewal>

    或者更常见的是直接生成新许可证:

    pyarmor licenses [options] <customer_code_or_output_name>

    核心概念:

    • registration_code (注册码) / customer_code: 这是一个用户定义的字符串,用于唯一标识这个许可证的“意图”或“目标用户/设备”。例如,它可以是客户的名称、订单号、设备ID等。PyArmor 会基于这个字符串以及其他选项来生成 license.lic 文件。输出的 license.lic