Skip to content

身份验证器 ​

身份验证是身份访问管理(Identity and Access Management, IAM)的一个基本概念,让系统能够验证用户的身份。身份验证因素是在授予访问权限之前证明用户本人身份的安全机制。

目前存在三种类型的身份验证因素:

  • 知识因素(knowledge factor):一次性密码(one-time password, OTP),个人识别码(PIN)/密码、密保问题的答案;
  • 持有的对象(possession factor):移动设备或其他物理设备、硬件令牌(Ukey)、安全令牌/安全密钥;
  • 生物特征(inherence factor):生物特征,如指纹、面部识别、视网膜扫描、语音识别等;

像github、cloudflare之类的很多服务商都要求启用2FA来增强登陆的安全性。基本流程就是在服务商页面启用2FA时,用身份验证器(Authenticator)来扫描一个QR code,程序就会记录下这个服务提供的信息,并生成有一定有效期的验证码, 验证码过期后会重新生成。登陆服务商时, 第一个认证通过后,要求输入2FA的验证码,此验证码存在有效期,所以要在有效期内输入验证码。

2FA & MFA ​

双因子身份验证(2FA)是多因子身份验证的一种,是一种在授予在线帐户访问权限之前通过两个因素验证最终用户的身份的安全机制。

多因素身份验证 (MFA)要求用户在授权系统访问之前提供两个或更多不同的身份验证因素。它使用身份验证因素的组合。

本质上, 2FA是MFA的一个子集。MFA需要更多的因素来验证并在验证通过后授予用户访问权限。

但并不是说MFA比2FA更安全,而是取决于用户身份验证时采用的验证方式的组合,安全强度取决于其附加身份验证方法的安全性。比如说,电子邮件和短信验证码的安全性不如生物特征的身份验证,因为大量的网络钓鱼诈骗以及犯罪分子破解 SIM 卡(或是伪基站)的能力会带来额外的网络安全风险。当验证方式依赖于用户独有且难以复制的生物特征认证因素时,安全性会更有保障。

有了短信验证码还不够吗? ​

短信验证码只是表示这个验证码是被用户绑定的手机卡的设别接受,无法证明是用户本人,所以它安全但又不安全,存在一定的风险,例如:

  • 手机被盗;
  • 伪基站;
  • 附属卡克隆;
  • 有效期内验证码泄漏;
  • 手机安装了木马、病毒;

单一的短信验证码, 需要叠加其他验证方式,更能提高账户安全性。

Authenticator原理 ​

身份验证应用程序的工作原理基于某个验证模型。 当您在账上设置时,账户服务器会创建一个二维码,该二维码能被身份验证应用程序扫描。 二维码内涵一个秘密的算法约定。

只有身份验证应用程序和账户服务器才拥有该秘密算法。 他们会在同一时间内各自使用这个密钥来生成完全相同的验证码。

当用户登录时,他们需要输入身份验证应用程序中显示的验证码。 服务器则会检查输入的验证码是否与其生成的验证码相匹配。 如果验证码匹配,则通过此次验证。 如果不匹配,则验证不通过。

TOTP算法 ​

Time-based One-Time Password(基于时间的一次性密码算法),简称TOTP,是一种根据预共享的密钥与当前时间计算一次性密码的算法。

根据RFC 6238标准,供参考的实现如下:

  • 生成一个任意字节的字符串密钥K,与客户端安全地共享。
  • 基于T0的协商后,Unix时间从时间间隔(TI)开始计数时间步骤,TI则用于计算计数器C(默认情况下TI的数值是T0和30秒)的数值
  • 协商加密哈希算法(默认为HMAC-SHA-1)
  • 协商密码长度(默认6位)

参数协商完毕,密码开始按照如下方法生成:

  1. 从T0开始已经过的时间,每个TI为一个单位,总数记为C。
  2. 使用C作为消息,K作为密钥,计算HMAC哈希值H(定义来自之前的HMAC算法,但是大部分加密算法库都有支持)。K应当保持原样继续传递,C应当以64位的原始无符号的整形数值传递。
  3. 取H中有意义最后4位数的作为弥补,记为O。
  4. 取H当中的4位,从O字节MSB开始,丢弃最高有效位,将剩余位储存为(无符号)的32位整形数值I。
  5. 密码即为基数为10的最低N位数。如果结果位数少于N,从左边开始用0补全。

