精华5
阅读权限90
最后登录2025-6-21
在线时间141 小时
累计签到:377 天 连续签到:1 天
域主
名望- 126 点
星币- 6580 枚
星辰- 15 颗
好评- 328 点
|
注册登录后全站资源免费查看下载
您需要 登录 才可以下载或查看,没有账号?立即注册
×
纯前端实现,密钥不离本地
实时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[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>
复制代码 优化代码
- <!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[hmacBytes.length - 1] & 0x0F;
- return (
- ((hmacBytes[offset] & 0x7F) << 24) |
- ((hmacBytes[offset + 1] & 0xFF) << 16) |
- ((hmacBytes[offset + 2] & 0xFF) << 8) |
- (hmacBytes[offset + 3] & 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[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}`;
- }
- }
-
- 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[type="checkbox"]: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[0];
- 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[type="checkbox"]:checked')).map(checkbox => checkbox.value);
- if (selectedKeys.length === 0) {
- alert("请选择要导出的密钥!");
- return;
- }
- const blob = new Blob([selectedKeys.join('\n')], { 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>
复制代码
|
|