探究了常见的动态密码的实现方式及其底层原理,并基于java做出了工程实践。
A.来源于一个现象的好奇
用过公司vpn的都知道,不管是阿里郎还是字节那个连vpn的工具(叫啥记不得了),在电脑连上的时候,都需要手机端做一个二次校验,输入六位数字,这个六位数字每隔几十秒会变动一下。
对于这个东东,有时候好奇心驱使,会去想,这是怎么实现的呢?
最开始我想的一个方案:
- 客户端维护一个定时器,每隔几十秒去请求一个获取验证码的接口,每次请求的时候,里面存储的验证码会被刷新。
这种想法很合理(起初),逐渐往深处想,这个二次验证(2FA)是干嘛用的呢?
- 当我们的办公电脑不在内网环境的时候,需要通过这个vpn软件连接内网,当连接上内网后一段时间不使用,会自动注销。
- 这个连接vpn的软件默认是登录了我们的公司账号的。
- 如果其他人在我们电脑打开的状态下接触到了我们的电脑,那么会因为无法输入验证码而无法进入公司内网。
这个二次验证的功能就是这样没错了,直到有一天,意外出现了:
- 手机打开飞行模式忘关了,然后我打开阿里郎,像往常一样在电脑上输入阿里郎的动态验证码,连接内网。
- 奇怪的事情发生了,竟然连上了!!!
- 这样我们上面的假想就推翻了,因为上面假想的是要去请求特定的接口来刷新验证码,按照这个逻辑,验证码肯定是服务端生成的。但是现在没网的状态下,也能正常登陆上,说明这个验证码的生成逻辑在客户端!!!
是怎样实现,服务端和客户端不进行通信的情况下,也能做二次验证的,这点引起了我的好奇心,于是开始了查找资料的过程, 就有了后面的梳理和实现。
说到这里,突然想起还有一个现象,就是在大概七八年前,银行卡进行支付的时候,有时候需要输入一个动态密码卡里面的动态密码才能正常支付,这个卡很明显是没联网的,但是也能实现安全的验证,现在想来和上面这个现象应该是一样的道理。
B.2FA
首先我们要了解一下什么是2FA,2FA的全称是双重身份验证,是一种为帐户增加额外安全性的方法。往往第一个因素是账户的密码,而第二个因素区别于传统的密码验证,由于传统的密码验证是由一组静态信息组成,如:字符、图像、手势等,很容易被获取,相对不安全。2FA是基于时间、历史长度、实物(信用卡、SMS手机、令牌、指纹)等自然变量结合一定的加密算法组合出一组动态密码,一般每60秒刷新一次。不容易被获取和破解,相对安全。(取自百度百科)
总结一下,2FA就是在静态密码验证的基础上,再结合一定的自然变量组合出动态密码进行二次验证。
很明显,上述这个现象就是一种2FA的实现。而2FA 系统的其他名称包括OTP(一次性密码)和TOTP(基于时间的一次性密码算法)。
我们这里详细的了解一下TOTP。
C.TOTP
1.什么是TOTP
TOTP(Time-Based One-Time Password Algorithm),是一种基于时间的一次性密码算法,算法的详细说明见:https://www.rfc-editor.org/rfc/rfc6238,其公式表示如下:
// K 代表我们在认证服务器端以及密码生成端(客户设备)之间共享的密钥
// C 表示事件计数的值,8 字节的整数,称为移动因子(moving factor)
// HMAC-SHA-1 表示对共享密钥以及移动因子进行 HMAC 的 SHA1 算法加密,得到 160 位长度(20字节)的哈希结果
// Truncate 表示截断函数 将得到的哈希值进行阶段
// digit 指定动态密码长度,比如我们常见的都是 6 位长度的动态密码
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
PWD(K,C,digit) = HOTP(K,C) mod 10^Digit
TOTP = HOTP(K, T) // T is an integer and represents the number of time steps between the initial counter time T0 and the current Unix time
More specifically, T = (Current Unix time - T0) / X, where the
default floor function is used in the computation.
TOTP算法实际上是基于HOTP(An HMAC-Based One-Time Password Algorithm)(基于事件计数的一次性密码生成算法),只不过把事件计数变成了时间计数。HOTP算法的详细说明见:https://www.rfc-editor.org/rfc/rfc4226。
2.原理详解(基于java-totp项目分析)
从上面的公式大概可以看出,是这么一个流程:
- 生成一个随机的密钥。
- 基于时间取一个计数值。通常的做法是用当前的时间除以动态密码的有效时间。
- 然后将这个计数值和密钥进行哈希运算,并截断到指定的长度。
- 最后对数位的最大值取余。
有一个java库实现了TOTP算法,我们可以详细分析一下实现的原理: https://github.com/samdjstevens/java-totp。
使用java-totp总共分为两个部分:
- 1.生成密钥。
- 2.验证动态密码。
密钥生成部分代码如下:
可以看到随机生成了20个字节并且用base32进行编码。
动态密码验证部分的代码如下:
大概分为以下几个部分:
- 1.先取出当前时间所在的桶(30秒一个桶)。
- 2.在允许的时间误差范围内进行验证。
- 3.验证的过程先用时间的桶和密钥进行一个哈希运算,然后取低32位(一个整数),最后对10^6取余。
可以看出来,这个实现完美遵循了上面算法的标准,原理也很好理解了。
3.这样真的安全吗
理解了这个算法后,还是有点疑惑,这个算法真的安全吗?
虽然说一个桶内生成的动态密码一定是一样的,但是不同的桶生成的密码一定是不一样的吗?这可不一定,注意到哈希后进行了低32位的截断操作,并且还有对数位的取余操作,其实这两个操作大大提升了密码碰撞的概率。根据生日攻击,是有可能被碰撞出来的。
但是,我们追求的是计算上的安全,在短短的几十秒内碰撞出来的概率,实在是太小了,并且实际应用的时候,如果连续输错几次,可以结合图形验证码进行进一步的验证。
所以,这样的算法在计算上,是非常安全的。
4.常见的支持TOTP的软件
1.Google Authenticator
Google Authenticator毫无疑问是最受欢迎的2FA软件,简洁轻便,无需登录google账号。缺点是数据都存储在本地,换手机的话需要导出数据。
2.Microsoft Authenticator
Microsoft Authenticator 支持工作、学校和非 Microsoft 帐户的多重身份验证。输入密码后,应用提供第二层安全保护。登录时,你将输入密码,然后系统将要求你再用一种方式来证明是你本人。请批准发送至 Microsoft Authenticator 的通知,或输入应用生成的验证码。
https://baike.baidu.com/item/Microsoft%20Authenticator/58322728?fr=aladdin
D.项目实践(基于Java)
我们可以简单的基于java-totp库实现一个用于2FA的验证service。(底层实现这种就没必要重复造轮子了)
先导包:
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
上代码:
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.code.DefaultCodeVerifier;
import dev.samstevens.totp.code.HashingAlgorithm;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrDataFactory;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;
import static dev.samstevens.totp.util.Utils.getDataUriForImage;
@Service
@Slf4j
public class TOTPVerifyService {
private final SecretGenerator secretGenerator = new DefaultSecretGenerator();
private final QrDataFactory qrDataFactory = new QrDataFactory(HashingAlgorithm.SHA1, 6, 30);
private final QrGenerator qrGenerator = new ZxingPngQrGenerator();
private final CodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(HashingAlgorithm.SHA1, 6), new SystemTimeProvider());
private final Map<String, String> uidAndSecret = new HashMap<>();
public Map<String, String> setupDevice() throws QrGenerationException {
// 生成 TOTP 密钥
String secret = secretGenerator.generate();
QrData data = qrDataFactory.newBuilder().secret(secret).issuer("ATFWUS-TEST").build();
// 将生成的 TOTP 密钥转换为 Base64 图像字符串
String qrCodeImage = getDataUriForImage(
qrGenerator.generate(data),
qrGenerator.getImageMimeType());
System.out.println(secret);
// 返回密钥 和 密钥二维码
Map<String, String> result = new HashMap<>(2);
result.put("secret", secret);
result.put("qrCodeImage", qrCodeImage);
return result;
}
public boolean deviceVerify(String uid, String secret, String code) {
if (verifier.isValidCode(secret, code)) {
// 将uid绑定secret并存储
uidAndSecret.put(uid, secret);
return true;
} else {
return false;
}
}
public void cancelDeviceVerify(String uid) {
// 取消绑定TOTP密钥
uidAndSecret.remove(uid);
}
public boolean checkCode(String uid, String verifyCode) {
// 用uid取出绑定的secret
if(!uidAndSecret.containsKey(uid)) {
return false;
}
String secret = uidAndSecret.get(uid);
return verifier.isValidCode(secret, verifyCode);
}
// test
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
TOTPVerifyService totpVerifyService = new TOTPVerifyService();
totpVerifyService.setupDevice();
String uid = "atfwus";
System.out.println("bind your device!");
while(true) {
System.out.println("input secret: ");
String secret = sc.nextLine();
System.out.println("input code: ");
String code = sc.nextLine();
if(totpVerifyService.deviceVerify(uid, secret, code)) {
break;
}
System.out.println("try again!!!");
}
System.out.println("bind device successful!!!");
System.out.println();
System.out.println("2FA check!!!");
while(true) {
System.out.println("input secret: ");
String secret = sc.nextLine();
System.out.println("input code: ");
String code = sc.nextLine();
if(totpVerifyService.checkCode(uid, code)) {
break;
}
System.out.println("try again!!!");
}
System.out.println("check code pass!!!");
}
}
将生成的secret复制到手机上,或者扫描生成的二维码即可完成验证。
E.总结
连公司vpn的那个软件其实就是通过TOTP的方式进行的验证(也有可能基于其它计数机制,但总的原理不变),这样客户端无需和服务端进行通信,也能实现安全的验证。现实生活中还能找到很多类似的应用例子:像银行的动态密码卡就是一个实例。文章来源:https://www.toymoban.com/news/detail-634101.html
参考资料
- https://www.rfc-editor.org/rfc/rfc6238 TOTP算法详细介绍
- https://www.rfc-editor.org/rfc/rfc4226 HOTP算法详细介绍
- https://github.com/samdjstevens/java-totp 一个基于TOTP实现的java库
- https://baike.baidu.com/item/2FA/14695073?fr=aladdin 2FA介绍
- https://segmentfault.com/a/1190000008394200 动态密码算法介绍
ATFWUS 2022-12-07文章来源地址https://www.toymoban.com/news/detail-634101.html
到了这里,关于【TOTP】基于时间的动态密码及其工程实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!