OTP的应用和底层原理

1,365 阅读4分钟

image.png OTP是One-Time Password的缩写,即:一次性口令,多因素认证的一种,可以提高安全性,分为两种:

  • HOTP HMAC-Based One-Time Password
  • TOTP Time-Based One-Time Password

一般用于在账号密码认证获取登录态后操作敏感信息时再次通过otp做安全校验,也可以用于在登录时做多因素校验,不仅校验账号密码也对otp做校验,想比较于短信或邮箱等方式的二次校验,otp不仅免费安全还及时。本文将介绍:

  1. 如何在自己的系统中增加otp的支持
  2. otp内部的实现原理是什么

举例几个使用场景

  1. 在GitHub平台添加SSH Key时

image.png

  1. 在GitHub平台删除项目时

image.png

  1. 修改JetBrains密码时(旗下产品:Intellij IDEA 、Goland、PyCharm..)

image.png

快速使用

  1. 获取依赖
go get github.com/lidenger/otp
  1. 使用
import "github.com/lidenger/otp"

key := "6KFGGMBBCCRAAY3D"
// 生成6位code
code, err := otp.TOTP(key)

// 验证code
code := "712512"
result, err := otp.VerifyTOTP(key, code)


深入原理

HOTP

HMAC-Based One-Time Password

基于HMAC算法的一次性口令,默认的算法为 HMAC SHA1,RFC文档中记载了该算法的标准规范:datatracker.ietf.org/doc/html/rf…,所以我们按照标准实现即可,计算公式为:

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

K 密钥种子(shared secret between client and server),base32 encode ,一个账号一个,全局唯一
C 64位的增量因子(8-byte counter value, the moving factor),可以为时间戳,也可以传入TOTP计算的时间(这里实际上已经提到了TOTP实现,后面会聊这块)

  1. 根据K和C生成HMAC SHA1摘要

Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C) // HS is a 20-byte string

func generateDigest(key, data []byte) [20]byte {
	h := hmac.New(sha1.New, key)
	h.Write(data)
	digested := h.Sum(nil)
	return [20]byte(digested[:])
}
  1. 根据offset截取摘要的4个字节

Step 2: Generate a 4-byte string (Dynamic Truncation) Let Sbits = DT(HS) // DT, defined below, // returns a 31-bit string 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) ;

func truncation(digested [20]byte) int32 {
	offset := digested[19] & 0xf
	trunc := int32(digested[offset]) & 0x7f << 24
	trunc |= int32(digested[offset+1]) & 0xff << 16
	trunc |= int32(digested[offset+2]) & 0xff << 8
	trunc |= int32(digested[offset+3]) & 0xff
	return trunc
}
  1. 和10^6求余得到6位code码

Return D = Snum mod 10^Digit

code := trunc % 1000000

小结:核心是使用HMAC SHA1计算出6位数字,看着没什么深奥的算法,就是定义了一种计算方式,只要大家都遵循这个规则生成和计算就可以正常工作

TOTP

Time-Based One-Time Password

基于时间的一次性口令,值得说明的是它是基于HOTP的,RFC标准: datatracker.ietf.org/doc/html/rf…,公式如下:

TOTP = HOTP(K, T)

K 密钥种子,base32 encode
T 时间增量,UTC时间戳(从1970年到当前时间的毫秒值) 从公式中得到只需要将计算好的时间增量传入HTOP中即可得到TOTP了

  1. 定义想要的配置
var (
	T0      int64 = 0                 // 时间起始时间戳,0表示UNIX起始时间即:1970-01-01 00:00:00
	step    int64 = 30                // 步骤:每个窗口30秒
	windows       = [3]int8{0, -1, 1} // 当前窗口,前一个窗口,后一个窗口
	digit         = 6                 // 验证码位数
)

增加窗口可以提高口令验证的兼容性,一般口令是6位数字,有效期是为30秒

  1. 计算TOTP
func TOTPWithOptions(k []byte, t int64, window int8) string {
	T := (t - T0) / step
	T += int64(window)
	data := [8]byte{}
	for i := 7; i > 0; i-- {
		data[i] = byte(T)
		T = T >> 8
	}
	code := HOTP(k, data[:])
	codeStr := strconv.FormatInt(int64(code), 10)
	for len(codeStr) != digit {
		codeStr = "0" + codeStr
	}
	return codeStr
}

// TOTP 
// k base32 encode string
func TOTP(k string) (string, error) {
	now := time.Now().Unix()
	keyDecode, err := base32.StdEncoding.DecodeString(k)
	if err != nil {
		return "", err
	}
	code := TOTPWithOptions(keyDecode, now, 0)
	return code, nil
}

  1. 验证TOTP
func VerifyTOTP(k string, code string) (bool, error) {
	keyDecode, err := base32.StdEncoding.DecodeString(k)
	if err != nil {
		return false, err
	}
	for _, window := range windows {
		now := time.Now().Unix()
		c := TOTPWithOptions(keyDecode, now, window)
		if c == code {
			return true, nil
		}
	}
	return false, nil
}

验证TOTP增加了时间窗口,支持前一个,当前,后一个,提供了验证的通过率,当前这个参数也和安全性相关

源码地址:github.com/lidenger/ot…

总结

如果不关心内部实现细节可以直接使用 github.com/lidenger/otp ,其实不难看出OTP的实现原理不复杂更像是规范化的标准,TOTP将当前时间作为参数调用HTOP得到了基于时间的一次性口令


摸鱼时刻012677BA.png

推荐一款南美热带鱼:鼠鱼

淡水小型热带鱼
成年体长:3-8厘米
饲养温度:20℃ - 26℃
饲养难度:一般
栖息地:亚马孙河

金线鼠

超级二线

太空飞鼠

尼青鼠