起源

在学校创新实践课程上,老师希望我们在这个课程上完成一个有关量子加密(量子随机数)的项目。

经过一段时间的调研和思考,结合老师的建议,我还是决定做基于本地量子加密的私人存储网盘这样一个量子密码的项目。

整个项目需要实现的功能就是,首先对一个文件进行加密,加密的方式是,通过生成一个与文件字节数一致的量子随机数(物理量子随机数生成器件)作为加密的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) # 解除int转str的长度限制

# TODO: 1. 支持大文件的分片加密
# TODO: 2. 支持其他文件(不是UTF-8的文件)


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) # 将字符串转换成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) # 将字符串转换成Path对象
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' # 密钥文件路径
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) # 解除int转str的长度限制

# 没有进行文件的写入
# TODO: 1. 支持大文件的分片加密


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=len(content), byteorder='big').decode()
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  
# import io
from pathlib import Path
from secrets import token_bytes
from bypy import ByPy
# import threading
# import struct
import sys

# threadLock = threading.Lock() # 线程锁
sys.set_int_max_str_digits(0) # 解除int转str的长度限制


# TODO: 1. 支持大文件的分片加密
# TODO: 2. 由于大量的IO操作,所以速度很慢,需要优化
# FIXME 进行量子加密的过程中,只需要把生成随机数的函数改成量子随机数生成器即可
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=len(content), byteorder='big').decode()
return int.to_bytes(content_decrypted, length=content_length, byteorder='big') # 整数转字节串返回


# def write_string(string, path):
# threadLock.acquire() # 同步锁
# with open(path, 'wt') as f:
# json.dump(string, f)
# threadLock.release()


def encrypt_file(file_path):
path = Path(file_path) # 转化为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) # 转化为Path对象
key_path = Path(key_path) # 转化为Path对象
# key_path = path_encrypted.parent / 'key' # 密钥文件路径
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=len(content), byteorder='big').decode()
return int.to_bytes(content_decrypted, length=content_length, byteorder='big') # 整数转字节串返回


def encrypt_file(file_path):
path = Path(file_path) # 转化为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) # 转化为Path对象
key_path = Path(key_path) # 转化为Path对象
# key_path = path_encrypted.parent / 'key' # 密钥文件路径
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')
# 通过key的大小获取data_length的大小
# data_length = key.bit_length() // 8
# print(data_length) data_length = int.from_bytes(f4.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分
更新日志:

  1. 增加了自动搜索key的功能,不用手动输入密钥位置(密钥存储在固定密钥库)
  2. 更改了key、datalength、加密文件等文件的命名方式:文件名称[创建时间戳].文件类型
    如:test_1[1699444671-0686579].txttest_1[1699444671-0686579].keytest_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
# 把每个 key 都验证一遍,验证通过之间返回
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分
更新说明:

  1. 完成桌面端应用程序开发
  2. 修复单选框不能横向滚动
  3. 修复异常情况抛出
  4. 删除加密后产生的文件,防止占用空间
  5. 云盘存储从百度网盘迁移至阿里云服务器(alist本地存储驱动映射)
  6. 修复验证key函数bug
  7. 增加服务器在线判断,修复异常抛出报错
  8. 修复异常报错
  9. 更改默认密钥库存放位置
  10. 修复目录选中后取消bug
  11. 增加用户后端验证
  12. 优化登录回车确认事件
  13. 增加用户权限判断

增加的alist接口请求代码

Alist是一个支持多种存储的文件列表程序,具有以下功能

  1. 支持多种存储服务
  2. 网页版管理
  3. WebDAV 服务
  4. 在线播放和浏览
  5. 部署简便