跳至主要內容
Skip to content

TOTP 實作原理

TOTP 是很多人最熟悉的第二因素:

text
打開 Google Authenticator / Microsoft Authenticator / 1Password
看到一組 6 位數驗證碼
30 秒後換下一組

它看起來像是 app 從伺服器拿到最新驗證碼。

但其實不是。

TOTP 的關鍵是:

text
伺服器和使用者的 Authenticator app 事先共享同一把 secret,
之後雙方都用「目前時間」和這把 secret 算出同一組 code。

所以 Authenticator app 離線也能產生驗證碼。

這也是 TOTP 和 SMS / Email OTP 最大的差別之一:

text
SMS / Email OTP 是伺服器產生 code 再送出去。
TOTP 是伺服器和裝置各自用同一個算法算出 code。

一、 一句話回答

如果面試官問:

TOTP 的實作原理是什麼?

可以先回答:

TOTP 是 Time-Based One-Time Password,定義在 RFC 6238,建立在 HOTP 之上。使用者啟用 TOTP 時,伺服器產生一個隨機 shared secret,透過 otpauth URI / QR code 交給 Authenticator app。之後 app 和伺服器都用同一個 secret,加上目前時間切成固定時間步長,例如 30 秒,得到 time counter,再用 HMAC 算出動態值,經過 truncation 後取出 6 位數 code。驗證時伺服器會用自己保存的 secret 計算目前時間窗口的 code,通常允許前後一個窗口容忍時鐘偏移,並限制重試次數和防止同一時間窗口 code 被重放。TOTP 比 SMS 更不依賴電信通道,但 shared secret 必須安全保存,且 TOTP 仍可能被即時釣魚代理轉送,所以高風險場景可以再升級到 WebAuthn / Passkey。

短一點可以說:

text
TOTP = shared secret + current time + HMAC -> 6 位數驗證碼。

二、 TOTP 和 HOTP 的關係

TOTP 來自 HOTP。

HOTP 是 HMAC-Based One-Time Password,定義在 RFC 4226。

HOTP 的輸入是:

text
shared secret + counter

TOTP 把 counter 換成時間:

text
shared secret + time counter

也就是:

text
HOTP(K, C)
TOTP(K, T)

其中:

符號意義
Kshared secret
Ccounter
Ttime counter

TOTP 的 time counter 通常是:

text
T = floor((currentUnixTime - T0) / X)

常見設定:

text
T0 = 0
X = 30 秒

也就是每 30 秒切一格。

三、 TOTP 流程總覽

啟用 TOTP:

登入驗證:

四、 Shared Secret 是什麼

TOTP 的 shared secret 是伺服器和 authenticator app 都知道的一段隨機秘密。

例如 Base32 表示:

text
JBSWY3DPEHPK3PXP

啟用時通常用 QR code 傳給 app,QR code 內容是 otpauth URI:

text
otpauth://totp/Hirimu:alice@example.com
  ?secret=JBSWY3DPEHPK3PXP
  &issuer=Hirimu
  &algorithm=SHA1
  &digits=6
  &period=30

參數:

參數說明
secretshared secret,通常 Base32
issuer服務名稱
labelapp 裡顯示的帳號
algorithmHMAC 演算法,常見 SHA1
digitscode 位數,常見 6
period時間步長,常見 30 秒

這個 QR code 很敏感。

任何人掃到它,就能產生使用者的 TOTP code。

所以啟用流程要注意:

  • QR code 只在近期重新驗證後顯示。
  • secret 先 pending,驗證成功才啟用。
  • 不要把 secret 寫進 log。
  • 不要讓 QR code 被快取。
  • 不要在啟用後再次顯示原 secret。

五、 TOTP 怎麼算

簡化流程:

text
1. 取得目前 Unix time。
2. 除以 30 秒,得到 time counter。
3. 用 shared secret 對 time counter 做 HMAC。
4. 從 HMAC 結果做 dynamic truncation。
5. 取 modulo 10^digits,得到 6 位數 code。

概念:

text
timeCounter = floor(currentUnixTime / 30)
hmac = HMAC-SHA1(secret, timeCounter)
number = dynamicTruncate(hmac)
code = number mod 1_000_000

Node.js 簡化範例:

ts
import crypto from 'crypto'

function hotp(secret: Buffer, counter: number, digits = 6) {
  const buffer = Buffer.alloc(8)
  buffer.writeBigUInt64BE(BigInt(counter))

  const hmac = crypto
    .createHmac('sha1', secret)
    .update(buffer)
    .digest()

  const offset = hmac[hmac.length - 1] & 0x0f
  const code =
    ((hmac[offset] & 0x7f) << 24) |
    ((hmac[offset + 1] & 0xff) << 16) |
    ((hmac[offset + 2] & 0xff) << 8) |
    (hmac[offset + 3] & 0xff)

  return (code % 10 ** digits).toString().padStart(digits, '0')
}

