TOTP 验证码生成器原理

上网看到一个直接把TOTP密钥放入网址生成验证码的网站,于是了解了一下。 首先介绍一下TOTP。在 2FA 双因素认证中,基于时间的动态验证算法 TOTP 已被接纳为 RFC 6238 标准,成为 OATH 的基石。
TOTP 的计算步骤比较简单,大致如下:

获取当前时间戳:通常以 30 秒为一个步长(Step)。

T=CurrentTimeT0XT = \lfloor \frac{CurrentTime - T0}{X} \rfloor

HMAC-SHA1 运算:使用你的密钥(Secret)对步长 TT 进行哈希运算。

动态截断:从哈希结果中取出 6 位数字。

示例

TOTP 验证码生成器

当前验证码
......
有效剩余时间: --s

Python 脚本

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
import hmac
import hashlib
import time
import struct
import base64
import os

def get_totp_token(secret):
# 1. 处理密钥:Base32 解码
secret += '=' * (8 - len(secret) % 8) # 补齐长度以符合 Base32 规范
key = base64.b32decode(secret, casefold=True)

# 2. 获取时间步长(Time Step)和 剩余秒数
# 公式:T = (Current Time - T0) / X,默认步长 X = 30秒
now = int(time.time())
remaining_seconds = 30 - (now % 30)
intervals_no = now // 30

# 将步长转换为 8 字节的大端序字节流
msg = struct.pack(">Q", intervals_no)

# 3. HMAC-SHA1 签名
h = hmac.new(key, msg, hashlib.sha1).digest()

# 4. 动态截断(Dynamic Truncation)
# 取哈希值的最后 4 位作为偏移量
offset = h[-1] & 0x0f
# 从偏移量位置开始取 4 个字节
binary = struct.unpack(">I", h[offset:offset+4])[0] & 0x7fffffff

# 5. 生成 6 位数字验证码
token = binary % 1000000
return str(token).zfill(6), remaining_seconds

Cloudflare Worker 脚本

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const secret = url.pathname.slice(1).replace(/\//g, "");
if (!secret || secret === "") { // 如果没有输入 secret,返回使用说明
return new Response("用法: https://your-worker.url/YOUR_SECRET_HERE", { status: 400 });
}

try {
const { token } = await getTOTP(secret);
return new Response(token, {
headers: {
"Content-Type": "text/plain;charset=UTF-8",
"Access-Control-Allow-Origin": "*" // 允许跨域,方便调用
},
});
} catch (e) {
return new Response("错误: 无效的 Base32 密钥", { status: 400 });
}
}
};

async function getTOTP(secret) {
const keyBuf = base32ToBuf(secret);
const epoch = Math.floor(Date.now() / 1000);
const counter = Math.floor(epoch / 30);

const counterBuf = new ArrayBuffer(8);
const view = new DataView(counterBuf);
view.setUint32(4, counter, false);

const cryptoKey = await crypto.subtle.importKey(
"raw", keyBuf, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]
);
const signature = await crypto.subtle.sign("HMAC", cryptoKey, counterBuf);
const sigBytes = new Uint8Array(signature);

const offset = sigBytes[sigBytes.length - 1] & 0xf;
const code = ((sigBytes[offset] & 0x7f) << 24) |
((sigBytes[offset + 1] & 0xff) << 16) |
((sigBytes[offset + 2] & 0xff) << 8) |
(sigBytes[offset + 3] & 0xff);

return {
token: (code % 1e6).toString().padStart(6, "0")
};
}

function base32ToBuf(base32) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const str = base32.toUpperCase().replace(/=+$/, "");
const buf = new Uint8Array(Math.floor((str.length * 5) / 8));
let bits = 0, value = 0, index = 0;
for (let i = 0; i < str.length; i++) {
const idx = alphabet.indexOf(str[i]);
if (idx === -1) continue;
value = (value << 5) | idx;
bits += 5;
if (bits >= 8) {
buf[index++] = (value >> (bits - 8)) & 255;
bits -= 8;
}
}
return buf;
}

TOTP 验证码生成器原理

https://psu.monster/post/2026/5cb4b3a684c4

作者

psu

发布于

2026-03-28

更新于

2026-03-28

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×