├── .gitignore ├── README.md ├── VERSION ├── docs └── so加密.md ├── encryptpy ├── __init__.py ├── base.py ├── cmdline.py ├── conf │ ├── __init__.py │ ├── filter.yaml │ ├── logconf.py │ └── setting.py ├── so_encrypt │ ├── __init__.py │ └── so_build.py └── utils │ ├── __init__.py │ ├── tempdir.py │ └── utils.py ├── requirements.txt ├── setup.py └── test ├── test002.py └── test003.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | __pycache__ 4 | build.log 5 | build 6 | dist 7 | Encrypt_py.egg-info -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # encryptpy 2 | # 包 3 | 4 | - Cython==0.29.24 5 | - PyYAML==5.4.1 6 | 7 | # 简介 8 | 9 | - 用于加密python文件,python项目 10 | - 应该秉承足够简洁, 思路清晰的原则 11 | 12 | # docs 13 | 14 | - 可支持的加密方式 15 | - [x] py->.so 16 | - [ ] 代码混淆 (未完成) 17 | 18 | ## 问题 19 | 20 | ### 方案一 21 | 22 | - 编译的python版本必须与运行版本一致 23 | - 加密相同名称的文件时包的构建会出错,解决方案为通过不同的进程进行加密,需要尽可能保证项目内文件的互异性 24 | - 脚本方式 25 | - 多进程方案 26 | 27 | # 用法 28 | 29 | > encrypt -i xxx project dir -o output dir 30 | 31 | ## 参数 32 | 33 | ### -h, --help 34 | 35 | > - 显示所有参数的含义 36 | 37 | ### -i INPUT, --input INPUT 38 | 39 | > 项目的路径, 支持相对路径 40 | 41 | ### -o OUTPUT, --output OUTPUT 42 | 43 | > 加密项目路径,支持相对路径 44 | > 默认: ./build 45 | 46 | ### -t TYPE, --type TYPE 47 | 48 | > 加密类型, 当前只支持so加密方式 49 | > 默认: 1 50 | 51 | ### -k KEEP_STEP, --keep_step KEEP_STEP 52 | 53 | > 保存加密过程中产生的中间文件 54 | > 默认:False 55 | 56 | ### -d IGNORED [IGNORED ...], --ignored IGNORED [IGNORED ...] 57 | 58 | > 排除的文件夹目录, 支持正则 59 | > 默认:[.git, __pycache__, .vscode, tests, migrations, __pycache__] 60 | 61 | ### -f IGNORE_PF [IGNORE_PF ...], --ignore_pf IGNORE_PF [IGNORE_PF ...] 62 | 63 | > 排除不需要加密的python文件,目前仅支持文件名,后续增加正则 64 | > 默认:[server.py, config.py] 65 | 66 | ### -l LOGFILE, --logfile LOGFILE 67 | 68 | > log文件路径 69 | > 默认./build.log 70 | 71 | ### -c CONFIG, --config CONFIG 72 | 73 | > 支持配置文件,目前仅支持yaml文件,下文详细说明配置方法 74 | > 需要注意的是应用配置文件其他参数将失效切记 75 | 76 | 77 | 78 | ## yaml文件配置 79 | 80 | ```yaml 81 | version: 1.0.0 82 | 83 | input: /home/sk/project/solar_platform/solar_iter_api_original 84 | output: /home/sk/project/solar_platform/solar_iter_api 85 | type: '1' 86 | keep_step: False 87 | logfile: './build.log' 88 | filter: 89 | ignored: [.git, __pycache__, .vscode, tests, migrations, __pycache__] 90 | ignore_pf: [server.py, config.py] 91 | 92 | ``` 93 | # 参考方案致谢 94 | - -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 -------------------------------------------------------------------------------- /docs/so加密.md: -------------------------------------------------------------------------------- 1 | # so加密文档 2 | 3 | ## 基本思路 4 | - 将代码copy到目标目录 5 | - 进行so加密 6 | - 删除中间过程 -------------------------------------------------------------------------------- /encryptpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .so_encrypt import SOEncryptPy -------------------------------------------------------------------------------- /encryptpy/base.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : base.py 4 | @Time : 2021/10/18 14:43:47 5 | @Author : sk 6 | @Version : 1.0 7 | @Contact : kaixuan.sun@boonray.com 8 | @License : (C)Copyright 2017-2018, Liugroup-NLPR-CASIA 9 | @Desc : None 10 | ''' 11 | # here put the import lib 12 | 13 | import logging 14 | 15 | import os 16 | import shutil 17 | from abc import abstractmethod 18 | from shutil import copytree, ignore_patterns 19 | from typing import List 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | class EncryptPyBase: 24 | 25 | def __init__(self, 26 | input: str, 27 | output: str, 28 | ignored: List = None, 29 | ignore_pf: List = None, 30 | *args, **kwargs) -> None: 31 | 32 | """ 33 | * input_path : 输入路径 可以为文件夹 可以为文件 34 | * output_path : 输出路径 必须为文件夹 35 | * ignored :忽略的文件或文件夹,将不会copy到编译项目下 36 | * ignore_pf :忽略的文件,将不会被加密,但是保存在编译目录下 37 | * keep_step: 保留中间过程文件 38 | """ 39 | self.input = input 40 | self.output = output 41 | self.ignored = ignored 42 | self.ignore_pf = ignore_pf if ignore_pf else [] 43 | self.max_workers = kwargs.get('max_workers', 8) 44 | self.keep_step = kwargs.get('keep_step', False) 45 | 46 | 47 | def copyproject(self): 48 | """拷贝项目到目标文件夹""" 49 | 50 | if os.path.exists(self.output): 51 | shutil.rmtree(self.output) 52 | 53 | return copytree(self.input, 54 | self.output, 55 | ignore=ignore_patterns(*self.ignored), 56 | dirs_exist_ok=True) 57 | 58 | def gen_searchfiles(self): 59 | """搜索可被加密的文件""" 60 | 61 | for root, _, files in os.walk(self.output): 62 | for filename in files: 63 | path = os.path.join(root, filename) 64 | 65 | # TODO: 希望支持正则法则 66 | if (not filename.endswith('.py') or 67 | filename in ['__init__.py'] or 68 | filename in self.ignore_pf): 69 | 70 | logger.warning(f'Exclude file: {root}/{filename} not build') 71 | continue 72 | 73 | yield path 74 | 75 | @abstractmethod 76 | def execute(self): pass 77 | 78 | -------------------------------------------------------------------------------- /encryptpy/cmdline.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import logging.config 4 | import os 5 | 6 | import yaml 7 | 8 | from encryptpy import SOEncryptPy 9 | from encryptpy.conf.logconf import logconfig 10 | from encryptpy.conf.setting import EncryptType 11 | from encryptpy.conf.setting import Setting as setting 12 | 13 | 14 | def init_args(): 15 | """ 16 | initialization args 17 | """ 18 | ignored_default = setting.default_filter_config.get('filter', {}).get('ignored') 19 | ignore_pf_default = setting.default_filter_config.get('filter', {}).get('ignore_pf') 20 | 21 | parser = argparse.ArgumentParser(description="Python encryption program") 22 | 23 | # config 配置将替换掉所有命令 24 | parser.add_argument('-c', '--config', type=str, required=False, help="yaml config file") 25 | 26 | parser.add_argument('-i', '--input', type=str, required=False, help='path of the project') 27 | parser.add_argument('-o', '--output', default='./build', type=str, required=False, help='Absolute path of output') 28 | parser.add_argument('-t', '--type', default=EncryptType.so, type=str, required=False, help="Encryption type") 29 | parser.add_argument('-k', '--keep_step', default=False, type=bool, required=False, help="Keep intermediate files") 30 | parser.add_argument('-d', '--ignored', default=ignored_default, nargs='+', required=False, help="Excluded folders") 31 | parser.add_argument('-f', '--ignore_pf', default=ignore_pf_default, nargs='+', required=False, help="Excluded Python files") 32 | parser.add_argument('-l', '--logfile', default='./build.log', type=str, required=False, help="Log file path") 33 | 34 | return parser.parse_args() 35 | 36 | 37 | def args_filter(args): 38 | 39 | input = args.input 40 | output = args.output 41 | config = args.config 42 | type = args.type 43 | keep_step = args.keep_step 44 | ignored = args.ignored 45 | ignore_pf = args.ignore_pf 46 | logfile = args.logfile 47 | 48 | if config: 49 | if not os.path.isabs(config): 50 | config = os.path.abspath(config) 51 | with open(config) as fi: 52 | config_ = yaml.load(fi, Loader=yaml.FullLoader) 53 | 54 | input = config_.get('input') 55 | output = config_.get('output', './build') 56 | type = config_.get('type', EncryptType.so) 57 | keep_step = config_.get('keep_step', False) 58 | ignored = config_.get('filter', {}).get('ignored') 59 | ignore_pf = config_.get('filter', {}).get('ignore_pf') 60 | logfile = config_.get('logfile', './build.log') 61 | 62 | if not os.path.isabs(input): 63 | input = os.path.abspath(input) 64 | 65 | if not os.path.isabs(output): 66 | output = os.path.abspath(output) 67 | 68 | if not os.path.isabs(logfile): 69 | logfile = os.path.abspath(logfile) 70 | 71 | 72 | return { 73 | 'input': input, 74 | 'output': output, 75 | 'type': type, 76 | 'keep_step': keep_step, 77 | 'ignored': ignored, 78 | 'ignore_pf': ignore_pf, 79 | 'logfile': logfile 80 | } 81 | 82 | 83 | def execute(): 84 | 85 | args = init_args() 86 | args = args_filter(args) 87 | 88 | logfile = args.get('logfile') 89 | log_conf = logconfig(logfile) 90 | logging.config.dictConfig(log_conf) 91 | logger = logging.getLogger(__name__) 92 | 93 | logger.info(f'input parameter: {args}') 94 | 95 | args.pop('logfile') 96 | 97 | type = args.get('type') 98 | if type == EncryptType.so: 99 | exec_cls = SOEncryptPy 100 | else: 101 | raise Exception(f'This encryption type does not exist: {type}') 102 | 103 | encrypt_program = exec_cls(**args) 104 | encrypt_program.execute() 105 | 106 | if __name__ == "__main__": 107 | execute() -------------------------------------------------------------------------------- /encryptpy/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColaSk/encrypt-py/a3dbac8e593708151a5b1f4c927f692003f8319f/encryptpy/conf/__init__.py -------------------------------------------------------------------------------- /encryptpy/conf/filter.yaml: -------------------------------------------------------------------------------- 1 | version: 1.0.0 2 | 3 | input: /home/sk/project/solar_platform/solar_iter_api_original 4 | output: /home/sk/project/solar_platform/solar_iter_api 5 | type: '1' 6 | keep_step: False 7 | logfile: './build.log' 8 | 9 | filter: 10 | ignored: [.git, __pycache__, .vscode, tests, migrations, __pycache__] 11 | ignore_pf: [server.py, config.py] 12 | -------------------------------------------------------------------------------- /encryptpy/conf/logconf.py: -------------------------------------------------------------------------------- 1 | 2 | def logconfig(log_file): 3 | 4 | return { 5 | 'version': 1, 6 | 'disable_existing_loggers': False, 7 | 'formatters': { 8 | 'simple': { 9 | 'format': '[%(asctime)s: %(levelname)s/%(processName)s] %(message)s' 10 | } 11 | }, 12 | 'handlers': { 13 | 'console': { 14 | 'level': 'DEBUG', 15 | 'class': 'logging.StreamHandler', 16 | 'formatter': 'simple', 17 | }, 18 | 'default': { 19 | 'level': 'DEBUG', 20 | 'class': 'logging.handlers.RotatingFileHandler', 21 | 'formatter': 'simple', 22 | 'filename': log_file, 23 | 'backupCount': 10, 24 | 'encoding': 'utf-8' 25 | } 26 | }, 27 | 'root': { 28 | 'handlers': ['console', 'default'], 29 | 'level': 'INFO' 30 | } 31 | } -------------------------------------------------------------------------------- /encryptpy/conf/setting.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : setting.py 4 | @Time : 2021/11/11 11:38:25 5 | @Author : sk 6 | @Version : 1.0 7 | @Contact : kaixuan.sun@boonray.com 8 | @License : (C)Copyright 2017-2018, Liugroup-NLPR-CASIA 9 | @Desc : None 10 | ''' 11 | 12 | # here put the import lib 13 | import os 14 | 15 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | 18 | 19 | class Setting: 20 | 21 | default_filter_config = { 22 | 'filter': { 23 | 'ignored': ['.git',' __pycache__', '.vscode', 'tests', 'migrations', '__pycache__'], 24 | 'ignore_pf': ['server.py', 'config.py'] 25 | } 26 | } 27 | pass 28 | 29 | class EncryptType: 30 | so = '1' -------------------------------------------------------------------------------- /encryptpy/so_encrypt/__init__.py: -------------------------------------------------------------------------------- 1 | from .so_build import SOEncryptPy -------------------------------------------------------------------------------- /encryptpy/so_encrypt/so_build.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : so_build.py 4 | @Time : 2021/10/18 14:41:13 5 | @Author : sk 6 | @Version : 1.0 7 | @Contact : kaixuan.sun@boonray.com 8 | @License : (C)Copyright 2017-2018, Liugroup-NLPR-CASIA 9 | @Desc : None 10 | ''' 11 | 12 | # here put the import lib 13 | 14 | import logging 15 | import os 16 | import re 17 | from concurrent.futures import ProcessPoolExecutor, as_completed 18 | from distutils.command.build_py import build_py 19 | from distutils.core import setup 20 | 21 | from Cython.Build import cythonize 22 | from encryptpy.base import EncryptPyBase 23 | from encryptpy.utils import TempDirContext, running_time 24 | 25 | def get_package_dir(*args, **kwargs): 26 | return "" 27 | 28 | # TODO ? 重写get_package_dir, 否则生成的so文件路径有问题 29 | build_py.get_package_dir = get_package_dir 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | class SOEncryptPy(EncryptPyBase): 34 | """.so 加密python项目""" 35 | 36 | COMPILER_DIRECTIVES = { 37 | 'language_level': 3, # 语言版本 38 | 'always_allow_keywords': True, # 39 | 'annotation_typing': False # 禁止强制类型验证 40 | } 41 | 42 | def encrypt(self, py_file): 43 | 44 | logger.info(f'file: {py_file} Execution starting') 45 | 46 | dir_name = os.path.dirname(py_file) 47 | file_name = os.path.basename(py_file) 48 | os.chdir(dir_name) 49 | 50 | with TempDirContext() as dc: 51 | 52 | setup( 53 | ext_modules=cythonize([file_name], quiet=True, compiler_directives=self.COMPILER_DIRECTIVES), 54 | script_args=['build_ext', '-t', dc, '--inplace'] 55 | ) 56 | 57 | self.del_step_file(py_file) 58 | 59 | logger.info(f'file: {py_file} Execution complete') 60 | 61 | return py_file, True 62 | 63 | def rename_so(self, filepath): 64 | """重命名.so文件""" 65 | 66 | if filepath.endswith('.so'): 67 | newname = re.sub("(.*)\..*\.(.*)", r"\1.\2", filepath) 68 | os.rename(filepath, newname) 69 | logger.info(f'Rename: {filepath}->{newname}') 70 | 71 | def del_step_file(self, py_file): 72 | """删除源文件与过程文件""" 73 | 74 | os.remove(py_file) 75 | 76 | if not self.keep_step: 77 | 78 | if py_file.endswith('.py'): 79 | c_file = py_file.replace('.py', '.c') 80 | else: 81 | c_file = py_file.replace('.pyx', '.c') 82 | 83 | if os.path.exists(c_file): 84 | os.remove(c_file) 85 | 86 | def gen_search_refiles(self): 87 | """搜索需要重命名的.so文件""" 88 | 89 | for root, _, files in os.walk(self.output): 90 | 91 | for filename in files: 92 | path = os.path.join(root, filename) 93 | if not filename.endswith('.so'): 94 | continue 95 | yield path 96 | 97 | @running_time 98 | def execute(self): 99 | """加密执行 100 | 1.拷贝项目 101 | 2.加密文件 102 | 3.删除源文件与中间文件 103 | 4.对.so文件重命名 104 | """ 105 | logger.info('Encryption start'.center(100, '*')) 106 | # copy input -> output 107 | self.copyproject() 108 | 109 | # encrypt py -> .so 110 | # del intermediate file .c 111 | tasks = [] 112 | 113 | # 通过多进程解决同文件编译问题 114 | # TODO:由于进程创建销毁过程导致性能损耗问题,需要解决 115 | for filepath in self.gen_searchfiles(): 116 | with ProcessPoolExecutor(max_workers=1) as pool: 117 | task = pool.submit(self.encrypt, filepath) 118 | tasks.append(task) 119 | 120 | for task in as_completed(tasks): 121 | try: 122 | re_file, rt = task.result() 123 | logger.info(f'file: {re_file} build success, RESULT: {rt}') 124 | except Exception as e: 125 | logger.error(f'unknown error: {e.__str__()}') 126 | 127 | # rename .so 128 | # for filepath in self.gen_search_refiles(): 129 | # self.rename_so(filepath) 130 | 131 | logger.info('Encryption end'.center(100, '*')) 132 | -------------------------------------------------------------------------------- /encryptpy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .tempdir import TempDirContext 2 | from .utils import running_time -------------------------------------------------------------------------------- /encryptpy/utils/tempdir.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : tempdir.py 4 | @Time : 2021/09/07 14:25:54 5 | @Author : sk 6 | @Version : 1.0 7 | @License : (C)Copyright 2017-2018, Liugroup-NLPR-CASIA 8 | @Desc : None 9 | ''' 10 | 11 | # here put the import lib 12 | import shutil 13 | import tempfile 14 | 15 | from contextlib import ContextDecorator 16 | 17 | class TempDirContext(ContextDecorator): 18 | """临时文件夹 19 | """ 20 | 21 | def __init__(self, 22 | suffix=None, 23 | prefix=None, 24 | dir=None) -> None: 25 | 26 | """ 27 | * suffix : 后缀 28 | * prefix : 前缀 29 | * dir : 文件夹路径 30 | """ 31 | 32 | self.suffix = suffix 33 | self.prefix = prefix 34 | self.dir = dir 35 | 36 | 37 | def __enter__(self): 38 | self.dir_name = tempfile.mkdtemp( 39 | self.suffix, self.prefix, self.dir 40 | ) 41 | return self.dir_name 42 | 43 | def __exit__(self, exc_type, exc_value, traceback): 44 | shutil.rmtree(self.dir_name) 45 | -------------------------------------------------------------------------------- /encryptpy/utils/utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | ''' 3 | @File : utils.py 4 | @Time : 2021/09/06 18:44:56 5 | @Author : sk 6 | @Version : 1.0 7 | @License : (C)Copyright 2017-2018, Liugroup-NLPR-CASIA 8 | @Desc : None 9 | ''' 10 | # here put the import lib 11 | 12 | import logging 13 | from datetime import datetime 14 | from functools import wraps 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | def running_time(func): 19 | @wraps(func) 20 | def inner(*args, **kwargs): 21 | curr_time = datetime.now() 22 | rt = func(*args, **kwargs) 23 | run_time = (datetime.now()-curr_time).seconds 24 | logger.info(f'{func.__name__} running time: {run_time} s') 25 | return rt 26 | return inner 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Cython==0.29.24 2 | PyYAML==5.4.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_packages 3 | 4 | VERSION = '1.0.0' 5 | PACKAGES = find_packages() 6 | NAME = 'Encrypt-py' 7 | 8 | INSTALL_REQUIRES = [ 9 | 'Cython==0.29.24', 10 | 'PyYAML==5.4.1' 11 | ] 12 | 13 | with open("README.md", "r") as fh: 14 | long_description = fh.read() 15 | 16 | setup( 17 | name=NAME, # package name 18 | version=VERSION, # package version 19 | author="sk", 20 | author_email="ldu_sunkaixuan@163.com", 21 | description='python Encryption program', # package description 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | license="MIT", 25 | install_requires=INSTALL_REQUIRES, 26 | entry_points={"console_scripts": ["encrypt = encryptpy.cmdline:execute"]}, 27 | packages=find_packages(), 28 | include_package_data=True, 29 | classifiers=["Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: Implementation :: CPython"], 31 | ) -------------------------------------------------------------------------------- /test/test002.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("../") 3 | import logging 4 | from encryptpy import SOEncryptPy 5 | 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | if __name__ == "__main__": 9 | ignored = ['.git', '__pycache__', '.vscode', 'build'] 10 | ignore_pf = ['main.py'] 11 | 12 | so_ep = SOEncryptPy('/home/sk/project/test/encrypted_python', 13 | '/home/sk/project/test/build', 14 | ignored, ignore_pf) 15 | 16 | so_ep.execute() -------------------------------------------------------------------------------- /test/test003.py: -------------------------------------------------------------------------------- 1 | """solar_iter_api 测试""" 2 | import sys 3 | sys.path.append("../") 4 | import logging 5 | import os 6 | 7 | from encryptpy.cmdline import execute 8 | 9 | if __name__ == "__main__": 10 | execute() --------------------------------------------------------------------------------