動態密碼算法介紹與實現

動態密碼,亦稱一次性密碼(One Time Password, 簡稱 OTP),是一種高效簡單又比較安全的密碼生成算法,在咱們的生活以及工做中隨處可見,身爲開發者,也或多或少在本身的業務系統中集成了二步驗證機制,那麼,技術運用,既要知其然,更要知其因此然,動態密碼算法是怎樣的?html

讀前指引

  • 經過這篇文章,你能夠了解如下知識:git

    • 動態密碼的背景知識github

    • 動態密碼的分類算法

    • 不一樣動態密碼的生成算法,HOTP 以及 TOTP編程

    • HOTP 以及 TOTP 的簡單的 Ruby 編程語言的實現安全

    • 兩類算法各自注意事項ruby

  • 限於篇幅,我不會討論如下幾點,有興趣的同窗能夠參考我文章末尾給出的參考資料瞭解:服務器

    • 不一樣動態密碼的安全性分析網絡

    • 計時動態密碼如何確保有效期間內,密碼不被二次使用編程語言

動態密碼背景介紹

從個人角度理解,動態密碼是指隨着某一事件(密碼被使用、必定的時間流逝等)的發生而從新生成的密碼,由於動態密碼自己最大優勢是防重複執行攻擊(replay attack),它能很好地避免相似靜態密碼可能被暴力破解等的缺陷,現實運用中,通常採用「靜態密碼+動態密碼」相結合的雙因素認證,咱們也稱二步驗證。

而動態密碼其實很早就出如今咱們的生活裏了,在移動支付發展起來以前,網銀是當時最爲流行的在線支付渠道,當時銀行爲了確保你們的網銀帳號支付安全,都會給網銀客戶配發動態密碼卡,好比中國銀行電子口令卡(按時間差定時生成新密碼,口令卡自帶電池,可保證連續使用幾年),或者工商銀行的電子銀行口令卡(網銀支付網頁每次生成不一樣的行列序號,用戶根據指定行列組合刮開密碼卡上的塗層獲取密碼,密碼使用後失效),又或者銀行強制要求的短信驗證碼,這些均可以歸入動態密碼的範疇。
中行電子口令卡
工行電子銀行口令卡

而隨着移動互聯網的發展以及移動設備的智能化的不斷提升,設備間的同步能力大幅提高,之前依賴獨立設備的動態密碼生成技術很快演變成了手機上的動態密碼生成軟件,以手機軟件的形式生成動態密碼的方式極大提升了動態密碼的便攜性,一個用戶一個手機就能夠管理任意多個動態密碼的生成,這也使得在網站上推進二步驗證減小了不少阻力,由於以往客戶可能由於使用口令卡太麻煩,而拒絕打開二步驗證機制,從而讓本身的帳號暴露在風險之下。最爲知名的動態密碼生成軟件,當屬 Google 的 Authenticator APP。
Google Authenticator

動態密碼算法探索之旅

動態密碼的分類

通常來講,常見的動態密碼有兩類:

  • 計次使用:計次使用的OTP產出後,可在不限時間內使用,知道下次成功使用後,計數器加 1,生成新的密碼。用於實現計次使用動態密碼的算法叫 HOTP,接下來會對這個算法展開介紹;

  • 計時使用:計時使用的OTP則可設定密碼有效時間,從30秒到兩分鐘不等,而OTP在進行認證以後即廢棄不用,下次認證必須使用新的密碼。用於實現計時使用動態密碼的算法叫 TOTP,接下來會對這個算法展開介紹。

在真正開展算法介紹以前,須要補充介紹的是:動態密碼的基本認證原理是在認證雙方共享密鑰,也稱種子密鑰,並使用的同一個種子密鑰對某一個事件計數、或時間值進行密碼算法計算,使用的算法有對稱算法、HASH、HMAC等。記住這一點,這個是全部動態密碼算法實現的基礎。

HOTP

HOTP 算法,全稱是「An HMAC-Based One-Time Password Algorithm」,是一種基於事件計數的一次性密碼生成算法,詳細的算法介紹能夠查看 RFC 4226。其實算法自己很是簡單,算法自己能夠用兩條簡短的表達式描述:

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
PWD(K,C,digit) = HOTP(K,C) mod 10^Digit

