☰
S/Key一次性口令认证协议Python实现:MD5哈希链原理与代码拆解
2026/10/22 8:17:25 网站建设 项目流程

简介:这是一份面向网络安全与密码学课程学习者的S/Key身份认证协议Python实现源码,适合正在做课程设计、实验报告或想理解一次性口令认证机制的学生与开发者参考。资源包共13个文件,以6个xml配置、2个py源码、2个iml工程文件及gitignore、说明文档为主,压缩包约10KB,体量轻巧便于快速阅读与二次修改。代码基于Python 3.9在Arch Linux与PyCharm环境下开发,完整实现了客户端与服务器交互流程:用户名查询与反馈、Seed拼接MD5哈希、异或运算生成初始口令、多次哈希迭代产生n个一次性口令,以及口令用尽后的初始化逻辑,同时附带用户登录日志记录功能。已有107人学习,读者可借此理清S/Key协议从口令生成、传输到服务器逐次校验的完整链路,掌握哈希链与异或运算在身份认证中的实际用法,并可直接运行调试、对照实验要求补充注释或扩展功能。

1. 从一份 S/Key 认证源码说起:它到底解决了什么问题

口令在网络上裸奔,是很多课程设计和内部小工具的通病。明文传输、服务端存明文、重放攻击随便打——这三个问题只要中一个,整套认证就等于没做。这份基于 Python 实现的 S/Key 身份认证协议代码,正好是拿来拆解「一次性口令」思路的合适样本:客户端和服务端各一个脚本,用 MD5 做单向哈希链,把「每次登录用不同口令」这件事跑通,还带上了用户登录日志记录。

它适合谁?正在做网络安全、密码学或协议课程设计的学生,想找一个能跑起来、能改参数、能看懂认证流程的最小实现;也适合已经工作、但没亲手写过认证协议、想补上这块认知的开发者。整套代码结构不复杂,SKeyClient和SKeyServer两个目录,核心逻辑集中在client.py和server.py,环境是 Python 3.9,在 Arch Linux 上验证过,PyCharm 里直接打开就能读。下面我按「先讲清 S/Key 的哈希链原理,再落到代码怎么跑、参数怎么调、坑在哪」的顺序拆一遍,让你拿到手能复现,而不是只看个热闹。

2. S/Key 协议的哈希链原理:为什么口令只能用一次

S/Key 的核心不是加密,而是「单向哈希链」。理解这一点,后面看代码才不会懵。这一章先把原理和选型理由讲透,再对照源码里的实现位置。

2.1 从 Seed 到口令序列:一次 MD5 加异或的推导

S/Key 的思路是:客户端和服务器共享一个初始种子(Seed),客户端用哈希函数反复迭代,生成一串「倒序使用」的口令。服务器只存上一个用过的口令的哈希值,收到新口令后做一次哈希,比对是否等于存的值。这样即使口令在传输中被截获,攻击者也无法推出下一个口令——因为哈希是单向的。

具体到这份代码,流程是这样的:客户端拿到用户名不存在的反馈后,接收服务器发来的 Seed,把 Seed 和用户名首尾相连,做一次 MD5 哈希。然后取这个哈希值的前 8 位和后 8 位做异或运算,得到一个中间值 S。接下来对 S 连续做 MD5:做 1 次得到第 n 个口令,做 2 次得到第 n-1 个口令,以此类推,做 n 次得到第 1 个口令。客户端把第 1 个口令发给服务器保存,登录时依次使用第 2 到第 n 个口令。

这里有个容易绕晕的点:口令的使用顺序和生成顺序是相反的。先生成的是「最后一个要用」的口令,最后生成的才是「第一个要用」的口令。服务器每次收到口令后做一次 MD5,和存储的上一个口令比对,一致就通过,并把当前口令存下来,供下次比对。这样每次登录用的口令都不同,重放旧口令会被直接拒绝。

提示:n 的取值直接决定这串口令能用多少次。代码里如果没显式暴露 n,通常藏在循环次数或常量里,改之前先确认服务端和客户端用的是同一个值,否则第一次登录就会失败。

2.2 客户端与服务端的职责划分

这份代码把职责切得很干净。客户端负责:连接服务器、提示输入用户名、发送用户名、根据服务器反馈决定是走初始化流程还是直接输入口令、生成哈希链、发送口令。服务端负责:维护用户信息字典、查询用户名是否存在、生成并下发 Seed、接收口令、做一次 MD5 后与存储值比对、记录登录日志。

