找回密码
 注册
搜索
[新手上路]批处理新手入门导读[视频教程]批处理基础视频教程[视频教程]VBS基础视频教程[批处理精品]批处理版照片整理器
[批处理精品]纯批处理备份&还原驱动[批处理精品]CMD命令50条不能说的秘密[在线下载]第三方命令行工具[在线帮助]VBScript / JScript 在线参考
查看: 104|回复: 4

[原创代码] 双因素验证 TOTP 计算器(JScript)

[复制链接]
发表于 16 小时前 | 显示全部楼层 |阅读模式
TOTP.js — 纯 Windows 终端双因素验证码生成器

一个用 JScript 编写的 TOTP(基于时间的一次性密码)验证码生成器,无需安装任何额外软件,在 cmd 终端中实时显示所有账号的 6 位验证码。

特性

  • 零依赖 — SHA-1、HMAC-SHA1、Base32 解码全部用 JScript 手写实现,不调用 PowerShell、Python 或任何外部工具
  • ANSI 彩色界面 — 验证码颜色随剩余时间动态变化:绿色 → 黄色 → 红色
  • 原地刷新无闪烁 — 使用 ANSI 光标控制序列原地重绘,每秒平滑刷新
  • 纯文本模式 — --plain 选项适配不支持 ANSI 的终端,每 15 秒输出一次纯文本快照
  • 标准 otpauth URI — 直接兼容 Google Authenticator 导出格式
  • 进度条倒计时 — 彩色模式用 ANSI 背景色,纯文本模式用 ASCII 字符
  • 隐藏光标 — 彩色模式下隐藏光标,界面干净无闪烁


运行环境

Windows 7 及以上(需内置 cscript.exe,所有 Windows 版本自带),cmd.exe 或 Windows Terminal。

使用方法

1. 将你的 TOTP 密钥保存为 TOTP.txt,放在与 TOTP.js 同目录下
2. 打开 cmd 运行 cscript //nologo TOTP.js
3. 按 Ctrl+C 退出

Win10修改注册表来支持颜色控制符(感谢cutebe):
  1. reg add "HKCU\Console" /v "VirtualTerminalLevel" /t REG_DWORD /d 1 /f
复制代码


纯文本模式

如果终端不支持 ANSI 控制序列(颜色/光标定位),加 --plain 参数:

  1. cscript //nologo TOTP.js --plain
复制代码


纯文本模式下:无颜色、无光标控制、不含任何 ANSI 转义序列,每 15 秒输出一次完整的验证码快照(不清屏,历史输出自然向下滚动),进度条用 ASCII 字符 # 和 - 绘制。

TOTP.txt 格式

每行一个 otpauth URI(Google Authenticator 标准导出格式),以 # 开头的行视为注释跳过:

  1. otpauth://totp/GitHub:username?secret=JBSWY3DPEHPK3PXP&issuer=GitHub
  2. otpauth://totp/Google:user@gmail.com?secret=JBSWY3DPEHPK3PXP&issuer=Google
  3. # 这是一行注释,会被跳过
  4. otpauth://totp/Microsoft:user@outlook.com?secret=JBSWY3DPEHPK3PXP&issuer=Microsoft
复制代码


支持的参数(均为可选,缺省使用标准值):

  1. secret    Base32 编码的密钥(必填)    无默认值
  2. issuer    显示名称                      从 label 提取
  3. digits    验证码位数                     6
  4. period    时间步长(秒)                  30
  5. algorithm 哈希算法(仅 SHA1)            SHA1
复制代码


界面说明

彩色模式(默认):

  1.   TOTP Authenticator                              02:16:51
  2.   ==========================================================

  3.   GitHub                   086483  ████████░░░░░░░░░░░░  9s
  4.   JetBrains Account        524254  ████████░░░░░░░░░░░░  9s
  5.   Google                   512861  ████████░░░░░░░░░░░░  9s

  6.   ----------------------------------------------------------
  7.   Refresh 1s | Ctrl+C to exit
复制代码


