一文搞懂 OTP 雙因素認證

GitHub 在 2023 年 3 月推出瞭雙因素認證(two-factor authentication)簡稱 2FA,並且承諾所有在 GitHub 上貢獻的開發者在 2023 年底前啟用雙因素認證。因此最近在訪問 GitHub 時如果註意的話經常會看到提示讓在 2023 年 10 月 12 日之前開啟,否則可能影響賬戶的使用:

如果之前沒瞭解過這個概念,我們在開啟之前不禁會想啥是雙因素認證呢?如果你也有同樣的疑問,那麼耐心看完本文就能比較好的理解原理和使用瞭。so,讓我們開始吧。

1.什麼是雙因素認證(2FA)

回想一下我們通常登錄網站的時候通常輸入用戶名、密碼就可以登錄瞭,也就是說網站是通過用戶名和密碼來確認你的身份,假如其他人或者黑客通過一些手段獲取到你的密碼,那麼他們就可以冒充你直接登錄網站獲取和你同等的服務,而網站對這一切並不知情,所以這個時候就需要借助於用戶名和密碼之外的其他方式來驗證你的身份,這種例子也非常多。

例如我們首次登錄支付寶、微信這類應用時需要填寫手機驗證碼。早期在銀行支付時需要使用銀行下發給你的 U 盾或動態口令牌來確保隻有你自己才可以支付,直到現在的指紋、刷臉支付其實都是在通過你持有的設備或你本身具有的生理特征來確保你就是你。

所以,所謂雙因素認證(2FA)就是在用戶名和密碼認證之外再添加一種確認身份的方式,加強對賬戶的保護,至於具體的方式就有非常多瞭。包括我們上面說的最常用的短信驗證碼、指紋、刷臉,還有硬件方式的 U 盾、口令牌等,還有我們今天要說的 OTP (One-Time Password),也就是一次性密碼的意思。上面這些方式都屬於雙因素認證,所以這個概念聽起來很牛,其實是比較好理解的。

2.什麼是 OTP

上面說瞭 OTP 全稱 One-Time Password,也就是一次性密碼,那麼下面我們介紹下 OTP 的實現原理。

OTP 的實現主要有兩種:

  1. HOTP(HMAC-Based One-Time Password Algorithm):基於 HMAC 的一次性密碼。
  2. TOTP(Time-Based One-Time Password Algorithm):基於時間戳的一次性密碼。

其中 HOTP 可以參考 RFC4226,TOTP 可以參考 RFC6238。

HOTP 出現的比較早,是基於 HMAC 實現的,其實 HMAC 就是 Hash + 消息或者說是 Hash + 鹽(salt)來實現的,其中 Hash 函數通常采用 MD5、SHA 系列(SHA1/SHA256/SHA512 等)的單向消息摘要算法來實現。在 HOTP 中,Hash 函數采用 SHA1,在消息或者鹽值部分放的是一個計數器,也就是一個大小為 8 字節的數字,主要的生成公式如下:

HOTP = Truncate(HMAC_{SHA1}(key, counter))

其中,key 表示對消息加密的密鑰,counter 就是計數器的值。然後兩者通過基於 SHA1 的 HMAC 算法計算出結果,所以結果長度是 20 字節,如果用 16 進制表示長度就是 40。由於結果太長瞭顯然不利於用戶輸入,因此通過 Truncate 函數進行處理,處理成 6-8 位的數字,就和短信驗證碼一樣,這樣用戶就可以很快的輸入並進行驗證。

上面隻是計算 HOTP 結果主要的概括,至於具體的細節,例如 key 是如何編碼的、counter 是怎麼處理的以及至關重要的 Truncate 又是怎麼工作的,這些下面會將,目前先不用關心,我們先有個整體的認識就可以瞭,如何計算並不影響具體的交互方式。

首先認證之前客戶端和服務端都需要約定相同的 key 也就是密鑰,而且這個密鑰決不能泄露,否則也就是失去瞭 2FA 的意義,同時客戶端和服務端也需要從相同的計數器值開始輪轉,比如都從 1 開始,要增加一塊增加,必須保證一致。也就是說 key 和 counter 必須都對起來,否則會驗證失敗,這個過程可以總結如下:

