0
点赞
收藏
分享

微信扫一扫

安全的数据库图形管理工具(2):三个问题

上次虽然实现了加密传输,也通过了简单的测试,但是我在进一步测试时发现了一些问题,下面我就来从根本上解决这些问题,在解决这些问题之前,首先附上之前文章的链接。

​​安全的数据库图形管理工具(1):准备密钥​​

加密长字节序列

之前我只是用两个短字节序列来进行密钥测试,那两个字节序列都比较短,可是我在进行进一步测试的时候发现长字节序列无法被加密,不相信的话我可以尝试一下。

为了进行简单的测试,我就把客户端代码要发送的字节改成特别长的而已。

1import rsa
2import socket
3public_key = open("server_public_key.pem", "rb").read() # 打开公钥文件并读取
4public_key = rsa.PublicKey.load_pkcs1(public_key) # 加载公钥
5private_key = open("self_private_key.pem", "rb").read() # 打开私钥文件并读取
6private_key = rsa.PrivateKey.load_pkcs1(private_key) # 加载私钥
7# 用公钥加密要发送的数据
8send_encode_data = rsa.encrypt(b"123456789012345678901234567890", public_key)
9s = socket.socket() # 创建套接字对象
10host = "111.230.108.44" # 服务器的IP地址
11port = 1234 # 服务器程序的端口号
12s.connect((host, port)) # 连接服务器
13s.send(send_encode_data) # 发送已经加密的数据
14receive_encode_data = s.recv(128) # 接受已经加密的数据
15print(rsa.decrypt(receive_encode_data, private_key).decode()) # 解密接收到的加密数据并输出

要加密的节已经够长了,下面我们来看看运行情况。

安全的数据库图形管理工具(2):三个问题_字节序

运行之后发现出问题了,稍微翻译一下出错信息:消息需要30个字节,但是只有21个字节的空间。我们首先来想一个问题,为什么一次只能加密21个字节?21从何而来?

我直接给出结论吧,可以被加密的字节长度与密钥的比特数呈线性正相关,我们有如下公式:
安全的数据库图形管理工具(2):三个问题_数据_02
我上次设置的密钥比特数是256,最大长度也就是256/8-11=21。21就是这么来的,超过了这个长度就会出现问题。如何解决这样的问题其实很简单,密钥比特数设置一个很大的数就行了。但是这样治标不治本,万一加密的数据比那个很大的数还要长怎么办?还是很简单,我把这一个长字节序列分成一块一块的,每一块20个字节,在解密的时候,我们也一块一块的解密,然后拼接起来就行。

1for i in range(0, len(cmd), 20):
2 sock.send(rsa.encrypt(cmd[i:i + 20], public_key))

其中cmd是要加密的字节序列,sock是一个套接字对象,这就是一个先加密后发送的过程,有些人会有一个问题,发送过去一定要让对方接收吧,不可能只发送不接收,既然发送需要分成一块一块的,我接收也应该是一块一块的,发送20个长度的字节序列,接收应该也是接收20个长度的字节序列啊?!如果真的是这样,那么最后一块该如何接收?因为最后一块几乎不可能是20个字节长度,比如我有45个字节序列需要发送,两个20发完之后最后发一个5个字节的块。就在这个时候,我必须要求接收缓冲区只能接5个字节,如果多了就会出现问题。因为接收缓冲区如果依旧是用20个字节从接收缓冲区读取数据,就会出现这样一种情况,接收到的数据也是20个字节,前5个是最后一次发送的数据,后15个是第二次发送的20个字节的后15个字节。如何解决这个问题将在后面讨论,因为现在即使解决了这个问题,接收方解密依旧还是有问题。RSA加密算法规定,只要长度在合法的范围内,我们有如下公式:
安全的数据库图形管理工具(2):三个问题_客户端_03
通过上面的公式我们可以看出在其他条件不变的情况下,密文长度与明文长度无关,不管明文多长,密文的字节长度固定不变,在我这里就是256/8=32,所以我要求接收方每次接收32个字节长度。

TCP粘包

