在上一篇文章中,我们了解了 OTP(一次性密码)的基本概念和发展历程。然而,要真正理解 OTP 如何保障账户安全,我们需要深入探讨其背后的技术原理和算法实现。
- 第一篇:什么是OTP?
- 第二篇:OTP 技术原理与算法(当前阅读)
- 第三篇:OTP 实现方式与传输渠道(即将上线)
- 第四篇:OTP 应用方式(即将上线)
- 第五篇:OTP 的安全威胁与防御(即将上线)
本文将全面剖析 OTP 的两大核心算法:HOTP(基于哈希的一次性密码) 和 TOTP(基于时间的一次性密码),从数学原理到代码实现,带你彻底掌握这项关键的安全技术。无论你是安全工程师、后端开发者,还是对密码学感兴趣的技术爱好者,本文都将为你提供系统而深入的技术解析。
第一部分:密码学基础知识
1.1 什么是哈希函数(Hash Function)
在深入 OTP 算法之前,我们需要理解哈希函数这一密码学基石。哈希函数的定义:哈希函数是一种将任意长度的输入数据转换为固定长度输出的单向函数。
核心特性:
1)确定性(Deterministic)
- 相同的输入永远产生相同的输出
- 示例:SHA256(“hello”) 始终等于 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
2)单向性(One-way)
- 给定输出值,计算上不可能找到原始输入
- 无法从哈希值反推原始数据
3)雪崩效应(Avalanche Effect)
- 输入的微小变化导致输出的巨大差异
- 示例:
SHA256("hello") = 2cf24dba5fb0a30e...
SHA256("helo") = d3751d33f9cd5049... (完全不同)
4)抗碰撞性(Collision Resistance)
- 找到两个不同输入产生相同输出在计算上不可行
- 对于 SHA-256,碰撞概率约为 1/2^256
常用哈希算法对比
| 算法 | 输出长度 | 安全性 | 应用场景 |
|---|---|---|---|
| MD5 | 128 位 | ❌ 已破解 | 仅用于校验和,不用于安全 |
| SHA-1 | 160 位 | ⚠️ 弱安全 | 逐步淘汰 |
| SHA-256 | 256 位 | ✅ 强安全 | OTP、数字签名、区块链 |
| SHA-512 | 512 位 | ✅ 强安全 | 高安全要求场景 |
1.2 HMAC:带密钥的哈希函数
HMAC(Hash-based Message Authentication Code) 是基于哈希函数的消息认证码,它在哈希函数基础上增加了密钥。
HMAC 计算公式:
HMAC(K, M) = H((K ⊕ opad) || H((K ⊕ ipad) || M))
其中:
- K = 密钥(Secret Key)
- M = 消息(Message)
- H = 哈希函数(如 SHA-256)
- ⊕ = 异或运算
- || = 连接运算
- opad = 外部填充(0x5c5c5c...)
- ipad = 内部填充(0x363636...)
HMAC 的优势:
- 即使知道消息和输出,没有密钥也无法伪造
- 提供消息完整性和身份认证
- 是 OTP 算法的核心组件
简化理解:
python
# HMAC 的简化概念(实际实现更复杂)
def simple_hmac(key, message):
# 第一轮:用密钥和内部填充处理消息
inner_hash = sha256(key + ipad + message)
# 第二轮:用密钥和外部填充处理第一轮结果
outer_hash = sha256(key + opad + inner_hash)
return outer_hash
第二部分:HOTP 算法深度解析
2.1 HOTP 标准规范
官方标准:RFC 4226(2005 年 12 月发布)
全称:HMAC-Based One-Time Password Algorithm
制定组织:IETF(互联网工程任务组)
2.2 HOTP 工作原理
HOTP 基于事件计数器(Counter) 生成密码。服务器和客户端共享一个密钥(Secret Key)和一个同步计数器。每次生成新密码时,计数器递增。
核心概念:

