平台工程

GitHub 强制 2FA 的小白处置 · TOTP 原理与离线生成器

入档:2026-06-29 来源:帮一位非技术作者处理 GitHub 弹出的「Enable 2FA / 7 天内必须开启」拦截页;约束=国内网络、Chrome、手机没装任何验证器 App、不便用外网工具 状态:全流程跑通并验证(2FA 成功启用 + 官方恢复码与自存副本逐字一致 + 离线生成器经 Python/Node 同时间戳交叉验证算码一致)

一句话总结

GitHub 弹的「Enable two-factor authentication」不是收费,是强制两步验证(对上传过代码的账号强制,免费)。开启它本质只需要一个能持续产出 6 位码的 TOTP 生成器——而 TOTP 是纯本地算法(密钥 + 时间,HMAC-SHA1),根本不连任何服务器,所以「国内 / 不能用外网」对它毫无影响。手机没 App、商店进不去时,二维码旁的 setup key 就是 TOTP 密钥,可以直接喂给任何标准验证器,或做成一个纯离线 HTML 网页生成器(双击即出码),并务必把恢复码单独留底。

破除两个常见误解(都是卡住小白的根因)

  1. 「GitHub 要收费」 ❌ —— 那是强制 2FA 的安全要求,不是付费墙。
  2. 「国内用不了验证器,因为要连外网」 ❌ —— 验证器(Authenticator App / 浏览器扩展 / 本地脚本)算码是 离线 的:它只用「密钥 + 当前时间」在本地算,不上传、不连外国服务器。卡国内用户的从来不是算码本身,而是**「怎么把那个外国 App / 商店装上」**。绕过”装”这一关,问题就没了。

为什么这么选(决策链)

非技术 + 国内 + Chrome + 没手机 App + 不便外网,三条路逐一权衡:

迁移判据:当”装标准验证器”这一步被环境卡死(无 App / 商店或扩展弹窗不可用),而你已能拿到 TOTP 密钥时,离线 HTML 生成器是兜底正解。能正常装 App / 扩展时,优先用标准工具(更”正规”、跨站通用)。

可照做(完整步骤)

  1. 拿到密钥:在 2FA 扫码页点「setup key / 设置密钥」,它显示的 base32 串(形如 XXXXXXXXXXXXXXXX)就是 TOTP 密钥。二维码内容 = otpauth://totp/...&secret=该密钥,两者等价。
  2. 本地验证密钥(可选但推荐,确认没抄错):用 Python 现算一个码,和后面工具出的码对比。
    import hmac, hashlib, struct, time, base64
    key = base64.b32decode("你的密钥".replace(" ", ""))
    c = struct.pack(">Q", int(time.time()) // 30)
    h = hmac.new(key, c, hashlib.sha1).digest(); o = h[19] & 0xf
    n = struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff
    print(str(n % 1000000).zfill(6))   # 当前 6 位码
  3. 做离线 HTML 生成器:把下面模板里 SECRET 换成密钥,存成桌面 GitHub验证码.html,双击即用(纯本地、不联网)。核心是浏览器 crypto.subtle 做 HMAC-SHA1:
    <script>
    const SECRET = "你的密钥";   // base32
    function b32d(s){const A="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";let b="";
      for(const c of s.toUpperCase()){const v=A.indexOf(c);if(v<0)continue;b+=v.toString(2).padStart(5,"0");}
      const o=[];for(let i=0;i+8<=b.length;i+=8)o.push(parseInt(b.substr(i,8),2));return new Uint8Array(o);}
    let key;async function code(){
      key=key||await crypto.subtle.importKey("raw",b32d(SECRET),{name:"HMAC",hash:"SHA-1"},false,["sign"]);
      const ctr=Math.floor(Date.now()/1000/30),buf=new ArrayBuffer(8),dv=new DataView(buf);
      dv.setUint32(4,ctr>>>0);const sig=new Uint8Array(await crypto.subtle.sign("HMAC",key,buf));
      const off=sig[19]&0xf,n=((sig[off]&0x7f)<<24)|(sig[off+1]<<16)|(sig[off+2]<<8)|sig[off+3];
      return String(n%1000000).padStart(6,"0");}
    setInterval(async()=>document.title=await code(),1000);
    </script>
    登录要码时双击这个文件,把当前数字填进去即可(每 30 秒一变)。
  4. 填码完成设置:把当前 6 位码填进「Verify the code」→ Continue。码窗口 30 秒,留够余量(剩 <15 秒就等下一窗再填);GitHub 一般也接受相邻窗口。
  5. 恢复码必须双留底:Continue 后 GitHub 给 16 个恢复码——点官方「Download」存一份(github-recovery-codes.txt,零抄写风险),再单独抄/存一份(纸 + 与电脑分离)。手机/生成器都失效时,恢复码是唯一入口(每个用一次)。点「I have saved my recovery codes」收尾。

关键设计点 / 踩坑(都会咬人)

边界

关联文档

类型/平台工程主题/账号安全主题/2FA