查看: 46|回复: 0

[网站源码] 纯网页版TOTP验证码生成器

[复制链接]
累计签到:377 天
连续签到:1 天

1297

主题

-17

回帖

1万

积分

域主

名望
126
星币
6580
星辰
15
好评
328
发表于 6 小时前 | 显示全部楼层 |阅读模式

注册登录后全站资源免费查看下载

您需要 登录 才可以下载或查看,没有账号?立即注册

×
纯前实现,密钥不离本地
实时30秒倒计时可视化展示无需注册,即开即用
以下是完整代码

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>TOTP 倒计时</title>
  7.     <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
  8.     <style>
  9.         body {
  10.             font-family: 'Arial', sans-serif;
  11.             max-width: 500px;
  12.             margin: 0 auto;
  13.             padding: 20px;
  14.             text-align: center;
  15.             background: #f5f5f5;
  16.         }
  17.         input {
  18.             padding: 12px;
  19.             width: 300px;
  20.             margin: 15px 0;
  21.             font-size: 16px;
  22.             border: 2px solid #ddd;
  23.             border-radius: 4px;
  24.         }
  25.         button {
  26.             padding: 12px 25px;
  27.             background: #4285f4;
  28.             color: white;
  29.             border: none;
  30.             border-radius: 4px;
  31.             font-size: 16px;
  32.             cursor: pointer;
  33.             transition: background 0.3s;
  34.         }
  35.         button:hover {
  36.             background: #3367d6;
  37.         }
  38.         .totp-display {
  39.             font-family: Arial, sans-serif;
  40.             font-weight: bold;
  41.             font-size: 48px;
  42.             margin: 20px 0;
  43.             letter-spacing: 5px;
  44.             transition: color 0.3s;
  45.         }
  46.         .totp-display.green {
  47.             color: #4CAF50;
  48.         }
  49.         .totp-display.blue {
  50.             color: #2196F3;
  51.         }
  52.         .totp-display.red {
  53.             color: #f44336;
  54.             animation: pulse 0.5s infinite alternate;
  55.         }
  56.         .countdown-container {
  57.             position: relative;
  58.             width: 120px;
  59.             height: 120px;
  60.             margin: 30px auto;
  61.         }
  62.         .countdown-circle {
  63.             width: 100%;
  64.             height: 100%;
  65.         }
  66.         .countdown-circle-bg {
  67.             fill: none;
  68.             stroke: #e0e0e0;
  69.             stroke-width: 10;
  70.         }
  71.         .countdown-circle-fg {
  72.             fill: none;
  73.             stroke: #4CAF50;
  74.             stroke-width: 10;
  75.             stroke-linecap: round;
  76.             transform: rotate(-90deg);
  77.             transform-origin: 50% 50%;
  78.             transition: all 0.1s linear;
  79.         }
  80.         .countdown-circle-fg.blue {
  81.             stroke: #2196F3;
  82.         }
  83.         .countdown-circle-fg.red {
  84.             stroke: #f44336;
  85.         }
  86.         .countdown-text {
  87.             position: absolute;
  88.             top: 50%;
  89.             left: 50%;
  90.             transform: translate(-50%, -50%);
  91.             font-size: 30px;
  92.             font-weight: bold;
  93.             color: #333;
  94.         }
  95.         @keyframes pulse {
  96.             from { opacity: 1; }
  97.             to { opacity: 0.5; }
  98.         }
  99.     </style>
  100. </head>
  101. <body>
  102.     <h1>TOTP 验证码生成器</h1>
  103.     <p>请输入 Base32 密钥:</p>
  104.     <input type="text" id="secret" placeholder="例如:JBSWY3DPEHPK3PXP" />
  105.     <button>生成动态验证码</button>
  106.        <div class="totp-display" id="result">000000</div>
  107.        <div class="countdown-container">
  108.         <svg class="countdown-circle" viewBox="0 0 100 100">
  109.             <circle class="countdown-circle-bg" cx="50" cy="50" r="45"/>
  110.             <circle class="countdown-circle-fg" id="countdown-circle" cx="50" cy="50" r="45"/>
  111.         </svg>
  112.         <div class="countdown-text" id="countdown">30</div>
  113.     </div>
  114.     <script>
  115.         // Base32 解码
  116.         function base32Decode(base32) {
  117.             const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  118.             base32 = base32.replace(/[^A-Z2-7]/gi, '').toUpperCase();
  119.             let bits = 0, value = 0, output = [];
  120.             for (let i = 0; i < base32.length; i++) {
  121.                 const char = base32.charAt(i);
  122.                 const index = alphabet.indexOf(char);
  123.                 if (index === -1) continue;
  124.                 value = (value << 5) | index;
  125.                 bits += 5;
  126.                 if (bits >= 8) {
  127.                     bits -= 8;
  128.                     output.push((value >>> bits) & 0xFF);
  129.                 }
  130.             }
  131.             return output;
  132.         }
  133.         // 计算 HMAC-SHA1
  134.         function hmacSHA1Bytes(keyBytes, messageBytes) {
  135.             const key = CryptoJS.lib.WordArray.create(keyBytes);
  136.             const message = CryptoJS.lib.WordArray.create(messageBytes);
  137.             const hmac = CryptoJS.HmacSHA1(message, key);
  138.             return hmac.toString(CryptoJS.enc.Hex)
  139.                       .match(/.{1,2}/g)
  140.                       .map(byte => parseInt(byte, 16));
  141.         }
  142.         // 动态截断
  143.         function dynamicTruncation(hmacBytes) {
  144.             const offset = hmacBytes[hmacBytes.length - 1] & 0x0F;
  145.             return (
  146.                 ((hmacBytes[offset]     & 0x7F) << 24) |
  147.                 ((hmacBytes[offset + 1] & 0xFF) << 16) |
  148.                 ((hmacBytes[offset + 2] & 0xFF) <<  8) |
  149.                  (hmacBytes[offset + 3] & 0xFF)
  150.             );
  151.         }
  152.         // 计算 TOTP
  153.         function calculateTOTP(secret) {
  154.             try {
  155.                 const keyBytes = base32Decode(secret);
  156.                 if (keyBytes.length === 0) throw new Error("无效的 Base32 密钥");
  157.                 const timeStep = 30;
  158.                 const timestamp = Math.floor(Date.now() / 1000);
  159.                 const counter = Math.floor(timestamp / timeStep);
  160.                 const counterBytes = new Array(8).fill(0);
  161.                 for (let i = 0; i < 8; i++) {
  162.                     counterBytes[7 - i] = (counter >>> (i * 8)) & 0xFF;
  163.                 }
  164.                 const hmacBytes = hmacSHA1Bytes(keyBytes, counterBytes);
  165.                 const binary = dynamicTruncation(hmacBytes);
  166.                 return (binary % 1000000).toString().padStart(6, '0');
  167.             } catch (e) {
  168.                 return `错误: ${e.message}`;
  169.             }
  170.         }
  171.         // 更新倒计时和 TOTP
  172.         function updateTOTPAndCountdown() {
  173.             const secret = document.getElementById('secret').value.trim();
  174.             if (!secret) return;
  175.             const timestamp = Math.floor(Date.now() / 1000);
  176.             const elapsed = timestamp % 30;
  177.             const remainingSeconds = 30 - elapsed;
  178.             const progress = elapsed / 30;
  179.            // 获取亓素
  180.             const circle = document.getElementById('countdown-circle');
  181.             const totpDisplay = document.getElementById('result');
  182.            
  183.             // 先移除所有颜色类
  184.             circle.classList.remove('blue', 'red');
  185.             totpDisplay.classList.remove('green', 'blue', 'red');
  186.             
  187.             // 根据剩余时间设置不同颜色和效果
  188.             if (remainingSeconds > 20) {
  189.                 // 30-21秒:绿色
  190.                 circle.style.stroke = '#4CAF50';
  191.                 totpDisplay.classList.add('green');
  192.             } else if (remainingSeconds > 5) {
  193.                 // 20-6秒:蓝色
  194.                 circle.style.stroke = '#2196F3';
  195.                 circle.classList.add('blue');
  196.                 totpDisplay.classList.add('blue');
  197.             } else {
  198.                 // 5-0秒:红色闪烁
  199.                 circle.style.stroke = '#f44336';
  200.                 circle.classList.add('red');
  201.                 totpDisplay.classList.add('red');
  202.             }
  203.             
  204.             // 更新圆圈进度(逆时针减少)
  205.             const circumference = 2 * Math.PI * 45;
  206.             circle.style.strokeDasharray = circumference;
  207.             circle.style.strokeDashoffset = circumference * progress;
  208.             
  209.             // 更新倒计时数字
  210.             document.getElementById('countdown').textContent = remainingSeconds;
  211.             
  212.             // 更新 TOTP
  213.             document.getElementById('result').textContent = calculateTOTP(secret);

  214.             setTimeout(updateTOTPAndCountdown, 1000);
  215.         }

  216.         // 启动 TOTP 计算
  217.         function startTOTP() {
  218.             const secret = document.getElementById('secret').value.trim();
  219.             if (!secret) {
  220.                 alert("请输入 Base32 密钥!");
  221.                 return;
  222.             }
  223.             
  224.             // 初始化圆圈和TOTP显示
  225.             const circle = document.getElementById('countdown-circle');
  226.             const totpDisplay = document.getElementById('result');
  227.             const circumference = 2 * Math.PI * 45;
  228.             
  229.             circle.style.strokeDasharray = circumference;
  230.             circle.style.strokeDashoffset = 0;
  231.             circle.classList.remove('blue', 'red');
  232.             circle.style.stroke = '#4CAF50';
  233.             
  234.             totpDisplay.classList.remove('blue', 'red');
  235.             totpDisplay.classList.add('green');
  236.             
  237.             updateTOTPAndCountdown();
  238.         }
  239.     </script>
  240. </body>
  241. </html>
