使用两步验证提高账号安全性

很早之前我的 dnspod 账号被盗过,导致域名被人通过 Api 设置了泛解析,指向了一堆垃圾网站。从那之后我终于痛下决心,把常用的 WEB 服务全部梳理了一遍,使用各不相同的密码(Mac 和 iPhone 上我都买了 1Password,用来生成和管理这些密码)。对于支持两步验证的网站,我一定会启用它。最近,imququ.com 这个我基于 ThinkJS 开发的博客系统,后台登录也启用了两步验证。本文来讨论有关两步验证的话题。

什么是两步验证

两步验证,对应的英文是 Two-factor Authentication(2FA),有些地方也称之为 Two-step Verification,它跟 OAuth 2.0 没有关系,不要搞混了。从名字可以看出,「两步」是 2FA 的重点,广义的 2FA 是指提供多种方案完成用户权限鉴定。例如某些网站,在验证完用户名和密码之后,还需要输入手机收到的校验码才能登录成功;某些安全性要求比较高的场景,比如银行网银或者 VPN,会单独提供一个硬件设备辅助完成用户权限认证。这都是 2FA。

我们知道,登录非 HTTPS 网站时,用户名密码都是明文传输,很容易被人截获。即使采用了 HTTPS,如果电脑被装了键盘记录器,或者浏览器被装了不良扩展,密码还是会被盗。两步验证解决的问题是:即使用户名密码输入正确,还需要输入额外的校验码才能登录;而这个校验码是一次性的,即使在传输过程中被拦截到也不要紧。既然用户电脑环境并不一定安全,网站需要使用其他途径分发校验码,例如通过短信、电话、硬件设备,才能保证最终的安全。

这个网站列出了哪些网站支持 2FA,哪些不支持,以及各自支持生成校验码的方式。从这个列表中可以看到,大部分国外知名 WEB 服务都支持 2FA。这个列表最后一列,是指用软件实现(Software Implementation)的 2FA。它的工作原理是网站为每个用户生成一个密钥,用户手机需要安装能根据这个密钥算出校验码的软件,用户通过算好的校验码,在网站上完成额外的验证。这种方案成本低廉,使用方便,得到了大部分网站的支持。

HOTP 和 TOTP

前面说过,2FA 中使用的是一次性密码(One Time Password,OTP),也被称作动态密码。一般 OTP 有两种策略:计次使用和计时使用。计次使用的密码使用过一次就失效;计时使用的密码过一段时间就失效。

HOTP 的全称是 HMAC-based One Time Password,它是基于 HMAC 的一次性密码生成算法。HMAC 的全称是 Hash-based Message Authentication Code,是指密钥相关的哈希运算消息认证码。HMAC 利用 MD5、SHA-1 等哈希算法,针对输入的密钥和消息,输出消息摘要。HOTP 算法中,传入密钥 K 和计数器 C,得到数字校验码。

实际使用 HOTP 中,服务端会给用户生成密钥 K,并约定起始计数器 C。客户端根据 K 和 C 生成校验码,并在用户点击刷新按钮后将计数器加 1,同时更新校验码;而服务端会在每次校验成功后将计数器加 1,这就保证了校验码只能使用一次。但客户端刷新并不通知服务端,很可能出现客户端计数器大于服务端的情况。所以一般的实现里,服务端如果用 PASSWORD = HOTP(K, C) 验证失败,还会尝试 C+1、C+2...,如果匹配上了,就更新服务端的计数器,保证跟客户端步调一致。出于安全考虑,服务端会设置一个最大值,并不会无限制地尝试下去。

HOTP 的优点是可以事先算好一批校验码,用户可以把他们打印出来随身携带逐个使用,用一个划掉一个,达到客户端计数器累加的效果,这样可以完全不依赖于电子设备。HOTP 的缺点是计数器很容易不一致,服务端经常需要通过不断尝试来同步计数器,从而降低了安全性。

TOTP 的全称是 Time-based One-time Password,它是基于时间的一次性密码生成算法。TOTP 算法需要约定一个起始时间戳 T0,以及间隔时间 TS。把当前时间戳 now 减去 T0,用得到的时间差除以 TS 并取整,可以得到整数 TC。根据 PASSWORD = HOTP(K, TC) 就可以得到数字校验码。

TOTP 实际上只是把 HOTP 的递增计数器换成了与当前时间有关的 TS,从而在服务端 / 客户端时间一致的前提下,解决了 HOTP 需要同步计数器的问题。同时,TOTP 算法需要用到当前时间,需要现场计算,无法提前算好打印出来。默认情况下,TOTP 在间隔时间 TS 内都能通过校验,并不是一次有效。这个问题可以通过在服务端记录最后一次 TC 来解决,由于 TS 一般很短,通常也可以忽略。

在 Node 中使用