2.3 HOTP 算法步骤详解
步骤 1:计算 HMAC 值
import hmac
import hashlib
import struct
def generate_hotp(secret_key, counter):
"""
生成 HOTP 一次性密码
参数:
secret_key: 共享密钥(字节串)
counter: 计数器值(整数)
返回:
6 位数字 OTP
"""
# 将计数器转换为 8 字节大端序字节串
counter_bytes = struct.pack('>Q', counter)
# 计算 HMAC-SHA1
hmac_result = hmac.new(
secret_key,
counter_bytes,
hashlib.sha1
).digest()
# hmac_result 是 20 字节的哈希值
return hmac_result
示例计算:
假设:
- Secret Key =
"12345678901234567890"(20 字节) - Counter =
0
secret = b"12345678901234567890"
counter = 0
# 计算 HMAC-SHA1
counter_bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00'
hmac_result = hmac.new(secret, counter_bytes, hashlib.sha1).digest()
# 输出(十六进制):
# cc93cf18508d94934c64b65d8ba7667fb7cde4b0
步骤 2:动态截断(Dynamic Truncation)
HMAC-SHA1 输出 20 字节(160 位),我们需要将其转换为 6-8 位数字密码。
动态截断算法:
def dynamic_truncate(hmac_hash):
"""
动态截断 HMAC 哈希值
参数:
hmac_hash: 20 字节的 HMAC-SHA1 结果
返回:
4 字节整数
"""
# 取最后一个字节的低 4 位作为偏移量
offset = hmac_hash[-1] & 0x0f
# 从偏移位置提取 4 个字节
truncated_hash = hmac_hash[offset:offset+4]
# 转换为 32 位整数(大端序)
code = struct.unpack('>I', truncated_hash)[0]
# 去掉最高位(确保为正数)
code = code & 0x7fffffff
return code
详细示例:
继续上面的例子,HMAC 结果为:
cc93cf18508d94934c64b65d8ba7667fb7cde4b0
- 取最后一个字节 0xb0,低 4 位为 0x0
- 偏移量 = 0
- 从位置 0 提取 4 字节:cc93cf18
- 转换为整数:0xcc93cf18 = 3428550424
- 去掉最高位:3428550424 & 0x7fffffff = 3428550424
步骤 3:生成最终密码
python
def generate_otp_code(truncated_value, digits=6):
"""
生成最终的 OTP 数字密码
参数:
truncated_value: 截断后的整数
digits: 密码位数(通常为 6 或 8)
返回:
格式化的数字密码字符串
"""
# 对 10^digits 取模
modulus = 10 ** digits
otp = truncated_value % modulus
# 格式化为指定位数(前导零填充)
return str(otp).zfill(digits)
最终计算:
python
truncated = 3428550424
digits = 6
otp = 3428550424 % 1000000 = 550424
最终 OTP: "550424"
2.4 HOTP 完整实现代码
python
import hmac
import hashlib
import struct
import base64
class HOTPGenerator:
def __init__(self, secret_key_base32, digits=6):
"""
初始化 HOTP 生成器
参数:
secret_key_base32: Base32 编码的密钥字符串
digits: OTP 位数(默认 6 位)
"""
# 解码 Base32 密钥
self.secret_key = base64.b32decode(secret_key_base32, casefold=True)
self.digits = digits
def generate(self, counter):
"""
生成 HOTP 密码
参数:
counter: 计数器值
返回:
OTP 字符串
"""
# 步骤 1: 将计数器转换为 8 字节
counter_bytes = struct.pack('>Q', counter)
# 步骤 2: 计算 HMAC-SHA1
hmac_hash = hmac.new(
self.secret_key,
counter_bytes,
hashlib.sha1
).digest()
# 步骤 3: 动态截断
offset = hmac_hash[-1] & 0x0f
truncated = struct.unpack('>I', hmac_hash[offset:offset+4])[0]
truncated &= 0x7fffffff
# 步骤 4: 生成 OTP
otp = truncated % (10 ** self.digits)
return str(otp).zfill(self.digits)
# 使用示例
if __name__ == "__main__":
# 生成测试密钥
secret = "JBSWY3DPEHPK3PXP" # Base32 编码
generator = HOTPGenerator(secret, digits=6)
# 生成不同计数器的 OTP
for counter in range(5):
otp = generator.generate(counter)
print(f"Counter {counter}: {otp}")
# 输出示例:
# Counter 0: 282760
# Counter 1: 996554
# Counter 2: 602287
# Counter 3: 143627
# Counter 4: 960129
2.5 HOTP 的同步问题
挑战:客户端和服务器的计数器必须保持同步。
失步场景:
- 用户多次点击”获取验证码”但未使用
- 客户端崩溃或重置
- 网络传输丢失导致验证失败
解决方案:Look-ahead Window(预测窗口)
python
class HOTPVerifier:
def __init__(self, secret_key_base32, look_ahead_window=10):
self.generator = HOTPGenerator(secret_key_base32)
self.look_ahead_window = look_ahead_window
self.server_counter = 0 # 服务器端计数器
def verify(self, user_otp, resync=True):
"""
验证用户提交的 OTP
参数:
user_otp: 用户输入的 OTP
resync: 是否允许重新同步计数器
返回:
(是否验证成功, 新的计数器值)
"""
# 在预测窗口内搜索匹配的 OTP
for i in range(self.look_ahead_window):
test_counter = self.server_counter + i
expected_otp = self.generator.generate(test_counter)
if user_otp == expected_otp:
if resync:
# 重新同步计数器
self.server_counter = test_counter + 1
return True, test_counter
# 未找到匹配
return False, self.server_counter
# 使用示例
verifier = HOTPVerifier("JBSWY3DPEHPK3PXP", look_ahead_window=10)
# 模拟用户提交计数器 3 的 OTP
generator = HOTPGenerator("JBSWY3DPEHPK3PXP")
user_otp = generator.generate(3)
success, new_counter = verifier.verify(user_otp)
print(f"验证成功: {success}, 新计数器: {new_counter}")
# 输出: 验证成功: True, 新计数器: 4
2.6 HOTP 的优缺点分析
优点:
- 不依赖时间同步:适合时钟不准确的嵌入式设备;离线环境也能工作。
- 实现简单:算法逻辑清晰;计算开销小。
- 可预生成备用密码:可以打印一系列备用码;适合应急恢复场景。
缺点:
- 计数器同步问题:需要维护同步状态;失步后需要重新同步。
- 有效期较长:OTP 没有自动过期机制;需要手动设置过期时间。
- 安全性相对较低:如果计数器可预测,攻击者可能提前计算未来的 OTP。
第三部分:TOTP 算法深度解析
3.1 TOTP 标准规范
官方标准:RFC 6238(2011 年 5 月发布)
全称:Time-Based One-Time Password Algorithm
核心创新:使用时间戳代替计数器
3.2 TOTP 工作原理
TOTP 是 HOTP 的改进版本,用当前时间代替事件计数器。
核心公式:TOTP = HOTP(Secret, T),其中:T = floor((Current Unix Time – T0) / X)
- Current Unix Time: 当前 Unix 时间戳(秒)
- T0: 初始时间(通常为 0,即 1970-01-01 00:00:00 UTC)
- X: 时间步长(通常为 30 秒)
时间窗口示例:
时间步长 = 30 秒
时间范围 时间计数器 T 生成的 OTP
──────────────────────────────────────────────────
00:00:00 - 00:00:29 T = 0 482910
00:00:30 - 00:00:59 T = 1 162583
00:01:00 - 00:01:29 T = 2 399871
00:01:30 - 00:01:59 T = 3 520489
...
3.3 TOTP 算法实现
python
import time
import hmac
import hashlib
import struct
import base64
class TOTPGenerator:
def __init__(self, secret_key_base32, time_step=30, digits=6,
algorithm='SHA1', t0=0):
"""
初始化 TOTP 生成器
参数:
secret_key_base32: Base32 编码的密钥
time_step: 时间步长(秒),默认 30
digits: OTP 位数,默认 6
algorithm: 哈希算法(SHA1/SHA256/SHA512)
t0: 初始时间,默认 0
"""
self.secret_key = base64.b32decode(secret_key_base32, casefold=True)
self.time_step = time_step
self.digits = digits
self.t0 = t0
# 选择哈希算法
if algorithm == 'SHA1':
self.hash_algo = hashlib.sha1
elif algorithm == 'SHA256':
self.hash_algo = hashlib.sha256
elif algorithm == 'SHA512':
self.hash_algo = hashlib.sha512
else:
raise ValueError("Unsupported algorithm")
def get_time_counter(self, timestamp=None):
"""
计算时间计数器
参数:
timestamp: Unix 时间戳(秒),默认使用当前时间
返回:
时间计数器 T
"""
if timestamp is None:
timestamp = time.time()
return int((timestamp - self.t0) / self.time_step)
def generate(self, timestamp=None):
"""
生成 TOTP 密码
参数:
timestamp: Unix 时间戳,默认当前时间
返回:
TOTP 字符串
"""
# 计算时间计数器
time_counter = self.get_time_counter(timestamp)
# 转换为 8 字节大端序
counter_bytes = struct.pack('>Q', time_counter)
# 计算 HMAC
hmac_hash = hmac.new(
self.secret_key,
counter_bytes,
self.hash_algo
).digest()
# 动态截断
offset = hmac_hash[-1] & 0x0f
truncated = struct.unpack('>I', hmac_hash[offset:offset+4])[0]
truncated &= 0x7fffffff
# 生成 OTP
otp = truncated % (10 ** self.digits)
return str(otp).zfill(self.digits)
def get_remaining_time(self):
"""
获取当前 OTP 剩余有效时间(秒)
返回:
剩余秒数
"""
current_time = time.time()
elapsed = (current_time - self.t0) % self.time_step
remaining = self.time_step - elapsed
return int(remaining)
# 使用示例
if __name__ == "__main__":
secret = "JBSWY3DPEHPK3PXP"
generator = TOTPGenerator(secret, time_step=30, digits=6)
# 生成当前时间的 TOTP
current_otp = generator.generate()
remaining = generator.get_remaining_time()
print(f"当前 OTP: {current_otp}")
print(f"剩余有效时间: {remaining} 秒")
# 输出示例:
# 当前 OTP: 482910
# 剩余有效时间: 18 秒
3.4 TOTP 验证与时间容差
由于客户端和服务器的时钟可能存在偏差,TOTP 验证时通常会检查当前时间窗口及其前后相邻窗口。
python
class TOTPVerifier:
def __init__(self, secret_key_base32, time_step=30, digits=6,
window=1):
"""
初始化 TOTP 验证器
参数:
secret_key_base32: Base32 编码的密钥
time_step: 时间步长
digits: OTP 位数
window: 时间窗口容差(前后各验证几个窗口)
"""
self.generator = TOTPGenerator(secret_key_base32, time_step, digits)
self.window = window
def verify(self, user_otp, timestamp=None):
"""
验证用户提交的 TOTP
参数:
user_otp: 用户输入的 OTP
timestamp: 验证时间点,默认当前时间
返回:
(是否验证成功, 时间偏移量)
"""
if timestamp is None:
timestamp = time.time()
# 当前时间计数器
current_counter = self.generator.get_time_counter(timestamp)
# 在时间窗口内验证
for offset in range(-self.window, self.window + 1):
test_counter = current_counter + offset
test_timestamp = self.generator.t0 + test_counter * self.generator.time_step
expected_otp = self.generator.generate(test_timestamp)
if user_otp == expected_otp:
return True, offset
# 验证失败
return False, None
# 使用示例
verifier = TOTPVerifier("JBSWY3DPEHPK3PXP", window=1)
# 模拟用户提交当前时间的 OTP
generator = TOTPGenerator("JBSWY3DPEHPK3PXP")
user_otp = generator.generate()
success, offset = verifier.verify(user_otp)
print(f"验证成功: {success}, 时间偏移: {offset}")
# 输出: 验证成功: True, 时间偏移: 0
时间容差窗口示例:

