简介:这份网络安全课程设计资料围绕加密技术展开,面向计算机与网络安全相关专业学生及需要完成课程设计、实验报告的人群,可用于理解 RSA 等公钥加密算法的原理、实现流程与报告撰写框架。压缩包共 2 个文件,包含 1 个 doc 课程设计报告和 1 个 c 语言源代码文件,整体约 596KB,报告与代码相互对应,便于对照阅读算法说明、模块设计与程序实现。已有 562 人学习下载,具备一定的参考与交流价值。读者可从中获取课程设计选题组织、加密算法关键步骤梳理、RSA 程序实现思路以及实验结论与文档排版参考,适合用作课程作业撰写、代码复现和答辩准备时的辅助材料,也能为后续深入学习密码学与安全编程提供入门参照。
1. 打开压缩包先看什么:一份能跑通的网络安全课程设计长什么样
“网络安全课程设计加密技术报告及源代码”这个名字乍看像期末作业打包,拆开才会发现里面是两套东西:一套讲原理的文档,一套把原理跑起来的代码。同类资源最常见的毛病不是算法写错,而是文档和代码互相对不上——报告里画满 AES 轮函数,答辩时被问一句“密钥怎么从口令派生出来”就接不上。这份资源包的价值在于两边能对上号:对称加密、非对称加密、消息摘要、数字签名四条线,每条都有可执行的 Demo,报告里的流程能在源码里找到对应函数。它适合正在做安全方向课程设计、需要当场演示的学生,也适合写过业务代码、第一次把加密原语落到文件读写上的开发者。下面按“结构—模块—跑通—改造”的次序拆。
2. 资源包结构拆解:报告、源码与测试数据怎么分工
拿到一个带文档的资源包,先不要急着打开报告。我的习惯是先看目录树,因为目录结构直接暴露了作者对项目的理解程度:公共逻辑有没有单独抽出来,测试数据是不是齐全,入口文件是否清晰。这套资源的组织方式偏工程化,docs、src、tests三层分开,源码里也没有把四个算法全塞进一个main.py,这在课程设计里算比较讲究的写法。
2.1 目录树:先认清哪几个文件是入口
crypto-course-design/ ├── docs/ │ ├── report.docx # 课程设计报告正文 │ ├── report.pdf # 导出稿,答辩打印用 │ └── defense.pptx # 答辩幻灯片 ├── src/ │ ├── crypto_utils.py # 密钥派生、填充、哈希等公共函数 │ ├── aes_file.py # 对称加密:文件加解密入口 │ ├── rsa_sign.py # 非对称:密钥对生成、签名、验签 │ ├── hash_check.py # 摘要:单文件/批量校验 │ └── hybrid_demo.py # 混合加密:RSA 包会话密钥 + AES 加密数据 ├── tests/ │ ├── sample.txt # 小文本,快速验证 │ ├── sample.bin # 二进制样本,验证分块边界 │ └── run_all.sh # 一键跑完四个模块 ├── requirements.txt └── README.md真正需要先读的是crypto_utils.py,四个模块都依赖它,填充规则、密钥派生参数、文件头格式都定义在这里。如果这一层没统一,后面四个 Demo 很容易出现“A 文件加密的结果 B 文件解不开”的尴尬情况,这在课程设计验收时非常常见。tests/sample.bin是用来验证分块边界的,文件大小接近 64KB,正好能触发“最后一块是否需要填充”的分支,不要跳过它直接用文本文件测试。
2.2 技术选型:Python + pycryptodome 的取舍
实现语言是 Python 3,依赖集中在requirements.txt,核心库是pycryptodome。安装方式很直接:
# 建议先建虚拟环境,避免和系统 Python 的包打架 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt为什么选它而不是cryptography?两个原因。第一,pycryptodome的接口更贴近教材:AES.new(key, AES.MODE_CBC, iv)这一行和报告里“分组密码 + 工作模式 + 初始向量”的描述几乎一一对应,答辩时对照着讲很顺;cryptography的高层封装更工程化,但把细节藏得深,反而不利于讲清原理。第二,RSA 签名部分用的是 PKCS#1 v1.5 填充,pycryptodome提供了pkcs1_15这个独立模块,签名、验签各一行,学生能看懂每一步在做什么。
提示:如果报告里明确要求“自行实现 AES 算法”,不要在文件加密主链路里用纯 Python 手写版。手写实现适合放在附录里跑 NIST 测试向量证明正确性,性能和边界处理都撑不住真实文件,容易在演示环节暴露问题。
还有一种常见误用是把hashlib和pycryptodome的哈希接口混着用,比如一边用hashlib.sha256()算摘要,一边用Crypto.Hash.SHA256验签。两者输出一致,但对象类型不同,传给pkcs1_15.verify()时会报类型错误。资源包里统一用Crypto.Hash.SHA256,就是为了避开这个坑。
2.3 报告骨架:验收时会被追问的六个章节
文档部分不是凑页数的,它的结构和源码是映射关系。下面这张表是我整理出来的对应关系,改代码时最好同步改报告,不然答辩现场对不上很难看:
| 报告章节 | 要写清的内容 | 对应源码 |
|---|---|---|
| 需求分析 | 威胁模型:保护什么、不保护什么 | README 使用说明 |
| 算法原理 | AES 分组与轮函数、RSA 数学基础、SHA-256 压缩函数 | docs 附录 |
| 方案设计 | 文件格式、密钥管理、模块划分 | crypto_utils.py |
| 核心实现 | 关键函数签名与调用流程 | aes_file.py、rsa_sign.py |
| 测试与结果 | 功能用例、性能数据、异常用例 | tests/run_all.sh |
| 安全性分析 | 已知攻击面与方案局限 | 报告正文 |
最容易写空的是“安全性分析”这一节。合格的写法不是罗列“AES 很安全”,而是说清楚这套方案防住了什么、没防住什么:它能防住文件被直接读取,但防不住口令被键盘记录;CBC 模式保证了密文块之间有关联,但没有完整性认证,攻击者翻转密文比特仍能造成明文可预测的变化。把局限写出来,比堆十页原理更能体现你真的想过。
3. 对称加密模块:AES-CBC 从口令派生到分块写盘的完整链路
对称加密是整个资源包里代码量最大的一块,也是二次开发时改动最频繁的地方。很多课程设计停在这一步:能对一段字符串做加解密,一换成文件就出问题。原因通常不在 AES 本身,而在密钥怎么来、文件怎么分块、尾部怎么处理这三个工程细节上。aes_file.py把这三件事拆开写,结构值得照着抄。
3.1 密钥派生:口令不能直接截断成 16 字节
初学者最常见的写法是key = password.encode().ljust(16, b'\0'),把口令补零凑够 16 字节直接当密钥。这种做法有两个硬伤:一是口令熵太低,攻击者拿字典跑几十万次就能撞出来;二是补零后不同口令可能产生相同密钥。正确做法是用 PBKDF2 做密钥派生,把迭代次数拉高,让每次尝试的计算成本变大。
import os from Crypto.Hash import SHA256 from Crypto.Protocol.KDF import PBKDF2 from Crypto.Random import get_random_bytes SALT_LEN = 16 KDF_ITER = 200_000 # 迭代次数,课程设计里写到报告中的安全参数 def derive_key(password: str, salt: bytes) -> bytes: """口令 + 盐 -> 32 字节 AES-256 密钥""" return PBKDF2( password, salt, dkLen=32, # AES-256 要 32 字节 count=KDF_ITER, hmac_hash_module=SHA256, ) def new_salt() -> bytes: # 每个文件独立随机盐,绝不能全局写死 return get_random_bytes(SALT_LEN)dkLen=32对应 AES-256,如果报告里写的是 AES-128,这里改成 16 即可,但密钥长度和AES.new会自动匹配,不需要额外传参。count是迭代次数,调高更安全但加密变慢,20 万次在现代机器上大约是几百毫秒,作为课程设计的安全参数足够。salt必须每个文件随机生成并随密文一起存储,它的作用不是保密,而是让相同的口令派生不出相同的密钥,从而阻止彩虹表批量破解。
还有一个细节:PBKDF2的第一个参数既接受str也接受bytes,但内部编码方式不同。统一传str更稳,避免在 Windows 和 Linux 上因为默认编码差异派生出不同密钥,导致“同一口令换台机器就解不开”的异常情况。
3.2 文件头设计:魔数、原始长度、盐与 IV
加密文件不能只写密文,否则解密端不知道盐和 IV 在哪,也不知道明文原本多长。aes_file.py在文件开头写了一段固定长度的头:
import struct MAGIC = b'ENCF' # 4 字节魔数,用来识别自家格式 HEAD_LEN = 4 + 8 + SALT_LEN + 16 # 魔数 + 原长 + 盐 + IV def build_header(size: int, salt: bytes, iv: bytes) -> bytes: # >Q 表示大端 8 字节无符号整数,跨平台字节序一致 return MAGIC + struct.pack('>Q', size) + salt + iv四个字段各有用途。MAGIC是格式标识,解密前先比对,能挡住“拿错文件”这类低级错误;原始长度用 8 字节大端存储,解密完按这个长度截断,避免填充字节混进明文;盐和 IV 都是 16 字节,直接拼在头里。整个头 44 字节,读取时一次性读满再解析,不要用多次read(4)、read(8)这种写法,否则文件句柄位置很容易算错。
注意:IV 不需要保密,但必须每次加密都重新随机生成。CBC 模式下如果两次加密用同一个密钥和同一个 IV,相同的前缀会产生相同的密文,攻击者能直接看出来两个文件开头一样。这一点在报告的安全性分析里值得单独写一段。
3.3 分块加解密实现与 20MB 文件回归验证
文件读写用固定大小的块循环,不要一次性read()整个文件。块大小取 64KB,必须是 16 的整数倍,因为 AES 分组长度是 16 字节,CBC 要求每个输入块对齐。
from Crypto.Cipher import AES CHUNK = 64 * 1024 # 64KB,16 的整数倍 def _pad(data: bytes) -> bytes: """PKCS#7 填充:缺 n 字节就补 n 个值为 n 的字节""" n = 16 - len(data) % 16 return data + bytes([n]) * n def _unpad(data: bytes) -> bytes: n = data[-1] if n < 1 or n > 16: raise ValueError('填充非法:口令错误或密文被篡改') return data[:-n] def encrypt_file(src: str, dst: str, password: str) -> None: salt, iv = new_salt(), get_random_bytes(16) cipher = AES.new(derive_key(password, salt), AES.MODE_CBC, iv) size = os.path.getsize(src) with open(src, 'rb') as fin, open(dst, 'wb') as fout: fout.write(build_header(size, salt, iv)) while True: chunk = fin.read(CHUNK) if len(chunk) < CHUNK: # 尾块(含空文件)必须填充 fout.write(cipher.encrypt(_pad(chunk))) break fout.write(cipher.encrypt(chunk)) def decrypt_file(src: str, dst: str, password: str) -> None: with open(src, 'rb') as fin: header = fin.read(HEAD_LEN) if len(header) < HEAD_LEN or header[:4] != MAGIC: raise ValueError('不是本工具生成的加密文件') size = struct.unpack('>Q', header[4:12])[0] salt, iv = header[12:28], header[28:44] cipher = AES.new(derive_key(password, salt), AES.MODE_CBC, iv) written = 0 with open(dst, 'wb') as fout: while True: block = fin.read(CHUNK) if not block: break plain = cipher.decrypt(block) if len(block) < CHUNK: # 最后一块去掉 PKCS#7 填充 plain = _unpad(plain) take = plain[:size - written] # 按原始长度截断 fout.write(take) written += len(take) if written != size: raise ValueError('长度校验失败,密文可能被截断')这段代码有三个关键点。第一,加密循环用len(chunk) < CHUNK判断尾块,包括空文件的情况——空文件第一次读就返回b'',_pad(b'')会补出 16 个0x10,解密端能正确还原成空文件。第二,如果文件大小正好是 64KB 的整数倍,循环会多走一轮读到空块,再补一个完整填充块,这是 PKCS#7 的标准行为,不是 bug。第三,解密端的plain[:size - written]负责把填充字节切掉,同时兜住“密文被追加垃圾数据”的情况。
验证时别只用一个小文本文件。准备三个样本:空文件、1 字节文件、正好 64KB 的文件,再加一个 20MB 的二进制文件,跑一轮“加密—解密—cmp比对”:
# Linux/macOS 下逐字节比对,输出为空即一致 cmp tests/sample.bin tests/sample.bin.dec && echo "round-trip OK"Windows 下没有cmp,可以用certutil -hashfile 文件 SHA256分别算两个文件的摘要比对,效果一样。这一步跑通,对称加密模块基本就稳了。
4. 非对称与摘要模块:RSA 密钥对、数字签名与 SHA-256 校验
对称加密解决的是“内容不被看到”,非对称和摘要解决的是另外两个问题:密钥怎么安全传递、文件有没有被改过。rsa_sign.py和hash_check.py加起来代码量不大,但概念密度高,答辩时被追问的概率也最高。很多课程设计把 RSA 写成“加密一段短字符串就完事”,其实数字签名的链路更能体现对非对称体系的理解。
4.1 密钥对生成与 PEM 文件落盘
RSA 密钥对生成只需要一行核心调用,但落盘格式和权限处理有讲究。资源包里统一导出成 PEM,私钥用 PKCS#8 格式,公钥单独存一个文件,方便分发给验签方。
from Crypto.PublicKey import RSA def gen_keypair(priv_path='private.pem', pub_path='public.pem', bits=2048, passphrase=None): """生成 RSA 密钥对并写入 PEM 文件""" key = RSA.generate(bits) # 私钥加口令保护,导出时用 PKCS#8 结构 priv_pem = key.export_key( format='PEM', pkcs=8, passphrase=passphrase, # None 表示不加密私钥 protection='PBKDF2WithHMAC-SHA1AndAES256-CBC', ) with open(priv_path, 'wb') as f: f.write(priv_pem) os.chmod(priv_path, 0o600) # 私钥只允许本人读写 with open(pub_path, 'wb') as f: f.write(key.publickey().export_key(format='PEM'))bits=2048是课程设计里比较稳妥的选择,1024 位现在偏弱,4096 位生成密钥会明显变慢,演示时容易卡住。passphrase参数建议留空时也要在报告里说明“私钥未加密存储,仅适用于本地演示”,把这当作已知局限写进安全性分析,比假装没这回事更专业。os.chmod在 Windows 上不起作用,但加上不会报错,跨平台代码里保留它没有坏处。
读取密钥时用RSA.import_key(),它同时接受文件路径对应的字节串和带口令的私钥:
key = RSA.import_key(open('private.pem', 'rb').read(), passphrase='你的口令')口令错误会抛ValueError: Incorrect padding,这个异常信息很误导人,看起来像填充问题,实际是口令不对,遇到时不要往加密模式上排查。
4.2 签名链路:先摘要再签,顺序反了必挂
数字签名的正确顺序是:对原文算摘要,再用私钥对摘要签名;验签时用公钥解出摘要,和自己重新算的摘要比对。pkcs1_15这个模块已经把摘要计算和填充打包好了,调用时传进去的必须是哈希对象,不能是原始数据。
from Crypto.Hash import SHA256 from Crypto.Signature import pkcs1_15 def sign_file(path: str, priv_path: str, passphrase=None) -> bytes: """对文件内容签名,返回签名字节串""" key = RSA.import_key(open(priv_path, 'rb').read(), passphrase=passphrase) h = SHA256.new() with open(path, 'rb') as f: for chunk in iter(lambda: f.read(8192), b''): h.update(chunk) # 分块喂给哈希,避免大文件占内存 return pkcs1_15.new(key).sign(h) def verify_file(path: str, signature: bytes, pub_path: str) -> bool: key = RSA.import_key(open(pub_path, 'rb').read()) h = SHA256.new() with open(path, 'rb') as f: for chunk in iter(lambda: f.read(8192), b''): h.update(chunk) try: pkcs1_15.new(key).verify(h, signature) return True except (ValueError, TypeError): return False # 验签失败统一返回 Falseiter(lambda: f.read(8192), b'')这个写法值得记住,它等价于一个循环读到文件尾,但不需要手写while,在处理大文件时比f.read()一次性读取安全得多。verify失败会抛ValueError,不要用assert包住它,否则在生产环境里开启-O优化会被直接跳过,签名验证形同虚设。
签名的典型用途是“防篡改”:把签名和文件一起发给对方,对方拿到后用公钥验签,验不过说明文件被动过。它不提供保密性,文件本身还是明文,这一点在报告里要和加密区分清楚。
4.3 文件完整性校验与批量测试脚本
hash_check.py做的事情更简单:计算文件 SHA-256,输出十六进制摘要,支持批量比对。它和签名的区别在于没有密钥,任何人都能重新算一份摘要,所以只能防意外损坏,防不住故意篡改。课程设计里经常把两者混为一谈,实际上摘要用于校验传输完整性,签名用于确认来源。
def sha256_file(path: str) -> str: h = SHA256.new() with open(path, 'rb') as f: for chunk in iter(lambda: f.read(65536), b''): h.update(chunk) return h.hexdigest() def check_manifest(manifest_path: str) -> int: """逐行读取 '摘要 文件路径',返回不一致的条数""" bad = 0 with open(manifest_path, encoding='utf-8') as f: for line in f: digest, _, filepath = line.strip().partition(' ') if not filepath: continue actual = sha256_file(filepath) if actual != digest: print(f'[FAIL] {filepath}\n 期望 {digest}\n 实际 {actual}') bad += 1 return bad清单文件用两个空格分隔,是为了兼容sha256sum生成的格式,这样在 Linux 上可以直接用系统命令交叉验证,不必完全信任自己写的代码。tests/run_all.sh里就是先跑一遍自己的脚本,再跑一遍sha256sum -c,两边结果一致才算通过。
5. 避坑与排查:加密 Demo 跑不通的五个现场
加密代码的报错信息普遍不友好,同一个异常可能对应三四种原因。下面这五条是这类课程设计资源里出现频率最高的翻车现场,每条都按“现象—原因—解决”记录,遇到问题时按顺序排查,能省下大量时间。
5.1 解密抛 ValueError: Padding is incorrect
现象:加密正常,解密时抛ValueError: Padding is incorrect,换小文件又能成功。
原因:九成是口令输错,或者文件不是用当前口令加密的。CBC 解密出来的最后一个块是随机字节,_unpad读到末字节不在 1 到 16 之间就报这个错。如果文件大小接近块边界,错误口令偶尔也能“碰巧”通过填充校验,但解出来的内容是乱码,所以不能只看有没有报错,还要比对内容。
解决:先把口令打印出来核对,确认没有首尾空格;再确认加密和解密用的是同一个KDF_ITER和dkLen,参数不一致会导致派生出的密钥完全不同,表现和口令错误一模一样。排查时可以在derive_key里临时打印密钥前 4 字节的十六进制,两边对比,确认派生环节没问题再往下查。
5.2 换了口令仍能解开:盐值写死的隐蔽坑
现象:修改了代码里的口令,重新加密一个文件,用旧口令居然还能解开。
原因:盐值被写成了常量,比如salt = b'0123456789abcdef'。盐固定后,口令派生出的密钥只由口令决定,旧口令对应的密钥没变,自然能解开。这种情况在只测一个文件的场景下很难发现,因为加密解密都用同一份代码。
解决:检查new_salt()是否每次调用都走get_random_bytes,并确认盐被写进了文件头而不是从常量读取。验证方法是连续加密同一个文件两次,比较两个密文的头部字节,盐和 IV 都应该不同;如果完全相同,说明随机源没生效。
5.3 大文件加密内存暴涨:read() 不带参数
现象:加密一个几百 MB 的视频文件,进程内存直接飙到几个 G,机器开始卡顿甚至被系统杀掉。
原因:fin.read()不带参数会一次性把整个文件读进内存,再交给cipher.encrypt()。AES 本身按块处理,但 Python 层的字节串复制会额外占一份内存,文件越大越明显。
解决:所有文件读写都带块大小参数,按 64KB 循环。检查代码里有没有f.read()、f.read(-1)这种写法,全局搜一遍。哈希计算同样要注意,SHA256.new(f.read())也是同样的坑,改成循环update。
5.4 RSA 直接加密文件报 Plaintext is too long
现象:想用 RSA 直接加密一个几 KB 的文本文件,抛ValueError: Plaintext is too long。
原因:RSA 能加密的数据长度受模数限制,2048 位密钥配合 PKCS#1 v1.5 填充,单次最多加密 245 字节。文件稍微大一点就超限,这不是代码写错,是算法本身的约束。
解决:改用混合加密。随机生成一个 32 字节的 AES 会话密钥,用 RSA 公钥加密这个会话密钥,文件内容用 AES 加密,解密时先用 RSA 私钥解出会话密钥再解文件。hybrid_demo.py就是这条链路的最小实现。报告里要写清楚“非对称加密只用来传密钥,不直接加密数据”,这是得分点。
5.5 Windows 下中文路径报 UnicodeDecodeError
现象:在 Windows 上对含中文的路径执行加解密,抛UnicodeDecodeError或直接找不到文件。
原因:open()在部分环境下默认编码不是 UTF-8,路径里的中文被按 GBK 解码后传给系统调用,文件名对不上。这个问题在跨平台代码里很常见,Linux 上一切正常,换到 Windows 就失败。
解决:文本文件读写显式传encoding='utf-8';路径参数统一用pathlib.Path包装,避免字符串拼接;命令行入口用argparse接收参数时,中文路径在 Windows 终端里可能被转义,建议直接用引号包住路径测试。如果仍然失败,检查 Python 版本,3.6 以后的文件系统编码在 Windows 上默认是 UTF-8,老版本需要额外设置。
6. 二次开发:把这套 Demo 改成能交付的文件加密工具
课程设计交上去只是开始,这套代码真正有价值的地方是它能当一个小工具的骨架。我拿到手后做的第一件事是把 CBC 换成 GCM,原因很直接:CBC 只保证机密性,不保证完整性,攻击者翻转密文比特能让明文产生可预测的变化,而 GCM 自带认证标签,解密时标签对不上直接报错,省去自己拼 HMAC 的麻烦。
from Crypto.Cipher import AES from Crypto.Random import get_random_bytes def encrypt_gcm(plain: bytes, key: bytes) -> tuple: cipher = AES.new(key, AES.MODE_GCM, nonce=get_random_bytes(12)) ct, tag = cipher.encrypt_and_digest(plain) # nonce、tag 都要随密文一起存,缺一不可 return cipher.nonce, tag, ct def decrypt_gcm(nonce: bytes, tag: bytes, ct: bytes, key: bytes) -> bytes: cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) return cipher.decrypt_and_verify(ct, tag) # 标签不对直接抛异常改造时有三处要同步动:文件头要加 nonce 和 tag 字段,长度从 44 字节变成 12 + 16 + 44;decrypt_file里的_unpad调用要删掉,GCM 是流式模式,不需要填充;tests/run_all.sh要加一条篡改用例,把密文任意一个字节取反,确认解密会失败而不是返回乱码。这三步做完,才算真正换掉了加密模式,而不是只在代码里改了个常量。
再往下走可以做命令行参数化,用argparse把encrypt、decrypt、sign、verify四个子命令挂上去,入口统一成python -m crypto_tool encrypt 输入 输出,交付给同学使用时不用改代码。进度显示用tqdm包一层循环即可,大文件加密时体感提升明显。
最后说个血泪经验。我做这类文件加密功能,最开始只测正常大小的文本文件,结果答辩现场用一个正好 64KB 的二进制样本演示,解密出来的文件末尾多了 16 个字节,当场翻车。从那以后我每次交付带加密功能的模块,都强制先跑一遍空文件、1 字节文件、正好 64KB 文件这三个边界样本,再谈功能。资源包里的tests/目录就是按这个习惯整理的,报告和源码都在里面,照着边界用例跑一轮再改,能少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取