返回列表 发布新帖
查看: 21|回复: 0

纯网页版TOTP验证码生成器

1786

主题

0

回帖

5662

积分

论坛元老

积分
5662
发表于 5 小时前 | 查看全部 |阅读模式
分享一个自己写的纯网页版TOTP生成工具
纯前端实现,密钥不离本地
实时30秒倒计时可视化展示无需注册,即开即用
以下是完整代码
附运行截图
    

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;
            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[hmacBytes.length - 1] & 0x0F;
            return (
                ((hmacBytes[offset]     & 0x7F) << 24) |
                ((hmacBytes[offset + 1] & 0xFF) << 16) |
                ((hmacBytes[offset + 2] & 0xFF) <<  8) |
                 (hmacBytes[offset + 3] & 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[7 - i] = (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>
复制代码
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Copyright © 2001-2025 52线报网 版权所有 All Rights Reserved. |网站地图 闽ICP备19006036号-4
关灯 在本版发帖
扫一扫添加微信客服
返回顶部
快速回复 返回顶部 返回列表