利用cython实现python项目代码加密

利用cython实现python项目代码加密

仅用于加密,无法加速项目

# -*- coding: utf-8 -*-
"""
@File    : encrypt.py
@Time    : 2024/05/07 11:43:09
@Author  : WHY
@Version : 1.0
@Desc    : python项目代码加密脚本
"""

from __future__ import annotations

import ast
import os
import shutil
import sys
from pathlib import Path
from time import sleep


class RemoveAnnotationsTransformer(ast.NodeTransformer):

    def visit_FunctionDef(self, node):
        # 移除函数返回值的类型注解
        node.returns = None

        # 移除函数参数的类型注解
        if node.args.args:
            for arg in node.args.args:
                arg.annotation = None

        # 继续遍历函数体内的语句
        return self.generic_visit(node)

    def visit_AnnAssign(self, node):
        # 如果是纯类型注解语句(没有赋值),则移除
        if node.value is None:
            return None
        else:
            # 创建新的Assign节点,并设置正确的行号信息
            new_node = ast.Assign(targets=[node.target], value=node.value)
            # 从原AnnAssign节点复制行号信息
            new_node.lineno = node.lineno
            new_node.col_offset = node.col_offset
            # 返回新的Assign节点,effectively移除了类型注解但保留了赋值
            return new_node


