巨二兔 发表于 9 小时前

纯网页版TOTP验证码生成器

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TOTP 倒计时</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
    <style>
      body {
            font-family: 'Arial', sans-serif;
            max-width: 500px;
            margin: 0 auto;
            padding: 20px;
            text-align: center;
            background: #f5f5f5;
      }
      input {
            padding: 12px;
            width: 300px;
            margin: 15px 0;
            font-size: 16px;
            border: 2px solid #ddd;
            border-radius: 4px;
      }
      button {
            padding: 12px 25px;
            background: #4285f4;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer;
            transition: background 0.3s;
      }
      button:hover {
            background: #3367d6;
      }
      .totp-display {
            font-family: Arial, sans-serif;
            font-weight: bold;
            font-size: 48px;
            margin: 20px 0;
            letter-spacing: 5px;
            transition: color 0.3s;
      }
      .totp-display.green {
            color: #4CAF50;
      }
      .totp-display.blue {
            color: #2196F3;
      }
      .totp-display.red {
            color: #f44336;
            animation: pulse 0.5s infinite alternate;
      }
      .countdown-container {
            position: relative;
            width: 120px;
            height: 120px;
            margin: 30px auto;
      }
      .countdown-circle {
            width: 100%;
            height: 100%;
      }
      .countdown-circle-bg {
            fill: none;
            stroke: #e0e0e0;
            stroke-width: 10;
      }
      .countdown-circle-fg {
            fill: none;
            stroke: #4CAF50;
            stroke-width: 10;
            stroke-linecap: round;
            transform: rotate(-90deg);
            transform-origin: 50% 50%;
            transition: all 0.1s linear;
      }
      .countdown-circle-fg.blue {
            stroke: #2196F3;
      }
      .countdown-circle-fg.red {
            stroke: #f44336;
      }
      .countdown-text {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 30px;
            font-weight: bold;
            color: #333;
      }
      @keyframes pulse {
            from { opacity: 1; }
            to { opacity: 0.5; }
      }
    </style>
</head>
<body>
    <h1>TOTP 验证码生成器</h1>
    <p>请输入 Base32 密钥:</p>
    <input type="text" id="secret" placeholder="例如:JBSWY3DPEHPK3PXP" />
    <button>生成动态验证码</button>
       <div class="totp-display" id="result">000000</div>
       <div class="countdown-container">
      <svg class="countdown-circle" viewBox="0 0 100 100">
            <circle class="countdown-circle-bg" cx="50" cy="50" r="45"/>
            <circle class="countdown-circle-fg" id="countdown-circle" cx="50" cy="50" r="45"/>
      </svg>
      <div class="countdown-text" id="countdown">30</div>
    </div>
    <script>
      // Base32 解码
      function base32Decode(base32) {
            const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
            base32 = base32.replace(/[^A-Z2-7]/gi, '').toUpperCase();
            let bits = 0, value = 0, output = [];
            for (let i = 0; i < base32.length; i++) {
                const char = base32.charAt(i);
                const index = alphabet.indexOf(char);
                if (index === -1) continue;
                value = (value << 5) | index;
                bits += 5;
                if (bits >= 8) {
                  bits -= 8;
                  output.push((value >>> bits) & 0xFF);
                }
            }
            return output;
      }
      // 计算 HMAC-SHA1
      function hmacSHA1Bytes(keyBytes, messageBytes) {
            const key = CryptoJS.lib.WordArray.create(keyBytes);
            const message = CryptoJS.lib.WordArray.create(messageBytes);
            const hmac = CryptoJS.HmacSHA1(message, key);
            return hmac.toString(CryptoJS.enc.Hex)
                      .match(/.{1,2}/g)
                      .map(byte => parseInt(byte, 16));
      }
      // 动态截断
      function dynamicTruncation(hmacBytes) {
            const offset = hmacBytes & 0x0F;
            return (
                ((hmacBytes   & 0x7F) << 24) |
                ((hmacBytes & 0xFF) << 16) |
                ((hmacBytes & 0xFF) <<8) |
               (hmacBytes & 0xFF)
            );
      }
      // 计算 TOTP
      function calculateTOTP(secret) {
            try {
                const keyBytes = base32Decode(secret);
                if (keyBytes.length === 0) throw new Error("无效的 Base32 密钥");
                const timeStep = 30;
                const timestamp = Math.floor(Date.now() / 1000);
                const counter = Math.floor(timestamp / timeStep);
                const counterBytes = new Array(8).fill(0);
                for (let i = 0; i < 8; i++) {
                  counterBytes = (counter >>> (i * 8)) & 0xFF;
                }
                const hmacBytes = hmacSHA1Bytes(keyBytes, counterBytes);
                const binary = dynamicTruncation(hmacBytes);
                return (binary % 1000000).toString().padStart(6, '0');
            } catch (e) {
                return `错误: ${e.message}`;
            }
      }
      // 更新倒计时和 TOTP
      function updateTOTPAndCountdown() {
            const secret = document.getElementById('secret').value.trim();
            if (!secret) return;
            const timestamp = Math.floor(Date.now() / 1000);
            const elapsed = timestamp % 30;
            const remainingSeconds = 30 - elapsed;
            const progress = elapsed / 30;
         // 获取亓素
            const circle = document.getElementById('countdown-circle');
            const totpDisplay = document.getElementById('result');
         
            // 先移除所有颜色类
            circle.classList.remove('blue', 'red');
            totpDisplay.classList.remove('green', 'blue', 'red');
            
            // 根据剩余时间设置不同颜色和效果
            if (remainingSeconds > 20) {
                // 30-21秒:绿色
                circle.style.stroke = '#4CAF50';
                totpDisplay.classList.add('green');
            } else if (remainingSeconds > 5) {
                // 20-6秒:蓝色
                circle.style.stroke = '#2196F3';
                circle.classList.add('blue');
                totpDisplay.classList.add('blue');
            } else {
                // 5-0秒:红色闪烁
                circle.style.stroke = '#f44336';
                circle.classList.add('red');
                totpDisplay.classList.add('red');
            }
            
            // 更新圆圈进度(逆时针减少)
            const circumference = 2 * Math.PI * 45;
            circle.style.strokeDasharray = circumference;
            circle.style.strokeDashoffset = circumference * progress;
            
            // 更新倒计时数字
            document.getElementById('countdown').textContent = remainingSeconds;
            
            // 更新 TOTP
            document.getElementById('result').textContent = calculateTOTP(secret);

            setTimeout(updateTOTPAndCountdown, 1000);
      }

      // 启动 TOTP 计算
      function startTOTP() {
            const secret = document.getElementById('secret').value.trim();
            if (!secret) {
                alert("请输入 Base32 密钥!");
                return;
            }
            
            // 初始化圆圈和TOTP显示
            const circle = document.getElementById('countdown-circle');
            const totpDisplay = document.getElementById('result');
            const circumference = 2 * Math.PI * 45;
            
            circle.style.strokeDasharray = circumference;
            circle.style.strokeDashoffset = 0;
            circle.classList.remove('blue', 'red');
            circle.style.stroke = '#4CAF50';
            
            totpDisplay.classList.remove('blue', 'red');
            totpDisplay.classList.add('green');
            
            updateTOTPAndCountdown();
      }
    </script>