用户信息字典是服务端的状态核心。它至少存了用户名、当前应该匹配的口令哈希、以及这串口令还剩多少次没用。当用户名不存在,或者存在但 n 个口令已经用完,服务端就走初始化流程:发 Seed,等客户端把第 1 个口令传回来存好。用户名存在且还有剩余口令时,直接提示客户端输入口令。

这种「服务端只存哈希、不存明文」的设计,是它比明文口令方案强的地方。但要注意,S/Key 本身不提供加密通道,口令在传输时虽然是哈希后的值,仍然可能被截获用于当次登录(如果攻击者抢在合法用户之前使用)。所以它通常需要配合其他机制使用,这一点在课程设计里经常被忽略。

3. 把代码跑起来:环境、启动顺序与关键参数

原理清楚了,接下来是动手。这一章按「环境准备 → 启动服务端 → 启动客户端 → 走一遍完整认证」的顺序来,每一步都落到具体操作。

3.1 环境准备与依赖确认

代码标注的环境是 Python 3.9,在 Arch Linux 上验证。实际跑的时候,Python 3.8 到 3.11 一般都能兼容,因为用到的hashlib和socket都是标准库,不需要额外装包。这一点对课程设计很友好——不用折腾虚拟环境和依赖冲突。

先把压缩包解压,目录结构大致是这样:

S-Key的-master/ ├── SKeyClient/ │ ├── .idea/ │ └── client.py ├── SKeyServer/ │ ├── .idea/ │ └── server.py └── 使用说明.txt

.idea是 PyCharm 的工程配置目录,不影响运行,用命令行跑的话可以忽略。确认 Python 版本:

python3 --version # 期望输出 Python 3.9.x 或相近版本

如果系统里同时有 Python 2 和 3,注意用python3而不是python,否则hashlib的调用方式会有差异。这一步看着简单,但确实是新手最容易翻车的地方之一。

3.2 启动服务端:先跑 server.py

服务端要先起来,因为客户端连接时需要目标端口已经处于监听状态。进入SKeyServer目录:

cd S-Key的-master/SKeyServer python3 server.py

服务端启动后会在某个端口上监听(常见做法是绑定127.0.0.1加一个固定端口,比如 9999 或 8888,具体看server.py里的bind调用)。如果启动时报Address already in use,说明端口被占用了,要么关掉占用进程,要么改server.py里的端口号,同时记得客户端也要改成一样的。

服务端跑起来后,通常会打印一行监听提示。这时候别关终端,另开一个终端跑客户端。用户信息字典在服务端进程内存里,重启服务端会清空所有已注册用户和口令状态——这一点在调试时要有心理准备,别以为是代码写错了。

3.3 启动客户端并走完一次认证

另开一个终端,进入SKeyClient目录:

cd S-Key的-master/SKeyClient python3 client.py

客户端会提示输入用户名。第一次用一个不存在的用户名,比如testuser,观察完整流程:

请输入用户名: testuser # 服务端返回用户名不存在,下发 Seed # 客户端生成哈希链,发送第 1 个口令 # 服务端保存,提示初始化完成

然后再用同一个用户名登录一次,这次服务端会发现用户名存在且口令未用完,直接提示输入口令。客户端用第 2 个口令登录,服务端做一次 MD5 后与存储值比对,一致则通过,并更新存储值为当前口令。

这里有个细节值得注意:客户端生成哈希链后,需要把「当前用到第几个口令」这个状态记住。如果每次登录都重新生成整条链,那就永远在用第 1 个口令,第二次登录必然失败。代码里通常用一个计数器或列表索引来跟踪,读client.py时重点看这块。

3.4 关键参数:n 值、Seed 和端口

三个参数决定了这套代码能不能正常跑:

参数作用调整位置注意事项
n哈希链长度,决定口令可用次数客户端生成循环、服务端剩余次数判断两端必须一致
Seed哈希链初始种子服务端生成、客户端接收每次初始化应不同
端口客户端连接目标server.py 的 bind、client.py 的 connect两端必须一致

n 值如果设得太小,比如 3,那登录三次后就要重新初始化,测试起来频繁触发初始化分支,反而不好观察正常登录流程。常见做法是设成 10 到 100 之间的值,既能跑多次,又不会让生成过程太慢。Seed 如果写死成固定字符串,安全性会打折扣,但作为课程设计演示够用;想改的话,服务端用随机数生成即可。