class Encrypt():
    '''
    利用cython实现python项目代码加密,保留`'main.py','manage.py'`为入口文件
    '''

    # setup.py文件的源码格式
    setup_fmt= \
        '''
from distutils.core import setup
from Cython.Build import cythonize

if __name__ == '__main__':
    setup(ext_modules = cythonize('{pyfile_name}',compiler_directives={{'language_level': 3}}))
        '''

    def __init__(
        self,
        src_path: Path | str,
        dst_path: Path | str = None,
        interpreter_path: Path | str = None,
        pylist: list[str | Path] = None,
    ) -> None:
        """
        ~:利用cython实现python项目代码加密,保留`'main.py','manage.py'`为入口文件

        Parameters
        ----------
        src_path: Path | str, 项目源文件路径
        dst_path: Path | str = None, 目的路径,默认为项目根目录同级路径下 `${项目文件夹名称}.encrypt`
        interpreter_path: Path | str = None, 项目所用python解释器路径,默认为当前解释器
        pylist: list[str | Path] = None, 指定项目内要加密的文件,只能使用相对于项目文件夹的相对路径, \
            默认不指定,即加密整个项目
        """
        # 源码路径
        self.src_path = Path(src_path).absolute()
        # 目的路径,默认为源码根目录下 ${源码目录名}.encrypt
        if dst_path is None:
            self.dst_path = Path(self.src_path.parent /
                                 (self.src_path.name + '.encrypt')).absolute()
        else:
            self.dst_path = Path(dst_path).absolute()
        # 目的路径不能与源路径相同
        if self.dst_path.resolve() == self.src_path.resolve():
            raise Exception('为了防止源码被加密后丢失,目的路径不能与源路径相同!')
        # 源码所用python解释器路径,默认为当前解释器
        if interpreter_path is None:
            self.interpreter_path = Path(sys.executable).absolute()
        else:
            self.interpreter_path = Path(interpreter_path).absolute()
        # 不加密的文件列表
        self.exclude = ['__init__.py', 'setup.py', 'main.py', 'manage.py']
        # python文件列表
        self.pylist: list[Path] = []
        # __init__文件列表,存在__init__.py文件时无法使用cython加密
        self.init_file_list: list[Path] = []
        if pylist is None:
            self.not_specified_pylist()
        else:
            self.specified_pylist(pylist)
        # 开始加密
        self.start_encrypt()

    @staticmethod
    def remove_annotation(path_src: Path, path_dst: Path):
        # 读取python源码
        code_str = path_src.read_text(encoding='utf8')
        # 解析代码字符串,得到AST
        tree = ast.parse(code_str)
        # 创建RemoveAnnotationsTransformer实例
        transformer = RemoveAnnotationsTransformer()
        # 遍历并修改AST
        modified_tree = transformer.visit(tree)
        # 将修改后的AST转换回代码字符串
        cleaned_code = ast.unparse(modified_tree)
        # 重新写入无注解的源码
        path_dst.write_text(cleaned_code, encoding='utf8')

    def specified_pylist(self, pylist: list[Path | str]):
        """
        ~:指定项目内要加密的文件,只能使用相对于项目文件夹的相对路径
        
        Parameters
        ----------
        - pylist: list[str | Path], 指定项目内要加密的文件,只能使用相对于项目文件夹的相对路径
        """
        for item_relative in pylist:
            item_src = self.src_path / item_relative
            item = self.dst_path / item_relative
            # 处理文件
            if item_src.is_file():
                # 如果文件父目录不存在则创建
                if not item.parent.exists():
                    item.parent.mkdir(parents=True)
                # 复制文件并移除注解
                shutil.copyfile(item_src, item)
                Encrypt.remove_annotation(item, item)
                # 将__init__.py文件重命名为__init__.py.not_encrypt,后续恢复原名
                if '__init__.py' in item.parent.iterdir():
                    new_name = item.parent / '__init__.py.not_encrypt'
                    item.rename(new_name)
                    self.init_file_list.append(new_name)
                else:
                    self.pylist.append(item)
            else:
                if item.exists():
                    shutil.rmtree(item)
                # 等待删除结束
                sleep(1)
                shutil.copytree(item_src, item)
                self.search_py(item)

    def not_specified_pylist(self):
        # 如果目的路径存在则删除
        if self.dst_path.exists():
            shutil.rmtree(self.dst_path)
        # 等待删除结束
        sleep(1)
        # 拷贝源码,后续处理在此拷贝的文件夹中进行
        shutil.copytree(self.src_path, self.dst_path)
        # 检索python文件
        self.search_py(self.dst_path)

    def start_encrypt(self):

        for pyfile in self.pylist:
            setup_str = self.setup_fmt.format_map({
                'pyfile_name': pyfile.name,
            })
            setup_file = pyfile.parent / 'setup.py'
            setup_file.write_text(setup_str, encoding='utf8')
            # 切换到要加密文件的根目录
            os.chdir(setup_file.parent)
            # 执行加密
            os.system(fr"{self.interpreter_path} setup.py build_ext --inplace")
            # 清理中间文件及源码文件
            self.clean(pyfile)
            # 每个文件加密输出的分界线
            print(f'\033[36m{"-*-"*30}\033[0m')
            # 将__init__.py文件恢复原名
        for init_file in self.init_file_list:
            init_file.rename(init_file.parent / '__init__.py')

    def search_py(self, folder: Path):
        """
        ~:递归搜索python文件,并处理
        
        Parameters
        ----------
        """

        for item in folder.iterdir():
            if item.is_file():
                # 存在__init__.py文件时windows下无法正常加密,linux下加密后导入会出现问题, \
                # 所以先重命名,后续会改回来
                if item.name == '__init__.py':
                    new_name = item.parent / '__init__.py.not_encrypt'
                    item.rename(new_name)
                    self.init_file_list.append(new_name)
                elif item.name in self.exclude:
                    continue
                elif item.suffix == '.py':
                    # cython加密最好移除类型注解,因为如果如果实际类型与类型注解不同,加密后代码运行时会报错
                    Encrypt.remove_annotation(item, item)
                    self.pylist.append(item)
            # 删除缓存的字节码,.pyc文件可以很轻易反编译为源码,如果要加密,该字节码文件一定要删除
            elif item.name == '__pycache__':
                shutil.rmtree(item)
            # 递归查找
            else:
                self.search_py(item)

    def clean(self, pyfile: Path):
        """
        ~:清理中间文件及源码文件
        
        Parameters
        ----------
        """

        for path in [
                'build',
                pyfile.stem + '.c',
                pyfile.stem + '.py',
                'setup.py',
        ]:
            path = pyfile.parent / path
            if path.exists():
                if path.is_file():
                    path.unlink()
                else:
                    shutil.rmtree(path)


if __name__ == '__main__':

    Encrypt(r'/path/to/project')

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值