Python3.6数据加密、代码加密——跨平台release版本发布工具

当交付工程项目时,一般希望核心代码和数据不被公开,此时需要进行加密处理,并保证代码能够正确运行。

下面介绍一个我最近写的一个跨平台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文件

上述代码的使用还有几点需要说明:

  1. 工作过程
    将需要发布的数据和代码拷贝到release文件夹下,先将数据加密,再将代码编译成so(Linux平台),最后删除py源码。
  2. 拷贝文件代码块
    该代码块将需要发布的代码单独拷贝到release文件夹,若是多平台使用,只需要拷贝一次,换平台时将上一平台产生的release代码数据移植到新平台,该部分代码注释。
  3. 数据加密代码块
    该代码块将列表中给出的数据加密,并将密钥保存到文本文件中,多平台使用时数据也只需加密一次,多个平台重复加密将导致无法解密。换平台时该平台代码注释。
  4. 代码加密代码块
    如果是多平台移植,在最后一个平台构建前请将delete_py(code_path_list)函数注释,该函数做删除源码处理,仅在所有代码加密工作完成时调用才能调用该函数。多平台使用情况下请将py代码在对应平台编译。

最后,我的程序是经过自己使用测试的,可能写的不那么优美,但是我很乐意和大家分享自己的东西,也希望朋友们能指出我程序中存在的问题,让我能够不断提升,为大家带来更多有用的东西。

本篇文章Python代码编译部分参考了这篇文章:python:让源码更安全之将py编译成so

猜你喜欢

转载自blog.csdn.net/hfutdog/article/details/80980110