TOTP 生成器

根據 base32 金鑰生成並校驗基於時間的一次性密碼(TOTP),相容 Google 身份驗證器與 RFC 6238。

一次性密碼,RFC 6238。 根據 base32 金鑰生成當前 TOTP,並校驗你收到的驗證碼。計數器由當前時間推導,因此每 30 或 60 秒重新整理一次。全部在瀏覽器內完成。

------

校驗驗證碼

TOTP 的工作原理

共享金鑰加上時鐘

TOTP 由 RFC 6238 定義,本質上是 HOTP 之上的一層薄封裝。雙方持有同一個金鑰,通常在開通時以二維碼形式交付一次,並儲存在驗證器應用中。生成驗證碼時,兩側各自取當前 Unix 時間,除以步長(預設 30 秒)得到計數器,用金鑰對該計數器做 HMAC-SHA1,再把結果截斷成所需位數。

由於計數器來自時鐘而非某條訊息,開通之後兩方之間無需再傳輸任何資料。服務端不必下發挑戰值,應用也不需要網路連線——這正是驗證器應用在飛航模式下依然可用的原因。

截斷與位數

HMAC 輸出 20 位元組,遠多於一個 6 位驗證碼所需。動態截斷的做法是:取最後一個位元組的低四位作為偏移量,從該位置讀取 4 個位元組,遮蔽最高位以避免符號問題,再對 10^位數 取模。使用可變偏移而非固定偏移,意味著攻擊者無法把分析集中在摘要中某個已知片段上。

6 位幾乎是通用預設值。多數應用也支援 8 位,能多提供約兩位十進位制的抗猜測能力,但這遠不如限流重要——6 位驗證碼單次猜中的機率是百萬分之一,真正的防線是連續失敗幾次後鎖定。

時鐘漂移與重放

裝置時鐘會漂移。服務端通常除當前視窗外,還接受前一個和後一個視窗的驗證碼,從而提供約 ±30 秒的容差。把視窗放寬到一兩分鐘以上,攻擊面就會明顯增大,而且這通常說明該修的是裝置時鐘。

部署時有兩點值得記牢。一是驗證碼在整個視窗內都有效,因此被截獲的驗證碼可以在該視窗內重放,除非服務端把用過的計數器標記為已消費。二是金鑰是對稱的:任何拿到服務端副本的人都能永久生成有效驗證碼,所以它需要與密碼資料庫同等級別的保護——靜態加密、絕不寫日誌、絕不進 git 倉庫。

開源說明:使用瀏覽器原生的 Web Crypto(crypto.subtle)API 與自研 base32 解碼實現,不依賴第三方庫。

常見問題

它能替代我的身份驗證器 App 嗎?
用於測試或手機不在身邊時臨時取碼很方便,但瀏覽器標籤頁並不是長期保管共享金鑰的安全地點。
為什麼我的驗證碼被拒絕了?
幾乎總是時鐘偏移。TOTP 根據當前 UNIX 時間推算驗證碼,裝置只要偏離伺服器超過一個 30 秒步長,算出的碼伺服器就已經翻過去了。
Base32 金鑰代表什麼?
它是雙方共同持有的共享金鑰。之所以用 Base32,是因為它不區分大小寫且避開了易混字元,便於手動輸入或編進二維碼。
為什麼驗證碼是六位?
RFC 6238 把 HMAC 截斷為 31 位元再對 10^6 取模。六位數在易用性和每次百萬分之一的猜中率之間取得平衡,再配合限流就足夠安全。
TOTP 和 HOTP 有什麼區別?
TOTP 按 30 秒的時間步長計數;HOTP 按事件計數,只有驗證碼被使用時才遞增。TOTP 要求時鐘同步,HOTP 要求計數器同步。
同一個驗證碼可以用兩次嗎?
不應該。驗證碼在整個時間步長內都有效,因此伺服器需要記錄使用者已經消費過哪個步長,並拒絕重放。