4. 避坑与排查:认证失败时先看这几处

这套代码逻辑不复杂,但认证类程序的特点是「错一步就全错」,而且报错信息往往不直观。下面几条是我在拆这类 S/Key 实现时最常遇到的坑,按「现象 → 原因 → 解决」列出来。

现象一:客户端连不上服务端,报 Connection refused。原因通常是服务端没启动,或者端口不一致。先确认服务端终端还在运行、没有报错退出;再对比server.py里的bind端口和client.py里的connect端口是否相同。如果服务端绑定的是127.0.0.1,客户端也只能连127.0.0.1,连localhost在某些系统上解析结果不同,可能连不上。

现象二:第一次登录成功,第二次登录提示口令错误。这是最典型的哈希链顺序问题。原因多半是客户端每次登录都重新生成了整条链,导致一直在发第 1 个口令,而服务端已经把它更新成了第 2 个口令的哈希。解决方法是检查客户端是否维护了口令索引,确保第二次登录发的是第 2 个口令。如果代码里没有这个状态维护,需要自己加一个计数器。

现象三:用户名存在但口令已用完,初始化后仍然登录失败。原因可能是服务端在初始化时没有正确重置剩余次数,或者客户端初始化后没有把第 1 个口令正确发送。排查时在服务端打印用户信息字典的内容,看口令哈希和剩余次数是否被更新。另一个可能是 Seed 在初始化时变了,但客户端用的还是旧 Seed 生成的链。

现象四:MD5 结果对不上,服务端比对始终失败。原因通常是编码问题。Python 3 里字符串做哈希前必须先encode(),如果一端用了utf-8,另一端用了默认编码,或者干脆忘了编码,哈希值就会不同。统一用encode('utf-8')是最稳妥的做法。另外,前 8 位和后 8 位异或时,如果取的是字符串切片而不是字节切片,结果也会不一样。

现象五:服务端重启后所有用户都要重新初始化。这不是 bug,是设计使然——用户信息字典存在内存里,没有持久化。如果希望重启后保留状态,需要把字典写到文件或数据库。课程设计里通常不要求持久化,但演示时要注意别中途重启服务端。

注意:调试认证流程时,在服务端把收到的口令和存储的口令哈希都打印出来,比盯着客户端猜要快得多。这类程序的黑匣子属性很强,不打日志基本靠玄学。

5. 进阶玩法:把日志、持久化和参数校验补上

跑通基本流程之后,这份代码还有几个可以动手改的方向,既能加深对 S/Key 的理解,也能让课程设计更有说服力。

第一个是登录日志。代码里已经提到「用户登录日志记录」,但具体记了什么、记在哪,需要看server.py的实现。常见做法是每次认证成功或失败时,往一个列表或文件里追加一条记录,包含时间戳、用户名、结果。如果想做得细一点,可以区分「初始化」「正常登录」「口令耗尽」三种事件。改的时候注意日志写入不要阻塞主认证流程,简单场景直接追加就行。

第二个是用户信息持久化。把内存字典换成 JSON 文件或 SQLite,服务端重启后状态还在。用 JSON 的话,注意哈希值是二进制转成的十六进制字符串,读写时不用额外编码;用 SQLite 的话,建一张users表,字段包括用户名、当前口令哈希、剩余次数、Seed。这一步做完,整套东西就更像一个能用的最小系统,而不是一次性的演示脚本。

第三个是参数校验。现在的代码对输入基本是「给什么用什么」,用户名传空、口令传超长字符串,都可能出问题。加一层简单校验:用户名非空且长度合理,口令是十六进制字符串且长度符合 MD5 输出。这些校验放在服务端做,客户端也做一遍,能挡掉大部分低级错误。

最后一个技巧是关于 n 值的动态化。现在 n 大概率是写死的,可以改成服务端在初始化时根据安全需求下发,客户端按收到的 n 生成链。这样同一套代码能适配不同强度要求,也顺便练了「协议参数协商」这个思路。

我自己的习惯是,每次改完这类认证代码,都强制走一遍「不存在用户初始化 → 连续登录到口令耗尽 → 重新初始化」的完整循环,确认三个分支都能正确切换。从那以后我每次碰认证逻辑,都先把状态机画清楚再动手,省得在分支里绕晕。希望这份拆解帮到你,拿到代码后先跑通,再按上面的方向改,比一上来就啃源码要顺得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询