手機上那組每 30 秒變一次的 6 位數,是怎麼算出來的?TOTP 白話

登入時要輸入的那組動態驗證碼(TOTP)背後其實不連網、不用簡訊。這篇白話拆解它怎麼運作,以及我在診所系統裡怎麼設計鎖定與時鐘容錯。

#資安#totp#mfa#authentication#教學

重點摘要

很多服務登入時,除了密碼還要你打一組「手機驗證器上每 30 秒換一次的 6 位數」。這個東西叫 TOTP。神奇的是:你的手機和伺服器沒有連線,卻能算出同一組數字。這篇講它為什麼做得到、以及我在診所系統 ExClinCalc 裡怎麼設計它的鎖定策略和時鐘容錯。


先分清楚:MFA 為什麼有用

MFA(多因素驗證)的核心概念是:要求「兩種不同類型」的證明。常見三類:

  • 你知道的(密碼)
  • 你擁有的(手機、實體金鑰)
  • 你本身的(指紋、臉)

密碼只是第一類。問題是密碼會外洩、會被猜、會被重複用。加上第二類(你擁有的手機)之後,攻擊者光偷到密碼還不夠 —— 他還得偷到你的手機。TOTP 就是用手機當「你擁有的」那個因素。


TOTP 怎麼在「不連網」的情況下對得上

這是最反直覺、也最漂亮的部分。手機驗證器不連網、不收簡訊,卻能跟伺服器算出同一組碼。祕密在於它們共享一個只有彼此知道的密鑰,再各自用「同一個時間」去算。

流程是這樣:

綁定時(只做一次):
  伺服器產生一把隨機密鑰 → 用 QR code 給你的手機掃 → 雙方都存起來

每次要驗證時:
  6 位數 = 某個函數(共享密鑰, 現在的時間)
  • 共享密鑰:綁定時就存進手機,之後不再傳輸。
  • 現在的時間:雙方都看得到(時間是公開的),通常取「現在時間 ÷ 30 秒」當作一個計數。

因為兩邊用同一把密鑰 + 同一個時間丟進同一個函數,自然算出同一組 6 位數。時間每過 30 秒,算出來的就換一組。全程不需要連線。

(背後的函數是 HMAC 雜湊,把密鑰和時間計數算成一串,再截成 6 位數。標準定義在 RFC 6238。)


為什麼不用簡訊驗證碼就好?

簡訊 OTP 看起來更簡單,但它有幾個實際問題:

  • 簡訊會被攔截 / SIM 卡會被盜轉(SIM swapping)。
  • 簡訊要成本、要連線、可能延遲
  • 簡訊依賴電信商這個第三方

TOTP 純本地計算,沒有這些問題。代價是使用者要裝一個驗證器 App、要完成綁定 —— 這個門檻換來的是明顯更高的安全性。


我在 ExClinCalc 裡的兩個設計決定

把 TOTP 接進真實系統,有兩個「魔鬼在細節」的地方:

決定 1:時鐘容錯(±1 個時間窗)

手機和伺服器的時鐘不會分毫不差。如果差了幾秒、剛好跨過 30 秒的邊界,算出來的碼就對不上、驗證失敗。

對策:伺服器驗證時,不只比對「現在這個時間窗」,也接受「前一個和後一個時間窗」(±1)。這樣輕微的時鐘偏差不會誤擋使用者。初版沒做這個,結果時鐘差 30 秒就一直失敗,debug 才發現。

決定 2:鎖定策略(鎖 user、不鎖 IP)

為了防止有人狂試驗證碼(暴力破解),要設「失敗幾次就鎖」。但鎖什麼是關鍵:

  • 鎖 IP 的問題:攻擊者換 IP 就繞過;而且同一個診所的人可能共用 IP,一個人打錯害大家被鎖。
  • 我的選擇:鎖帳號 —— 5 次失敗鎖定該 user 30 分鐘。攻擊者針對某個帳號猛試會觸發鎖定;而因為鎖的是帳號,攻擊者沒有密碼也無法藉此鎖別人(要先過密碼那關才會進到 TOTP)。

誠實說:目前的缺口

TOTP 有一個現實痛點:手機掉了怎麼辦?

我目前的實作還沒有 backup code(備援碼)機制。如果使用者換手機又沒先解綁,會需要管理員介入重設。這是我明確知道的缺口,未來方向是加一組一次性備援碼 + magic link 救援流程。

我寧願先誠實標記這個限制,也不假裝它已經完善 —— 這跟我做整套系統的態度一致:講得清楚自己防了什麼、還缺什麼。


延伸閱讀