TOTP - GitHub 的登录认证方案
什么是 TOTP?
上文 UKEY - 常见安全的 2FA 实现 中有提到:USBKEY 的身份认证,即密码 + 某件个人物品的方式,安全但不方便(用户不可能随时携带 UKEY)
相对而言,手机才是最好的替代品。密码 + 手机是当下最佳的双因素认证方案。国内的很多网站要求,用户输入密码时,还要提供短消息发送的验证码,以证明用户确实拥有该手机。但是,短消息是不安全的,容易被拦截和伪造,SIM 卡也可以克隆。已经有案例 (opens new window),先伪造身份证,再申请一模一样的手机号码,把钱转走
因此,安全的双因素认证不是密码 + 短消息,而是 TOTP(Time-based One-time Password),它是公认的可靠解决方案,已经写入国际标准 RFC6238 (opens new window)
GitHub 在 23 年启用了 TOTP 的 2FA 登录,原理如下
用户和服务器协商好一份统一的密钥 key
用户本地采用一个计时程序,每 t 秒生成一串定长的短验证码,验证码的生成公式如下
其中,time 为当前时间戳,key 为协商好的密钥在登陆时,系统要求用户输入当前的验证码 H1,后端接收到该登录请求后,将在后端根据当前时间和用户的密钥生成一份验证码 H2
将用户输入的 H1 和后端生成的 H2 作比较,若一致则通过认证,否则拒绝
考虑到网络延迟和计算延迟,时间 time 该如何统一呢?其实很简单,通过除法向下取整的方式,举个简单的例子,95 和 107 在除以 30 并且向下取整时,得到的结果均为 3
同理,对于每个时间戳,减去 1970.1.1 日的初始时间得到的时间间隔(以秒为单位),而后除以 30 并取整,则在同一个 30s 内能够得到相同的 time
本文将对这一登录过程进行简单的复现
设计
后端需要实现两个接口:注册和登录,逻辑较为简单
- 注册时创建用户私钥,服务器数据库需要与用户本地统一
- 登录时通过比对用户本地哈希值(通过本地时间戳和私钥生成)与服务器哈希值(通过服务器时间和数据库中用户私钥生成)进行认证
数据表设计
数据表设计:user
字段名 | 类型 | 说明 |
---|---|---|
username | varchar | 用户名 |
password | varchar | 用户密码 |
key | varchar | 用户私钥 |
能用 redis 存吗,我在想,就不用建表了,用一个<String, [<String, String>]>
的结构存用户的密码和密钥
密钥生成
注册接口:生成私钥返回给用户,同时将用户名、密码和证书信息写入数据库表
接口说明
- URL:
/register
- Method:
Post
请求参数
参数 | 类型 | 示例 |
---|---|---|
username | String | "northboat" |
password | String | "123456" |
返回结果:res
状态码(code) | 信息(message) | 数据(data) |
---|---|---|
200 | "成功" | {"key":"私钥字符串"} |
500 | "服务器错误" | {"error":"具体错误信息"} |
TOTP 认证实现
登录接口:接收用户的签名结果,再通过用户 username 取出数据库中证书对当前时间戳进行签名并与客户端结果比对,进行一重认证,而后对密码进行二重认证
接口说明
- URL:
/login
- Method:
Post
请求参数
参数 | 类型 | 示例 |
---|---|---|
username | String | "northboat" |
password | String | "123456" |
key | String | "MII56DJKLA..." |
hash | String | "652156" |
返回结果:res
状态码(code) | 信息(message) | 数据(data) |
---|---|---|
200 | "成功" | null |
200 | "失败" | {"error":"具体错误信息"} |
500 | "服务器错误" | {"error":"具体错误信息"} |
具体过程
接受用户传参,先与数据库中密码 password 进行比对,若不一致则返回状态 ②,若一致继续第二因素认证
通过 username 读数据库取出私钥 Key,对以下数据进行签名
其中,T 指时间戳,30 是登录码的刷新频率,通过该公式将得到 Hash' = H(Key, TC)将传入的 Hash 与 Hash' 比对,若不一样,则返回状态 ②,若一样,则认证成功,返回状态 ①
这里的 TC 计算是 TOTP 算法的精髓:本地计算 TC 后,将请求打到服务器,服务器会立马计算当前时间戳对应的 TC',由于向下取整的关系,在 30s 内,本地生成的 TC 和服务器的 TC' 将会保持一致,于是最后生成的哈希值将会一致(在证书一致的前提下)
当然本地和服务器的时间需要是同步的,同时还有少许的网络延迟
前端工作
前端需要做的工作如下
注册时
- 发送表单请求服务器
register
接口,获取私钥 - 将获取到的私钥在客户端进行本地 I/O,写作文件
2fa.cer
登录时
- 本地读取
2fa.cer
,获得私钥字符串 - 通过私钥和当前时间戳,生成哈希值
- 将登录表单连同哈希值请求
/login
接口
编码
选型
后端:Springboot、MySQL
前端:Electron(考虑到本地文件 I/O)
实现
新建文件夹ing