Google兼容OTP的js实现
本文讨论了Google Authentiator(GA)兼容的OTP程序的JavaScript实现。
OTP和应用流程
OPT就是One Time Password,它是一种比较安全的登录认证技术和模式。这个名字其实不太好,特别是对于普通用户,容易引起迷惑和误解。他们熟悉的用户名/密码的登录认证方式,包括IT专业人士使用的远程SSH连接登录,都是需要输入一个和用户关联的固定的密码,怎么会是一次性密码呢?所以可能“动态衍生密码”的描述更准确,但却也更技术化,不利于理解和推广。
OTP通常的使用场景通常就是用户认证,从信息安全的角度而言,OTP确实显然更安全,但对于用户而言,这个动态的密码其实是无法记忆和使用的,所以必须要使用一个工具来辅助认证。此外,OTP也很少完全单独使用,而通常是作为“多因子认证(Multi-Factor Authentication, MFA)”的一个组成部分来参与认证,提供更高的认证安全性来使用的。我们以GA和Gmail的认证过程为例,来说明一下这个过程:
1 下载并安装认证器软件
以前的认证器可能是一个专门的硬件设备,但现在都使用软件了。比如如果要使用谷歌的OTP二次认证,就需要下载和安装一个名为Google Authticatior的APP软件。
2 配置账号
GA支持多个符合OTP规范的登录项目。所以,我们可以在软件中专门为Gmail的登录创建一个OTP项目。创建的方式是首先使用浏览器访问谷歌的账号管理页面,在里面有一个MFA的设置项目,选择“身份认证器”,它会提示你使用APP来扫描一个二维码,并完成设置。
别被扫码这个动作迷惑了,从技术上而言,这个二维码就是一个专门为认证器而生成的密钥,它在服务端和用户的账号进行关联。而在客户端的APP程序里,扫码的过程,就是输入并且配置密钥的过程。扫码只是简化了这个信息的输入过程,提供了更好的用户体验,当然也增加了一点点技术和高级感。
3 登录验证
当需要访问Gmail的页面的时候,除了输入普通的用户名和密码之外,如果设置了MFA,就会进入附加验证的阶段。这是会提示你输入GA上面谷歌认证当前的密码(OTP),这时我们就需要打开GA手机程序,查看并输入这个密码,来完成附加的认证过程。
这就是一般的使用OTP进行登录的流程。由于每次认证的信息都会变化,本身就能够比固定密码提供更高的安全性(在防暴力破解和回放攻击方面)。另外,如果密码泄露,只要能够保证GA的安全,也不会造成伪冒的认证。这些机制,都能够大幅度的提高用户和应用体系的安全性。
Google验证器(GA)
前面已经提到,GA是一个由谷歌开发的OPT生成的手机软件(下图),类似的产品还有Microsoft Authenticator。
OTP认证器也不是什么新事物,资历比较老的用户可能会使用过RSA SecurID 这种产品(下图)。
本质上,它也是一个OTA实现,但它是硬件实现。当然,在当时的技术条件和应用场景中,这也是合理的。SecurID将算法和设置写到一个硬件芯片当中,无需一个复杂的计算环境,在当时提供了很大的应用方便性(反面是难以修改配置和调整部署,且成本高昂)。
基于相同的思路,在智能手机和手机软件大规模普及后,很自然的,就产生了GA这种产品。和原理的技术方案相比,GA和类似实现的价值和意义在于,它可以使用公开的算法和机制,基于手机软件,只通过配置的方式来进行实现。这样的操作,大大降低了这一技术的使用和部署成本,使大规模开放性应用成为可能。
笔者已经确定,GA和微软认证器使用的算法,也是完全相同的,看起来也算一个行业标准了。测试的方法是输入同一个密钥,然后观察在两个应用程序中的OTP是否相同。但有一个问题就是就是,双方生成的二维码,却是不能互相操作使用的,可能是编码中加了一些特别的内容和设置。而且,在中国,微软认证器软件更容易安装和配置,可以优先选择使用。
实现原理和参考代码
下面,我们会来示例,实现一个和GA兼容的OTP生成程序。
前面已经看到,OTP的本质,是基于一个固定的密钥,按照某种规则和算法,可以生成一个动态而且一致(在认证各方)的编码。这里有两个要点,就是变化并且一致。幸运的是,在我们的现实世界中,有一个事物天然能够符合这两个要求,就是-时间。
严格来讲,如果需要使用时间作为一个一致要素,在各方是需要保证一个“同时性的”,但在实践和工程上其实无法保证绝对的同时,只能通过降低计时经典的方式,来保证一个相对的同时,比如将计时的精度降低到30秒,这样如果双方的真实时间不完全同步,比如差了几秒钟,那么时间的数组可能也是一样的。另外,还需要考虑到这个应用流程中,是需要人参与的,人的反应和操作过程,也需要时间,就需要为“同时性”预留一些延迟和裕度。所以,现在GA的OTP使用的默认时间粒度为30秒,也就是说OTP每隔30秒会生成一个新的代码作为当前密码,这里考虑了两个系统间时间的差异和人为操作所需要的时间,可能就是一个比较合适的选择。虽然这里有一些裕度,但也要求系统的时间基本一致,这个在实践中需要了解并注意这种情况(需要各方有时间对齐的机制)。
除了时间之外,还有一种所谓“序列”的方式,也可以提供动态一致性。方式就是在两端同步记录并使用一个序列,使用一次OTP,就累进加1,作为动态信息。有一些网络银行登录,使用的序列一次性密码,大概也是这个原理。但这种方式有一些额外的操作和代价,需要记录当前状态,实现起来比较麻烦,使用的就不是那么广泛,这里就不深入讨论这种方式了。
有了动态一致的信息之后,只需要在原始固定的共享的密钥之上,在进行一些衍生计算,相同的算法,就可以保证计算结果也是一致的,可以用于匹配验证。那么,消息验证码(HMAC)算法就是一个比较合适的选择,它可以保证信息的离散性,也可以保证衍生过程的不可逆,适合OTP这种应用场景。
按照上述基本原理,我们就可以看到,实现一个OTP生成程序并不难,主要的问题是,我们自己编写的OTP程序,可能和GA不兼容,那么我们就需要再编写和部署我们自己的OTP客户端程序或者APP,那样就太麻烦了。直接使用GA,可能是最方便和快速的技术方案。这样就需要反过来操作,按照GA兼容的算法,实现相应的OTP生成程序部署在我们自己的应用系统中,然后,就可以直接使用GA来辅助登录验证了。
根据GA的算法规则,实现的JS参考代码如下:
GA OTP算法的默认设定为:
- 步进时间: 30s
- 加密摘要算法:HmacSHA1
- 密钥格式: Base32,最小8个字符
- 输出格式: 6位10进制字符
其核心设定和操作包括:
-
- 获取TimeBuffer
这段代码可以将当前的时间转换为一个整数,并进一步使用这个整数填充一个Buffer(字节数组)作为HMAC编码信息。这段处理的代码如下:
// timestemp in second
let epoch = Math.round( Date.now() / 1000);
// convert to hex of period value add offset
let time = Math.floor(epoch / options.period) + options.ofset;
// convert time to 8 byte buffer
let i = 8, timeBuf = Buffer.alloc(i);
while (i-- && time) {
timeBuf[i] = time & 0xFF;
time >>= 8;
};
-
- 使用Key对时间buffer进行HMAC计算
在认证的两端,都使用一个共享的认证密钥。GA使用一个Base32字符串作为这个密钥的格式,所以表面上这是一个字符串,但是在实际使用时,会将其转换为一个字节数组使用。
然后,GA算法使用这个字节数组作为Key,以TimeBuffer作为内容,进行HMAC计算。GA使用HMACSha1算法,其使用的摘要算法就是SHA1,所以,这个HMAC的计算结果是一个长度为20的Buffer(codeBuffer)。它的操作非常简单,直接使用crypto.createHmac.updata.digest方法。
-
- 对codeBuffer进行编码
OTP通常是给人来使用的,要考虑到方便性和可读性。GA的一般设定是6位10进制数字。所以还需要进一步转换工作。这个操作是GA OTP比较独特的地方,需要详细说明一下,相关代码的实现如下:
// offset of hmac
const offset = hmac[hmac.length - 1] & 0xf;
let icode =
((hmac[offset ] & 0x7f) << 24)|
((hmac[offset + 1] & 0xff) << 16)|
((hmac[offset + 2] & 0xff) << 8 )|
(hmac[offset + 3] & 0xff);
// return padding string
return (icode % (10 ** options.digits)).toString().padStart(options.digits,"0");
它先从codeBuffer的最后一个元素,对16求余,计算一个偏移值(0~15之间),然后取从这个偏移值开始的buffer中的四个元素,构造一个整数,并对100000(默认6位)求余获得的结果,就是最后需要的那个编码。
关于GA OTP算法的实现,笔者有几点小小的疑问,一是HMAC之后,生成的整数编码,并没有完整的使用到HMAC计算结果Buffer中所有的信息,只使用了其中的四位;二是对于时间的编码,也只是简单将其求余填入Buffer中,其实只有后三四位是有效变化的,而且不是随机信息,是否有一定的安全影响?
GA OTP算法为什么这样设计,或者有没有更好的设计和实现(显然应该有),已经超出了本文要探讨的内容,也许最优先考虑的是操作的简便性和计算性能吧。
Base32
GA的标准规范约定,它使用的密钥的编码方式是Base32,其字符组成为26个字母大写+234567六个数字(why?)。Base32字符串的形式是字符串,但它的实质,是一个32进制的整数,所以使用32个字符来表示它在各个进位上的值。
笔者另有一篇文章《从Base32编解码算法理解位移运算》详细讨论的Base32的编解码处理方式,这里不再赘述。只需要强调的是,在GA中,它使用的密钥,是使用Base32编码的,用户在使用的时候需要予以注意。比如,它会提醒你使用了非法的字符,可能指的就是1或者8、9等等,因为它们不是Base32字符。另外可能密钥的长度也有要求,那应该是基于安全性的考虑,需要比较长的字符串来提供更高的安全性。
扩展应用场景
除了常规的使用GA或者OTP客户端程序来做用户认证之外,其实基于它的原理和安全性,我们可以扩展很多应用场景。
- 客户端API工具的实时认证
在很多业务系统的运维和管理工作当中,为了方便和高效,通常会开发一些客户端(CLI)工具来辅助工作。和普通的应用系统登录-操作的业务流程不同,这些工具可能会直接使用预配置的认证信息,但这样也会带来一些安全的风险。我们可以在这一过程中,加入OTP和其认证环节,来提升客户端工具使用的安全性。比如,在输入某个命令进行操作时,需要需要同时输入OPT编码,就可以做到每次操作都进行动态的验证,并且只能由特定的管理员(安装并且配置了OTP密钥的程序)才能进行,显然更加安全,同时也没有造成太多不便。
- 智能门锁
还有一种有趣的应用场景就是分离式认证。比如小米的智能门锁的临时密码,笔者猜想,就应该是利用这个原理实现的,大致过程应该是这样的:
智能门锁在出厂时,每个都会内置一个随机并且唯一的密钥,然后用户在安装时绑定操作,就可以在自己的手机上记录这个密钥。在使用临时密码开锁时,用户需要先在自己的手机上,看到当前的OTP,并告知要开锁的人在智能门锁上输入,由于相同的密钥和算法,这个临时密码如果匹配,智能门锁就认为认证通过,予以开锁。由于这个代码是一次性并且不断变化的,用户也不用担心密码泄露和被滥用,这样就可以保证一定程度的安全。
小结
本文探讨了一种OTP技术方案和实现,特别是这个技术方案是兼容谷歌和微软认证器APP软件的,可以很好的结合使用,这样就可以在自己的业务应用系统中进行集成和应用,从而可以提高应用系统的安全性。
这个技术实现的核心有四个:
- 时间取值和编码方式
- 密钥编码方式限定在Base32
- 摘要算法,选择的是标准和成熟的SHA1
- 将摘要转换为整数字符串的算法使用了偏移值和四个其后位置的值
最后吐槽一下,SSH的登录,好像没有支持多因子认证的模式呢?这显然可以大幅度提高系统和用户信息安全的水平。
转载自:https://juejin.cn/post/7312736427325685796