MOOSSin
選單

全部文章 · 開源工具

argon2-cffi 和標準函式庫的 PBKDF2:密碼雜湊該選哪個,取決於雜湊被拿到之後會發生什麼

argon2 是 2015 年密碼雜湊競賽的優勝者,記憶體硬度讓 GPU 破解變貴;PBKDF2 是 Python 內建、二十年老、仍在標準裡的做法。這篇講兩者的參數、各自的代價,以及我在什麼條件下允許自己用後者。先從「為什麼不能直接存密碼」講起,因為真的有人問過。

有一次系上的同學做網站專題,很得意地跟我展示他的登入功能。我問他密碼怎麼存的,他說「就存在資料庫裡啊」。我問存的是什麼,他說「密碼啊」。我沉默了大概三秒,他說「怎麼了嗎」。

沒有怎麼了,只是這三秒裡我腦中閃過了一整段網路安全史。儲存密碼永遠不能存明文,也不能存單純的 SHA-256,這兩件事現在沒有爭議。有爭議的是雜湊演算法選哪個。我在兩個小專案裡各用了一種:argon2-cffi 和標準函式庫的 PBKDF2。這篇把兩者攤開。

為什麼不能直接存,也不能只用 SHA-256

先補一下那位同學缺的課。資料庫總有一天可能外洩,存明文等於把所有人的密碼印成海報。那用 SHA-256 雜湊過再存呢?問題是 SHA-256 太快了,快到一張顯卡一秒可以算幾十億次。攻擊者拿到雜湊,拿一本常見密碼字典逐個算,幾分鐘就能對出大部分人的密碼。

密碼雜湊演算法的設計目標剛好相反:要慢,而且要慢得可以調。你登入的時候算一次慢個幾十毫秒沒感覺,攻擊者要算幾十億次就會很有感覺。這就像門鎖,不是為了讓你自己進不去,是為了讓別人試一萬把鑰匙的時候試到天亮。

argon2:目前的首選

argon2 在 2015 年贏得密碼雜湊競賽,2021 年成為 RFC 9106。它有三個參數:記憶體用量、迭代次數、平行度。記憶體硬度是重點,攻擊者用 GPU 或 ASIC 平行破解時,每一條路都要佔一份記憶體,成本上不去。用比喻說,PBKDF2 是要求每個試鑰匙的人跑一圈操場,argon2 是要求每個人跑一圈操場而且要自己扛一張桌子,你可以找一萬個人來跑,但你找不到一萬張桌子。三個變體裡 argon2id 是建議的預設,兼顧抗側通道和抗 GPU。

argon2-cffi 是 Python 上最常用的綁定,MIT 授權。PasswordHasher() 一個物件把參數版本、鹽、編碼格式全包好,hash() 和 verify() 兩個方法,check_needs_rehash() 告訴你舊雜湊是不是用過時的參數做的,該不該在下次登入時升級。API 乾淨到幾乎沒有可以寫錯的地方,這對密碼相關的程式碼是很大的優點,因為這種地方寫錯了不會有紅字,只會有幾年後的新聞。

代價是它帶著 C 擴充。底下是 argon2-cffi-bindings 和 cffi,都是原生 wheel,跨平台要各自有對應的 wheel 可裝。大部分情況下都有,但在一些冷門平台或很新的 Python 版本上,你會有一段等 wheel 的時間。

PBKDF2:內建、老、還在標準裡

PBKDF2 定義在 RFC 8018,做法是把 HMAC 重複很多次。Python 的 hashlib.pbkdf2_hmac("sha256", password, salt, iterations) 直接可用,不裝任何套件。它沒有記憶體硬度,只能靠迭代次數拉高成本,而迭代次數對 GPU 的效果遠不如記憶體。OWASP 目前對 PBKDF2-HMAC-SHA256 的建議是至少六十萬次迭代。六十萬次聽起來很多,實際上登入時算一次大概幾十毫秒,使用者感覺不到。

儲存格式要自己設計。我的做法是 pbkdf2$v1$鹽$摘要,鹽是十六位元組的隨機值,格式裡帶版本號,將來換演算法時舊的雜湊還認得出來,可以在下一次登入時順手升級。這個「順手升級」的設計很重要,因為你不可能叫所有使用者重設密碼,你只能等他們下次登入時偷偷幫他們換一把更好的鎖。

小劇場:同學的後續

「那我現在改成用 SHA-256 可以嗎?」

「不行,剛剛講過了。」

「那 MD5?」

「你是故意的嗎。」

「開玩笑的。所以我要裝那個 argon 什麼的?」

「argon2-cffi。pip 裝一下,三行程式,你的專題就從『會上新聞』升級成『符合現代標準』。」

「那你自己為什麼有一個專案用 PBKDF2?」

「因為那個專案只有我一個帳號,登入頁只在本機看得到。條件不一樣,答案就不一樣。你的專題有註冊功能,對吧?」

「有。」

「那你沒有選擇。」