上式中:

  • K 表明咱們在認證服務器端以及密碼生成端(客戶設備)之間共享的密鑰,在 RFC 4226 中,做者要求共享密鑰最小長度是 128 位,而做者自己推薦使用 160 位長度的密鑰

  • C 表示事件計數的值,8 字節的整數,稱爲移動因子(moving factor),須要注意的是,這裏的 C 的整數值須要用二進制的字符串表達,好比某個事件計數爲 3,則C是 "11"(此處省略了前面的二進制的數字0)

  • HMAC-SHA-1 表示對共享密鑰以及移動因子進行 HMAC 的 SHA1 算法加密,獲得 160 位長度(20字節)的加密結果

  • Truncate 即截斷函數,後面會詳述

  • digit 指定動態密碼長度,好比咱們常見的都是 6 位長度的動態密碼

Truncate 截斷函數

因爲 SHA-1 算法是既有算法,不是咱們討論重點,故而 Truncate 函數就是整個算法中最爲關鍵的部分了。如下引用 Truncate 函數的步驟說明:

DT(String) // String = String[0]...String[19]

Let OffsetBits be the low-order 4 bits of String[19]
Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15
Let P = String[OffSet]...String[OffSet+3]
Return the Last 31 bits of P

結合上面的公式理解,大概的描述就是:

  1. 先從第一步經過 SHA-1 算法加密獲得的 20 字節長度的結果中選取最後一個字節的低字節位的 4 位(注意:動態密碼算法中採用的大端(big-endian)存儲);

  2. 將這 4 位的二進制值轉換爲無標點數的整數值,獲得 0 到 15(包含 0 和 15)之間的一個數,這個數字做爲 20 個字節中從 0 開始的偏移量;

  3. 接着從指定偏移位開始,連續截取 4 個字節(32 位),最後返回 32 位中的後面 31 位。

回到算法自己,在得到 31 位的截斷結果以後,咱們將其又轉換爲無標點的大端表示的整數值,這個值的取值範圍是 0 ~ 2^31,也即 0 ~ 2.147483648E9,最後咱們將這個數對10的乘方(digit 指數範圍 1-10)取模,獲得一個餘值,對其前面補0獲得指定位數的字符串。

代碼示例

如下代碼示例也可訪問 Gist 獲取。

require 'openssl'

def hotp(secret, counter, digits = 6)
  hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), secret, int_to_bytestring(counter))  # SHA-1 算法加密
  "%0#{digits}i" % (truncate(hash) % 10**digits)  # 取模獲取指定長度數字密碼
end

def truncate(string)
  offset = string.bytes.last & 0xf           # 取最後一個字節
  partial = string.bytes[offset..offset+3]   # 從偏移量開始,連續取 4 個字節
  partial.pack("C*").unpack("N").first & 0x7fffffff    # 取後面 31 位結果後獲得整數
end

def int_to_bytestring(int, padding = 8)
  result = []
  until int == 0
    result << (int & 0xFF).chr
    int >>= 8
  end
  result.reverse.join.rjust(padding, 0.chr)
end

上面的算法實現代碼量不多,核心都是按照算法描述進行多個掩碼運算跟位操做而已。

密碼失效機制

從上面的分析能夠看到,一個動態密碼的生成,取決於共享密鑰以及移動因子的值,而共享密鑰是保持不變的,最終就只有移動因子決定了密碼的生成結果。因此在 HOTP 算法中,要求每次密碼驗證成功後,認證服務器端以及密碼生成器(客戶端)都要將計數器的值加1,已確保獲得新的密碼。

可是在這裏就會引入一個問題,假如認證服務器端與密碼生成器之間因爲通訊故障或者其餘意外狀況,致使兩邊計數器的值不一樣步了,那麼就會致使兩邊生成的密碼沒法正確匹配。爲了解決這個問題,算法在分析中建議認證服務器端在驗證密碼失敗後,能夠主動嘗試計數器減1以後從新生成的新密碼是否與客戶端提交密碼一致,若是是,則能夠認定是客戶端計數器未同步致使,這種狀況下能夠經過驗證,而且要求客戶端從新同步計數器的值。

出了上面提到的計數器不一樣步的問題,我另外想的是,若是客戶有多個密碼生成器(假設 iPad 和 iPhone)爲同個帳號生成密碼,那麼計數器在多個設備間的同步可能就須要另外考慮的方案了。

