TOTP(基于时间的一次性密码)生成与验证详解
下面我将详细解释如何生成和验证TOTP,并提供完整的Python实现示例。
TOTP工作原理
- 共享密钥:服务端和客户端共享一个Base32编码的密钥
- 时间窗口:时间被分为30秒为一个单位的窗口
- 计数计算:当前时间窗口计数 = 当前Unix时间戳 / 30秒
- HMAC计算:使用共享密钥和时间窗口计数通过HMAC-SHA1算法生成哈希值
- 动态码生成:从哈希值中提取4-8位数字(通常6位)
Python实现TOTP生成与验证
import hmac
import hashlib
import base64
import struct
import time
import os
def generate_secret_key(length=16):
"""生成随机的Base32密钥"""
# Base32字母表(RFC 3548)
base32_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
# 生成随机字节
random_bytes = os.urandom(length)
# 转换为Base32
secret = base64.b32encode(random_bytes).decode('utf-8')
# 移除Base32编码的填充等号
return secret.rstrip('=')
def generate_totp(secret, timestamp=None, digits=6):
"""
生成TOTP验证码
参数:
secret: 共享密钥(Base32编码)
timestamp: 时间戳(默认当前时间)
digits: 生成的验证码位数(4-8)
返回:
str: TOTP验证码
"""
# 确保密钥长度是8的倍数(添加填充)
secret += '=' * (8 - len(secret) % 8) if len(secret) % 8 else ''
# 解码Base32密钥
try:
key = base64.b32decode(secret.upper())
except:
raise ValueError("无效的Base32密钥")
# 获取当前时间戳(30秒为一个窗口)
counter = timestamp or int(time.time())
counter = counter // 30
# 将计数器打包为8字节的大端字节序
counter_bytes = struct.pack(">Q", counter)
# 使用HMAC-SHA1计算哈希值
hmac_hash = hmac.new(key, counter_bytes, hashlib.sha1).digest()
# 动态截断法(Dynamic Truncation)
offset = hmac_hash[-1] & 0x0F
dynamic_code = (
(hmac_hash[offset] & 0x7F) << 24
| (hmac_hash[offset+1] & 0xFF) << 16
| (hmac_hash[offset+2] & 0xFF) << 8
| (hmac_hash[offset+3] & 0xFF)
)
# 限制位数
code = dynamic_code % (10 ** digits)
# 格式化为固定位数
return str(code).zfill(digits)
def verify_totp(secret, code, digits=6):
"""
验证TOTP验证码
参数:
secret: 共享密钥(Base32编码)
code: 用户提供的验证码
digits: 验证码位数
返回:
bool: 验证是否通过
"""
# 考虑到时间漂移,检查前后一个时间窗口(总共3个窗口)
timestamp = int(time.time())
# 当前时间窗口
if generate_totp(secret, timestamp, digits) == code:
return True
# 前一个时间窗口(30秒前)
if generate_totp(secret, timestamp - 30, digits) == code:
return True
# 后一个时间窗口(30秒后)
if generate_totp(secret, timestamp + 30, digits) == code:
return True
return False
if __name__ == "__main__":
# 生成共享密钥
secret_key = generate_secret_key()
print(f"共享密钥: {secret_key}")
# 生成TOTP验证码
totp_code = generate_totp(secret_key)
print(f"当前TOTP验证码: {totp_code}")
# 验证TOTP验证码
print("\n验证测试:")
print(f"验证正确代码: {verify_totp(secret_key, totp_code)}") # 应返回True
print(f"验证错误代码: {verify_totp(secret_key, '123456')}") # 应返回False
# 模拟时间窗口切换
print("\n时间窗口切换测试:")
print("等待时间窗口切换...")
time.sleep(31) # 等待超过30秒以便切换到下一个时间窗口
new_totp_code = generate_totp(secret_key)
print(f"新TOTP验证码: {new_totp_code}")
print(f"验证过期代码: {verify_totp(secret_key, totp_code)}") # 应返回False
print(f"验证最新代码: {verify_totp(secret_key, new_totp_code)}") # 应返回True
print(f"验证上一个窗口代码: {verify_totp(secret_key, totp_code, timestamp=int(time.time())-30)}") # 应返回True
代码说明
1. 生成共享密钥
- 使用安全的随机数生成器(
os.urandom
)创建随机字节 - 使用Base32编码转换为可打印字符串
- 移除Base32的填充等号使密钥更简洁
2. 生成TOTP验证码
-
准备密钥:
- 确保密钥长度是8的倍数(添加等号填充)
- 使用base64.b32decode解码Base32密钥
-
时间窗口计算:
- 获取当前Unix时间戳
- 除以30得到时间窗口计数
-
HMAC-SHA1计算:
- 使用HMAC算法结合密钥和时间窗口计数生成哈希值
- HMAC提供消息完整性验证和身份验证
-
动态截断法:
- 从哈希值中提取4字节的动态代码
- 使用最后一个字节的低4位作为偏移量
-
生成数字验证码:
- 将动态代码转换为数字
- 截取指定位数(默认6位)
- 添加前导零使长度固定
3. 验证TOTP验证码
- 考虑时间漂移问题,检查三个连续时间窗口:
- 当前时间窗口
- 前一个时间窗口(30秒前)
- 后一个时间窗口(30秒后)
- 如果任一窗口生成的验证码匹配用户输入,则验证通过
TOTP最佳实践
-
密钥安全:
- 使用强随机密钥(至少128位)
- 安全存储密钥(加密存储)
- 避免在日志或客户端暴露密钥
-
用户体验:
- 使用6位验证码(足够安全且便于用户输入)
- 提供清晰的倒计时提示
- 支持恢复密钥机制
-
安全增强:
- 限制验证尝试次数(防止暴力破解)
- 实施账户锁定策略
- 记录所有验证尝试
-
与标准兼容:
- 遵循RFC 6238(TOTP)和RFC 4226(HOTP)标准
- 支持Google Authenticator等通用应用
使用pyotp库的简化版本
import pyotp
import time
# 生成共享密钥
secret = pyotp.random_base32()
print(f"共享密钥: {secret}")
# 创建TOTP对象
totp = pyotp.TOTP(secret, interval=30, digits=6)
# 生成当前验证码
current_code = totp.now()
print(f"当前TOTP验证码: {current_code}")
# 验证验证码
print(f"验证结果: {totp.verify(current_code)}")
# 生成Provisioning URI (用于生成二维码)
uri = totp.provisioning_uri("user@example.com", issuer_name="MyApp")
print(f"\nProvisioning URI:\n{uri}")
pyotp库提供了更简化的接口,同时保持了与标准兼容性,并支持生成用于认证器应用的URI。
总结
TOTP是一种强大的双因素认证机制,通过结合时间因素和共享密钥提供额外的安全层。实现时需要注意密钥安全、时间同步和用户体验等问题。对于生产环境,建议使用经过充分测试的库如pyotp,同时结合数据库安全存储用户密钥。