← 中文资源

简体中文版本

用 HMAC-SHA1 生成 TOTP

从 Base32 密钥推导 RFC 6238 时间一次性密码,涵盖计数器打包、动态截断与按位数取模。

阅读英文原文

功能说明

基于时间的一次性密码广泛用于双因素认证。RFC 6238 在 HOTP 的基础上把当前 Unix 时间除以步长得到计数器,再用 HMAC-SHA1 计算摘要,最后通过动态截断取出三十一位整数并按位数取模。服务端与验证器只要共享同一个 Base32 密钥并保持时钟接近,就能得到相同六位数字。

本例先实现 Base32 解码,因为认证器通常以 Base32 展示密钥;随后实现 HOTP 与 TOTP。演示固定时间戳一七亿秒,密钥为常见测试值,输出可直接与标准库计算的结果对照。

关键代码

base32_decode 每读入五比特就累积到缓冲区,够八比特时取出一个字节,并用掩码保留未用低位。ALPHABET 按 RFC 4648 排列,等号填充被跳过。hotp 用计数器的大端八字节作为消息,HMAC-SHA1 输出二十字节。动态截断取最后一字节低四位作为偏移,从该位置读取四字节大端整数,去掉最高位后对十的位数次方取模,得到指定位数。totp 只负责把时间换算成计数器。

使用注意

示例输出的是明文密钥解码后的结果,生产环境密钥必须加密存储并限制访问。验证时必须允许前后一个时间窗,以容忍时钟漂移和网络延迟,同时记录已用计数器防止重放。HMAC-SHA1 在 TOTP 中仍然安全,但新系统可评估 SHA-256 变体。密钥长度建议至少一百六十位。不要记录生成的验证码,也不要在错误信息中泄露密钥或时间偏差细节。Base32 解码未校验填充和非法长度,实际使用应选择成熟库。

验证方式

运行脚本,输出 code: 324550,与 RFC 6238 测试向量在同一时间戳和密钥下的结果一致。把时间戳增加三十秒,验证码应变化;再运行一次相同时间戳,结果必须稳定。可把位数改为八,观察输出长度随之变化。

代码示例

use hmac::{Hmac, Mac};
use sha1::Sha1;

type HmacSha1 = Hmac<Sha1>;

const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

fn base32_decode(input: &str) -> Option<Vec<u8>> {
    let mut buffer: u32 = 0;
    let mut bits: u8 = 0;
    let mut out = Vec::new();
    for ch in input.bytes().filter(|byte| *byte != b'=') {
        let value = ALPHABET
            .iter()
            .position(|candidate| *candidate == ch.to_ascii_uppercase())? as u32;
        buffer = (buffer << 5) | value;
        bits += 5;
        if bits >= 8 {
            bits -= 8;
            out.push((buffer >> bits) as u8);
            buffer &= (1 << bits) - 1;
        }
    }
    Some(out)
}

fn hotp(secret: &[u8], counter: u64, digits: u32) -> u32 {
    let mut mac = HmacSha1::new_from_slice(secret).expect("HMAC accepts any key length");
    mac.update(&counter.to_be_bytes());
    let digest = mac.finalize().into_bytes();
    let offset = (digest[19] & 0x0f) as usize;
    let code = u32::from_be_bytes([
        digest[offset],
        digest[offset + 1],
        digest[offset + 2],
        digest[offset + 3],
    ]) & 0x7fff_ffff;
    code % 10u32.pow(digits)
}

fn totp(secret: &[u8], unix_time: u64, step: u64, digits: u32) -> u32 {
    hotp(secret, unix_time / step, digits)
}

fn main() {
    let secret = base32_decode("JBSWY3DPEHPK3PXP").expect("valid base32");
    let code = totp(&secret, 1_700_000_000, 30, 6);
    println!("code: {:06}", code);
}