纯文本模式(--plain):

  1.   TOTP Authenticator                              02:16:51
  2.   ==========================================================

  3.   GitHub                   086483  [########------------] 9s
  4.   JetBrains Account        524254  [########------------] 9s
  5.   Google                   512861  [########------------] 9s

  6.   ----------------------------------------------------------
  7.   Refresh 15s | Ctrl+C to exit
复制代码


  • 账号名左对齐 24 字符宽
  • 验证码 6 位数字
  • 进度条 20 格,填充部分表示已用时间
  • 剩余秒数右对齐


技术实现

整个项目用纯 JScript(Windows Script Host)实现,不依赖任何外部库或工具:

  • Base32 解码 — 按 RFC 4648 标准将密钥字符串转为字节数组
  • SHA-1 — 完整实现 RFC 3174 规范,含消息填充和 80 轮压缩
  • HMAC-SHA1 — 按 RFC 2104 实现,ipad/opad 异或 + 双哈希
  • TOTP — 按 RFC 6238 实现,动态截断取 6 位数字
  • ANSI 渲染 — ESC 序列控制光标定位、行清除、前景/背景色(彩色模式)
  • 纯文本降级 — --plain 模式下完全不含 ANSI 转义,每 15 秒输出纯文本快照 + ASCII 进度条


限制

  • 仅支持 SHA-1 算法(覆盖 99% 的 TOTP 服务)
  • 彩色模式需在 cmd.exe 或 Windows Terminal 中直接运行,通过管道重定向会看到原始转义码
  • 不支持二维码扫描,需手动准备 TOTP.txt


源码

TOTP.js(主程序)

  1. // TOTP.js - ANSI TOTP Authenticator
  2. // Run: cscript //nologo TOTP.js [path\to\totp.txt] [--plain]
  3. // --plain: non-color mode, 15s interval, output snapshots without clearing screen (for ANSI-unsupported terminals)
  4. // Ctrl+C to exit

  5. var ESC = String.fromCharCode(27);

  6. (function() {
  7.     var fso = new ActiveXObject("Scripting.FileSystemObject");
  8.     var args = WScript.Arguments;
  9.     var scriptDir = fso.GetParentFolderName(WScript.ScriptFullName);
  10.     var txtPath = null;
  11.     var plain = false;

  12.     for (var i = 0; i < args.length; i++) {
  13.         if (args(i).toLowerCase() === "--plain") {
  14.             plain = true;
  15.         } else if (!txtPath) {
  16.             txtPath = args(i);
  17.         }
  18.     }
  19.     if (!txtPath) txtPath = fso.BuildPath(scriptDir, "TOTP.txt");

  20.     if (!fso.FileExists(txtPath)) {
  21.         WScript.Echo("Error: " + txtPath + " not found");
  22.         WScript.Quit(1);
  23.     }

  24.     var entries = parseFile(fso, txtPath);
  25.     if (entries.length === 0) {
  26.         WScript.Echo("Error: No valid entries in " + txtPath);
  27.         WScript.Quit(1);
  28.     }

  29.     var shell = plain ? new ActiveXObject("WScript.Shell") : null;
  30.     var refreshMs = plain ? 15000 : 1000;

  31.     if (!plain) {
  32.         WScript.StdOut.Write(ESC + "[?25l"); // hide cursor
  33.     }

  34.     try {
  35.         var firstDraw = true;
  36.         while (true) {
  37.             if (plain) {
  38.             }
  39.             WScript.StdOut.Write(render(entries, firstDraw, plain));
  40.             firstDraw = false;
  41.             WScript.Sleep(refreshMs);
  42.         }
  43.     } finally {
  44.         if (!plain) {
  45.             WScript.StdOut.Write(ESC + "[?25h" + ESC + "[0m\r\n"); // show cursor + reset
  46.         }
  47.     }
  48. })();

  49. function parseFile(fso, path) {
  50.     var file = fso.OpenTextFile(path, 1, false, 0);
  51.     var content = file.ReadAll();
  52.     file.Close();

  53.     var lines = content.split(/\r?\n/);
  54.     var entries = [];

  55.     for (var i = 0; i < lines.length; i++) {
  56.         var line = lines[i].replace(/^\s+|\s+$/g, "");
  57.         if (!line || line.charAt(0) === "#") continue;
  58.         if (line.indexOf("otpauth://") !== 0) continue;

  59.         var secret = extractParam(line, "secret");
  60.         if (!secret) continue;

  61.         var issuer = extractParam(line, "issuer");
  62.         var label = extractLabel(line);
  63.         var digits = parseInt(extractParam(line, "digits") || "6", 10);
  64.         var period = parseInt(extractParam(line, "period") || "30", 10);

  65.         var name = urlDecode(issuer ? issuer : (label ? label : "Unknown"));

  66.         entries.push({ name: name, secret: secret, digits: digits, period: period });
  67.     }
  68.     return entries;
  69. }

  70. function render(entries, firstDraw, plain) {
  71.     var now = Math.floor(new Date().getTime() / 1000);
  72.     var d = new Date();
  73.     var timeStr = pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
  74.     var W = 58;

  75.     var out;
  76.     if (plain) {
  77.         out = "";
  78.     } else {
  79.         out = firstDraw ? (ESC + "[2J" + ESC + "[H") : (ESC + "[H");
  80.     }

  81.     // Header
  82.     out += nl(plain);
  83.     if (plain) {
  84.         out += "  TOTP Authenticator" + spaces(W - 20 - 8) + timeStr + "\r\n";
  85.         out += "  " + repeat("=", W) + "\r\n";
  86.     } else {
  87.         out += ESC + "[2K  " + ESC + "[1;36m" + "TOTP Authenticator" + ESC + "[0m" +
  88.                spaces(W - 20 - 8) + ESC + "[90m" + timeStr + ESC + "[0m\r\n";
  89.         out += ESC + "[2K  " + ESC + "[90m" + repeat("=", W) + ESC + "[0m\r\n";
  90.     }
  91.     out += nl(plain);

  92.     // Entries
  93.     for (var i = 0; i < entries.length; i++) {
  94.         var e = entries[i];
  95.         var remaining = e.period - (now % e.period);
  96.         var counter = Math.floor(now / e.period);
  97.         var code = computeTOTP(e.secret, counter, e.digits);

  98.         var barW = 20;
  99.         var elapsed = e.period - remaining;
  100.         var filled = Math.floor(elapsed / e.period * barW);
  101.         if (filled > barW) filled = barW;

  102.         if (plain) {
  103.             var bar = "[";
  104.             for (var j = 0; j < barW; j++) bar += (j < filled) ? "#" : "-";
  105.             bar += "]";

  106.             out += "  " + padRight(e.name, 24) + " " + code + " " + bar + " " +
  107.                    padLeft(String(remaining), 2) + "s\r\n";
  108.         } else {
  109.             var codeSgr, barSgr;
  110.             if (remaining <= 5)       { codeSgr = "1;31"; barSgr = "41"; }
  111.             else if (remaining <= 10) { codeSgr = "1;33"; barSgr = "43"; }
  112.             else                      { codeSgr = "1;32"; barSgr = "42"; }

  113.             var bar2 = ESC + "[" + barSgr + "m" + spaces(filled) + ESC + "[0m" +
  114.                        ESC + "[100m" + spaces(barW - filled) + ESC + "[0m";

  115.             out += ESC + "[2K";
  116.             out += "  " + ESC + "[97m" + padRight(e.name, 24) + ESC + "[0m";
  117.             out += " " + ESC + "[" + codeSgr + "m" + code + ESC + "[0m";
  118.             out += " " + bar2;
  119.             out += " " + ESC + "[90m" + padLeft(String(remaining), 2) + "s" + ESC + "[0m";
  120.             out += "\r\n";
  121.         }
  122.     }

  123.     // Footer
  124.     out += nl(plain);
  125.     if (plain) {
  126.         out += "  " + repeat("-", W) + "\r\n";
  127.         out += "  Refresh 15s | Ctrl+C to exit\r\n";
  128.     } else {
  129.         out += ESC + "[2K  " + ESC + "[90m" + repeat("-", W) + ESC + "[0m\r\n";
  130.         out += ESC + "[2K  " + ESC + "[90m" + "Refresh 1s | Ctrl+C to exit" + ESC + "[0m\r\n";
  131.     }

  132.     return out;
  133. }

  134. // ===== plain-mode newline helper =====
  135. function nl(plain) {
  136.     return plain ? "\r\n" : (ESC + "[2K\r\n");
  137. }

  138. // ===== URI / string helpers =====
  139. function extractParam(uri, param) {
  140.     var re = new RegExp("[?&]" + param + "=([^&]*)");
  141.     var m = uri.match(re);
  142.     return m ? m[1] : "";
  143. }
  144. function extractLabel(uri) {
  145.     var m = uri.match(/otpauth:\/\/totp\/([^?]*)/);
  146.     return m ? m[1] : "";
  147. }
  148. function urlDecode(str) {
  149.     str = str.replace(/\+/g, " ");
  150.     return str.replace(/%([0-9A-Fa-f]{2})/g, function(_, hex) {
  151.         var c = parseInt(hex, 16);
  152.         return (c < 128) ? String.fromCharCode(c) : ("%" + hex);
  153.     });
  154. }
  155. function pad2(n) { return (n < 10 ? "0" : "") + n; }
  156. function padRight(s, n) {
  157.     s = String(s);
  158.     while (s.length < n) s += " ";
  159.     return s.length > n ? s.substr(0, n) : s;
  160. }
  161. function padLeft(s, n) {
  162.     while (s.length < n) s = " " + s;
  163.     return s;
  164. }
  165. function spaces(n) { var s = ""; while (n-- > 0) s += " "; return s; }
  166. function repeat(s, n) { var r = ""; while (n-- > 0) r += s; return r; }

  167. // ===== Base32 decode =====
  168. function base32decode(str) {
  169.     var alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  170.     str = str.toUpperCase().replace(/=/g, "").replace(/\s/g, "");
  171.     var bits = "";
  172.     for (var i = 0; i < str.length; i++) {
  173.         var v = alpha.indexOf(str.charAt(i));
  174.         if (v < 0) continue;
  175.         var b = v.toString(2);
  176.         while (b.length < 5) b = "0" + b;
  177.         bits += b;
  178.     }
  179.     var bytes = [];
  180.     for (var i = 0; i + 8 <= bits.length; i += 8) bytes.push(parseInt(bits.substr(i, 8), 2));
  181.     return bytes;
  182. }

  183. // ===== SHA-1 =====
  184. function rotl(x, n) { return (x << n) | (x >>> (32 - n)); }
  185. function sha1(data) {
  186.     var h0 = 0x67452301, h1 = 0xEFCDAB89, h2 = 0x98BADCFE, h3 = 0x10325476, h4 = 0xC3D2E1F0;
  187.     var len = data.length, bitLen = len * 8;
  188.     data.push(0x80);
  189.     while (data.length % 64 !== 56) data.push(0);
  190.     data.push(0, 0, 0, 0, (bitLen >>> 24) & 0xFF, (bitLen >>> 16) & 0xFF, (bitLen >>> 8) & 0xFF, bitLen & 0xFF);
  191.     for (var i = 0; i < data.length; i += 64) {
  192.         var w = new Array(80);
  193.         for (var j = 0; j < 16; j++)
  194.             w[j] = (data[i + j * 4] << 24) | (data[i + j * 4 + 1] << 16) | (data[i + j * 4 + 2] << 8) | data[i + j * 4 + 3];
  195.         for (var j = 16; j < 80; j++)
  196.             w[j] = rotl(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
  197.         var a = h0, b = h1, c = h2, d = h3, e = h4;
  198.         for (var j = 0; j < 80; j++) {
  199.             var f, k;
  200.             if (j < 20)       { f = (b & c) | ((~b) & d);          k = 0x5A827999; }
  201.             else if (j < 40)  { f = b ^ c ^ d;                     k = 0x6ED9EBA1; }
  202.             else if (j < 60)  { f = (b & c) | (b & d) | (c & d);   k = 0x8F1BBCDC; }
  203.             else              { f = b ^ c ^ d;                     k = 0xCA62C1D6; }
  204.             var t = (rotl(a, 5) + f + e + k + w[j]) | 0;
  205.             e = d; d = c; c = rotl(b, 30); b = a; a = t;
  206.         }
  207.         h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0;
  208.     }
  209.     return [
  210.         (h0 >>> 24) & 0xFF, (h0 >>> 16) & 0xFF, (h0 >>> 8) & 0xFF, h0 & 0xFF,
  211.         (h1 >>> 24) & 0xFF, (h1 >>> 16) & 0xFF, (h1 >>> 8) & 0xFF, h1 & 0xFF,
  212.         (h2 >>> 24) & 0xFF, (h2 >>> 16) & 0xFF, (h2 >>> 8) & 0xFF, h2 & 0xFF,
  213.         (h3 >>> 24) & 0xFF, (h3 >>> 16) & 0xFF, (h3 >>> 8) & 0xFF, h3 & 0xFF,
  214.         (h4 >>> 24) & 0xFF, (h4 >>> 16) & 0xFF, (h4 >>> 8) & 0xFF, h4 & 0xFF
  215.     ];
  216. }

  217. // ===== HMAC-SHA1 =====
  218. function hmacSha1(key, msg) {
  219.     var bs = 64;
  220.     if (key.length > bs) key = sha1(key.slice());
  221.     var pk = key.slice();
  222.     while (pk.length < bs) pk.push(0);
  223.     var ipad = [], opad = [];
  224.     for (var i = 0; i < bs; i++) { ipad.push(pk[i] ^ 0x36); opad.push(pk[i] ^ 0x5C); }
  225.     return sha1(opad.concat(sha1(ipad.concat(msg))));
  226. }

  227. // ===== TOTP =====
  228. function computeTOTP(secret, counter, digits) {
  229.     var key = base32decode(secret);
  230.     var msg = [];
  231.     for (var i = 7; i >= 0; i--) {
  232.         if (i >= 4) msg.push(0);
  233.         else msg.push((counter >>> (i * 8)) & 0xFF);
  234.     }
  235.     var h = hmacSha1(key, msg);
  236.     var off = h[h.length - 1] & 0x0F;
  237.     var code = ((h[off] & 0x7F) << 24) | ((h[off + 1] & 0xFF) << 16) | ((h[off + 2] & 0xFF) << 8) | (h[off + 3] & 0xFF);
  238.     code %= Math.pow(10, digits);
  239.     var s = code.toString();
  240.     while (s.length < digits) s = "0" + s;
  241.     return s;
  242. }

复制代码
发表于 10 小时前 | 显示全部楼层
Win11测试报错信息:TOTP.js(128, 50) Microsoft JScript 编译错误: 正则表达式中缺少 ')'
AI说:这个正则表达式的问题在于捕获组中的 <sup>?</sup> 部分是无效的正则语法。看起来这可能是 HTML 标签被错误地混入了正则表达式中。
发表于 7 小时前 | 显示全部楼层

可能需要修改一下

Win10修改注册表来支持颜色控制符:
  1. reg add "HKCU\Console" /v "VirtualTerminalLevel" /t REG_DWORD /d 1 /f
复制代码


两个函数(可能方案)修改:
  1. function extractParam(uri, param) {
  2.         var re = new RegExp("[?&]" + param + "=([^&]*)");
  3.         var m = uri.match(re);
  4.         return m ? m[1] : "";
  5. }        //uri=otpauth://totp/GitHub:username?secret=JBSWY3DPEHPK3PXP&issuer=GitHub
  6. function extractLabel(uri) {
  7.         var m = uri.match(/otpauth:\/\/totp\/([^?]*)/);
  8.         return m ? m[1] : "";
  9. }
复制代码
 楼主| 发表于 2 小时前 | 显示全部楼层
ShowCode 发表于 2026-8-20 08:25
Win11测试报错信息:TOTP.js(128, 50) Microsoft JScript 编译错误: 正则表达式中缺少 ')'
AI说:这个正则 ...

已更新,我让ai写一个bbcode版本的介绍,并且把代码附在最后
心想这么简单的需求应该不会错吧,就没把代码复制出来测试
结果还是出问题了,晕
 楼主| 发表于 2 小时前 | 显示全部楼层
cutebe 发表于 2026-8-20 11:23
Win10修改注册表来支持颜色控制符:


感谢纠正。文章已更新
新加了 --plain 参数去掉花里胡哨的(关闭ansi控制字符+彩显,15s刷新一次)
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|批处理之家 ( 渝ICP备10000708号 )

GMT+8, 2026-8-20 19:21

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表