程序简介

  • 编程语言:python3.6
  • 源文件:aes_encode.py
  • 可执行程序:aes_encode.exe

程序功能

实现对小于1GB的任何格式的文件进行AES加密

设计思路

  • 文件加密过程
  • 要求使用者提供key(解密时用来验证身份)
  • 生成256bit随机字符串(python中为二进制)作为AES加密密钥
  • 将文件二进制流读入内存,并进行AES加密
  • 加密后的文件名称为“加密_”+原文件名
  • 将加密所用密钥以及使用者key和文件hash值等信息进行异或加密之后存入data.dat
  • 文件解密过程
  • 读取data.dat信息进入内存
  • 根据data.dat信息判断文件是否是加密文件。
  • 对用户解密时输入的key进行校对,若与加密时输入的key相同,则进行解密

使用说明

  1. 若有python的环境,可打开cmd窗口,定位到requirements.txt同级目录,运行
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt

下载所需要的模块,然后双击py文件即可

  1. 或者直接运行exe文件
  2. 第一次打开文件时,会要求设置初始密码,初始密码的作用为导出信息,至关重要,其存储路径为注册表中的位置
计算机\HKEY_CURRENT_USER\Software\bylyl\encode
  1. 设置完初始密码后会进入主界面
  2. 首先点击选择文件或者在输入框中输入完整的程序路径以选择要加密的文件
  3. 设置密码,用来在解密时验证用户的身份
  4. 选择模式,有加密和解密两种,默认为加密
  5. 点击确认加密

解密的时候操作与加密时候的操作类似,选择要解密的文件,输入加密时候的key,点击确认解密

注意

  1. 若忘记密码,可点击导出密码,在正确输入初始密码之后会在当前文件夹生成info.dat文件,用记事本打开,其格式为:
{
  "加密文件hash":{
      "primary_hash": "源文件hash",
      "time": "加密时间",
      "aes_key": "加密所用aes密钥",
      "ensure_key": "加密时用户输入的key"
  }
}
  1. log.txt会记录对文件的操作过程
  2. data.dat是经过加密之后用来存储文件的重要信息,即info.dat的密文形式,若删除可能会造成无法解密

程序代码

import os
import hashlib
from cryptography.fernet import Fernet
import pickle
from datetime import datetime
import functools
import time
import multiprocessing
import json


# 定义获取文件hash时读取的大小
FILE_SIZE_HASH = 4096
# 定义一次读取文件的大小
FILE_SIZE_ONCE_READ = 1024*1024*2048
# 密码本的位置
INFO_PATH = os.path.join(os.path.dirname(__file__), 'data.dat')
# 用于给密码本异或加密的密钥
INFO_KEY = 'madebylyl'
# 日记位置
LOG_PATH = os.path.join(os.path.dirname(__file__), 'log.txt')
# 支持加密文件的最大大小
FILE_MAX_SIZE = 1024*1024*1024
# 解密信息的位置
DECODE_INFO_PATH = os.path.join(os.path.dirname(__file__), 'info.dat')


def console_log(text, path=LOG_PATH):
    with open(path, 'a') as f:
        f.write(datetime.now().strftime("%Y-%m-%d %H-%M") + ':' + text + '\n')

# 计时器
def time_count(text):
    def decorator(function):
        @functools.wraps(function)
        def wrapper(*args, **kwargs):
            text = function(*args, **kwargs)
            first = time.time()
            end = time.time()
            console_log("操作文件%s" % (args[0]))
            return text
        return wrapper
    return decorator

def get_file_hash(path):
    with open(path, 'rb') as f:
        data = f.read(FILE_SIZE_HASH)
    return hashlib.sha256(data).hexdigest()


def xor(de_data, key=INFO_KEY):
    count = 0
    en_data = b''
    for byte in de_data:
        new_byte = byte ^ ord(key[count % len(key)])
        en_data += bytes([new_byte])
        count += 1
    return en_data


def get_data(path=INFO_PATH, key=INFO_KEY):
    data = {}
    if not os.path.exists(INFO_PATH):
        print("We will create a file called 'data.dat' to storage infos, please remind not to delete it!")
        with open(path, 'wb') as f:
            f.write(xor(pickle.dumps(data)))
    else:
        with open(path, 'rb') as f:
            data = pickle.loads(xor(f.read()))
    return data

def get_decode_data(path=DECODE_INFO_PATH):
    data = get_data()
    with open(path, 'w') as f:
        f.write(json.dumps(data, indent=2, ensure_ascii=False))


