TOTP算法介绍

news/2025/2/24 9:14:49/文章来源:https://www.cnblogs.com/lori/p/18733339

摘要

本文根据 RFC4226 和 RFC6238 文档,详细的介绍 HOTP 和 TOTP 算法的原理和实现。

两步验证已经被广泛应用于各种互联网应用当中,用来提供安全性。对于如何使用两步验证,大家并不陌生,无非是开启两步验证,然后出现一个二维码,使用支持两步验证的移动应用比如
Google Authenticator 或者 LassPass Authenticator
扫一下二维码。这时候应用会出现一个6位数的一次性密码,首次需要输入验证从而完成开启过程。以后在登陆的时候,除了输入用户名和密码外,还需要把当前的移动应用上显示的6位数编码输入才能完成登陆。

这个过程的背后主要由两个算法来支撑:HOTP 和 TOTP。也分别对应着两份 RFC 协议 RFC4266 和 RFC6238。前者是 HOTP 的标准,后者是
TOTP 的标准。本文将使用图文并茂的方式详细介绍 HOTP 和 TOTP 的算法原理,并在最后分析其安全性。当然所有内容都是基于协议的,通过自己的理解更加直观的表达出来。

协议解决的核心问题

通过前面两步验证的使用场景分析,不难看出问题的核心在于如何能够让用户手机应用产生的验证码和服务器产生的验证码一致,或者是在一定范围内一致。参考下图:

所以我们的算法就是在解决如何更好的生成这个验证码,既能保证服务器端和客户端同步,还能保证验证码不重复并且不容易被别人反向破解出共享密钥。其中如果是计数,则是
HOTP, 如果是使用时间来生成验证码,则是 TOTP。

HOTP 算法图解

符号定义

对于 HOTP,通过上图我们已经看到输入算法的主要有两个元素,一个是共享密钥,另外一个是计数。在 RFC 算法中用一下字母表示:

  • K 共享密钥,这个密钥的要求是每个 HOTP 的生成器都必须是唯一的。一般我们都是通过一些随机生成种子的库来实现。
  • C 计数器,RFC 中把它称为移动元素(moving factor)是一个 8个 byte的数值,而且需要服务器和客户端同步。
    另外一个参数比较好理解,
  • Digit 表示产生的验证码的位数
    最后两个参数可能暂时不好理解,我们先放在这,等用到在解释
  • T 称为限制参数(Throttling Parameter)表示当用户尝试 T 次 OTP 授权后不成功将拒绝该用户的连接。
  • s 称为重新同步参数(Resynchronization Parameter)表示服务器将通过累加计数器,来尝试多次验证输入的一次性密码,而这个尝试的次数及为
    s。该参数主要是有效的容忍用户在客户端无意中生成了额外不用于验证的验证码,导致客户端和服务端不一致,但同时也限制了用户无限制的生成不用于验证的一次性密码。

基础知识

javax.crypto.Mac

javax.crypto.Mac 是 Java 中用于计算消息认证码(MAC)的类。MAC 是一种用于验证消息完整性和真实性的技术,通常在数据传输和通信中使用。

具体来说,javax.crypto.Mac 类提供了以下功能:

  1. MAC 计算:它允许你使用指定的密钥来计算消息的消息认证码。MAC 是通过将消息和密钥作为输入来生成的,确保了消息的完整性和未被篡改。

  2. 多种算法支持:Java的 javax.crypto.Mac 类支持多种不同的MAC算法,例如HMAC(基于哈希函数的MAC)等。

  3. 灵活性:它允许你根据需要使用不同的密钥来计算不同的消息的MAC。这对于确保数据的安全性和验证消息来源非常有用。

  4. 提供完整性:MAC 算法可以防止数据在传输过程中被篡改。如果接收到的消息的MAC与预期的MAC不匹配,那么可以确定消息已被篡改。

总之,javax.crypto.Mac 类是Java中用于实现消息认证码的工具,有助于确保数据的完整性和验证消息的真实性。这对于安全通信和数据传输非常重要。