有一个名为 speakeasy 的 Node 包,可以用来生成 HOTP 或 TOTP,直接通过 npm install speakeasy 就能安装。Google 出品了一个名为 Authenticator 的客户端,支持 HOTP 和 TOTP,适用于各大移动平台。这里就以 speakeasyGoogle Authenticator 为例说明如何在 Node 中使用 2FA。

生成密钥

首先,通过以下代码可以生成密钥 K:

var speakeasy = require('speakeasy');
var key = speakeasy.generate_key({});
console.log(key);

{ 
  ascii: '@H?e#/)iOxS65k09h8g2DJ?/EavR7CCs',
  hex: '40483f65232f29694f785336356b303968386732444a3f2f4561765237434373',
  base32: 'IBED6ZJDF4UWST3YKM3DK2ZQHFUDQZZSIRFD6L2FMF3FEN2DINZQ'
}

speakeasy 默认会生成三种格式的密钥,其中 Google Authenticator 需要用到 base32。服务端需要为每个用户生成并存储密钥。

生成二维码

上面一步生成的密钥,用户可以在 Google Authenticator 里手动录入,更方便的做法是把密钥转成二维码。Google Authenticator 支持的二维码格式是:

otpauth://TYPE/LABEL?PARAMETERS

TYPE 支持 hotp 或 totp;LABEL 用来指定用户身份,例如用户名、邮箱或者手机号,前面还可以加上服务提供者,需要做 URI 编码。它是给人看的,不影响最终校验码的生成。

PARAMETERS 用来指定参数,它的格式与 URL 的 Query 部分一样,也是由多对 key 和 value 组成,也需要做 URL 编码。可指定的参数有这些:

  • secret:必须,密钥 K,需要编码为 base32 格式;
  • algorithm:可选,HMAC 的哈希算法,默认 SHA1。Google Authenticator 不支持这个参数;
  • digits:可选,校验码长度,6 位或 8 位,默认 6 位。Google Authenticator 不支持这个参数;
  • counter:可选,指定 HOTP 算法中,计数器 C 的默认值,默认 0;
  • period:可选,指定 TOTP 算法中的间隔时间 TS,默认 30 秒。Google Authenticator 不支持这个参数;
  • issuer:可选(强烈推荐),指定服务提供者。这个字段会在 Google Authenticator 客户端中单独显示,在添加了多个服务者提供的 2FA 后特别有用;

另外,Google Authenticator 也不支持指定 TOTP 算法中起始时间戳 T0。下面是两个完整的例子,将他们生成二维码,通过 Google Authenticator 扫描就可以添加进去了:

otpauth://hotp/TEST:ququ@test.com?secret=IBED6ZJDF4UWST3YKM3DK2ZQHFUDQZZSIRFD6L2FMF3FEN2DINZQ&issuer=ququblog&counter=0
otpauth://totp/TEST:ququ@test.com?secret=IBED6ZJDF4UWST3YKM3DK2ZQHFUDQZZSIRFD6L2FMF3FEN2DINZQ&issuer=ququblog

有关 Google Authenticator 二维码格式的更多说明,可以参考官方 wiki

服务端验证

以下代码分别可以生成 HOTP 和 TOTP,用于服务端验证用户提交的校验码是否正确。需要注意的是,代码中的 key 参数默认是 ascii 格式,传其他格式需要指定 encoding 参数。

speakeasy.hotp({ key : key.ascii, counter : counter};

speakeasy.totp({ key : key.ascii});

服务端根据用户信息读出密钥(针对 HOTP 还需要读出最新的计数器),算出服务端校验码,跟用户提交的校验码比较,如果一致就验证通过。针对 HOTP,验证失败需要尝试同步计数器;验证通过需要更新服务端计数器。

安全风险讨论

最后我打算聊一聊我能想到的关于 OTP 的安全风险点:

首先,无论是 HOTP 和 TOTP,在服务端验证时需要用到密钥,这就需要在服务端存储密钥,如果服务端代码或者数据库发生泄露,密钥可能会被人拿到。其次,生成二维码这一步也需要用到密钥,如果用户电脑存在恶意软件,也存在密钥泄露的风险。另外,手机 2FA 客户端也需要存储密钥,如果安全防护没做好,也有密钥被其他 APP 盗取的风险(所以从正规途径下载知名公司开发的 2FA 客户端,并且手机不要越狱或 ROOT 很重要)。后两个问题,使用独立的 OTP 硬件设备应该能解决。

但是,无论是何种 OTP,一定会存在需要禁用 OTP,或者更换密钥的场景(例如 HOTP 中计数器达到最大值,手机丢失或 OTP 硬件设备丢失),如果这里考虑不周,一样会影响账户安全。所以,WEB 安全也是一个系统工程,各方面都需要考虑周全。

注:我现在已经用 Authy 替代 Google Authenticator 了。Authy 可以方便地实现备份和多终端同步,而 Google Authenticator 在 iPhone 恢复备份时会丢失所有记录。

本文链接:参与评论 »

--EOF--

提醒:本文最后更新于 557 天前,文中所描述的信息可能已发生改变,请谨慎使用。

Comments