TOTP 原理:30 秒驗證碼、共用密鑰與時鐘偏移
DDEVELOPER
開發發布: 12 分鐘閱讀

TOTP 原理:30 秒驗證碼、共用密鑰與時鐘偏移

驗證器 App 可以根據與伺服器共用的密鑰及目前時間,算出一組短驗證碼。不過,這不代表所有時鐘都完全同步,也不代表驗證碼一定是六位數、每 30 秒一個週期。本文先說明算出相同結果所需的條件,再區分驗證端的重複使用防護與嘗試次數限制,最後比較 TOTP 與通行密鑰。

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

什麼條件下,雙方能離線算出相同驗證碼?

TOTP 是確定性的計算:使用相同密鑰、演算法、位數、時間步長、起算點,且落在同一時間步長區間,才會得到相同結果。TOTP 本身不會讓時鐘自動完全同步;六位數與 30 秒很常見,但不是唯一設定。

初次設定可以掃描 QR Code 或手動輸入密鑰。設定完成後,產生驗證碼本身可以離線進行;登入時送出驗證碼、裝置校時,以及 App 的備份或同步,仍可能需要連線。設定用 QR Code 與密鑰都是驗證憑證,請勿公開或分享。

從 HOTP 到 TOTP:用時間取代事件計數器

RFC 4226(2005 年)的 HOTP 使用共用密鑰 K 與 8 位元組計數器 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 秒。區間內的位置、延遲與已記錄的偏移量都會影響結果。

驗證碼一旦成功完成驗證,即使仍在有效期間內,也不得再次接受。多個請求同時到達時,也必須確保同一驗證碼只被使用一次;驗證與更新已使用狀態需要原子性處理。只有數字相符並不足夠。

短驗證碼也需要限制嘗試次數或頻率。不要將「錯誤三到五次就鎖定」當成所有規格共通的固定要求,還需要考量威脅、帳號復原與使用便利性。時鐘偏移過大時,應透過另一個可信的驗證方式,安全地重新同步或設定。

QR Code 與 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 文件有部分實作會忽略參數的舊註記,不能把它當成所有現行 App 版本的相容性清單。應在實際採用的 App 與版本中,匯入設定後以已知值確認演算法、位數及週期。

TOTP 的限制:網路釣魚與密鑰管理

即使是真正的驗證碼,輸入假網站後仍可能被轉送到真正的服務。手動輸入的 TOTP 沒有將驗證碼綁定至目標網域或登入工作階段,因此不屬於具備抗網路釣魚能力的驗證方式。攻擊者可利用多久,取決於驗證端的接受規則,不一定剛好是 30 秒。

  • 保護可能接觸共用密鑰的路徑,包括設定 QR Code、裝置、備份與驗證伺服器。伺服器需要密鑰本身,或能安全推導密鑰的資料;一般密碼雜湊無法直接重算 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 Code 或密鑰貼到公開檢查表單、共用聊天室或紀錄檔。

用已知測試值確認計算結果

以下使用 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 協助編輯,並於發布前由編輯確認。內容仍可能包含事實、解讀或時效上的錯誤;進行重要判斷前,請查閱所列的一手資料或官方文件。

相關工具

相關文章