1. 一句话定位

基于时间戳的一次性密码算法(Time-based One-Time Password, TOTP) 是广义双因素认证(Two-Factor Authentication, 2FA)中最核心的软件令牌标准(RFC 6238)。它基于客户端与服务端共享的对称密钥(Secret Key)以及当前 Unix 时间戳,通过 HMAC 散列和动态截断算法,完全在离线环境下同步生成 6 位(或 8 位)动态数字口令,生命周期通常为 30 秒。

关联:


2. 核心数学模型与计算流程

TOTP 本质上是 HOTP(RFC 4226) 的时间特化版本:HOTP 依赖物理按键递增的计数器 $C$,而 TOTP 将计数器 $C$ 替换为基于 Unix 时间步长的整除因子。

整个计算过程无需任何网络请求,分为五个严密的密码学步骤:

flowchart TD
    A["共享密钥 (Base32 编码字符串)"] --> B["1. Base32 解码为原始字节流 K"]
    C["Unix 毫秒时间戳 (当前系统时间)"] --> D["2. 时间步长计数: C = floor(T / 30)"]
    D --> E["转换为 8 字节大端序字节数组"]
    B & E --> F["3. HMAC-SHA1(K, C) 散列签名 (20 字节)"]
    F --> G["4. 动态截断 (Dynamic Truncation, DT) 提取 4 字节整数"]
    G --> H["5. 取模 10^6 补齐前导零 -> 输出 6 位动态口令"]

步骤详解

步骤一:密钥解码(Base32 Decoding)

