TOTP - GitHub 的登录认证方案

7/4/2024 Authentication

什么是 TOTP?

双因素认证(2FA)教程 - 阮一峰的网络日志 (ruanyifeng.com) (opens new window)

上文 UKEY - 常见安全的 2FA 实现 中有提到:USBKEY 的身份认证,即密码 + 某件个人物品的方式,安全但不方便(用户不可能随时携带 UKEY)

相对而言,手机才是最好的替代品。密码 + 手机是当下最佳的双因素认证方案。国内的很多网站要求,用户输入密码时,还要提供短消息发送的验证码,以证明用户确实拥有该手机。但是,短消息是不安全的,容易被拦截和伪造,SIM 卡也可以克隆。已经有案例 (opens new window),先伪造身份证,再申请一模一样的手机号码,把钱转走

因此,安全的双因素认证不是密码 + 短消息,而是 TOTP(Time-based One-time Password),它是公认的可靠解决方案,已经写入国际标准 RFC6238 (opens new window)

GitHub 在 23 年启用了 TOTP 的 2FA 登录,原理如下

  1. 用户和服务器协商好一份统一的密钥 key

  2. 用户本地采用一个计时程序,每 t 秒生成一串定长的短验证码,验证码的生成公式如下

    H1=Hash(time,key) H_1 = Hash(time, key)
    其中,time 为当前时间戳,key 为协商好的密钥

  3. 在登陆时,系统要求用户输入当前的验证码 H1,后端接收到该登录请求后,将在后端根据当前时间和用户的密钥生成一份验证码 H2

  4. 将用户输入的 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,对以下数据进行签名

    TC=(TnowT1970)/30 TC = (T_{now}-T_{1970})/30
    其中,T 指时间戳,30 是登录码的刷新频率,通过该公式将得到 Hash' = H(Key, TC)

  • 将传入的 Hash 与 Hash' 比对,若不一样,则返回状态 ②,若一样,则认证成功,返回状态 ①

这里的 TC 计算是 TOTP 算法的精髓:本地计算 TC 后,将请求打到服务器,服务器会立马计算当前时间戳对应的 TC',由于向下取整的关系,在 30s 内,本地生成的 TC 和服务器的 TC' 将会保持一致,于是最后生成的哈希值将会一致(在证书一致的前提下)

当然本地和服务器的时间需要是同步的,同时还有少许的网络延迟

前端工作

前端需要做的工作如下

注册时

  1. 发送表单请求服务器register接口,获取私钥
  2. 将获取到的私钥在客户端进行本地 I/O,写作文件2fa.cer

登录时

  1. 本地读取2fa.cer,获得私钥字符串
  2. 通过私钥和当前时间戳,生成哈希值
  3. 将登录表单连同哈希值请求/login接口

编码

选型

后端:Springboot、MySQL

前端:Electron(考虑到本地文件 I/O)

实现

新建文件夹ing

Last Updated: 9/17/2024, 4:16:37 PM
妖风过海
刘森