TOTP 算法
TOTP(Time-based One-Time Password)算法是一种基于时间的一次性密码算法,常用于增强身份验证的安全性。TOTP算法基于HMAC(Hash-based Message Authentication Code)算法和动态口令技术,每隔一段时间生成一个基于时间戳的动态口令,用于身份验证。
TOTP算法的优点包括:
- 安全性较高:TOTP算法采用了HMAC算法进行哈希处理,保证了口令的安全性,同时动态口令在短时间内失效,有效避免了口令被盗用的风险。
- 方便性:TOTP算法不需要联网,用户只需在本地设备上安装一个TOTP算法的应用程序,即可生成动态口令,无需携带物理令牌或接收短信等方式。
- 易于集成:TOTP算法是一种开放的标准算法,广泛应用于多种身份验证场景中,例如Google Authenticator和微信两步验证等应用。
TOTP算法的缺点包括:
- 设备安全性问题:如果用户的本地设备被攻击或者病毒感染,动态口令可能会被窃取,从而导致身份验证的不安全性。
- 时间同步问题:TOTP算法的安全性依赖于用户设备的时间同步,如果用户设备的时间不准确,会导致生成的动态口令不正确,从而无法通过身份验证。
- 用户体验问题:由于TOTP算法每隔一段时间就会生成一个新的动态口令,用户需要不断地输入口令,可能会影响用户的使用体验。
算法流程
现有实现了 TOTP 的软件中的算法 本质上就是 HMAC-SHA-1 算法,也就是带有盐值的 SHA-1
-
以 secret 密钥为盐值取当前时间的摘要,即 HMAC-SHA-1(K,C)
K 为密钥,C 为当前 UNIX 时间 / 30,之所以除以 30 就是为了取整获得一个 30 内相同的值
这样就得到了一个原始的哈希值,当然得到这个哈希值还不行,因为哈希值是 20 字节长的,对于 30 秒的验证码来说太长了,所以 HEX = HMAC-SHA-1(K,C) 等下还要用
-
取 HEX 的第 20 字节,也就是 **HEX[19] 的低四位(后四位)**作为偏移量 OFFSET
-
在 HEX 中,从偏移量 OFFSET 开始取四个字节作为验证码中间值 WIP
-
将 WIP mod 10^6 得到 6 位数字,不够 6 位高位补 0 即验证码
Go 语言实现
-
以 secret 密钥为盐值取当前时间的摘要
currentTime := time.Now().Unix() currentTimeSlice := make([]byte, 8) binary.BigEndian.PutUint64(currentTimeSlice, uint64(currentTime/timeStep)) hash := hmac.New(sha1.New, secret) hash.Write(currentTimeSlice) hmacHash := hash.Sum(nil) -
取 HEX 的第 20 字节,也就是 HEX[19] 的低四位(后四位)作为偏移量 OFFSET
offset := hmacHash[len(hmacHash)-1] & 0xf binaryCode := ((int32(hmacHash[offset]) & 0x7f) << 24) | ((int32(hmacHash[offset+1] & 0xff)) << 16) | ((int32(hmacHash[offset+2] & 0xff)) << 8) | (int32(hmacHash[offset+3]) & 0xff) -
在 HEX 中,从偏移量 OFFSET 开始取四个字节作为验证码中间值 WIP totp := int(binaryCode % int32(powerOf10(digits)))
-
将 WIP mod 10^6 得到 6 位数字,不够 6 位高位补 0 即验证码
先写一个处理 mod 的函数
func powerOf10(n int) int { result := 1 for i := 0; i < n; i++ { result *= 10 } return result }然后调用并返回结果
return fmt.Sprintf(fmt.Sprintf("%%0%dd", digits), totp) -
在 main 函数中调用
totp := generateTOTP([]byte("ABCDEFG"), int64(30), 6)
完整代码
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/binary"
"fmt"
"time"
)
func main() {
// 生成密钥
totp := generateTOTP([]byte("ABCDEFG"), int64(30), 6)
fmt.Println(totp)
}
// 生成TOTP值的函数
func generateTOTP(secret []byte, timeStep int64, digits int) string {
// 步骤1:以 secret 密钥为盐值取当前时间的摘要
currentTime := time.Now().Unix()
currentTimeSlice := make([]byte, 8)
binary.BigEndian.PutUint64(currentTimeSlice, uint64(currentTime/timeStep))
hash := hmac.New(sha1.New, secret)
hash.Write(currentTimeSlice)
hmacHash := hash.Sum(nil)
// 步骤2:取 HEX 的第 20 字节,也就是 HEX[19] 的低四位(后四位)作为偏移量 OFFSET
offset := hmacHash[len(hmacHash)-1] & 0xf
binaryCode := ((int32(hmacHash[offset]) & 0x7f) << 24) |
((int32(hmacHash[offset+1] & 0xff)) << 16) |
((int32(hmacHash[offset+2] & 0xff)) << 8) |
(int32(hmacHash[offset+3]) & 0xff)
// 步骤3:在 HEX 中,从偏移量 OFFSET 开始取四个字节作为验证码中间值 WIP
totp := int(binaryCode % int32(powerOf10(digits)))
// 步骤4:将 WIP mod 10^6 得到 6 位数字,不够 6 位高位补 0 即验证码
return fmt.Sprintf(fmt.Sprintf("%%0%dd", digits), totp)
}
func powerOf10(n int) int {
result := 1
for i := 0; i < n; i++ {
result *= 10
}
return result
}