什么是2fa?

2fa,即 two-factor authentication,双因素认证。在登录网站及计算机系统时,使用两个不同的认证因素来证明自己。双因素认证依赖用户提供的密码以及第二个因素,通常是一个token手机验证码,邮箱验证码或生物特征,例如指纹和面部。

为什么要用2fa?

平时使用密码对有些人来说已经是个挑战,我们经常忘记密码,不得不点击“忘记密码”,然后使用手机或邮箱进行重置。为何还要再添加新的验证方式?因为:

  • 数据泄露使许密码的安全性降低。
  • 多个站点使用同样的密码,黑客可以使用一个站点的密码进行撞库
  • 弱密码。

认证因素是什么?

很多网站提供“安全性问题”,来辅助验证或作为密码找回的手段!例如“第一所学校是什么”,“喜欢的宠物叫什么“,”最喜欢的电影是什么“,… 这些是否可以作为第二因素进行验证?通常这些内容对于熟知我们的人来说,并不是秘密,可以通过社会工程学的方式绕开。所以这些并不能作为第二因素。

平时使用的过程中,我们常常会在填入用户名和密码后,会让选择发送”验证码“,或输入指纹等。刚开始玩游戏的(应该是梦幻西游),会用一个带一小块显示屏的设备,每按一次会产生一个新的数字串作为第二因素。那这个产生数字串的设备的工作原理是什么?当时对计算机没有什么概念,就知道另一个名字:电脑,以为产生数字串的设备是一个联网设备,与服务端共享同一串数字!

其实,它是使用了 TOTP(Time-based One-time Password)的概念,全称是基于时间的一次性密码。在客户端与服务端共享密码的前提下,根据时间生成了一串数字。

TOTP算法

使用下面的时间计数器生成一串根据时间的数字:

TC = floor((unit.Time() - unix.Time(T0))/TS
  • TC: 时间计数器(Time Counter)
  • T0: 约定的起始时间,可以使用有默认值0
  • TS: 有效时间

Golang 实现

下面是 mojotv 的实现

 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
package main

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/binary"
	"fmt"
	"time"
)

func main() {
    key := []byte("MOJOTV_CN_IS_AWESOME_AND_AWESOME_SECRET_KEY")
		number := totp(key, time.Now(), 6)
		fmt.Println("2FA code: ",number)
}

func hotp(key []byte, counter uint64, digits int) int {
	//RFC 6238
	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 totp(key []byte, t time.Time, digits int) int {
	return hotp(key, uint64(t.Unix())/30, digits)
    //return hotp(key, uint64(t.UnixNano())/30e9, digits)
}

上面的 main 函数中,改写如下:

1
2
3
4
5
6
7
8
9
func main() {
  key := []byte("MOJOTV_CN_IS_AWESOME_AND_AWESOME_SECRET_KEY")
	number := totp(key, time.Now(), 6)
	fmt.Println("2FA code: ",number)
  time.Sleep(10*time.Second)
  
  number := totp(key, time.Now(), 6)
	fmt.Println("2FA code: ",number)
}

会出现两次密码不一致的情况,应该是是除去30的时间数值位于临界点,即类型:11/312/3的值不一样。重试一次,结果正确。

个人实现,将 totp 的实现修改如下:

1
2
3
4
func totp(key []byte, t time.Time, digits int) int {
  timeStr := uint64(t.Unix())
  return hotp(key, timeStr & (math.MaxUint64-30), digits)
}

使用 & 操作,将数字 timeStr 对于30s的位置为0

其他实现:

参考:

延伸阅读

  1. 2fa explained: How to enable it and how it works
  2. two-factor authentication (2FA)