4.1. 加密过程详解

本文档适用于下列用户

  • 默认选项无法满足加密脚本的性能或者安全方面的需求

  • 加密脚本出现问题不知道错在那里

阅读本文档需要一定的 Python 基础,了解 Shell 脚本,环境变量等相关知识。

一般用户在操作系统中都一个用户目录,默认情况 pyarmor 运行过程的配置和数据文件会存放在这里。在命令行使用下面的命令查看配置目录的位置:

$ echo $HOME/.pyarmor

或者在命令行输入 python 命令,打开 Python 控制台,运行下面的语句:

>>> import os
>>> print(os.path.expanduser("~/.pyarmor"))

备注

在本文档的示例代码中,以 $ 开头的命令都是 Shell 命令,而以 >>> 开头的都是在 Python 控制台运行的脚本语句。

在本文档中,后面会使用 ~/.pyarmor/ 表示这个目录。

我们首先看下面是一个最简单的加密命令:

$ pyarmor gen foo.py

运行完成之后,加密脚本存放在目录 dist/ 下面,输出的文件:

dist/
    foo.py
    pyarmor_runtime.so

在这个过程中,pyarmor 都做了什么呢?

4.1.1. 设置运行环境

pyarmor 首先设置运行环境,主要的步骤如下

  1. 创建运行环境 ctx,根据全局配置文件进行初始化

  2. 如果当前目录存在 .pyarmor/config 文件,使用其中的选项修改 ctx

  3. 读取命令行选项,修改运行环境 ctx

4.1.2. 搜索脚本

接下来 pyarmor 根据命令行传入的文件或者目录,开始搜索需要加密的所有脚本。

配置项 pyexts 列出了需要加密的脚本扩展名,默认是 [.py, .pyw]

首先,命令行输入的脚本,即便扩展名不在 pyexts 之中,也会被增加到需要加密的脚本列表 ctx.resources

其次,命令行输入的目录下面,所有扩展名在 pyexts 之中的会被增加到列表

然后根据下面的两个配置项,决定下一步的搜索范围

  • 选项 --recursive 递归搜索所有的子目录

  • 选项 --find-all 查找所有依赖的模块和包(尚未实现)

4.1.3. 依赖脚本查找

这个功能尚未实现

--find-all 选项设定的时候,会自动查找加密脚本的依赖模块和包。

pyarmor 首先设定搜索路径

  • 继承目前 Python 系统设置的搜索路径

  • 把配置项 pypaths 中指定的路径增加到搜索列表

  • 把当前目录增加到搜索列表

然后使用下面的算法查找脚本 foo 依赖模块:

  1. 把脚本 foo.py 解析生成语法树 tree

  2. 搜索树中所有的 import/from...import 语句

  3. 把其中导入的模块作为依赖项

  4. 根据模块名称和搜索路径,查找对应的模块文件

  5. 把模块文件加入到需要加密的脚本列表

有一些隐含导入的模块无法通过上面的算法发现,这时候会使用脚本的规则文件来辅助查找。脚本的规则文件一般是人工创建,把依赖文件或者包名称添加到组 finder 下面,例如:

[finder]
includes = a.py b/c d.pt

脚本规则文件的命名和存放路径必须符合下面的规则,否则无法起作用:

  • ~/.pyarmor/config/foo.rules 存放在全局配置目录 rules 下面的同名文件

  • .pyarmor/foo.rules 当前配置目录下的扩展名为 .rules 的同名文件

如果规则文件存在,那么这里面指定的模块和文件都会被进行加密处理,并且如果指定了具体的文件名称,即便扩展名不在配置项 .pyexts 也会被增加到加密列表中。

4.1.4. 源代码处理

找到所有需要处理的脚本之后,下一步就是依次对每一个脚本的源代码进行处理

源代码的处理算法主要步骤

  • 读取文件内容,生成资源文件 res

  • 遍历环境配置中所有源代码处理器,依次处理资源文件

  • 处理完成之后把源代码编译成为语法树

文件的编码使用的是系统默认值,如果读取文件出现编码错误,修改配置文件的选项 encoding ,设定正确的值。

4.1.4.1. 内联注释处理器

内置的源代码处理器是内联注释处理器,默认启用的。

内联注释处理器只是简单的把源代码中每一行包含的 # pyarmor: 字符串替换为空,这样可以使得原来被注释的代码生效。例如,原来的脚本如下

# pyarmor: print('inline pyarmor code')
def foo():
    i = 2
    # pyarmor: i += 1
    print('i is', i)

处理完之后,代码的内容就变成

print('inline pyarmor code')
def foo():
    i = 2
    i += 1
    print('i is', i)

这个的设计初衷是有时候需要执行一些只能在加密脚本中才能运行的代码,这样在没有加密的时候,调试起来很不方便。所以先把这部分的代码注释掉,在加密之前把注释自动去掉。

4.1.5. 语法树处理

源代码处理完成之后,输出的是源代码对应的 ast.Module 语法树。

语法树的处理算法主要步骤

  • 根据环境配置,找到所有的语法树处理器

  • 遍历所有语法树处理器,依次处理资源文件

  • 最后输出的是经过各个处理器修改后的语法树

为了节省内存,源代码处理完成,文件内容会被释放。如果语法树处理器需要从文件的源代码读取信息,需要重新打开文件读取。

每一个语法处理器接受的输入是上一个语法处理器的输出,所以后面的语法处理器处理的语法树可能和最初的源代码并不一致。

内置的语法树处理器主要有二个

  • 函数调用处理器,通过修改 ast.Call,确保调用的函数是经过加密的

  • 导入模块处理器,通过修改 ast.Import,确保导入的模块是经过加密的

4.1.6. 模块代码处理

所有的语法树处理器完成之后,输出的修改后的 ast.Module 语法树。接下来就是对模块代码进行处理:

* 根据环境配置,找到所有的模块代码处理器
* 编译语法树为模块代码对象 mco
* 遍历所有模块代码处理器,依次处理资源文件的模块代码对象 mco
* 最后输出的是经过各个处理器修改后的模块代码对象

每一个模块代码处理器接受的输入是上一个模块代码处理器的输出,所以除了第一个代码处理器之外,后面的处理器输入的 mco 可能和语法树 mtree 不一致。

内置的模块代码处理器有二个

  • 重构代码处理器,主要实现自动重命名模块中类,函数,属性和变量的名称

  • 加密代码处理器,主要是对代码对象进行加密处理

4.1.7. 加密脚本生成

下列相关选项影响加密脚本中导入 运行辅助包 的方式

4.1.7.1. 同名脚本的输出路径

如果存在同名脚本,例如:

pyarmor gen a/foo.py b/foo.py

默认输出分别是:

  • a/foo.py -> dist/foo.py

  • b/foo.py -> dist-1/foo.py

4.1.7.2. 运行辅助文件生成

选项 --enable-themida--platform 决定了运行辅助文件中需要那些动态库。