我在什麼條件下用 PBKDF2

只有一個帳號、登入介面只聽本機迴路位址、攻擊者要先拿到資料庫檔案才輪得到離線破解,這三個條件同時成立時,我允許自己為了少一個原生相依而選 PBKDF2,並把迭代次數設在建議值附近。任何一個條件不成立,例如開放註冊、登入頁在公開網路上,答案就變成 argon2id,沒有討論空間。

我把這三個條件寫在程式碼旁邊的註解裡。不是給別人看的,是給未來的自己看的,因為未來的自己一定會在某個晚上想「我要不要開放註冊」,那時候這段註解會跳出來說「先換鎖」。

一個順帶的原則

同一個主機名底下如果有兩個後台,它們的 session cookie 名稱要不同,收到不屬於自己的 cookie 要忽略而不是報錯。這跟雜湊演算法無關,但跟「密碼被拿到之後會發生什麼」是同一個問題的鄰居:邊界要畫清楚,畫清楚之後出事的範圍才會小。

我的結論

演算法的選擇跟工具的名氣關係不大,跟威脅模型關係很大。argon2id 是預設答案;PBKDF2 是在明確的限制條件下、為了零相依而做的例外,而且要把條件寫下來,條件變了就換。

至於那位同學,他的專題最後用了 argon2,還在報告裡花了一頁解釋為什麼不能存明文。評審問他是怎麼知道的,他說「被朋友沉默了三秒之後查的」。我覺得這是我對資安教育做過最大的貢獻。

密碼雜湊的小小家族史

這個領域的演算法不多,每一個都有故事。bcrypt 是 1999 年為 OpenBSD 設計的,二十幾年了還在被大量使用,它的成本參數是「幾輪」,每加一輪時間翻倍。scrypt 是 2009 年 Colin Percival 為他的備份服務設計的,第一個把「記憶體」當成成本的演算法,後來因為被某個加密貨幣拿去當挖礦演算法而出名,這大概不是作者的本意。

然後是 2013 年到 2015 年的密碼雜湊競賽。一群密碼學家覺得該有一個現代的、公開評選的標準,收了二十幾個候選,評了兩年,argon2 勝出。競賽這個形式在密碼學界很常見,AES 和 SHA-3 都是這樣選出來的,公開的評選比某間公司說「我們的最安全」可信得多。

PBKDF2 是這幾個裡最老的,2000 年就進了標準。它老,但它在標準裡,很多合規要求點名要它,所以它會一直存在。老不等於壞,只是要知道它的極限在哪。

常見的三個誤會

第一個誤會是「加鹽就安全了」。鹽的作用是讓同樣的密碼產生不同的雜湊,擋掉預先算好的彩虹表。它不會讓單一雜湊變得更難破解,那是迭代次數和記憶體硬度的工作。鹽是必要的,但不是全部。

第二個誤會是「雜湊過就不用管密碼強度了」。再好的雜湊也擋不住密碼是 123456 的使用者,攻擊者第一個就會試它。雜湊拉高的是每次嘗試的成本,嘗試一次就中的密碼不需要成本。所以註冊時擋掉常見密碼跟選對演算法一樣重要。

第三個誤會是「參數設越高越好」。設太高,登入要等三秒,使用者會以為網站壞了;同時一千個人登入,你的伺服器會被自己的雜湊算到停擺。參數要調到「使用者感覺不到、攻擊者感覺得到」的那個點,通常是幾十到幾百毫秒。

一個週末可以試的小練習

開一個 Python,pip install argon2-cffi。用 PasswordHasher().hash("你的測試密碼") 看它吐出來的字串,那一串裡有演算法、版本、參數、鹽、摘要,全部編在一起。用 time 模組量一下 hash() 要多久,大概幾十毫秒。

然後用 hashlib.pbkdf2_hmac("sha256", b"同一個密碼", b"隨便的鹽", 600_000) 也量一次。比較兩個時間。接著把 PBKDF2 的迭代次數改成 6,000,再量一次,你會看到它變得很快,快到讓你理解為什麼舊網站的 PBKDF2 設定常常不夠:他們十年前設的數字,在今天的硬體上已經是瞬間的事。演算法沒變,硬體變了,這就是為什麼要有 check_needs_rehash() 這種東西。

後記

那位同學畢業後去了一間做網站的公司,有一次傳訊息跟我說,他們公司的舊系統密碼是 MD5 存的,他在會議上提了,被指派負責換掉。他說那一刻他想到我沉默的那三秒。我說那三秒現在值一個專案了,他說值一個月的加班。有些教育的代價是延遲支付的。

專案頁面:argon2-cffi.readthedocs.io、docs.python.org hashlib

閱讀偏好

返回全部文章作者與內容更正

留言(0)

還沒有留言。讀完有想法,歡迎留下第一則。

留言經站長審核後才會顯示。暱稱和留言會公開,請不要寫下電話、地址等個人資料;資料怎麼處理,請見隱私權政策。