基于时间的一次性密码算法

March 8, 2022

动态码口令牌,口令每隔60秒动态变化一次,因为口令牌不需要联网,所以觉得很神奇。一直没搞懂基于怎样的原理,这两天在网上注册帐号又接触到了,就是Google Authenticator(谷歌身份验证器),出于好奇,了解下其基本原理。

一次性密码算法 一次性密码算法(One-time Password,简称OTP),又称动态口令,是指计算机系统或其他数字设备上只能使用一次的密码,有效期为只有一次登录会话或交易。

相对于静态密码,OTP 最重要的优点是它们不容易受到重放攻击 (replay attack)。

OTP一般分为两种:基于事件同步(HMAC-based One-Time Password,简写HMAC)和基于时间同步(Time-based One-Time Password,简写TOTP)。

两者基本原理一致,我们着重讨论基于时间的OTP。

基本原理 为了更好的理解,我们把实现动态口令进行需求拆解:

实现两端生成相同的口令; 实现两端生成相同的动态口令; 实现两端同时生成相同的动态口令; 实现需求1 比较简单,无非就是需要客户端与服务端采用某种算法,例如:对字符串进行hash。

//为简单起见,采用python实现(python3) import hashlib

//对字符串abc进行md5 hashlib.md5(b'abc').hexdigest() //输出'900150983cd24fb0d6963f7d28e17f72' 好了,只要是对字符串abc进行md5,无论何时何地运行,结果都是一样的。输出的900150983cd24fb0d6963f7d28e17f72就是口令。

实现需求2 口令要求是动态的,那么hash的数据也必须是动态的才行,固定字符串abc就不行了,我们需要找个随时变化的字符串。计算机系统中可变化的数就是时钟了,当前时间永远在变,也是我们最常用的参数。

import hashlib import time

//获取当前时间,并转为字符串 t = str(time.time()).encode('utf-8')

//对字符串abc进行md5 hashlib.md5(t).hexdigest() 现在每次运行这段程序,生成的口令都不一样了,因为时间一直在变化。当然,客户端与服务端同时生成的口令可能并不相同,因为两者运行的时间可能并不一致。

实现需求3 要实现两端生成口令一致,涉及两点:

两端时间必须同步,这样采样的时间样本才能相同; 考虑到两端的运行效率,采样的时间单位上要考虑容错; 按照上述程序,如果客户端与服务端时间是同步的,采样的时间单位是秒,那么如果两端同一时刻运行程序,结果应该一致。

但现实应用场景中,需要用户去输入动态码,单秒时间内用户肯定完成不了,而且还要考虑客户端与服务端的传输速率及网络状况,时间容错需要更长些,一般时间容错为30秒到60秒之间。

import hashlib import time

//获取当前时间,容错30秒 t = str(time.time()//30).encode('utf-8')

//对字符串abc进行md5 hashlib.md5(t).hexdigest() 这样处理后,基本就成型了,只要两端时间同步,则可同时生成相同的动态口令。

还剩什么 还剩一些细节处理,如目前我们生成的md5是字符串,不是数字,且长度过长。还有安全问题,一旦他人知道我们采用时间进行md5,则可以伪造。

TOTP是国际标准,有现成的标准可用,这些问题都考虑到了,我们直接拿来用就可以了。

TOTP 先看下TOTP的计算公式:

TOTP = Truncate(HMAC−SHA−1(K,(now / TS))

K表示秘钥串; now表示当前时间; TS表示时间间隔,如30秒; HMAC-SHA-1表示使用HMAC SHA-1算法; Truncate是一个截取函数,并取加密后串的哪些字段组成一个数字。 K是客户端与服务端内置的密钥,只要密钥不泄露,这个算法就是安全的。具体实现和我们上面的代码差不多,有兴趣可以找找开源代码。