</body>
</html>优化代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TOTP倒计时生成器</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
    <style>
      body {
            font-family: 'Arial', sans-serif;
            display: flex;
            max-width: 1200px; /* 增加最大宽度 */
            margin: 0 auto;
            background: #f5f5f5;
      }
      #key-container {
            width: 30%;
            padding: 20px;
            border-right: 1px solid #ddd; /* 右侧边框 */
            background-color: white;
            height: 100vh; /* 设置高度与视口相同 */
            overflow-y: auto; /* 如果内容超出则出现滚动条 */
      }
      #key-container h2 {
            font-size: 20px;
            margin: 0 0 15px;
      }
      #key-list {
            list-style: none;
            padding: 0;
      }
      #key-list li {
            padding: 10px;
            border: 1px solid #ddd;
            margin-bottom: 5px;
            border-radius: 4px;
            position: relative;
      }
      #main-container {
            width: 70%; /* 右侧主体内容宽度 */
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center; /* 水平居中 */
      }
      input {
            padding: 12px;
            width: 240px;
            margin: 15px 0;
            font-size: 16px;
            border: 2px solid #ddd;
            border-radius: 4px;
            transition: border-color 0.3s;
      }
      input:focus {
            border-color: #4285f4;
            outline: none;
      }
      button {
            padding: 12px 15px;
            background: #4285f4;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer;
            transition: background 0.3s, box-shadow 0.3s;
            margin-left: 10px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      }
      button:hover {
            background: #3367d6;
            box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2);
      }
      button:active {
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
            transform: translateY(2px);
      }
      .totp-display {
            font-family: Arial, sans-serif;
            font-weight: bold;
            font-size: 48px;
            margin: 20px 0;
            letter-spacing: 5px;
            transition: color 0.3s;
      }
      .countdown-container {
            position: relative;
            width: 120px;
            height: 120px;
            margin: 30px auto; /* 上下外边距自动 */
            display: flex; /* 使用 Flexbox 进行居中 */
            flex-direction: column; /* 纵列布局 */
            align-items: center; /* 水平居中 */
            justify-content: center; /* 垂直居中 */
      }
      .countdown-circle {
            width: 100%;
            height: 100%;
      }
      .countdown-circle-bg {
            fill: none;
            stroke: #e0e0e0;
            stroke-width: 10;
      }
      .countdown-circle-fg {
            fill: none;
            stroke: #4CAF50;
            stroke-width: 10;
            stroke-linecap: round;
            transform: rotate(-90deg);
            transform-origin: 50% 50%;
            transition: stroke 0.1s linear;
      }
      #countdown {
            font-size: 24px;
            position: absolute; /* 绝对定位在圆圈中/心 */
            text-align: center;
            width: 100%; /* 宽度占满 */
            top: 50%; /* 垂直居中 */
            left: 50%; /* 水平居中 */
            transform: translate(-50%, -50%); /* 使其真正居中 */
      }
    </style>
