純網頁版TOTP驗證碼生成器

分享一個自己寫的純網頁版 TOTP 生成工具
純前端實現,密鑰不離本地
實時 30 秒倒計時可視化展示無需注冊,即開即用
以下是完整代碼
附運行截圖
純網頁版TOTP驗證碼生成器?純網頁版TOTP驗證碼生成器?純網頁版TOTP驗證碼生成器

html 代碼:

  1. <!DOCTYPE html>
  2. <html lang=”en”>
  3. <head>
  4. ? ? <meta charset=”UTF-8″>
  5. ? ? <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
  6. ? ? <title>TOTP 倒計時 </title>
  7. ? ? <script src=”https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js”></script>
  8. ? ? <style>
  9. ? ?? ???body {
  10. ? ?? ?? ?? ?font-family: ‘Arial’, sans-serif;
  11. ? ?? ?? ?? ?max-width: 500px;
  12. ? ?? ?? ?? ?margin: 0 auto;
  13. ? ?? ?? ?? ?padding: 20px;
  14. ? ?? ?? ?? ?text-align: center;
  15. ? ?? ?? ?? ?background: #f5f5f5;
  16. ? ?? ???}
  17. ? ?? ???input {
  18. ? ?? ?? ?? ?padding: 12px;
  19. ? ?? ?? ?? ?width: 300px;
  20. ? ?? ?? ?? ?margin: 15px 0;
  21. ? ?? ?? ?? ?font-size: 16px;
  22. ? ?? ?? ?? ?border: 2px solid #ddd;
  23. ? ?? ?? ?? ?border-radius: 4px;
  24. ? ?? ???}
  25. ? ?? ???button {
  26. ? ?? ?? ?? ?padding: 12px 25px;
  27. ? ?? ?? ?? ?background: #4285f4;
  28. ? ?? ?? ?? ?color: white;
  29. ? ?? ?? ?? ?border: none;
  30. ? ?? ?? ?? ?border-radius: 4px;
  31. ? ?? ?? ?? ?font-size: 16px;
  32. ? ?? ?? ?? ?cursor: pointer;
  33. ? ?? ?? ?? ?transition: background 0.3s;
  34. ? ?? ???}
  35. ? ?? ???button:hover {
  36. ? ?? ?? ?? ?background: #3367d6;
  37. ? ?? ???}
  38. ? ?? ???.totp-display {
  39. ? ?? ?? ?? ?font-family: Arial, sans-serif;
  40. ? ?? ?? ?? ?font-weight: bold;
  41. ? ?? ?? ?? ?font-size: 48px;
  42. ? ?? ?? ?? ?margin: 20px 0;
  43. ? ?? ?? ?? ?letter-spacing: 5px;
  44. ? ?? ?? ?? ?transition: color 0.3s;
  45. ? ?? ???}
  46. ? ?? ???.totp-display.green {
  47. ? ?? ?? ?? ?color: #4CAF50;
  48. ? ?? ???}
  49. ? ?? ???.totp-display.blue {
  50. ? ?? ?? ?? ?color: #2196F3;
  51. ? ?? ???}
  52. ? ?? ???.totp-display.red {
  53. ? ?? ?? ?? ?color: #f44336;
  54. ? ?? ?? ?? ?animation: pulse 0.5s infinite alternate;
  55. ? ?? ???}
  56. ? ?? ???.countdown-container {
  57. ? ?? ?? ?? ?position: relative;
  58. ? ?? ?? ?? ?width: 120px;
  59. ? ?? ?? ?? ?height: 120px;
  60. ? ?? ?? ?? ?margin: 30px auto;
  61. ? ?? ???}
  62. ? ?? ???.countdown-circle {
  63. ? ?? ?? ?? ?width: 100%;
  64. ? ?? ?? ?? ?height: 100%;
  65. ? ?? ???}
  66. ? ?? ???.countdown-circle-bg {
  67. ? ?? ?? ?? ?fill: none;
  68. ? ?? ?? ?? ?stroke: #e0e0e0;
  69. ? ?? ?? ?? ?stroke-width: 10;
  70. ? ?? ???}
  71. ? ?? ???.countdown-circle-fg {
  72. ? ?? ?? ?? ?fill: none;
  73. ? ?? ?? ?? ?stroke: #4CAF50;
  74. ? ?? ?? ?? ?stroke-width: 10;
  75. ? ?? ?? ?? ?stroke-linecap: round;
  76. ? ?? ?? ?? ?transform: rotate(-90deg);
  77. ? ?? ?? ?? ?transform-origin: 50% 50%;
  78. ? ?? ?? ?? ?transition: all 0.1s linear;
  79. ? ?? ???}
  80. ? ?? ???.countdown-circle-fg.blue {
  81. ? ?? ?? ?? ?stroke: #2196F3;
  82. ? ?? ???}
  83. ? ?? ???.countdown-circle-fg.red {
  84. ? ?? ?? ?? ?stroke: #f44336;
  85. ? ?? ???}
  86. ? ?? ???.countdown-text {
  87. ? ?? ?? ?? ?position: absolute;
  88. ? ?? ?? ?? ?top: 50%;
  89. ? ?? ?? ?? ?left: 50%;
  90. ? ?? ?? ?? ?transform: translate(-50%, -50%);
  91. ? ?? ?? ?? ?font-size: 30px;
  92. ? ?? ?? ?? ?font-weight: bold;
  93. ? ?? ?? ?? ?color: #333;
  94. ? ?? ???}
  95. ? ?? ???@keyframes pulse {
  96. ? ?? ?? ?? ?from {opacity: 1;}
  97. ? ?? ?? ?? ?to {opacity: 0.5;}
  98. ? ?? ???}
  99. ? ? </style>
  100. </head>
  101. <body>
  102. ? ? <h1>TOTP 驗證碼生成器 </h1>
  103. ? ? <p> 請輸入 Base32 密鑰:</p>
  104. ? ? <input type=”text” id=”secret” placeholder=” 例如:JBSWY3DPEHPK3PXP” />
  105. ? ? <button> 生成動態驗證碼 </button>
  106. ? ?? ? <div class=”totp-display” id=”result”>000000</div>
  107. ? ?? ? <div class=”countdown-container”>
  108. ? ?? ???<svg class=”countdown-circle” viewBox=”0 0 100 100″>
  109. ? ?? ?? ?? ?<circle class=”countdown-circle-bg” cx=”50″ cy=”50″ r=”45″/>
  110. ? ?? ?? ?? ?<circle class=”countdown-circle-fg” id=”countdown-circle” cx=”50″ cy=”50″ r=”45″/>
  111. ? ?? ???</svg>
  112. ? ?? ???<div class=”countdown-text” id=”countdown”>30</div>
  113. ? ? </div>
  114. ? ? <script>
  115. ? ?? ???// Base32 解碼
  116. ? ?? ???function base32Decode(base32) {
  117. ? ?? ?? ?? ?const alphabet = “ABCDEFGHIJKLMNOPQRSTUVWXYZ234567”;
  118. ? ?? ?? ?? ?base32 = base32.replace(/[^A-Z2-7]/gi, ”).toUpperCase();
  119. ? ?? ?? ?? ?let bits = 0, value = 0, output = [];
  120. ? ?? ?? ?? ?for (let i = 0; i < base32.length; i++) {
  121. ? ?? ?? ?? ?? ? const char = base32.charAt(i);
  122. ? ?? ?? ?? ?? ? const index = alphabet.indexOf(char);
  123. ? ?? ?? ?? ?? ? if (index === -1) continue;
  124. ? ?? ?? ?? ?? ? value = (value << 5) | index;
  125. ? ?? ?? ?? ?? ? bits += 5;
  126. ? ?? ?? ?? ?? ? if (bits >= 8) {
  127. ? ?? ?? ?? ?? ?? ???bits -= 8;
  128. ? ?? ?? ?? ?? ?? ???output.push((value >>> bits) & 0xFF);
  129. ? ?? ?? ?? ?? ? }
  130. ? ?? ?? ?? ?}
  131. ? ?? ?? ?? ?return output;
  132. ? ?? ???}
  133. ? ?? ???// 計算 HMAC-SHA1
  134. ? ?? ???function hmacSHA1Bytes(keyBytes, messageBytes) {
  135. ? ?? ?? ?? ?const key = CryptoJS.lib.WordArray.create(keyBytes);
  136. ? ?? ?? ?? ?const message = CryptoJS.lib.WordArray.create(messageBytes);
  137. ? ?? ?? ?? ?const hmac = CryptoJS.HmacSHA1(message, key);
  138. ? ?? ?? ?? ?return hmac.toString(CryptoJS.enc.Hex)
  139. ? ?? ?? ?? ?? ?? ?? ? .match(/.{1,2}/g)
  140. ? ?? ?? ?? ?? ?? ?? ? .map(byte => parseInt(byte, 16));
  141. ? ?? ???}
  142. ? ?? ???// 動態截斷
  143. ? ?? ???function dynamicTruncation(hmacBytes) {
  144. ? ?? ?? ?? ?const offset = hmacBytes[hmacBytes.length – 1] & 0x0F;
  145. ? ?? ?? ?? ?return (
  146. ? ?? ?? ?? ?? ? ((hmacBytes[offset]? ???& 0x7F) << 24) |
  147. ? ?? ?? ?? ?? ? ((hmacBytes[offset + 1] & 0xFF) << 16) |
  148. ? ?? ?? ?? ?? ? ((hmacBytes[offset + 2] & 0xFF) <<??8) |
  149. ? ?? ?? ?? ?? ???(hmacBytes[offset + 3] & 0xFF)
  150. ? ?? ?? ?? ?);
  151. ? ?? ???}
  152. ? ?? ???// 計算 TOTP
  153. ? ?? ???function calculateTOTP(secret) {
  154. ? ?? ?? ?? ?try {
  155. ? ?? ?? ?? ?? ? const keyBytes = base32Decode(secret);
  156. ? ?? ?? ?? ?? ? if (keyBytes.length === 0) throw new Error(“ 無效的 Base32 密鑰 ”);
  157. ? ?? ?? ?? ?? ? const timeStep = 30;
  158. ? ?? ?? ?? ?? ? const timestamp = Math.floor(Date.now() / 1000);
  159. ? ?? ?? ?? ?? ? const counter = Math.floor(timestamp / timeStep);
  160. ? ?? ?? ?? ?? ? const counterBytes = new Array(8).fill(0);
  161. ? ?? ?? ?? ?? ? for (let i = 0; i < 8; i++) {
  162. ? ?? ?? ?? ?? ?? ???counterBytes[7 – i] = (counter >>> (i * 8)) & 0xFF;
  163. ? ?? ?? ?? ?? ? }
  164. ? ?? ?? ?? ?? ? const hmacBytes = hmacSHA1Bytes(keyBytes, counterBytes);
  165. ? ?? ?? ?? ?? ? const binary = dynamicTruncation(hmacBytes);
  166. ? ?? ?? ?? ?? ? return (binary % 1000000).toString().padStart(6, ‘0’);
  167. ? ?? ?? ?? ?} catch (e) {
  168. ? ?? ?? ?? ?? ? return ` 錯誤: ${e.message}`;
  169. ? ?? ?? ?? ?}
  170. ? ?? ???}
  171. ? ?? ???// 更新倒計時和 TOTP
  172. ? ?? ???function updateTOTPAndCountdown() {
  173. ? ?? ?? ?? ?const secret = document.getElementById(‘secret’).value.trim();
  174. ? ?? ?? ?? ?if (!secret) return;
  175. ? ?? ?? ?? ?const timestamp = Math.floor(Date.now() / 1000);
  176. ? ?? ?? ?? ?const elapsed = timestamp % 30;
  177. ? ?? ?? ?? ?const remainingSeconds = 30 – elapsed;
  178. ? ?? ?? ?? ?const progress = elapsed / 30;
  179. ? ?? ?? ???// 獲取元素
  180. ? ?? ?? ?? ?const circle = document.getElementById(‘countdown-circle’);
  181. ? ?? ?? ?? ?const totpDisplay = document.getElementById(‘result’);
  182. ? ?? ?? ?? ?// 先移除所有顏色類
  183. ? ?? ?? ?? ?circle.classList.remove(‘blue’, ‘red’);
  184. ? ?? ?? ?? ?totpDisplay.classList.remove(‘green’, ‘blue’, ‘red’);
  185. ? ?? ?? ?? ?// 根據剩余時間設置不同顏色和效果
  186. ? ?? ?? ?? ?if (remainingSeconds > 20) {
  187. ? ?? ?? ?? ?? ? // 30-21 秒:綠色
  188. ? ?? ?? ?? ?? ? circle.style.stroke = ‘#4CAF50’;
  189. ? ?? ?? ?? ?? ? totpDisplay.classList.add(‘green’);
  190. ? ?? ?? ?? ?} else if (remainingSeconds > 5) {
  191. ? ?? ?? ?? ?? ? // 20- 6 秒:藍色
  192. ? ?? ?? ?? ?? ? circle.style.stroke = ‘#2196F3’;
  193. ? ?? ?? ?? ?? ? circle.classList.add(‘blue’);
  194. ? ?? ?? ?? ?? ? totpDisplay.classList.add(‘blue’);
  195. ? ?? ?? ?? ?} else {
  196. ? ?? ?? ?? ?? ? // 5- 0 秒:紅色閃爍
  197. ? ?? ?? ?? ?? ? circle.style.stroke = ‘#f44336’;
  198. ? ?? ?? ?? ?? ? circle.classList.add(‘red’);
  199. ? ?? ?? ?? ?? ? totpDisplay.classList.add(‘red’);
  200. ? ?? ?? ?? ?}
  201. ? ?? ?? ?? ?// 更新圓圈進度(逆時針減少)
  202. ? ?? ?? ?? ?const circumference = 2 * Math.PI * 45;
  203. ? ?? ?? ?? ?circle.style.strokeDasharray = circumference;
  204. ? ?? ?? ?? ?circle.style.strokeDashoffset = circumference * progress;
  205. ? ?? ?? ?? ?// 更新倒計時數字
  206. ? ?? ?? ?? ?document.getElementById(‘countdown’).textContent = remainingSeconds;
  207. ? ?? ?? ?? ?// 更新 TOTP
  208. ? ?? ?? ?? ?document.getElementById(‘result’).textContent = calculateTOTP(secret);
  209. ? ?? ?? ?? ?setTimeout(updateTOTPAndCountdown, 1000);
  210. ? ?? ???}
  211. ? ?? ???// 啟動 TOTP 計算
  212. ? ?? ???function startTOTP() {
  213. ? ?? ?? ?? ?const secret = document.getElementById(‘secret’).value.trim();
  214. ? ?? ?? ?? ?if (!secret) {
  215. ? ?? ?? ?? ?? ? alert(“ 請輸入 Base32 密鑰!”);
  216. ? ?? ?? ?? ?? ? return;
  217. ? ?? ?? ?? ?}
  218. ? ?? ?? ?? ?// 初始化圓圈和 TOTP 顯示
  219. ? ?? ?? ?? ?const circle = document.getElementById(‘countdown-circle’);
  220. ? ?? ?? ?? ?const totpDisplay = document.getElementById(‘result’);
  221. ? ?? ?? ?? ?const circumference = 2 * Math.PI * 45;
  222. ? ?? ?? ?? ?circle.style.strokeDasharray = circumference;
  223. ? ?? ?? ?? ?circle.style.strokeDashoffset = 0;
  224. ? ?? ?? ?? ?circle.classList.remove(‘blue’, ‘red’);
  225. ? ?? ?? ?? ?circle.style.stroke = ‘#4CAF50’;
  226. ? ?? ?? ?? ?totpDisplay.classList.remove(‘blue’, ‘red’);
  227. ? ?? ?? ?? ?totpDisplay.classList.add(‘green’);
  228. ? ?? ?? ?? ?updateTOTPAndCountdown();
  229. ? ?? ???}
  230. ? ? </script>
  231. </body>
  232. </html>
? 版權聲明
THE END
喜歡就支持一下吧
點贊7 分享