分享一個自己寫的純網頁版 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>
? 版權聲明
文章版權歸作者所有,未經允許請勿轉載。
THE END