【TOTP】基于时间的动态密码及其工程实践

探究了常见的动态密码的实现方式及其底层原理,并基于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.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

  • 28
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ATFWUS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值