服务器与客户端都会计算密码,但是由服务器来检查客户端提供的密码是否匹配服务器本地生成的密码。考虑到轻微的时钟偏移、网络延迟或用户延误等情况,有些服务器允许接受本应该在早先已生成或稍后才生成的密码。

尽管参数协商只在最开始发生,后续不再能看到原始的参数,但依然存在和密码一样被钓鱼攻击的可能性。同时不限制登录尝试的TOTP实现容易被暴力破解,因此尝试次数限制必须非常少。

toml
[dependencies]  
hmac = "0.13.0-pre.4"  
sha1 = "0.11.0-pre.4"
rust
use hmac::{Hmac, KeyInit, Mac};  
use sha1::Sha1;  
use std::io::{Error, ErrorKind};  
use std::time::SystemTime;  
  
type HmacSha1 = Hmac<Sha1>;  
  
pub fn generate_totp_code(msg: &str, algorithm: &str) -> Result<String, Error> {  
    let msg = msg.as_bytes();  
    let duration_sec: u64 = 30;  
    let duration: u64 = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();  
    let secret: &[u8] = &(duration / duration_sec).to_be_bytes();  
    let mut hash_value: Vec<u8> = Vec::new();  
  
    // default use hmac-sha1 algorithm  
    match algorithm {  
        "hmac-sha-1" => {  
            hash_value = generate_hmac_sha1(msg, secret)  
        }  
        _ => {  
            hash_value = generate_hmac_sha1(msg, secret)  
        }  
    }  
    let res = convert_to_code(hash_value, 6);  
    match res {  
        Ok(code) => {  
            Ok(format!("{:06}", code))  
        }  
        Err(e) => {  
            Err(e)  
        }  
    }  
}  
  
fn generate_hmac_sha1(msg: &[u8], secret: &[u8]) -> Vec<u8> {  
    let mut mac = HmacSha1::new_from_slice(secret).expect("Invalid secret");  
    mac.update(msg);  
    mac.finalize().into_bytes().to_vec()  
}  
  
fn convert_to_code(v: Vec<u8>, mod_digit: usize) -> Result<u32, Error> {  
    if v.len() < 20 {  
        return Err(Error::new(ErrorKind::InvalidInput, format!("unexpected vec length[{}]", v.len())))  
    }  
    let offset: usize = (v[19] & 0x0f) as usize;  
    let p = &v[offset..offset+4];  
    let r: u32 = u32::from_be_bytes(p.try_into().unwrap());  
    Ok(r % (10u32.pow(mod_digit as u32)))  
}
rust
#[cfg(test)]  
mod tests {  
    use super::*;  
  
    #[test]  
    fn test_generate_hmac_sha1() {  
        let msg = "test".as_bytes();  
        let secret = "secret".as_bytes();  
       let res = generate_hmac_sha1(msg, secret);  
        print!("{:?}", res);  
    }  
  
    #[test]  
    fn test_convert_to_code() {  
        let msg = "test".as_bytes();  
        let secret = "secret".as_bytes();  
        let v = generate_hmac_sha1(msg, secret);  
        let result = convert_to_code(v, 6);  
        println!("{:?}", result.unwrap());  
    }  
  
    #[test]  
    fn test_generate_totp() {  
        let result = generate_totp_code("test", "hmac-sha-1");  
        println!("{:?}", result);  
    }  
}

推送通知验证 ​

微软的 Authenticator 还支持自家的推送通知验证方式,流程为: 用户登陆时, 页面提示一个数字。 需要打开Authenticator 软件, 此时软件会显示一组数字,从中选择页面显示的数字,点击确认批准即可。此方法需要借助于 Microsoft Entra ID 技术。

参考 ​