function totp(secret: Buffer, now = Date.now(), period = 30, digits = 6) {
  const counter = Math.floor(Math.floor(now / 1000) / period)
  return hotp(secret, counter, digits)
}

實務上建議使用成熟 library,而不是自己維護密碼學細節。

但面試時能講出這個流程,代表你不是只會「掃 QR code」。

六、 為什麼常見是 30 秒

TOTP 需要在安全和體驗之間平衡。

如果時間太短:

  • 使用者還沒輸入就過期。
  • 客服和使用者體驗變差。
  • 時鐘偏移更容易造成失敗。

如果時間太長:

  • code 可用時間變長。
  • 被釣魚或肩窺後可利用時間變長。

30 秒是常見預設。

搭配前後時間窗口,實際可接受範圍可能是:

text
前一格、目前格、下一格

也就是最多約 90 秒內某些 code 可能被接受。

所以你不能只說:

text
TOTP 只有 30 秒有效。

實務上還要看 server 接受幾個 window。

七、 驗證窗口與時鐘偏移

使用者手機和伺服器時間可能不完全一致。

所以伺服器通常允許少量偏移:

text
window = 1

表示接受:

  • 前一個 time step。
  • 目前 time step。
  • 下一個 time step。

驗證概念:

ts
function verifyTotp(inputCode: string, secret: Buffer, now = Date.now()) {
  const period = 30
  const currentCounter = Math.floor(Math.floor(now / 1000) / period)

  for (const drift of [-1, 0, 1]) {
    const expected = hotp(secret, currentCounter + drift)
    if (safeCompare(inputCode, expected)) {
      return {
        valid: true,
        counter: currentCounter + drift,
      }
    }
  }

  return { valid: false }
}

window 不宜太大。

因為越大的 window,攻擊者猜中的機率越高,可重放時間也越長。

常見做法:

text
window = 1

若使用硬體 token 或特殊場景,可另外處理 clock drift,但一般 app 類 TOTP 不應無限制放寬。

八、 重放防護

TOTP 的 code 在同一時間窗口內會保持不變。

例如 30 秒內都是:

text
839204

如果使用者已經用這個 code 登入成功,攻擊者在同一個時間窗口內重放同一 code,理論上可能成功。

所以高風險系統可以記錄:

text
last_used_counter

驗證成功後:

ts
if (matchedCounter <= authenticator.lastUsedCounter) {
  throw new Error('TOTP_REPLAYED')
}

await updateLastUsedCounter(authenticator.id, matchedCounter)

注意:

  • 如果允許前一個 window,就要小心 counter 順序。
  • 多裝置同時登入可能有 race condition。
  • 更新 last_used_counter 要在 transaction 裡做。

對一般產品,至少要做到:

  • 成功驗證後提升 session。
  • 限制嘗試次數。
  • 記錄 last_used_at。
  • 對敏感操作要求 fresh MFA。

對高風險產品,建議加入 counter replay protection。

九、 嘗試次數與速率限制

6 位數 TOTP 有一百萬種可能。

如果接受 3 個時間窗口,單次猜中機率約是:

text
3 / 1,000,000

看起來很低,但如果沒有速率限制,就會出問題。

限制應該包含:

  • 每個 user / authenticator 的嘗試次數。
  • 每個 session 的嘗試次數。
  • 每個 IP 的嘗試頻率。
  • 登入失敗累積風險。
  • 多次失敗後要求重新輸入密碼。

例如:

text
同一 MFA challenge 最多嘗試 5 次。
同一 user 5 分鐘最多 10 次。
同一 IP 1 分鐘最多 30 次。

錯誤訊息不要太精準:

text
驗證碼錯誤或已過期。

避免讓攻擊者知道是時間偏移、counter replay,還是 code 猜錯。

十、 Secret 怎麼保存

這是 TOTP 很重要的地方。

伺服器必須能重新計算 TOTP,所以不能只存 hash。

它需要存取原始 secret,或能解密得到原始 secret。

常見做法:

text
encrypted_secret = encrypt(kms_key, totp_secret)

資料庫存:

sql
create table mfa_totp_authenticators (
  id uuid primary key,
  user_id uuid not null references users(id),
  name text,
  encrypted_secret text not null,
  digits int not null default 6,
  period int not null default 30,
  algorithm text not null default 'SHA1',
  enabled boolean not null default false,
  verified_at timestamp,
  last_used_at timestamp,
  last_used_counter bigint,
  created_at timestamp not null
);

保存原則:

  • secret 要加密。
  • 加密 key 不要和資料庫放在一起。
  • 不要把 secret 寫入 log。
  • 匯出 / 查看 secret 要禁止或嚴格限制。
  • 啟用後不再顯示原 QR code。
  • rotate / reset 要走重新驗證。

