纯网页版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]