在上面我稍微提到了一个问题,假设我有45个字节序列需要发送,两个20发完之后最后发一个5个字节的块。就在这个时候,我必须要求接收缓冲区只能接5个字节,如果多了就会出现问题。因为接收缓冲区如果依旧是用20个字节从接收缓冲区读取数据,就会出现这样一种情况,接收到的数据也是20个字节,前5个是最后一次发送的数据,后15个是第二次发送的20个字节的后15个字节,我们称这种情况叫粘包。下面我来重点解决这个问题,为什么会出现粘包?因为发送和接收都太快了,导致缓冲区没有刷新,最简单的办法我们就是使用sleep给缓冲区一个刷新的时间,但这样做性能太差了,我们暂时先想一下有没有更好的办法,如果我们规定发送多少个字节就接收多少个字节,这样就可以获得一个平衡,从而不会出现接收到多余的无用的数据。现在最关键的问题出来了,我怎么把发送要发送的字节长度告诉接收方?接收方又该如何接收?接收多少个字节?如果我就简单的把长度这个整数使用str转换成字符串,然后编码成字节,这个字节的长度是不确定的,接收方设置接收字节数就陷入了麻烦,如何把长度给固定住?为此我们可以使用模块struct,struct可以把一个整数压缩成四个字节,现在又出现了一个问题,4个字节存放的整数有范围,万一越界怎么办?很简单,我再做一层封装,先创建一个报头,再把报头转成字节,然后把字节报头的长度用struct压缩打包发过去就行了。

缓冲区溢出

在网络编程中,如果服务器发送速度和客户端接收速度不匹配,假设服务器发送太快,客户端接收的有点慢,默认情况下服务器并不会配合客户端的接收速度,而是会一股脑的把数据丢在缓冲区,分块发送按理来说没毛病,但是如果不给服务器刷新缓冲区的机会,依旧会造成溢出。在python网络编程中,我一时半伙找不到清理套接字缓冲区的办法,只能sleep将就了。

一个简单的SSH远程控制终端

下面我通过编写一个简单的SSH远程控制终端来进行进一步测试,首先说一下设计思路。我们要求客户端输入命令发送过去,服务器返回命令执行结果给客户端,数据传输一律是非对称加密。下面我详细的说一下客户端程序与服务器程序的设计细节。

客户端

客户端的实现非常简单,首先读取自己的私钥和服务器的公钥并赋值给两个变量。然后连接服务器,连好之后就是开始输入命令,输入完成之后就将命令分块加密发送,发送完成之后就接收对方响应过来的报头长度,然后接收报头,之后就开始接收真实数据,然后把接收的数据解密即可。具体细节我就不讲了,直接给出源代码。

1import socket
2import rsa
3import json
4import struct
5public_key = rsa.PublicKey.load_pkcs1(open("server_public_key.pem", "rb").read()) # 加载公钥
6private_key = rsa.PrivateKey.load_pkcs1(open("self_private_key.pem", "rb").read()) # 加载私钥
7sock = socket.socket() # 创建套接字对象
8sock.connect(('', 8080)) # 连接服务器
9while True:
10 cmd = input().strip().encode() # 1.输入命令 2.去除无效字符 3.编码成字节序列
11 if not cmd: # 如果输入的命令为空,继续下一次循环
12 continue
13 elif cmd == b'logout': # 如果命令是logout就结束循环
14 break
15 for i in range(0, len(cmd), 20): # 分块加密,一块20个字节
16 sock.send(rsa.encrypt(cmd[i:i + 20], public_key)) # 发送加密的数据
17 head = sock.recv(4) # 接收报头长度
18 head_json = struct.unpack("i", head)[0] # 获取报头长度
19 head_dic = json.loads(sock.recv(head_json).decode()) # 1.接收报头 2.将接收的报头解码成字符串 3.将字符串转换成对应的字典
20 data_size = head_dic["data_size"] # 获取字典的value,也就是真实数据长度
21 block_list = [] # 接收数据的容器
22 recv_size = 0 # 接收到的数据长度
23 while recv_size < data_size: # 当实际接收的数据长度小于应该接收的数据长度,就继续接收
24 block = sock.recv(32) # 接收数据,一次32个字节
25 recv_size += len(block) # 改变实际接收的数据长度
26 block_list.append(block) # 将接收的数据添加到容器中
27 if data_size != 0: # 如果应该接收的数据长度不等于0
28 if block_list[-1] == b'': # 如果最后一块是空字节
29 del block_list[-1] # 将最后一块删去
30 for i in range(len(block_list)): # 分块解密
31 block_list[i] = rsa.decrypt(block_list[i], private_key)
32 response = b"".join(block_list).decode() # 拼接容器中的数据并解码成字符串
33 print(response) # 输出这个字符串
34sock.close() # 程序结束之前,关闭套接字对象

服务器