class FileDispose:

    def __init__(self, path, ensure_key, data, aes_key="", file_new_name=""):
        # 判断是否是本人进行的操作
        self.ensure_key = ensure_key
        # 判断是加密还是解密,如果是加密则生成新的密钥,否则接受传入的密钥
        self.aes_key = self.get_key(aes_key)
        self.aes_f = Fernet(self.aes_key)
        # 根据原名称构造加密文件或解密文件的名称
        self.base_path, self.file_name = os.path.split(path)
        self.write_path = self.get_write_path(file_new_name)
        self.file_hash = get_file_hash(path)
        self.file_datas = self.read_file(path)
        self.write_file(data)


    def read_file(self, path):
        with open(path, 'rb') as f:
            while True:
                data = f.read(FILE_SIZE_ONCE_READ)
                if not data:
                    break
                else:
                    yield data

    def get_key(self, aes_key):
        # 判断是加密还是解密,如果是加密则生成新的密钥,否则接受传入的密钥
        if len(aes_key) == 0:
            # 过程为加密
            self.code = 'encode'
            return Fernet.generate_key()
        else:
            self.code = 'decode'
            return aes_key.encode()

    def encode(self, de_data):
        return self.aes_f.encrypt(de_data)

    def decode(self, en_data):
        return self.aes_f.decrypt(en_data)


    def write_file(self, data):
        f = open(self.write_path, 'wb')
        for file_data in self.file_datas:
            code = getattr(self, self.code)
            f.write(code(file_data))
        f.close()
        self.new_file_hash = get_file_hash(self.write_path)
        self.write_info(data)
        # 这里应该加上信息

    def write_info(self, data, info_path=INFO_PATH):
        # 写入的信息应该加密
        # 应该检查是否重复
        if self.code == "encode":
            info = {
                self.new_file_hash:
                    {
                        'primary_hash': self.file_hash,
                        'time': datetime.now().strftime("%Y-%m-%d %H-%M"),
                        'aes_key': self.aes_key.decode(),
                        'ensure_key': self.ensure_key,
                    }
            }
            data.update(info)

        elif self.code == "decode":
            data.pop(self.file_hash)

        with open(info_path, 'wb') as f:
            f.write(xor(pickle.dumps(data)))


    def get_write_path(self, file_new_name):
        # 如果是加密
        if self.code == 'encode':
            # 默认命名
            if len(file_new_name) == 0:
                new_name = "加密_" + self.file_name
                return os.path.join(self.base_path, new_name)
            else:
                return os.path.join(self.base_path, file_new_name)
        # 如果是解密
        if self.code == 'decode':
            if len(file_new_name) == 0:
                new_name = "解密_" + self.file_name
                return os.path.join(self.base_path, new_name)
            else:
                return os.path.join(self.base_path, file_new_name)





@time_count("加密")
def encode_file(path, ensure_key):
    data = get_data()
    file_hash = get_file_hash(path)
    if file_hash in data:
        text = "文件已经被加密!"
    elif file_hash in str(data):
        text = "该文件已经存在加密版本"
    else:
        text = "ok"
        t = multiprocessing.Process(target=FileDispose, args=(path, ensure_key, data))
        t.start()
    return text

@time_count("解密")
def decode_file(path, ensure_key):
    data = get_data()
    file_hash = get_file_hash(path)
    if not file_hash in data:
        text = "该文件没有被加密"
    else:
        file_info = data.get(file_hash)
        if ensure_key != file_info.get("ensure_key"):
            text = "密钥不正确"
        else:
            text = "ok"
            t = multiprocessing.Process(target=FileDispose, args=(path, file_info.get("ensure_key"), data, file_info.get("aes_key")))
            t.start()
    return text



# #######################UI界面############################
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QComboBox, QLabel, QApplication, QFileDialog, QMessageBox, QLineEdit, QInputDialog
from PyQt5.QtCore import QSettings

