2FA双因素认证 - TOTP

发布时间 2023-08-19 19:03:20作者: warm3snow

主页

引言

在2FA双因素认证中,TOTP可谓是标准化程度最高的技术方案。它已被互联网工程任务组接纳为RFC 6238标准,成为OATH(开放标准,用于授权和身份认证)的基石,并被用于众多多重要素验证系统当中。本文将进一步介绍TOTP的工作原理以及相关密码技术应用,并用代码示例来剖析底层细节。以便于读者能够根据需要实现自己的TOTP双因素认证机制。

本文内容组织:

  • 简介
  • TOTP工作原理
    • 共享密钥生成
    • OTP生成算法
  • 代码实战
  • 总结
  • 参考资料

简介

TOTP定义:基于时间的一次性密码算法(英语:Time-based One-Time Password,简称:TOTP)是一种根据共享密钥当前时间计算一次性密码的算法。(该定义来自wikipedia)
从定义上我们可以了解到:

  • TOTP是一种算法,该算法主要用来生成一次密码(这里的一次密码可以理解为仅能使用一次的验证码
  • TOTP基于”预共享密钥“,TOTP在使用过程中一般涉及到两端(客户端和服务端),客户端和服务端分别生成验证码,生成过程需要基于预共享密钥。该共享密钥主要保证安全性,在不知道该密钥的情况下,第三方或攻击者无法生成相同的验证码
  • TOTP基于”当前时间“,当前时间会作为TOTP算法的随机因子,保证验证码的一次性

TOTP工作原理

《2FA双因素认证 - 原理和应用》 一文中,我们简单提到过TOTP的工作原理:

从上图中,我们可以看到客户端(用户手机)和服务端(servers)分别计算出了 相同验证码,因此用户在使用该验证码时,会被验证通过。
要深入理解TOTP的工作原理,我们需要注意图中的以下几点,后文我们将重点介绍:

  • OTP密钥:该密钥就是“共享密钥”
  • HMAC:HMAC是一种密码技术,可以将任意长度的输入转换为固定长度的输出, 在转换过程中依赖于密钥保证安全性(这里的密钥就是“共享密钥”),输出结果的长度依赖于HMAC依赖的哈希算法。
  • 截取:由于HMAC输出的结果一般较长,而为了方便记忆和在终端输入,我们平时使用的验证码都是4-6位整数。所以这里需要对HMAC结果进行截取处理。

TOTP详细流程如下:

TOTP认证主要包含三个阶段:

  • 密钥共享
  • 一次密码OTP获取
  • 一次密码OTP验证

密钥共享

共享密钥由服务端生成,并通过安全的通信渠道发送给用户,或者通过安全的密钥交换协议传输给用户。这个密钥通常是一个Base32编码的字符串,可以包含字母(A-Z)和数字(2-7),长度一般为16字节。生成密钥时要确保使用足够的随机性,可以使用安全的随机数生成器。

在TOTP rfc6238文档中并未给出详细的共享密钥生成算法,而是直接使用了HTOP rfc4226文档中<7.5 Management of Shared Secrets>介绍的方式。
在HTOP rfc4226中给出了两种方式来生成共享密钥:确定生成方式 和 随机生成方式

确定生成方式

共享密钥确定生成方式如下:

  • 服务端维护一个主密钥(MK = Master Key)
  • 共享密钥使用算法K_i = SHA-1 (MK,i)基于主密钥生成,这里i 代表用户序号,一般一个用户对应一个i, 用于区分不同用户端

注:这里的确定生成方式指的是生成共享密钥是确定的,但生成主密钥MK是随机数的,并且需要安全保存。在服务端验证用户登陆时输入的一次密码时,需要基于用户序号和主密钥生成共享密钥。

该方式的优点是,服务端只需要保护一个主密钥,密钥管理简单;但如果主密钥丢失,则所有共享密钥都存在安全风险。

随机生成方式

  • 服务端不依赖于主密钥
  • 服务端会为每个客户端(用户)生成唯一的,与其他用户无关的随机数,作为共享密钥。

该方式的优点是,不同共享密钥之间时独立无关的,服务端一个共享密钥的丢失不会对其他共享密钥产生安全隐患;但是如果客户端或用户较多,密钥管理负担较重。

OTP生成算法

《2FA双因素认证 - 原理和应用》 一文中,我们已经提到过OTP生成算法,如下:

  #  T表示当前时间,T0表示初始时间(一般为0,可省略)
  # Period表示更新周期(一般为30秒)
  # C表示基于时间生成的计数
   C = (T - T0) / Period

  # K:加密密码,作为HMAC密码算法输入,由于只有客户端和服务端共享,因此在不知道K的情况下,无法生成,保证安全性。
  # C:计数器,客户端和服务端基于本地时间分别计算
  # h:表示使用密码技术得到的一次密码(但是由于h长度较大,不适合作为验证码,因此需要进一步截取
   h = HMAC(K, C)

  # otp:one time password(一次密码),通过对h进行截取处理(这里的digit代表需要截取的位数,通常情况下为6)
   otp = Trunc(h, digit)

在实际应用中,TOTP生成算法T0一般为0,Period一般为30秒,一次密码长度为6,则TOTP生成算法如下:

TOTP(K,C) = Truncate(HMAC-SHA-1(K,T/30)

注:HMAC-SHA-1表示HMAC底层使用了SHA-1哈希算法,该算法输出为160位二进制 = 20字节, 因此需要使用Truncate算法进行截取,该算法如下:

        int offset   =  hmac_result[19] & 0xf ;
        int bin_code = (hmac_result[offset]  & 0x7f) << 24
           | (hmac_result[offset+1] & 0xff) << 16
           | (hmac_result[offset+2] & 0xff) <<  8
           | (hmac_result[offset+3] & 0xff) ;

为了方便理解,对于Truncate算法我们可以通过例子解释一下,假设HMAC-SHA-1生成的结果如下:

   -------------------------------------------------------------
   | Byte Number                                               |
   -------------------------------------------------------------
   |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|
   -------------------------------------------------------------
   | Byte Value                                                |
   -------------------------------------------------------------
   |1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a|
   -------------------------------***********----------------++|

- 最后一个字节(第19个字节)的十六进制值为0x5a。
- 低4位的值为0xa(偏移值)。
- 偏移值为字节10(0xa)。
- 从第10个字节开始的4个字节的值为0x50ef7f19,即动态二进制码DBC1。
- DBC1的最高有效位为0x50,因此DBC1 = 0x50ef7f19。
然后,我们将这个数对1,000,000(10^6)取模,以生成6位的TOTP值872921(十进制)。

Demo演示

我们使用golang语言demo来演示Google Authenticator中OTP生成和验证。
OTP生成:

// 生成TOTP密码
func generateTOTP(secretKey []byte, currentTime int64) int {
	// TOTP参数
	timeStep := int64(30)
	digitCount := 6

	// 计算时间步数和时间步长
	currentStep := currentTime / timeStep

	// 创建HMAC-SHA1哈希实例
	hash := hmac.New(sha1.New, secretKey)

	// 将时间步数转换为字节数组
	stepBytes := make([]byte, 8)
	for i := 7; i >= 0; i-- {
		stepBytes[i] = byte(currentStep & 0xff)
		currentStep >>= 8
	}

	// 对时间步数进行哈希
	hash.Write(stepBytes)
	hmacResult := hash.Sum(nil)

	// 获取HMAC-SHA1的最后一个字节的低4位
	offset := hmacResult[19] & 0x0f

	// 从HMAC结果中截取4个字节
	codeBytes := hmacResult[offset : offset+4]

	// 将4个字节转换为整数
	totpPassword := int(
		(uint32(codeBytes[0])&0x7f)<<24 |
			(uint32(codeBytes[1])&0xff)<<16 |
			(uint32(codeBytes[2])&0xff)<<8 |
			(uint32(codeBytes[3]) & 0xff),
	)

	// 对密码进行取模,生成指定位数的密码
	return totpPassword % intPow(10, digitCount)
}

OTP验证

// 验证输入的TOTP密码是否有效
func validateTOTP(secretKey []byte, currentTime int64, userInputPassword int) bool {
	// 计算时间步数和时间步长
	currentStep := currentTime / int64(30)

	// 遍历前后几个时间步,检查密码是否匹配
	for i := -1; i <= 1; i++ {
		// 生成待验证的TOTP密码
		validationTime := currentStep + int64(i)
		validationPassword := generateTOTP(secretKey, validationTime)

		if userInputPassword == validationPassword {
			return true
		}
	}

	return false
}

辅助函数:

// 计算x的y次方
func intPow(x, y int) int {
	result := 1
	for i := 0; i < y; i++ {
		result *= x
	}
	return result
}

// 生成随机的字节数组密钥
func generateRandomKey(length int) []byte {
	// 创建一个字节数组,用于存储随机生成的密钥
	key := make([]byte, length)
	_, err := rand.Read(key)
	if err != nil {
		panic("Error generating random key")
	}
	return key
}

测试方式:

func main() {
	// 在实际应用中,这个密钥应该是从用户输入或存储中获取的
	// 这里为了演示目的,使用一个固定的密钥
	encodedKey := "JBSWY3DPEHPK3PXP"

	// 解码Base32编码的密钥
	secretKey, err := base32.StdEncoding.DecodeString(encodedKey)
	if err != nil {
		panic("Error decoding secret key")
	}

	// 获取当前时间戳
	currentTime := time.Now().Unix()

	// 生成TOTP密码
	totpPassword := generateTOTP(secretKey, currentTime)

	fmt.Printf("Generated TOTP Password: %06d\n", totpPassword)

	// 假设用户输入的密码是6位整数
	userInputPassword := 123456

	// 验证输入的密码是否匹配
	isValid := validateTOTP(secretKey, currentTime, userInputPassword)
	if isValid {
		fmt.Println("Password is valid")
	} else {
		fmt.Println("Password is not valid")
	}
}

总结

本文我们对TOTP共享密钥生成原理、一次密钥生成和验证进行了详细介绍,并通过golang进行了demo演示。TOTP作为一种标准化程度较高的2FA认证方式,已经越来越普遍,在网络身份认证中应用2FA技术,对于系统安全性、用户身份安全具有重要意义。

参考资料