服务器的实现也非常简单,基本上和客户端差不了多少,就是多了一个处理数据的过程,处理数据非常简单,就是执行命令并获取命令结果,执行命令可以调用os模块中的system函数,当然有更好的办法,我是直接怎么简单怎么来。至于如何获取命令执行结果我也是用最简单的方法了。命令执行有两种结果,正确和错误,正确的结果在标准输出流stdout中,错误的输出结果在标准出错流stderr中,我们直接对输出重定向,将结果直接写入文件。然后就是读取文件,发送数据。下面具体的细节也不讲了,直接给出源代码。

1import socket
2import rsa
3from os import system
4import json
5import struct
6from time import sleep
7sock = socket.socket() # 创建套接字对象
8sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 设置套接字选项
9sock.bind(('', 8080)) # 将IP和端口捆绑
10sock.listen(1) # 设置最大连接数
11public_key = rsa.PublicKey.load_pkcs1(open("client_public_key.pem", "rb").read()) # 加载公钥
12private_key = rsa.PrivateKey.load_pkcs1(open("self_private_key.pem", "rb").read()) # 加载私钥
13conn, addr = sock.accept() # 接受客户端连接
14while True:
15 try:
16 conn.setblocking(True) # 设置阻塞,防止后面的超时一直生效
17 block = conn.recv(32) # 接收数据
18 block_list = [] # 接收数据的容器
19 conn.settimeout(1) # 设置超时,防止第23行发生阻塞
20 while len(block) == 32: # 当接收的字节等于32个,就一直接收
21 block_list.append(block) # 将接收的数据添加到容器中
22 try: # 尝试继续接收
23 block = conn.recv(32)
24 except socket.timeout: # 如果超时,就把接收的数据置空,因为没有数据到来
25 block = b""
26 conn.setblocking(True) # 设置阻塞,防止前面的超时一直生效
27 block_list.append(block) # 最后一次添加到容器
28 if block_list[-1] == b'': # 如果最后一个是空字节,就删去
29 del block_list[-1]
30 for i in range(len(block_list)): # 分块解密
31 block_list[i] = rsa.decrypt(block_list[i], private_key)
32 request = b"".join(block_list).decode() # 拼接容器中的数据并解码成字符串
33 system(request+" 1> out 2> err") # 执行命令(在命令执行过程中已经重定向到文件了)
34 out, err = open("out", "rb").read(), open("err", "rb").read() # 读取文件中的内容
35 err_list = [] # 出错列表
36 out_list = [] # 输出列表
37 for i in range(0, len(err), 20): # 如果当前的块不为空,将加密之后添加到出错列表中
38 if err[i:i+20] != b"":
39 err_list.append(rsa.encrypt(err[i:i+20], public_key))
40 else:
41 break
42 for i in range(0, len(out), 20): # 如果当前的块不为空,将加密之后添加到输出列表中
43 if out[i:i+20] != b"":
44 out_list.append(rsa.encrypt(out[i:i+20], public_key))
45 else:
46 break
47 err = b"".join(err_list) # 拼接加密之后的错误数据
48 out = b"".join(out_list) # 拼接加密之后的正确数据
49 response_head = json.dumps({"data_size": len(out)+len(err)}).encode() # 设置报头并转换成对应的类型
50 conn.send(struct.pack("i", len(response_head))) # 将报头长度压缩成一个定长字节序列并发送
51 conn.send(response_head) # 发送报头
52 for i in range(0, len(err), 32): # 分块发送出错的数据
53 if len(err[i:i+32]) != 0:
54 conn.send(err[i:i+32])
55 sleep(0.001) # 防止因为发送太快发送缓冲区溢出
56 for i in range(0, len(out), 32): # 分块发送正确的数据
57 if len(out[i:i+32]) != 0:
58 conn.send(out[i:i+32])
59 sleep(0.001) # 防止因为发送太快发送缓冲区溢出
60 except KeyboardInterrupt:
61 break
62 except ConnectionResetError:
63 break
64sock.close() # 在程序结束之前,关闭套接字对象

测试

下面再稍微的做一些测试看看有没有问题,运行这个程序非常简单,先服务器再客户端,然后在客户端控制台中输入命令,等待结果返回就行,运行结果如图所示。

安全的数据库图形管理工具(2):三个问题_数据_04

通过结果我们可以看出,服务器能够正常执行命令,客户端也同样可以接受到命令的结果。

今天的文章有不懂的可以加群,群号:822163725,备注:小陈学Python,不备注可是会被拒绝的哦~!

最后欢迎大家扫码关注

安全的数据库图形管理工具(2):三个问题_数据_05

举报

相关推荐

0 条评论