复制代码
优化代码
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>TOTP倒计时生成器</title>
  7.     <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
  8.     <style>
  9.         body {
  10.             font-family: 'Arial', sans-serif;
  11.             display: flex;
  12.             max-width: 1200px; /* 增加最大宽度 */
  13.             margin: 0 auto;
  14.             background: #f5f5f5;
  15.         }
  16.         #key-container {
  17.             width: 30%;
  18.             padding: 20px;
  19.             border-right: 1px solid #ddd; /* 右侧边框 */
  20.             background-color: white;
  21.             height: 100vh; /* 设置高度与视口相同 */
  22.             overflow-y: auto; /* 如果内容超出则出现滚动条 */
  23.         }
  24.         #key-container h2 {
  25.             font-size: 20px;
  26.             margin: 0 0 15px;
  27.         }
  28.         #key-list {
  29.             list-style: none;
  30.             padding: 0;
  31.         }
  32.         #key-list li {
  33.             padding: 10px;
  34.             border: 1px solid #ddd;
  35.             margin-bottom: 5px;
  36.             border-radius: 4px;
  37.             position: relative;
  38.         }
  39.         #main-container {
  40.             width: 70%; /* 右侧主体内容宽度 */
  41.             padding: 20px;
  42.             display: flex;
  43.             flex-direction: column;
  44.             align-items: center; /* 水平居中 */
  45.         }
  46.         input {
  47.             padding: 12px;
  48.             width: 240px;
  49.             margin: 15px 0;
  50.             font-size: 16px;
  51.             border: 2px solid #ddd;
  52.             border-radius: 4px;
  53.             transition: border-color 0.3s;
  54.         }
  55.         input:focus {
  56.             border-color: #4285f4;
  57.             outline: none;
  58.         }
  59.         button {
  60.             padding: 12px 15px;
  61.             background: #4285f4;
  62.             color: white;
  63.             border: none;
  64.             border-radius: 4px;
  65.             font-size: 16px;
  66.             cursor: pointer;
  67.             transition: background 0.3s, box-shadow 0.3s;
  68.             margin-left: 10px;
  69.             box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  70.         }
  71.         button:hover {
  72.             background: #3367d6;
  73.             box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2);
  74.         }
  75.         button:active {
  76.             box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  77.             transform: translateY(2px);
  78.         }
  79.         .totp-display {
  80.             font-family: Arial, sans-serif;
  81.             font-weight: bold;
  82.             font-size: 48px;
  83.             margin: 20px 0;
  84.             letter-spacing: 5px;
  85.             transition: color 0.3s;
  86.         }
  87.         .countdown-container {
  88.             position: relative;
  89.             width: 120px;
  90.             height: 120px;
  91.             margin: 30px auto; /* 上下外边距自动 */
  92.             display: flex; /* 使用 Flexbox 进行居中 */
  93.             flex-direction: column; /* 纵列布局 */
  94.             align-items: center; /* 水平居中 */
  95.             justify-content: center; /* 垂直居中 */
  96.         }
  97.         .countdown-circle {
  98.             width: 100%;
  99.             height: 100%;
  100.         }
  101.         .countdown-circle-bg {
  102.             fill: none;
  103.             stroke: #e0e0e0;
  104.             stroke-width: 10;
  105.         }
  106.         .countdown-circle-fg {
  107.             fill: none;
  108.             stroke: #4CAF50;
  109.             stroke-width: 10;
  110.             stroke-linecap: round;
  111.             transform: rotate(-90deg);
  112.             transform-origin: 50% 50%;
  113.             transition: stroke 0.1s linear;
  114.         }
  115.         #countdown {
  116.             font-size: 24px;
  117.             position: absolute; /* 绝对定位在圆圈中/心 */
  118.             text-align: center;
  119.             width: 100%; /* 宽度占满 */
  120.             top: 50%; /* 垂直居中 */
  121.             left: 50%; /* 水平居中 */
  122.             transform: translate(-50%, -50%); /* 使其真正居中 */
  123.         }
  124.     </style>
  125. </head>
  126. <body>
  127.     <div id="key-container">
  128.         <h2>临时存放密钥列表</h2>
  129.         <ul id="key-list"></ul>
  130.         <button id="remove-selected">删除选中</button>
  131.     </div>

  132.     <div id="main-container">
  133.         <h1>TOTP 验证码生成器</h1>
  134.         <p>请输入 Base32 密钥:</p>
  135.          
  136.         <div style="display: flex; justify-content: center; align-items: center;">
  137.             <input type="text" id="secret" placeholder="例如:JBSWY3DPEHPK3PXP" />
  138.             <button id="generate">生成动态验证码</button>
  139.         </div>

  140.         <div style="margin: 10px 0; text-align: center;">
  141.             <button id="add-key">添加</button>
  142.             <input type="file" id="file-input" style="display:none;">
  143.             <button id="import">导入密钥</button>
  144.             <button id="export">导出选中</button>
  145.         </div>

  146.         <div class="totp-display" id="result">------</div>

  147.         <div class="countdown-container">
  148.             <svg class="countdown-circle" viewBox="0 0 100 100">
  149.                 <circle class="countdown-circle-bg" cx="50" cy="50" r="45"/>
  150.                 <circle class="countdown-circle-fg" id="countdown-circle" cx="50" cy="50" r="45"/>
  151.             </svg>
  152.             <div class="countdown-text" id="countdown">30</div>
  153.         </div>
  154.     </div>

  155.     <script>
  156.         let keys = [];

  157.         function base32Decode(base32) {
  158.             const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  159.             base32 = base32.replace(/[^A-Z2-7]/gi, '').toUpperCase();
  160.             let bits = 0, value = 0, output = [];
  161.             for (let i = 0; i < base32.length; i++) {
  162.                 const char = base32.charAt(i);
  163.                 const index = alphabet.indexOf(char);
  164.                 if (index === -1) continue;
  165.                 value = (value << 5) | index;
  166.                 bits += 5;
  167.                 if (bits >= 8) {
  168.                     bits -= 8;
  169.                     output.push((value >>> bits) & 0xFF);
  170.                 }
  171.             }
  172.             return output;
  173.         }

  174.         function hmacSHA1Bytes(keyBytes, messageBytes) {
  175.             const key = CryptoJS.lib.WordArray.create(keyBytes);
  176.             const message = CryptoJS.lib.WordArray.create(messageBytes);
  177.             const hmac = CryptoJS.HmacSHA1(message, key);
  178.             return hmac.toString(CryptoJS.enc.Hex)
  179.                       .match(/.{1,2}/g)
  180.                       .map(byte => parseInt(byte, 16));
  181.         }

  182.         function dynamicTruncation(hmacBytes) {
  183.             const offset = hmacBytes[hmacBytes.length - 1] & 0x0F;
  184.             return (
  185.                 ((hmacBytes[offset]     & 0x7F) << 24) |
  186.                 ((hmacBytes[offset + 1] & 0xFF) << 16) |
  187.                 ((hmacBytes[offset + 2] & 0xFF) <<  8) |
  188.                 (hmacBytes[offset + 3] & 0xFF)
  189.             );
  190.         }

  191.         function calculateTOTP(secret) {
  192.             try {
  193.                 const keyBytes = base32Decode(secret);
  194.                 if (keyBytes.length === 0) throw new Error("无效的 Base32 密钥");
  195.                 const timeStep = 30;
  196.                 const timestamp = Math.floor(Date.now() / 1000);
  197.                 const counter = Math.floor(timestamp / timeStep);
  198.                 const counterBytes = new Array(8).fill(0);
  199.                 for (let i = 0; i < 8; i++) {
  200.                     counterBytes[7 - i] = (counter >>> (i * 8)) & 0xFF;
  201.                 }
  202.                 const hmacBytes = hmacSHA1Bytes(keyBytes, counterBytes);
  203.                 const binary = dynamicTruncation(hmacBytes);
  204.                 return (binary % 1000000).toString().padStart(6, '0');
  205.             } catch (e) {
  206.                 return `错误: ${e.message}`;
  207.             }
  208.         }

  209.         function updateTOTPAndCountdown() {
  210.             const secret = document.getElementById('secret').value.trim();
  211.             if (!secret) return;

  212.             const timestamp = Math.floor(Date.now() / 1000);
  213.             const elapsed = timestamp % 30;
  214.             const remainingSeconds = 30 - elapsed;
  215.             const progress = elapsed / 30;

  216.             const circle = document.getElementById('countdown-circle');
  217.             const totpDisplay = document.getElementById('result');
  218.             const circumference = 2 * Math.PI * 45;

  219.             circle.style.strokeDasharray = circumference;
  220.             circle.style.strokeDashoffset = circumference * progress;

  221.             document.getElementById('countdown').textContent = remainingSeconds;
  222.             document.getElementById('result').textContent = calculateTOTP(secret);

  223.             setTimeout(updateTOTPAndCountdown, 1000);
  224.         }

  225.         document.getElementById('generate').onclick = function() {
  226.             const secret = document.getElementById('secret').value.trim();
  227.             if (!secret) {
  228.                 alert("请输入 Base32 密钥!");
  229.                 return;
  230.             }
  231.             updateTOTPAndCountdown();
  232.         };

  233.         document.getElementById('add-key').onclick = function() {
  234.             const secret = document.getElementById('secret').value.trim();
  235.             if (secret) {
  236.                 addKey(secret);
  237.                 document.getElementById('secret').value = ''; // 清空输入框
  238.             } else {
  239.                 alert("请输入一个有效的密钥!");
  240.             }
  241.         };

  242.         document.getElementById('remove-selected').onclick = function() {
  243.             const checkboxes = document.querySelectorAll('#key-list input[type="checkbox"]:checked');
  244.             checkboxes.forEach(checkbox => {
  245.                 const li = checkbox.parentElement;
  246.                 const index = Array.prototype.indexOf.call(li.parentElement.children, li);
  247.                 keys.splice(index, 1);
  248.                 li.remove();
  249.             });
  250.         };

  251.         document.getElementById('import').onclick = function() {
  252.             document.getElementById('file-input').click();
  253.         };

  254.         document.getElementById('file-input').onchange = function(event) {
  255.             const file = event.target.files[0];
  256.             if (file) {
  257.                 const reader = new FileReader();
  258.                 reader.onload = function(e) {
  259.                     const secretList = e.target.result.trim().split('\n');
  260.                     secretList.forEach(secret => {
  261.                         addKey(secret);
  262.                     });
  263.                 };
  264.                 reader.readAsText(file);
  265.             }
  266.         };

  267.         document.getElementById('export').onclick = function() {
  268.             const selectedKeys = Array.from(document.querySelectorAll('#key-list input[type="checkbox"]:checked')).map(checkbox => checkbox.value);
  269.             if (selectedKeys.length === 0) {
  270.                 alert("请选择要导出的密钥!");
  271.                 return;
  272.             }
  273.             const blob = new Blob([selectedKeys.join('\n')], { type: 'text/plain' });
  274.             const url = URL.createObjectURL(blob);
  275.             const a = document.createElement('a');
  276.             a.href = url;
  277.             a.download = 'totp_secrets.txt';
  278.             document.body.appendChild(a);
  279.             a.click();
  280.             document.body.removeChild(a);
  281.         };

  282.         function addKey(secret) {
  283.             if (!keys.includes(secret)) {
  284.                 keys.push(secret);
  285.                 const li = document.createElement('li');
  286.                 li.innerHTML = `<input type="checkbox" value="${secret}" /> ${secret.substring(0, 6)}... <span class="remove-key" style="cursor:pointer; margin-left:10px; color:red;">X</span>`;
  287.                 li.querySelector('.remove-key').onclick = function() {
  288.                     const index = keys.indexOf(secret);
  289.                     if (index !== -1) {
  290.                         keys.splice(index, 1);
  291.                         li.remove();
  292.                     }
  293.                 };
  294.                 li.onclick = function() {
  295.                     document.getElementById('secret').value = secret;
  296.                 };
  297.                 document.getElementById('key-list').appendChild(li);
  298.             } else {
  299.                 alert("密钥已存在!");
  300.             }
  301.         }
  302.     </script>
  303. </body>
  304. </html>
复制代码


默认签名:偏爱是我家,发展靠大家! 社区反馈邮箱Mail To:service@pai.al或paijishu@outlook.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|偏爱技术社区-偏爱技术吧-源码-科学刀-我爱辅助-娱乐网--教开服-游戏源码

偏爱技术社区-偏爱技术吧-源码-科学刀-我爱辅助-娱乐网-游戏源码

Powered by Discuz! X3.5

GMT+8, 2025-6-21 07:23 , Processed in 0.080360 second(s), 32 queries .

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