一次性密码 One Time Password, 简称 OTP。

动态密码优点:

  • 由服务器和工具生成密码,无须记录。
  • 随机生产,一次有效性。
  • 根据时间计算,有时效性。

动态密码的产生方式,主要是以时间差做为服务器与密码产生器的同步条件。在需要登录的时候,就利用密码产生器产生动态密码,OTP一般分为计次使用以及计时使用两种,计次使用的OTP产出后,可在不限时间内使用;计时使用的OTP则可设置密码有效时间,从30秒到两分钟不等,而OTP在进行认证之后即废弃不用,下次认证必须使用新的密码,增加了试图不经授权访问有限制资源的难度。

国内一些令牌也是基于动态密码算法生成。 steam,itch, ali,google, 坚果云, 等等都采取了 OTP 来进行二次验证。

动态密码的分类

常见的动态密码有两类:

  • 计次使用:计次使用的OTP产出后,可在不限时间内使用,知道下次成功使用后,计数器加 1,生成新的密码。用于实现计次使用动态密码的算法叫 HOTP,接下来会对这个算法展开介绍;
  • 计时使用:计时使用的OTP则可设定密码有效时间,从30秒到两分钟不等,而OTP在进行认证之后即废弃不用,下次认证必须使用新的密码。用于实现计时使用动态密码的算法叫 TOTP,接下来会对这个算法展开介绍。

动态密码的基本认证原理是在认证双方共享密钥,也称种子密钥,并使用的同一个种子密钥对某一个事件计数、或时间值进行密码算法计算,使用的算法有对称算法、HASH、HMAC等,这个是所有动态密码算法实现的基础。

HOTP

HOTP 算法,全称是“An HMAC-Based One-Time Password Algorithm”,是一种基于事件计数的一次性密码生成算法,详细的算法介绍可以查看 RFC 4226

算法本身可以用两条简短的表达式描述:

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

PWD(K,C,digit) = HOTP(K,C) mod 10^Digit

公式中

  • K 代表我们在认证服务器端以及密码生成端(客户设备)之间共享的密钥,在 RFC 4226 中,作者要求共享密钥最小长度是 128 位,而作者本身推荐使用 160 位长度的密钥
  • C 表示事件计数的值,8 字节的整数,称为移动因子(moving factor),需要注意的是,这里的 C 的整数值需要用二进制的字符串表达,比如某个事件计数为 3,则C是 "11"(此处省略了前面的二进制的数字0)
  • HMAC-SHA-1 表示对共享密钥以及移动因子进行 HMAC 的 SHA1 算法加密,得到 160 位长度(20字节)的加密结果,当然也可以使用HMAC-MD5等算法。
  • Truncate 即截断函数,后面会详述
  • digit 指定动态密码长度,比如我们常见的都是 6 位长度的动态密码

客户端和服务器事先协商好一个密钥K,用于一次性密码的生成过程,此密钥不被任何第三方所知道。此外,客户端和服务器各有一个计数器C,并且事先将计数值同步。

Truncate 截断函数

由于 SHA-1 算法是既有算法,不是我们讨论重点,故而 Truncate 函数就是整个算法中最为关键的部分了。以下引用 Truncate 函数的步骤说明:

DT(String) // String = String[0]…String[19]

Let OffsetBits be the low-order 4 bits of String[19]
Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15
Let P = String[OffSet]...String[OffSet+3]
Return the Last 31 bits of P

结合上面的公式理解,大概的描述就是:

先从第一步通过 SHA-1 算法加密得到的 20 字节长度的结果中选取最后一个字节的低字节位的 4 位(注意:动态密码算法中采用的大端(big-endian)存储);
将这 4 位的二进制值转换为无标点数的整数值,得到 0 到 15(包含 0 和 15)之间的一个数,这个数字作为 20 个字节中从 0 开始的偏移量;
接着从指定偏移位开始,连续截取 4 个字节(32 位),最后返回 32 位中的后面 31 位。

回到算法本身,在获得 31 位的截断结果之后,我们将其又转换为无标点的大端表示的整数值,这个值的取值范围是 0 ~ $2^{31}$,也即 0 ~ 2.147483648E9,最后我们将这个数对10的乘方(digit 指数范围 1-10)取模,得到一个余值,对其前面补0得到指定位数的字符串。

TOTP

TOTP 算法,全称是 TOTP: Time-Based One-Time Password Algorithm,其基于 HOTP 算法实现,核心是将移动因子从 HOTP 中的事件计数改为时间差。完整的 TOTP 算法的说明可以查看 RFC 6238,其公式描述也非常简单:

TOTP = HOTP(K, T) 

// T is an integer and represents the number of time steps between the initial counter time T0 and the current Unix time More specifically,
 T = (Current Unix time – T0) / X, where the default floor function is used in the computation.

