文章目录
- Pre
- TOTP是什么
- TOTP 算法工作原理
- TOTP 生成公式
- TOTP 与 HOTP 的对比
- Code
- 生成TOTP
- 验证 TOTP
- 使用场景
- 小结
- TOTP 与 HOTP 的主要区别
- TOTP 与 HOTP应用场景比较
- TOTP 与 HOTP安全性分析
Pre
加密与安全_HTOP 一次性密码生成算法
https://github.com/samdjstevens/java-totp
https://medium.com/@rakesh.open.source/time-based-one-time-password-totp-java-implementation-82a472bd6bf9
https://gist.github.com/rakeshopensource/def80fac825c3e65804e0d080d2fa9a7
TOTP是什么
TOTP (Time-based One-Time Password) 是基于时间的动态密码生成算法,是 HOTP (基于HMAC的一次性密码) 的一个扩展。它的主要区别在于,TOTP 使用当前时间戳作为动态因子,而不是计数器。因此,生成的密码随时间变化,通常在一段时间(如30秒或60秒)内有效。
TOTP 算法工作原理
TOTP 使用当前时间戳与共享的密钥结合,生成一次性密码。以下是 TOTP 生成密码的主要步骤:
-
共享密钥:客户端和服务端预先共享一个密钥(通常是 Base32 编码),这和 HOTP 中的密钥是相同的。
-
时间戳:TOTP 使用当前时间戳,按时间步长(例如 30 秒)划分成时间段。每个时间段对应一个唯一的密码。
-
时间步数:将当前时间戳除以时间步长,得到时间步数。这个步数类似于 HOTP 中的计数器。
-
HMAC 计算:使用共享的密钥和时间步数,使用 HMAC-SHA1 算法计算出一个哈希值。
-
截取密码:从 HMAC 的输出中提取 6 位或 8 位的动态密码。
TOTP 生成公式
TOTP 的生成公式如下:
TOTP = Truncate(HMAC-SHA-1(K, T))
其中:
K
是客户端和服务端之间的共享密钥。T
是当前的时间步数,用公式T = (currentUnixTime - T0) / X
计算。currentUnixTime
是当前时间戳(以秒为单位)。T0
是时间的起始点(一般为0)。X
是时间步长(通常为30秒)。
HMAC-SHA-1
使用K
作为密钥,对时间步数T
进行 HMAC 计算。Truncate
是截取函数,将 HMAC 的结果截取为 6 位或 8 位的数字。
TOTP 与 HOTP 的对比
- 时间敏感 vs 计数敏感:TOTP 使用当前时间生成密码,因此密码是时间敏感的,每个密码只有在特定时间段内有效。HOTP 则基于计数器,每个密码对应一个计数器值。
- 同步问题:TOTP 依赖于时间戳,因此客户端和服务端的时间需要保持同步,而 HOTP 需要计数器同步。
Code
生成TOTP
下面是一个简单的 TOTP 实现,生成 6 位的一次性密码。代码依赖于 javax.crypto.Mac
和 java.security.Key
类来处理 HMAC-SHA1。
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.util.Base32;
import java.util.TimeZone;
import java.time.Instant;
import java.time.Clock;
public class TOTP {
// TOTP 生成算法
public static String generateTOTP(String secret, long time, int digits) throws Exception {
// 使用时间步长30秒
long timeStep = 30;
// 计算时间步数
long t = time / timeStep;
// 将密钥解码为字节
Base32 base32 = new Base32();
byte[] key = base32.decode(secret);
// 使用 HMAC-SHA1 计算
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA1");
mac.init(keySpec);
// 将时间步数转换为 8 字节的大端字节数组
byte[] timeBytes = ByteBuffer.allocate(8).putLong(t).array();
// 计算 HMAC 值
byte[] hash = mac.doFinal(timeBytes);
// 提取动态截取码
int offset = hash[hash.length - 1] & 0xf;
int binary = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
// 生成 OTP,取二进制数模10^digits,得到指定位数的OTP
int otp = binary % (int) Math.pow(10, digits);
// 格式化为指定位数
return String.format("%0" + digits + "d", otp);
}
public static void main(String[] args) {
try {
// 示例密钥(注意:密钥应为 Base32 编码)
String secret = "JBSWY3DPEHPK3PXP";
// 获取当前 Unix 时间戳
long currentTime = Instant.now().getEpochSecond();
// 生成 TOTP
String otp = generateTOTP(secret, currentTime, 6);
System.out.println("生成的 OTP: " + otp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码解析
- 密钥解码:使用 Base32 解码器将字符串密钥解码为字节数组。在 TOTP 中,密钥通常以 Base32 编码存储。
- 时间步长:时间步长设为 30 秒,即 TOTP 密码每 30 秒更新一次。
- HMAC 计算:使用 HMAC-SHA1 算法,结合密钥和当前时间步数来计算哈希值。
- 动态截取:从哈希值中截取出一个 6 位的密码。
验证 TOTP
验证 TOTP 时,客户端和服务端必须在同一时间段内生成相同的密码。为了处理客户端与服务端之间的时间不同步问题,服务端可以允许一个时间窗口范围(例如 ±1 个时间步长)来容错。
为了验证 TOTP,服务端会接收用户输入的 OTP,并根据当前时间戳生成自己的 TOTP,进行比对。如果两者匹配,验证成功,否则失败。为了容错,服务端通常会允许一定的时间窗口来处理客户端和服务端之间可能存在的轻微时间不同步问题。
TOTP 验证流程:
- 当前时间戳计算:服务端根据当前时间戳生成 TOTP。
- 时间窗口:为了容错,服务端可以生成多个不同时间步内的 TOTP,通常是当前时间步及前后时间步。用户输入的 OTP 与服务端生成的这些 OTP 进行匹配。
- 密钥共享:TOTP 的核心是基于一个共享的密钥,客户端和服务端都必须使用同样的密钥生成 OTP。
验证TOTP的Java代码
package com.artisan.otp.totp;
import org.apache.commons.codec.binary.Base32;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.time.Instant;
/**
* @author 小工匠
* @version 1.0
* @date 2024/10/2 9:37
* @mark: show me the code , change the world
*/
public class TOTPValidator {
/**
* TOTP 生成算法(和生成 OTP 的算法一致)
*
* @param secret
* @param time
* @param digits
* @return
* @throws Exception
*/
public static String generateTOTP(String secret, long time, int digits) throws Exception {
// 时间步长,通常为30秒
long timeStep = 30;
// 将时间转换为时间步数
long t = time / timeStep;
// Base32 解码密钥
Base32 base32 = new Base32();
byte[] key = base32.decode(secret);
// 使用 HMAC-SHA1
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA1");
mac.init(keySpec);
// 将时间步数转换为8字节的大端字节数组
byte[] timeBytes = ByteBuffer.allocate(8).putLong(t).array();
// 生成 HMAC 哈希值
byte[] hash = mac.doFinal(timeBytes);
// 提取动态截取码
int offset = hash[hash.length - 1] & 0xf;
int binary = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
// 生成指定位数的 OTP
int otp = binary % (int) Math.pow(10, digits);
// 格式化 OTP,确保是6位
return String.format("%0" + digits + "d", otp);
}
/**
* TOTP 验证算法
* @param secret
* @param inputOTP
* @param window
* @param digits
* @return
* @throws Exception
*/
public static boolean validateTOTP(String secret, String inputOTP, int window, int digits) throws Exception {
// 当前时间戳
long currentTime = Instant.now().getEpochSecond();
// 在前后时间步长的窗口内验证
for (int i = -window; i <= window; i++) {
String generatedOTP = generateTOTP(secret, currentTime + (i * 30), digits);
System.out.println("Generated OTP: " + generatedOTP);
if (generatedOTP.equals(inputOTP)) {
// 验证成功
return true;
}
}
// 验证失败
return false;
}
public static void main(String[] args) {
try {
// 示例密钥(注意:应为 Base32 编码)
String secret = "JBSWY3DPEHPK3PXP";
// 假设用户输入的 OTP(通常由客户端生成的 TOTP)
// 示例 OTP,需要实际生成 TOTP
String inputOTP = "306461";
// 获取当前 Unix 时间戳
long currentTime = Instant.now().getEpochSecond();
// 生成 TOTP
inputOTP = generateTOTP(secret, currentTime, 6);
System.out.println("inputOTP: " +inputOTP);
// 验证 OTP 容错窗口为1,6位OTP
boolean isValid = validateTOTP(secret, inputOTP, 1, 6);
if (isValid) {
System.out.println("OTP 验证成功!");
} else {
System.out.println("OTP 验证失败!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码说明
- generateTOTP: 生成 TOTP 的算法,使用当前时间戳和共享密钥生成一次性密码。
- validateTOTP: 验证用户输入的 OTP 是否有效。此函数允许一个时间窗口(
window
),比如前后 30 秒范围内的 OTP 都可接受。默认 6 位 OTP。 - Base32 解码:
Base32
编码用于解码密钥,因为密钥通常以 Base32 编码形式存储。 - 时间步长: TOTP 的时间步长通常是 30 秒。代码中以当前时间为基准,根据时间步长计算时间步数,生成 OTP。
- 容错窗口: 允许服务端生成当前时间步以及前后若干步内的 OTP,防止客户端与服务端时间不同步。
如何使用
- 密钥一致: 确保客户端和服务端使用同一个共享密钥。密钥通常是 Base32 编码的字符串。
- 验证 OTP: 服务端使用当前时间步生成 TOTP,并与用户输入的 OTP 进行比对。如果在设定的时间窗口内有匹配的 OTP,验证成功。
调试步骤
- 打印调试信息: 在生成和验证过程中打印 OTP 和相关的时间步,帮助确认时间步数和生成的 OTP 是否一致。
- 调整时间窗口: 如果客户端和服务端的时间差异较大,尝试增加
window
的大小,允许更大的容错范围。
这个实现适用于 TOTP 的典型应用场景,例如双因素认证 (2FA)【基于时间的一次性密码生成和验证】。
使用场景
- 双因素认证 (2FA):TOTP 是常见的 2FA 算法之一,用户使用手机中的身份验证器(如 Google Authenticator)生成一次性密码进行登录验证。
- 高安全性系统:银行、电子商务网站等需要额外的安全措施,使用 TOTP 来防止密码泄露和账户被劫持。
小结
TOTP 是一种基于时间的动态密码算法,通过时间戳和共享密钥生成一次性密码,常用于双因素身份验证场景。相比于 HOTP,TOTP 不需要计数器同步,使用更加便捷,但要求客户端和服务端的时间同步。
TOTP 与 HOTP 的主要区别
特点 | HOTP | TOTP |
---|---|---|
依赖性 | 计数器 | 时间戳 |
密码有效性 | 永久有效,直到被使用 | 短时间内有效(30 或 60 秒) |
生成方式 | 每次生成后计数器递增 | 每个时间周期内自动生成 |
安全性 | 密码可能长期有效,安全性较低 | 动态更新,安全性更高 |
适用场景 | 适用于基于事件的认证系统 | 适用于二次身份验证系统 |
TOTP 与 HOTP应用场景比较
-
HOTP 适用场景:
- 基于事件触发的认证系统:每次用户请求认证时,系统会递增计数器生成密码。这类系统适用于需要物理令牌或硬件设备的场景(如早期银行安全令牌)。
- 设备或网络不稳定环境:由于 HOTP 不依赖时间,客户端和服务器的时间不同步问题不会影响认证。
-
TOTP 适用场景:
- 二次身份验证(2FA):TOTP 在大多数现代的二次身份验证系统中使用,如 Google Authenticator、Microsoft Authenticator 等。用户每 30 秒生成一个新密码,确保密码及时失效。
- 需要更高安全性和频繁登录的系统:由于 TOTP 密码动态更新且过期较快,适合频繁使用和安全性要求较高的应用。
TOTP 与 HOTP安全性分析
- HOTP 的安全性弱点:由于 HOTP 密码在未使用前一直有效,攻击者可以通过拦截未被使用的密码进行重放攻击,或暴力猜测计数器值。
- TOTP 的安全优势:TOTP 基于时间戳生成,密码的有效期较短(通常 30 秒)。即使攻击者截获密码,也很难在其
过期前使用,因此 TOTP 提供了更强的安全性。