2025-10-25-Python实现TOTP

2025-10-25-Python实现TOTP

十月 25, 2025

用 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) # 8字节大端

hmac_result = hmac.new(self.secret, counter_bytes, self.digest).digest()

# 动态截断(RFC 4226)
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. 测试函数
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位 |