TOTP 的安全性取決於:

text
使用者裝置保護 secret;
伺服器也保護 secret。

任一邊 secret 外洩,TOTP 就失效。

十一、 啟用流程

TOTP 啟用流程應該有 pending 狀態。

不要一產生 secret 就直接啟用。

比較好的流程:

text
1. 使用者已登入。
2. 要求近期重新驗證。
3. 伺服器產生 pending secret。
4. 顯示 QR code。
5. 使用者掃描後輸入第一組 TOTP。
6. 伺服器驗證成功。
7. 將 authenticator 標記 enabled。
8. 產生 backup codes。
9. 寄安全通知。
10. 寫 audit log。

如果使用者沒有輸入正確 code,就不啟用。

因為你不能確定他真的有把 secret 加到 authenticator app。

十二、 QR code 與 otpauth URI

Authenticator app 通常讀取 otpauth:// URI。

範例:

text
otpauth://totp/Hirimu:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Hirimu&digits=6&period=30

建議:

  • issuer 使用產品名稱。
  • label 包含產品和帳號,方便使用者辨識。
  • 不要在 label 放過多敏感資料。
  • secret 使用足夠隨機性。
  • QR code 頁面禁止快取。
  • 顯示手動輸入 secret 時要提醒保存風險。

HTTP header 可考慮:

text
Cache-Control: no-store

十三、 TOTP 和 SMS OTP 比較

比較SMS OTPTOTP
code 來源伺服器產生後送出app 和伺服器各自計算
是否需要網路需要 SMS 通道app 可離線
成本每封簡訊成本幾乎無發送成本
主要風險SIM swap、攔截、電信商風險secret 外洩、釣魚代理、裝置遺失
使用體驗不用安裝 app,但可能延遲需安裝或使用密碼管理器
釣魚抗性不具備不具備完整釣魚抗性

TOTP 通常比 SMS OTP 更適合作為第二因素。

但它仍不是最強 MFA。

即時釣魚代理仍可以騙使用者輸入 TOTP,再轉送到真網站。

若要更強釣魚抗性,要看 WebAuthn / Passkey。

十四、 裝置遺失與備援

TOTP 最大的產品問題是:

text
使用者換手機或手機遺失怎麼辦?

安全備援方式:

  • Backup codes。
  • 綁定第二個 authenticator。
  • Passkey / security key 作為備援。
  • 已登入可信裝置重新驗證。
  • 高風險帳號走人工審核。

危險備援方式:

  • 只靠 email 一鍵關閉 MFA。
  • 只靠客服問生日。
  • 只靠安全問題。
  • 客服可以無稽核重設 MFA。

TOTP 的安全強度取決於恢復流程。

如果恢復流程很弱,攻擊者不需要破解 TOTP,只要走「我手機不見了」。

十五、 常見錯誤

1. Secret 明文保存

錯誤:

text
totp_secret = JBSWY3DPEHPK3PXP

正確:

text
encrypted_secret = encrypt(kms_key, totp_secret)

2. 產生 secret 後立刻啟用

錯誤:

text
使用者打開設定頁就已啟用 TOTP。

正確:

text
必須先輸入一組正確 TOTP,確認 app 已保存 secret 後才啟用。

3. window 開太大

錯誤:

text
接受前後 10 分鐘的 TOTP。

正確:

text
通常只接受前後一個 30 秒窗口。

4. 沒有速率限制

錯誤:

text
TOTP 可以無限嘗試。

正確:

text
限制每個 challenge、user、IP 的嘗試次數。

5. 不處理重放

錯誤:

text
同一時間窗口內同一 code 可以被多次用於敏感操作。

正確:

text
高風險場景記錄 last_used_counter,避免同一 counter 重放。

6. 以為 TOTP 防釣魚

錯誤:

text
有 TOTP 就不怕 phishing。

正確:

text
TOTP 仍可被即時釣魚代理轉送;釣魚抗性要靠 WebAuthn / Passkey。

十六、 面試追問題庫

Q1:TOTP 為什麼離線也能產生 code?

因為 app 和伺服器事先共享同一個 secret,之後雙方都用 secret 和目前時間根據同一算法計算 code。app 不需要向伺服器請求最新驗證碼。

Q2:TOTP 和 HOTP 差在哪?

HOTP 使用遞增 counter,TOTP 使用時間 counter。TOTP 可以理解成把 HOTP 的 counter 換成 floor(currentTime / period)

Q3:為什麼 TOTP 常見是 30 秒?

30 秒是在安全和體驗之間的折衷。太短使用者來不及輸入,太長 code 可被利用時間變長。伺服器通常會允許前後一個時間窗口來容忍時鐘偏移。

Q4:TOTP secret 可以 hash 保存嗎?

