上次虽然实现了加密传输,也通过了简单的测试,但是我在进一步测试时发现了一些问题,下面我就来从根本上解决这些问题,在解决这些问题之前,首先附上之前文章的链接。
之前我只是用两个短字节序列来进行密钥测试,那两个字节序列都比较短,可是我在进行进一步测试的时候发现长字节序列无法被加密,不相信的话我可以尝试一下。
为了进行简单的测试,我就把客户端代码要发送的字节改成特别长的而已。
import rsa
import socket
public_key = open("server_public_key.pem", "rb").read() # 打开公钥文件并读取
public_key = rsa.PublicKey.load_pkcs1(public_key) # 加载公钥
private_key = open("self_private_key.pem", "rb").read() # 打开私钥文件并读取
private_key = rsa.PrivateKey.load_pkcs1(private_key) # 加载私钥
# 用公钥加密要发送的数据
send_encode_data = rsa.encrypt(b"123456789012345678901234567890", public_key)
s = socket.socket() # 创建套接字对象
host = "111.230.108.44" # 服务器的IP地址
port = 1234 # 服务器程序的端口号
s.connect((host, port)) # 连接服务器
s.send(send_encode_data) # 发送已经加密的数据
receive_encode_data = s.recv(128) # 接受已经加密的数据
print(rsa.decrypt(receive_encode_data, private_key).decode()) # 解密接收到的加密数据并输出
要加密的节已经够长了,下面我们来看看运行情况。
运行之后发现出问题了,稍微翻译一下出错信息:消息需要30个字节,但是只有21个字节的空间。我们首先来想一个问题,为什么一次只能加密21个字节?21从何而来?
我直接给出结论吧,可以被加密的字节长度与密钥的比特数呈线性正相关,我们有如下公式:
我上次设置的密钥比特数是256,最大长度也就是256/8-11=21。21就是这么来的,超过了这个长度就会出现问题。如何解决这样的问题其实很简单,密钥比特数设置一个很大的数就行了。但是这样治标不治本,万一加密的数据比那个很大的数还要长怎么办?还是很简单,我把这一个长字节序列分成一块一块的,每一块20个字节,在解密的时候,我们也一块一块的解密,然后拼接起来就行。
for i in range(0, len(cmd), 20):
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加密算法规定,只要长度在合法的范围内,我们有如下公式:
通过上面的公式我们可以看出在其他条件不变的情况下,密文长度与明文长度无关,不管明文多长,密文的字节长度固定不变,在我这里就是256/8=32,所以我要求接收方每次接收32个字节长度。
在上面我稍微提到了一个问题,假设我有45个字节序列需要发送,两个20发完之后最后发一个5个字节的块。就在这个时候,我必须要求接收缓冲区只能接5个字节,如果多了就会出现问题。因为接收缓冲区如果依旧是用20个字节从接收缓冲区读取数据,就会出现这样一种情况,接收到的数据也是20个字节,前5个是最后一次发送的数据,后15个是第二次发送的20个字节的后15个字节,我们称这种情况叫粘包。下面我来重点解决这个问题,为什么会出现粘包?因为发送和接收都太快了,导致缓冲区没有刷新,最简单的办法我们就是使用sleep给缓冲区一个刷新的时间,但这样做性能太差了,我们暂时先想一下有没有更好的办法,如果我们规定发送多少个字节就接收多少个字节,这样就可以获得一个平衡,从而不会出现接收到多余的无用的数据。现在最关键的问题出来了,我怎么把发送要发送的字节长度告诉接收方?接收方又该如何接收?接收多少个字节?如果我就简单的把长度这个整数使用str转换成字符串,然后编码成字节,这个字节的长度是不确定的,接收方设置接收字节数就陷入了麻烦,如何把长度给固定住?为此我们可以使用模块struct,struct可以把一个整数压缩成四个字节,现在又出现了一个问题,4个字节存放的整数有范围,万一越界怎么办?很简单,我再做一层封装,先创建一个报头,再把报头转成字节,然后把字节报头的长度用struct压缩打包发过去就行了。
在网络编程中,如果服务器发送速度和客户端接收速度不匹配,假设服务器发送太快,客户端接收的有点慢,默认情况下服务器并不会配合客户端的接收速度,而是会一股脑的把数据丢在缓冲区,分块发送按理来说没毛病,但是如果不给服务器刷新缓冲区的机会,依旧会造成溢出。在python网络编程中,我一时半伙找不到清理套接字缓冲区的办法,只能sleep将就了。
下面我通过编写一个简单的SSH远程控制终端来进行进一步测试,首先说一下设计思路。我们要求客户端输入命令发送过去,服务器返回命令执行结果给客户端,数据传输一律是非对称加密。下面我详细的说一下客户端程序与服务器程序的设计细节。
客户端的实现非常简单,首先读取自己的私钥和服务器的公钥并赋值给两个变量。然后连接服务器,连好之后就是开始输入命令,输入完成之后就将命令分块加密发送,发送完成之后就接收对方响应过来的报头长度,然后接收报头,之后就开始接收真实数据,然后把接收的数据解密即可。具体细节我就不讲了,直接给出源代码。
import socket
import rsa
import json
import struct
public_key = rsa.PublicKey.load_pkcs1(open("server_public_key.pem", "rb").read()) # 加载公钥
private_key = rsa.PrivateKey.load_pkcs1(open("self_private_key.pem", "rb").read()) # 加载私钥
sock = socket.socket() # 创建套接字对象
sock.connect(('', 8080)) # 连接服务器
while True:
cmd = input().strip().encode() # 1.输入命令 2.去除无效字符 3.编码成字节序列
if not cmd: # 如果输入的命令为空,继续下一次循环
continue
elif cmd == b'logout': # 如果命令是logout就结束循环
break
for i in range(0, len(cmd), 20): # 分块加密,一块20个字节
sock.send(rsa.encrypt(cmd[i:i + 20], public_key)) # 发送加密的数据
head = sock.recv(4) # 接收报头长度
head_json = struct.unpack("i", head)[0] # 获取报头长度
head_dic = json.loads(sock.recv(head_json).decode()) # 1.接收报头 2.将接收的报头解码成字符串 3.将字符串转换成对应的字典
data_size = head_dic["data_size"] # 获取字典的value,也就是真实数据长度
block_list = [] # 接收数据的容器
recv_size = 0 # 接收到的数据长度
while recv_size < data_size: # 当实际接收的数据长度小于应该接收的数据长度,就继续接收
block = sock.recv(32) # 接收数据,一次32个字节
recv_size += len(block) # 改变实际接收的数据长度
block_list.append(block) # 将接收的数据添加到容器中
if data_size != 0: # 如果应该接收的数据长度不等于0
if block_list[-1] == b'': # 如果最后一块是空字节
del block_list[-1] # 将最后一块删去
for i in range(len(block_list)): # 分块解密
block_list[i] = rsa.decrypt(block_list[i], private_key)
response = b"".join(block_list).decode() # 拼接容器中的数据并解码成字符串
print(response) # 输出这个字符串
sock.close() # 程序结束之前,关闭套接字对象
服务器的实现也非常简单,基本上和客户端差不了多少,就是多了一个处理数据的过程,处理数据非常简单,就是执行命令并获取命令结果,执行命令可以调用os模块中的system函数,当然有更好的办法,我是直接怎么简单怎么来。至于如何获取命令执行结果我也是用最简单的方法了。命令执行有两种结果,正确和错误,正确的结果在标准输出流stdout中,错误的输出结果在标准出错流stderr中,我们直接对输出重定向,将结果直接写入文件。然后就是读取文件,发送数据。下面具体的细节也不讲了,直接给出源代码。
import socket
import rsa
from os import system
import json
import struct
from time import sleep
sock = socket.socket() # 创建套接字对象
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 设置套接字选项
sock.bind(('', 8080)) # 将IP和端口捆绑
sock.listen(1) # 设置最大连接数
public_key = rsa.PublicKey.load_pkcs1(open("client_public_key.pem", "rb").read()) # 加载公钥
private_key = rsa.PrivateKey.load_pkcs1(open("self_private_key.pem", "rb").read()) # 加载私钥
conn, addr = sock.accept() # 接受客户端连接
while True:
try:
conn.setblocking(True) # 设置阻塞,防止后面的超时一直生效
block = conn.recv(32) # 接收数据
block_list = [] # 接收数据的容器
conn.settimeout(1) # 设置超时,防止第23行发生阻塞
while len(block) == 32: # 当接收的字节等于32个,就一直接收
block_list.append(block) # 将接收的数据添加到容器中
try: # 尝试继续接收
block = conn.recv(32)
except socket.timeout: # 如果超时,就把接收的数据置空,因为没有数据到来
block = b""
conn.setblocking(True) # 设置阻塞,防止前面的超时一直生效
block_list.append(block) # 最后一次添加到容器
if block_list[-1] == b'': # 如果最后一个是空字节,就删去
del block_list[-1]
for i in range(len(block_list)): # 分块解密
block_list[i] = rsa.decrypt(block_list[i], private_key)
request = b"".join(block_list).decode() # 拼接容器中的数据并解码成字符串
system(request+" 1> out 2> err") # 执行命令(在命令执行过程中已经重定向到文件了)
out, err = open("out", "rb").read(), open("err", "rb").read() # 读取文件中的内容
err_list = [] # 出错列表
out_list = [] # 输出列表
for i in range(0, len(err), 20): # 如果当前的块不为空,将加密之后添加到出错列表中
if err[i:i+20] != b"":
err_list.append(rsa.encrypt(err[i:i+20], public_key))
else:
break
for i in range(0, len(out), 20): # 如果当前的块不为空,将加密之后添加到输出列表中
if out[i:i+20] != b"":
out_list.append(rsa.encrypt(out[i:i+20], public_key))
else:
break
err = b"".join(err_list) # 拼接加密之后的错误数据
out = b"".join(out_list) # 拼接加密之后的正确数据
response_head = json.dumps({"data_size": len(out)+len(err)}).encode() # 设置报头并转换成对应的类型
conn.send(struct.pack("i", len(response_head))) # 将报头长度压缩成一个定长字节序列并发送
conn.send(response_head) # 发送报头
for i in range(0, len(err), 32): # 分块发送出错的数据
if len(err[i:i+32]) != 0:
conn.send(err[i:i+32])
sleep(0.001) # 防止因为发送太快发送缓冲区溢出
for i in range(0, len(out), 32): # 分块发送正确的数据
if len(out[i:i+32]) != 0:
conn.send(out[i:i+32])
sleep(0.001) # 防止因为发送太快发送缓冲区溢出
except KeyboardInterrupt:
break
except ConnectionResetError:
break
sock.close() # 在程序结束之前,关闭套接字对象
下面再稍微的做一些测试看看有没有问题,运行这个程序非常简单,先服务器再客户端,然后在客户端控制台中输入命令,等待结果返回就行,运行结果如图所示。
通过结果我们可以看出,服务器能够正常执行命令,客户端也同样可以接受到命令的结果。
本文分享自 Python机器学习算法说书人 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!