</head>
<body>
    <div id="key-container">
      <h2>临时存放密钥列表</h2>
      <ul id="key-list"></ul>
      <button id="remove-selected">删除选中</button>
    </div>

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

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

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

      <div class="countdown-container">
            <svg class="countdown-circle" viewBox="0 0 100 100">
                <circle class="countdown-circle-bg" cx="50" cy="50" r="45"/>
                <circle class="countdown-circle-fg" id="countdown-circle" cx="50" cy="50" r="45"/>
            </svg>
            <div class="countdown-text" id="countdown">30</div>
      </div>
    </div>

    <script>
      let keys = [];

      function base32Decode(base32) {
            const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
            base32 = base32.replace(/[^A-Z2-7]/gi, '').toUpperCase();
            let bits = 0, value = 0, output = [];
            for (let i = 0; i < base32.length; i++) {
                const char = base32.charAt(i);
                const index = alphabet.indexOf(char);
                if (index === -1) continue;
                value = (value << 5) | index;
                bits += 5;
                if (bits >= 8) {
                  bits -= 8;
                  output.push((value >>> bits) & 0xFF);
                }
            }
            return output;
      }

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

      function dynamicTruncation(hmacBytes) {
            const offset = hmacBytes & 0x0F;
            return (
                ((hmacBytes   & 0x7F) << 24) |
                ((hmacBytes & 0xFF) << 16) |
                ((hmacBytes & 0xFF) <<8) |
                (hmacBytes & 0xFF)
            );
      }

      function calculateTOTP(secret) {
            try {
                const keyBytes = base32Decode(secret);
                if (keyBytes.length === 0) throw new Error("无效的 Base32 密钥");
                const timeStep = 30;
                const timestamp = Math.floor(Date.now() / 1000);
                const counter = Math.floor(timestamp / timeStep);
                const counterBytes = new Array(8).fill(0);
                for (let i = 0; i < 8; i++) {
                  counterBytes = (counter >>> (i * 8)) & 0xFF;
                }
                const hmacBytes = hmacSHA1Bytes(keyBytes, counterBytes);
                const binary = dynamicTruncation(hmacBytes);
                return (binary % 1000000).toString().padStart(6, '0');
            } catch (e) {
                return `错误: ${e.message}`;
            }
      }

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

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

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

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

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

            setTimeout(updateTOTPAndCountdown, 1000);
      }

      document.getElementById('generate').onclick = function() {
            const secret = document.getElementById('secret').value.trim();
            if (!secret) {
                alert("请输入 Base32 密钥!");
                return;
            }
            updateTOTPAndCountdown();
      };

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

      document.getElementById('remove-selected').onclick = function() {
            const checkboxes = document.querySelectorAll('#key-list input:checked');
            checkboxes.forEach(checkbox => {
                const li = checkbox.parentElement;
                const index = Array.prototype.indexOf.call(li.parentElement.children, li);
                keys.splice(index, 1);
                li.remove();
            });
      };

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

      document.getElementById('file-input').onchange = function(event) {
            const file = event.target.files;
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                  const secretList = e.target.result.trim().split('\n');
                  secretList.forEach(secret => {
                        addKey(secret);
                  });
                };
                reader.readAsText(file);
            }
      };

      document.getElementById('export').onclick = function() {
            const selectedKeys = Array.from(document.querySelectorAll('#key-list input:checked')).map(checkbox => checkbox.value);
            if (selectedKeys.length === 0) {
                alert("请选择要导出的密钥!");
                return;
            }
            const blob = new Blob(, { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'totp_secrets.txt';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
      };

      function addKey(secret) {
            if (!keys.includes(secret)) {
                keys.push(secret);
                const li = document.createElement('li');
                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>`;
                li.querySelector('.remove-key').onclick = function() {
                  const index = keys.indexOf(secret);
                  if (index !== -1) {
                        keys.splice(index, 1);
                        li.remove();
                  }
                };
                li.onclick = function() {
                  document.getElementById('secret').value = secret;
                };
                document.getElementById('key-list').appendChild(li);
            } else {
                alert("密钥已存在!");
            }
      }
    </script>
</body>
</html>

页: [1]
查看完整版本: 纯网页版TOTP验证码生成器