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 + 1
… counter + 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