站点图标 久久日记本

两步验证原理和简单实现

简单来说这篇应该叫做在Garden上实现简单的两步验证功能,不过还是想简单记录一下,以供以后自己参考。

1. 什么是两步验证

参考维基百科:多重要素验证

多重要素验证(英语:Multi-factor authentication,缩写为 MFA),又译多因子认证、多因素验证、多因素认证,是一种电脑访问控制的方法,用户要通过两种以上的认证机制之后,才能得到授权,使用电脑资源[1][2]。例如,用户要输入PIN码,插入银行卡,最后再经指纹比对,通过这三种认证方式,才能获得授权。这种认证方式可以提高安全性。

国内多用手机短信作为两步验证码,因为SIM的可复制性和短信可攻击嗅探,这种方法存在不安全性。

如果不考虑网站数据库被攻破或者用户被钓鱼了两步验证代码的情况下,应用版的两步验证相对更安全。

2. 两步验证原理

这里我们说的两步验证,叫Time-Based One-Time Password,即TOTP

根据RFC 6238标准,供参考的实现如下:

    生成一个任意字节的字符串密钥K,与客户端安全地共享。
    基于T0的协商后,Unix时间从时间间隔(TI)开始计数时间步骤,TI则用于计算计数器C(默认情况下TI的数值是T0和30秒)的数值
    协商加密哈希算法(默认为SHA-1)
    协商密码长度(默认6位)

3. 简单的实现和demo

TOTP的Key会使用大写字母ABCDEFGHIJKLMNOPQRSTUVWXYZ和数字234567和生成secret,为什么不用0189呢,因为 0和O,1和l,8和B,9和g 很像,当然真正的原因应该只是历史原因。

如果你用了0189,那么Google和MS的验证器无法支持。

使用python的random.choice生成32位随机字符作为 secret 显示给用户

生成secret

''.join(random.choice(string.ascii_uppercase + '234567') for _ in range(32))

根据secret生成6位数字代码

def hotp_32bit(self, secret, counter=int(time.time() / 30), digits=6, digest='sha1'):
    key = base64.b32decode(secret.upper() + '=' * ((8 - len(secret)) % 8))
    counter = struct.pack('>Q', counter)
    mac = hmac.new(key, counter, digest).digest()
    offset = mac[-1] & 0x0f
    binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
    return str(binary)[-digits:].zfill(digits)

这样实现好了之后,改造一下页面:

后台 新增添加2fa功能,生成secret,用户绑定到app并验证通过后存储到数据库;

前台 登录逻辑,用户输入用户名密码后,

如果未启用2fa,则直接提供验证通过的token;
如果已启用2fa,则生成中间验证token,跳转到验证6位数字页面,再次验证后才能提供验证通过的token;

2021-06-16更新: 当然,严谨的验证需要将未启用和已启动分开验证可能更好

4. 参考资料

Implementing 2FA: How Time-Based One-Time Password Actually Works [With Python Examples]

totp-base32-vs-base64

How To Clone SIM Card Under 15 Minutes [Step by Step Guide]

维基百科:多重要素验证

维基百科:基于时间的一次性密码算法

退出移动版