通常来说,TOTP 中所使用的时间差都是当前时间戳,TOTP 将时间差除以时间窗口(密码有效期,默认 30 秒)得到时间窗口计数,以此作为动态密码算法的移动因子,这样基于 HOTP 算法就能方便得到基于时间的动态密码了。

注意: RFC 6238 提到,在 TOTP 算法中,可以指定不同的键控哈希算法,比如在 HOTP 中使用的是 HMAC-SHA1 算法,而在 TOTP,除此之外,还可以使用 HMAC-SHA256 或者 HMAC-SHA512。

{# /images/otp.jpg #} otp

APP

code by golang

package otp

// For more info, please visit https://tools.ietf.org/html/rfc4226
import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base32"
	"encoding/binary"
	"fmt"
	"net/url"
	"time"
)

//
// OTP(K,C) = Truncate(HMAC-SHA-1(K,C))
//
func OTP(secret string, counter int64) int {
	key, err := base32.StdEncoding.DecodeString(secret)
	if err != nil {
		return -1
	}

	hash := hmac.New(sha1.New, key)
	err = binary.Write(hash, binary.BigEndian, counter)
	if err != nil {
		return -1
	}
	hs := hash.Sum(nil)

	offset := hs[19] & 0x0f

	truncated := binary.BigEndian.Uint32(hs[offset : offset+4])

	truncated &= 0x7fffffff
	code := truncated % 1000000

	return int(code)

}

// HOTP ...
type HOTP struct {
	secret string
	digits int
	size   int // 3-5 验证范围 totp 时候追加验证的范围
}

// At 生成
func (h HOTP) At(counter int64) int {
	return OTP(h.secret, counter)
}

// Verify 验证 OTP code
func (h HOTP) Verify(code int, counter int64) bool {
	return h.At(counter) == code
}

// NewHOTP 生成 HOTP instance
func NewHOTP(secret string, digits int) (h *HOTP) {
	h = new(HOTP)
	h.secret = secret
	h.digits = digits
	return
}

//-------------------

// TOTP 基于时间戳的双因子验证
type TOTP struct {
	secret string
	digits int
	x      int64 // 设定有效期,建议 30 ,30秒
	UTC    bool
}

// NewTOTP generate new TOTP instance
// digits 位数
// x 时间有效期 秒
func NewTOTP(secret string, digits, x int) (t *TOTP) {
	t = new(TOTP)
	t.secret = secret
	t.digits = digits
	t.x = int64(x)
	return t
}

// At 获取时间因子 OTP code
func (t *TOTP) At(timestamp int64) int {
	// 用时间戳当做算法的移动因子, 除去有效实际
	counter := int64(timestamp / t.x)
	return OTP(t.secret, counter)
}

// Now 获取当前时间 TOTP
func (t *TOTP) Now() int {
	now := time.Now()
	timestamp := now.Unix()
	return t.At(timestamp)
}

// VerifyNow 验证当前时间 OTP code
func (t *TOTP) VerifyNow(code int) bool {
	return t.Now() == code
}

// Verify 验证 OTP code
func (t *TOTP) Verify(code int, timestamp int64) bool {
	return t.At(timestamp) == code
}

// URI 生成 Google Authenticator 字符串
// 用于二维码生成
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
func (t *TOTP) URI(user string, issuer string) string {
	auth := "totp/"
	q := make(url.Values)

	// if HotpCounter > 0 {
	// 	auth = "hotp/"
	// 	q.Add("counter", strconv.Itoa(HotpCounter))
	// }

	q.Add("secret", t.secret)
	q.Add("period", fmt.Sprintf("%d", t.x))
	q.Add("digits", fmt.Sprintf("%d", t.digits))
	q.Add("algorithm", "SHA1")

	if issuer != "" {
		q.Add("issuer", issuer)
		auth += issuer + ":"
	}

	return "otpauth://" + auth + user + "?" + q.Encode()

}

// ScratchCode 救援码验证
func (t *TOTP) ScratchCode(Scratch ScratchCode, code int) bool {
	return Scratch.Verify(code)
}

// ScratchCode 救援码
type ScratchCode interface {
	Verify(code int) bool // 救援码验证
	Generate() []string   // 救援吗生成
}

// Scratch 救援码
type Scratch struct {
	ScratchCodes []int
}

// Generate 救援吗生成
func (s *Scratch) Generate(code ...int) {
	s.ScratchCodes = append(s.ScratchCodes, code...)
}

// Verify 救援码 ?? 修改为数据。。。
func (s *Scratch) Verify(code int) bool {
	for i, v := range s.ScratchCodes {
		if code == v {
			// 存在则删除
			l := len(s.ScratchCodes) - 1
			s.ScratchCodes[i] = s.ScratchCodes[l]
			s.ScratchCodes = s.ScratchCodes[0:l] // 复制(重置长度)
			return true
		}
	}
	return false
}

引用