动态密码,亦称一次性密码(One Time Password, 简称 OTP),是一种高效简单又比较安全的密码生成算法,在我们的生活以及工作中随处可见,身为开发者,也或多或少在自己的业务系统中集成了二步验证机制,那么,技术运用,既要知其然,更要知其所以然,动态密码算法是怎样的?
从我的角度理解,动态密码是指随着某一事件(密码被使用、一定的时间流逝等)的发生而重新生成的密码,因为动态密码本身最大优点是防重复执行攻击 (replay attack),它能很好地避免类似静态密码可能被暴力破解等的缺陷,现实运用中,一般采用“静态密码 + 动态密码”相结合的双因素认证,我们也称二步验证。
而动态密码其实很早就出现在我们的生活里了,在移动支付发展起来之前,网银是当时最为流行的在线支付渠道,当时银行为了确保大家的网银账号支付安全,都会给网银客户配发动态密码卡,比如中国银行电子口令卡(按时间差定时生成新密码,口令卡自带电池,可保证连续使用几年),或者工商银行的电子银行口令卡(网银支付网页每次生成不同的行列序号,用户根据指定行列组合刮开密码卡上的涂层获取密码,密码使用后失效),又或者银行强制要求的短信验证码,这些都可以纳入动态密码的范畴。
而随着移动互联网的发展以及移动设备的智能化的不断提高,设备间的同步能力大幅提升,以前依赖独立设备的动态密码生成技术很快演变成了手机上的动态密码生成软件,以手机软件的形式生成动态密码的方式极大提高了动态密码的便携性,一个用户一个手机就可以管理任意多个动态密码的生成,这也使得在网站上推动二步验证减少了很多阻力,因为以往客户可能因为使用口令卡太麻烦,而拒绝打开二步验证机制,从而让自己的账号暴露在风险之下。最为知名的动态密码生成软件,当属 Google 的 Authenticator APP。
一般来说,常见的动态密码有两类:
在真正开展算法介绍之前,需要补充介绍的是:动态密码的基本认证原理是在认证双方共享密钥,也称种子密钥,并使用的同一个种子密钥对某一个事件计数、或时间值进行密码算法计算,使用的算法有对称算法、HASH、HMAC 等。记住这一点,这个是所有动态密码算法实现的基础。
HOTP 算法,全称是“An HMAC-Based One-Time Password Algorithm”,是一种基于事件计数的一次性密码生成算法,详细的算法介绍可以查看 RFC 4226。其实算法本身非常简单,算法本身可以用两条简短的表达式描述:
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
PWD(K,C,digit) = HOTP(K,C) mod 10^Digit
上式中:
"11"
(此处省略了前面的二进制的数字 0)由于 HMAC SHA-1 算法是既有算法,不是我们讨论重点,故而 Truncate 函数就是整个算法中最为关键的部分了。以下引用 Truncate 函数的步骤说明:
DT(String) // String = String[0]...String[19]
Let OffsetBits be the low-order 4 bits of String[19]
Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15
Let P = String[OffSet]...String[OffSet+3]
Return the Last 31 bits of P
结合上面的公式理解,大概的描述就是:
回到算法本身,在获得 31 位的截断结果之后,我们将其又转换为无标点的大端表示的整数值,这个值的取值范围是 0 ~ 2^31,也即 0 ~ 2.147483648E9,最后我们将这个数对 10 的乘方(digit 指数范围 1-10)取模,得到一个余值,对其前面补 0 得到指定位数的字符串。
以下代码示例也可访问 Gist 获取。
require 'openssl'
def hotp(secret, counter, digits = 6)
hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), secret, int_to_bytestring(counter)) # SHA-1 算法加密
"%0#{digits}i" % (truncate(hash) % 10**digits) # 取模获取指定长度数字密码
end
def truncate(string)
offset = string.bytes.last & 0xf # 取最后一个字节
partial = string.bytes[offset..offset+3] # 从偏移量开始,连续取 4 个字节
partial.pack("C*").unpack("N").first & 0x7fffffff # 取后面 31 位结果后得到整数
end
def int_to_bytestring(int, padding = 8)
result = []
until int == 0
result << (int & 0xFF).chr
int >>= 8
end
result.reverse.join.rjust(padding, 0.chr)
end
上面的算法实现代码量很少,核心都是按照算法描述进行多个掩码运算跟位操作而已。
从上面的分析可以看到,一个动态密码的生成,取决于共享密钥以及移动因子的值,而共享密钥是保持不变的,最终就只有移动因子决定了密码的生成结果。所以在 HOTP 算法中,要求每次密码验证成功后,认证服务器端以及密码生成器(客户端)都要将计数器的值加 1,已确保得到新的密码。
但是在这里就会引入一个问题,假如认证服务器端与密码生成器之间由于通信故障或者其他意外情况,导致两边计数器的值不同步了,那么就会导致两边生成的密码无法正确匹配。为了解决这个问题,算法在分析中建议认证服务器端在验证密码失败后,可以主动尝试计数器减 1 之后重新生成的新密码是否与客户端提交密码一致,如果是,则可以认定是客户端计数器未同步导致,这种情况下可以通过验证,并且要求客户端重新同步计数器的值。
出了上面提到的计数器不同步的问题,我另外想的是,如果客户有多个密码生成器(假设 iPad 和 iPhone)为同个账号生成密码,那么计数器在多个设备间的同步可能就需要另外考虑的方案了。
其实 HOTP 的算法比我在阅读算法前所想象的要简洁得多,而且仍然足够强健。算法本身巧妙利用了加密算法对共享密钥和计数器进行加密,确保这两个动态密码生成因子不被篡改,接着通过一个 truncate 函数随机得到一个最长 10 位的 10 进制整数,最终实现对 1 - 10 位长度动态密码的支持。算法本身的简洁也确保了算法本身可以在各种设备上实现。
TOTP 算法,全称是 TOTP: Time-Based One-Time Password Algorithm,其基于 HOTP 算法实现,核心是将移动因子从 HOTP 中的事件计数改为时间差。完整的 TOTP 算法的说明可以查看 RFC 6238,其公式描述也非常简单:
TOTP = HOTP(K, T) // T is an integer and represents the number of time steps between the initial counter time T0 and the current Unix time
More specifically, T = (Current Unix time - T0) / X, where the default floor function is used in the computation.
通常来说,TOTP 中所使用的时间差都是当前时间戳,TOTP 将时间差除以时间窗口(密码有效期,默认 30 秒)得到时间窗口计数,以此作为动态密码算法的移动因子,这样基于 HOTP 算法就能方便得到基于时间的动态密码了。
注意: RFC 6238 提到,在 TOTP 算法中,可以指定不同的键控哈希算法,比如在 HOTP 中使用的是 HMAC-SHA1 算法,而在 TOTP,除此之外,还可以使用 HMAC-SHA256 或者 HMAC-SHA512。
TOTP implementations MAY use HMAC-SHA-256 or HMAC-SHA-512 functions, based on SHA-256 or SHA-512 [SHA2] hash functions, instead of the HMAC-SHA-1 function that has been specified for the HOTP computation in [RFC4226].
以下代码示例也可访问 Gist 获取。
require 'hotp'
# 仅为示例方便,复用了前述代码,如果需要使用 HMAC-SHA256 或者 HMAC-SHA512,需要另外实现
def totp(secret, digits = 6, step = 30, initial_time = 0)
steps = (Time.now.to_i - initial_time) / step
hotp(secret, steps, digits)
end
看到了吧,极其简短的实现代码!一个时间计数的动态密码算法就此诞生,如此简单的算法,却是支撑多少业务系统安全运作的基石,颇有四两拨千斤的快感!
在 Google Authenticator 的开源项目的 README 里有明确提到:
These implementations support the HMAC-Based One-time Password (HOTP) algorithm specified in RFC 4226 and the Time-based One-time Password (TOTP) algorithm specified in RFC 6238.
也就是说,至此,我们也明白了,其实 Google Authenticator 算法核心也是 HOTP 以及 TOTP,在明白了整个动态密码算法的核心之后,有没有一种豁然开朗的感觉呢?既知其然,又知其所以然了,对吧?每次看着应用定时生成密码,
这篇文章简单介绍了两类常见的动态密码的生成算法,算法本身简洁不复杂,效率并且足够强健。这篇文章的目的是方便跟我一样希望了解算法核心的小伙伴,而在 RFC 文档中,仍有大量关于算法本身的安全性方面的探讨,有兴趣的小伙伴可以去看一下。