javax.crypto.Mac验证消息完整性

当验证消息的完整性时,通常使用 HMAC(基于哈希函数的消息认证码)。下面是一个使用 javax.crypto.Mac 来计算和验证消息完整性的简单Java示例:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;public class MessageIntegrityVerification {public static void main(String[] args) throws Exception {// 1. 选择一个MAC算法(这里使用HMAC-SHA256)String algorithm = "HmacSHA256";// 2. 准备密钥String secretKey = "YourSecretKey"; // 替换成你的密钥Key key = new SecretKeySpec(secretKey.getBytes(), algorithm);// 3. 创建MAC对象并初始化Mac mac = Mac.getInstance(algorithm);mac.init(key);// 4. 要验证的消息String message = "This is a message to be verified.";// 5. 计算MACbyte[] macBytes = mac.doFinal(message.getBytes());// 6. 发送消息和MAC给接收方// 接收方将使用相同的密钥和消息来计算MAC,并比较它与接收到的MAC是否匹配// 模拟接收方验证boolean isValid = verifyMAC(key, message, macBytes);if (isValid) {System.out.println("消息完整性验证通过。");} else {System.out.println("消息完整性验证失败。");}}// 验证MAC的方法public static boolean verifyMAC(Key key, String message, byte[] receivedMAC) throws Exception {Mac mac = Mac.getInstance(key.getAlgorithm());mac.init(key);byte[] calculatedMAC = mac.doFinal(message.getBytes());// 使用MessageDigest的isEqual方法来比较两个MAC是否相等return Arrays.equals(calculatedMAC, receivedMAC);}
}

在上述示例中,我们选择了HMAC-SHA256作为MAC算法,准备了一个密钥,然后计算消息的MAC。在实际应用中,你将消息和MAC发送给接收方,接收方可以使用相同的密钥和消息来验证消息的完整性。

totp实例中的应用

  /*** 生成key.* @param password* @return* @throws UnsupportedEncodingException*/protected Key generateKey(String password) throws UnsupportedEncodingException {byte[] keyBytes = password.getBytes("UTF-8");SecretKeySpec signingKey = new SecretKeySpec(keyBytes, this.mac.getAlgorithm());return signingKey;}/*** Generates a one-time password using the given key and counter value.*/public synchronized int generateOneTimePassword(String password, final long counter) throws Exception {Key key = generateKey(password);this.mac.init(key);this.buffer[0] = (byte) ((counter & 0xff00000000000000L) >>> 56);this.buffer[1] = (byte) ((counter & 0x00ff000000000000L) >>> 48);this.buffer[2] = (byte) ((counter & 0x0000ff0000000000L) >>> 40);this.buffer[3] = (byte) ((counter & 0x000000ff00000000L) >>> 32);this.buffer[4] = (byte) ((counter & 0x00000000ff000000L) >>> 24);this.buffer[5] = (byte) ((counter & 0x0000000000ff0000L) >>> 16);this.buffer[6] = (byte) ((counter & 0x000000000000ff00L) >>> 8);this.buffer[7] = (byte) (counter & 0x00000000000000ffL);this.mac.update(this.buffer, 0, 8);try {this.mac.doFinal(this.buffer, 0);}catch (final ShortBufferException e) {// We allocated the buffer to (at least) match the size of the MAC length at// construction time, so this// should never happen.throw new RuntimeException(e);}final int offset = this.buffer[this.buffer.length - 1] & 0x0f;return ((this.buffer[offset] & 0x7f) << 24 | (this.buffer[offset + 1] & 0xff) << 16| (this.buffer[offset + 2] & 0xff) << 8 | (this.buffer[offset + 3] & 0xff)) % this.modDivisor;}

这段代码的作用是用于生成一个消息验证码(Message Authentication Code),通常用于验证消息的完整性和真实性。这里是一个简要的解释:

  1. generateKey(password) 函数生成一个密钥 key,这个密钥用于计算 MAC。

  2. this.mac.init(key) 初始化一个 MAC 实例,以便后续用密钥进行消息认证码的计算。

  3. 接下来的代码将 counter 的不同字节部分提取出来,并存储在 this.buffer 数组中,以便用于计算 MAC。counter
    是一个长整数,每个字节代表一个特定的位。

  4. this.buffer[0] = (byte) ((counter & 0xff00000000000000L) >>> 56);

    这行代码的实际意义是从一个长整数 counter 中提取出特定的字节,然后将其转换为字节类型 (byte)

    让我解释这行代码的各个部分:

    • counter & 0xff00000000000000L:这部分是一个位运算,使用与操作 & 和一个掩码 0xff00000000000000L
      ,它的作用是保留 counter 的最高字节(8个比特)并将其他字节清零。

    • >>> 56:这部分是无符号右移操作,它将上面结果的字节向右移动,使得最终的结果在字节的最低位置。

    所以,(byte) ((counter & 0xff00000000000000L) >>> 56) 这行代码提取了 counter
    的最高字节,并将其作为一个字节值返回。这通常用于将长整数的不同字节部分转换为字节数据,以便后续用于计算消息验证码等操作。

  5. this.mac.update(this.buffer, 0, 8) 使用 this.buffer 中的数据来更新 MAC 实例,以便进行 MAC 计算。

  6. this.mac.doFinal(this.buffer, 0) 完成 MAC 计算,将结果存储在 this.buffer 中。

  7. 最后,通过 trycatch 块捕获 ShortBufferException 异常,如果发生异常,会抛出一个 RuntimeException
    。这是因为在构造时,this.buffer 被分配了足够大的空间来存储 MAC 的结果,所以不应该发生短缓冲区异常。

总之,这段代码的主要作用是使用给定的密钥和计数器生成一个消息验证码,以确保消息的完整性和真实性。

算法流程

核心步骤主要是使用 K C 和 Digit。

  • 第一步:使用 HMAC-SHA-1 算法基于 K 和 C 生成一个20个字节的十六进制字符串(HS)。关于如何生成这个是另外一个协议来规定的,RFC
    2104 HMAC Keyed-Hashing for Message Authentication. 实际上这里的算法并不唯一,还可以使用 HMAC-SHA-256 和 HMAC-SHA-512
    生成更长的序列。对应到协议中的算法标识就是
    HS = HMAC-SHA-1(K,C)

  • 第二步:选择这个20个字节的十六进制字符串(HS 下文使用 HS 代替 )的最后一个字节,取其低4位数并转化为十进制。比如图中的例子,最后的字节是
    5a,第四位就是 a,十六进制也就是 0xa,转化为十进制就是 10。该数字我们定义为 Offset,也就是偏移量。

  • 第三步:根据偏移量 Offset,我们从 HS 中的第 10(偏移量)个字节开始选取 4 个字节,作为我们生成 OTP 的基础数据。图中例子就是选择
    50ef7f19,十六进制表示就是 0x50ef7f19,我们成为 Sbits
    以上两步在协议中的成为 Dynamic Truncation (DT)算法,具体参考以下伪代码:

Let Sbits = DT(HS)  // DT, defined below,
// returns a 31-bit string展开就是
DT(String) // String = String[0]...String[19]
Let OffsetBits be the low-order 4 bits of String[19]
Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15
Let P = String[OffSet]...String[OffSet+3]
Return the Last 31 bits of P
  • 第四步:将上一步4个字节的十六进制字符串 Sbits 转化为十进制,然后用该十进制数对 10的Digit次幂 进行取模运算。其原理很简单根据取模运算的性质,比如
    比10大的数 MOD 10 结果必然是 0到9, MOD 100 结果必然是 0-99。图中的例子,50ef7f19 转化为十进制为 1357872921,然后如果需要6位
    OTP 验证码,则 1357872921 MOD 10^6 = 872921。 872921 就是我们最终生成的 OTP。
    对应到协议中的算法为:
Let Snum = StToNum(Sbits) // Convert S to a number in
// 0...2^{31}-1Return D = Snum mod 10^Digit // D is a number in the range
// 0...10^{Digit}-1

这一步可能还需要注意一点就是图中案例 Digit 不能超过10,因为即使超过10,
1357872921 取模后也不会超过10位了。所以如果我们希望获取更长的验证码,需要在三步中拿到更多的十六进制字节,从而得到更大的十进制数。这个十进制决定了生成的
OTP 编码的长度。

TOTP 算法图解

在 HOTP 算法的基础上,对于 TOTP 算法的解释是不难了,因为 TOTP 实际上是基础 HOTP 的,只不过 HOTP 的计数器在 TOTP
中不再是直接的计数器了,而是使用时间来简介计数的。下图将会详细介绍 TOTP 是如何在 HOTP 基础上使用时间来计数的。

符号定义

时间窗口 X :表示多长时间算作计数器的一步,通常会设为30秒
初始计数时间戳 $T_0$: 使用 Unix 时间戳来表示 OTP 生成的时候的初始计数时间。

算法流程

TOTP 算法的关键在于如何更具当前时间和时间窗口计算出计数,也就是如何根据当前时间和 X 来计算 HOTP 算法中的 C。

HOTP 算法中的 C 是使用当前 Unix 时间戳减去初始计数时间戳,然后除以时间窗口而获得的。

算法安全性分析

上一节我们的算法中有两个参数没有用,T 和 s。这两个参数对安全有重要的作用。
官方协议在这里给出了5点安全要求,其中第一点是协议本身的要求,理论上进行约束,我们主要关心另外4点,分别是 HOTP
的验证,限制访问参数,重新同步参数以及共享密钥的管理。
对于二步验证的安全问题实际上就是如何保证第二步验证尽可能不被攻击的前提下向用户提供更方便的服务。

通过下图我们可以详细的了解 HOTP 的验证过程,同时还可以了解参数 s 和 T 的用途。

如果用户严格按照生成一次 OTP,然后验证一次的话,服务器直接可以验证成功。因为算法将会输入相同的参数。

如果用户无意间多生成了若干次 OTP 但是没有用来验证,服务器和客户端就产生差异,这时候服务器端会自动累积计数器来尝试匹配用户输入的
OTP,但是这种尝试是有限制的,这也就是前面说到的参数 s 的作用。一旦服务器尝试 s 次仍未匹配成功,那么就会通知客户端需要重新生成验证来验证,只要验证成功。

协议中对于参数 s 给出的建议是:服务器建议设置该参数,但是在保证可用性的前提下,尽可能小。另外还可以要求用户输入一个 HOTP
序列(比如连续生成多个 OTP 发送到服务器进行验证)来进行重新同步计数器。

既然涉及到重试,服务器同样对重试次数有所限制,从而防止暴力破解。这里当用户重试次数超过了阈值 T,服务器就应该将该账号标记为有安全风险,需要提醒用户。

协议中对个参数 T 给出的建议是:同样 T
也不建议设的很大。另外还提供了另外一个基于延时的策略还防止暴力破解攻击,服务器设置一个惩罚时间,一旦用户验证失败,需要在惩罚时间乘以失败次数的时间内禁止用户重新尝试验证。这个延时需要跨不同的登陆会话,以防止并发的攻击。

总结

通过上面的图解,基本解释了 HOTP 和 TOTP 两种算法的关键步骤,并且通过验证的场景对其安全要求进行了分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/888886.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5090D-deepseek-Anythingllm-Ollama运行测试

ollama ollama配置环境变量ollama地址与镜像 C:\Users\DK>curl http://10.208.10.240:11434 Ollama is running C:\Users\DK>ollama list NAME ID SIZE MODIFIED bge-m3:latest 790764642607 1.2 GB 28 ho…

web课程

<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>这是一个标题</title> </head> &…

解密ZAB协议:Zookeeper一致性的核心实现

一致性问题 设计一个分布式系统必定会遇到一个问题—— 因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡 。这就是著名的 CAP 定理。 ZooKeeper 的处理方式,保证了 CP(数据一致性) 一致…

解密prompt系列49. 回顾R1之前的思维链发展路线

我先按照自己的思路来梳理下R1之前整个模型思维链的发展过程,可以分成3个阶段:大模型能思考,外生慢思考,内生慢思考在所有人都在谈论R1的今天,作为算法也是有些千头万绪无从抓起。所以这一章先复盘,我先按照自己的思路来梳理下R1之前整个模型思维链的发展过程。下一章再展…

CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比

CSnakes 是一个用于在.NET项目中嵌入Python代码的工具,由.NET源生成器和运行时组成,能够实现高效的跨语言调用,Github:https://github.com/tonybaloney/CSnakes。以下是关键信息整理:核心特性跨版本支持:兼容.NET 8-9、Python 3.9-3.13,支持Windows/macOS/Linux系统高性…

Plotly.NET 一个为 .NET 打造的强大开源交互式图表库

前言 今天大姚给大家分享一个 .NET 强大、免费、开源的交互式图表库:Plotly.NET。 项目介绍 Plotly.NET 一个为 .NET 打造的强大、免费、开源的交互式图表库,支持 C# 和 F#编程语言,它构建在 plotly.js 之上并提供多个 API 层,用于创建、样式化和渲染美观的数据可视化图表。…

Apple Store 无法更新 App 解决方案 All In One

Apple Store 无法更新 App 解决方案 All In One Apple store 中可以查看新版 App, 但是无法更新新版 App, 会一直在转圈加载, 然后更新失败 ❌ 新版 Apps 与当前操作系统版本不匹配,需要先更新系统,才能安装新版的 Apps 更新 iOS 系统 ✅Apple Store 无法更新 App 解决方案 A…

合宙 ESP32C3 首个ardunio程序

https://blog.csdn.net/dpjcn1990/article/details/136085443 #define LED_BUILTIN1 12 #define LED_BUILTIN2 13//使用USB转串口即可 #define RS485_RX_PIN 1 #define RS485_TX_PIN 0 #define RS485_ENABLE_PIN 18 #define RS485 Serial1 void setup() { Serial.begin(9…

全网仅存方案,跟限速说拜拜!

关注A梦的小伙伴们都知道,A梦资源分享主要夸克网盘。有很多小伙伴也留言过想要其它网盘的链接,这方面确实众口难调,每个人的偏好和需求不同;另一个原因是夸克提供了较大的存储空间,能够更好地满足A梦的资源分享需求。 但网盘严重的限速也是很多小伙伴吐槽的点,考虑到A梦粉…

uniapp+h5---进行混合开发

uniapp和h5进行混合开发,最近在接手开发微信小程序,在技术选型的时候,拟采用uniapp+h5进行混合开发。 想必为啥要使用uniapp开发,就不用多说了?就是为了进行跨平台开发。如果使用微信小程序的开发方式,开发出来的应用就只能在微信小程序使用,但是使用uniapp就可以多端开…

dokcer-compose方式部署 mongo集群

声明:本人在单台机器上部署的mongo机器, mongo版本为8.0, 这玩意居然部署了一天,终于搞好了, 希望对后面想要部署的人有帮助 先创建一个mongodb目录, 后续的配置,数据都存放在这个目录中: 1. 创建mongo-secrets目录,mkdir mongo-secrets 2. 创建keyfile文件, openssl…

两个终端小玩具:Yazi和elinks

1. 终端文件管理器yazi 可以很方便地查找文件,跳转,并且很容易看出文件 1.1 下载安装 参考官方地址:鸭子官方安装手册安装rust编译环境curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup update编译yazigit clone https://github.com/sxyazi/yazi.git c…