3.3. 生成加密脚本的环境
这里列出了和命令 pyarmor 相关的所有一切。
首先命令 pyarmor 运行在 开发机器 上面,使用 supported Python versions 运行在这些 supported platforms
命令行选项, 配置文件选项 , 加密插件 , 脚本补丁 和一些环境变量影响和改变着命令 pyarmor 的行为。
所有的命令行选项和相关的环境变量在 命令手册 中有详细描述。
3.3.1. 支持的 Python 版本
Python |
2.7 |
3.0~3.6 |
3.7~3.10 |
3.11 |
3.12 |
3.13 |
备注 |
---|---|---|---|---|---|---|---|
RFT 模式 |
No |
No |
Y |
Y |
Y |
Y |
|
BCC 模式 |
No |
No |
Y |
Y |
Y |
Y |
|
pyarmor 8 基础功能 |
No |
No |
Y |
Y |
Y |
Y |
|
pyarmor-7 |
Y |
Y |
Y |
No |
No |
No |
3.3.2. 支持的平台
OS |
Windows |
Apple 2 |
Linux 3 |
||||
---|---|---|---|---|---|---|---|
Arch |
x86/x86_64 |
x86_64 |
arm64 |
x86/x86_64 |
aarch64 |
armv7 |
armv6 |
Themida 保护 |
Y |
No |
No |
No |
No |
No |
No |
RFT 模式 |
Y |
Y |
Y |
Y |
Y |
Y |
No |
BCC 模式 |
Y |
Y |
Y |
Y |
Y |
N/y |
No |
pyarmor 8 基础功能 |
Y |
Y |
Y |
Y |
Y |
Y |
No |
pyarmor-7 4 |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
OS |
FreeBSD |
Alpine Linux |
Android |
||||
---|---|---|---|---|---|---|---|
Arch |
x86_64 |
x86_64 |
aarch64 |
x86/x86_64 |
aarch64 |
armv7 |
armv6 |
RFT 模式 |
Y |
Y |
Y |
Y |
Y |
Y |
No |
BCC 模式 |
Y |
Y |
Y |
Y |
Y |
Y |
No |
pyarmor 8 基础功能 |
Y |
Y |
Y |
Y |
Y |
Y |
No |
pyarmor-7 |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
OS |
Linux |
Linux, Alpine Linux |
---|---|---|
Arch |
loongarch64 |
ppc64le, mips32el/64el, riscv64 7 |
RFT 模式 |
Y |
Y |
BCC 模式 |
N |
N |
JIT 功能 |
N |
Y |
pyarmor 8 基础功能 |
Y |
Y |
注释
- 1
N/y
意味着现在还不支持,但是将来会支持- 2
Apple Silcon 仅支持 Python 3.9+
- 3
这里的 Linux 使用的是 glibc
- 4
pyarmor-7 支持更多的平台,参考 Pyarmor 7.x 文档.
- 5
下表中平台是在 Pyarmor 8.3 新增加的
- 6
这些框架在 Pyarmor 8.5.9 中新增加的并且未经测试,预编译的扩展模块分别存放在包 pyarmor.cli.core.linux and pyarmor.cli.core.alpine
- 7
框架 riscv64 仅支持 Python 3.10+
重要
pyarmor-7 是 Pyarmor 7.x 的问题修正版本,只支持老版本的许可证。在新版本的许可证下面使用可能会报错 HTTP 401 error
3.3.3. 配置文件选项
有三种类型的配置文件
使用命令 pyarmor cfg 来查看和设置配置文件选项。
3.3.4. 加密插件
在 8.2 版本加入.
加密插件是一个 Python 脚本,在生成输出文件之后被调用,可以对输出文件进行一些修改和调整,也可以做任何需要的事情。
加密插件常用的场景:
对输出目录的文件进行额外处理
修改加密脚本中导入运行辅助包的语句,以满足特殊的导入需求
增加注释信息到 外部密钥 文件,这样通过注释使用外部脚本就可以读取运行配置信息
修改运行辅助包中扩展模块
pyarmor_runtime
的后缀,以避免名称冲突使用 install_name_tool 修改扩展模块
pyarmor_runtime
的依赖包,解决有些 Darwin 环境无法装载扩展模块的问题
一个加密插件的脚本可以定义一个或者多个插件类,同时必须定义属性 __all__
来输出脚本中定义的插件类,没有输出的插件类不起作用
插件类的定义如下
- class PluginName
- static post_script(ctx, res, source)
如果该方法存在,那么它会在加密脚本生成之后被调用
这个方法主要用来对生成的加密脚本进行修改定制,通过修改 source 并返回修改后的脚本内容
- 参数
ctx (Context) -- 加密环境
res (FileResource) -- 实例 pyarmor.cli.resource.FileResource
source (str) -- 加密后的脚本内容
- static post_build(ctx, inputs, outputs, pack=None)
如果该方法存在,那么当所有加密文件都生成之后会被 pyarmor gen 调用
- 参数
ctx (Context) -- 加密环境
inputs (list) -- 所有命令行输入的脚本和包名称
outputs (list) -- 输出目录
pack (str) -- 要么是 None,要么是选项
--pack
指定的文件名称
- static post_key(ctx, keyfile, **keyinfo)
如果该方法存在,那么当 外部密钥 文件生成之后会被 pyarmor gen key 调用
- 参数
ctx (Context) -- 加密环境
keyfile (str) -- 输出的外部密钥文件
keyinfo (dict) -- 外部密钥绑定的信息
参数
keyinfo
中可能的项目有- Key expired
None 或者有效期(epoch)
- Key devices
None 或者设备信息列表
- Key data
None 或者是绑定的私有数据(Bytes)
- Key period
None 或者是定时检查运行密钥的周期(秒)
- static post_runtime(ctx, source, dest, platform)
如果该方法存在,那么当每一个运行平台的扩展模块
pyarmor_runtime
生成之后会被 pyarmor gen 调用,如果是生成多平台的脚本,它可能被调用多次。- 参数
ctx (Context) -- 加密环境
source (str) -- 预编译的扩展模块文件名称
dest (str) -- 输出的扩展模块文件名称
platform (str) -- 扩展模块对应的 运行平台 名称
为了启用插件脚本,需要进行配置。配置的时候不需要输入扩展名 .py
,直接使用脚本名称。例如:
$ pyarmor cfg plugins + "script name"
Pyarmor 按照顺序依次搜索同名的脚本:
这里有一个示例插件脚本 fooplugin.py
__all__ = ['EchoPlugin']
class EchoPlugin:
@staticmethod
def post_runtime(ctx, source, dest, platform):
print('-------- test fooplugin ----------')
print('ctx is', ctx)
print('source is', source)
print('dest is', dest)
print('platform is', platform)
把它保存到 .pyarmor/fooplugin.py
并启用它:
$ pyarmor cfg plugins + "fooplugin"
加密一个脚本,在控制台可以看到插件运行的输出信息:
$ pyarmor gen foo.py
禁用插件使用下面的方式:
$ pyarmor cfg plugins - "fooplugin"
3.3.5. 脚本补丁
在 8.2 版本加入.
脚本补丁也是一个 Python 脚本,在加密的时候被嵌入到脚本的最前面,相当于直接在脚本的前面插入了插件源代码,这样加密脚本运行的时候就会首先执行插件代码。
当加密脚本的时候,Pyarmor 会在 本地配置 和 全局配置 目录下面查看有没有路径 hooks
,如果有的话,那么会查看有没有和加密脚本同名的文件,有的话就会被插入到加密脚本中。
例如, .pyarmor/hooks/foo.py
就是 foo.py
的补丁,而 .pyarmor/hooks/joker.card.py
是 joker/card.py
的补丁。
脚本补丁就是一个普通的 Python 脚本,但是它可以用两个特殊的内置函数 __pyarmor__()
and __assert_armored__()
来做一些加密脚本特有的事情。
需要注意的是补丁是直接插入到脚本的模块级别的代码中,所以要避免名称冲突,影响了原来脚本的执行。
参见
__pyarmor__()
__assert_armorred__()
3.3.5.1. 特殊脚本补丁
在 8.3 版本加入.
一般的脚本补丁是嵌入到了加密脚本中,如果需要在运行加密脚本之前就进行一些定制或者额外的检查,那么就需要使用到特殊的脚本补丁 .pyarmor/hooks/pyarmor_runtime.py
,这个脚本补丁可以定义在加密脚本执行之前就被调用的函数。
首先创建脚本 .pyarmor/hooks/pyarmor_runtime.py
,然后定义一个函数 bootstrap()
,这个函数在扩展模块 pyarmor_runtime 初始化的过程中被调用,其他代码都会被忽略。
- bootstrap(user_data)
- 参数
user_data (bytes) -- 运行密钥里面的用户自定义数据
- 返回
如果返回 False ,那么扩展模块 pyarmor_runtime 初始化失败,并且抛出保护异常 返回其他任何值,继续执行加密脚本
- 抛出
SystemExit -- 直接退出,不显示调用堆栈
ohter Exception -- 退出并且显示调用堆栈
An example script:
def bootstrap(user_data):
# 必须在函数内容导入需要的名称,不要在模块级别导入
import sys
import time
from struct import calcsize
print('user data is', user_data)
# 检查平台,不支持 32 位
if sys.platform == 'win32' and calcsize('P'.encode()) * 8 == 32:
raise SystemExit('no support for 32-bit windows')
# 在 Windows 平台下面检查是否有调试器存在
if sys.platform == 'win32':
from ctypes import windll
if windll.kernel32.IsDebuggerPresent():
print('found debugger')
return False
# 在这个例子中,传入的自定义数据是时间戳
if time.time() > int(user_data.decode()):
return False
验证一下这个脚本,首先拷贝这个脚本到 .pyarmor/hooks/pyarmor_runtime.py
,然后执行下面的命令:
$ pyarmor gen --bind-data 12345 foo.py
$ python dist/foo.py
user data is b'12345'
Traceback (most recent call last):
File "dist/foo.py", line 2, in <module>
...
RuntimeError: unauthorized use of script (1:10325)
3.4. 运行加密脚本的环境
加密脚本运行在 客户设备 上面
3.4.1. 支持的 Python 版本和平台
运行加密脚本支持的平台和 Python 版本和 生成加密脚本的环境 是一样的。
3.4.2. 环境变量
这里的环境变量会被加密脚本使用来决定一些运行设置
- LANG
操作系统环境变量,加密脚本读取它来决定运行时刻的语言设置
3.4.3. 第三方解释器的支持
对于第三方的解释器(例如 Jython 等)以及通过嵌入 Python C/C++ 代码调用加密脚本,只要第三方解释器能够和 CPython 扩展模块 兼容,就可以使用加密脚本。查看第三方解释器的文档,确认它是否支持 CPython 的扩展模块,
已知的一些问题
PyPy 无法运行加密脚本,因为它完全不同于 CPython 。
在 Linux 下面 装载 Python 动态库 libpythonXY.so 的时候 dlopen 必须设置 RTLD_GLOBAL ,否则加密脚本无法运行。
Boost::python,默认装载 Python 动态库是没有设置 RTLD_GLOAL 的,运行加密脚本的时候会报错 "No PyCode_Type found" 。解决方法就是在初始化的调用方法 sys.setdlopenflags(os.RTLD_GLOBAL) ,这样就可以共享动态库输出的函数和变量。
模块 ctypes 必须存在并且 ctypes.pythonapi._handle 必须被设置为 Python 动态库的句柄,PyArmor 会通过该句柄获取 Python C API 的地址。
WASM 目前不支持,因为这需要把运行库的代码也编译成为 WASM,但是 WASM 是很容易就被反编译成为原来的 C 代码,为了安全性,所以目前没有支持 WASM 的计划。如果有更多的用户提出这个需求,会考虑实现一个轻量级的运行库,只支持能够运行 RFT 模式的加密脚本,但是目前还没有开发计划。
3.4.4. 加密脚本内置函数
在 8.2 版本加入.
有两个特殊的函数无需导入,可以直接在加密脚本中使用,通常它们一般用在 脚本补丁 中。
- __pyarmor__(arg, kwarg, name, flag)
- 参数
name (bytes) -- 必须是
b'hdinfo'
或者b'keyinfo'
flag (int) -- 必须是
1
查询当前设备硬件信息
当
name
等于b'hdinfo'
时候,调用这个函数可以获取当前设备硬件信息- 参数
arg (int) -- 获取不同类型的设备信息,有效值:0,1,2,4
kwarg (str) -- None,或者设备名称
- 返回
arg 为 0 返回硬盘序列号
- 返回
arg 为 1 返回网卡以太网地址
- 返回
arg 为 2 返回IPv4 地址
- 返回
arg 为 3 无效值
- 返回
arg 为 4 返回当前域名
- 返回类型
字符串
例如,
__pyarmor__(0, None, b'hdinfo', 1) __pyarmor__(1, None, b'hdinfo', 1)
在 Linux 系统,
kwarg
可以用来指定网卡或者硬盘的设备名称,例如:__pyarmor__(0, "/dev/vda2", b'hdinfo', 1) __pyarmor__(1, "eth2", b'hdinfo', 1)
在 Windows 系统,
kwarg
还可以用来得到全部网卡或者全部硬盘的信息。例如:__pyarmor__(0, "/0", b'hdinfo', 1) # First disk __pyarmor__(0, "/1", b'hdinfo', 1) # Second disk __pyarmor__(1, "*", b'hdinfo', 1) __pyarmor__(1, "*", b'hdinfo', 1)
查询运行密钥中的数据和有效期
当
name
等于b'keyinfo'
的时候,调用这个函数可以获取运行密钥中信息- 参数
arg (int) -- 指定获取的信息,有效值:0,1
kwarg -- None,暂时没有使用
- 返回
arg 为 0 返回运行密钥绑定的私有数据,没有绑定的数据返回
b''
- 返回类型
Bytes
- 返回
arg 为 1 返回运行密钥的有效期 (epoch),如果没有设置有效期返回 -1
- 返回类型
Long
- 返回
如果发生错误,那么返回 None
例如,
print('bind data is', __pyarmor__(0, None, b'keyinfo', 1)) print('expired epoch is' __pyarmor__(1, None, b'keyinfo', 1))
- __assert_armored__(arg)
- 参数
arg (object) -- 模块,函数或者方法
- 返回
如果
arg
指定的对象是被加密过的,那么返回arg
自身,否则抛出保护异常
例如
m = __import__('abc') __assert_armored__(m) def hello(msg): print(msg) __assert_armored__(hello) hello('abc')