我是Oct,这篇文章主要介绍了TOTP的技术原理,并提供了一个Rust实现样例。
希望能给你带来一点启发(●’◡’●)
1. 背景
一次性密码(One-Time Password, OTP)又称动态密码或单次有效密码。相比较固定密码而言,一次性密码具有不可比拟的安全性优势。
一次性密码最广为人知的应用场景就是网上银行,以下是笔者经历的网上银行OTP发展简史:
- 早些时候,网上银行开通时会发放一张“刮刮卡”一样的一次性密码卡。密码卡上有一个一个的网格,由横纵坐标可以确定一个具体的网格,刮开后能看到一个数字。这就是该次操作需要的一次性密码。(我以前手欠,背着父母刮了两张卡玩,然后被狠狠教训了QAQ)
- 再后来,网上银行更新了电子令牌。这些设备一般有一个液晶显示屏和一个按键。按下按键,液晶显示屏将会亮起,显示一组数字和一个倒计时1。当倒计时归零时,数字将会刷新。当进行网上转账时,屏幕上显示的就是当前转账的一次性密码。
- 近年来,网上银行将“一次性密码”更换成了短信验证码(也是用后失效,所以也算广义的OTP(?)。只要用户持有在银行注册的手机号就可接收验证码。因为一般来说用户不会将注册的手机号出借给其它人,所以这种验证方法的安全性和便利性相较于以前的方式都有了很大的提升。
我以前就对电子令牌的功能感到好奇。这个小设备不能联网,却能生成服务器认可的密码。服务器是怎么知道电子令牌上显示的是什么密码的呢?后来我了解到,这就是时基一次性密码(Time-based One-Time Password, TOTP)。
2. TOTP的技术原理
时基一次性密码,是国际公认的双因素认证(Two-factor Authentication, 2FA)方案之一,已被写入国际标准RFC6238。
TOTP的核心可以总结为一组计算式:
T = (Current Unix time - T0) / X
TOTP = HOTP(K, T)
可以看到TOTP中使用了HOTP(HMAC-based One-Time Password)算法2。这是一种事件同步的OTP生成算法,其通过某一特定的事件次序及相同的种子值作为输入,通过HASH算法运算出一致的密码。那么,简化来说,TOTP就是使用时间+一串密钥
为输入数据,进行HASH后取得的一次性密码。
密钥是用户和服务器之间的秘密,没有第三方知道。加入时间这个公有元素则解决了重放攻击3的问题。眼尖的你可能注意到了,我们使用的是经过处理的T
而不是直接使用当前时间Current Unix time
。为什么不直接使用当前时间戳呢?这主要有两个原因:
- 直接使用当前时间戳,密码会一秒一变,非常不方便输入(什么手速能一秒钟输入一组6~8位的密码?)
- 任何设备的计时都存在误差,即使是联网的设备也不是时刻都在同步时间,因此用户的密钥生成设备和服务器之间存在一两秒的时差是应该被考虑在内的事情。
由于以上两个原因,我们不能直接使用当前时间戳,因此RFC6238中规定使用处理后的时间戳。具体来讲,就是将当前时间戳减去一个标定的数值T0
(一般为0)后对一个固定的时间间隔X
(一般为30)向下取整。这样一来,T就是一个每过X
时间间隔就更新一次的元素了。
3. TOTP Rust实战
use std::io::Result;
use data_encoding::BASE32;
use regex::Regex;
use ring::hmac;
// secret为随机生成的32位密钥
// setp为每个OTP的有效时长(生成间隔)
pub fn generate_totp_code(secret: String, step: u32) -> Result<u32> {
{
// 正则表达式检查secret是否符合标准要求
let reg = Regex::new(r"^[A-Z2-7=]{32}$").unwrap();
if !reg.is_match(&secret) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid TOTP secret",
));
}
}
let key = BASE32.decode(secret.as_bytes()).unwrap();
let time = chrono::Utc::now().timestamp() / step as i64;
// 将time转化为8B数组
let mut time_bytes = [0u8; 8];
for i in 0..8 {
time_bytes[i] = ((time >> ((7 - i) * 8)) & 0xFF) as u8;
}
// 计算HMAC
let hmac_tag = hmac::sign(
&hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, &key),
&time_bytes,
);
let hmac = hmac_tag.as_ref();
// 计算动态码
let offset = (hmac[hmac.len() - 1] & 0x0F) as usize; // 取最后一个字节的低4位
let mut code: u32 = 0;
for i in 0..4 {
// 取4个字节,组成一个32位整数
code <<= 8;
code |= hmac[offset + i] as u32;
}
code &= 0x7FFFFFFF; // 取低31位(忽略符号位)
code %= 1_000_000; // 取模(6位)
Ok(code)
}
4. 扩展:Authenticator & Google Authenticator
也有的设备上没有倒计时,而是每按一次按键,数字就会更新一次,这也是一种OTP,叫HMAC-based One-Time Password (HOTP) ↩︎