Google Authenticator实现原理

曾就职的公司好几家都使用谷歌认证器(Google Authenticator,俗称谷歌令牌),作为二次校验的工具.相比于短信这样的并不算安全的OTP(One Time Password),使用令牌可以增强安全性, 同时还节省了短信的费用.

类似产品还有阿里巴巴的身份宝,默认的时间是60秒; 腾讯的Token,时间是60秒

客户端(可以是Google Authenticator的APP,也可以是浏览器插件,或者集成进钉钉/企业微信等IM的类似插件等)绑定证书后, 会隔一段时间(如30s)产生一串随机数字(一般为6位). 服务端和客户端始终保持产生的数字相同,这样客户端在发起请求时,就多了一层校验.

问题是,如何保证两边的验证码一致? (实际上,客户端绑定完证书,从此之后断网,依然能和服务端生成一致的验证码)

本质上说,其实非常简单: 双方约定好使用的哈希函数,约定好一个秘钥,而后对(当前的时间戳/时间间隔)和秘钥做哈希运算,这样双方在时间戳相同的情况下,就能得到相同的一个哈希值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pyotp
import base64
import time

secretKey = base64.b32encode('the_key_of_shuang'.encode())
totp = pyotp.TOTP(secretKey)
code = totp.now()

print(code)

# OTP verified for current time
a = totp.verify(code)

print(a) # => True
time.sleep(30)
b = totp.verify(code)

print(b) # => False

输出为:

1
2
3
652804
True
False

Golang可参阅gotp实现


有个疑惑:

客户端和服务端是同一个秘钥吗? 这不就成了对称加密,如果客户端的秘钥泄露,不是很容易伪造吗?



更多参考:

双因子认证(2FA)

OTP,HOTP,TOTP基本原理

谷歌身份验证器后面的实现原理




通过浏览器插件,免打开手机,可参考 https://hi-andy.com/tools/Chrome_Plugins_Authenticator/




2024.07.02

佩文告诉了一个用go实现的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package google_auth

import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"fmt"
"net/url"
"strings"
"time"
)

var GlobalGoogleAuth *GoogleAuth

type GoogleAuth struct {
}

func NewGoogleAuth() *GoogleAuth {
return &GoogleAuth{}
}

func InitGoogleAuth() {
GlobalGoogleAuth = NewGoogleAuth()
}

func (this *GoogleAuth) un() int64 {
return time.Now().UnixNano() / 1000 / 30
}

func (this *GoogleAuth) hmacSha1(key, data []byte) []byte {
h := hmac.New(sha1.New, key)
if total := len(data); total > 0 {
h.Write(data)
}
return h.Sum(nil)
}

func (this *GoogleAuth) base32encode(src []byte) string {
return base32.StdEncoding.EncodeToString(src)
}

func (this *GoogleAuth) base32decode(s string) ([]byte, error) {
return base32.StdEncoding.DecodeString(s)
}

func (this *GoogleAuth) toBytes(value int64) []byte {
var result []byte
mask := int64(0xFF)
shifts := [8]uint16{56, 48, 40, 32, 24, 16, 8, 0}
for _, shift := range shifts {
result = append(result, byte((value>>shift)&mask))
}
return result
}

func (this *GoogleAuth) toUint32(bts []byte) uint32 {
return (uint32(bts[0]) << 24) + (uint32(bts[1]) << 16) +
(uint32(bts[2]) << 8) + uint32(bts[3])
}

func (this *GoogleAuth) oneTimePassword(key []byte, data []byte) uint32 {
hash := this.hmacSha1(key, data)
offset := hash[len(hash)-1] & 0x0F
hashParts := hash[offset : offset+4]
hashParts[0] = hashParts[0] & 0x7F
number := this.toUint32(hashParts)
return number % 1000000
}

// 获取秘钥
func (this *GoogleAuth) GetSecret() string {
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, this.un())
return strings.ToUpper(this.base32encode(this.hmacSha1(buf.Bytes(), nil)))
}

// 获取动态码
func (this *GoogleAuth) GetCode(secret string) (string, error) {
secretUpper := strings.ToUpper(secret)
secretKey, err := this.base32decode(secretUpper)
if err != nil {
return "", err
}
number := this.oneTimePassword(secretKey, this.toBytes(time.Now().Unix()/30))
return fmt.Sprintf("%06d", number), nil
}

// 获取动态码二维码内容
func (this *GoogleAuth) GetQrcode(user, secret string) string {
return fmt.Sprintf("otpauth://totp/%s?secret=%s", user, secret)
}

// 获取动态码二维码图片地址,这里是第三方二维码api
func (this *GoogleAuth) GetQrcodeUrl(user, secret string) string {
qrcode := this.GetQrcode(user, secret)
width := "200"
height := "200"
data := url.Values{}
data.Set("data", qrcode)
return "https://api.qrserver.com/v1/create-qr-code/?" + data.Encode() + "&size=" + width + "x" + height + "&ecc=M"
}

// 验证动态码
func (this *GoogleAuth) VerifyCode(secret, code string) (bool, error) {
_code, err := this.GetCode(secret)
fmt.Println(_code, code, err)
if err != nil {
return false, err
}
return _code == code, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package google_auth

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGoogleAuth_GetCode(t *testing.T) {
secret := "FSUBBMZAWF74ZC5L"
ga := NewGoogleAuth()
code, err := ga.GetCode(secret)
assert.Nil(t, err)
fmt.Println(code)
}

文章目录