Python One-Time Password 的好朋友 - pyotp

Posted on  Jul 27, 2019  in  Python 模組/套件推薦  by  Amo Chen  ‐ 3 min read

隨著資訊安全越來越受重視,也越來越多網站鼓勵用戶啟用 2FA(two-factor authentication) 或 MFA(multi-factor authentication) 以增加帳號的安全性。

而這些網站通常都會推薦用戶使用 Google Authenticator 作為 2FA / MFA 的 APP 。

因此對於開發者而言,如果要實作 2FA / MFA 功能,只要有能夠同時滿足產生 One-Time Password 與方便整合 Google Authenticator 的套件,就是最幸福的事!

如果你是 Python 的開發者,那麼推薦你使用 pyotp !

本文環境

  • Python 3.6.5
  • pyotp 2.2.7
$ pip install pyotp==2.2.7

pyotp

pyotp 主要是用來產生與驗證一次性密碼(OTP, One-Time Password)的 Python 函式庫,很適合作為開發 2FA / MFA 功能時使用。

PyOTP is a Python library for generating and verifying one-time passwords.

目前 pyotp 提供 2 種 one-time password 可供使用:

  • HOTP (HMAC-Based One-Time Password)
  • TOTP (Time-Based One-Time Password)

簡而言之,這些 OTP 的運作都是由使用者端與伺服器端共享幾個機密的資料,例如密鑰、隨機數字等等,由使用者產生利用這些共享的機密資料,透過相同演算法產生一次性密碼之後,將一次性密碼送到伺服器端,由伺服器端利用相同的機密資料看能否產生出一樣的一次性密碼進行驗證。

HOTP

HOTP 又被稱為 count-based one-time password ,一般會被運用在 Smart Card 這類裝置中 。HOTP 的產生原理主要是由 HMAC 演算法(例如其中 1 種 HMAC-SHA-1)所產生的雜湊(hash)值中組合出一組密碼,詳見 RFC4226

HMAC-SHA-1 的範例:

import hmac
from hashlib import sha1


def hmac_sha1(key_bytes, code_bytes):
    return hmac.new(key_bytes, code_bytes, sha1).hexdigest()

上述範例的 key_bytes 是使用者與伺服器端都共享的密鑰(secret) ,所以必須確保該密鑰只有使用者本人與伺服器端擁有,而 code_bytes 在 HOTP 中是一個隨機數,也就是所謂的 counter ,同樣存在於使用者與伺服器端,每當 HOTP 驗證成功之後就需要遞增 +1 該 counter 以避免重現攻擊(replay attack) 。

但同時由於使用者端可能重新產生 OTP ,導致其所擁有的 counter 遞增了好幾次,造成伺服器端所記錄的 counter 與使用者端的 counter 不一致,所以伺服器端的驗證通常會設置一個 look-ahead window n ,只要使用者傳送上來的密碼與 counter + 1counter + n 內的任一結果相同就可以通過驗證(並將伺服器端的 counter 更新為正確的 counter 以同步使用者端與伺服器端的 counter)。

此外也應設置重試次數的上限,當使用者 OTP 輸入失敗 x 次之後就凍結使用者帳號,避免 OTP 被不斷重試直到被猜中為止。

大致說明 HOTP 原理之後,就來看看 pyotp 怎麼使用 HOTP:

hotp = pyotp.HOTP('base32secret3232')  # base32secret3232 是共享的密鑰

counter = 0
hotp.at(counter) # 產生 OTP '260182'

counter = 1
hotp.at(counter) # 產生 OTP '055283'

counter = 1401
hotp.at(counter) # 產生 OTP '316439'

# 伺服器端驗證方法
hotp = pyotp.HOTP('base32secret3232')  # base32secret3232 是共享的密鑰

counter = 1401
hotp.verify('316439', counter) # 驗證通過 True

counter = 1402
hotp.verify('316439', counter) # 驗證失敗 False

看起來十分簡單吧!上述範例的 base32secret3232 是使用者與伺服器端共享的密鑰,千萬別讓所有使用者都使用相同的密鑰,這將造成嚴重的資安問題。

如果想為使用者產生密鑰, pyotp 也提供以下方法輕鬆產生:

import pyotp


pyotp.random_base32() 

TOTP

相較於 HOTP 而言, TOTP 則又更直覺一些, TOTP 將 HOTP 共享的隨機數改為由 timestamp (以秒為單位) 產生,為保證在某段時間內(目前建議為 30 秒)產生的 OTP 都是相同的,所以設置一個 time step in seconds ,將 timestamp 除以 time step 用以取代 HOTP 中隨機數,剩餘的一次性密碼產生過程就與 HOTP 相同,詳見 RFC6328

TOTP 產生隨機數的範例:

import time

from datetime import datetime


time_steps = int(
	time.mktime(datetime.utcnow().timetuple()) / 30
)

而 pyotp 產生 TOTP 也同樣地簡單:

import pyotp


totp = pyotp.TOTP('base32secret3232')
totp.now() # 產生 OTP '492039'


# OTP verified for current time
totp.verify('492039') # 驗證 OTP
time.sleep(30)
totp.verify('492039') # 驗證 OTP 因超過 30 秒被視為驗證未通過

結合 Google Authenticator

pyotp 更提供相容 Google Authenticator 的 URI ,讓我們能夠輕鬆整合 Google Authenticator:

>>> import pyotp
>>> pyotp.totp.TOTP('base32secret3232').provisioning_uri('[email protected]', issuer_name='YourApp')
'otpauth://totp/YourApp:user%40example.com?secret=base32secret3232&issuer=YourApp'

最後,我們只要將 'otpauth://totp/YourApp:user%40example.com?secret=base32secret3232&issuer=YourApp' 交給專門產生 QR code 的套件即可。

後話

由於 HOTP, TOTP 都需要傳輸機敏資料到使用者端,因此利用其實作 2FA / MFA 時都要確保與使用者之間的傳輸通道是安全可靠的,避免有心人士從中竊聽就能夠取得使用者的密鑰,這也是呼應 pyotp 文件上的善意提醒:

Ensure transport confidentiality by using HTTPS

以上, Happy Coding!

References

https://github.com/pyauth/pyotp

https://tools.ietf.org/html/rfc4226

https://tools.ietf.org/html/rfc6238

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!