Google Authenticator 实现的 OTP 算法


2FA 全称是 Two Factor Authentication,即双因子认证,是 MFA 的一种。日常访问系统时,除了用户名和密码外,一些安全级别更高的则会要求你启用 2FA。

简单来说,就是当你的密码泄露后,还有一个额外的认证手段来保证你的信息安全。关于 2FA 更加详细的内容,可以参考 IMB 的这篇文章:什么是双重身份验证 (2FA)

常见的 2FA 验证方式有短信,邮件,或者 Google Authenticator 等。这里主要是深入认识一下 Google Authenticator 以及它实现的 OTP 算法。

OTP 算法

OTP 全称是 One-time Password,一次性密码算法,通常我们也称为动态口令。一般 OTP 算法会有两种类型,基于 HMAC 的和基于时间的,即 HOTP 和 TOTP,在 Google Authenticator 中的开源仓库中,也说明其实现了这两种协议算法。

相对于静态的密码,OTP 的好处是可以有效地抵御重放攻击(Replay Attack)。

实现需求

在 OTP 中,最重要是需要两端同时生成同一个密码串,例如我们常用的场景就是在服务端(访问资源)和 Google Authenticator 这两端都生成同一个密码串,才能通过验证。

我们在刚开始使用 Authenticator 时,都需要扫码或者输入给到一个 Key,这个 Key 是双方认证的一个基础。

在协议文件 RFC 4226 中描述这个算法为:

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

其中 K 是 key 的值,C 是一个增量计数器的值,HMAC-SHA-1 是 HMAC-SHA-1 算法,Truncate 是截取算法。

增量计数器的存在才使得这个密码串是动态生成的,而不是固定不变的。

所以生成密码串的过程并不复杂,简单分三步走:

  1. 使用 HMAC-SHA-1 算法基于 K 和 C 计算出一个值
  2. 基于第一步的值再生成一个 4 字节的字符串
  3. 基于第二步的值生成最终的值

第一步很简单,且其结果是一个 20 个字节的字符串,第二步和第三步是为了将该字符串转为比较容易使用的 6(或者自定义)数字密码,也就是 Truncate 的实现。使用 Node 来实现的话,大概如下:

import crypto from "crypto";

const key = ""; // 你输入的 key
const count = ""; // 关于如何生成 count,后边我们再来解释

// 第一步
const hmac = crypto.createHmac("sha1", key).update(count).digest();

// 第二步
// 计算出一个偏移量,然后取其中 4 个字节的字符串来使用
const offset = hmac[hmac.length - 1] & 0x0f;
const value =
  ((hmac[offset + 0] & 0x7f) << 24) |
  ((hmac[offset + 1] & 0xff) << 16) |
  ((hmac[offset + 2] & 0xff) << 8) |
  (hmac[offset + 3] & 0xff);

// 第三步
// 确定我们要的数字密码长度,通过模运算获取最终结果
const code = value % 10 ** 6;

Count 的同步

如何生成 HOTP 中需要用到的 Count 呢,这里关键的是需要保证两端(服务端和客户端)的 Count 同步。

我们先假定 C = 1,服务端每次验证成功后会 C += 1 来更新 count,而客户端每次点击生成密码串时会 C +=1 ,如果每次成功验证后再点击客户端更新 count,一一对应的话是可以保证两端同步的。

但现实情况不会那么完美,协议中建议可以设置一个往前查询的窗口值,如果服务端验证 C + 1 时失败再尝试 C + 2, C + 3 等直到成功为止,成功验证后即保持同步了。

TOTP

基于 Count 多个验证来保持两端同步的 HOTP 算法是不是感觉没有那么好,稍微想想,时间本身就是一个可以在多端同步且一直在变化的值,所以 TOTP 基本上就是将 Count 换成了时间。

我们可以使用 Unix 时间戳来作为这个变化的值,但是,每一秒都在变化,人为输入密码的时候并没有那么快,是没法验证成功的。我们可以用一个时间片(如 30s 或者 60s)来作为一个有效期,在有效期期间,该值是稳定的。

实现代码也很简单:Math.floor(Date.now() / 1000 / 30) 即可。

密钥是最重要的

理解了 OTP 算法的实现,就能够明白密钥的重要性了,本质上验证的就是这个密钥,动态口令或者密码虽然是一次性的,但是密钥是永久有效的,保护好密钥才能确保你的信息安全。