Skip to content

totp

什么是 totp

TOTP(Time-based One-Time Password Algorithm)是基于时间的一次性密码算法,它通过计算一个共享密钥和当前时间戳,生成一个6位数的验证码。

TOTP 算法的特点是:

  • 共享密钥:TOTP 算法的共享密钥是用户自己生成的,用户可以选择自己喜欢的任意密码,并把它保存在一个安全的地方。
  • 时间戳:TOTP 算法使用当前时间戳作为输入,生成验证码。
  • 验证码:TOTP 算法生成的验证码是一个6位数,它是根据共享密钥和当前时间戳计算出来的。

如何使用 TOTP

下面的代码已经验证过了 6位code码, 30秒种过期, 可以生成二维码, 扫描后用 app 上的软件使用, 获取的 totp code 与实现算出的 totp code 的一致.

代码
go
package main

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base32"
	"encoding/binary"
	"errors"
	"fmt"
	"net/url"
	"testing"
	"time"
)

// GoogleAuthenticator2FaSha1 只实现google authenticator sha1
type GoogleAuthenticator2FaSha1 struct {
	Base32NoPaddingEncodedSecret string //The base32NoPaddingEncodedSecret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. The padding specified in RFC 3548 section 2.2 is not required and should be omitted.
	ExpireSecond                 uint64 //更新周期单位秒
	Digits                       int    //数字数量
}

// otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
const testSecret = "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ" //base32-no-padding-encoded-string

// Totp 计算Time-based One-time Password 数字
func (m *GoogleAuthenticator2FaSha1) Totp() (code string, err error) {
	count := uint64(time.Now().Unix()) / m.ExpireSecond
	key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(m.Base32NoPaddingEncodedSecret)
	if err != nil {
		return "", errors.New("https://github.com/google/google-authenticator/wiki/Key-Uri-Format,REQUIRED: The base32NoPaddingEncodedSecret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. The padding specified in RFC 3548 section 2.2 is not required and should be omitted")
	}
	codeInt := hotp(key, count, m.Digits)
	intFormat := fmt.Sprintf("%%0%dd", m.Digits) //数字长度补零
	return fmt.Sprintf(intFormat, codeInt), nil
}

// QrString google authenticator 扫描二维码的二维码字符串
func (m *GoogleAuthenticator2FaSha1) QrString(label, issuer string) (qr string) {
	issuer = url.QueryEscape(issuer)
	return fmt.Sprintf(`otpauth://totp/%s?secret=%s&issuer=%s&algorithm=SHA1&digits=%d&period=%d`, label, m.Base32NoPaddingEncodedSecret, issuer, m.Digits, m.ExpireSecond)
}

func hotp(key []byte, counter uint64, digits int) int {
	//RFC 6238
	//只支持sha1
	h := hmac.New(sha1.New, key)
	binary.Write(h, binary.BigEndian, counter)
	sum := h.Sum(nil)
	//取sha1的最后4byte
	//0x7FFFFFFF 是long int的最大值
	//math.MaxUint32 == 2^32-1
	//& 0x7FFFFFFF == 2^31  Set the first bit of truncatedHash to zero  //remove the most significant bit
	// len(sum)-1]&0x0F 最后 像登陆 (bytes.len-4)
	//取sha1 bytes的最后4byte 转换成 uint32
	v := binary.BigEndian.Uint32(sum[sum[len(sum)-1]&0x0F:]) & 0x7FFFFFFF
	d := uint32(1)
	//取十进制的余数
	for i := 0; i < digits && i < 8; i++ {
		d *= 10
	}
	return int(v % d)
}
func TestTotp(t *testing.T) {
	g := GoogleAuthenticator2FaSha1{
		Base32NoPaddingEncodedSecret: testSecret,
		ExpireSecond:                 30,
		Digits:                       6,
	}
	totp, err := g.Totp()
	if err != nil {
		t.Error(err)
		return
	}
	t.Log(totp)
}

func TestQr(t *testing.T) {
	g := GoogleAuthenticator2FaSha1{
		Base32NoPaddingEncodedSecret: testSecret,
		ExpireSecond:                 30,
		Digits:                       6,
	}
	qrString := g.QrString("TechBlog:mojotv.cn", "Eric Zhou")
	t.Log(qrString)
}
func main() {
	//HXDMVCCCJJWSRB3HWIZR4IFUGFTMXBOZ 密码串,暂时没弄明白加密原理
	//expiresecond  刷新时间
	//digits 数字位数
	//label 使用者
	//issuer 颁发者
	//返回一个二维码地址
	a := GoogleAuthenticator2FaSha1{"HXDMVCCCJJWSRB3HWIZR4IFUGFTMXBOZ", 30, 6}
	b := a.QrString("fushisanlang", "dao.server")
	fmt.Println(b)
	escapeUrl := url.QueryEscape(b)
	fmt.Println("https://api.pwmqr.com/qrcode/create/?url=" + escapeUrl)
	totp, err := a.Totp()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("TOTP Code:", totp)
}