简介:这份资源是应用密码学实验六的完整工程包,面向正在学习密码学课程、需要把零散算法整合为可运行系统的高校学生与自学者。它解决的是“算法都会写、却拼不出完整安全通信流程”的问题,围绕密钥协商、对称加密或序列密码加密、数字签名与签名认证等环节,构建了一个端到端的安全通信系统,适合课程实验提交与综合复习。压缩包共289个文件,约85.63MB,以cpp与h源码、obj中间文件、tlog与log构建日志、dll动态库、exe可执行程序及pdb调试符号为主,另含sln解决方案与doc文档,完整保留了工程编译与运行痕迹。已有470人学习下载。读者可从中获得可编译运行的通信系统源码、客户端与服务端交互的套接字实现、密钥协商与签名验证的整合思路,以及工程配置与调试排错参考,便于对照实验要求逐项核验并二次开发。
1. 从一次“密钥裸奔”的联调说起
做应用密码学课设,最容易被低估的一步不是算法本身,而是把算法拼成一个能跑通的通信系统。我见过太多同学,AES 加密函数写得漂漂亮亮,一到联调就翻车:密钥直接硬编码在客户端源码里,抓包一看明文密钥躺在报文中,加密等于没加。这个实验要解决的核心问题很具体——在一个客户端与服务端之间,搭出一条“身份可信、密钥可协商、数据可加密、消息可防篡改”的完整链路,而不是孤立地实现某个密码算法。它适合正在做应用密码学课程设计、信息安全毕设,或者需要交付一份可演示安全通信方案的人。下面这套结构,从需求拆到验收,你可以直接照着改。
2. 需求拆解与技术选型:四类安全目标对应哪套密码原语
动手写代码之前,先把“安全”这个词拆成可验收的条目。一个通信系统说它安全,至少要回答四个问题:对方是不是真的、密钥怎么安全地产生、数据怎么藏起来、数据有没有被改过。这四个问题分别对应身份认证、密钥协商、对称加密、消息完整性,缺一个都不算完整。很多课设只做了第三项,答辩时被问一句“中间人换了公钥怎么办”就答不上来。
2.1 四类安全目标与密码原语的映射
把目标落到具体原语上,选型就有了依据。身份认证用非对称签名或证书,密钥协商用 Diffie-Hellman 类协议,数据加密用对称分组密码,完整性用消息认证码或带认证的加密模式。这里的关键是理解“为什么不能只用一种”。对称加密快,但密钥怎么送到对方手里是个死结;非对称加密能解决密钥分发,但速度慢,不适合加密大段数据。所以工程上的通行做法是混合:用非对称机制协商出一个会话密钥,再用对称算法加密业务数据。
| 安全目标 | 典型原语 | 在本系统中的位置 | 验收要点 |
|---|---|---|---|
| 身份认证 | RSA/ECDSA 签名、数字证书 | 握手阶段验证对端身份 | 伪造身份应被拒绝 |
| 密钥协商 | Diffie-Hellman(含临时密钥) | 握手阶段生成会话密钥 | 每次会话密钥不同 |
| 数据机密性 | AES-CBC / AES-GCM | 业务报文加密 | 抓包不可见明文 |
| 消息完整性 | HMAC-SHA256 或 GCM 认证标签 | 每条报文附带校验 | 篡改一字节即失败 |
选型上我一般建议:如果实验允许用现成库,优先选 AES-GCM,它把加密和认证合在一起,少一个“先加密再算 MAC”的顺序坑;如果课程要求手写分组模式,那就用 AES-CBC 加 HMAC,但必须记住“先加密后 MAC”的顺序,顺序反了会引入填充预言攻击的面。哈希用 SHA-256,签名用 RSA-2048 或 ECDSA P-256,密钥交换用带临时密钥的 DH,保证前向安全。
2.2 为什么不用“自己发明的协议”
有一种很常见的错误做法:把用户名密码用 Base64 编码一下,再套一层异或,就号称加密通信。这类“自创密码”在课设里也许能跑,但完全经不起分析。Base64 是编码不是加密,异或的密钥空间小到可以暴力枚举。选型的原则是:只用经过公开分析的标准算法,协议流程参考成熟范式(如 TLS 握手的简化版),不要为了显得“有创新”而改算法结构。创新点应该放在系统设计、异常处理、性能优化上,而不是密码原语本身。
2.3 威胁模型先写清楚
需求文档里必须有一节威胁模型,明确“防谁”。是防局域网抓包的路人,还是防能篡改报文的主动攻击者,还是防持有服务端私钥的内部人员?威胁模型不同,方案复杂度差很多。课设场景下,合理的威胁模型是:攻击者能监听和篡改网络报文,但不能攻破标准密码算法,也不能物理接触服务端。把这条写进设计文档,答辩时能直接回应“你的方案防不住什么”这类追问,反而显得专业。
3. 总体架构设计:握手层、会话层与报文层的职责划分
系统结构上,我倾向于分成三层:握手层负责身份认证和密钥协商,会话层管理会话状态和密钥生命周期,报文层负责单条消息的加解密与校验。这样分层的好处是,每层可以单独测试,出问题时能快速定位是握手失败还是报文解析失败。很多同学把所有逻辑塞进一个send()函数里,调试时只能靠打印,效率极低。
3.1 三层结构与模块职责
握手层在连接建立时运行一次,完成证书交换、签名验证、DH 参数交换,最终双方各自算出相同的会话密钥。会话层持有这个密钥和会话 ID,负责密钥的过期与重协商。报文层对每条业务消息做加密、附加认证标签、编号防重放。三层之间通过明确的接口通信,握手层输出一个会话对象,报文层只依赖这个对象,不关心密钥是怎么来的。
客户端 服务端 |--- ClientHello(随机数, DH公钥) --->| |<-- ServerHello(随机数, DH公钥, 证书) -| |--- 验证证书, 发送签名 ------------>| |<-- 验证签名, 双方计算会话密钥 ------| |=== 加密报文(序号, 密文, 认证标签) ===|上面这个流程是简化版握手,实际实现时随机数要足够长(至少 16 字节),DH 公钥要每次连接重新生成,证书验证要检查有效期和签名链。流程图画在文档里,代码里每个箭头对应一个函数,答辩演示时按箭头讲,逻辑一目了然。
3.2 会话密钥的派生方式
DH 协商出来的共享秘密不能直接当 AES 密钥用,通常的做法是用 HKDF 或简单的哈希做密钥派生,把共享秘密、双方随机数、会话标识一起哈希,得到加密密钥和 MAC 密钥。这样即使某次随机数生成有偏差,密钥也不会直接暴露。派生时把加密密钥和认证密钥分开,避免同一把密钥既加密又做 MAC 带来的交叉风险。
import hashlib def derive_keys(shared_secret: bytes, client_nonce: bytes, server_nonce: bytes): # 用共享秘密和双方随机数派生加密密钥与认证密钥 seed = shared_secret + client_nonce + server_nonce enc_key = hashlib.sha256(b"ENC" + seed).digest() # 32 字节,供 AES-256 使用 mac_key = hashlib.sha256(b"MAC" + seed).digest() # 32 字节,供 HMAC-SHA256 使用 return enc_key, mac_key这段派生的关键点是加上了b"ENC"和b"MAC"两个标签,保证两条派生路径不会撞车。参数上,shared_secret来自 DH 计算,长度取决于所用群;两个 nonce 各 16 字节以上。实际项目中更规范的做法是用 HKDF,这里用哈希拼接是为了课设环境少依赖,逻辑等价但安全性略弱,文档里要注明这个取舍。
3.3 报文格式与序号防重放
每条加密报文建议包含:版本号、会话 ID、单调递增序号、密文、认证标签。序号的作用是防重放——接收方维护一个期望序号窗口,收到重复或过旧的序号直接丢弃。没有序号,攻击者可以把登录报文原样重发,服务端会重复执行操作。报文格式定好后写进文档,前后端按同一张表解析,联调时少扯皮。
4. 关键模块实现:从 DH 协商到 AES-GCM 加解密的落地代码
这一章给的是能直接跑的核心片段,语言用 Python,依赖cryptography库。选它是因为接口清晰、算法实现经过审计,比自己拼大数运算靠谱得多。下面按握手、加密、校验三个动作展开,每段代码后面说明参数怎么调、失败时看哪里。
4.1 DH 密钥协商的实现
from cryptography.hazmat.primitives.asymmetric import x25519 def dh_keypair(): # 生成临时密钥对,每次连接都重新生成,保证前向安全 private_key = x25519.X25519PrivateKey.generate() public_key = private_key.public_key() return private_key, public_key def dh_shared(private_key, peer_public_key): # 计算共享秘密,双方结果一致 return private_key.exchange(peer_public_key)这里用 X25519 而不是传统的大素数 DH,原因是它密钥短、速度快、避免了自己选素数踩到弱参数的血泪坑。generate()每次调用产生新密钥,绝不能把私钥存到文件里复用。exchange()的输出是 32 字节共享秘密,直接喂给上一节的派生函数。如果握手时报“无效公钥”,先检查对端公钥是不是全零或长度不对,这类输入必须在校验阶段拒绝。
4.2 AES-GCM 加解密与认证
from cryptography.hazmat.primitives.ciphers.aead import AESGCM import os def encrypt_message(key: bytes, plaintext: bytes, aad: bytes): # 每条消息用独立随机 nonce,绝不复用 nonce = os.urandom(12) aes = AESGCM(key) ciphertext = aes.encrypt(nonce, plaintext, aad) return nonce + ciphertext # 前 12 字节为 nonce,其余含密文与认证标签 def decrypt_message(key: bytes, packet: bytes, aad: bytes): nonce, ciphertext = packet[:12], packet[12:] aes = AESGCM(key) return aes.decrypt(nonce, ciphertext, aad) # 认证失败会抛异常GCM 的 nonce 复用是致命错误,同一密钥下重复 nonce 会直接泄露明文异或关系,这是密码学里少数“一次失误全盘皆输”的地方,务必用os.urandom每次新生成。aad放报文头(版本、会话 ID、序号),它不加密但参与认证,攻击者改了头部会导致解密抛异常。解密函数抛InvalidTag就说明密文或 aad 被篡改,捕获后直接断开连接,不要尝试“容错解密”。
4.3 完整性校验与异常处理策略
除了 GCM 自带的认证,握手阶段的签名验证也要单独写清楚。验签失败、证书过期、序号回退,这三类异常要分别记录日志并断开连接,不能笼统地 catch 后继续。日志里不要打印密钥和明文,只记录事件类型和会话 ID,避免日志本身成为泄露源。异常处理的原则是“失败即关闭”,安全系统里没有“降级继续”的选项。
5. 避坑与排查:联调阶段最容易翻车的五个点
5.1 现象:本地能通,换台机器就握手失败
原因通常是随机数或时间相关逻辑依赖了本机环境,比如证书有效期校验用了本地时区,或者 DH 参数被缓存成了固定值。解决办法是把所有随机源统一到系统安全随机数接口,证书校验用 UTC 时间,握手参数每次连接重新生成。排查时在两台机器上分别打印握手各阶段的中间值哈希,对比哪一步开始分叉。
5.2 现象:解密偶尔抛 InvalidTag
最常见的原因是 nonce 复用或报文拼接错位。检查加密端是否每条消息都新生成 nonce,以及nonce + ciphertext的切分长度是否和接收端一致。另一个隐蔽原因是 aad 在发送端和接收端拼装顺序不同,比如一端带了序号一端没带。解决方法是把报文头序列化逻辑抽成一个函数,收发两端共用。
5.3 现象:签名验证总是失败
先确认签名和验签用的是同一份数据,很多失败是因为签名时对原始字节签,验签时对十六进制字符串验。其次检查密钥格式,PEM 和 DER 混用会导致解析出不同的公钥。最后确认哈希算法一致,签名用 SHA-256,验签也必须用 SHA-256。逐项排除后基本能定位。
5.4 现象:系统跑久了内存涨、连接卡死
多半是会话对象没有释放,或者异常路径下没有关闭 socket。安全通信里异常分支特别多,每个except都要确保资源释放。建议用上下文管理器管理连接,握手失败、解密失败、超时都走统一的清理路径。压测时观察连接数曲线,只涨不落就是泄漏。
5.5 现象:答辩演示时被问“中间人攻击怎么防”
如果方案里没有证书或预共享信任锚,这个问题答不上来。补救办法是在握手阶段加入服务端证书验证,客户端内置 CA 公钥或证书指纹。课设环境可以简化为预置服务端公钥指纹,但文档里要说明这是简化,生产环境应使用完整证书链。提前把这个点写进威胁模型,答辩时主动讲,比被问出来强。
6. 验收与答辩:用一张检查表把方案讲透
验收不是“能跑就行”,而是逐条对照安全目标给出证据。我一般会准备一张检查表,每项对应一个可演示的动作和一个预期结果,答辩时按表走,评委问什么都能落到具体证据上。
| 验收项 | 演示动作 | 预期结果 |
|---|---|---|
| 身份认证 | 用错误证书连接 | 握手被拒绝并记录日志 |
| 密钥协商 | 连续建立两次连接 | 两次会话密钥不同 |
| 数据机密性 | 抓包查看业务报文 | 看不到明文内容 |
| 完整性 | 篡改密文一字节后发送 | 接收端抛认证失败并断开 |
| 防重放 | 重发上一条报文 | 序号校验拒绝 |
| 异常处理 | 中途断网再重连 | 资源释放,可重新握手 |
演示顺序建议从正常流程开始,再逐项展示攻击被拦截,最后展示日志。日志是加分项,能证明系统“知道自己被攻击了”。答辩陈述控制在八分钟内:两分钟讲需求和威胁模型,三分钟讲架构和关键代码,三分钟演示检查表。被问到没做的部分,直接说“当前威胁模型不覆盖,扩展方向是……”,比硬编强。
代码和文档要能对得上,文档里写的报文格式、密钥长度、算法名称,代码里必须一致。我见过文档写 AES-256、代码里用 AES-128 的情况,被指出来很尴尬。提交前把文档里的每个参数在代码里搜一遍,确认没有出入。
从那以后我每次带课设,都强制要求先写威胁模型和验收表,再动手写代码,返工率明显下降。希望这套结构帮到你,照着改就能交出一份站得住的方案。
本文还有配套的精品资源,点击获取