TOTP 原理:30 秒一次性密碼、共用密鑰與時鐘偏差
DDEVELOPER
開發發布: 12 分鐘閱讀

TOTP 原理:30 秒一次性密碼、共用密鑰與時鐘偏差

認證應用程式顯示的短數字碼,可以由與伺服器共用的密鑰及時間計算出來。這類 TOTP 一次性密碼並不表示所有時鐘都完全同步,也不一定只有六位數、每 30 秒一個週期。本文先解釋雙方算出相同值的條件,再說明驗證端如何防止重用及限制嘗試,最後比較 TOTP 與通行密鑰。

日文原文發布: 2026-04-30

沒有網絡連線,為何仍能產生相同驗證碼?

TOTP 的計算是確定性的。雙方若使用相同的密鑰、演算法、位數、時間步長及起算點,並處於同一時間區間,便會算出相同值。它不會自動令時鐘完全同步;六位數及 30 秒只是常見設定。

初次設定可以掃描 QR 碼或手動輸入密鑰。設定後,產生驗證碼本身毋須連線,但登入時提交驗證碼、校準裝置時間,以及應用程式的備份或同步,仍可能需要網絡。設定用的 QR 碼及密鑰屬於認證資料,不應分享或公開。

由 HOTP 到 TOTP:以時間取代事件計數器

RFC 4226(2005 年)的 HOTP,以共用密鑰 K 及 8 位元組(byte)的計數器 C 計算 HMAC-SHA-1,再轉成短數字碼。產生端每產生一組新碼便推進計數器,驗證端則按照已接受的碼更新狀態。因此,即使產生了碼卻沒有用來登入,雙方計數器亦可能出現差距。向前搜尋多少個值,沒有一個通用的固定數字。

RFC 6238(2011 年)的 TOTP,將事件計數器換成由時間算出的整數 T,並容許使用 HMAC-SHA-256 或 HMAC-SHA-512。兩份 RFC 均屬 Informational 文件,並非認證服務所有營運要求的完整規範。

T = floor((unixSeconds - T0) / X)
T0 = 0   // 預設起算點:Unix epoch
X = 30   // 預設時間步長,以秒為單位
TOTP = 以 HOTP 方式計算(K, T)

2026 年 4 月 30 日 12:00:00 UTC 的 Unix 時間是 1777550400,T 為 59251680。雙方毋須處於同一秒,只要在同一時間步長區間,時間輸入便相同。這裏使用 Unix 秒數,而非本地時區顯示的時間。由於結果會縮成短數字碼,即使跨過區間邊界,顯示值亦有機會碰巧相同。

動態截斷,以及保留開頭的零

動態截斷會按 HMAC 結果選取位置,讀出一個 31 位元的非負整數,再除以「10 的位數次方」並取餘數。六位數的範圍為 000000 至 999999。顯示時要在開頭補零,不能直接以一般整數呈現。SHA-1 的輸出長度是 20 位元組,SHA-256 及 SHA-512 的長度則不同。

以下學習範例可在 Node.js 以 CommonJS 執行,固定使用 T0=0、X=30。輸入的密鑰必須是解碼後的位元組,而不是 Base32 字串。程式未涵蓋密鑰的安全發行及儲存、嘗試頻率限制、拒絕重用、註冊及帳戶復原,不能單靠這個函數建立登入認證服務。

const { createHmac } = require("node:crypto");

// 只供學習及核對測試向量,並非完整的認證伺服器實作。
function totp(key, unixSeconds, algorithm = "sha1", digits = 6) {
  if (!Buffer.isBuffer(key) || key.length < 16) throw new Error("無效密鑰");
  if (!Number.isSafeInteger(unixSeconds) || unixSeconds < 0) throw new Error("無效時間");
  if (!["sha1", "sha256", "sha512"].includes(algorithm)) throw new Error("無效演算法");
  if (![6, 8].includes(digits)) throw new Error("無效位數");
  const step = BigInt(Math.floor(unixSeconds / 30)); // T0 = 0,X = 30
  const message = Buffer.alloc(8);
  message.writeBigUInt64BE(step); // 8 位元組,以高位元組在前的 big-endian 次序寫入
  const digest = createHmac(algorithm, key).update(message).digest();
  const offset = digest[digest.length - 1] & 15;
  const value = digest.readUInt32BE(offset) & 0x7fffffff;
  return String(value % (10 ** digits)).padStart(digits, "0");
}