小結

其實 HOTP 的算法比我在閱讀算法前所想象的要簡潔得多,並且仍然足夠強健。算法自己巧妙利用了加密算法對共享密鑰和計數器進行加密,確保這兩個動態密碼生成因子不被篡改,接着經過一個 truncate 函數隨機獲得一個最長 10 位的 10 進制整數,最終實現對 1 - 10 位長度動態密碼的支持。算法自己的簡潔也確保了算法自己能夠在各類設備上實現。

TOTP

TOTP 算法,全稱是 TOTP: Time-Based One-Time Password Algorithm,其基於 HOTP 算法實現,核心是將移動因子從 HOTP 中的事件計數改成時間差。完整的 TOTP 算法的說明能夠查看 RFC 6238,其公式描述也很是簡單:

TOTP = HOTP(K, T) // T is an integer
and represents the number of time steps between the initial counter
time T0 and the current Unix time

More specifically, T = (Current Unix time - T0) / X, where the
default floor function is used in the computation.

一般來講,TOTP 中所使用的時間差都是當前時間戳,TOTP 將時間差除以時間窗口(密碼有效期,默認 30 秒)獲得時間窗口計數,以此做爲動態密碼算法的移動因子,這樣基於 HOTP 算法就能方便獲得基於時間的動態密碼了。

代碼示例

如下代碼示例也可訪問 Gist 獲取。

require 'hotp'

def totp(secret, digits = 6, step = 30, initial_time = 0)
  steps = (Time.now.to_i - initial_time) / step

  hotp(secret, steps, digits)
end

看到了吧,極其簡短的實現代碼!一個時間計數的動態密碼算法就此誕生,如此簡單的算法,倒是支撐多少業務系統安全運做的基石,很有四兩撥千斤的快感!

問題探討

  1. ROTP 算法中的主要問題是計數器的同步,而 TOTP 也不例外,只是問題在於服務器端與客戶端之間時間的同步,因爲如今互聯網的發達,加上移動設備通常都會按照網絡時間設置設備時間,基本上時間的相對同步都不是問題;

  2. 時間同步的另外一個問題實際上是邊界問題,假如客戶端生成密碼的時間恰好是第 29 秒,而因爲網絡延遲等緣由,服務器受理驗證時恰好是下一個時間窗口的第 1 秒,這個時候會致使密碼驗證失效。因而,TOTP 算法在其算法討論中,也建議服務器在驗證密碼失敗以後,能夠嘗試將自身的時間窗口值減 1 以後從新生成密碼比對,若是驗證經過,說明驗證不經過是時間窗口的邊界問題致使,這個時候能夠認爲密碼驗證經過。

  3. 基於時間的動態密碼的另外一個好處是避免了基於計數器的多設備間的計數器同步問題,由於每臺設備以及服務端均可以自行與網絡時間(共同時間標準)校準,而無需依賴服務端的時間。

Google Authenticator

在 Google Authenticator 的開源項目的 README 裏有明確提到:

These implementations support the HMAC-Based One-time Password (HOTP) algorithm specified in RFC 4226 and the Time-based One-time Password (TOTP) algorithm specified in RFC 6238.

也就是說,至此,咱們也明白了,其實 Google Authenticator 算法核心也是 HOTP 以及 TOTP ,在明白了整個動態密碼算法的核心以後,有沒有一種豁然開朗的感受呢?既知其然,又知其因此然了,對吧?每次看着應用定時生成密碼,

總結

這篇文章簡單介紹了兩類常見的動態密碼的生成算法,算法自己簡潔不復雜,效率而且足夠強健。這篇文章的目的是方便跟我同樣但願瞭解算法核心的小夥伴,而在 RFC 文檔中,仍有大量關於算法自己的安全性方面的探討,有興趣的小夥伴能夠去看一下。

參考資料

  1. Wikipedia: 一次性密碼

  2. github: rotp,HOTP 以及 TOTP 的算法實現以及其餘封裝

  3. 動態口令(OTP)認證技術概覽

  4. RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm

  5. RFC 6238 - TOTP: Time-Based One-Time Password Algorithm

示例源碼

Gist: OTP algorithms in Ruby

相關文章
相關標籤/搜索