TOTP 實作原理
TOTP 是很多人最熟悉的第二因素:
打開 Google Authenticator / Microsoft Authenticator / 1Password
看到一組 6 位數驗證碼
30 秒後換下一組它看起來像是 app 從伺服器拿到最新驗證碼。
但其實不是。
TOTP 的關鍵是:
伺服器和使用者的 Authenticator app 事先共享同一把 secret,
之後雙方都用「目前時間」和這把 secret 算出同一組 code。所以 Authenticator app 離線也能產生驗證碼。
這也是 TOTP 和 SMS / Email OTP 最大的差別之一:
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。
短一點可以說:
TOTP = shared secret + current time + HMAC -> 6 位數驗證碼。二、 TOTP 和 HOTP 的關係
TOTP 來自 HOTP。
HOTP 是 HMAC-Based One-Time Password,定義在 RFC 4226。
HOTP 的輸入是:
shared secret + counterTOTP 把 counter 換成時間:
shared secret + time counter也就是:
HOTP(K, C)
TOTP(K, T)其中:
| 符號 | 意義 |
|---|---|
| K | shared secret |
| C | counter |
| T | time counter |
TOTP 的 time counter 通常是:
T = floor((currentUnixTime - T0) / X)常見設定:
T0 = 0
X = 30 秒也就是每 30 秒切一格。
三、 TOTP 流程總覽
啟用 TOTP:
登入驗證:
四、 Shared Secret 是什麼
TOTP 的 shared secret 是伺服器和 authenticator app 都知道的一段隨機秘密。
例如 Base32 表示:
JBSWY3DPEHPK3PXP啟用時通常用 QR code 傳給 app,QR code 內容是 otpauth URI:
otpauth://totp/Hirimu:alice@example.com
?secret=JBSWY3DPEHPK3PXP
&issuer=Hirimu
&algorithm=SHA1
&digits=6
&period=30參數:
| 參數 | 說明 |
|---|---|
| secret | shared secret,通常 Base32 |
| issuer | 服務名稱 |
| label | app 裡顯示的帳號 |
| algorithm | HMAC 演算法,常見 SHA1 |
| digits | code 位數,常見 6 |
| period | 時間步長,常見 30 秒 |
這個 QR code 很敏感。
任何人掃到它,就能產生使用者的 TOTP code。
所以啟用流程要注意:
- QR code 只在近期重新驗證後顯示。
- secret 先 pending,驗證成功才啟用。
- 不要把 secret 寫進 log。
- 不要讓 QR code 被快取。
- 不要在啟用後再次顯示原 secret。
五、 TOTP 怎麼算
簡化流程:
1. 取得目前 Unix time。
2. 除以 30 秒,得到 time counter。
3. 用 shared secret 對 time counter 做 HMAC。
4. 從 HMAC 結果做 dynamic truncation。
5. 取 modulo 10^digits,得到 6 位數 code。概念:
timeCounter = floor(currentUnixTime / 30)
hmac = HMAC-SHA1(secret, timeCounter)
number = dynamicTruncate(hmac)
code = number mod 1_000_000Node.js 簡化範例:
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 秒是常見預設。
搭配前後時間窗口,實際可接受範圍可能是:
前一格、目前格、下一格也就是最多約 90 秒內某些 code 可能被接受。
所以你不能只說:
TOTP 只有 30 秒有效。實務上還要看 server 接受幾個 window。
七、 驗證窗口與時鐘偏移
使用者手機和伺服器時間可能不完全一致。
所以伺服器通常允許少量偏移:
window = 1表示接受:
- 前一個 time step。
- 目前 time step。
- 下一個 time step。
驗證概念:
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,攻擊者猜中的機率越高,可重放時間也越長。
常見做法:
window = 1若使用硬體 token 或特殊場景,可另外處理 clock drift,但一般 app 類 TOTP 不應無限制放寬。
八、 重放防護
TOTP 的 code 在同一時間窗口內會保持不變。
例如 30 秒內都是:
839204如果使用者已經用這個 code 登入成功,攻擊者在同一個時間窗口內重放同一 code,理論上可能成功。
所以高風險系統可以記錄:
last_used_counter驗證成功後:
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 個時間窗口,單次猜中機率約是:
3 / 1,000,000看起來很低,但如果沒有速率限制,就會出問題。
限制應該包含:
- 每個 user / authenticator 的嘗試次數。
- 每個 session 的嘗試次數。
- 每個 IP 的嘗試頻率。
- 登入失敗累積風險。
- 多次失敗後要求重新輸入密碼。
例如:
同一 MFA challenge 最多嘗試 5 次。
同一 user 5 分鐘最多 10 次。
同一 IP 1 分鐘最多 30 次。錯誤訊息不要太精準:
驗證碼錯誤或已過期。避免讓攻擊者知道是時間偏移、counter replay,還是 code 猜錯。
十、 Secret 怎麼保存
這是 TOTP 很重要的地方。
伺服器必須能重新計算 TOTP,所以不能只存 hash。
它需要存取原始 secret,或能解密得到原始 secret。
常見做法:
encrypted_secret = encrypt(kms_key, totp_secret)資料庫存:
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 的安全性取決於:
使用者裝置保護 secret;
伺服器也保護 secret。任一邊 secret 外洩,TOTP 就失效。
十一、 啟用流程
TOTP 啟用流程應該有 pending 狀態。
不要一產生 secret 就直接啟用。
比較好的流程:
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。
範例:
otpauth://totp/Hirimu:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Hirimu&digits=6&period=30建議:
- issuer 使用產品名稱。
- label 包含產品和帳號,方便使用者辨識。
- 不要在 label 放過多敏感資料。
- secret 使用足夠隨機性。
- QR code 頁面禁止快取。
- 顯示手動輸入 secret 時要提醒保存風險。
HTTP header 可考慮:
Cache-Control: no-store十三、 TOTP 和 SMS OTP 比較
| 比較 | SMS OTP | TOTP |
|---|---|---|
| code 來源 | 伺服器產生後送出 | app 和伺服器各自計算 |
| 是否需要網路 | 需要 SMS 通道 | app 可離線 |
| 成本 | 每封簡訊成本 | 幾乎無發送成本 |
| 主要風險 | SIM swap、攔截、電信商風險 | secret 外洩、釣魚代理、裝置遺失 |
| 使用體驗 | 不用安裝 app,但可能延遲 | 需安裝或使用密碼管理器 |
| 釣魚抗性 | 不具備 | 不具備完整釣魚抗性 |
TOTP 通常比 SMS OTP 更適合作為第二因素。
但它仍不是最強 MFA。
即時釣魚代理仍可以騙使用者輸入 TOTP,再轉送到真網站。
若要更強釣魚抗性,要看 WebAuthn / Passkey。
十四、 裝置遺失與備援
TOTP 最大的產品問題是:
使用者換手機或手機遺失怎麼辦?安全備援方式:
- Backup codes。
- 綁定第二個 authenticator。
- Passkey / security key 作為備援。
- 已登入可信裝置重新驗證。
- 高風險帳號走人工審核。
危險備援方式:
- 只靠 email 一鍵關閉 MFA。
- 只靠客服問生日。
- 只靠安全問題。
- 客服可以無稽核重設 MFA。
TOTP 的安全強度取決於恢復流程。
如果恢復流程很弱,攻擊者不需要破解 TOTP,只要走「我手機不見了」。
十五、 常見錯誤
1. Secret 明文保存
錯誤:
totp_secret = JBSWY3DPEHPK3PXP正確:
encrypted_secret = encrypt(kms_key, totp_secret)2. 產生 secret 後立刻啟用
錯誤:
使用者打開設定頁就已啟用 TOTP。正確:
必須先輸入一組正確 TOTP,確認 app 已保存 secret 後才啟用。3. window 開太大
錯誤:
接受前後 10 分鐘的 TOTP。正確:
通常只接受前後一個 30 秒窗口。4. 沒有速率限制
錯誤:
TOTP 可以無限嘗試。正確:
限制每個 challenge、user、IP 的嘗試次數。5. 不處理重放
錯誤:
同一時間窗口內同一 code 可以被多次用於敏感操作。正確:
高風險場景記錄 last_used_counter,避免同一 counter 重放。6. 以為 TOTP 防釣魚
錯誤:
有 TOTP 就不怕 phishing。正確:
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 啟用流程
請描述:
- 使用者已登入。
- 要求近期重新驗證。
- 建立 pending authenticator。
- 產生 shared secret。
- 回傳 otpauth URI / QR code。
- 使用者輸入第一組 TOTP。
- 驗證成功後啟用。
- 產生 backup codes。
- 寄通知並寫 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。
總結
| 問題 | 回答重點 |
|---|---|
| TOTP | Time-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。