1.5. 定制和扩展
内容
Pyarmor 使用下面的方式进行定制和扩展
使用命令 pyarmor cfg 修改默认配置
使用 加密插件 对加密过程和输出文件进行扩展和定制
使用 脚本补丁 对运行时刻的加密脚本进行扩展和定制
1.5.1. 设置运行辅助包的名称
在 8.2 版本加入: 1
默认情况下运行辅助包的名称是 pyarmor_runtime_xxxxxx
这个名称可以被配置成为任何合法的包名称。例如设定名称为 my_runtime
:
pyarmor cfg package_name_format "my_runtime"
- 1
试用版本不可以修改运行辅助包的名称,修改后的加密脚本无法运行
1.5.2. 自定义需要保护的函数和模块
在 8.2 版本加入.
Pyarmor 8.2 新增加一个配置项 auto_mode
用来实现自定义需要保护的函数和模块,它的默认值为 and
,这时候过滤方式和以前的版本是一样的。 and
的含义是所有的操作对象除了是自动识别之外,还必须满足 includes
和 excludes
条件。
如果修改其值为 or
,则表示除了自动识别的函数和模块之外,还需要保护 includes
里面的函数。例如,下面的命令,,除了保护自动识别的函数之外,还额外保护函数 foo
和 koo
:
$ pyarmor cfg ast.call:auto_mode "or"
$ pyarmor cfg ast.call:includes "foo koo"
$ pyarmor gen --assert-call foo.py
下面的命令可以用来保护没有使用 import
语句直接导入的加密模块 joker.card
:
$ pyarmor cfg ast.import:auto_mode "or"
$ pyarmor cfg ast.import:includes "joker.card"
$ pyarmor gen --assert-import joker/
1.5.3. 使用加密插件修正运行辅助包的依赖项
在 8.2 版本加入.
在使用 Dawin 的设备中,如果 Python 没有安装在标准路径,那么运行加密脚本的时候可能会因为找不到依赖的 Python 动态库而出现装载错误。
如果需要运行加密脚本的环境在这种设备,那么在加密脚本的时候需要修正运行辅助包的依赖库位置。
首先查看一些运行辅助包中动态库 pyarmor_runtime.so
的依赖项:
$ otool -L dist/pyarmor_runtime_000000/pyarmor_runtime.so
dist/pyarmor_runtime_000000/pyarmor_runtime.so:
pyarmor_runtime.so (compatibility version 0.0.0, current version 1.0.0)
...
@rpath/lib/libpython3.9.dylib (compatibility version 3.9.0, current version 3.9.0)
...
如果 客户设备 上面没有 @rpath/lib/libpython3.9.dylib
,而是 @rpath/lib/libpython3.9.so
,那么加密脚本无法被装载。
这时候可以通过插件来修正这个问题,首先创建一个插件脚本 .pyarmor/conda.py
:
__all__ = ['CondaPlugin']
class CondaPlugin:
def _fixup(self, target):
from subprocess import check_call
check_call('install_name_tool -change @rpath/lib/libpython3.9.dylib @rpath/lib/libpython3.9.so %s' % target)
check_call('codesign -f -s - %s' % target)
@staticmethod
def post_runtime(ctx, source, target, platform):
if platform.startswith('darwin.'):
print('using install_name_tool to fix %s' % target)
self._fixup(target)
启用这个插件脚本,然后重新加密脚本:
$ pyarmor cfg plugins + "conda"
$ pyarmor gen foo.py
请根据具体的环境修改上面的插件脚本以满足需要。
参见
1.5.4. 使用脚本补丁绑定脚本到 Docker
在 8.2 版本加入.
假设我们要把脚本 app.py
绑定运行在两个 Docker 上面,它们的 id 分别是 docker-a1
, docker-b2
那么,首先创建一个 脚本补丁 .pyarmor/hooks/app.py
def _pyarmor_check_docker():
cid = None
with open("/proc/self/cgroup") as f:
for line in f:
if line.split(':', 2)[1] == 'name=systemd':
cid = line.strip().split('/')[-1]
break
docker_ids = __pyarmor__(0, None, b'keyinfo', 1).decode('utf-8')
if cid is None or cid not in docker_ids.split(','):
raise RuntimeError('license is not for this machine')
_pyarmor_check_docker()
然后加密脚本,同时把 Docker 的信息存储到 运行密钥 中:
$ pyarmor gen --bind-data "docker-a1,docker-b2" app.py
运行加密脚本以验证其效果,可以增加一些 print 语句在脚本补丁中进行调试。
1.5.5. 使用其他网络时间服务来检查脚本有效期
在 8.2 版本加入.
默认情况下 Pyarmor 是请求 NTP 服务器来验证加密脚本的有效期,如果 NTP 端口没有开放,也可以通过 脚本补丁 使用其他网络时间服务器来进行验证。
首先创建脚本补丁 .pyarmor/hooks/foo.py
def _pyarmor_check_worldtime(host, path):
from http.client import HTTPSConnection
expired = __pyarmor__(1, None, b'keyinfo', 1)
conn = HTTPSConnection(host)
conn.request("GET", path)
res = conn.getresponse()
if res.code == 200:
data = res.read()
s = data.find(b'"unixtime":')
n = data.find(b',', s)
current = int(data[s+11:n])
if current > expire:
raise RuntimeError('license is expired')
else:
raise RuntimeError('got network time failed')
_pyarmor_check_worldtime('worldtimeapi.org', '/api/timezone/Europe/Paris')
然后加密脚本,有效期的设置使用本地时间:
$ pyarmor gen -e .30 foo.py
这样就可以使用定制的代码检查网络时间。
1.5.6. 保护运行辅助模块
在 8.2 版本加入.
下面的例子说明如何检查运行辅助模块 pyarmor_runtime.so
的文件内容来确保其没有被修改
首先创建一个补丁脚本 .pyarmor/hooks/foo.py
:
1def check_pyarmor_runtime(value):
2 from pyarmor_runtime_000000 import pyarmor_runtime
3 with open(pyarmor_runtime.__file__, 'rb') as f:
4 if sum(bytearray(f.read())) != value:
5 raise RuntimeError('unexpected %s' % filename)
6
7check_pyarmor_runtime(EXCEPTED_VALUE)
第 7 行的 EXCEPTED_VALUE
需要被替换成为实际值,但是这里存在一个问题。每一次加密之后运行辅助模块 pyarmor_runtime.so
是不同的,所以必须在生成运行辅助模块的同时得到其文件字节总和。这个我们可以通过加密插件来实现,在生成辅助文件之后,自动计算字节总和,然后修改补丁脚本
# Plugin script: .pyarmor/myplugin.py
__all__ = ['RuntimePlugin', 'CondaPlugin']
class RuntimePlugin:
@staticmethod
def post_runtime(ctx, source, target, platform):
with open(target, 'rb') as f:
value = sum(bytearray(f.read()))
with open('.pyarmor/hooks/foo.py', 'r') as f:
source = f.read()
source = source.replace('EXPECTED_VALUE', str(value))
with open('.pyarmor/hooks/foo.py', 'r') as f:
f.write(source)
class CondaPlugin:
...
然后启用这个插件:
$ pyarmor cfg plugins + "myplugin"
最后生成加密脚本,并进行验证:
$ pyarmor gen foo.py
$ python dist/foo.py
这个例子只是演示如何去做,并不能在实际项目中使用。任何公开源码的检查方式一般都可以找到相应的方法绕过,所以请编写自己私有的检查脚本,这样才能真正的提高安全性。
参见
1.5.7. 在外部密钥中增加注释
在 8.2 版本加入.
加密脚本检查 外部密钥 文件的时候会忽略头部的任何可打印的字符,所以可以在外部密钥文件的开始增加注释,来对这个密钥进行备注说明。
Pyarmor 提供了加密插件可以用来对外部密钥添加注释,下面这个例子会把所有的绑定信息打印到屏幕,并且把有效期写入到外部密钥中:
# Plugin script: .pyarmor/myplugin.py
from datetime import datetime
__all__ = ['CommentPlugin']
class CommentPlugin:
@staticmethod
def post_key(ctx, keyfile, **keyinfo):
expired = None
for name, value in keyinfo.items():
print(name, value)
if name == 'expired':
expired = datetime.fromtimestamp(value).isoformat()
if expired:
print('patching runtime key')
comment = '# expired date: %s\n' % expired
with open(keyfile, 'rb') as f:
keydata = f.read()
with open(keyfile, 'wb') as f:
f.write(comment.encode())
f.write(keydata)
启用这个插件,然后生成一个外部密钥:
$ pyarmor cfg plugins + "myplugin"
$ pyarmor gen key -e 2023-05-06
查看外部密钥中的注释:
$ head -n 1 dist/pyarmor.rkey
参见