本来是密码学的一个实验,验收感觉还是不错的。
1、PGP流程
PGP可以同时对原始信息做签名和加密操作,具体流程如下:
- 发送者创建需要发送给接收者的原始信息内容;
- 发送者基于原始信息用SHA1算法产生一个160bit的散列值(称为消息摘要),然后用发送者自己的私钥对摘要进行加密;
- 发送者将第(2)步得到的加密摘要与原始信息封装并采用ZIP算法进行压缩;
- 发送者生成用于对称加密的会话秘钥;
- 发送者采用对称秘钥算法通过第(4)步生成的会话秘钥对第(3)步生成的压缩结果进行加密;
- 发送者采用来自接收者的公钥对第(4)步生成的会话秘钥进行加密;
- 发送者将加密后的信息和会话秘钥封装成报文一同发送给接收者;
- 接收者通过自己的私钥将报文中含有的加密的会话秘钥进行解密得到原始的会话秘钥;
- 接收者利用第(8)步得到的会话秘钥采用和发送者相同的对称秘钥算法对加密信息进行解密,得到压缩后的加密摘要和原始信息;
- 接收者进行解压缩操作,得到加密摘要和原始信息;
- 接收者通过从发送者处得到的公钥将报文中包含的摘要进行解密,获得原始未加密的摘要信息;
- 接收者使用与发送者相同的算法针对报文中含有的原始信息生成一个新的散列值,并与第(11)步得到的解密后的摘要信息进行比对,如果两者完全匹配,则接收者收到的信息来自于发送者(因为接收者用发送者给的公钥解密了发送者发送的加密报文,即身份认证),且在发送过程中未被篡改(接收者生成的摘要和发送者发送的摘要完全匹配,即完整性检查)。
以上流程来源于PGP工作原理简述 | Mr.Muzi (marcuseddie.github.io)。大佬的博客还是很耐看的。
2、源码
所需要的库:
requirements.txt文件如下:
crypto 1.4.1
cryptography 42.0.5
ntplib 0.4.0
pycryptodome 3.20.0
执行以下命令安装即可。我的Python解释器版本是3.10.11。3.8和以下的貌似不行。
pip install -r requirements.txt
项目文件:
包含要发送的原始数据包,双方的公私钥,以及双方发送和接收的文件。
生成双方的公私密钥:
首先需要生成发送方和接收方的公私密钥,这里采用的是RSA非对称加密的2048位密钥。后续双方通信的过程中需要使用。
from Crypto.PublicKey import RSA
def generate_keys(role):
# 生成2048位的RSA密钥对
key = RSA.generate(2048)
# 私钥
private_key = key.export_key()
private_key_filename = f"{role}_private_key.pem"
with open(private_key_filename, "wb") as priv_file:
priv_file.write(private_key)
# 公钥
public_key = key.publickey().export_key()
public_key_filename = f"{role}_public_key.pem"
with open(public_key_filename, "wb") as pub_file:
pub_file.write(public_key)
print(f"{role.capitalize()} keys generated and saved to {private_key_filename} and {public_key_filename}.")
if __name__ == "__main__":
generate_keys("sender")
generate_keys("receiver")
发送方代码:
- 实现了PGP发送方的任务。对原始数据生成摘要,然后用自己的私钥签名,将签名后的摘要和原始数据压缩为zip包。再用AES加密算法生成会话密钥对zip包加密,并用接收者的公钥对会话密钥加密,一起打包发送给接收方。
- 加了时间戳。避免使用本地的time()函数,向服务器请求时间。因为如果处于大洋彼岸的双方时间可能会不一致。设置时间限制300是,来防止重放攻击。(其实我也不太懂)
- 简单加了GUI用户界面。
发送方源码如下:
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import os
import socket
import zipfile
import ntplib
from io import BytesIO
from datetime import datetime
from cryptography.hazmat.primitives import hashes, padding as sym_padding
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
from os import urandom
class SenderGUI:
def __init__(self, master):
self.master = master
master.title("Secure File Sender")
master.geometry('400x300')
master.resizable(False, False)
frame = ttk.Frame(master)
frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
self.filepath = tk.StringVar()
file_select_button = ttk.Button(frame, text="Select File to Send", command=self.select_file)
file_select_button.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
ttk.Label(frame, text="Recipient IP:").grid(row=1, column=0, padx=10, pady=5, sticky="w")
self.ip_entry = ttk.Entry(frame)
self.ip_entry.grid(row=1, column=1, padx=10, pady=5, sticky="ew")
ttk.Label(frame, text="Port:").grid(row=2, column=0, padx=10, pady=5, sticky="w")
self.port_entry = ttk.Entry(frame)
self.port_entry.insert(0, '12345')
self.port_entry.grid(row=2, column=1, padx=10, pady=5, sticky="ew")
self.private_key_path = tk.StringVar()
self.public_key_path = tk.StringVar()
ttk.Button(frame, text="Select Your Private Key", command=self.select_private_key).grid(row=3, column=0, padx=10, pady=10, sticky="ew")
ttk.Button(frame, text="Select Recipient's Public Key", command=self.select_public_key).grid(row=3, column=1, padx=10, pady=10, sticky="ew")
send_button = ttk.Button(frame, text="Send File", command=self.send_file)
send_button.grid(row=4, column=0, columnspan=2, padx=10, pady=10, sticky="ew")
self.progress = ttk.Progressbar(frame, orient=tk.HORIZONTAL, length=100, mode='determinate')
self.progress.grid(row=5, column=0, columnspan=2, padx=10, pady=10, sticky="ew")
def select_file(self):
file_path = filedialog.askopenfilename()
if file_path:
self.filepath.set(file_path)
messagebox.showinfo("File Selected", f"Selected: {file_path}")
def select_private_key(self):
key_path = filedialog.askopenfilename(title="Select Your Private Key", filetypes=[("PEM files", "*.pem")])
if key_path:
self.private_key_path.set(key_path)
messagebox.showinfo("Key Selected", f"Private Key Selected: {key_path}")
def select_public_key(self):
key_path = filedialog.askopenfilename(title="Select Recipient's Public Key", filetypes=[("PEM files", "*.pem")])
if key_path:
self.public_key_path.set(key_path)
messagebox.showinfo("Key Selected", f"Recipient's Public Key Selected: {key_path}")
def send_file(self):
if not all([self.filepath.get(), self.private_key_path.get(), self.public_key_path.get()]):
messagebox.showerror("Error", "Please select all necessary files and keys.")
return
try:
with open(self.private_key_path.get(), "rb") as key_file:
sender_private_key = load_pem_private_key(key_file.read(), password=None)
with open(self.public_key_path.get(), "rb") as key_file:
receiver_public_key = load_pem_public_key(key_file.read())
zip_content = self.package_and_compress(self.filepath.get(), sender_private_key)
session_key = urandom(32)
iv, encrypted_data = self.encrypt_data(zip_content, session_key)
encrypted_session_key = self.encrypt_key(session_key, receiver_public_key)
final_package = iv + encrypted_session_key + encrypted_data
self.send_data_over_socket(final_package, self.ip_entry.get(), int(self.port_entry.get()))
messagebox.showinfo("Success", "File sent successfully.")
except Exception as e:
messagebox.showerror("Error", str(e))
def generate_message_digest(self, data):
digest = hashes.Hash(hashes.SHA256())
digest.update(data)
return digest.finalize()
def sign_message(self, private_key, message):
return private_key.sign(
message,
asym_padding.PSS(
mgf=asym_padding.MGF1(hashes.SHA256()),
salt_length=asym_padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
def package_and_compress(self, filename, sender_private_key):
with open(filename, "rb") as f:
original_data = f.read()
signature = self.sign_message(sender_private_key, self.generate_message_digest(original_data))
ntp_client = ntplib.NTPClient()
response = ntp_client.request('time.nist.gov')
ntp_timestamp = response.tx_time
formatted_timestamp = datetime.fromtimestamp(ntp_timestamp).strftime('%Y-%m-%d %H:%M:%S').encode('utf-8')
mem_file = BytesIO()
with zipfile.ZipFile(mem_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipf.writestr('original_data', original_data)
zipf.writestr('signature.txt', signature)
zipf.writestr('timestamp.txt', formatted_timestamp)
mem_file.seek(0)
return mem_file.getvalue()
def encrypt_data(self, data, key):
iv = urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
encryptor = cipher.encryptor()
padder = sym_padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(data) + padder.finalize()
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
return iv, encrypted_data
def encrypt_key(self, key, public_key):
return public_key.encrypt(
key,
asym_padding.OAEP(
mgf=asym_padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
def send_data_over_socket(self, data, host, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
sock.sendall(data)
self.progress['value'] = 100
def main():
root = tk.Tk()
app = SenderGUI(root)
root.mainloop()
if __name__ == "__main__":
main()
接收方源码:
接收方接收到来的数据,首先将将会话密钥和加密的数据分开。用自己的私钥对会话密钥解密得到会话密钥,用会话密钥解密加密的数据,得到压缩包。从压缩包中分离出原始数据和签名,使用自己的私钥对签名进行验证,得到摘要。对原始数据进行哈希生成摘要,和刚刚的摘要比对,一致则签名和完整性验证成功。
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import socket
import zipfile
from io import BytesIO
from datetime import datetime
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import hashes, padding as sym_padding
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.serialization import load_pem_public_key, load_pem_private_key
from os import urandom
from threading import Thread
class ReceiverGUI:
def __init__(self, master):
self.master = master
master.title("Secure File Receiver")
master.geometry('400x300')
master.resizable(False, False)
frame = ttk.Frame(master)
frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
ttk.Label(frame, text="Listening Port:").grid(row=0, column=0, padx=10, pady=5, sticky="w")
self.port_entry = ttk.Entry(frame)
self.port_entry.insert(0, '12345')
self.port_entry.grid(row=0, column=1, padx=10, pady=5, sticky="ew")
self.public_key_path = tk.StringVar()
self.private_key_path = tk.StringVar()
ttk.Button(frame, text="Select Your Private Key", command=self.select_private_key).grid(row=1, column=0, padx=10, pady=10, sticky="ew")
ttk.Button(frame, text="Select Sender's Public Key", command=self.select_sender_public_key).grid(row=1, column=1, padx=10, pady=10, sticky="ew")
self.receive_button = ttk.Button(frame, text="Start Receiving", command=self.start_receiving)
self.receive_button.grid(row=2, column=0, columnspan=2, padx=10, pady
=10, sticky="ew")
self.status_label = ttk.Label(frame, text="Status: Ready")
self.status_label.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
self.progress = ttk.Progressbar(frame, orient=tk.HORIZONTAL, length=100, mode='determinate')
self.progress.grid(row=4, column=0, columnspan=2, padx=10, pady=10, sticky="ew")
def select_private_key(self):
key_path = filedialog.askopenfilename(title="Select Your Private Key", filetypes=[("PEM files", "*.pem")])
if key_path:
self.private_key_path.set(key_path)
messagebox.showinfo("Key Selected", f"Your Private Key Selected: {key_path}")
def select_sender_public_key(self):
key_path = filedialog.askopenfilename(title="Select Sender's Public Key", filetypes=[("PEM files", "*.pem")])
if key_path:
self.public_key_path.set(key_path)
messagebox.showinfo("Key Selected", f"Sender's Public Key Selected: {key_path}")
def start_receiving(self):
if not all([self.private_key_path.get(), self.public_key_path.get()]):
messagebox.showerror("Error", "Please select both private and sender's public keys before receiving.")
return
self.receive_button.config(state="disabled")
self.status_label.config(text="Status: Listening")
port = int(self.port_entry.get())
thread = Thread(target=self.listen_for_data, args=(port,))
thread.start()
def listen_for_data(self, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(('', port))
sock.listen(1)
self.master.after(50, lambda: self.status_label.config(text="Status: Waiting for connection"))
conn, addr = sock.accept()
with conn:
self.master.after(50, lambda: self.status_label.config(text="Status: Receiving Data"))
total_data = b''
while True:
data = conn.recv(65536)
if not data:
break
total_data += data
if total_data:
Thread(target=self.process_received_data, args=(total_data,)).start()
def process_received_data(self, data):
try:
self.status_label.config(text="Status: Processing Data")
iv, encrypted_key, encrypted_data = data[:16], data[16:272], data[272:]
session_key = self.decrypt_key(encrypted_key, self.private_key_path.get())
decrypted_data = self.decrypt_data(encrypted_data, session_key, iv)
original_data, signature, timestamp = self.extract_data(decrypted_data)
sender_public_key = load_pem_public_key(open(self.public_key_path.get(), "rb").read())
# Signature Verification
try:
sender_public_key.verify(
signature,
self.generate_message_digest(original_data),
asym_padding.PSS(
mgf=asym_padding.MGF1(hashes.SHA256()),
salt_length=asym_padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
verification_result = True
except Exception as e:
verification_result = False
messagebox.showerror("Verification Error", "Signature verification failed.")
# Timestamp Validation
timestamp_valid = self.check_timestamp(timestamp)
if not timestamp_valid:
messagebox.showwarning("Timestamp Error", "The timestamp is not valid or too old.")
if verification_result and timestamp_valid:
self.save_received_file(original_data)
messagebox.showinfo("Verification Result", "Signature Verified: True and Timestamp Valid: True")
else:
self.status_label.config(text="Status: Verification Failed")
except Exception as e:
messagebox.showerror("Error", f"Failed to process received data: {str(e)}")
finally:
self.status_label.config(text="Status: Ready")
self.receive_button.config(state="normal")
def generate_message_digest(self,data):
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(data)
return digest.finalize()
def decrypt_key(self, encrypted_key, private_key_path):
with open(private_key_path, "rb") as key_file:
private_key = load_pem_private_key(key_file.read(), password=None)
return private_key.decrypt(
encrypted_key,
asym_padding.OAEP(
mgf=asym_padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
def decrypt_data(self, encrypted_data, key, iv):
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
decryptor = cipher.decryptor()
return decryptor.update(encrypted_data) + decryptor.finalize()
def extract_data(self, decrypted_data):
with zipfile.ZipFile(BytesIO(decrypted_data), 'r') as zipf:
original_data = zipf.read('original_data')
signature = zipf.read('signature.txt')
timestamp = zipf.read('timestamp.txt')
return original_data, signature, timestamp
def verify_signature(self, data, signature, public_key):
try:
public_key.verify(
signature,
data,
asym_padding.PSS(
mgf=asym_padding.MGF1(hashes.SHA256()),
salt_length=asym_padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except Exception as e:
return False
def check_timestamp(self, timestamp):
received_time = datetime.strptime(timestamp.decode('utf-8'), '%Y-%m-%d %H:%M:%S')
current_time = datetime.now()
return (current_time - received_time).total_seconds() < 300
def save_received_file(self, data):
save_path = filedialog.asksaveasfilename(title="Save Received File", filetypes=[("All files", "*.*")])
if save_path:
with open(save_path, 'wb') as file:
file.write(data)
messagebox.showinfo("File Saved", f"File successfully saved to {save_path}")
def main():
root = tk.Tk()
app = ReceiverGUI(root)
root.mainloop()
if __name__ == "__main__":
main()
3、运行结果
首先设置接收方参数。开启监听。
设置发送方参数,发送文件。
一段时间后,接收方接收到文件,保存。
签名验证成功,时间戳有效。
4、可能的问题
发送之后不关闭窗口,可重复发送;
如果遇到“目标计算机积极拒绝”等,更换端口;
最好不要挂梯子;
若传输1个G文件,速度较慢。