服务端在初次绑定时生成随机二进制密钥(建议 160-bit / 20 字节),并将其以 Base32(RFC 4648) 编码展示为用户可见的字符串或二维码(otpauth://totp/...):

  • 字符集:A-Z 与 2-7(去除了容易混淆的 0/O、1/l/I,且不区分大小写,方便人肉输入);
  • 计算前先清洗空格与连字符,将其解码为字节数组 $K$。

步骤二:时间切片计数器(Time-step Counter)

读取当前纪元秒级时间戳 $T$(Epoch Seconds):
$$C = \left\lfloor \frac{T - T_0}{X} \right\rfloor$$

  • 默认参数:起始时间 $T_0 = 0$,时间步长 $X = 30$ 秒。
  • 将 64 位无符号整数 $C$ 写入固定 8 字节大端序(Big-Endian / Network Byte Order) 内存缓冲区。

步骤三:哈希消息认证码(HMAC Computation)

使用预设的单向哈希函数计算 HMAC 消息摘要:
$$\text{HS} = \text{HMAC-SHA1}(K, C)$$

  • 默认算法为 HMAC-SHA1(输出定长 20 字节 / 160 bit),规范亦允许 HMAC-SHA256 或 HMAC-SHA512。

步骤四:动态截断(Dynamic Truncation, DT)

这是 TOTP 算法最精妙之处。如何从 20 字节的哈希中公平、随机且确定性地取出 4 个字节?

  1. 取哈希摘要最后一个字节(第 19 字节)的低 4 位(Low 4 bits)作为偏移量(Offset):
    $$\text{Offset} = \text{HS}[19] \ \& \ \text{0x0F}$$
    ($\text{0x0F} = 15$,因此 Offset 的取值范围严格落在 $[0, 15]$ 之间)。
  2. 从 $\text{HS}[\text{Offset}]$ 开始连续提取 4 个字节:
    $$P = \text{HS}[\text{Offset} \dots \text{Offset}+3]$$
  3. 屏蔽最高有效位(MSB),防止有符号 32 位整型溢出:
    $$\text{BinaryCode} = ((P[0] \ \& \ \text{0x7F}) \ll 24) \mid ((P[1] \ \& \ \text{0xFF}) \ll 16) \mid ((P[2] \ \& \ \text{0xFF}) \ll 8) \mid (P[3] \ \& \ \text{0xFF})$$

步骤五:取模与格式化(Modulo & Padding)

将无符号 31 位整数对 $10^{\text{digits}}$ 取模,生成最终用户界面口令:
$$\text{Code} = \text{BinaryCode} \pmod{10^6}$$
如果计算结果不足 6 位,必须使用前导零补齐(例如 725 $\rightarrow$ 000725)。


3. 核心工程实现范式

3.1 客户端(浏览器纯前端 Web Crypto API)

现代浏览器完全无需第三方库即可利用硬件加速完成运算:

async function generateTOTP(secretBase32, period = 30, digits = 6) {
  // 1. Base32 解码
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  const cleanSecret = secretBase32.toUpperCase().replace(/[\s-]/g, "").replace(/=+$/, "");
  let bits = 0, val = 0;
  const keyBytes = [];
  for (let i = 0; i < cleanSecret.length; i++) {
    val = (val << 5) | alphabet.indexOf(cleanSecret[i]);
    bits += 5;
    if (bits >= 8) {
      keyBytes.push((val >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }

  // 2. 时间步长计数器 (8 字节大端序)
  const counter = Math.floor((Date.now() / 1000) / period);
  const counterBuffer = new ArrayBuffer(8);
  const view = new DataView(counterBuffer);
  view.setUint32(0, Math.floor(counter / 0x100000000));
  view.setUint32(4, counter >>> 0);

  // 3. Web Crypto API 执行 HMAC-SHA1
  const cryptoKey = await crypto.subtle.importKey(
    "raw",
    new Uint8Array(keyBytes),
    { name: "HMAC", hash: "SHA-1" },
    false,
    ["sign"]
  );
  const sig = await crypto.subtle.sign("HMAC", cryptoKey, counterBuffer);
  const hash = new Uint8Array(sig);

  // 4. 动态截断
  const offset = hash[hash.length - 1] & 0x0f;
  const binCode =
    ((hash[offset] & 0x7f) << 24) |
    ((hash[offset + 1] & 0xff) << 16) |
    ((hash[offset + 2] & 0xff) << 8) |
    (hash[offset + 3] & 0xff);

  // 5. 取模补零
  const code = (binCode % (10 ** digits)).toString().padStart(digits, "0");
  const remain = period - (Math.floor(Date.now() / 1000) % period);
  return { code, remain };
}

3.2 服务端核验容错(Python 原生实现)

import hmac
import hashlib
import time
import base64
import struct

def verify_totp(secret_base32: str, user_input_code: str, window: int = 1) -> bool:
    """
    验证 TOTP,window=1 允许时钟前后漂移 +/- 30 秒(容忍 3 个时间窗口)
    """
    # 补齐 Base32 填充字符 '='
    missing_padding = len(secret_base32) % 8
    if missing_padding:
        secret_base32 += '=' * (8 - missing_padding)
    key = base64.b32decode(secret_base32.upper(), casefold=True)

    current_counter = int(time.time()) // 30

    # 遍历容错窗口 [-1, 0, +1]
    for step in range(-window, window + 1):
        counter = current_counter + step
        msg = struct.pack(">Q", counter)  # 8 字节大端序整型
        h = hmac.new(key, msg, hashlib.sha1).digest()
        
        offset = h[19] & 0x0f
        bin_code = struct.unpack(">I", h[offset:offset+4])[0] & 0x7fffffff
        totp_code = str(bin_code % 1000000).zfill(6)
        
        # 使用恒定时间比较防止时序攻击
        if hmac.compare_digest(totp_code, user_input_code.strip()):
            return True
    return False

4. 关键边界考量与安全陷阱(Pitfalls)

4.1 时钟漂移(Clock Drift / Skew)

  • 现象:客户端手机时间比标准授时慢了 15 秒,而服务端开启了 NTP 对齐,导致用户输入验证码时服务端判定失败。
  • 治理规范:服务端验证口令时,必须引入滑动窗口(Look-around Window)。标准做法是核验当前时间片 $C$ 以及 $C-1$ 与 $C+1$(即允许前后各 30 秒的物理误差),窗口过大会降低抗暴力枚举能力,绝不能盲目放宽到几分钟。

4.2 重放攻击(Replay Attack)

  • 现象:在同一个 30 秒有效期内,拦截者截获到动态码并重新提交一次。
  • 防御机制:动态码不仅是“一次性有效(One-Time)”,更必须是“一次性消费(Single-Use)”。服务端核验成功后,必须将对应的 (UserID, Counter) 写入 Redis 设置 60~90 秒过期。若检测到相同计数器的再次提交,立刻拒绝。

4.3 字符串比对的时序攻击(Timing Attack)

  • 陷阱:在代码中使用普通字符串判等 code == input_code。
  • 后果:普通判等逐字符比对,遇第一个不匹配字符提前返回,攻击者可通过高精度网络延迟差推断每位字符。
  • 解法:必须使用密码学恒定时间比较函数(如 Python 的 hmac.compare_digest 或 Node.js 的 crypto.timingSafeEqual)。

4.4 密钥静态存储泄露风险(Symmetric Vulnerability)

  • 根本弱点:TOTP 是对称密钥体系,客户端拥有的 Secret 与服务端数据库保存的 Secret 完全一致。若服务端数据库遭遇拖库(SQL 注入/内部泄露),攻击者可批量计算出所有用户的离线 TOTP,使 2FA 形同虚设。
  • 演进对策:现代化生产系统(如 Google/GitHub)正积极推广基于非对称密码学的 FIDO2 / WebAuthn / Passkeys,私钥永不出用户硬件安全芯片(Secure Enclave)。

5. 常见 2FA 认证体系全景对比

认证方案核心协议 / 算法对称/非对称离线可用性防钓鱼能力 (Anti-Phishing)典型代表
TOTPRFC 6238 (HMAC-SHA1)对称密钥 (Symmetric)支持完全离线弱(用户仍可能在钓鱼假站手动输入)Google Authenticator, 1Password, Bitwarden
HOTPRFC 4226 (HMAC-SHA1)对称密钥 (Symmetric)支持完全离线弱(易因误触产生计数器失步)早期银行按键式物理动态口令牌
WebAuthn / PasskeysFIDO2 / W3C (secp256r1/Ed25519)非对称公私钥支持(需硬件握手)极强(天然绑定域名与信道来源,免疫钓鱼)YubiKey, Touch ID, Windows Hello
Push 2FA双向 TLS / 长连接推送专有业务通道不支持(强依赖网络)中等(易受 MFA Fatigue 疲劳轰炸撞库)Apple ID 弹窗, Microsoft Authenticator
SMS OTP服务端暂存 Redis + 电信网随机短效串不支持(依赖蜂窝网络)极弱(易受 SIM 换卡劫持、拦截木马)手机短信验证码

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web