class MyUI(QWidget):

    def __init__(self):
        super().__init__()

        # 从注册表获取初始密码
        self.get_primary_key()
        self.initUI()

    def get_primary_key(self):
        settings = QSettings("bylyl", "encode")
        value = settings.value("primary_key", '/')
        if value == "/":
            # 初始化密码
            self.primary_key = self.init_key()
        else:
            self.primary_key = value

    def init_key(self):
        while True:
            while True:
                pwd1, ok = QInputDialog.getText(self, "初始密码", "!!!请输入初始密码")
                if not ok:
                    sys.exit()
                else:
                    if pwd1 == '' or pwd1 == '/':
                        self.show_error("密码不合法!")
                    else:
                        break
            while True:
                pwd2, ok = QInputDialog.getText(self, "初始密码", "!!!请确认初始密码")
                if ok:
                    if pwd2 != pwd1:
                        self.show_error("两次输入密码不一致")
                    else:
                        # 写入注册表
                        settings = QSettings("bylyl", "encode")
                        settings.setValue("primary_key", pwd1)
                        self.show_info("请牢记密码》{0}".format(pwd1))
                        return pwd1



    def initUI(self):
        self.btn1 = QPushButton('选择文件', self)
        self.btn1.move(20, 20)
        self.btn1.clicked.connect(self.choose_file)


        self.le = QLineEdit(self, )
        self.le.move(130, 20)
        self.le.resize(350, 30)
        self.le.setPlaceholderText("输入文件完整路径或在左边选择文件")

        # 输入密码框
        self.le1 = QLineEdit(self, )
        self.le1.move(130, 70)
        self.le1.resize(200, 30)
        self.le1.setPlaceholderText("请输入密码")
        self.le1.setEchoMode(self.le1.Password)

        self.le2 = QLineEdit(self, )
        self.le2.move(130, 110)
        self.le2.resize(200, 30)
        self.le2.setPlaceholderText("确认密码")
        self.le2.setEchoMode(self.le1.Password)


        # 设置标签
        self.info = QLabel(self)
        self.info.move(60, 160)
        self.info.setText("<b>模式</b>")
        # 设置下拉选项框
        self.box = QComboBox(self)
        self.box.addItems(("encode", "decode")),
        self.box.move(130, 155)
        self.box.currentIndexChanged.connect(self.change_mode)

        # 确认按钮
        self.btn2 = QPushButton("确认加密",self)
        self.btn2.resize(150,70)
        self.btn2.move(350, 80)
        self.btn2.clicked.connect(self.check_file)

        # 忘记密码的按钮
        self.btn3 = QPushButton("导出密码", self)
        self.btn3.resize(110,50)
        self.btn3.move(10, 74)
        self.btn3.clicked.connect(self.get_secret)


        self.setGeometry(500, 500, 600, 200)
        self.setWindowTitle("加密软件:made_by_lyl")
        self.show()

    def choose_file(self):
        fname = QFileDialog.getOpenFileName(self, "选择文件", '/home')

        if fname[0]:
            if os.path.getsize(fname[0]) > FILE_MAX_SIZE:
                self.show_error("当前只支持小于1GB文件")
            else:
                self.le.setText(str(fname[0]))

    def change_mode(self):
        text = self.box.currentText()
        if text == "encode":
            self.btn2.setText("确认加密")
        elif text == "decode":
            self.btn2.setText("确认解密")

    def check_file(self):
        mode = self.box.currentText()
        path = self.le.text()
        password1 = self.le1.text().strip()
        password2 = self.le2.text().strip()

        if len(password1) == 0:
            self.show_error("密码不能为空")

        elif password1 != password2:
            self.show_error("两次输入密码不一致")

        elif len(path) == 0:
            self.show_error("请输入路径")

        elif not os.path.isfile(path):
            self.show_error("路径不合法")

        
        elif os.path.getsize(path) > FILE_MAX_SIZE:
            self.show_error("当前只支持小于1GB文件")
            self.le.clear()

        else:
            if mode == "encode":
                text = encode_file(path, password1)
                if text.lower() != "ok":
                    self.show_error(text)
                else:
                    self.show_info("已进入后台加密")
                    self.show_im("请牢记密码:{0}".format(password1))

            elif mode == "decode":
                text = decode_file(path, password1)
                if text.lower() != "ok":
                    self.show_error(text)
                else:
                    self.show_info("已进入后台解密")


    def get_secret(self):
        text, ok = QInputDialog.getText(self, "导出密码", "请输入初始密码", QLineEdit.Password)
        if ok:
            if text == self.primary_key:
                get_decode_data()
                self.show_info("请查看文件info.dat")
            else:
                self.show_error("密码错误")


    def show_info(self, text):
        QMessageBox.information(self, "提示", text)

    def show_im(self, text):
        QMessageBox.critical(self, "重要", text)

    def show_error(self, text):
        QMessageBox.warning(self, "错误", text)

def main():
    # 打包时不加这一条pyinstaller会默认不支持多进程
    multiprocessing.freeze_support()
    try:
        app = QApplication(sys.argv)
        ex = MyUI()
        sys.exit(app.exec_())

    except Exception as e:
        console_log(e)

if __name__ == '__main__':
    main()