基于时间的一次性密码(TOTP)一般用于两步验证中,通过生成动态的6位或者8位的数字密码来增强安全性。这里将结合RFC 6238文档学习TOTP的算法原理并使用JavaScript进行实现。
TOTP的算法
根据RFC 6238,使用HMAC-SHA-1
、HMAC-SHA-256
或HMAC-SHA-512
算法生成一次性密码。
核心步骤如下:
-
共享密钥:用户和服务器之间共享一个密钥,该密钥对外保密,是生成TOTP的基础。通常情况下,这个密钥会经过base32编码,然后去掉填充的
=
。 -
时间步长:将当前时间戳按设定的时间步长(通常为30秒)分割。时间步长用于计算时间窗口索引。
-
计算时间窗口索引:使用公式
T = floor((currentTime - T0) / X)
,其中currentTime
是当前时间戳,T0
是起始时间(通常为Unix纪元),X
是时间步长。T表示时间窗口索引。 -
生成哈希值:使用
HMAC-SHA
算法,将密钥和时间窗口索引作为输入,生成一个哈希值。 -
截取动态密码:从哈希值中截取一部分作为一次性密码。
-
密码验证:服务器根据密钥和当前时间戳生成对应的TOTP,并与用户输入的密码进行对比。如果匹配,则验证通过。
JavaScript实现TOTP
以下是一个在JavaScript中实现TOTP的示例代码:
const crypto = require('crypto')
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
const base32Lookup = {}
for (let i = 0; i < base32Chars.length; i++) {
base32Lookup[base32Chars[i]] = i
}
function base32Decode(secret) {
let bits = []
for (let i = 0; i < secret.length; i++) {
const value = base32Lookup[secret[i].toUpperCase()]
bits.push(value.toString(2).padStart(5, '0'))
}
bits = bits.join('')
const bytes = []
for (let i = 0; i + 8 <= bits.length; i += 8) {
bytes.push(parseInt(bits.slice(i, i + 8), 2))
}
return Buffer.from(bytes)
}
function generateTOTP(secret, window = 0, digits = 6, algorithm = 'sha1', period = 30) {
const key = base32Decode(secret)
const epoch = Math.floor(Date.now() / 1000)
const timeWindow = Math.floor(epoch / period) + window
const timeBuffer = Buffer.alloc(8)
timeBuffer.writeUInt32BE(0, 0)
timeBuffer.writeUInt32BE(timeWindow, 4)
const hmac = crypto.createHmac(algorithm, key)
hmac.update(timeBuffer)
const hmacResult = hmac.digest()
const offset = hmacResult[hmacResult.length - 1] & 0x0f
const binary =
((hmacResult[offset] & 0x7f) << 24) |
((hmacResult[offset + 1] & 0xff) << 16) |
((hmacResult[offset + 2] & 0xff) << 8) |
(hmacResult[offset + 3] & 0xff)
const otp = binary % Math.pow(10, digits)
return otp.toString().padStart(digits, '0')
}
const secret = '36NFPP3U'
const totp = generateTOTP(secret)
console.log(`TOTP: ${totp}`)
代码示例说明
下面对代码实现的几个关键部分进行说明:
- base32Decode:这个函数用于将Base32编码的密钥解码为二进制数据,对于Base32的编解码规则这里不做赘述,有兴趣的请自行搜索。
以36NFPP3U
为例,经过解码后,获得的二进制数据是1101111110011010010101111011111101110100
- generateTOTP:这个函数接受五个参数:密钥(secret)、时间窗口偏移(window)、密码位数(digits)、哈希算法(algorithm)和时间步长(period)。
在计算时间窗口时,使用Math.floor(Date.now() / 1000)
获取当前的时间戳(以秒为单位),Math.floor(epoch / period)
将时间戳按设定的步长分割,计算出时间窗口索引。
再使用HMAC-SHA
算法,结合密钥和时间窗口索引生成哈希值。再获取到哈希值的二进制数据,比如这里计算后的二进制数据是0111011111100000101010101101001100100110000010101111111110111101111010111010001101001101011110101001110110100110010010101110000011110001101010011110111111101100
,一共160位。
显然这个长度太长了,我们需要截断,截断的算法十分精巧,首先我们将这串数据与0x0f
相与,获取最后4位,结果是1100
,这就是我们的起始偏移量,十进制为12
。
从刚才的160位二进制的第97位开始,与0x7f
相与,将首位变为0,保证是正数,然后左移24位作为最高的一个字节,也就是00011101
。然后依次取第11、12和13个字节,作为第2、3、4个字节,分别是10100110
,01001010
,11100000
,合并之后得到二进制数00011101101001100100101011100000
,换算为10进制是497437408
,一般我们取后6位,这样就得到了437408
作为密码。
Comments NOTHING