
双因子(多因子认证)
双因子认证多增加了一步一次性密码校验,更加安全。
有两种方式来生成一次性密码:
SMS-based(基于短信):每次用户登录,都会从注册手机那里收到一条包含一次性密码的短信
TOTP-based(基于时间):当开启双因子认证后,用户需要用特定的手机应用APP扫描二维码,这个应用程序会不断为用户生成一次性密码
TOTP工作流程
其中存在3个问题点:
应用程序如何用密钥(Secret Key)和计数器(Counter)生成一次性密码(One-Time -Password)
计数器如何更新?服务器如何保持跟踪计数器?
服务器怎么把密钥分享给应用程序?
第一个问题的答案就在HOTP算法中
HOTP又是什么呢
HMAC-Based One-Time Password. 该算法由IETF(互联网工程任务组)发布在RFC4226
使用方法:
从一个密钥和计数器生成HMAC哈希
hmac_result= HMAC-SHA-1(secret-key, counter)
通过上一步得到的输出是20个字节长的字符串,这个长字符串并不适合用作一次性密码,所以需要对它进行截断
int offset = hmac_result[19] & 0xf;
int bin_code = (hmac_result)[offset] & 0x7f << 24 | (hmac_result[offset+1] & 0xff) << 16 | (hmac_result[offset+2] & 0xff) << 8 | (hmac_result[offset+3] & 0xff) ;
finalOTP = (bin_code % (10 ^ numberOfDigitsRequiredInOTP));
SHA-1 HMAC bytes(Example)
-------------------------------------------------------------
| Byte Number |
-------------------------------------------------------------
|00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|
-------------------------------------------------------------
| Byte Value |
-------------------------------------------------------------
|1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a|
-------------------------------***********----------------++|
最后一个字节,hmac_result[19]的16进制是0x5a,低4位bit是0xa
offset 为字节0xa,十进制为10
从第10位开始算4个字节,得到0x50ef7f19,即动态二进制码,DBC1
DBC1的 MSB 是0x50,所以DBC2=DBC2
HOTP = DBC2 % 10^6=872921(6位动态码)
TOTP
TOTP
使用HOTP
算法获得一次性密码,两者不同的是TOTP使用Time
时间参数替换counter
(计数器),这样就解决了第二个问题。
与其初始化counter
并跟踪它,不如使用time
参数去获取OTP
(一次性密码)。因此服务器和手机并须同步时间,这样就不用去跟踪counter了。
当然为了解决服务器和手机时区不同步的问题,使用Unix timestamp
,这样就不用关注时区了。
但是Unix Time是以秒计算的,每秒都在变化,意味着生成的密码也将每秒变化,这样就不太好了。因此,需要设置一个significant interval
间隔区间来更新密码。
Goole Authenticator APP每30秒更改一次密码
counter = currentUnixTime / 30
解决了第二个问题,我们接下来用QR code
(二维码)来解决第三个同步密码问题。
QR code
虽然我们可以要求用户直接将密钥输入到他们的手机应用程序中,但出于安全原因,我们希望使密钥相当长,要求用户输入如此长的字符串不会是一种用户友好的体验。
现在手机都配置了摄像头,可以要求用户通过它来扫描QR code获取密码。我们要做的就说把密码转换成QR code展示给用户。
到现在3个问题都解决了,那么如何在程序中实现TOTP呢?
实现TOTP
有一些免费的手机程序(如Google Authenticator APP、Authy等),因此创建你自己的手机APP并不是必须的。
当用户请求开启双因子认证
服务器生成一个长度为20字节的密钥 secretKey = generateSecretKey(20)
为特定用户保存secret key到数据库中saveUserSecretKey(userId, secretKey)
把secret key转换成图片二维码image.qrCode = convertToQrCode(secretKey)
把图片二维码作为响应发送给用户responseresponse(qrCode)
用户扫描二维码获得secret key,用secret key、当前Unix time和HOTP算法,手机程序会生成动态密码
用户输入生成的动态密码
服务器从数据库中获取指定用户的secret key = getSecretKeyOfUser(userId)
如果服务器生成的动态密码和用户输入的相同,那么就可以为用户开启双因子认证。if (codeTypedByUser == getHOTP(secretKey, currentUnixTime / 30)) { enableTwoFactorAuthentication(userId);}
现在,在每次登录操作之后,我们需要检查这个特定用户是否启用了双因素身份验证。如果启用,那么我们要求在应用程序中显示一次性密码。如果输入的密码是正确的,那么只有这样才能对用户进行身份验证。
参考
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)