const testKey = Buffer.from("12345678901234567890", "ascii"); // 公開測試用密鑰
console.log(totp(testKey, 59, "sha1", 8)); // 94287082

以上述公開測試密鑰及 Unix 時間 59 秒計算,八位數結果是 94287082,六位數則是 287082。這把密鑰已公開,切勿用於真實帳戶。六位數在只接受一個候選值、只猜一次的情況下,猜中的機會約為百萬分之一。取餘數會帶來輕微偏差,而接受的候選值及嘗試次數越多,風險亦越高。這是猜測成功率,與一般所說的碰撞機率不同。

時鐘偏差、網絡延遲與重用,須分開處理

RFC 6238 第 5.2 節建議,網絡延遲的容許範圍最多為一個時間步長。這並不是要求所有驗證端都接受「目前區間加前後各一個區間」。時鐘偏差在第 6 節另行處理,容許向過去及未來搜尋多遠,由驗證端設定。

假設驗證端檢查目前及前後各一個區間:X=30、伺服器時間為 60 秒時,T=2,候選值便來自 T=1、2、3。對應的產生時間為 30 秒或以上、但少於 120 秒。這不能一概說成「容許時鐘偏差 ±30 秒」,亦不是「輸入後有效 90 秒」;區間邊界、延遲及已記錄的偏差都會影響結果。

一旦 OTP 已成功通過認證,即使仍在有效期內,亦不得再次接受。多個請求同時到達時,也要確保只使用一次。驗證及更新已使用狀態須以原子性操作處理,不能只看數字是否相符。

短數字碼需要限制嘗試次數或頻率。「錯誤三至五次便鎖定」不是所有規格的共同要求,還要考慮威脅、帳戶復原及使用便利。時鐘偏差過大時,應透過另一個可信的認證方法,安全地重新同步或設定。

QR 碼與 otpauth URI:Base32 並非加密

Google 的 Key URI Format 常用於傳遞認證應用程式的設定,但它不是 RFC 6238 本身定義的註冊流程。以下一行使用公開測試密鑰,切勿用於真實帳戶。實際 URI 亦不能混入註解或換行。

otpauth://totp/Example:alice%40example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Example&algorithm=SHA1&digits=6&period=30

secret 是以 Base32 表示的密鑰,issuer 是發行者,而 algorithm、digits、period 是計算設定。Base32 只是編碼,並非加密;字元數也不等於密鑰的位元長度。例子的 32 個字元可解碼成 20 位元組,即 160 位元,但公開測試資料並不具備秘密性。

RFC 4226 規定共用密鑰至少 128 位元,並建議使用 160 位元;RFC 6238 則建議密鑰長度與 HMAC 輸出長度相同。產生密鑰應使用密碼學安全的隨機數,並為每個產生端配置獨立密鑰。NIST SP 800-63B-4 對密鑰及演算法的安全強度要求,不能直接當作字串長度或 RFC 的密鑰長度要求。

Key URI 文件保留了一些關於實作會忽略參數的舊註記。這些註記不是所有現行應用程式版本的兼容性清單。應在實際採用的應用程式及版本匯入設定後,以已知值核對演算法、位數及週期。

TOTP 的限制:網絡釣魚及密鑰保護

即使驗證碼正確,輸入假網站後亦可能被即時轉送至真正服務。手動輸入的 TOTP 並沒有將驗證碼綁定至目標網域或登入工作階段,因此不屬於具備抗網絡釣魚能力的認證方式。可被濫用多久取決於驗證端的接受規則,並非一律 30 秒。

  • 保護可能接觸共用密鑰的途徑,包括設定 QR 碼、裝置、備份及驗證伺服器。伺服器需要密鑰本身,或可安全推導密鑰的資料;一般密碼雜湊無法直接重算 TOTP。
  • 結合儲存時加密、密鑰管理、存取控制及審核。系統遭入侵不代表所有密鑰必定外洩,但有加密也不表示絕不外洩。
  • TOTP 計算本身不依賴 SIM 卡。不過,若系統允許透過短訊復原帳戶或登入同步服務,SIM 卡轉移攻擊仍可能間接影響安全。

