JavaScript动手实现2FA动态认证码

8 阅读5分钟

前言

好久没登陆Github了,发现Github需要经过2FA认证才能正常使用功能。Github推荐用可以用认证软件来扫描二维码来生成动态密码,或者利用secretKey专属密钥来生成动态密码。

我试了试用github推荐的软件,但是他们要么要付费,要么国内手机号无法注册,行不通呀根本行不通。

所以我就在百度里面搜免费2FA动态密码生成器,真的找到了好心人提供的生成器,输入动态密码后丝滑进入Github。

2FA认证是什么

2FA,2 Factor Authentication,双因子验证/双因素验证,是一种安全密码验证方式。区别于传统的密码验证,由于传统的密码验证是由一组静态信息组成,如:字符、图像、手势等,很容易被获取,相对不安全。2FA是基于时间、历史长度、实物(信用卡、SMS手机、令牌、指纹)等自然变量结合一定的加密算法组合出一组动态密码,一般每60秒刷新一次。不容易被获取和破解,相对安全。 ————以上来自百度百科

在Github这里,2FA采用的双因子一般是时间和个人密钥。Github会根据时间和账号的密钥比对我们填写的动态密码的一致性,一致才能通过验证。

PS

  1. Github中动态密码长度为6位;
  2. Github中动态密码的有效期为30s,也就是说一次性密码每半分钟会变化。

TOTP和HOTP

TOTP(Time-Based One-Time Password Algorithm) 是基于时间的一次性密码算法,可以根据时间因子和密钥生成一次性密码。TOTP算法是基于HOTP算法的,HOTP算法是根据计数器(移动因子)和密钥生成一次性密码。

HOTP

算法公式:HOTP(K,C) = Trancate(HMAC-SHA-1(K,C))

  • K:密钥,两端之间共享,不同的用户密钥应该保证唯一且不同
  • C:计数器(移动因子),是一个8字节的值
  • Digit:一次性密码的位数

HOTP算法是根据计数器(移动因子)和密钥生成一次性密码。

在经过 HMAC-SHA-1算法用 密钥K 加密 计数器C 后,我们会得到20个字节的十六进制字符串。

Trancate算法会对加密得到的十六进制字符串进行处理。首先会选取最后1个字节的16进制串,将其转化为十进制数字offset; 然后会从offset开始选取4个字节的字符串,将其转化为十进制数字;最后会根据 Digit位数 对十进制数字取模,获取最后几位数字,如果位数不够就在前方补0。

image.png

TOTP

算法公式:TOTP(K,T) = HOTP(K,(T-T0)/X)

  • K:密钥,两端之间共享,不同的用户密钥应该保证唯一且不同
  • T:当前时间戳(以秒为单位)
  • T0: 初始时间戳,一般为0
  • X:时间步长,一般为30s、60s,这里是30s
  • Digit:一次性密码的位数

从TOTP算法公式可以看到,我们只需要获取时间因子,将时间因子替换HOTP中的计数器即可。

JavaScript实现代码

第一步:密钥解码

Github 2FA认证提供的是经过base32(RFC3548)编码后的secretKey,所以在这里我们需要对其进行解码,解码的encoding类型选择RFC3548

因为Node crypto中的加密的参数需要传递binaryLike形式的值,所以解码的内容我们要用Buffer来进行接收。

const secret = 'Github 2FA认证提供的经过base32(RFC3548)编码后的secretKey'; // 这是你的秘钥,需要保密
const secretKey = Buffer.from(base32Decode(secret, 'RFC3548'));

第二步:实现HOTP

// 生成HMAC-based One-Time Password (HOTP)
function generateHOTP(key, counter, digits = 6) {
    // 将计数器转换为8字节的Buffer
    const counterBuffer = Buffer.alloc(8);
    counterBuffer.writeBigInt64BE(BigInt(counter), 0);

    // 使用HMAC-SHA1算法计算HMAC
    const hmac = crypto.createHmac('sha1', key).update(counterBuffer).digest();

    // 获取HMAC的最后一个字节的低四位作为偏移量
    const offset = hmac[hmac.length - 1] & 0x0F;

    // 将动态密码的部分转换为整数
    let dynamicPassword = (hmac[offset] & 0x7F)<< 24 
      | (hmac[offset + 1] & 0xFF) << 16
      | (hmac[offset + 2] & 0xFF) << 8
      | (hmac[offset + 3] & 0xFF);

    // 限制密码长度
    dynamicPassword = dynamicPassword % Math.pow(10, digits);

    // 根据指定的位数格式化密码
    return dynamicPassword.toString().padStart(digits, '0');
}

第三步:获取和处理时间戳

// 获取当前时间的时间戳,并以时间步长划分为计数器
function getCounter(timeStep = 30) {
    const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位:秒)
    return Math.floor(currentTime / timeStep);
}

第四步:得到TOTP动态密码

// 生成 TOTP
function generateTOTP(key, timeStep = 30, digits = 6) {
    const counter = getCounter(timeStep);
    console.log(counter)
    return generateHOTP(key, counter, digits);
}

全部代码

const crypto = require('crypto');
const base32Decode = require('base32-decode')
// 生成HMAC-based One-Time Password (HOTP)
function generateHOTP(key, counter, digits = 6) {
    // 将计数器转换为8字节的Buffer
    const counterBuffer = Buffer.alloc(8);
    counterBuffer.writeBigInt64BE(BigInt(counter), 0);

    // 使用HMAC-SHA1算法计算HMAC
    const hmac = crypto.createHmac('sha1', key).update(counterBuffer).digest();

    // 获取HMAC的最后一个字节的低四位作为偏移量
    const offset = hmac[hmac.length - 1] & 0x0F;

    // 将动态密码的部分转换为整数
    let dynamicPassword = (hmac[offset] & 0x7F)<< 24 
      | (hmac[offset + 1] & 0xFF) << 16
      | (hmac[offset + 2] & 0xFF) << 8
      | (hmac[offset + 3] & 0xFF);

    // 限制密码长度
    dynamicPassword = dynamicPassword % Math.pow(10, digits);

    // 根据指定的位数格式化密码
    return dynamicPassword.toString().padStart(digits, '0');
}

// 获取当前时间的时间戳,并以时间步长划分为计数器
function getCounter(timeStep = 30) {
    const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位:秒)
    return Math.floor(currentTime / timeStep);
}

// 生成 TOTP
function generateTOTP(key, timeStep = 30, digits = 6) {
    const counter = getCounter(timeStep);
    console.log(counter)
    return generateHOTP(key, counter, digits);
}

// 示例
const secret = 'Github 2FA认证提供的经过base32(RFC3548)编码后的secretKey'; // 这是你的秘钥,需要保密
const secretKey = Buffer.from(base32Decode(secret, 'RFC3548'));
const totp = generateTOTP(secretKey);
console.log('Generated TOTP:', totp);

参考文档

  1. blog.csdn.net/weixin_4279…
  2. 2FA百度百科