当交付工程项目时,一般希望核心代码和数据不被公开,此时需要进行加密处理,并保证代码能够正确运行。
下面介绍一个我最近写的一个跨平台release版本发布工具,实现了txt文件、excel文件加密,并将python代码编译成so文件(Linux平台)。其中txt, excel文件加密使用了AES加密算法,在这些数据加密后代码要做一定的更改,即加密的数据在需要使用时解码,使用完再将数据加密放回去。众所周知,Python代码是很难做到完全保密的,本文使用cython,gcc将Python代码转换成C代码,然后编译成so文件,实现了相对较高的安全性,至少增加了反编译的成本。
以下是数据加解密的代码,包括AES加密、解密txt, excel文件,以及加密整个嵌套文件夹下的txt、excel文件等功能,注释比较清楚,请看代码:
"""
文件加解密工具
"""
import os
import csv
import xlrd
import xlwt
import hashlib
import base64
import random
import string
from Crypto.Cipher import AES
class Encryptor():
def __init__(self):
self.mode = AES.MODE_CBC
def encrypt(self, key, text):
"""
加密函数,如果text不是16的倍数(加密文本text必须为16的倍数),那就补足为16的倍数
:param text:
:return:
"""
sha384 = hashlib.sha384()
sha384.update(key.encode('utf-8'))
res = sha384.digest()
key = res[0:32]
iv = res[32:48]
cryptor = AES.new(key, self.mode, iv)
length = 16
count = len(text)
add = length - (count % length)
text = text + ('\0' * add).encode('utf-8')
self.ciphertext = cryptor.encrypt(text)
return base64.encodestring(self.ciphertext)
def write_encrypt_text(self, key, file_name):
"""
将一个文件加密并写回原文件
:param key: 加密密钥
:param file_name: 需要加密的文件名
"""
file_name = file_name.rstrip("\n").rstrip(" ")
file_object = open(file_name, 'r', encoding='utf-8')
encrypt_str = file_object.read()
file_object.close()
encrypt_str = encrypt_str.rstrip("\n").rstrip(" ")
e = self.encrypt(key, encrypt_str.encode('utf-8'))
file_object = open(file_name, 'w', encoding='utf-8')
file_object.write(e.decode('utf-8'))
file_object.close()
# pass
def make_key(self):
"""
随机产生一个32位密钥
:return:
"""
return ''.join(random.sample(string.ascii_letters + string.digits, 32))
def make_key_list(self, key_num):
"""
产生key_num个数量的32位随机密钥
:param key_num:
:return:
"""
return [''.join(random.sample(string.ascii_letters + string.digits, 32)) for i in range(key_num)]
def encrypt_files_in_folder(self, folder_name):
"""
加密一个文件夹下的所有文件
:param folder_name
:return: 文件名、密钥字典
"""
file_names = get_file_names(folder_name)
key_list = self.make_key_list(len(file_names))
file_key_dict = {}
for i, file_name in enumerate(file_names):
file_key_dict[file_name] = key_list[i]
self.write_encrypt_text(key_list[i], folder_name + '/' + file_name)
return file_key_dict
def encrypt_files_in_folder_with_dict(self, key_dict, folder_name):
"""
利用传入的key_dict解密一个文件夹下的文件
:param key_dict:
:param folder_name:
:return:
"""
file_names = get_file_names(folder_name)
for i, file_name in enumerate(file_names):
self.write_encrypt_text(key_dict.get(file_name), folder_name + '/' + file_name)
# pass
@staticmethod
def save_key_dict(key_dict, save_path):
"""
保存每个文件对应的密钥
:return:
"""
file_writer = open(save_path, 'a', encoding='utf-8')
for key, value in key_dict.items():
file_writer.write(key + ' ' + value)
file_writer.write('\n')
file_writer.close()
class Decryptor():
def __init__(self):
self.mode = AES.MODE_CBC
def decrypt(self, key, text):
"""
解密函数,利用密钥key解密text内容,加密时补足成16位的地方删除
:param key: 密钥
:param text: 需要解密的内容
:return:
"""
sha384 = hashlib.sha384()
sha384.update(key.encode('utf-8'))
res = sha384.digest()
key = res[0:32]
iv = res[32:48]
cryptor = AES.new(key, self.mode,iv)
plain_text = cryptor.decrypt(base64.decodestring(text))
return plain_text.rstrip('\0'.encode('utf-8'))
def write_decrypt_text(self, key, file_name):
"""
将文件解密并重新写回该文件
:param key: 密钥
:param file_name: 需要解密的文件名
:return:
"""
file_name = file_name.rstrip("\n").rstrip(" ")
file_object = open(file_name, 'r', encoding='utf-8')
decrypt_str = file_object.read()
file_object.close()
decrypt_str = decrypt_str.rstrip("\n").rstrip(" ")
e = self.decrypt(key, decrypt_str.encode('utf-8'))
file_object = open(file_name, 'w', encoding='utf-8')
file_object.write(e.decode('utf-8'))
file_object.close()
# pass
def decrypt_files_in_folder(self, key_dict, folder_name):
"""
利用key_dict解密一个文件夹下的所有文件
:param key_dict: 文件名、密钥字典
:param folder_name: 要解密的文件夹名
:return:
"""
file_names = get_file_names(folder_name)
for i, file_name in enumerate(file_names):
self.write_decrypt_text(key_dict.get(file_name), folder_name + '/' + file_name)
# pass
@staticmethod
def get_key_dict(key_dict_file):
"""
从文件中读取key_dict
:param key_dict_file:
:return:
"""
key_dict = {}
with open(key_dict_file, 'r', encoding='utf-8') as file:
for line in file:
line_list = line.strip().split(' ')
key_dict[line_list[0]] = line_list[1]
return key_dict
class CSVUtils:
@staticmethod
def write_csv(file_name, text_list):
"""
向csv文件写入数据
:param file_name: 需要写入的文件名
:param text_list: 列表形式的数据
:return:
"""
with open(file_name, 'w', encoding='utf-8') as csv_file:
csv_writer = csv.writer(csv_file, dialect='excel')
for t_l in text_list:
csv_writer.writerow(t_l)
@staticmethod
def read_csv(file_name):
"""读取csv文件"""
text_list = []
with open(file_name, 'r', encoding='utf-8') as csv_file:
csv_reader = csv.reader(csv_file, dialect='excel')
for row in csv_reader:
text_list.append(row)
return text_list
class ExcelUtils:
@staticmethod
def read_excel(file_name):
"""
读取excel文件,返回列表
:param file_name: 表格文件名
:return:
"""
excel_file = xlrd.open_workbook(file_name) # 读取Excel文件
sheet1 = excel_file.sheet_by_index(0) # 读取sheet1
row_value_list = []
for row_num in range(sheet1.nrows):
row_value = sheet1.row_values(row_num)
row_value_list.append(row_value)
return row_value_list
@staticmethod
def write_excel(text_list, save_file):
"""
写Excel文件
:param text_list: 需要写入表格的数据
:param save_file: 数据保存的文件名
:return:
"""
workbook = xlwt.Workbook(encoding='utf-8')
sheet = workbook.add_sheet(sheetname='Sheet1', cell_overwrite_ok=True)
for row_id, line in enumerate(text_list):
i = 0
for svalue in line:
sheet.write(row_id, i, svalue)
i = i + 1
workbook.save(save_file)
def get_file_names(dir_name):
"""
获取一个文件夹下的所有子文件名
:param dir_name: 文件夹名
:return:
"""
for root, _dir, file in os.walk(dir_name):
return file
def decrypt_csv_to_excel(key, csv_file_name):
"""
将加密的csv文件解密为excel文件
:param key: 密钥
:param csv_file_name: 需要解密的csv文件名
:return:
"""
# 将csv文件解密并写回csv文件
decryptor = Decryptor()
decryptor.write_decrypt_text(key, csv_file_name) # 最好不要创建解密的csv文件
# 读取解密后的csv文件
text_list = CSVUtils.read_csv(csv_file_name)
# 删除解密的csv文件
if os.path.exists(csv_file_name):
os.remove(csv_file_name)
# csv文件转换为excel
excel_utils = ExcelUtils()
excel_utils.write_excel(text_list, csv_file_name.replace('.csv', '.xlsx'))
def encrypt_excel_to_csv(key, excel_file_name):
"""
将excel文件加密为csv文件
:param key: 密钥
:param excel_file_name: 需要加密的excel文件名
:return:
"""
# 读取未加密的excel文件
excel_utils = ExcelUtils()
text_list = excel_utils.read_excel(excel_file_name)
# 删除未加密的excel文件
if os.path.exists(excel_file_name):
os.remove(excel_file_name)
# 转换为csv文件
CSVUtils.write_csv(excel_file_name.replace('.xlsx', '.csv'), text_list)
# 加密csv文件
encryptor = Encryptor()
encryptor.write_encrypt_text(key, excel_file_name.replace('.xlsx', '.csv'))
def recursive_encrypt(key_dict, path_list):
"""
对path_list中路径下的所有txt, excel数据加密,密钥随机产生
:param key_dict: 文件名:密钥字典
:param path_list: 需要加密的文件路径列表
:return:
"""
# 初始化加密器
enc = Encryptor()
for path in path_list:
base_path = os.path.abspath(path)
counter = 0
if os.path.isfile(base_path):
key = enc.make_key()
if os.path.splitext(base_path)[1] == '.txt':
enc.write_encrypt_text(key, base_path)
if os.path.splitext(base_path)[1] == '.xlsx':
encrypt_excel_to_csv(key, base_path)
key_dict[base_path] = key
counter += 1
if counter == len(path_list):
return
elif os.path.isdir(base_path):
dirs = [os.path.join(base_path, _dir) for _dir in os.listdir(base_path)]
recursive_encrypt(key_dict, dirs)
def recursive_encrypt_with_key(key_dict, path_list):
"""
对path_list中路径下的所有txt, excel数据加密,密钥由参数key_dict给定
:param key_dict: 文件名:密钥字典
:param path_list: 需要加密的文件路径列表
:return:
"""
# 初始化加密器
enc = Encryptor()
for path in path_list:
base_path = os.path.abspath(path)
counter = 0
if os.path.isfile(base_path):
if os.path.splitext(base_path)[1] == '.txt':
enc.write_encrypt_text(key_dict[os.path.relpath(base_path)], base_path)
if os.path.splitext(base_path)[1] == '.xlsx':
encrypt_excel_to_csv(key_dict[os.path.relpath(base_path)], base_path)
counter += 1
if counter == len(path_list):
return
elif os.path.isdir(base_path):
dirs = [os.path.join(base_path, _dir) for _dir in os.listdir(base_path)]
recursive_encrypt_with_key(key_dict, dirs)
使用以上代码需要依赖pycryto,安装使用以下命令:
pip install pycrypto
接下来是代码加密,就是将py文件编译成so,我的代码依赖python-devel,gcc,cython。
可以使用以下命令安装:
sudo apt-get install python-devel
sudo apt-get install gcc
pip install Cython
具体代码如下:
import os
import sys
import shutil
import time
from distutils.core import setup
from Cython.Build import cythonize
def get_py(base_path=os.path.abspath('.'), parent_path='', name='', excepts=(), copy_other=False, del_c=False, start_time=0.0):
"""
获取py文件的路径
:param base_path: 根路径
:param parent_path: 父路径
:param name: 文件夹名
:param excepts: 需要排除的文件
:param copy_other: 是否copy其他文件
:param del_c: 是否删除c文件
:param start_time: 程序开始时间
:return: py文件的迭代器
"""
full_path = os.path.join(base_path, parent_path, name)
for fname in os.listdir(full_path): # 列出文件夹下所有路径名称,筛选返回需要的文件名称
ffile = os.path.join(full_path, fname)
if os.path.isdir(ffile) and not fname.startswith('.'):
for f in get_py(base_path, os.path.join(parent_path, name), fname, excepts, copy_other, del_c):
yield f
elif os.path.isfile(ffile):
ext = os.path.splitext(fname)[1]
if ext == ".c":
if del_c and os.stat(ffile).st_mtime > start_time:
os.remove(ffile)
elif ffile not in excepts and os.path.splitext(fname)[1] not in('.pyc', '.pyx'): # 如果文件不在排除列表中,并且文件不是.c, .pyc, .pyx
if os.path.splitext(fname)[1] in('.py', '.pyx') and not fname.startswith('__'):
yield ffile
elif copy_other:
dst_dir = os.path.join(base_path, parent_path, name)
if not os.path.isdir(dst_dir):
os.makedirs(dst_dir)
shutil.copyfile(ffile, os.path.join(dst_dir, fname))
else:
pass
def build_codes(path_list):
"""
将路径列表下的文件编译成.so文件
:param path_list
:return:
"""
start_time = time.time()
for path in path_list:
if os.path.isdir(path): # 如果是文件夹,将so按照原路径编到对应位置
curr_dir = os.path.abspath(path)
parent_path = sys.argv[1] if len(sys.argv) > 1 else ""
setup_file = os.path.join(os.path.abspath('.'), __file__)
# 获取py列表
module_list = list(get_py(base_path=curr_dir, parent_path=parent_path, excepts=(setup_file), start_time=start_time))
try:
for module in module_list:
setup(ext_modules=cythonize(module), script_args=["build_ext", "-b", os.path.abspath(os.path.dirname(module))])
except Exception as ex:
print("Error: ", ex)
exit(1)
else:
module_list = list(get_py(base_path=curr_dir, parent_path=parent_path, excepts=(setup_file), copy_other=False, start_time=start_time))
module_list = list(get_py(base_path=curr_dir, parent_path=parent_path, excepts=(setup_file), del_c=True, start_time=start_time)) # 删除编译过程产生的c文件
elif os.path.isfile(path): # 如果是文件,直接编译到原位置
try:
setup(ext_modules=cythonize(path), script_args=["build_ext", "-b", os.path.abspath(os.path.dirname(path))])
except Exception as ex:
print("Error", ex)
exit(1)
if os.path.splitext(path)[1] == '.py':
c_path = path.replace('.py', '.c')
if os.path.exists(c_path) and os.stat(c_path).st_atime > start_time:
os.remove(c_path)
if os.path.exists('./build'): # 删除build过程产生的临时文件
shutil.rmtree('./build')
print("Complete! time:", time.time()-start_time, 's')
def delete_py(path_list):
"""
删除给定路径下的py文件
:param path_list: 需要删除的py文件路径列表,可以是文件夹名,也可以是文件名
:return:
"""
for path in path_list:
base_path = os.path.abspath(path)
counter = 0 # 文件删除计数器
if os.path.isfile(base_path) and os.path.splitext(base_path)[1] == '.py':
os.remove(base_path)
counter += 1
if counter == len(path_list):
return # 直到一个文件夹中的文件删除完退出递归
elif os.path.isdir(base_path):
dirs = [os.path.join(base_path, _dir) for _dir in os.listdir(base_path)]
delete_py(dirs)
最后是版本发布工具,调用了上述两个代码,具体如下:
import shutil
import os
from code_encryptor import build_codes, delete_py
from src.text_encryptor import *
# 拷贝文件
if os.path.exists('./release'):
print('存在先删除')
shutil.rmtree('./release')
shutil.copytree('源路径', '目标路径')
# 数据加密
text_path_list = ['需要加密的数据文件夹(可以是嵌套文件夹)或文件路径', '需要加密的数据文件夹(可以是嵌套文件夹)或文件路径',...]
key_dict = {}
recursive_encrypt(key_dict, text_path_list)
Encryptor.save_key_dict(key_dict, './密钥.txt') # 将随机生成的密钥保存在文件中
# 代码编译
code_path_list = ['需要加密的代码文件夹(可以是嵌套文件夹)或文件路径', '需要加密的代码文件夹(可以是嵌套文件夹)或文件路径',...]
build_codes(code_path_list) # 将指定路径下的.py文件编译成.so文件
delete_py(code_path_list) # 删除指定路径下的.py文件
上述代码的使用还有几点需要说明:
- 工作过程
将需要发布的数据和代码拷贝到release文件夹下,先将数据加密,再将代码编译成so(Linux平台),最后删除py源码。 - 拷贝文件代码块
该代码块将需要发布的代码单独拷贝到release文件夹,若是多平台使用,只需要拷贝一次,换平台时将上一平台产生的release代码数据移植到新平台,该部分代码注释。 - 数据加密代码块
该代码块将列表中给出的数据加密,并将密钥保存到文本文件中,多平台使用时数据也只需加密一次,多个平台重复加密将导致无法解密。换平台时该平台代码注释。 - 代码加密代码块
如果是多平台移植,在最后一个平台构建前请将delete_py(code_path_list)函数注释,该函数做删除源码处理,仅在所有代码加密工作完成时调用才能调用该函数。多平台使用情况下请将py代码在对应平台编译。
最后,我的程序是经过自己使用测试的,可能写的不那么优美,但是我很乐意和大家分享自己的东西,也希望朋友们能指出我程序中存在的问题,让我能够不断提升,为大家带来更多有用的东西。
本篇文章Python代码编译部分参考了这篇文章:python:让源码更安全之将py编译成so