在只有密碼的流程加入 OTP 可以加強保護,但仍須把註冊、帳戶復原及備份納入整體設計。

通行密鑰有何不同?認證保證等級又如何判斷?

通行密鑰(passkey)是採用公開密鑰方式的認證憑證。WebAuthn 由 W3C 制定,CTAP 則由 FIDO Alliance 制定;伺服器使用公開密鑰驗證簽署,毋須與認證器共用秘密密鑰。RP ID 及 origin 的檢查把憑證的使用限制於適當網站,與手動輸入 TOTP 不同。

使用憑證時可配合裝置 PIN 或生物特徵認證來確認。通行密鑰分為同步型及裝置綁定型,並非每一把都會同步。抗網絡釣魚能力亦不代表能完全防止裝置被入侵、工作階段被盜取,或帳戶復原途徑過於薄弱的風險。

NIST 的認證保證等級(AAL)不能單憑演算法名稱判定。OTP 配合哪些認證因素,以及驗證端符合哪些條件,都影響是否符合 AAL1 或 AAL2;AAL2 亦要求提供具抗網絡釣魚能力的認證選項。可同步的認證器不能用於 AAL3,裝置綁定密鑰也不會自動符合 AAL3,仍須滿足密鑰不可匯出、硬件保護及相關驗證要求。

採用程式庫前,先核對版本及驗證端責任

即使使用 otpauth、speakeasy、PyOTP、pquerna/otp、totp-rs 或 OTPHP 等程式庫,也要核對所選版本的維護狀況及規格。不能一概保證「完全符合規格」,亦不能假設差異只有可接受的時間範圍。

  • 核對 RFC 測試向量、開頭的零、30 秒邊界,以及 2038 年之後的時間。
  • 確認密鑰格式及長度、位數、週期和雜湊演算法的兼容性。
  • 另外設計嘗試頻率限制、拒絕已使用的碼、同時請求處理、密鑰儲存及帳戶復原。

本文程式只供學習。一般密碼字串產生器不會替你核對 TOTP 密鑰的位元組長度及 Base32 格式。Base64 與 Base32 亦是不同格式,Base64 編碼解碼工具並非 TOTP 設定密鑰驗證器。切勿將真實設定 QR 碼或密鑰貼到公開檢查表格、共用聊天室或記錄檔。

以已知測試值核對結果

下表採用 RFC 6238 附錄 B 的 SHA-1 公開測試密鑰,即 ASCII 字串 12345678901234567890 的位元組,設定為 T0=0、X=30、八位數。即使結果以 0 開頭,也要保留完整八個字元。

如表格未能完整顯示,可左右捲動。

Unix 秒數T八位數驗證碼
59194287082
11111111093703703607081804
2000000000066666666665353130

資料來源:RFC 6238 附錄 B。結果相符只證明這些計算符合測試值,並不代表整個正式服務都安全。測試 SHA-256 或 SHA-512 時,亦須分別使用 RFC 對應的測試密鑰。

重點:計算條件與驗證政策是兩回事

  • 相同密鑰、計算設定及時間步長區間,會產生相同 OTP。
  • 六位數及 30 秒是常見設定,並非唯一選擇。
  • 離線產生驗證碼,不表示登入及同步也毋須連線。
  • 安全發行及儲存密鑰、限制嘗試及防止重用,不能單靠一個計算函數完成。
  • TOTP 與通行密鑰的關鍵差異,在於密鑰如何保存,以及認證是否綁定至正確的目標。

參考資料及來源

編輯說明

本文使用 AI 協助編輯,並在發布前由編輯核對。內容仍可能包含事實、理解或時效上的錯誤;作出重要決定前,請查閱所列的一手資料或官方文件。

相關工具

相關文章