使用 Go 语言实现 TOTP 验证器算法 | 青训营笔记

793 阅读4分钟

TOTP 算法

TOTP(Time-based One-Time Password)算法是一种基于时间的一次性密码算法,常用于增强身份验证的安全性。TOTP算法基于HMAC(Hash-based Message Authentication Code)算法和动态口令技术,每隔一段时间生成一个基于时间戳的动态口令,用于身份验证。

TOTP算法的优点包括:

  1. 安全性较高:TOTP算法采用了HMAC算法进行哈希处理,保证了口令的安全性,同时动态口令在短时间内失效,有效避免了口令被盗用的风险。
  2. 方便性:TOTP算法不需要联网,用户只需在本地设备上安装一个TOTP算法的应用程序,即可生成动态口令,无需携带物理令牌或接收短信等方式。
  3. 易于集成:TOTP算法是一种开放的标准算法,广泛应用于多种身份验证场景中,例如Google Authenticator和微信两步验证等应用。

TOTP算法的缺点包括:

  1. 设备安全性问题:如果用户的本地设备被攻击或者病毒感染,动态口令可能会被窃取,从而导致身份验证的不安全性。
  2. 时间同步问题:TOTP算法的安全性依赖于用户设备的时间同步,如果用户设备的时间不准确,会导致生成的动态口令不正确,从而无法通过身份验证。
  3. 用户体验问题:由于TOTP算法每隔一段时间就会生成一个新的动态口令,用户需要不断地输入口令,可能会影响用户的使用体验。

算法流程

现有实现了 TOTP 的软件中的算法 本质上就是 HMAC-SHA-1 算法,也就是带有盐值的 SHA-1

  1. 以 secret 密钥为盐值取当前时间的摘要,即 HMAC-SHA-1(K,C)

    K 为密钥,C 为当前 UNIX 时间 / 30,之所以除以 30 就是为了取整获得一个 30 内相同的值

    这样就得到了一个原始的哈希值,当然得到这个哈希值还不行,因为哈希值是 20 字节长的,对于 30 秒的验证码来说太长了,所以 HEX = HMAC-SHA-1(K,C) 等下还要用

  2. 取 HEX 的第 20 字节,也就是 **HEX[19] 的低四位(后四位)**作为偏移量 OFFSET

  3. 在 HEX 中,从偏移量 OFFSET 开始取四个字节作为验证码中间值 WIP

  4. WIP mod 10^6 得到 6 位数字,不够 6 位高位补 0 即验证码

    TOTP 取验证码

Go 语言实现

  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 即验证码

    先写一个处理 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)
    
  5. 在 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
}