简介:这份资源是应用密码学实验六的完整工程包,面向正在学习密码学课程、需要动手实现综合安全通信系统的高校学生与自学者。它围绕密钥协商、对称加密或序列密码加密、数字签名及签名认证等环节,给出一个可编译运行的通信系统实例,帮助读者把分散的密码算法串成完整流程,理解安全通信中各模块的协作方式。压缩包共289个文件,约85.63MB,以tlog、obj、pdb、ilk等编译中间与调试文件为主,另含h头文件、cpp源文件、dll动态库、exe可执行程序及sln解决方案等工程文件,便于直接打开工程对照源码学习。已有470人学习下载,适合作为课程实验参考,读者可从中获取密钥协商与签名验证的实现思路、客户端与服务端通信的代码组织方式,以及调试排错时可借鉴的工程结构。
1. 一个安全通信系统实验包:从密钥协商到密文传输的完整链路
做密码学课程设计时,最让人头疼的不是单个算法写不出来,而是把对称加密、非对称加密、哈希校验、数字签名这几块拼成一个能跑通的通信系统。很多同学交上来的代码,AES 单独测能过,RSA 单独测能过,一联调就翻车——密钥传错了、填充对不上、签名验不过。这个实验包解决的正是这个断层问题:它把“一个安全的通信系统”拆成可独立验证的模块,再串成一条完整的收发链路。适合正在做应用密码学实验、需要交课程设计、或者想搞明白“真实通信里密码学到底怎么落地”的从业者和学生。下面按我拆包复现的顺序讲,重点放在参数怎么设、坑在哪。
2. 拆开实验包:模块划分与密码学选型逻辑
拿到压缩包先别急着跑代码,先看清楚它把系统切成了哪几块。一个安全的通信系统,核心要回答四个问题:身份怎么确认、密钥怎么安全到达、数据怎么加密、完整性怎么保证。实验包的目录结构通常围绕这四个问题组织,我拆过的同类包里,典型划分是这样的:
| 模块 | 承担职责 | 常见算法选型 |
|---|---|---|
| 密钥协商 | 双方在不安全信道上生成共享密钥 | DH / ECDH |
| 对称加密 | 大批量数据的机密性 | AES-CBC / AES-GCM |
| 非对称加密 | 密钥封装或小数据加密 | RSA-OAEP |
| 哈希与签名 | 完整性校验、身份认证 | SHA-256 + RSA/ECDSA |
这个划分不是随便定的。对称加密快但密钥分发是死穴,非对称加密解决分发但慢得离谱,所以真实系统一律是“非对称协商出对称密钥,再用对称密钥加密数据”。实验包如果按这个思路组织,说明设计是靠谱的;如果它让你直接用 RSA 加密整段消息,那基本是教学简化版,跑通可以,别当成生产方案。
2.1 先确认运行环境与依赖
拆包第一步永远是看依赖。Python 实现的密码学实验包,常见依赖是pycryptodome(提供 AES、RSA、哈希)或cryptography。先建虚拟环境再装,别污染全局:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install pycryptodome cryptography装完先跑一个最小验证,确认库能用、版本没冲突:
from Crypto.Cipher import AES from Crypto.PublicKey import RSA from Crypto.Hash import SHA256 # 验证 AES 可用,密钥长度必须是 16/24/32 字节 key = b'0' * 32 cipher = AES.new(key, AES.MODE_GCM) print("AES-GCM 初始化成功,nonce 长度:", len(cipher.nonce)) # 验证 RSA 密钥生成 rsa_key = RSA.generate(2048) print("RSA 模长:", rsa_key.size_in_bits())这段的作用是把“环境是否就绪”和“业务逻辑”分开。很多人一上来就跑主程序,报错信息淹没在业务代码里,根本分不清是环境问题还是逻辑问题。先做这个冒烟测试,AES 和 RSA 都能初始化,再往下走。参数上注意:AES 密钥长度只能是 16、24、32 字节,对应 AES-128/192/256;RSA 模长教学用 2048 位够了,1024 位现在不安全,别用。
2.2 密钥协商模块:DH 参数怎么选
密钥协商是整条链路的起点,也是最容易出玄学问题的地方。实验包如果用的是 Diffie-Hellman,核心参数是素数 p 和生成元 g。教学包里常见两种写法:一种硬编码一组标准参数,一种现场生成。现场生成大素数很慢,我一般直接用标准组:
# 使用标准 DH 参数(RFC 3526 的 2048 位组,教学场景够用) P = int( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" "83655D23DCA3AD961C62F356208552BB9ED529077096966D" "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16 ) G = 2 def dh_keypair(): private = int.from_bytes(__import__('os').urandom(32), 'big') public = pow(G, private, P) return private, public def dh_shared(private, peer_public): return pow(peer_public, private, P)逻辑很直白:双方各自生成私钥,算出公钥发给对方,再用对方公钥和自己的私钥算出同一个共享值。参数说明:p 用 2048 位标准组,g 取 2,私钥用 32 字节随机数。这里有个血泪经验——共享值算出来是个大整数,不能直接当 AES 密钥用,必须过一遍哈希(比如 SHA-256)截取到 32 字节,否则长度对不上,AES 直接报错。实验包里如果已经做了这步,说明作者踩过坑;没做的话你自己补上。
3. 把加密链路跑通:AES-GCM 与签名验证的实操
密钥协商完,接下来是数据加密和完整性保护。这里我强烈建议用 AES-GCM 而不是 AES-CBC,原因后面避坑章节细说。GCM 是认证加密,加密的同时生成认证标签,解密时自动校验,省掉单独做 HMAC 的麻烦。
3.1 AES-GCM 加密与解密的完整调用
from Crypto.Cipher import AES from Crypto.Random import get_random_bytes import hashlib def derive_aes_key(shared_secret): # 共享值转 32 字节密钥,用 SHA-256 截断 return hashlib.sha256(str(shared_secret).encode()).digest() def encrypt_message(key, plaintext, aad=b""): cipher = AES.new(key, AES.MODE_GCM) cipher.update(aad) # 附加认证数据,可选 ciphertext, tag = cipher.encrypt_and_digest(plaintext) return cipher.nonce, ciphertext, tag def decrypt_message(key, nonce, ciphertext, tag, aad=b""): cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) cipher.update(aad) return cipher.decrypt_and_verify(ciphertext, tag)参数说明:nonce由库自动生成,12 字节,绝对不能重复使用——同一个密钥下 nonce 重复,GCM 的安全性直接崩掉。aad是附加认证数据,不加密但参与完整性校验,适合放消息头、时间戳这类需要防篡改的明文。tag是 16 字节认证标签,解密时必须带上,decrypt_and_verify校验失败会抛ValueError,这个异常必须捕获,不能吞掉。
调用顺序上,发送方先协商密钥、派生 AES 密钥、加密、把 nonce 和 tag 随密文一起发;接收方用同样的共享值派生密钥、拿 nonce 和 tag 解密。这里最容易翻车的是 nonce 和 tag 的传输——它们不是秘密,但必须完整到达,少一个字节都解不开。
3.2 数字签名:确认消息来源
加密解决的是“别人看不到”,签名解决的是“确认是谁发的”。实验包里通常用 RSA 签名或 ECDSA。RSA 签名流程是:发送方用自己的私钥对消息哈希签名,接收方用发送方公钥验签。
from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA def sign_message(private_key, message): h = SHA256.new(message) return pkcs1_15.new(private_key).sign(h) def verify_signature(public_key, message, signature): h = SHA256.new(message) try: pkcs1_15.new(public_key).verify(h, signature) return True except (ValueError, TypeError): return False注意签名对象是“消息的哈希”而不是消息本身,pkcs1_15内部会再做一次编码。验签失败抛异常,这里用 try 包住返回布尔值,方便上层判断。参数上,签名用的 RSA 密钥和加密用的 RSA 密钥最好分开,教学场景混用问题不大,但生产环境密钥用途必须隔离,这是基本纪律。
3.3 串成完整收发流程
把上面几块拼起来,一次完整通信是这样的:A 生成 DH 密钥对,B 生成 DH 密钥对,互换公钥,各自算出共享值并派生 AES 密钥;A 用 AES-GCM 加密消息,同时用自己的 RSA 私钥对密文签名;B 收到后先验签、再解密。这个顺序不能反——先验签再解密,能挡住大部分伪造消息,避免对恶意数据做解密运算。
# 伪代码示意完整链路 a_priv, a_pub = dh_keypair() b_priv, b_pub = dh_keypair() shared_a = dh_shared(a_priv, b_pub) shared_b = dh_shared(b_priv, a_pub) assert shared_a == shared_b # 这一步必须相等,否则链路断了 aes_key = derive_aes_key(shared_a) nonce, ct, tag = encrypt_message(aes_key, b"hello secure world") sig = sign_message(a_rsa_priv, ct) # 接收端 assert verify_signature(a_rsa_pub, ct, sig) plain = decrypt_message(aes_key, nonce, ct, tag)assert shared_a == shared_b这行看着多余,实际是排错神器。两边算出来不相等,八成是公钥传错了或者参数组不一致,早发现早定位。
4. 避坑与排查:五个真实翻车现场
4.1 解密报 MAC check failed
现象:decrypt_and_verify抛ValueError: MAC check failed。原因通常有三个:密钥不一致、nonce 传错、密文被截断。先打印两边派生出的 AES 密钥哈希值对比,不一致就回头查 DH 共享值;密钥一致就查 nonce 和 tag 是否完整传输。解决:在加密端和解密端各打一行密钥指纹日志,定位是协商问题还是传输问题。
4.2 RSA 签名验不过但密钥没错
现象:公钥确认是对方的,签名就是验不过。原因多半是签名和验签用的哈希算法不一致,或者签名时对原始消息、验签时对密文。解决:统一约定“对什么签名”——是明文、密文还是消息摘要,写进协议文档,两端严格照做。我见过最隐蔽的一次是发送端签了明文、接收端拿密文验签,查了一下午。
4.3 DH 共享值长度不对导致 AES 报错
现象:AES.new报 key 长度错误。原因:直接把 DH 共享的大整数当密钥传了。解决:共享值必须过 KDF(密钥派生函数),最简单是 SHA-256 取 32 字节,规范做法是 HKDF。别偷懒用str(shared)[:32],字符编码会引入不可控字节。
4.4 nonce 复用导致密文可预测
现象:系统能跑,但安全性形同虚设。原因:为了“省事”把 nonce 写死成固定值。GCM 下同一密钥复用 nonce,攻击者能恢复明文甚至伪造密文。解决:每次加密都用get_random_bytes(12)生成新 nonce,随密文一起传。这是红线,没有例外。
4.5 大文件一次性读入内存爆掉
现象:加密稍大的文件就卡死或内存溢出。原因:encrypt_and_digest一次性处理全部数据。解决:分块加密,每块独立 nonce,或者改用流式加密模式。教学包数据量小不明显,一旦换成真实文件立刻暴露。
5. 进阶验证:用测试向量给系统做体检
系统跑通不等于正确。我习惯用标准测试向量(test vector)反向验证每个模块,这比“能加密能解密”靠谱得多。AES-GCM 有 NIST 公布的官方测试向量,拿一组已知密钥、nonce、明文、密文、tag 对进去,输出完全一致才算实现正确。
# NIST AES-GCM 测试向量(简化示例,实际请对照官方文档) key = bytes.fromhex("00000000000000000000000000000000") nonce = bytes.fromhex("000000000000000000000000") plaintext = bytes.fromhex("") # 空明文时,期望 tag 为 58e2fccefa7e3061367f1d57a4e7455a cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) ct, tag = cipher.encrypt_and_digest(plaintext) assert tag.hex() == "58e2fccefa7e3061367f1d57a4e7455a"这个断言过了,说明 GCM 实现和标准一致;过不了,要么库版本有问题,要么参数传错。同样的思路可以套到 SHA-256、RSA 签名上,每个模块都有公开向量。做完这轮体检,再回头跑完整链路,心里才有底。
另一个进阶技巧是给通信链路加“重放攻击”防护:在消息里带时间戳或递增序号,接收端维护一个窗口,超窗或重复的直接丢弃。实验包如果没做这层,自己补上,这是从“能跑”到“能用”的关键一步。我一般会在 AAD 里塞时间戳,验签通过后再校验时间窗口,两步都过才解密。
从那以后我每次拆这类密码学实验包,都强制先跑测试向量再碰业务代码——链路能跑通只是及格线,和标准对得上才算数。希望帮到你。
本文还有配套的精品资源,点击获取