通常不行。伺服器需要原始 secret 來重新計算 TOTP,因此必須可取回 secret。安全做法是加密保存 secret,而不是存明文,也不是只存不可逆 hash。

Q5:TOTP 能防釣魚嗎?

不能完整防。攻擊者可以用即時釣魚代理騙使用者輸入 TOTP,再轉送到真正網站。TOTP 可以降低密碼外洩風險,但 phishing-resistant MFA 應使用 WebAuthn / Passkey。

Q6:為什麼啟用 TOTP 要先驗一組 code?

因為伺服器要確認使用者真的已經把 secret 加到 authenticator app,而且 app 能算出正確 code。否則使用者可能以為已啟用,實際上沒有備援,或之後無法登入。

Q7:如何處理使用者換手機?

應提供 backup codes、第二個 authenticator、Passkey / security key 或安全帳號恢復流程。不能只靠 email 一鍵關閉 TOTP,否則 TOTP 的安全性會被恢復流程削弱。

十七、 實作題

題目一:設計 TOTP 啟用流程

請描述:

  1. 使用者已登入。
  2. 要求近期重新驗證。
  3. 建立 pending authenticator。
  4. 產生 shared secret。
  5. 回傳 otpauth URI / QR code。
  6. 使用者輸入第一組 TOTP。
  7. 驗證成功後啟用。
  8. 產生 backup codes。
  9. 寄通知並寫 audit log。

題目二:設計 TOTP 驗證

要求:

  • 支援 30 秒 period。
  • 支援前後一個 window。
  • 限制嘗試次數。
  • 記錄 last_used_at。
  • 高風險場景防止 counter replay。
  • 錯誤訊息不透露過多細節。

題目三:設計 TOTP secret 儲存

請說明:

  • secret 不能明文保存。
  • secret 不能只存 hash。
  • 應使用 KMS 或 application-level encryption。
  • 不寫入 log。
  • 啟用後不再顯示 QR code。
  • 重設要走 re-authentication 和 audit log。

十八、 資深視角

1. TOTP 的秘密在兩端

TOTP 的 shared secret 同時存在:

  • 使用者 authenticator app。
  • 你的伺服器。

所以你要保護兩端。

伺服器端要加密保存 secret;使用者端要提供備援與恢復策略。

2. TOTP 是好的第二因素,但不是終點

TOTP 比 SMS 更不依賴電信通道,也不需要伺服器發送 code。

但它仍可能被:

  • 即時釣魚代理。
  • 使用者裝置惡意軟體。
  • secret QR code 外洩。
  • 弱恢復流程。

所以高風險系統會逐步引導到:

  • Passkey。
  • WebAuthn security key。
  • phishing-resistant MFA。

3. MFA 的難點在生命週期

TOTP 算法本身不複雜。

真正難的是:

  • 如何安全啟用。
  • 如何安全保存 secret。
  • 如何處理換手機。
  • 如何防止客服旁路。
  • 如何做 backup codes。
  • 如何對敏感操作要求 fresh MFA。
  • 如何記錄與通知。

這也是面試中能拉開層次的地方。

4. 不要自己造密碼學輪子

理解算法是必要的。

但生產環境中,TOTP 應使用成熟、維護良好的 library,並把工程重點放在:

  • secret lifecycle。
  • rate limiting。
  • session assurance。
  • recovery。
  • audit log。
  • user experience。

總結

問題回答重點
TOTPTime-Based One-Time Password
標準RFC 6238,建立在 HOTP / RFC 4226 上
核心shared secret + time counter + HMAC
常見設定6 位數、30 秒、HMAC-SHA1
啟用產生 secret、QR code、驗第一組 code 後啟用
驗證窗口通常接受前後一個時間窗口
Secret 保存需可取回,應加密保存
重放防護高風險場景記錄 last_used_counter
主要風險secret 外洩、裝置遺失、即時釣魚代理
備援backup codes、第二 authenticator、Passkey/security key

一句完整的面試回答可以是:

TOTP 是 Time-Based One-Time Password,定義在 RFC 6238,建立在 HOTP 的 HMAC-based OTP 上。啟用時伺服器產生一個隨機 shared secret,透過 otpauth URI 或 QR code 交給 Authenticator app;之後 app 和伺服器都用同一個 secret,加上目前時間切出的 time counter,例如每 30 秒一格,用 HMAC 計算並截斷成 6 位數 code。驗證時伺服器會用保存的 secret 計算目前時間窗口,通常接受前後一個窗口以容忍時鐘偏移,並限制嘗試次數。因為伺服器需要重新計算 code,TOTP secret 不能只存 hash,應加密保存;啟用時要先驗一組 code 才真正啟用,並提供 backup codes。TOTP 比 SMS 更不依賴電信通道,但仍可能被即時釣魚代理轉送,所以高風險系統可再升級到 WebAuthn / Passkey。

延伸閱讀