起源
在学校创新实践课程上,老师希望我们在这个课程上完成一个有关量子加密(量子随机数)的项目。
经过一段时间的调研和思考,结合老师的建议,我还是决定做基于本地量子加密的私人存储网盘这样一个量子密码的项目。
整个项目需要实现的功能就是,首先对一个文件进行加密,加密的方式是,通过生成一个与文件字节数一致的量子随机数(物理量子随机数生成器件)作为加密的KEY密钥,随后与文件进行异或加密,生成加密过后的文件,并上传网盘,而KEY密钥则是存储在本地。我这里使用的网盘是百度网盘,当然也可以是阿里云盘等等现成的网盘。当然后续的改进过程中,我还会尝试运用云服务云存储,自己搭建一个网盘,同时写出一个网盘客户端,打通加密文件和网盘上传下载的功能。
对于这个项目,我认为安全性是最大的特点。因为对于一次一密的逐bit异或运算,虽然是最简单的形式,但是在量子随机数和本地密钥存储的条件下,这样的方案似乎是不可能被破解的。由于是私人网盘,所以最关键的密钥是不需要考虑密钥分发中怎么保证安全不会泄露这个问题的。不过在后续,如果需要增加利用网盘进行文件的分享还是需要设计这个问题的解决,这也是后续需要考虑的问题。
设计过程
需要注意的是,这里的量子随机数生成是利用Python的随机数生成进行模拟,两者有本质的区别,程序生成的是伪随机数,而量子随机数生成器件是利用物理光学器件生成真随机数
实现一个简单的文件加密程序,由于读写都是用UTF-8编码,所以对于一些文件,就会报错。如docx或者图片和视频等二进制文件。就不支持这样的加密方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| import json from pathlib import Path from secrets import token_bytes import sys sys.set_int_max_str_digits(0)
def random_key(length): key = token_bytes(nbytes=length) key_int = int.from_bytes(key, 'big') return key_int
def encrypt(raw): raw_bytes = raw.encode() raw_int = int.from_bytes(raw_bytes, 'big') key_int = random_key(len(raw_bytes)) return raw_int ^ key_int, key_int
def decrypt(encrypted, key_int): decrypted = encrypted ^ key_int length = (decrypted.bit_length() + 7) // 8 decrypted_bytes = int.to_bytes(decrypted, length, 'big') return decrypted_bytes.decode()
def encrypt_file(path, key_path=None, *, encoding='utf-8'): path = Path(path) cwd = path.cwd() / path.name.split('.')[0] path_encrypted = cwd / path.name if key_path is None: key_path = cwd / 'key' if not cwd.exists(): cwd.mkdir() path_encrypted.touch() key_path.touch() with path.open('rt', encoding=encoding, errors='ignore') as f1, \ path_encrypted.open('wt', encoding=encoding, errors='ignore') as f2, \ key_path.open('wt', encoding=encoding, errors='ignore') as f3: encrypted, key = encrypt(f1.read()) json.dump(encrypted, f2) json.dump(key, f3)
def decrypt_file(path_encrypted, key_path=None, *, encoding='utf-8'): path_encrypted = Path(path_encrypted) cwd = path_encrypted.cwd() / path_encrypted.name.split('.')[0] path_decrypted = cwd / 'decrypted' if not path_decrypted.exists(): path_decrypted.mkdir() path_decrypted /= path_encrypted.name path_decrypted.touch() if key_path is None: key_path = cwd / 'key' with path_encrypted.open('rt', encoding=encoding, errors='ignore') as f1, \ key_path.open('rt', encoding=encoding, errors='ignore') as f2, \ path_decrypted.open('wt', encoding=encoding, errors='ignore') as f3: decrypted = decrypt(json.load(f1), json.load(f2)) f3.write(decrypted)
|
接着修改以上的代码,利用二进制文件的读写形式,实现了图片视频等文件的加解密,但是这里只是实现了简单的文件解密,还没有把加密生成的随机数密钥存储下来,写进文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| from secrets import token_bytes import sys sys.set_int_max_str_digits(0)
def random_key(length): key = token_bytes(nbytes=length) key_int = int.from_bytes(key, 'big') return key_int def encrypt(content): key_int = random_key(len(content)) content_encrypted = int.from_bytes(content, 'big') ^ key_int return content_encrypted, key_int def decrypt(content_length, content_encrypted, key_int): content_decrypted = content_encrypted ^ key_int return int.to_bytes(content_decrypted, length=content_length, byteorder='big') def main(): with open("video_test.mkv", 'rb') as fp: content = fp.read() content_encrypted, key_int = encrypt(content) print("加密完成!") content_decrypted = decrypt(len(content), content_encrypted, key_int) print("解密完成!") with open("video_decrypted.mkv", 'wb') as f: f.write(content_decrypted) if __name__ == '__main__': main()
|
随后更改上面的代码实现了,把密钥、数据长度写入文件,保存成字符串文件。
接着就是根据上面的代码,增加与百度网盘的交互,实现上传网盘和从网盘上下载。这里我使用的是python的第三方库bypy,用于实现与百度网盘的关联。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| import json
from pathlib import Path from secrets import token_bytes from bypy import ByPy
import sys
sys.set_int_max_str_digits(0)
def random_key(length): key = token_bytes(nbytes=length) key_int = int.from_bytes(key, 'big') return key_int def encrypt(content): key_int = random_key(len(content)) content_encrypted = int.from_bytes(content, 'big') ^ key_int return content_encrypted, key_int def decrypt(content_length, content_encrypted, key_int): content_decrypted = content_encrypted ^ key_int return int.to_bytes(content_decrypted, length=content_length, byteorder='big')
def encrypt_file(file_path): path = Path(file_path) dir_path = path.parent / f'{str(path.stem)}_key' dir_path.mkdir(exist_ok=True) encrypted_file_path = dir_path / path.name key_path = dir_path / 'key' data_length_path = dir_path / 'data_length' data = path.read_bytes() data_length = len(data) data_encrypted, key = encrypt(data) with encrypted_file_path.open('wt') as f1, \ key_path.open('wt') as f2, \ data_length_path.open('wt') as f3: json.dump(data_encrypted, f1) json.dump(key, f2) json.dump(data_length, f3) return str(encrypted_file_path) def decrypt_file(encrypted_file_path, key_path): path_encrypted = Path(encrypted_file_path) key_path = Path(key_path) data_length_path = key_path.parent / 'data_length' dir_path = path_encrypted.parent / f'{str(path_encrypted.stem)}_decrypted' dir_path.mkdir(exist_ok=True) decrypted_file_path = dir_path / path_encrypted.name if not path_encrypted.exists() or not key_path.exists() or not data_length_path.exists(): print("加密文件不存在!") return 0 if not key_path.exists(): print("密钥文件不存在!") return 0 with path_encrypted.open('rt') as f1, \ key_path.open('rt') as f2, \ decrypted_file_path.open('wb') as f3, \ data_length_path.open('rt') as f4: decrypted = decrypt(json.load(f4), json.load(f1), json.load(f2)) f3.write(decrypted) path_encrypted.unlink() return 1 def main(): while True: bp = ByPy() choose = input("请选择:1.上传文件 2.下载文件 3.退出\n") if choose == '1': path = input("请输入文件路径:") print("正在加密中...") file_path = encrypt_file(path) if file_path: print("加密成功!") else: print("加密失败!") return 0 print("正在上传中...") bp.upload(file_path) print("上传成功!") elif choose == '2': print("文件列表:") print(bp.list()) file_name = input("请输入下载文件名:") path = input("请输入文件下载位置:") key_path = input("请输入本地密钥文件位置:") print("正在下载中...") bp.downfile(file_name, path) print("下载成功!") print("正在解密中...") decrypt_file(path + '\\' + file_name, key_path) print("解密成功!") elif choose == '3': print("退出成功!") return 1 else: print("输入错误!") if __name__ == '__main__': main()
|
其实写到这里,基本上的功能已经能够实现了,但是在测试的过程中发现,对于小文件加解密和上传下载的速度很快,但是文件稍微大一点,例如5mb的图片,整个过程就十分缓慢。
分析过程发现,文件卡在==正在加密中==的阶段,再仔细分析,确定就是文件写入的速度太慢,包括Key、加密后的文件的写入。
1
| 由于一次一密的加密形式,导致了当需要加密的文件很大时,key的长度也随之增加到很大,如果使用wt、rt的形式进行写和读,整个IO操作就会十分缓慢,这是整个程序最大的弊端。不过好在key和加密后的文件都是int类型的整数,我们完全可以转换成二进制的形式进行存储,而对于wb、rb二进制形式的读写,IO操作就会快得多,所以这是修改的方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| def encrypt(content): key_int = random_key(len(content)) content_encrypted = int.from_bytes(content, 'big') ^ key_int return content_encrypted, key_int def decrypt(content_length, content_encrypted, key_int): content_decrypted = content_encrypted ^ key_int return int.to_bytes(content_decrypted, length=content_length, byteorder='big') def encrypt_file(file_path): path = Path(file_path) dir_path = path.parent / f'{str(path.stem)}_key' dir_path.mkdir(exist_ok=True) encrypted_file_path = dir_path / path.name key_path = dir_path / 'key' data_length_path = dir_path / 'data_length' data = path.read_bytes() data_length = len(data) data_encrypted, key = encrypt(data) with encrypted_file_path.open('wb') as f1, \ key_path.open('wb') as f2, \ data_length_path.open('wb') as f3: byte_array = data_encrypted.to_bytes((data_encrypted.bit_length() + 7) // 8, byteorder='big') f1.write(byte_array) byte_array = data_length.to_bytes((data_length.bit_length() + 7) // 8, byteorder='big') f3.write(byte_array) byte_array = key.to_bytes((key.bit_length() + 7) // 8, byteorder='big') f2.write(byte_array) return str(encrypted_file_path) def decrypt_file(encrypted_file_path, key_path): path_encrypted = Path(encrypted_file_path) key_path = Path(key_path) data_length_path = key_path.parent / 'data_length' dir_path = path_encrypted.parent / f'{str(path_encrypted.stem)}_decrypted' dir_path.mkdir(exist_ok=True) decrypted_file_path = dir_path / path_encrypted.name if not path_encrypted.exists() or not key_path.exists(): print("加密文件不存在!") return 0 if not key_path.exists(): print("密钥文件不存在!") return 0 with path_encrypted.open('rb') as f1, \ key_path.open('rb') as f2, \ decrypted_file_path.open('wb') as f3, \ data_length_path.open('rb') as f4: data_encrypted = int.from_bytes(f1.read(), byteorder='big') key = int.from_bytes(f2.read(), byteorder='big') decrypted = decrypt(data_length, data_encrypted, key) f3.write(decrypted) path_encrypted.unlink() return 1
|
利用二进制类型来读写文件,大大提升了整个程序的执行效率,对于大文件在加密后的加密文件和key文件写入速度慢的问题得到了解决。
与此同时,我也在进行图形化界面的实现,希望能够写出一个桌面程序,来完成这个项目。对于桌面程序的实现,我也经过了长时间的网上调研和检索。最终确定用python的flet框架进行桌面应用的开发。 flet框架是基于谷歌的flutter开发的,所以用起来很容易上手,而且优点在于开发速度快、效率高。 以下是我简单写的一个demo:
目前已经实现了文件的加密和上传功能,接下来还需要实现文件的下载和解密功能,在这个过程中还需要联系云端网盘程序,也许之后会将这个网盘程序换成自己搭建的云端存储。
2023年11月8日 21点31分
更新日志:
- 增加了自动搜索key的功能,不用手动输入密钥位置(密钥存储在固定密钥库)
- 更改了key、datalength、加密文件等文件的命名方式:
文件名称[创建时间戳].文件类型
如:test_1[1699444671-0686579].txt
、test_1[1699444671-0686579].key
、test_1[1699444671-0686579].length
增加的两个函数,寻找密钥路径和验证密钥是否正确
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| def find_key_path(download_file_path): download_file_name = download_file_path.name file_name = download_file_name.split('[')[0] time_id = download_file_name.split(']')[-2].split('[')[-1] dir_keyword = f"{file_name}*{time_id}*" key_keyword = f"{file_name}*{time_id}*.key" key_store_path = Path("C:\\desktop\\KEY_STORE") dirs = list(key_store_path.glob(dir_keyword)) if len(dirs) == 0: print("没有找到密钥文件夹") return 0
for di in dirs: if di.is_dir(): key_path = di.glob(key_keyword) key_paths = list(key_path) if not key_paths: print("没有找到密钥文件!") return 0 for key in key_paths: if verify_key(key, download_file_path): print("找到密钥:" + str(key)) return key print("没有找到密钥文件!!") return 0
def verify_key(key_path: Path, download_file_path: Path): if key_path.stat().st_size == download_file_path.stat().st_size: print("密钥文件大小正确!") return 1 print("密钥文件大小不正确!") return 0
|
2023年11月16日 19点00分
更新说明:
- 完成桌面端应用程序开发
- 修复单选框不能横向滚动
- 修复异常情况抛出
- 删除加密后产生的文件,防止占用空间
- 云盘存储从百度网盘迁移至阿里云服务器(alist本地存储驱动映射)
- 修复验证key函数bug
- 增加服务器在线判断,修复异常抛出报错
- 修复异常报错
- 更改默认密钥库存放位置
- 修复目录选中后取消bug
- 增加用户后端验证
- 优化登录回车确认事件
- 增加用户权限判断
增加的alist接口请求代码
Alist是一个支持多种存储的文件列表程序,具有以下功能
- 支持多种存储服务
- 网页版管理
- WebDAV 服务
- 在线播放和浏览
- 部署简便