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 的含义是所有的操作对象除了是自动识别之外,还必须满足 includesexcludes 条件。

如果修改其值为 or ,则表示除了自动识别的函数和模块之外,还需要保护 includes 里面的函数。例如,下面的命令,,除了保护自动识别的函数之外,还额外保护函数 fookoo:

$ 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-a1docker-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

参见

加密插件