|
分享一个自己写的纯网页版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>
复制代码 |
|