其實這個過程我們可以看出來一個比較明顯的問題,就是計數器值需要保持一致。假如起始的計數器值是同步的,每次服務端需要用戶輸入的時候都自增一次,這個時候客戶端打開生成 OTP 的客戶端也自增一次,那麼隻要這個時候不小心刷新瞭瀏覽器或者觸碰瞭手機的刷新,都會導致計數器不一致,從而動態密碼失效。但是如果每次服務器都返回一個計數器值,用戶還需要在手機端手動輸入一次,操作起來比較麻煩。

那麼這個時候我們會想,如何才能保證服務器和客戶端沒有任何交互就能拿到一個相同的計數器值呢?

可能我們這個時候很自然地會想到用時間,沒錯,時間就是一個天然的計數器,隻要客戶端和服務器能在時間上基本一致就可以,這個目前是比較容易保證的。事實上,TOTP 就是這麼來的,它將計數器替換為時鐘值,從而提供短暫的一次性密碼,而且安全性也比較理想,所以目前被互聯網廣泛采用,TOTP 的算法和 HOTP 完全一樣,隻是將計數器換成瞭時間因子,其餘的並沒有變化,所以可以把 TOTP 看成 HOTP 的一個變體,上面的公式可以修改如下:

TOTP = Truncate(HMAC_{SHA1}(key, T))

其中 T 就是時間因子,註意我們這裡說的是時間因子,可不可以直接用 Unix 時間戳呢?我們想一想,假如使用時間戳,單位是 s,就算服務端和客戶端時間嚴格一致,那麼我們輸入一次性密碼總歸需要幾秒的時間,這樣一來受限於人的大腦和肢體反應速度,幾乎是不可能登錄成功的。所以如果隻要能保證一次性密碼在很小的一段時間內不變就可以,這個時間能比較充足的保證認證過程即可,在 TOTP 的 RFC 中是使用瞭步長(X)的概念,其默認值是 30s:

begin{aligned} &X = 30 \ &T = frac{UnixTime}{X} end{aligned}

這樣相當於以 30s 為一個自然時間窗口,時間因子就表示當前是從 1970 年 1 月 1 日以來第幾個 30s,所以在每一個窗口內,時間因子是固定不變的,這樣就給用戶留出充足的時間來輸入一次性密碼,同時對服務器和客戶端的時間同步性要求也不會特別嚴格,時間差個幾秒也不太會影響認證,最多也就是多輸入一次,這樣就比較好地解決瞭 HOTP 帶來的計數器同步問題,所以結果換成瞭這樣:

那麼現在還有一個問題就是密鑰(key)是怎麼同步的,通常都是服務器生成一個唯一的密鑰,客戶端保存這個密鑰,之後雙方都是用這個密鑰來生成一次性密鑰。

首先在傳輸過程中肯定不能明文傳輸密鑰,目前大部分網站都支持 HTTPS 訪問,因此傳輸過程是安全的。其次就是在開啟雙因素認證時,必須是用戶本人操作,假如被別人冒充,那麼以後自己就再也無法登錄自己的賬戶瞭,所以網站有個前提就是認為在開啟雙因素認證時,一定是用戶本人操作的,為瞭提高安全性,也可以在開啟一種認證方式是采用其他的方式進行認證,比如開啟 HOTP 之前先驗證手機或者郵箱,這樣安全性會更高一些。

3.OTP 的計算過程

首先我們需要有一個密鑰,這個是由服務器生成,密鑰可以是一段隨機生成的字節流,由於二進制不方便觀察和輸入,所以服務器會對這段二進制進行 base32 編碼然後輸出,當前主流的客戶端都是支持以 base32 編碼作為密鑰輸入的,我們下面用 Python 來生成一段隨機密鑰:

import os
import base64

# 由於 base32 需要 8 字節對齊,長度是原字符串的 8/5,為瞭不浪費空間密鑰長度盡量是 5 的倍數
key = os.urandom(10)
encoded_key = base64.b32encode(key)
print(encoded_key)

赞(0)