您现在的位置是: 网站首页 >Python Python

加密编译Python源代码与License控制程序运行

admin2020年2月13日 17:05 Python | 文件 2079人已围观

# Python加密方法 不想被别人看到源代码,且不能影响程序的正常运行 ![BLOG_20200214_125316_76](/media/blog/images/2020/02/BLOG_20200214_125316_76.png "博客图集BLOG_20200214_125316_76.png") ## 编译成pyc ```bash # 命令运行 python -m py_compile /path/test.py ``` ```python # Python代码 import py_compile py_compile.compile(r'/path/test.py') ``` 编译多个文件 ```python import compileall dirpath = '/path/' compileall.compile_dir(dirpath) ``` ## 编译成pyo pyo仅为pyc的一种优化格式,并不是说加密程度会更高 ```bash python -O -m py_compile /path/test.py ``` 编译成pyc或者pyo文件后命名与原来的文件一致,将其放在原来的目录下,虽然其他Python文件调用pyd时显示不能检测到该模块,但实际上可以运行。 由于pyc的编译可能与Python版本有关,如果移动到其他运行,最好Python版本保持一致。 ### 示例:Django之py->pyc 编译文件放置在需要编译的文件夹下面,递归编译所有的py代码 知识点:py编译为pyc,shutil删除文件,移动文件,相对路径,代码当前路径。 ```python #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver1.0 @Author : LR @License : (C) Copyright 2013-2017, MyStudy @Contact : xyliurui@look @Software: PyCharm @File : compile_all.py @Time : 2018/1/11 15:35 @Desc : 脚本放置在目录下,操作该目录下的文件 """ import compileall, py_compile import re import os import shutil # path = r'G:\LR@ProjectsSync\PycharmProjects\DjangoBookMarks_test_pyc' path = os.path.dirname(os.path.abspath(__file__)) # 指定项目的路径 # print(path) print(os.path.abspath(__file__)) if not os.path.exists(os.path.join(path, 'PyBackups')): print('新建py备份文件夹~~~') os.makedirs(os.path.join(path, 'PyBackups')) PyBackups = os.path.join(path, 'PyBackups') # compileall.compile_dir(path, force=True) def compile_all(path): print('尝试编译py文件~~~') for root, dirs, files in os.walk(path): # print(root) # print(True in (map(lambda exclude_path: exclude_path in root, ['migrations']))) if True in (map(lambda exclude_path: exclude_path in root, ['migrations', 'static', 'templates', 'PyBackups'])): print('········>跳过文件夹:', root) continue for filename in files: if filename.endswith('.py'): print('正在编译:', filename) src_file = os.path.join(root, filename) dst_file = os.path.join(root, filename + 'c') if os.path.exists(dst_file): os.remove(dst_file) if src_file == os.path.abspath(__file__): # 如果等于自身就跳过 continue if filename == 'wsgi.py': print('不编译wsgi.py文件!') continue py_compile.compile(src_file, cfile=dst_file) # 得到文件相对于编译文件的相对路径 relative_path = os.path.relpath(src_file) # print('相对路径:', relative_path) # 截取路径中的文件名 # print('文件名:', os.path.basename(relative_path)) # print('文件路径:', os.path.dirname(relative_path)) rel_dir_name = os.path.dirname(relative_path) # 如果备份目录下存在这个文件,则将其删除 if os.path.exists(os.path.join(PyBackups, relative_path)): os.remove(os.path.join(PyBackups, relative_path)) if rel_dir_name == '': print('········>文件的相对路径为空') shutil.move(src_file, PyBackups) else: print('········>文件的相对路径为:', rel_dir_name) py_bak_path = os.path.join(PyBackups, rel_dir_name) if not os.path.exists(py_bak_path): os.makedirs(py_bak_path) shutil.move(src_file, py_bak_path) def recovery_name(path, PyBackups): print('尝试恢复备份的文件~~~') os.chdir(PyBackups) for root, dirs, files in os.walk(PyBackups): for filename in files: if filename.endswith('.py'): print('正在恢复:', filename) src_file_bak = os.path.join(root, filename) # print(src_file_bak) # 得到文件相对于编译文件的相对路径 relative_path = os.path.relpath(src_file_bak) # print('相对路径:', relative_path) print('· > ', os.path.join(path, relative_path)) shutil.move(src_file_bak, os.path.join(path, relative_path)) os.chdir(path) def del_bak_path(PyBackups): passwd = input("输入删除密码,默认'del':") if passwd == 'del': shutil.rmtree(PyBackups) if __name__ == '__main__': while True: print('\n\nc:编译所有的py文件为pyc\nr:编译过程生成的.py.bak文件恢复为.py文件\nd:清空备份目录,且删除该文件夹\nq:退出选择') word = input('请输入选择:') if word == 'c': compile_all(path) elif word == 'r': recovery_name(path, PyBackups) elif word == 'd': del_bak_path(PyBackups) else: break ``` ## pyinstaller打包exe 最好针对单一的文件,需要运行在没有Python环境的Windows系统上 Windows打包Python程序,使用pip安装`pyinstaller` 将`UpdateScriptFile.py`放在一起,可以在同一目录下放置一个ico图标,运行即可 ```python import os os.system("pyinstaller -F UpdateScriptFile.py -i update.ico") ``` ## 编译成pyd或so文件 需要安装`pip install pycrypto Cython` 使用cpython将Python代码编译成C/C++,然后再编译成Python扩展模块,windows上为pyd文件(pyd文件实际就是dll文件),Linux上为so文件 pyd文件在被其它python文件调用时依然不能被识别,但能够运行 ```bash # 命令方式 # (在项目目录下打开命令行或者shell,一次只能编译一个文件, # 编译之后会现出现三个文件:test.c、test.html、test-win_amd64.pyd(Linux为test.xxx.so), # 此时将.c、.html和原.py文件删除,将.pyd文件或.so命名更改为test就可以) cythonize -a -i test.py ``` ### Python代码实现Ver0.1 ```python from distutils.core import setup from Cython.Build import cythonize import os import re import shutil import sys # 针对多文件情况设置,单文件就只写一个就行, 文件之间用逗号隔开 module_list = ['t_test.py', 'app/t_core.py', 'app/t_main.py'] # module_list = ['admin.py', 'models.py', 'urls.py', 'views.py'] current_dir = os.getcwd() # 当前目录,绝对路径 print("当前工作目录:", current_dir) # 该程序所在目录名称,最后一级目录名;使用setup()会自动在程序目录下生成和所在目录相同名称的文件夹,编译后的文件pyd都放在该文件夹中的 current_dir_name = os.path.basename(current_dir) print('工作目录名称:', current_dir_name) # 创建备份源代码文件夹,用于存放编译后相同结构的py文件 backup_dir = os.path.join(current_dir, 'PyBackups') if not os.path.exists(backup_dir): print('新建py备份文件夹:', backup_dir) os.makedirs(backup_dir) file_path_list = [os.path.normpath(os.path.join(current_dir, file)) for file in module_list] # normpath转为系统的路径格式 # print('处理的文件绝对路径:', file_path_list) def rreplace(s, old, new, *max): """ 从右往左替换 :param s: 字符串 :param old: 被替换的字符串 :param new: 新的字符换 :param max: 替换次数 :return: """ count = len(s) if max and str(max[0]).isdigit(): count = max[0] return new.join(s.rsplit(old, count)) def backup_py_file(file_path, backup_dir): """ 备份指定的文件(绝对路径)到新的文件夹 :param file_path: 备份文件绝对路径 :param backup_dir: 备份到的文件夹 :return: """ # 得到文件相对于编译文件的相对路径,例如 app\t_core.py 对应的相对路径如下: file_rel_path = os.path.relpath(file_path) # print('相对路径:', file_rel_path) # app\t_core.py # 截取相对路径中的文件名 # print('文件名:', os.path.basename(file_rel_path)) # t_core.py # print('文件路径:', os.path.dirname(file_rel_path)) # app # 文件相对路径中的目录部分 rel_dir_name = os.path.dirname(file_rel_path) # 如果备份目录下存在这个文件相对目录,则先进行创建 file_backup_dir = os.path.join(backup_dir, rel_dir_name) if not os.path.exists(file_backup_dir): os.makedirs(file_backup_dir) # 文件备份的绝对路径 file_backup_path = os.path.join(backup_dir, file_rel_path) print('【备份】py文件移动:{} ==> {}'.format(file_rel_path, file_backup_path)) if os.path.exists(file_backup_path): os.remove(file_backup_path) os.rename(file_path, file_backup_path) def encrypt_py(): """ 1、编译指定文件 2、删除build临时目录 3、删除.c文件 4、备份原.py文件 5、重命名.pyd或.so文件,并移动回原目录 6、删除编译目录 :return: """ # 编译 try: # 批量编译 setup( name="StarMeow app", ext_modules=cythonize(module_list, language_level=2), ) """ 1、将编译后的pyd、so文件的命名更改成与原py文件一致 2、删除编译后得到的c文件 """ except ValueError as e: print(e) # 编译结束 # 删除build目录 build_path = os.path.join(current_dir, 'build') print('\n× 删除build临时目录:{}\n'.format(build_path)) if os.path.exists(build_path): shutil.rmtree(build_path) for root, dirs, files in os.walk(current_dir): # print(root, dirs, files) for file in files: # 处理文件 file_path = os.path.join(root, file) # print(file_path) if file_path in file_path_list: # 删除.c文件:t_core.c c_file_path = file_path.replace('.py', '.c') print('× 删除.c文件:', c_file_path) if os.path.exists(c_file_path): os.remove(c_file_path) # 备份原py文件 backup_py_file(file_path, backup_dir) # 重命名.pyd或.so文件:t_core.cp37-win_amd64.pyd --> t_core.pyd # 正则匹配名称是file.env.pyd格式的 if re.match(r"\S+\.\S+\.(pyd|so)", file): # print(root, dirs, files) file_path_name, file_extension = os.path.splitext(file_path) # file_path:t_core.cp37-win_amd64.pyd ==> t_core.cp37-win_amd64 , .pyd new_file_path = file_path_name.split('.')[0] + file_extension # t_core.pyd # 文件本应该存放的目录,也就是去掉编译新生成的目录 if "/" in str(file_path): # Linux系统 new_file_path = rreplace(new_file_path, current_dir_name + '/', '', 1) else: # Windows系统 new_file_path = rreplace(new_file_path, current_dir_name + '\\', '', 1) if os.path.exists(new_file_path): # 删除已存在的 os.remove(new_file_path) print('【整合】.pyd文件移动到目标位置:{} ==> {}'.format(file_path, new_file_path)) os.rename(file_path, new_file_path) # 删除编译生成的目录 del_path = os.path.join(current_dir, current_dir_name) print('\n× 删除编译目标目录:{}\n'.format(del_path)) if os.path.exists(del_path): shutil.rmtree(del_path) def recovery_py(backup_dir, current_dir): os.chdir(backup_dir) print('切换目录:', backup_dir) for root, dirs, files in os.walk(backup_dir): for file in files: if file.endswith('.py'): src_file_bak = os.path.join(root, file) # print(src_file_bak) # 得到文件相对于编译文件的相对路径 relative_path = os.path.relpath(src_file_bak) # print('相对路径:', relative_path) rec_file_path = os.path.join(current_dir, relative_path) print('【恢复】.py文件移动到原位置:{} ==> {}'.format(file, rec_file_path)) shutil.move(src_file_bak, rec_file_path) # py恢复过程中,删除pyd文件 del_pyd_path = rec_file_path.replace('.py', '.pyd') if os.path.exists(del_pyd_path): print('× 删除.pyd文件:', del_pyd_path) os.remove(del_pyd_path) del_so_path = rec_file_path.replace('.py', '.so') if os.path.exists(del_so_path): print('× 删除.pyd文件:', del_so_path) os.remove(del_so_path) os.chdir(current_dir) print('返回目录:', current_dir) if __name__ == '__main__': if len(sys.argv) < 2: print('缺少参数,使用方法:程序放在项目下,指定module_list需要加密的相对路径文件列表\n编译文件:python encrypt.py build_ext --inplace\n恢复文件:python encrypt.py recovery') else: if sys.argv[1] == 'build_ext': encrypt_py() elif sys.argv[1] == 'recovery': recovery_py(backup_dir, current_dir) else: print('参数错误') ``` ### setup()参数script_args测试 ```python from distutils.core import setup from Cython.Build import cythonize import os setup( name="StarMeow app", version='0.2', description='加密Python源码', author='StarMeow', author_email='starmeow@qq.com', url='http://blog.starmeow.cn', ext_modules=cythonize(['t_test.py', 'app/t_core.py', 'app/t_main.py'], language_level=3), # 指定模块列表,language_level使用Py3,具体未查证 ) for file in os.popen("tree /f"): print(file, end='') ``` **程序所在目录可以为普通文件夹,也可以为Python包,但子文件夹必须为Python包,也就是存在`__init__.py`文件** 否则会影响output正常值输出。 #### python setup.py build_ext - pyd位置:`当前目录名/build/lib.win-amd64-版本/当前目录名/原结构/` - 临时文件:`当前目录名/build/temp.win-amd64-版本/Release/` ```bash > python setup.py build_ext EncryptTest. │ run.py │ setup.py │ t_test.c │ t_test.py │ __init__.py │ ├─app │ │ t_core.c │ │ t_core.py │ │ t_main.c │ │ t_main.py │ │ __init__.py │ │ │ └─__pycache__ │ t_core.cpython-37.pyc │ t_main.cpython-37.pyc │ __init__.cpython-37.pyc │ └─build ├─lib.win-amd64-3.7 │ └─EncryptTest │ │ t_test.cp37-win_amd64.pyd │ │ │ └─app │ t_core.cp37-win_amd64.pyd │ t_main.cp37-win_amd64.pyd │ └─temp.win-amd64-3.7 └─Release │ t_test.cp37-win_amd64.exp │ t_test.cp37-win_amd64.lib │ t_test.obj │ └─app t_core.cp37-win_amd64.exp t_core.cp37-win_amd64.lib t_core.obj t_main.cp37-win_amd64.exp t_main.cp37-win_amd64.lib t_main.obj # 所有文件都放在build中 ``` #### python setup.py build_ext --inplace - pyd位置:`当前目录名/当前目录名/原结构/` - 临时文件:`当前目录名/build/temp.win-amd64-版本/Release/` ```bash >python setup.py build_ext --inplace EncryptTest. │ run.py │ setup.py │ t_test.c │ t_test.py │ __init__.py │ ├─app │ │ t_core.c │ │ t_core.py │ │ t_main.c │ │ t_main.py │ │ __init__.py │ │ │ └─__pycache__ │ t_core.cpython-37.pyc │ t_main.cpython-37.pyc │ __init__.cpython-37.pyc │ ├─build │ └─temp.win-amd64-3.7 │ └─Release │ │ t_test.cp37-win_amd64.exp │ │ t_test.cp37-win_amd64.lib │ │ t_test.obj │ │ │ └─app │ t_core.cp37-win_amd64.exp │ t_core.cp37-win_amd64.lib │ t_core.obj │ t_main.cp37-win_amd64.exp │ t_main.cp37-win_amd64.lib │ t_main.obj │ └─EncryptTest │ t_test.cp37-win_amd64.pyd │ └─app t_core.cp37-win_amd64.pyd t_main.cp37-win_amd64.pyd # 编译临时文件放在build中,执行文件放在和当前目录同名的文件夹中 ``` #### python setup.py build_ext -b "output" - pyd位置:`当前目录名/-b 指定名称/当前目录名/原结构/` - 临时文件:`当前目录名/build/temp.win-amd64-版本/Release/` ```bash >python setup.py build_ext -b "output" EncryptTest. │ run.py │ setup.py │ t_test.c │ t_test.py │ __init__.py │ ├─app │ │ t_core.c │ │ t_core.py │ │ t_main.c │ │ t_main.py │ │ __init__.py │ │ │ └─__pycache__ │ t_core.cpython-37.pyc │ t_main.cpython-37.pyc │ __init__.cpython-37.pyc │ ├─build │ └─temp.win-amd64-3.7 │ └─Release │ │ t_test.cp37-win_amd64.exp │ │ t_test.cp37-win_amd64.lib │ │ t_test.obj │ │ │ └─app │ t_core.cp37-win_amd64.exp │ t_core.cp37-win_amd64.lib │ t_core.obj │ t_main.cp37-win_amd64.exp │ t_main.cp37-win_amd64.lib │ t_main.obj │ └─output └─EncryptTest │ t_test.cp37-win_amd64.pyd │ └─app t_core.cp37-win_amd64.pyd t_main.cp37-win_amd64.pyd ``` #### python setup.py build_ext -b "output" -t "build_tmp" - pyd位置:`当前目录名/-b 指定名称/当前目录名/原结构/` - 临时文件:`当前目录名/-t 指定名称/Release/` ```bash >python setup.py build_ext -b "output" -t "build_tmp" EncryptTest. │ run.py │ setup.py │ t_test.c │ t_test.py │ __init__.py │ ├─app │ │ t_core.c │ │ t_core.py │ │ t_main.c │ │ t_main.py │ │ __init__.py │ │ │ └─__pycache__ │ t_core.cpython-37.pyc │ t_main.cpython-37.pyc │ __init__.cpython-37.pyc │ ├─build_tmp │ └─Release │ │ t_test.cp37-win_amd64.exp │ │ t_test.cp37-win_amd64.lib │ │ t_test.obj │ │ │ └─app │ t_core.cp37-win_amd64.exp │ t_core.cp37-win_amd64.lib │ t_core.obj │ t_main.cp37-win_amd64.exp │ t_main.cp37-win_amd64.lib │ t_main.obj │ └─output └─EncryptTest │ t_test.cp37-win_amd64.pyd │ └─app t_core.cp37-win_amd64.pyd t_main.cp37-win_amd64.pyd ``` ### 测试文件夹和包生成编译文件路径区别 测试代码: ```python #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver1.0 @Author : StarMeow @License : (C) Copyright 2018-2020, blog.starmeow.cn @Contact : starmeow@qq.com @Software: PyCharm @File : setup.py @Time : 2020/2/11 9:54 @Desc : 测试文件夹和Python包,编译后生成的路径 """ from distutils.core import setup from Cython.Build import cythonize import os for file in os.popen("tree /f"): print(file, end='') # 编译 try: # 批量编译 setup( name="StarMeow app", version='0.2', description='加密Python源码', author='StarMeow', author_email='starmeow@qq.com', url='http://blog.starmeow.cn', ext_modules=cythonize(['file_in_a.py', 'b/file_in_b.py', 'c/file_in_c.py'], language_level=3), # 指定模块列表,language_level使用Py3,具体未查证 script_args=["build_ext", "-b", "build_output", "-t", "build_tmp"] ) except ValueError as e: print(e) # 编译结束 for file in os.popen("tree /f"): print(file, end='') ``` **是Python包会在编译的文件夹中生成该包的名称。** #### 1、Directory(Python Package+Python Package)√ 常见结构: a为目录 /a/file_in_a.py /a ~~/build_output~~ /file_in_a.pyd /a/b/file_in_b.py /a ~~/build_output~~ /b/file_in_b.pyd /a/c/file_in_c.py /a ~~/build_output~~ /c/file_in_c.pyd 要将pyd移动回原目录:重命名去掉`/build_output` ```bash a. │ file_in_a.py │ setup.py │ ├─b │ file_in_b.py │ __init__.py │ └─c file_in_c.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ ├─b │ file_in_b.c │ file_in_b.py │ __init__.py │ ├─build_output │ │ file_in_a.cp37-win_amd64.pyd │ │ │ ├─b │ │ file_in_b.cp37-win_amd64.pyd │ │ │ └─c │ file_in_c.cp37-win_amd64.pyd │ ├─build_tmp │ └─Release... │ └─c file_in_c.c file_in_c.py __init__.py ``` #### 2、Python Package(Python Package+Python Package)√ a为Python包 /a/file_in_a.py /a ~~/build_output/a~~ /file_in_a.pyd /a/b/file_in_b.py /a ~~/build_output/a~~ /b/file_in_b.pyd /a/c/file_in_c.py /a ~~/build_output/a~~ /c/file_in_c.pyd 要将pyd移动回原目录:重命名去掉`/build_output/a` ```bash a. │ file_in_a.py │ setup.py │ __init__.py │ ├─b │ file_in_b.py │ __init__.py │ └─c file_in_c.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ __init__.py │ ├─b │ file_in_b.c │ file_in_b.py │ __init__.py │ ├─build_output │ └─a │ │ file_in_a.cp37-win_amd64.pyd │ │ │ ├─b │ │ file_in_b.cp37-win_amd64.pyd │ │ │ └─c │ file_in_c.cp37-win_amd64.pyd │ ├─build_tmp │ └─Release... │ └─c file_in_c.c file_in_c.py __init__.py ``` #### 3、Python Package(Directory+Python Package) ~~不采用该结构~~ ```bash a. │ file_in_a.py │ setup.py │ __init__.py │ ├─b │ file_in_b.py │ └─c file_in_c.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ __init__.py │ ├─b │ file_in_b.c │ file_in_b.py │ ├─build_output │ │ file_in_b.cp37-win_amd64.pyd │ │ │ └─a │ │ file_in_a.cp37-win_amd64.pyd │ │ │ └─c │ file_in_c.cp37-win_amd64.pyd │ ├─build_tmp │ └─Release... │ └─c file_in_c.c file_in_c.py __init__.py ``` #### 4、Directory(Directory+Python Package) ~~不采用该结构~~ ```bash a. │ file_in_a.py │ setup.py │ ├─b │ file_in_b.py │ └─c file_in_c.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ ├─b │ file_in_b.c │ file_in_b.py │ ├─build_output │ │ file_in_a.cp37-win_amd64.pyd │ │ file_in_b.cp37-win_amd64.pyd │ │ │ └─c │ file_in_c.cp37-win_amd64.pyd │ ├─build_tmp │ └─Release... │ └─c file_in_c.c file_in_c.py __init__.py ``` #### 5、Directory(Python Package(same)+Python Package)√ a为目录,子Python包与父目录同名 /a/file_in_a.py /a ~~/build_output~~ /file_in_a.pyd /a/b/file_in_b.py /a ~~/build_output~~ /b/file_in_b.pyd /a/a/file_in_c.py /a ~~/build_output~~ /a/file_in_c.pyd 要将pyd移动回原目录:重命名去掉`/build_output` ```bash a. │ file_in_a.py │ setup.py │ ├─a │ file_in_c.py │ __init__.py │ └─b file_in_b.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ ├─a │ file_in_c.c │ file_in_c.py │ __init__.py │ ├─b │ file_in_b.c │ file_in_b.py │ __init__.py │ ├─build_output │ │ file_in_a.cp37-win_amd64.pyd │ │ │ ├─a │ │ file_in_c.cp37-win_amd64.pyd │ │ │ └─b │ file_in_b.cp37-win_amd64.pyd │ └─build_tmp └─Release... ``` #### 6、Python Package(Python Package(same)+Python Package)√ a为Python包,子Python与父Python包同名 /a/file_in_a.py /a ~~/build_output/a~~ /file_in_a.pyd /a/b/file_in_b.py /a ~~/build_output/a~~ /b/file_in_b.pyd /a/a/file_in_c.py /a ~~/build_output/a~~ /a/file_in_c.pyd 要将pyd移动回原目录:重命名去掉`/build_output/a` ```bash a. │ file_in_a.py │ setup.py │ __init__.py │ ├─a │ file_in_c.py │ __init__.py │ └─b file_in_b.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ __init__.py │ ├─a │ file_in_c.c │ file_in_c.py │ __init__.py │ ├─b │ file_in_b.c │ file_in_b.py │ __init__.py │ ├─build_output │ └─a │ │ file_in_a.cp37-win_amd64.pyd │ │ │ ├─a │ │ file_in_c.cp37-win_amd64.pyd │ │ │ └─b │ file_in_b.cp37-win_amd64.pyd │ └─build_tmp └─Release... ``` #### 7、Python Package(Directory(same)+Python Package) ~~a为Python包,子目录与父Python包同名~~ ```bash /a/file_in_a.py /a/a/build_output/file_in_a.pyd /a/b/file_in_b.py /a/a/b/build_output/file_in_b.pyd /a/a/file_in_c.py /a/build_output/file_in_c.pyd ``` 要将pyd移动回原目录:比较复杂 ```bash a. │ file_in_a.py │ setup.py │ __init__.py │ ├─a │ file_in_c.py │ └─b file_in_b.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ __init__.py │ ├─a │ file_in_c.c │ file_in_c.py │ ├─b │ file_in_b.c │ file_in_b.py │ __init__.py │ ├─build_output │ │ file_in_c.cp37-win_amd64.pyd │ │ │ └─a │ │ file_in_a.cp37-win_amd64.pyd │ │ │ └─b │ file_in_b.cp37-win_amd64.pyd │ └─build_tmp └─Release ``` #### 8、Directory(Directory(same)+Python Package) ~~a为目录,子目录与父目录同名~~ ```bash /a/file_in_a.py /a/build_output/file_in_a.pyd /a/b/file_in_b.py /a/b/build_output/file_in_b.pyd /a/a/file_in_c.py /a/build_output/file_in_c.pyd ``` 要将pyd移动回原目录:比较复杂 ```bash a. │ file_in_a.py │ setup.py │ ├─a │ file_in_c.py │ └─b file_in_b.py __init__.py ####################################### a. │ file_in_a.c │ file_in_a.py │ setup.py │ ├─a │ file_in_c.c │ file_in_c.py │ ├─b │ file_in_b.c │ file_in_b.py │ __init__.py │ ├─build_output │ │ file_in_a.cp37-win_amd64.pyd │ │ file_in_c.cp37-win_amd64.pyd │ │ │ └─b │ file_in_b.cp37-win_amd64.pyd │ └─build_tmp └─Release ``` 要将pyd移动回原目录:比较复杂 #### 结论 使用该方法编译时,**程序所在目录可以为普通文件夹,也可以Python包,但其子目录必须为Python包**。 ### Python代码实现Ver0.2 ```python from distutils.core import setup from Cython.Build import cythonize import os import re import datetime import shutil import sys tm = datetime.datetime.now().strftime("%Y%m%d%H%M%S") build_output = 'build_output' + tm build_tmp = 'build_tmp' + tm # print(build_output, build_tmp) config = { # 'module_list': ['t_test.py', 'app/t_core.py', 'app/t_main.py'], # 针对多文件情况设置,单文件就只写一个就行, 文件之间用逗号隔开,相对向前文件的路径 'module_list': [], 'include_file': [], # 只包含该名称的文件 'exclude_file': ['run.py'], # 排除该名称文件,后面添加了当前文件名称,以及Django不能编译的文件 'exclude_dir': ['.idea', '__pycache__', 'migrations', 'static', 'templates', "ProjectBackups", build_output, build_tmp], 'backup_dir': '', # 备份目录 'build_output': build_output, # 编译输出目录 'build_tmp': build_tmp # 编译临时目录 } # 指定module_list,则只处理该列表的文件; # 如果module_list为空,且include_file也为空,则只处理程序所在目录,排除exclude_file的py文件; # 如果module_list为空,且include_file不为空,则只处理程序所在目录,包含include_file的py文件 config['exclude_file'].append(os.path.basename(__file__)) # 当前文件名称添加到排除列表中 config['exclude_file'].extend(['__init__.py', 'manage.py', 'asgi.py', 'wsgi.py', 'test.py']) # Django不能编译的文件 # print(config['exclude_file']) current_dir = os.getcwd() # 当前目录,绝对路径 print("INFO··········>当前工作目录:", current_dir) # 该程序所在目录名称,最后一级目录名;使用setup()会自动在程序目录下生成和所在目录相同名称的文件夹,编译后的文件pyd都放在该文件夹中的 current_dir_name = os.path.basename(current_dir) print('INFO··········>工作目录名称:', current_dir_name) # 创建备份源代码文件夹,用于存放编译后相同结构的py文件 backup_dir = config['backup_dir'] if backup_dir != '' and os.path.exists(backup_dir) and os.path.isdir(backup_dir): # 如果指定的目录绝对路径不为空,且目录存在 backup_dir = os.path.join(backup_dir, current_dir_name) else: # 如果不存在,则在当前程序同级创建ProjectBackups目录 backup_dir = os.path.join(current_dir, 'ProjectBackups') if not os.path.exists(backup_dir): print('INFO··········>新建py备份文件夹:', backup_dir) os.makedirs(backup_dir) def rreplace(s, old, new, *max): """ 从右往左替换 :param s: 字符串 :param old: 被替换的字符串 :param new: 新的字符换 :param max: 替换次数 :return: """ count = len(s) if max and str(max[0]).isdigit(): count = max[0] return new.join(s.rsplit(old, count)) def recovery_py(backup_dir, current_dir): os.chdir(backup_dir) print('INFO··········>切换目录:{}\n'.format(backup_dir)) for root, dirs, files in os.walk(backup_dir): for file in files: if file.endswith('.py'): src_file_bak = os.path.join(root, file) # print(src_file_bak) # 得到文件相对于编译文件的相对路径 relative_path = os.path.relpath(src_file_bak) # print('相对路径:', relative_path) rec_file_path = os.path.join(current_dir, relative_path) print('【恢复】.py文件移动到原位置:{} ==> {}'.format(file, rec_file_path)) shutil.move(src_file_bak, rec_file_path) # py恢复过程中,删除pyd文件 del_pyd_path = rec_file_path.replace('.py', '.pyd') if os.path.exists(del_pyd_path): print('WARNING··········>×删除.pyd文件:', del_pyd_path) os.remove(del_pyd_path) del_so_path = rec_file_path.replace('.py', '.so') if os.path.exists(del_so_path): print('WARNING··········>×删除.pyd文件:', del_so_path) os.remove(del_so_path) os.chdir(current_dir) print('\nINFO··········>返回目录:', current_dir) def backup_py_file(file_path, backup_dir): """ 备份指定的文件(绝对路径)到新的文件夹 :param file_path: 备份文件绝对路径 :param backup_dir: 备份到的文件夹 :return: """ # 得到文件相对于编译文件的相对路径,例如 app\t_core.py 对应的相对路径如下: file_rel_path = os.path.relpath(file_path) # print('相对路径:', file_rel_path) # app\t_core.py # 截取相对路径中的文件名 # print('文件名:', os.path.basename(file_rel_path)) # t_core.py # print('文件路径:', os.path.dirname(file_rel_path)) # app # 文件相对路径中的目录部分 rel_dir_name = os.path.dirname(file_rel_path) # 如果备份目录下存在这个文件相对目录,则先进行创建 file_backup_dir = os.path.join(backup_dir, rel_dir_name) if not os.path.exists(file_backup_dir): os.makedirs(file_backup_dir) # 文件备份的绝对路径 file_backup_path = os.path.join(backup_dir, file_rel_path) print('【备份】.py文件移动:{} ==> {}'.format(file_rel_path, file_backup_path)) if os.path.exists(file_backup_path): os.remove(file_backup_path) os.rename(file_path, file_backup_path) def encrypt_py(): """ 1、编译指定文件 2、删除build临时目录 3、删除.c文件 4、备份原.py文件 5、重命名.pyd或.so文件,并移动回原目录 6、删除编译目录 :return: """ # 如果当前目录下存在与目录同名的子目录,则先对其重命名,避免影响编译结果 same_subdir_path = os.path.join(current_dir, current_dir_name) same_subdir_path_new = os.path.join(current_dir, 'tmp_' + current_dir_name + tm) if os.path.exists(same_subdir_path) and os.path.isdir(same_subdir_path): os.rename(same_subdir_path, same_subdir_path_new) print('\nINFO··········>目录同名更改:{} ==> {}\n'.format(same_subdir_path, same_subdir_path_new)) if config['module_list']: # normpath转为系统的路径格式,获取指定文件 file_path_list = [os.path.normpath(os.path.join(current_dir, file)) for file in config['module_list'] if os.path.basename(file) not in config['exclude_file']] else: # 如果没有指定文件,则使用当前目录下的所有 file_path_list = [] if config['include_file']: # 指定文件不为空,从当前目录中,选取指定名称的所有文件 for root, dirs, files in os.walk(current_dir): for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) in config['include_file'] and os.path.isfile(file_path): file_path_list.append(file_path) else: # 指定文件为空,则指处理排除文件名 for root, dirs, files in os.walk(current_dir): if True in (map(lambda exclude_path: exclude_path in root, config['exclude_dir'])): print('INFO··········>跳过文件夹:', root) continue for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) not in config['exclude_file'] and os.path.isfile(file_path) and os.path.splitext(file_path)[1] == '.py': file_path_list.append(file_path) print('\nINFO··········>处理文件路径:{}\n'.format(file_path_list)) # 编译 try: # 批量编译 setup( name="StarMeow app", version='0.2', description='加密Python源码', author='StarMeow', author_email='starmeow@qq.com', url='http://blog.starmeow.cn', ext_modules=cythonize(file_path_list, language_level=3), # 指定模块列表,language_level使用Py3,具体未查证 script_args=["build_ext", "-b", config["build_output"], "-t", config["build_tmp"]] ) except ValueError as e: print(e) # 编译结束 # 删除build临时目录 build_tmp_path = os.path.join(current_dir, config['build_tmp']) print('\nWARNING··········>×删除build临时目录:{}\n'.format(build_tmp_path)) if os.path.exists(build_tmp_path): shutil.rmtree(build_tmp_path) for root, dirs, files in os.walk(current_dir): # print(root, dirs, files) # 判断程序所在目录的子目录不是包:不是工作目录;(不存在__init__.py文件,且排除文件夹的名字在路径中) if root == current_dir or os.path.exists(os.path.join(root, '__init__.py')) or True in map(lambda exclude_path: exclude_path in root, config['exclude_dir']): pass else: print('\n\n\n报错位置:{}\n除程序运行目录,子目录必须要为Python包!请检查,程序还原中···\n\n\n'.format(root)) recovery_py(backup_dir, current_dir) # 还原操作 break for file in files: # 处理文件 file_path = os.path.join(root, file) # print(file_path) if file_path in file_path_list: # 删除.c文件:t_core.c c_file_path = file_path.replace('.py', '.c') print('WARNING··········>×删除.c文件:', c_file_path) if os.path.exists(c_file_path): os.remove(c_file_path) # 备份原py文件 backup_py_file(file_path, backup_dir) # 重命名.pyd或.so文件:t_core.cp37-win_amd64.pyd --> t_core.pyd # 正则匹配名称是file.env.pyd格式的 if re.match(r"\S+\.\S+\.(pyd|so)", file): # print(root, dirs, files) file_path_name, file_extension = os.path.splitext(file_path) # file_path:t_core.cp37-win_amd64.pyd ==> t_core.cp37-win_amd64 , .pyd new_file_path = file_path_name.split('.')[0] + file_extension # t_core.pyd print(new_file_path) # 文件本应该存放的目录,也就是去掉编译新生成的目录 # os.path.normpath('/a/b') 在Windows下会转换为 '\\a\\b' # 如果原来的目录是包(有__init__.py文件),则会在编译目录中创建包名文件夹,即: # root/app/__init__.py 存在,则 root/app/b.py 编译后为 root/build_output/app/b.pyd build_rel_path = os.path.normpath('/{}/{}'.format(config['build_output'], current_dir_name)) print(build_rel_path, str(new_file_path).replace(build_rel_path, '')) # 需要替换的相对路径在当前绝对路径中,且替换后保存路径是存在的(只取路径部分) new_file_path = str(new_file_path).replace(build_rel_path, '') # 程序运行目录是Python包的请况 if build_rel_path in new_file_path and os.path.exists(os.path.dirname(new_file_path)): new_file_path = new_file_path else: new_file_path = str(new_file_path).replace(os.path.normpath('/{}'.format(config['build_output'])), '') # # 程序运行目录是普通文件夹的请况 print('INFO··········>程序所在目录为文件夹,不是Python包:', current_dir) print(new_file_path) if os.path.exists(new_file_path): # 删除已存在的 os.remove(new_file_path) print('【整合】.pyd文件移动到目标位置:{} ==> {}'.format(file_path, new_file_path)) os.rename(file_path, new_file_path) # 删除编译生成的目录 build_output_path = os.path.join(current_dir, config['build_output']) print('\nWARNING··········>×删除编译目标目录:{}\n'.format(build_output_path)) if os.path.exists(build_output_path): shutil.rmtree(build_output_path) # 编译完成后,对相同名称的子目录进行名称还原 if os.path.exists(same_subdir_path_new) and os.path.isdir(same_subdir_path_new): os.rename(same_subdir_path_new, same_subdir_path) print('INFO··········>目录同名还原:{} ==> {}'.format(same_subdir_path_new, same_subdir_path)) # 同时对备份的目录也进行重命名 same_subdir_backup = os.path.join(backup_dir, 'tmp_' + current_dir_name + tm) same_subdir_backup_new = os.path.join(backup_dir, current_dir_name) if os.path.exists(same_subdir_backup_new): shutil.rmtree(same_subdir_backup_new) if os.path.exists(same_subdir_backup) and os.path.isdir(same_subdir_backup): os.rename(same_subdir_backup, same_subdir_backup_new) print('INFO··········>备份同名更正:{} ==> {}'.format(same_subdir_backup, same_subdir_backup_new)) if __name__ == '__main__': if len(sys.argv) < 2: print('缺少参数,使用方法:程序放在项目下,可指定module_list需要加密的相对路径文件列表\n编译文件:python setup.py build_ext\n恢复文件:python setup.py recovery') else: if sys.argv[1] == 'build_ext': encrypt_py() elif sys.argv[1] == 'recovery': recovery_py(backup_dir, current_dir) else: print('参数错误') ``` ### Python代码实现Ver0.3 ```python from distutils.core import setup from Cython.Build import cythonize import os import re import datetime import shutil import sys tm = datetime.datetime.now().strftime("%Y%m%d%H%M%S") build_output = 'build_output' + tm build_tmp = 'build_tmp' + tm # print(build_output, build_tmp) config = { # 'module_list': ['t_test.py', 'app/t_core.py', 'app/t_main.py'], # 针对多文件情况设置,单文件就只写一个就行, 文件之间用逗号隔开,相对向前文件的路径 'module_list': [], 'include_file': [], # 只包含该名称的文件 'exclude_file': ['run.py'], # 排除该名称文件,后面添加了当前文件名称,以及Django不能编译的文件 'exclude_dir': ['.idea', '__pycache__', 'migrations', 'static', 'templates', "ProjectBackups", build_output, build_tmp], 'backup_dir': '', # 备份目录 'build_output': build_output, # 编译输出目录 'build_tmp': build_tmp # 编译临时目录 } # 指定module_list,则只处理该列表的文件; # 如果module_list为空,且include_file也为空,则只处理程序所在目录,排除exclude_file的py文件; # 如果module_list为空,且include_file不为空,则只处理程序所在目录,包含include_file的py文件 config['exclude_file'].append(os.path.basename(__file__)) # 当前文件名称添加到排除列表中 config['exclude_file'].extend(['__init__.py', 'manage.py', 'asgi.py', 'wsgi.py', 'test.py']) # Django不能编译的文件 # print(config['exclude_file']) current_dir = os.getcwd() # 当前目录,绝对路径 print("INFO··········>当前工作目录:", current_dir) # 该程序所在目录名称,最后一级目录名;使用setup()会自动在程序目录下生成和所在目录相同名称的文件夹,编译后的文件pyd都放在该文件夹中的 current_dir_name = os.path.basename(current_dir) print('INFO··········>工作目录名称:', current_dir_name) # 创建备份源代码文件夹,用于存放编译后相同结构的py文件 backup_dir = config['backup_dir'] if backup_dir != '' and os.path.exists(backup_dir) and os.path.isdir(backup_dir): # 如果指定的目录绝对路径不为空,且目录存在 backup_dir = os.path.join(backup_dir, current_dir_name) else: # 如果不存在,则在当前程序同级创建ProjectBackups目录 backup_dir = os.path.join(current_dir, 'ProjectBackups') if not os.path.exists(backup_dir): print('INFO··········>新建py备份文件夹:', backup_dir) os.makedirs(backup_dir) def rreplace(s, old, new, *max): """ 从右往左替换 :param s: 字符串 :param old: 被替换的字符串 :param new: 新的字符换 :param max: 替换次数 :return: """ count = len(s) if max and str(max[0]).isdigit(): count = max[0] return new.join(s.rsplit(old, count)) def recovery_py(backup_dir, current_dir): os.chdir(backup_dir) print('INFO··········>切换目录:{}\n'.format(backup_dir)) for root, dirs, files in os.walk(backup_dir): for file in files: if file.endswith('.py'): src_file_bak = os.path.join(root, file) # print(src_file_bak) # 得到文件相对于编译文件的相对路径 relative_path = os.path.relpath(src_file_bak) # print('相对路径:', relative_path) rec_file_path = os.path.join(current_dir, relative_path) print('>RECOVERY<.py文件移动到原位置:{} ==> {}'.format(file, rec_file_path)) shutil.move(src_file_bak, rec_file_path) # py恢复过程中,删除pyd文件 del_pyd_path = rec_file_path.replace('.py', '.pyd') if os.path.exists(del_pyd_path): print('WARNING··········>×删除.pyd文件:', del_pyd_path) os.remove(del_pyd_path) del_so_path = rec_file_path.replace('.py', '.so') if os.path.exists(del_so_path): print('WARNING··········>×删除.pyd文件:', del_so_path) os.remove(del_so_path) os.chdir(current_dir) print('\nINFO··········>返回目录:', current_dir) def backup_py_file(file_path, backup_dir): """ 备份指定的文件(绝对路径)到新的文件夹 :param file_path: 备份文件绝对路径 :param backup_dir: 备份到的文件夹 :return: """ # 得到文件相对于编译文件的相对路径,例如 app\t_core.py 对应的相对路径如下: file_rel_path = os.path.relpath(file_path) # print('相对路径:', file_rel_path) # app\t_core.py # 截取相对路径中的文件名 # print('文件名:', os.path.basename(file_rel_path)) # t_core.py # print('文件路径:', os.path.dirname(file_rel_path)) # app # 文件相对路径中的目录部分 rel_dir_name = os.path.dirname(file_rel_path) # 如果备份目录下存在这个文件相对目录,则先进行创建 file_backup_dir = os.path.join(backup_dir, rel_dir_name) if not os.path.exists(file_backup_dir): os.makedirs(file_backup_dir) # 文件备份的绝对路径 file_backup_path = os.path.join(backup_dir, file_rel_path) print('<BACKUP> .py文件移动:{} ==> {}'.format(file_rel_path, file_backup_path)) if os.path.exists(file_backup_path): os.remove(file_backup_path) os.rename(file_path, file_backup_path) def encrypt_py(): """ 1、编译指定文件 2、删除build临时目录 3、删除.c文件 4、备份原.py文件 5、重命名.pyd或.so文件,并移动回原目录 6、删除编译目录 :return: """ if config['module_list']: # normpath转为系统的路径格式,获取指定文件 file_path_list = [os.path.normpath(os.path.join(current_dir, file)) for file in config['module_list'] if os.path.basename(file) not in config['exclude_file']] else: # 如果没有指定文件,则使用当前目录下的所有 file_path_list = [] if config['include_file']: # 指定文件不为空,从当前目录中,选取指定名称的所有文件 for root, dirs, files in os.walk(current_dir): for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) in config['include_file'] and os.path.isfile(file_path): file_path_list.append(file_path) else: # 指定文件为空,则指处理排除文件名 for root, dirs, files in os.walk(current_dir): if True in (map(lambda exclude_path: exclude_path in root, config['exclude_dir'])): print('INFO··········>跳过文件夹:', root) continue for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) not in config['exclude_file'] and os.path.isfile(file_path) and os.path.splitext(file_path)[1] == '.py': file_path_list.append(file_path) print('\nINFO··········>处理文件路径:{}\n'.format(file_path_list)) # 编译 try: # 批量编译 setup( name="StarMeow app", version='0.2', description='加密Python源码', author='StarMeow', author_email='starmeow@qq.com', url='http://blog.starmeow.cn', ext_modules=cythonize(file_path_list, language_level=3), # 指定模块列表,language_level使用Py3,具体未查证 script_args=["build_ext", "-b", config["build_output"], "-t", config["build_tmp"]] ) except ValueError as e: print(e) # 编译结束 # 删除build临时目录 build_tmp_path = os.path.join(current_dir, config['build_tmp']) print('\nWARNING··········>×删除build临时目录:{}\n'.format(build_tmp_path)) if os.path.exists(build_tmp_path): shutil.rmtree(build_tmp_path) for root, dirs, files in os.walk(current_dir): # print(root, dirs, files) # 判断子目录是否是Python包,如果不是,则进行错误标记,并结束子目录和父目录循环 error_flag = False for dir in dirs: dir_path = os.path.join(root, dir) # 判断程序所在目录的子目录不是包:不是工作目录;(不存在__init__.py文件,且排除文件夹的名字在路径中) if dir_path == current_dir \ or os.path.exists(os.path.join(dir_path, '__init__.py')) \ or True in map(lambda exclude_path: exclude_path in dir_path, config['exclude_dir']): # print('当前处理路径,通过:', dir_path) pass else: print('\n\n\n报错位置:{}\n除程序运行目录,子目录必须要为Python包!请检查,程序还原中···\n\n\n'.format(dir_path)) error_flag = True break # 解锁所有循环, if error_flag: recovery_py(backup_dir, current_dir) # 调用一次还原操作,备份后的py文件进行还原 break for file in files: # 处理文件 file_path = os.path.join(root, file) # print(file_path) if file_path in file_path_list: # 删除.c文件:t_core.c c_file_path = file_path.replace('.py', '.c') print('WARNING··········>×删除.c文件:', c_file_path) if os.path.exists(c_file_path): os.remove(c_file_path) # 备份原py文件 backup_py_file(file_path, backup_dir) # 重命名.pyd或.so文件:t_core.cp37-win_amd64.pyd --> t_core.pyd # 正则匹配名称是file.env.pyd格式的 if re.match(r"\S+\.\S+\.(pyd|so)", file): # print(root, dirs, files) file_path_name, file_extension = os.path.splitext(file_path) # file_path:t_core.cp37-win_amd64.pyd ==> t_core.cp37-win_amd64 , .pyd new_file_path = file_path_name.split('.')[0] + file_extension # t_core.pyd # print('RENAME----------第1次改名:', new_file_path) # 文件本应该存放的目录,也就是去掉编译新生成的目录 # os.path.normpath('/a/b') 在Windows下会转换为 '\\a\\b' # 如果原来的目录是包(有__init__.py文件),则会在编译目录中创建包名文件夹,即: # root/__init__.py 存在,则 root/app/b.py 编译后为 root/build_output/root/app/b.pyd # 请况一:Python包 \EncryptTest\build_output20200211113628\EncryptTest\t_test.pyd => \EncryptTest\t_test.pyd if os.path.exists(os.path.join(current_dir, '__init__.py')): build_rel_path = os.path.normpath('/{}/{}'.format(config['build_output'], current_dir_name)) # \build_output20200211113628\EncryptTest # 请况二:文件夹 \EncryptTest\build_output20200211114306\t_test.pyd => \EncryptTest\t_test.pyd else: print('INFO··········>程序所在目录为文件夹,不是Python包:', current_dir) build_rel_path = os.path.normpath('/{}'.format(config['build_output'])) # \build_output20200211113628 new_file_path = str(new_file_path).replace(build_rel_path, '') # print('RENAME----------第2次改名:', new_file_path) if os.path.exists(new_file_path): # 删除已存在的 os.remove(new_file_path) print('>MOVE> .pyd文件移动到目标位置:{} ==> {}'.format(file_path, new_file_path)) os.rename(file_path, new_file_path) # 删除编译生成的目录 build_output_path = os.path.join(current_dir, config['build_output']) print('\nWARNING··········>×删除编译目标目录:{}\n'.format(build_output_path)) if os.path.exists(build_output_path): shutil.rmtree(build_output_path) if __name__ == '__main__': if len(sys.argv) < 2: print('缺少参数,使用方法:程序放在项目下,可指定module_list需要加密的相对路径文件列表\n编译文件:python setup.py build_ext\n恢复文件:python setup.py recovery') else: if sys.argv[1] == 'build_ext': encrypt_py() elif sys.argv[1] == 'recovery': recovery_py(backup_dir, current_dir) else: print('参数错误') ``` ### Python代码实现Ver0.4 ```python from distutils.core import setup from Cython.Build import cythonize import os import re import datetime import shutil import sys tm = datetime.datetime.now().strftime("%Y%m%d%H%M%S") build_output = 'build_output' + tm build_tmp = 'build_tmp' + tm # print(build_output, build_tmp) config = { # 'module_list': ['t_test.py', 'app/t_core.py', 'app/t_main.py'], # 针对多文件情况设置,单文件就只写一个就行, 文件之间用逗号隔开,相对向前文件的路径 'module_list': [], 'include_file': [], # 只包含该名称的文件 'exclude_file': ['run.py'], # 排除该名称文件,后面添加了当前文件名称,以及Django不能编译的文件 'exclude_dir': ['.idea', '__pycache__', 'migrations', 'static', 'templates', "ProjectBackups", build_output, build_tmp], 'backup_dir': '', # 备份目录 'build_output': build_output, # 编译输出目录 'build_tmp': build_tmp # 编译临时目录 } # 指定module_list,则只处理该列表的文件; # 如果module_list为空,且include_file也为空,则只处理程序所在目录,排除exclude_file的py文件; # 如果module_list为空,且include_file不为空,则只处理程序所在目录,包含include_file的py文件 config['exclude_file'].append(os.path.basename(__file__)) # 当前文件名称添加到排除列表中 config['exclude_file'].extend(['__init__.py', 'manage.py', 'asgi.py', 'wsgi.py', 'test.py']) # Django不能编译的文件 # print(config['exclude_file']) current_dir = os.getcwd() # 当前目录,绝对路径 print("INFO··········>当前工作目录:", current_dir) # 该程序所在目录名称,最后一级目录名;使用setup()会自动在程序目录下生成和所在目录相同名称的文件夹,编译后的文件pyd都放在该文件夹中的 current_dir_name = os.path.basename(current_dir) print('INFO··········>工作目录名称:', current_dir_name) # 创建备份源代码文件夹,用于存放编译后相同结构的py文件 backup_dir = config['backup_dir'] if backup_dir != '' and os.path.exists(backup_dir) and os.path.isdir(backup_dir): # 如果指定的目录绝对路径不为空,且目录存在 backup_dir = os.path.join(backup_dir, current_dir_name) else: # 如果不存在,则在当前程序同级创建ProjectBackups目录 backup_dir = os.path.join(current_dir, 'ProjectBackups') if not os.path.exists(backup_dir): print('INFO··········>新建py备份文件夹:', backup_dir) os.makedirs(backup_dir) def rreplace(s, old, new, *max): """ 从右往左替换 :param s: 字符串 :param old: 被替换的字符串 :param new: 新的字符换 :param max: 替换次数 :return: """ count = len(s) if max and str(max[0]).isdigit(): count = max[0] return new.join(s.rsplit(old, count)) def recovery_py(backup_dir, current_dir): os.chdir(backup_dir) print('INFO··········>切换目录:{}\n'.format(backup_dir)) for root, dirs, files in os.walk(backup_dir): for file in files: if file.endswith('.py'): src_file_bak = os.path.join(root, file) # print(src_file_bak) # 得到文件相对于编译文件的相对路径 relative_path = os.path.relpath(src_file_bak) # print('相对路径:', relative_path) rec_file_path = os.path.join(current_dir, relative_path) print('>RECOVERY<.py文件移动到原位置:{} ==> {}'.format(file, rec_file_path)) shutil.move(src_file_bak, rec_file_path) # py恢复过程中,删除pyd文件 del_pyd_path = rec_file_path.replace('.py', '.pyd') if os.path.exists(del_pyd_path): print('WARNING··········>×删除.pyd文件:', del_pyd_path) os.remove(del_pyd_path) del_so_path = rec_file_path.replace('.py', '.so') if os.path.exists(del_so_path): print('WARNING··········>×删除.pyd文件:', del_so_path) os.remove(del_so_path) os.chdir(current_dir) print('\nINFO··········>返回目录:', current_dir) def backup_py_file(file_path, backup_dir): """ 备份指定的文件(绝对路径)到新的文件夹 :param file_path: 备份文件绝对路径 :param backup_dir: 备份到的文件夹 :return: """ # 得到文件相对于编译文件的相对路径,例如 app\t_core.py 对应的相对路径如下: file_rel_path = os.path.relpath(file_path) # print('相对路径:', file_rel_path) # app\t_core.py # 截取相对路径中的文件名 # print('文件名:', os.path.basename(file_rel_path)) # t_core.py # print('文件路径:', os.path.dirname(file_rel_path)) # app # 文件相对路径中的目录部分 rel_dir_name = os.path.dirname(file_rel_path) # 如果备份目录下存在这个文件相对目录,则先进行创建 file_backup_dir = os.path.join(backup_dir, rel_dir_name) if not os.path.exists(file_backup_dir): os.makedirs(file_backup_dir) # 文件备份的绝对路径 file_backup_path = os.path.join(backup_dir, file_rel_path) print('<BACKUP> .py文件移动:{} ==> {}'.format(file_rel_path, file_backup_path)) if os.path.exists(file_backup_path): os.remove(file_backup_path) os.rename(file_path, file_backup_path) def encrypt_py(): """ 1、编译指定文件 2、删除build临时目录 3、删除.c文件 4、备份原.py文件 5、重命名.pyd或.so文件,并移动回原目录 6、删除编译目录 :return: """ if config['module_list']: # normpath转为系统的路径格式,获取指定文件 file_path_list = [os.path.normpath(os.path.join(current_dir, file)) for file in config['module_list'] if os.path.basename(file) not in config['exclude_file']] else: # 如果没有指定文件,则使用当前目录下的所有 file_path_list = [] if config['include_file']: # 指定文件不为空,从当前目录中,选取指定名称的所有文件 for root, dirs, files in os.walk(current_dir): for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) in config['include_file'] and os.path.isfile(file_path): file_path_list.append(file_path) else: # 指定文件为空,则指处理排除文件名 for root, dirs, files in os.walk(current_dir): if True in (map(lambda exclude_path: exclude_path in root, config['exclude_dir'])): print('INFO··········>跳过文件夹:', root) continue for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) not in config['exclude_file'] and os.path.isfile(file_path) and os.path.splitext(file_path)[1] == '.py': file_path_list.append(file_path) print('\nINFO··········>处理文件路径:{}\n'.format(file_path_list)) # 编译 try: # 批量编译 setup( name="StarMeow app", version='0.2', description='加密Python源码', author='StarMeow', author_email='starmeow@qq.com', url='http://blog.starmeow.cn', ext_modules=cythonize(file_path_list, language_level=3), # 指定模块列表,language_level使用Py3,具体未查证 script_args=["build_ext", "-b", config["build_output"], "-t", config["build_tmp"]] ) except ValueError as e: print(e) # 编译结束 # 删除build临时目录 build_tmp_path = os.path.join(current_dir, config['build_tmp']) print('\nWARNING··········>×删除build临时目录:{}\n'.format(build_tmp_path)) if os.path.exists(build_tmp_path): shutil.rmtree(build_tmp_path) for root, dirs, files in os.walk(current_dir): # print(root, dirs, files) # 判断子目录是否是Python包,如果不是,则进行错误标记,并结束子目录和父目录循环 error_flag = False for dir in dirs: dir_path = os.path.join(root, dir) # 判断程序所在目录的子目录不是包:不是工作目录;(不存在__init__.py文件,且排除文件夹的名字在路径中) if dir_path == current_dir \ or os.path.exists(os.path.join(dir_path, '__init__.py')) \ or True in map(lambda exclude_path: exclude_path in dir_path, config['exclude_dir']): # print('当前处理路径,通过:', dir_path) pass else: print('\n\n\n报错位置:{}\n除程序运行目录,子目录必须要为Python包!请检查,程序还原中···\n\n\n'.format(dir_path)) error_flag = True break # 解锁所有循环, if error_flag: recovery_py(backup_dir, current_dir) # 调用一次还原操作,备份后的py文件进行还原 break for file in files: # 处理文件 file_path = os.path.join(root, file) # print(file_path) if file_path in file_path_list: # 删除.c文件:t_core.c c_file_path = file_path.replace('.py', '.c') print('WARNING··········>×删除.c文件:', c_file_path) if os.path.exists(c_file_path): os.remove(c_file_path) # 备份原py文件 backup_py_file(file_path, backup_dir) # 重命名.pyd或.so文件:t_core.cp37-win_amd64.pyd --> t_core.pyd # 正则匹配名称是file.env.pyd格式的 if re.match(r"\S+\.\S+\.(pyd|so)", file): # print(root, dirs, files) file_path_name, file_extension = os.path.splitext(file_path) # file_path:t_core.cp37-win_amd64.pyd ==> t_core.cp37-win_amd64 , .pyd new_file_path = file_path_name.split('.')[0] + file_extension # t_core.pyd # print('RENAME----------第1次改名:', new_file_path) # 文件本应该存放的目录,也就是去掉编译新生成的目录 # os.path.normpath('/a/b') 在Windows下会转换为 '\\a\\b' # 如果原来的目录是包(有__init__.py文件),则会在编译目录中创建包名文件夹,即: # root/__init__.py 存在,则 root/app/b.py 编译后为 root/build_output/root/app/b.pyd # 请况一:Python包 \EncryptTest\build_output20200211113628\EncryptTest\t_test.pyd => \EncryptTest\t_test.pyd if os.path.exists(os.path.join(current_dir, '__init__.py')): build_rel_path = os.path.normpath('/{}/{}'.format(config['build_output'], current_dir_name)) # \build_output20200211113628\EncryptTest # 请况二:文件夹 \EncryptTest\build_output20200211114306\t_test.pyd => \EncryptTest\t_test.pyd else: print('INFO··········>程序所在目录为文件夹,不是Python包:', current_dir) build_rel_path = os.path.normpath('/{}'.format(config['build_output'])) # \build_output20200211113628 new_file_path = str(new_file_path).replace(build_rel_path, '') # print('RENAME----------第2次改名:', new_file_path) if os.path.exists(new_file_path): # 删除已存在的 os.remove(new_file_path) print('>MOVE> .pyd文件移动到目标位置:{} ==> {}'.format(file_path, new_file_path)) os.rename(file_path, new_file_path) # 删除编译生成的目录 build_output_path = os.path.join(current_dir, config['build_output']) print('\nWARNING··········>×删除编译目标目录:{}\n'.format(build_output_path)) if os.path.exists(build_output_path): shutil.rmtree(build_output_path) if __name__ == '__main__': if len(sys.argv) < 2: print('缺少参数,使用方法:程序放在项目下,可指定module_list需要加密的相对路径文件列表\n编译文件:python starmeow_setup.py build_ext\n恢复文件:python starmeow_setup.py recovery') else: if sys.argv[1] == 'build_ext': encrypt_py() elif sys.argv[1] == 'recovery': recovery_py(backup_dir, current_dir) else: print('参数错误') ``` ### Python代码实现Ver0.5【starmeow_setup.py】 ```python #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver0.5 @Author : StarMeow @License : (C) Copyright 2018-2020, blog.starmeow.cn @Contact : starmeow@qq.com @Software: PyCharm @File : starmeow_setup.py @Time : 2020年2月13日 16:36:04 @Desc : 加密Python代码,编译成pyd或so文件 """ import os import re import sys import datetime import shutil from distutils.core import setup from Cython.Build import cythonize """ Linux安装:yum install python3-devel gcc、apt-get install python3-dev gcc Python库 :pip install pycrypto Cython (Windows安装稍复杂些,可能会存在安装不上的问题,Linux安装pycrypto:#include "Python.h"编译中断error: command 'gcc' failed with exit status 1,注意是安装python3-devel) 说明: 1、程序所在目录可以为普通文件夹,也可以为Python包,但子文件夹必须为Python包,也就是存在__init__.py文件。 2、module_list 指定只要编译的文件列表,相对路径或绝对路径均可以,需要排除本文件,以及入口py程序。 3、命令行中,进入这个文件所在目录:python starmeow_setup.py build_ext。 4、使用cpython将Python代码编译成C/C++,然后再编译成Python扩展模块,windows上为pyd文件(pyd文件实际就是dll文件),Linux上为so文件。 5、编译完成.pyd引用会显示错误,且编译器不能自动显示可使用的方法和类,但实际可以调用运行。 示例: EncryptTest. │ run.py │ starmeow_setup.py │ t_test.py │ __init__.py │ └─app │ t_core.py │ t_main.py │ __init__.py │ └─__pycache__ 一、执行 python starmeow_setup.py build_ext 编译后 EncryptTest. │ run.py │ starmeow_setup.py │ t_test.pyd │ __init__.py │ ├─app │ │ t_core.pyd │ │ t_main.pyd │ │ __init__.py │ │ │ └─__pycache__ │ └─ProjectBackups │ t_test.py │ └─app t_core.py t_main.py 二、恢复编译后备份的py文件:python starmeow_setup.py recovery ,会自动删除同名的pyd文件 执行 python starmeow_setup.py recovery 恢复后 EncryptTest. │ run.py │ starmeow_setup.py │ t_test.py │ __init__.py │ ├─app │ │ t_core.py │ │ t_main.py │ │ __init__.py │ │ │ └─__pycache__ │ └─ProjectBackups └─app """ tm = datetime.datetime.now().strftime("%Y%m%d%H%M%S") build_output = 'build_output' + tm build_tmp = 'build_tmp' + tm # print(build_output, build_tmp) config = { # 'module_list': ['t_test.py', 'app/t_core.py', 'app/t_main.py'], # 针对多文件情况设置,单文件就只写一个就行, 文件之间用逗号隔开,相对向前文件的路径 'module_list': [], 'include_file': [], # 只包含该名称的文件 'exclude_file': ['run.py'], # 排除该名称文件,后面添加了当前文件名称,以及Django不能编译的文件 'exclude_dir': ['.idea', '__pycache__', 'migrations', 'static', 'templates', "ProjectBackups", build_output, build_tmp], 'backup_dir': '', # 备份目录 'build_output': build_output, # 编译输出目录 'build_tmp': build_tmp # 编译临时目录 } # 指定module_list,则只处理该列表的文件; # 如果module_list为空,且include_file也为空,则只处理程序所在目录,排除exclude_file的py文件; # 如果module_list为空,且include_file不为空,则只处理程序所在目录,包含include_file的py文件 config['exclude_file'].append(os.path.basename(__file__)) # 当前文件名称添加到排除列表中 config['exclude_file'].extend(['__init__.py', 'manage.py', 'asgi.py', 'wsgi.py', 'test.py']) # Django不能编译的文件 # print(config['exclude_file']) current_dir = os.getcwd() # 当前目录,绝对路径 print("INFO··········>当前工作目录:", current_dir) # 该程序所在目录名称,最后一级目录名;使用setup()会自动在程序目录下生成和所在目录相同名称的文件夹,编译后的文件pyd都放在该文件夹中的 current_dir_name = os.path.basename(current_dir) print('INFO··········>工作目录名称:', current_dir_name) # 创建备份源代码文件夹,用于存放编译后相同结构的py文件 backup_dir = config['backup_dir'] if backup_dir != '' and os.path.exists(backup_dir) and os.path.isdir(backup_dir): # 如果指定的目录绝对路径不为空,且目录存在 backup_dir = os.path.join(backup_dir, current_dir_name) else: # 如果不存在,则在当前程序同级创建ProjectBackups目录 backup_dir = os.path.join(current_dir, 'ProjectBackups') if not os.path.exists(backup_dir): print('INFO··········>新建py备份文件夹:', backup_dir) os.makedirs(backup_dir) def recovery_py(backup_dir, current_dir): """ 将备份文件夹中的原文件按照目录结构恢复到原位置 :param backup_dir: :param current_dir: :return: """ os.chdir(backup_dir) print('INFO··········>切换目录:{}\n'.format(backup_dir)) for root, dirs, files in os.walk(backup_dir): for file in files: if file.endswith('.py'): src_file_bak = os.path.join(root, file) # print(src_file_bak) # 得到文件相对于编译文件的相对路径 relative_path = os.path.relpath(src_file_bak) # print('相对路径:', relative_path) rec_file_path = os.path.join(current_dir, relative_path) print('>RECOVERY<.py文件移动到原位置:{} ==> {}'.format(file, rec_file_path)) shutil.move(src_file_bak, rec_file_path) # py恢复过程中,删除pyd文件 del_pyd_path = rec_file_path.replace('.py', '.pyd') if os.path.exists(del_pyd_path): print('WARNING··········>×删除.pyd文件:', del_pyd_path) os.remove(del_pyd_path) del_so_path = rec_file_path.replace('.py', '.so') if os.path.exists(del_so_path): print('WARNING··········>×删除.pyd文件:', del_so_path) os.remove(del_so_path) os.chdir(current_dir) print('\nINFO··········>返回目录:', current_dir) def backup_py_file(file_path, backup_dir): """ 备份指定的文件(绝对路径)到新的文件夹 :param file_path: 备份文件绝对路径 :param backup_dir: 备份到的文件夹 :return: """ # 得到文件相对于编译文件的相对路径,例如 app\t_core.py 对应的相对路径如下: file_rel_path = os.path.relpath(file_path) # print('相对路径:', file_rel_path) # app\t_core.py # 截取相对路径中的文件名 # print('文件名:', os.path.basename(file_rel_path)) # t_core.py # print('文件路径:', os.path.dirname(file_rel_path)) # app # 文件相对路径中的目录部分 rel_dir_name = os.path.dirname(file_rel_path) # 如果备份目录下存在这个文件相对目录,则先进行创建 file_backup_dir = os.path.join(backup_dir, rel_dir_name) if not os.path.exists(file_backup_dir): os.makedirs(file_backup_dir) # 文件备份的绝对路径 file_backup_path = os.path.join(backup_dir, file_rel_path) print('<BACKUP> .py文件移动:{} ==> {}'.format(file_rel_path, file_backup_path)) if os.path.exists(file_backup_path): os.remove(file_backup_path) os.rename(file_path, file_backup_path) def encrypt_py(): """ 1、将需要编译的文件处理为列表; 2、setup编译 3、删除build临时目录 4、处理编译完成的文件 1、判断子目录如果不是Python包则进行recovery还原 2、删除.c文件 3、备份原.py文件到备份文件夹 4、处理.pyd名称,并将其移动回原.py文件目录 5、删除编译生成的目录 :return: """ if config['module_list']: # normpath转为系统的路径格式,获取指定文件 file_path_list = [os.path.normpath(os.path.join(current_dir, file)) for file in config['module_list'] if os.path.basename(file) not in config['exclude_file']] else: # 如果没有指定文件,则使用当前目录下的所有 file_path_list = [] if config['include_file']: # 指定文件不为空,从当前目录中,选取指定名称的所有文件 for root, dirs, files in os.walk(current_dir): for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) in config['include_file'] and os.path.isfile(file_path): file_path_list.append(file_path) else: # 指定文件为空,则指处理排除文件名 for root, dirs, files in os.walk(current_dir): if True in (map(lambda exclude_path: exclude_path in root, config['exclude_dir'])): print('INFO··········>跳过文件夹:', root) continue for file in files: file_path = os.path.join(root, file) if os.path.basename(file_path) not in config['exclude_file'] and os.path.isfile(file_path) and os.path.splitext(file_path)[1] == '.py': file_path_list.append(file_path) print('\nINFO··········>处理文件路径:{}\n'.format(file_path_list)) # 编译 try: # 批量编译 setup( name="StarMeow app", version='0.2', description='加密Python源码', author='StarMeow', author_email='starmeow@qq.com', url='http://blog.starmeow.cn', ext_modules=cythonize(file_path_list, language_level=3), # 指定模块列表,language_level使用Py3,具体未查证 script_args=["build_ext", "-b", config["build_output"], "-t", config["build_tmp"]] ) except ValueError as e: print(e) # 编译结束 # 删除build临时目录 build_tmp_path = os.path.join(current_dir, config['build_tmp']) print('\nWARNING··········>×删除build临时目录:{}\n'.format(build_tmp_path)) if os.path.exists(build_tmp_path): shutil.rmtree(build_tmp_path) for root, dirs, files in os.walk(current_dir): # print(root, dirs, files) # 判断子目录是否是Python包,如果不是,则进行错误标记,并结束子目录和父目录循环 error_flag = False for dir in dirs: dir_path = os.path.join(root, dir) # 判断程序所在目录的子目录不是包:不是工作目录;(不存在__init__.py文件,且排除文件夹的名字在路径中) if dir_path == current_dir \ or os.path.exists(os.path.join(dir_path, '__init__.py')) \ or True in map(lambda exclude_path: exclude_path in dir_path, config['exclude_dir']): # print('当前处理路径,通过:', dir_path) pass else: print('\n\n\n报错位置:{}\n除程序运行目录,子目录必须要为Python包!请检查,程序还原中···\n\n\n'.format(dir_path)) error_flag = True break # 解锁所有循环, if error_flag: recovery_py(backup_dir, current_dir) # 调用一次还原操作,备份后的py文件进行还原 break for file in files: # 处理文件 file_path = os.path.join(root, file) # print(file_path) if file_path in file_path_list: # 删除.c文件:t_core.c c_file_path = file_path.replace('.py', '.c') print('WARNING··········>×删除.c文件:', c_file_path) if os.path.exists(c_file_path): os.remove(c_file_path) # 备份原py文件 backup_py_file(file_path, backup_dir) # 重命名.pyd或.so文件:t_core.cp37-win_amd64.pyd --> t_core.pyd # 正则匹配名称是file.env.pyd格式的 if re.match(r"\S+\.\S+\.(pyd|so)", file): # print(root, dirs, files) file_path_name, file_extension = os.path.splitext(file_path) # file_path:t_core.cp37-win_amd64.pyd ==> t_core.cp37-win_amd64 , .pyd new_file_path = file_path_name.split('.')[0] + file_extension # t_core.pyd # print('RENAME----------第1次改名:', new_file_path) # 文件本应该存放的目录,也就是去掉编译新生成的目录 # os.path.normpath('/a/b') 在Windows下会转换为 '\\a\\b' # 如果原来的目录是包(有__init__.py文件),则会在编译目录中创建包名文件夹,即: # root/__init__.py 存在,则 root/app/b.py 编译后为 root/build_output/root/app/b.pyd # 请况一:Python包 \EncryptTest\build_output20200211113628\EncryptTest\t_test.pyd => \EncryptTest\t_test.pyd if os.path.exists(os.path.join(current_dir, '__init__.py')): build_rel_path = os.path.normpath('/{}/{}'.format(config['build_output'], current_dir_name)) # \build_output20200211113628\EncryptTest # 请况二:文件夹 \EncryptTest\build_output20200211114306\t_test.pyd => \EncryptTest\t_test.pyd else: print('INFO··········>程序所在目录为文件夹,不是Python包:', current_dir) build_rel_path = os.path.normpath('/{}'.format(config['build_output'])) # \build_output20200211113628 new_file_path = str(new_file_path).replace(build_rel_path, '') # print('RENAME----------第2次改名:', new_file_path) if os.path.exists(new_file_path): # 删除已存在的 os.remove(new_file_path) print('>MOVE> .pyd文件移动到目标位置:{} ==> {}'.format(file_path, new_file_path)) os.rename(file_path, new_file_path) # 删除编译生成的目录 build_output_path = os.path.join(current_dir, config['build_output']) print('\nWARNING··········>×删除编译目标目录:{}\n'.format(build_output_path)) if os.path.exists(build_output_path): shutil.rmtree(build_output_path) if __name__ == '__main__': if len(sys.argv) < 2: print('缺少参数,使用方法:程序放在项目下,可指定module_list需要加密的相对路径文件列表\n编译文件:python {0} build_ext\n恢复文件:python {0} recovery'.format(os.path.basename(__file__))) else: if sys.argv[1] == 'build_ext': encrypt_py() elif sys.argv[1] == 'recovery': recovery_py(backup_dir, current_dir) else: print('参数错误,需要在后面添加build_ext或recovery,表示编译或恢复') ``` ## 使用PyArmor加密 参考:https://pyarmor.readthedocs.io/zh/latest/usage.html # 使用License控制代码运行 限制用户只能在获得授权的机器上才能运行代码,就需要使用License控制。且只有经过编译后不可逆的代码才能进行License控制,即代码不能直观查看。 ## 加密算法分类 ### 对称加密 - 对称加密采用了对称密码编码技术,它的特点是文件**加密和解密使用相同的密钥**。 - 发送方和接收方需要持有**同一把密钥**,发送消息和接收消息均使用该密钥。 - 相对于非对称加密,对称加密具有**更高的加解密速度**,但双方都需要事先知道密钥,密钥在传输过程中可能会被窃取,因此对称加密采用了对称密码编码技术,它的特点是文件加密和解密使用相同的密钥。 常见的对称加密算法:DES,AES,3DES等等 ### 非对称加密 - 文件加密需要公开密钥(publickey)和私有密钥(privatekey)。 - 接收方在发送消息前需要事先生成公钥和私钥,然后将公钥发送给发送方。发送放收到公钥后,将**待发送数据用公钥加密**,发送给接收方。接收到收到数据后,用**私钥解密**。 - 在这个过程中,公钥负责加密,私钥负责解密,数据在传输过程中即使被截获,攻击者由于没有私钥,因此也无法破解。 - 非对称加密算法的**加解密速度低于对称加密算法**,但是**安全性更高**。 非对称加密算法:RSA、DSA、ECC等算法 ### 消息摘要 - 消息摘要算法可以验证信息是否被篡改。 - 在数据发送前,首先使用消息摘要算法生成该数据的签名,然后签名和数据一同发送给接收者。 - 接收者收到数据后,对收到的数据采用消息摘要算法获得签名,最后比较签名是否一致,以此来判断数据在传输过程中是否发生修改。 ## PyCrypto加密库 替换旧版的PyCrypto,使用`pip install pycryptodome`,这种情况下,所有的模块都在`Crypto`包下,必须避免同时安装 PyCrypto 和 PyCryptodome,因为它们会相互干扰。 独立PyCrypto的包,`pip install pycryptodomex`,在这种情况下,所有模块都安装在`Cryptodome`软件包下。PyCrypto和PyCryptodome可以共存。 ## 加解密Python实现 测试环境Windows,统一安装`pip install pycryptodome` ### DES加解密 全称为Data EncryptionStandard,即数据加密标准,是一种使用密钥加密的块算法 入口参数有三个:`Key、Data、Mode` - `Key`为7个字节共56位,是DES算法的工作密钥; - `Data`为8个字节64位,是要被加密或被解密的数据; - `Mode`为DES的工作方式,有两种:加密或解密 3DES(即Triple DES)是DES向AES过渡的加密算法, - 使用两个密钥,执行三次DES算法, - 加密的过程是`加密-解密-加密` - 解密的过程是`解密-加密-解密` #### DES代码实现 ```python import string import random import base64 from binascii import b2a_hex, a2b_hex, b2a_base64, a2b_base64 from Crypto.Cipher import DES def generate_random_str(num): # 生成随机字母数字字符串 key = ''.join(random.sample(string.ascii_letters + string.digits, num)) return key # DES加解密 class DESEncryptDecrypt(object): def __init__(self, key): self.__key = key.encode('utf-8') # 字符串key,使用时,需要转换为bytes # key=必须是8个字节长。 奇偶校验位将被忽略。类型:bytes/bytearray/memoryview self.des = DES.new(key=self.__key, mode=DES.MODE_ECB) # DES加密 def encrypt(self, data): """ str =(utf-8编码)=> bytes =(b' '补齐长度)=> bytes =(des加密)=> bytes =(b2a_xx编码)=> bytes =(utf-8解码)=> str :param data:明文字符串 :return:加密后的字符串 """ data = data.encode('utf-8') data += b' ' * (len(self.__key) - len(data) % len(self.__key)) # 转bytes类型长度不够填充空格 # plaintext : 要加密的数据。该长度必须是密码块长度的倍数。类型需为:bytes/bytearray/memoryview encrypt_data = self.des.encrypt(plaintext=data) # des编码 # print(base64.b64encode(encrypt_data).decode('utf-8')) # print(b2a_base64(encrypt_data).decode('utf-8')) # 等效上一种base64转换方法 encrypt_data = b2a_base64(encrypt_data).decode('utf-8') # base64编码 # encrypt_data = b2a_hex(encrypt_data).decode('utf-8') # 2进制转16进制 return encrypt_data.strip() # DES解密 def decrypt(self, encrypt_data): """ str =(utf-8编码)=> bytes =(a2b_xx解码)=> bytes =(des解码)=> bytes =(utf-8解码)=> str =(去掉空格)=> str :param encrypt_data: 加密后的字符串 :return: 明文字符串 """ decrypt_data = a2b_base64(encrypt_data.encode('utf-8')) # base64解码 # decrypt_data = a2b_hex(encrypt_data.encode('utf-8')) # 16进制转2进制 decrypt_data = self.des.decrypt(decrypt_data).decode('utf-8') # des解码 decrypt_data = decrypt_data.rstrip(' ') # 去除右边的空格 return decrypt_data if __name__ == '__main__': # print(generate_random_str(8)) # h618ZIw9 des = DESEncryptDecrypt(key='h618ZIw9') # key为8位字符串,转换为bytes长度相同 text = 'this is 明文字符串!123' print('原字符串:', text) des_en = des.encrypt(text) print('DES 加密:', des_en) des_de = des.decrypt(des_en) print('DES 解密:', des_de) print('解密后是否相同:', text == des_de) ``` 运行 ```bash 原字符串: this is 明文字符串!123 DES 加密: vFTeH57wHT4dzuvxRyXXpYOsxM/0wectKnL4/VxdrII= DES 解密: this is 明文字符串!123 解密后是否相同: True ``` ### AES加解密 高级加密标准(英语:Advanced EncryptionStandard,缩写:AES),这个标准用来替代原先的DES AES的区块长度固定为128 比特,密钥长度则可以是128,192或256比特(16、24和32字节) 大致步骤如下: 1、密钥扩展(KeyExpansion), 2、初始轮(Initial Round), 3、重复轮(Rounds),每一轮又包括:SubBytes、ShiftRows、MixColumns、AddRoundKey, 4、最终轮(Final Round),最终轮没有MixColumns。 #### 模式 AES只是个基本算法,实现AES有几种模式,主要有ECB、CBC、CFB和OFB这几种(其实还有个CTR): 1. ECB模式(电子密码本模式:Electronic codebook) ECB是最简单的块密码加密模式,加密前根据加密块大小(如AES为128位)分成若干块,之后将每块使用相同的密钥单独加密,解密同理。 2. CBC模式(密码分组链接:Cipher-block chaining) CBC模式对于每个待加密的密码块在加密前会先与前一个密码块的密文异或然后再用加密器加密。第一个明文块与一个叫初始化向量的数据块异或。 3. CFB模式(密文反馈:Cipher feedback) 与ECB和CBC模式只能够加密块数据不同,CFB能够将块密文(Block Cipher)转换为流密文(Stream Cipher)。 4. OFB模式(输出反馈:Output feedback) OFB是先用块加密器生成密钥流(Keystream),然后再将密钥流与明文流异或得到密文流,解密是先用块加密器生成密钥流,再将密钥流与密文流异或得到明文,由于异或操作的对称性所以加密和解密的流程是完全一样的。 #### AES代码实现 ```python import string import random import base64 from binascii import b2a_hex, a2b_hex, b2a_base64, a2b_base64 from Crypto.Cipher import AES def generate_random_str(num): # 生成随机字母数字字符串 key = ''.join(random.sample(string.ascii_letters + string.digits, num)) return key # AES加解密 class AESEncryptDecrypt(object): def __init__(self, key): self.__key = key.encode('utf-8') # key=必须为16、24或32个字节长(分别用于* AES-128 *,* AES-192 *或* AES-256 *)。仅对于``MODE_SIV``,它将为32、48或64个字节的两倍。 self.aes = AES.new(key=self.__key, mode=AES.MODE_ECB) # AES加密 def encrypt(self, data): """ str =(utf-8编码)=> bytes =(b' '补齐长度)=> bytes =(des加密)=> bytes =(b2a_xx编码)=> bytes =(utf-8解码)=> str :param data:明文字符串 :return:加密后的字符串 """ data = data.encode('utf-8') data += b' ' * (len(self.__key) - len(data) % len(self.__key)) # 转bytes类型长度不够填充空格 encrypt_data = self.aes.encrypt(data) # encrypt_data:bytes->bytes AES编码 # 因为AES加密时候得到的字符串不一定是ascii字符集的,输出到终端或者保存时候可能存在问题 # 可以把加密后的字符串转化为16进制字符串、base64编码等 encrypt_data = b2a_base64(encrypt_data).decode('utf-8') # base64编码 # encrypt_data = b2a_hex(encrypt_data).decode('utf-8') # 2进制转16进制 return encrypt_data.strip() # AES解密 def decrypt(self, encrypt_data): """ str [=(utf-8编码)=> bytes] =(a2b_xx解码)=> bytes =(des解码)=> bytes =(utf-8解码)=> str =(去掉空格)=> str :param encrypt_data: 加密后的字符串 :return: 明文字符串 """ decrypt_data = a2b_base64(encrypt_data.encode('utf-8')) # base64解码 # decrypt_data = a2b_hex(encrypt_data.encode('utf-8')) # 16进制转2进制 decrypt_data = self.aes.decrypt(decrypt_data).decode('utf-8') # des解码 decrypt_data = decrypt_data.rstrip(' ') # 去除右边的空格 return decrypt_data if __name__ == '__main__': # print(generate_random_str(32)) # iTVtgGcHYEvJj9BQLW40sxNmuA3fIUC8 aes = AESEncryptDecrypt(key='iTVtgGcHYEvJj9BQLW40sxNmuA3fIUC8') # key为16、24、32位字符串,转换为bytes长度相同 text = 'this is 明文字符串!123' print('原字符串:', text) aes_en = aes.encrypt(text) print('AES 加密:', aes_en) aes_de = aes.decrypt(aes_en) print('AES 解密:', aes_de) print('解密后是否相同:', text == aes_de) ``` 运行结果 ```bash 原字符串: this is 明文字符串!123 AES 加密: 2m/BQrfMMB56F/C6Vtc1nWCnbVvcpEtxDMrjFu5aMAc= AES 解密: this is 明文字符串!123 解密后是否相同: True ``` ### 合并DES、DES3、AES代码 ```python #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver1.0 @Author : StarMeow @License : (C) Copyright 2018-2020, blog.starmeow.cn @Contact : starmeow@qq.com @Software: PyCharm @File : 加解密实现.py @Time : 2020/2/15 10:50 @Desc : 使用Python实现DES、DES3、AES模式为MODE_ECB基础加解密 """ import string import random import base64 import sys from binascii import b2a_hex, a2b_hex, b2a_base64, a2b_base64 from Crypto.Cipher import AES, DES, DES3 def generate_random_str(num): # 生成随机字母数字字符串 key = ''.join(random.sample(string.ascii_letters + string.digits, num)) return key # DES、DES3、AES加解密 class AESDESEncryptDecrypt(object): def __init__(self, cipher_module, key): self.__key = key.encode('utf-8') # 字符串key,使用时,需要转换为bytes self.__key_size = len(self.__key) # 传入的key转换为bytes后的实际长度 # print(cipher_module, type(cipher_module)) # 指定加解密模块:<class 'module'> require_key_size = cipher_module.key_size # 使用这种加密方式,key的长度要求(元组和数字表示),类型为bytes if isinstance(require_key_size, tuple): if self.__key_size not in require_key_size: print('key长度错误,指定值:{},当前长度:{}'.format(require_key_size, self.__key_size)) sys.exit(1) else: if self.__key_size != require_key_size: print('key长度错误,指定值:{},当前长度:{}'.format(require_key_size, self.__key_size)) sys.exit(1) self.__block_size = cipher_module.block_size # 数据块的长度,即字符串转bytes后需要为该长度的倍数 self.cipher = cipher_module.new(key=self.__key, mode=cipher_module.MODE_ECB) # 创建该加密模块的实例,模式使用MODE_ECB模式 # 加密 def encrypt(self, data): """ str =(utf-8编码)=> bytes =(b' '补齐长度)=> bytes =(des加密)=> bytes =(b2a_xx编码)=> bytes =(utf-8解码)=> str :param data:明文字符串 :return:加密后的字符串 """ data = data.encode('utf-8') data += b' ' * (self.__block_size - len(data) % self.__block_size) # 转bytes类型长度不够填充空格 # plaintext : 要加密的数据。该长度必须是密码块长度的倍数。类型需为:bytes/bytearray/memoryview encrypt_data = self.cipher.encrypt(plaintext=data) # des编码 # print(base64.b64encode(encrypt_data).decode('utf-8')) # print(b2a_base64(encrypt_data).decode('utf-8')) # 等效上一种base64转换方法 encrypt_data = b2a_base64(encrypt_data).decode('utf-8') # base64编码 # encrypt_data = b2a_hex(encrypt_data).decode('utf-8') # 2进制转16进制 return encrypt_data.strip() # 解密 def decrypt(self, encrypt_data): """ str =(utf-8编码)=> bytes =(a2b_xx解码)=> bytes =(des解码)=> bytes =(utf-8解码)=> str =(去掉空格)=> str :param encrypt_data: 加密后的字符串 :return: 明文字符串 """ decrypt_data = a2b_base64(encrypt_data.encode('utf-8')) # base64解码 # decrypt_data = a2b_hex(encrypt_data.encode('utf-8')) # 16进制转2进制 decrypt_data = self.cipher.decrypt(decrypt_data).decode('utf-8') # des解码 decrypt_data = decrypt_data.rstrip(' ') # 去除右边的空格 return decrypt_data if __name__ == '__main__': # print(generate_random_str(16)) # h618ZIw9 # 支持的密码模块,cipher_module:DES(key长度:8)、DES3(key长度16, 24)、AES(key长度16, 24, 32) obj = AESDESEncryptDecrypt(cipher_module=AES, key='iTVtgGc1YEvJj9BQLW40sxNmuA3fIUC8') text = 'this is 明文字符串!123' print('原字符串:', text) text_en = obj.encrypt(text) print('加密:', text_en) text_de = obj.decrypt(text_en) print('解密:', text_de) print('解密后是否相同:', text == text_de) ``` 运行结果 ```bash 原字符串: this is 明文字符串!123 加密: M7PtTlKSgOARDjYhhbqaJLAUqjIoytZPEdiIwDfFIn4= 解密: this is 明文字符串!123 解密后是否相同: True ``` ### RSA加解密验签名 #### 加密(防泄露) 1. A生成公钥和私钥 2. A将公钥发给B 3. B使用公钥加密后,将密文发给A 4. A使用本地的私钥解密 原理:私钥存放在客户端本机,传输的内容是公钥和密文,及时中途被截获,也无法解密。 #### 签名(防篡改) 1. A生成公钥和私钥 2. A将公钥发给B 3. A将消息通过私钥签名,并将明文和签名发给B 4. B使用A的公钥将明文验签,验签通过,表明明文未被修改 原理:公钥公开,篡改了明文却没有私钥对明文进行重新签名,通过公钥验签可以对比明文是否已经被篡改。 如果公钥不是公开的,而是和明文、签名一起发出,那么被截获后就可以通过自己的私钥进行篡改公钥、明文以及签名。 #### RSA原理图解 ![BLOG_20200216_134708_58](/media/blog/images/2020/02/BLOG_20200216_134708_58.png "博客图集BLOG_20200216_134708_58.png") #### RSA代码实现 ```python import os import sys import json import binascii from binascii import a2b_base64, a2b_hex, b2a_base64, b2a_hex import pyasn1.error import rsa # pip install rsa """ 一般来说,加密分为两个部分: 1、非对称加密 2、对称加密 使用 对称加密 加密正文信息,使用 非对称加密 加密 对称加密 的密钥,然后发送加密数据(消息摘要和数字签名就不讨论了),这是正规的数据加密策略, 对称加密默认支持大数据分段加密策略,只需要从接口中完成加密即可,而且对称加密速度比非对称加密快很多,如果需要使用这个策略建议使用AES。 如果不愿意使用对称加密,只愿意使用RSA加密,那加解密速度就很慢,而且自己处理分段加密,因为RSA加密通常是117个字节就要分段(这个长度可能和密钥长度有关), 需要自己把数据变成N个117字节的数据段来完成加密,解密也需要自己完成字节拼装。 """ class RSAEncryptDecrypt(object): def __init__(self, pub_key, priv_key): if isinstance(public_key, str): # 如果传入的key是字符串,先将其编码 pub_key = pub_key.encode('utf-8') try: self.__pub_key = rsa.PublicKey.load_pkcs1(pub_key) # key为bytes类型,self.pub_key类型:<class 'rsa.key.PublicKey'> except pyasn1.error.PyAsn1Error as e: print('\n\n公钥加载错误,可能被修改:', e) sys.exit(1) except binascii.Error as e: print('\n\n公钥加载错误,可能被修改:', e) sys.exit(1) if isinstance(priv_key, str): priv_key = priv_key.encode('utf-8') try: self.__priv_key = rsa.PrivateKey.load_pkcs1(priv_key) # self.priv_key类型:<class 'rsa.key.PrivateKey'> except pyasn1.error.PyAsn1Error as e: print('\n\n私钥加载错误,可能被修改:', e) sys.exit(1) except binascii.Error as e: print('\n\n私钥加载错误,可能被修改:', e) sys.exit(1) # 公钥加密 def encrypt(self, data): """ 用 公钥 对明文字符串 加密成 密文base64编码字符串 :param data: 明文 :return: 密文 """ # print(self.pub_key, type(self.pub_key)) # encrypt_data = rsa.encrypt(data.encode('utf-8'), self.__pub_key) encrypt_data = b2a_base64(encrypt_data).decode('utf-8') return encrypt_data.strip() # 私钥解密 def decrypt(self, encrypt_data): """ 用 私钥 对密文base64编码字符串 解密成 明文字符串 :param encrypt_data: 密文 :return: 解密成功,返回明文内容;解密失败,返回False """ decrypt_data = a2b_base64(encrypt_data) try: decrypt_data = rsa.decrypt(decrypt_data, self.__priv_key).decode('utf-8') except rsa.pkcs1.DecryptionError: # 使用私钥解密失败 return False return decrypt_data # 私钥签名 def sign(self, data, hash_method): """ str =(utf-8编码)=> bytes =(rsa签名)=> bytes =(base64编码)=> bytes =(utf-8解码)=> str 使用SHA-1加密算法进行签名,也可以使用MD5,签名之后需要编好后输出 :param data: 明文 :param hash_method: 加密算法:'MD5', 'SHA-1', 'SHA-224', SHA-256', 'SHA-384' or 'SHA-512'. :return: 签名 """ sign_data = rsa.sign(data.encode('utf-8'), self.__priv_key, hash_method) sign_data = b2a_base64(sign_data).decode('utf-8') return sign_data.strip() # 公钥验签 def verify(self, data, sign_data, hash_method): """ rsa.verify()返回生成摘要算法名,即SHA-1 如果和传入的算发明相同,则验签通过 :param data: 明文 :param sign_data: 签名 :param hash_method: 加密算法 :return: 验签成功,返回True;验签失败,返回False """ sign_data = a2b_base64(sign_data) try: hash_m = rsa.verify(data.encode('utf-8'), sign_data, self.__pub_key) except rsa.pkcs1.VerificationError: # 使用公钥验签失败 return False return hash_m == hash_method def generate_rsa_key(tag=''): """ 生成公钥和私钥文件 """ if os.path.exists('{}pubkey.pem'.format(tag)) and os.path.exists('{}privkey.pem'.format(tag)): pass else: # pub_key, priv_key = rsa.newkeys(1024) # public_key = pub_key.save_pkcs1().decode('utf-8') # 与load_pkcs1(key)是互逆的 # private_key = priv_key.save_pkcs1().decode('utf-8') # print(public_key, private_key) pub_key, priv_key = rsa.newkeys(1024) public_key = pub_key.save_pkcs1() with open('{}pubkey.pem'.format(tag), 'wb') as f: f.write(public_key) private_key = priv_key.save_pkcs1() with open('{}privkey.pem'.format(tag), 'wb') as f: f.write(private_key) if __name__ == '__main__': generate_rsa_key() with open('pubkey.pem', 'rb') as f: public_key = f.read() # <class 'str'> with open('privkey.pem', 'r') as f: private_key = f.read() # <class 'str'> print('示例一:服务端生成密钥对,客户端的登录表单使用公钥加密,密文发送给服务端解密并验证帐密,服务端回复带签名的明文消息,客户端收到响应后验签,证明明文没被修改。') obj = RSAEncryptDecrypt(pub_key=public_key, priv_key=private_key) text = {'name': 'starmeow', 'pswd': 'blog.starmeow.com'} text = json.dumps(text) """ 一个中文3 bytes,一个英文数字1 bytes。 明文长度(bytes) <= 密钥长度(bytes)- 11 rsa.newkeys(key),key为256bits时,最长明文长度 = 32bytes(即256bits/8) - 11 = 21bytes RSA最长明文长度:key 512容纳53 bytes;key 1024容纳117 bytes """ print('明文:', text) print('\n请求:\n------客户端使用公钥加密,将密文发送给服务端') # 公钥加密 text_en = obj.encrypt(text) print('加密:', text_en) print('\n------服务端使用私钥解密,获取明文') # 私钥解密 text_de = obj.decrypt(text_en) if text_de is False: # 解密失败 print('使用私钥解密消息失败,不可读取原文内容【防泄漏】') else: print('只能使用私钥解密出原文内容') print('解密:', text_de) print('验证:', text == text_de) text = '登录成功!' hash_method = 'SHA-1' print('\n\n响应:\n------服务端使用私钥签名,将明文和签名发给客户端') # 私钥签名 text_sg = obj.sign(text, hash_method) print('签名:', text_sg) print('\n------客户端使用公钥验签,判断是否被修改') # 公钥验签 text_vf = obj.verify(text, text_sg, hash_method) if text_vf is False: print('使用公钥验证签名失败,消息可能被修改【防篡改】') else: print('签名验证成功,消息未被修改') print('明文:', text) print('-------------示例一结束--------------') print('\n\n示例二:同时防泄漏和防篡改。') generate_rsa_key('a_') print('生成A的密钥对,A:公钥 ===> B;') generate_rsa_key('b_') print('生成B的密钥对,B:公钥 ===> A。') print('\n从A中发送消息,使用B的公钥加密,A的私钥加签:') msg1 = 'A -> B' with open('b_pubkey.pem', 'rb') as f: b_public_key = f.read() # <class 'str'> with open('a_privkey.pem', 'r') as f: a_private_key = f.read() # <class 'str'> obj1_a = RSAEncryptDecrypt(pub_key=b_public_key, priv_key=a_private_key) msg1_en = obj1_a.encrypt(msg1) # A的密文 msg1_sg = obj1_a.sign(msg1, 'SHA-1') print('消息发送,A:密文+签名 ===> B') print('B收到A的密文和签名后,使用B的私钥解密,A的公钥验签') with open('a_pubkey.pem', 'rb') as f: a_public_key = f.read() # <class 'str'> with open('b_privkey.pem', 'r') as f: b_private_key = f.read() # <class 'str'> obj1_b = RSAEncryptDecrypt(pub_key=a_public_key, priv_key=b_private_key) msg1_de = obj1_b.decrypt(msg1_en) msg1_vf = obj1_b.verify(msg1_de, msg1_sg, 'SHA-1') if msg1_vf: # 验签成功 print('B验签成功,解密消息:', msg1_de) else: print('B验签失败') print('\n从B中发送消息,使用A的公钥加密,B的私钥加签:') msg2 = 'B -> A' with open('a_pubkey.pem', 'rb') as f: a_public_key = f.read() with open('b_privkey.pem', 'r') as f: b_private_key = f.read() obj2_b = RSAEncryptDecrypt(pub_key=a_public_key, priv_key=b_private_key) msg2_en = obj2_b.encrypt(msg2) msg2_sg = obj2_b.sign(msg2, 'SHA-1') print('消息发送,B:密文+签名 ===> A') print('A收到B的密文和签名后,使用A的私钥解密,B的公钥验签') with open('b_pubkey.pem', 'rb') as f: b_public_key = f.read() with open('a_privkey.pem', 'r') as f: a_private_key = f.read() obj2_a = RSAEncryptDecrypt(pub_key=b_public_key, priv_key=a_private_key) msg2_de = obj2_a.decrypt(msg2_en) msg2_vf = obj2_a.verify(msg2_de, msg2_sg, 'SHA-1') if msg2_vf: # 验签成功 print('A验签成功,解密消息:', msg2_de) else: print('A验签失败') print('-------------示例二结束--------------') ``` 运行结果 ```bash 示例一:服务端生成密钥对,客户端的登录表单使用公钥加密,密文发送给服务端解密并验证帐密,服务端回复带签名的明文消息,客户端收到响应后验签,证明明文没被修改。 明文: {"name": "starmeow", "pswd": "blog.starmeow.com"} 请求: ------客户端使用公钥加密,将密文发送给服务端 加密: S/SuvQMKGE5fzpH0NFPyyG6Pp2K0n572Ezy3V/aUNjwByqi+0N8VvMkLd5SVuuqZrspK8momZWgTqdzeWGDHIW9KpY+ikClgIJ12w/TYz2SIwgjkYKR+1iG/JFx+EGjnU2c8ZJMmlaMPdIkwtMvTypeN+Yd9jIfn/rFOZ4SRzWM= ------服务端使用私钥解密,获取明文 只能使用私钥解密出原文内容 解密: {"name": "starmeow", "pswd": "blog.starmeow.com"} 验证: True 响应: ------服务端使用私钥签名,将明文和签名发给客户端 签名: OBnAR6KShfLcUWfOanNMVHmL30OOl3IcKByDxM5gAtdUlaftwayKNs2HOzainG1sxHDPaNNaqwKGyJMJnoQ0YOLpYeG1zk/XxGu9DXhw4KXPZomaSmAHsJ7k/LfiGo9J5r3RwROWn1w+VTD4Z4wUlmOF3Fj/2/rVgsqjs5b2KJg= ------客户端使用公钥验签,判断是否被修改 签名验证成功,消息未被修改 明文: 登录成功! -------------示例一结束-------------- 示例二:同时防泄漏和防篡改。 生成A的密钥对,A:公钥 ===> B; 生成B的密钥对,B:公钥 ===> A。 从A中发送消息,使用B的公钥加密,A的私钥加签: 消息发送,A:密文+签名 ===> B B收到A的密文和签名后,使用B的私钥解密,A的公钥验签 B验签成功,解密消息: A -> B 从B中发送消息,使用A的公钥加密,B的私钥加签: 消息发送,B:密文+签名 ===> A A收到B的密文和签名后,使用A的私钥解密,B的公钥验签 A验签成功,解密消息: B -> A -------------示例二结束-------------- Process finished with exit code 0 ``` ## License生成与使用 ### 项目结构 ![BLOG_20200213_170718_22](/media/blog/images/2020/02/BLOG_20200213_170718_22.png "博客图集BLOG_20200213_170718_22.png") 使用`run.py`调用app包的文件运行,`run.py`应保持最简单代码 ```python # run.py(不编译、可读) #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver1.0 @Author : StarMeow @License : (C) Copyright 2018-2020, blog.starmeow.cn @Contact : starmeow@qq.com @Software: PyCharm @File : run.py @Time : 2020/2/9 12:50 @Desc : 程序入口程序,可读不编译 """ from app.t_main import run if __name__ == '__main__': run() ``` ```python # app/t_main.py(编译、不可读) #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver1.0 @Author : StarMeow @License : (C) Copyright 2018-2020, blog.starmeow.cn @Contact : starmeow@qq.com @Software: PyCharm @File : t_main.py @Time : 2020/2/8 23:53 @Desc : 主程序,需要进行编译不让读写,且需要对license进行校验 """ from app.t_core import get_node, get_platform from starmeow_cryptor import check_license def run(): if check_license(): print('程序继续运行!') print(get_node()) print(get_platform()) else: print('程序授权终止,退出!') ``` ```python # app/t_core.py(编译、不可读) #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver1.0 @Author : StarMeow @License : (C) Copyright 2018-2020, blog.starmeow.cn @Contact : starmeow@qq.com @Software: PyCharm @File : t_core.py @Time : 2020/2/8 23:53 @Desc : 程序运行核心代码,用于提供给t_main.py调用 """ import platform def get_platform(): '''获取操作系统名称及版本号''' return platform.platform() def get_node(): '''计算机的网络名称''' return platform.node() if __name__ == '__main__': pass ``` 以上三个文件都是很简单的示例,用于测试的,根据实际应用做参考调整。 程序交于服务器运行时,除去所有不必要加密的py以及程序入口py,其他代码均需要运行 `python starmeow_setup.py build_ext`编译所有代码成pyd或者so文件。 ![BLOG_20200213_170704_19](/media/blog/images/2020/02/BLOG_20200213_170704_19.png "博客图集BLOG_20200213_170704_19.png") 运行后将所有核心代码均进行编译,备份原py文件,将备份文件夹和`starmeow_setup.py`文件移动到安全的位置,当程序需要修改时,恢复到原位置,使用`python starmeow_setup.py recovery`进行还原。 ### 生产环境模拟测试 刚运行`python run.py`一般是没有`starmeow_license.cli`文件的,终端会有下面的提示 ```bash Error......license.lic文件不存在!如未获取License,请将 mTVDvOtRItCahettxCNJP7eXvBypKX/MxMpDEI8IUvg= 发给管理员获取授权。 程序授权终止,退出! ``` 将上面的代码放在未编译的`starmeow_cryptor.py`中 ![BLOG_20200213_170655_61](/media/blog/images/2020/02/BLOG_20200213_170655_61.png "博客图集BLOG_20200213_170655_61.png") 指定授权信息,直接运行`python starmeow_cryptor.py`,会在本地生成`starmeow_license.cli`文件,将其复制到服务器上`starmeow_cryptor.py`所在的目录中。 然后再运行`python run.py`就会得到如下结果了 ```bash 程序继续运行! StarMeow-VIP Windows-10-10.0.18362-SP0 ``` ### Python代码实现Ver0.1【starmeow_cryptor.py】 ```python #! /usr/bin/env python # -*- coding: utf-8 -*- """ @Version : Ver0.1 @Author : StarMeow @License : (C) Copyright 2018-2020, blog.starmeow.cn @Contact : starmeow@qq.com @Software: PyCharm @File : starmeow_cryptor.py @Time : 2020/2/11 13:50 @Desc : 加解密文本,生成License文件,客户端校验license.cli,该文件放在客户端上需进行加密,不可支持反编译 """ import string import random import json import os import datetime import socket import base64 from Crypto.Cipher import AES import netifaces """ Python库 :pip install pycrypto netifaces 说明: 1、AES使用的密钥保存在随机变量名中,不易被外部获取。 2、AES加密和解密字符串。 3、根据获取的mac地址明文AES生成密文,使用该密文及其他信息整合到json中,通过AES加密成密文,保存到License文件中。 4、其他程序调用校验License文件,如果校验通过则返回True,否则返回False。 1、判断硬件信息。 2、判断授权时间。 5、本文件运行到客户端时,需要对其编译,让其不可读,然后在程序入口非源码文件中放置判断程序。 # 验证license调用方法示例,需要关联starmeow_cryptor.py中的check_license from starmeow_cryptor import check_license # 检查生成的License是否正确 if check_license(): print('程序继续运行!') else: print('程序授权终止,退出!') """ # __all__属性,可用于模块导入时限制,如:from module import *,此时被导入模块若定义了__all__属性, # 则只有__all__内指定的属性、方法、类可被导入。若没定义,则导入模块内的所有公有属性,方法和类 。 __all__ = ['check_license'] def generate_random_str(num): # 生成随机字母数字字符串 key = ''.join(random.sample(string.ascii_letters + string.digits, num)) return key # secret_key = generate_random_str(32) # print('KEY:', secret_key, '(妥善保管,勿泄露!)') # 添加一段随机字符串的含义:代码加密后,导入包时不会自动显示该变量,防止从其他文件中import猜测获取该值 __random_str = generate_random_str(5) # 生成一个随机字符串 exec('__{0}_secret_key_{0} = "{1}"'.format(__random_str, "nctd7PpjhSkTHEmfOaxyZKsVY5M0IgXD")) # 使用随机字符串作为变量名,不易猜测的变量名 # 使用该变量: globals().get('__{0}_secret_key_{0}'.format(__random_str)) ,如果要替换变量名,本文件全文替换 __{0}_secret_key_{0} 字符串 lic_dir = os.getcwd() # 指定包含license的目录 lic_file = os.path.join(lic_dir, 'starmeow_license.lic') # 字符串补位 def add_to_16(v): # 不足16位并返回bytes while len(v) % 16 != 0: v = v + "\0" # return v.encode('utf-8') return v # 字符串AES加解密 class AESEncryptDecrypt(object): def __init__(self, key): self.__key = key self.__mode = AES.MODE_ECB # AES加密 def encrypt(self, data): """ str =(aes编码)=> bytes =(base64编码)=> bytes =(utf-8解码)=> str :param data: :return: """ data = add_to_16(data) # 字符串补位 <class 'str'> # print('补位:', data, '|end') cipher = AES.new(self.__key, self.__mode) encrypt_data = cipher.encrypt(data) # encrypt_data:<class 'bytes'> AES编码 encrypt_data = base64.b64encode(encrypt_data) # encrypt_data:<class 'bytes'> base64编码,参数为bytes类型 encrypt_data = encrypt_data.decode('utf-8') # encrypt_data:<class 'str'> 使用utf-8解码成字符串 return encrypt_data # AES解密 def decrypt(self, encrypt_data): """ str =(base64解码)=> bytes =(aes解码)=> bytes =(utf-8编码)=> str :param encrypt_data: :return: """ cipher = AES.new(self.__key, self.__mode) encrypt_data = base64.b64decode(encrypt_data) # <class 'bytes'> decrypt_data = cipher.decrypt(encrypt_data) # <class 'bytes'> decrypt_data = decrypt_data.decode('utf-8') # <class 'str'> decrypt_data = decrypt_data.rstrip('\0') return decrypt_data # MAC地址管理,返回明文的mac地址 class GetMacAddress(object): def __init__(self): self.ip = '' self.__mac = '' def get_ip_address(self): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(('114.114.114.114', 80)) self.ip = s.getsockname()[0] finally: s.close() def get_mac_address(self): # 根据指定的ip地址获取mac:00:50:56:c0:00:08 for i in netifaces.interfaces(): # print(i) addrs = netifaces.ifaddresses(i) try: # print(addrs[netifaces.AF_LINK], addrs[netifaces.AF_INET]) if_mac = addrs[netifaces.AF_LINK][0]['addr'] if_ip = addrs[netifaces.AF_INET][0]['addr'] if if_ip == self.ip: self.__mac = if_mac break except KeyError: pass def mac(self): self.get_ip_address() self.get_mac_address() return self.__mac # license管理工具,传递dict数据,生成密文到文件;校验密文返回True、False class LicenseManager(object): def __init__(self, lic_file, key, dict_data=None): if dict_data is None: dict_data = {} self.lic_file = lic_file self.dict_data = dict_data # 需要加密的dict类型数据 self.aes_crypt = AESEncryptDecrypt(key=key) def generator(self): data = json.dumps(self.dict_data) # print('原文json:', data, type(data)) encrypt_data = self.aes_crypt.encrypt(data) # print('加密:', encrypt_data) with open(self.lic_file, "w+") as lic: lic.write(encrypt_data) lic.close() print('生成License完成!请将生成的 {} 复制到客户端。'.format(os.path.basename(self.lic_file))) def calibrator(self): obj = GetMacAddress() mac = obj.mac() # 明文的mac地址 encrypt_code = self.aes_crypt.encrypt(mac) # AES加密后的mac if os.path.exists(self.lic_file): with open(self.lic_file) as lic: encrypt_data = lic.read() lic.close() else: print('Error......license.lic文件不存在!如未获取License,请将 {} 发给管理员获取授权。'.format(encrypt_code)) return False try: decrypt_data = self.aes_crypt.decrypt(encrypt_data) # print('解密:', decrypt_data) except Exception as e: # print(e) decrypt_data = '{}' dict_data = json.loads(decrypt_data) # print('原文dict:', dict_data, type(dict_data)) # 验证是否是本机mac的授权 if dict_data.get('unique_code') != encrypt_code: print('Error......非本机授权!请检查是否更换硬件,将 {} 发给管理员重新获取授权。'.format(encrypt_code)) return False # 终生授权 if dict_data.get('life_time') is True: return True # 非终生授权,在指定日期中可用 try: today_date = datetime.datetime.today() start_date = datetime.datetime.strptime(dict_data['start_date'], "%Y-%m-%d") end_date = datetime.datetime.strptime(dict_data['end_date'], "%Y-%m-%d") if dict_data.get('life_time') is False and start_date < today_date < end_date: return True else: print('Error......程序未在授权时间内使用!') return False except Exception as e: pass return False # 其他程序调用license校验 def check_license(): """ 专用于其他程序调用 :return: True or Flase,表明license是否检测通过 """ lic_manager = LicenseManager(lic_file, globals().get('__{0}_secret_key_{0}'.format(__random_str))) return lic_manager.calibrator() if __name__ == '__main__': # mac = '11:22:33:44:55' # 用户所在计算机的mac地址 encrypt_code = 'lXJ45GPb5n23peNLSS0EkJOlBHlZdCDSZZwSnLbTvSs=' # 用户发来的是加密后的唯一识别码(mac、主板ID、硬盘ID、CPU ID等),用于生成license data = { 'unique_code': encrypt_code, 'name': 'ProjectName', 'life_time': False, # 终生有效,如果为False,首先开始---结束时间 'start_date': '2020-2-12', # 开始时间 'end_date': '2020-10-11', # 结束时间 'create_date': datetime.datetime.now().strftime('%Y-%m-%d'), # 生成时间 } # 生成license lic_manager = LicenseManager(lic_file, globals().get('__{0}_secret_key_{0}'.format(__random_str)), data) # 生成license文件 lic_manager.generator() print('---' * 20) # 验证license lic_manager = LicenseManager(lic_file, globals().get('__{0}_secret_key_{0}'.format(__random_str))) print('验证解密过程,程序是否运行:', lic_manager.calibrator()) ``` ### py源代码编译前后import对比 `starmeow_cryptor.py`编译前 ![BLOG_20200213_170639_85](/media/blog/images/2020/02/BLOG_20200213_170639_85.png "博客图集BLOG_20200213_170639_85.png") 编译后 出现导入错误,但是程序能够正常运行。 ![BLOG_20200213_170633_97](/media/blog/images/2020/02/BLOG_20200213_170633_97.png "博客图集BLOG_20200213_170633_97.png") 盲猜可用的变量或者方法存在很大的难度。 ![BLOG_20200213_170623_41](/media/blog/images/2020/02/BLOG_20200213_170623_41.png "博客图集BLOG_20200213_170623_41.png") 也就是说保存密钥、算法等比较涉密的文件需要进行编译,且不让其反编译,达到隐藏代码的目的。 所以在`starmeow_cryptor.py`使用了随机动态变量名来保存AES密钥,应该还是比较可靠了吧。

很赞哦! (2)

文章交流

  • emoji
0人参与,0条评论

当前用户

未登录,点击   登录

站点信息

  • 建站时间:网站已运行2077天
  • 系统信息:Linux
  • 后台程序:Python: 3.8.10
  • 网站框架:Django: 3.2.6
  • 文章统计:256 篇
  • 文章评论:60 条
  • 腾讯分析网站概况-腾讯分析
  • 百度统计网站概况-百度统计
  • 公众号:微信扫描二维码,关注我们
  • QQ群:QQ加群,下载网站的学习源码
返回
顶部
标题 换行 登录
网站