用 Python 实现 TOTP(基于时间的一次性密码)
一、什么是 TOTP?TOTP 是 基于时间的一次性密码,其核心思想是:
- 共享密钥(Shared Secret):用户与服务端共享一个 Base32 编码的密钥。
- 当前时间:以 30 秒为一个时间步(interval)。
- HMAC-SHA1 算法:将时间计数器与密钥进行 HMAC 运算,生成动态验证码。
公式:TOTP = HOTP(K, T),其中 T = floor((当前时间 - T0) / 30)
二、完整代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import hashlib import hmac import struct import time from datetime import datetime
def base32_decode(secret_b32): """Base32 解码实现""" base32_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" base32_map = {char: i for i, char in enumerate(base32_chars)} secret_b32 = secret_b32.upper().replace('=', '').replace(' ', '') if len(secret_b32) % 8 != 0: secret_b32 += '=' * (8 - len(secret_b32) % 8) binary_str = "" for char in secret_b32: if char == '=': break binary_str += format(base32_map[char], '05b') result = bytearray() for i in range(0, len(binary_str), 8): byte_str = binary_str[i:i+8] if len(byte_str) == 8: result.append(int(byte_str, 2)) return bytes(result)
|
说明:
- Base32 使用 A-Z 和 2-7 共 32 个字符。
- 每 5 位表示 1 字节,8 个字符 = 40 位 = 5 字节。
- 去除 = 填充后补齐为 8 的倍数。
TOTP 主类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| class TOTP: def __init__(self, secret_b32, digits=6, interval=30, digest=hashlib.sha1): self.secret = base32_decode(secret_b32) self.digits = digits self.interval = interval self.digest = digest
def timecode(self, for_time=None): """计算时间计数器 T = floor(unix_time / interval)""" if for_time is None: for_time = datetime.now() if hasattr(for_time, 'timestamp'): timestamp = for_time.timestamp() else: timestamp = time.mktime(for_time.timetuple()) return int(timestamp // self.interval)
def generate_otp(self, counter): """HOTP 算法核心:HMAC + 动态截断""" counter_bytes = struct.pack('>Q', counter) hmac_result = hmac.new(self.secret, counter_bytes, self.digest).digest() offset = hmac_result[-1] & 0x0F binary_code = ( ((hmac_result[offset] & 0x7F) << 24) | ((hmac_result[offset + 1] & 0xFF) << 16) | ((hmac_result[offset + 2] & 0xFF) << 8) | (hmac_result[offset + 3] & 0xFF) ) otp = binary_code % (10 ** self.digits) return str(otp).zfill(self.digits)
def now(self): """生成当前时间的 OTP""" return self.generate_otp(self.timecode())
|
- 测试函数
1 2 3 4 5 6 7 8 9 10 11 12
| def test_totp(): secret_b32 = "4SPZZBDHH77F3XEYBXM7BB2CQWSI56DM" totp = TOTP(secret_b32) print("TOTP 测试结果:") print(f"密钥 Base32: {secret_b32}") print(f"密钥字节: {totp.secret.hex()}") print(f"当前时间计数器: {totp.timecode()}") print(f"当前 OTP: {totp.now()}")
if __name__ == "__main__": test_totp()
|
三、运行结果示例
1 2 3 4 5 6 7
| (base) ┌──(cure㉿LAPTOP-CMAM5D0J)-[/mnt/e/ctf/smallcode/TOTP] └─$ python self.py TOTP 测试结果: 密钥 Base32: 4SPZZBDHH77F3XEYBXM7BB2CQWSI56DM 密钥字节: e49f9c84673ffe5ddc980dd9f0874285a48ef86c 当前时间计数器: 58712664 当前 OTP: 782045
|
四、核心算法详解
| 步骤 | 说明 |
| ————- | —————————– |
| 1. 时间计数器 | T = floor((now - 1970) / 30) |
| 2. 编码 | T → 8字节大端整数 |
| 3. HMAC-SHA1 | HMAC(K, T) |
| 4. 动态截断 | 取最后一个字节低4位作为偏移量 |
| 5. 取4字节 | 构造31位整数(最高位清零) |
| 6. 取模 | code % 10^6 |
| 7. 补零 | 补足6位 |