










TOTP原理解析
高中的时候,我缠着我父亲要去办银行卡,当时为了网购开了网上银行,银行给了我一个U盾,每次从网上转账的时候,都要按一下U盾,U盾上显示一串数字,我把数字输到银行的网站里就能把钱转走,当时觉得这个操作很神奇。后来上了大学玩游戏发现很多游戏厂商都要强制人绑定一个与银行U盾功能差不多的验证码,后来我知道这叫2FA(Two Factor Authentication),也知道了这种U盾被叫做OTP(one-time password),昨天在用TOTP(Time-Based One-Time Password)的时候突然想到我竟然没有研究过TOTP是如何保证安全性的,于是写下本文,以窥探“时间带来的安全”。
本文主要简单回顾一下2FA、OTP的演化,以及HOTP(HMAC-Based One-Time Password)、TOTP的实现。
想必大家都经历过使用密码登录APP后,APP还要进行短信验证,甚至还要在一堆图片中挑出红绿灯、自行车等,你可能会想:我都输对了密码,为什么还要这么折磨我?后来我才知道这种多次验证身份的操作叫双因素验证,即2FA。2FA除了验证用户设置的密码之外,还会验证用户“拥有的东西”,比如:手机、U盾、指纹、面容等。攻击者通过其他渠道获取了你的用户名、密码,如果没有启用2FA验证,那么攻击者就可以直接使用偷取的用户名密码登录你的账户;如果你启用了2FA验证,那么攻击者就需要去验证你“拥有的东西”,而这些东西都是你贴身,甚至你独一无二的东西,攻击者无法通过远程的方式窃取。2FA的核心就是为一道门增加一种不同的锁,把门锁增加成两个不同类型。而且这两个锁不是简单的1+1=2,因为同时解锁两个不同类型锁的难度远大于一个锁。
想象一下,你家的防盗门:密码就是钥匙,2FA就像防盗门的锁外加了一把密码锁,虽然小偷能撬开防盗门的锁,但是却不知道密码,不能打开密码锁,密码锁的密码被你放在银行的保险柜里,小偷是无论如何都不可能去银行里偷这个密码锁的密码的。
常见2FA组合有:
综合下来,TOTP算是安全性、便捷性比较有性价比的2FA手段了。
要想了解TOTP的话,就应该先了解一下TOTP的前身HOTP。
HOTP全称是基于HMAC的一次性密码(HMAC-based One-time Password),是一种基于散列消息验证码的一次性密码算法,在2005年由IETF发布在RFC 4226标准文档中。
HOTP的核心思想,其实可以用一句话概括:服务器和你的令牌,各自悄悄藏着一模一样的两样东西——一把“秘密钥匙”和一个“计数器”。 它们用同一套固定的算法去搅拌这俩东西,因为输入相同、算法相同,算出来的数字必然相同;而攻击者没有那把钥匙,就永远算不出来。
这里有个关键角色叫 HMAC。要理解它,得先认识 SHA-1——一台“搅拌机”。你往里扔任何东西,它都吐出固定 20 字节的“乱码”,同样的输入永远得到同样的输出,但你没法从输出反推出输入(这叫单向性)。而 HMAC 给这台搅拌机配了一把钥匙,让它变成“认钥匙的搅拌机”:没有这把钥匙,盖不出相同的印章;换了钥匙,盖出来的印章完全不一样。一句话记——HMAC(钥匙, 消息) 就是一台认钥匙的搅拌机。
有了 HMAC,HOTP 的算法就只剩三步:
为什么要“让数据自己说从哪取”,而不是固定取前 4 字节?因为这样每次取的位置会随机落在 20 字节里的不同地方,避免不同密码之间出现规律性的关联。这就是 RFC 里所说的“动态截断”(Dynamic Truncation)。
举个真实的例子(RFC 里的标准数据)。假设 HMAC 算出来的 20 字节长这样:
位置: 00 01 02 ... 10 11 12 13 ... 19
数值: 1f 86 98 ... 50 ef 7f 19 ... 5a5a,它的低 4 位是 a(十进制 10),所以从第 10 个位置开始取;50 ef 7f 19,当成数字是 1,357,872,921;这就是那一刻 U 盾上显示的那串数字。
HOTP 有个根本性的尴尬:它的计数器,是交给人来“按”的。
这就解释了我小时候那个 U 盾为什么非得按一下——它在让计数器 +1。可“交给人的按钮”会带来一连串麻烦:
问题的根源其实只有一句话:HOTP 把“走几步”这件事,托付给了会犯错的人。 那能不能把这个责任,交给一个永远不会犯错的东西呢?——时间,正好永不犯错。于是就有了 TOTP。
把“按按钮的次数”换成“时间”
TOTP 全称是基于时间的一次性密码(Time-Based One-time Password),2011 年发布在 RFC 6238。它和 HOTP 的关系,一句话就能说清:
TOTP 就是 HOTP,只不过把“计数器”的来源,从“按按钮的次数”换成了“当前时间”。
搅拌、截取、取模那一整套机器,原封不动照搬。
这一换,HOTP 那四个麻烦就全解了:
说到底,TOTP 相对 HOTP 的全部进步,就是把对“人”的依赖,换成了对“时间”的依赖。人会因为手抖、健忘、嫌麻烦而出错;而时间,每过 30 秒就精准地走一步,谁也别想让它多走或少走。
那“时间”是怎么变成“计数器”的呢?很简单:
计数器 T = 当前 Unix 时间戳 ÷ 30 (向下取整)Unix 时间戳,就是从 1970 年 1 月 1 日 0 点到现在一共过了多少秒。把它除以 30,就是“已经过去了多少个 30 秒”。每隔 30 秒,T 就 +1,密码就跟着变一次。
为什么是 30 秒?这是安全和好用之间的折中。太长了,密码有效时间久,被偷看到的攻击窗口就大;太短了,你手还没敲完密码它就过期了,体验很差。30 秒是实践出来的甜点。
因为全世界所有人的钟都基于同一套标准时间(UTC),所以同一时刻,你和服务器算出的 T 永远相同,再加上同一把钥匙,双方就能在完全离线、互不通网的情况下,各自算出同一个密码。这也解释了为什么手机上的验证器 App 不需要联网就能蹦出数字——它只需要那把钥匙,和你手机自己的时钟。
这里有个特别妙的验证,能让你彻底相信“TOTP 就是 HOTP 换了个计数器”:
1094287082,取 6 位是 287082;1094287082 对 10⁸ 取余 = 94287082,正是 TOTP(59) 的值。同一个“1”,同一个中间数,只是最后截取的位数不同。 搅拌机是同一台,只不过是时间在替你按按钮。
当然,用时间也有新的麻烦:时钟会有偏差。你的手机如果快了几秒、慢了几秒,或者出国忘了调时区,算出来的 T 可能就跟服务器对不上。TOTP 的解法是:服务器验证时,不只看当前这一个 T,而是前后各放宽几步(一般 ±1 步,即 ±30 秒)一起试,只要命中任意一个就算通过。这就是为什么你刚生成的密码有时隔几秒还能用,但过一会儿就失效了——容错窗口在起作用。
了解了原理,再回头看安全性,会发现一件有意思的事:不管是 HOTP 还是 TOTP,最后都是一串 6~8 位数字,理论上只有一百万到一亿种可能,暴力穷举似乎很容易。那它凭什么安全?
靠的是两件事:
成功概率 ≈ (容错窗口 × 尝试次数) / 10⁶,只要尝试次数被限死,暴力破解就基本不可能。所以“时间”带来的安全,本质是:用一把共享的钥匙给“当前时间”盖一枚无法伪造的印章,再配合服务器对尝试次数的严格限制,让“猜”变得不现实。 而“时间”这个维度,相比 HOTP 的“按按钮”,最大的贡献是——它让验证变成了零操作:你什么都不用做,时间自己在走,密码自己在变,体验丝滑,安全照旧。
能防穷举,不代表什么问题都防住了。TOTP 的弱点,不在算法本身,而在它身处的那个更大的系统里。
所以结论很有趣:TOTP 防住了"远程猜到密码"这一关,却挡不住"你主动输给钓鱼网站"和"服务器里的密钥被人端走"这两关。 它是一个优秀的"第二把锁",但别忘了——锁永远只是整体安全链条里的一环。真正的安全,是链上最弱的一环说了算。
写到这里,我才算真正搞懂了童年那把 U 盾:它当年按一下蹦出来的那串数字,不是什么魔法,而是 HMAC 搅拌机和计数器的一次合谋。而今天手机里那个每 30 秒跳一次的验证码,不过是同一台搅拌机,换了个由时间替你按动的按钮罢了。
时间在走,密码在变,而那把藏在两端的钥匙,始终守口如瓶。
附上一个极简的实现(Python,十几行),好奇的话可以跑一下,亲眼看看 94287082 是怎么“蹦”出来的:import hmac, hashlib, struct, time
def hotp(secret, counter, digits=6):
msg = struct.pack(">Q", counter) # 计数器转成 8 字节
h = hmac.new(secret, msg, hashlib.sha1).digest()
o = h[-1] & 0x0f # 动态截取的起点
code = ((h[o] & 0x7f) << 24
| (h[o + 1] & 0xff) << 16
| (h[o + 2] & 0xff) << 8
| (h[o + 3] & 0xff))
return str(code % 10 ** digits).zfill(digits)
def totp(secret, t=None):
t = int(time.time()) if t is None else t
return hotp(secret, t // 30, digits=6)
# 用 RFC 标准密钥验证:counter=1 取 8 位,应该正好等于 TOTP(59)
secret = b"12345678901234567890"
print(hotp(secret, 1, digits=8)) # 94287082 —— 时间替你按的那一下“按钮” 《时间带来的安全》链接:https://xdym11235.com/archives/333.html
具体版权规定详见侧栏版权说明页面
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。