3.5 TOTP 的安全增强参数
参数 1:时间步长(Time Step)
| 时间步长 | 安全性 | 用户体验 | 适用场景 |
|---|---|---|---|
| 15 秒 | 高 | 较差(容易过期) | 高安全要求 |
| 30 秒 | 中高 | 良好 | 标准配置(推荐) |
| 60 秒 | 中 | 优秀 | 低安全要求 |
| 120 秒 | 低 | 极佳 | 不推荐 |
参数 2:密码位数(Digits)
| 位数 | 组合数 | 暴力破解难度 | 常见使用 |
|---|---|---|---|
| 4 位 | 10,000 | 低 | 不推荐 |
| 6 位 | 1,000,000 | 中 | 标准配置 |
| 8 位 | 100,000,000 | 高 | 高安全场景 |
| 10 位 | 10,000,000,000 | 极高 | 输入不便 |
参数 3:哈希算法(Hash Algorithm)
# 测试不同哈希算法的 TOTP
secret = "JBSWY3DPEHPK3PXP"
algorithms = ['SHA1', 'SHA256', 'SHA512']
for algo in algorithms:
generator = TOTPGenerator(secret, algorithm=algo)
otp = generator.generate()
print(f"{algo}: {otp}")
# 输出示例(同一时间窗口):
# SHA1: 482910
# SHA256: 720385
# SHA512: 164598
算法选择建议:
- SHA-1:兼容性最好,Google Authenticator 等主流 App 支持
- SHA-256:安全性更高,推荐用于新系统
- SHA-512:最高安全性,但部分旧设备可能不支持
3.6 TOTP 防重放攻击
即使在有效期内,同一个 TOTP 也不应被重复使用:
import redis
class TOTPVerifierWithReplayProtection:
def __init__(self, secret_key, redis_client):
self.generator = TOTPGenerator(secret_key)
self.redis = redis_client
def verify(self, user_id, user_otp):
"""
验证 TOTP 并防止重放攻击
参数:
user_id: 用户标识
user_otp: 用户输入的 OTP
返回:
是否验证成功
"""
# 先验证 OTP 是否正确
verifier = TOTPVerifier(self.generator.secret_key)
success, offset = verifier.verify(user_otp)
if not success:
return False
# 检查是否已使用过
cache_key = f"totp:used:{user_id}:{user_otp}"
# 尝试设置缓存(仅当不存在时)
is_first_use = self.redis.set(
cache_key,
'1',
ex=self.generator.time_step * 2, # 过期时间设为 2 个时间窗口
nx=True # 仅当键不存在时设置
)
if not is_first_use:
# OTP 已被使用过
return False
return True
# 使用示例
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
verifier = TOTPVerifierWithReplayProtection("JBSWY3DPEHPK3PXP", redis_client)
generator = TOTPGenerator("JBSWY3DPEHPK3PXP")
otp = generator.generate()
# 第一次使用
print(verifier.verify("user_123", otp)) # True
# 第二次使用相同 OTP
print(verifier.verify("user_123", otp)) # False(防重放)
3.7 TOTP 的优缺点分析
优点:
- 无需维护状态: 服务器不需要像 HOTP 那样存储计数器(Counter),只需验证当前时间戳下的计算结果,降低了数据库维护成本。
- 自动失效: 密码具有天然的时效性(通常为 30 秒),极大地缩短了攻击者截获并使用密码的窗口期。
- 用户体验良好: 用户无需担心因为多次点击而导致“失步”问题,只要手机时间准确即可。
缺点:
- 时钟依赖: 对客户端和服务器的时钟同步要求较高。如果手机时间偏差超过 1-2 分钟,验证将失效。
- 安全窗口: 在 30 秒的有效期内,如果不配合“防重放机制”,该验证码可以被多次使用。
第四部分:HOTP vs. TOTP 深度对比
为了方便你在实际项目中做决策,我们将两者进行多维度对比:
| 维度 | HOTP (RFC 4226) | TOTP (RFC 6238) |
| 核心变量 | 事件计数器 (Counter) | 当前时间戳 (Time) |
| 同步机制 | 预测窗口 (Look-ahead Window) | 时间偏移容差 (Time Tolerance) |
| 时效性 | 永久有效(直到被使用或计数器失效) | 短期有效(通常 30-60 秒) |
| 服务器负担 | 需要持久化存储每个用户的 Counter | 理论上无需存储状态(仅需 Secret) |
| 典型应用 | 硬件令牌、离线备用码、短信验证码 | 手机 App 验证器 (Google Authenticator) |
| 防重放 | 天然具备(验证后 Counter 即增加) | 需要结合缓存(如 Redis)手动实现 |
OTP 技术通过巧妙地结合 HMAC 算法 和 动态截断技术,将复杂的密码学原理转化为用户手机上简单的 6 位数字。从 HOTP 到 TOTP 的演进,体现了安全界从“基于事件”向“基于时间”的范式转移,在安全性与便捷性之间找到了极佳的平衡点。
下一章我们将带你了解 OTP 实现方式与传输渠道。