密碼儲存是軟體領域裡少數幾個真正存在標準答案的題目之一。答案早就公開了,一直有人維護,而且免費。但它同時也是最常被做錯的題目之一,原因很單純:那些現在被視為錯誤的做法,在某個年代確實是正確的,而後來沒有人再回頭看過一眼。

失敗的樣子很少稀奇。通常就是一套 2016 年依照 2012 年的建議搭起來的系統,到今天還在跑,還在接受登入請求,而自從當初寫下那段雜湊程式碼的人離職之後,就再也沒有人打開過那個函式。它沒出過事故,也沒被稽核過,所以從來不在任何人的待辦清單上。

如果只帶走一句話:用 Argon2id,真的不行就用 bcrypt。 本文其餘部分都是細節。像 SHA-256 這樣的通用雜湊函式,無論你套用多少次,都不會變成密碼雜湊。它的快是設計目標,而快正是你要從拿到資料庫的攻擊者手上奪走的東西。


密碼儲存只有一種正確的形態

儲存一個由刻意放慢、且需要大量記憶體的函式所產生的雜湊值,為每一組密碼配一個唯一的隨機鹽值,並使用針對你自己硬體調校過的參數。這三件事互為前提,缺了任何一件,另外兩件的價值都會大打折扣。

OWASP 的密碼儲存指南把目前的建議值寫得非常明確,而這些建議值值得當成數字來引用,而不是當成原則。

演算法建議參數何時使用
Argon2id19 MiB 記憶體,2 次疊代,1 平行度首選
scryptN=2^17(128 MiB),r=8,p=1Argon2id 無法使用時
bcrypt工作因子 10 或更高,密碼上限 72 byte舊有系統
PBKDF2-HMAC-SHA256600,000 次疊代需要符合 FIPS-140 規範時

關於這張表有兩點要補充。第一,Argon2id 的數值是下限,而且提高記憶體、減少疊代次數是一種等價交換,所以採用 47 MiB 記憶體加 1 次疊代的設定,安全性與基準線相當。你可以從自己能接受的回應時間往回推,決定要把哪一個軸拉高。

第二,bcrypt 的 72 byte 不是一條驗證規則,而是真實發生的截斷。超過 72 byte 的部分會被悄悄丟掉,不會有任何錯誤。如果你允許很長的通行短語,或是在交給 bcrypt 之前先把輸入雜湊過一次,這一點就會變成實實在在的弱點。

另外,這些參數會隨著硬體進步而往上修訂。真正動手實作的時候,請讀第一手來源,而不是讀這張表。

鹽值不是選配,但也不夠用

鹽值是與每個雜湊值一起保存的、唯一的隨機資料。它的作用範圍很窄,卻很重要:它確保兩個使用相同密碼的使用者會得到不同的雜湊值。這樣一來,事先算好的彩虹表就失效了,攻擊者也無法靠破解一組密碼,就順勢拿下所有共用該密碼的帳號。

但鹽值做不到的事情是:它不會讓任何人變慢。拿到你資料庫的攻擊者,同時也拿到了所有鹽值,因為鹽值必須可讀才能驗證登入,它本來就不是祕密。所以在一個快速雜湊上只加鹽,意味著攻擊者會逐一攻擊每一組密碼,而且速度極快,這正是你原本想避開的局面。

慢必須來自演算法本身。這正是記憶體密集型函式存在的理由:它刻意消耗資源,讓配備專用硬體的攻擊者所能取得的優勢,遠小於他們面對快速雜湊時能拿到的優勢。通用雜湊可以在顯示卡或專用晶片上同時跑成千上萬條管線,而記憶體密集型函式的每一條管線都要獨佔一整塊記憶體,硬體成本因此被迫拉高,能同時開出的平行規模也就被壓了下來。

胡椒是一個作用於所有密碼、卻保存在資料庫之外的祕密值,它確實能再加一層防護,因為這時候光是拖走一份資料庫已經不夠用了。代價是維運負擔:輪換胡椒需要在使用者下次登入時重新雜湊,而一旦遺失,所有帳號都會被鎖在門外。對高價值系統來說值得,對大多數系統來說可以略過。

那些能活好幾年的錯誤

加了鹽的快速雜湊。 幫 SHA-256 加鹽看起來很負責,其實不是。快本身就是漏洞。

用加密取代雜湊。 加密在設計上就是可逆的,也就代表存在一把金鑰,而任何拿到這把金鑰的人,就拿到了全部明文密碼。密碼永遠不該可還原,只該可驗證。

工作因子只設過一次,從未調高。 2015 年選定的 bcrypt 成本值,面對 2026 年的硬體已經明顯偏弱。請在使用者下一次成功登入時調高,那一刻明文短暫地在你手上,可以重新雜湊。

截斷或字元限制。 最大長度低於大約 64 個字元,或是禁止某些字元,通常代表密碼是以一種在意其內容的方式被保存的。它不該在意,因為你終究是要把它雜湊掉的。

會洩漏時間的比較。 驗證必須使用常數時間比較。標準函式庫的函式會這麼做,手寫的相等判斷不會。

把密碼寫進記錄檔。 它一旦流進錯誤記錄檔、請求追蹤或監控系統,就等於同時出現在三個存取控制比資料庫更弱的地方。

遷移一套既有系統

你無法重新雜湊手上沒有的東西,因為你保存的只有雜湊值。可行的做法只有一種:在登入時升級。

當使用者驗證成功的那一瞬間,明文密碼短暫地留在記憶體裡。先用舊的雜湊值驗證,然後立刻用目前的演算法算出新的雜湊值,取代已儲存的值。幾個月下來,絕大多數活躍帳號都會在使用者毫無察覺的情況下完成遷移。

逐筆記錄所使用的演算法,讓新舊兩種方式共存。實際落地時,往往只需要在每一筆資料上多存一個演算法識別碼與參數版本號,登入時據此挑選驗證方式,通過之後再判斷要不要順手升級。同時訂下一個日期:過了那天,剩下的舊雜湊一律作廢,相關使用者必須重設密碼。帶著弱雜湊沉睡的帳號,正是攻擊者最想要的。

不要為了省事,把舊的雜湊值再用新演算法包一層。這樣確實跑得動,但真正的安全邊界仍然是那個弱雜湊,只是外表看起來很現代而已。

比演算法更重要的事

正確的雜湊會在資料庫外洩時保護你。但面對更常見的那類攻擊,也就是有人拿著從別處取得的真實密碼直接登入,它一點作用也沒有。從其他網站外洩的憑證被大批拿來試,而你的系統看到的只是一連串在語法與憑證上都完全合法的登入請求。

驗證端點上的速率限制、可用的多因素驗證、把新密碼與已知外洩清單比對,以及針對憑證填充行為樣態的告警,處理的正是這件事,而在日常維運的尺度上,它們全都比在 Argon2id 與 bcrypt 之間怎麼選更重要。API 安全方面的控制措施屬於同一類問題。

兩件事都值得做,而順序值得說得坦白一些:雜湊便宜又一勞永逸,先一次性做對;之後持續投入的心力應該放在登入路徑上,因為攻擊真正抵達的地方就在那裡。Mecanik 在應用程式安全分析中會同時檢視這兩個面向。



常見問題

2026 年最好的密碼雜湊演算法是什麼? Argon2id,至少設定 19 MiB 記憶體、2 次疊代與 1 平行度。如果用不了,scrypt 採用 N=2^17、r=8、p=1 是一種替代方案;工作因子 10 或更高的 bcrypt 適合舊有系統;而 PBKDF2-HMAC-SHA256 配 600,000 次疊代可以滿足 FIPS-140 的要求。

幫密碼加鹽就夠了嗎? 不夠。鹽值確保相同的密碼產生不同的雜湊值,讓彩虹表失效,也阻止一組被破解的密碼打開所有共用它的帳號。但它不會拖慢攻擊者,因為驗證登入時鹽值必須是可讀的。慢必須來自一個刻意消耗大量記憶體的演算法。

為什麼不該用 SHA-256 存密碼? 因為它的快是設計目標,而快正是你要從握有你資料庫的攻擊者手上奪走的東西。通用雜湊函式不是密碼雜湊,無論套用多少次,也無論有沒有加鹽。

bcrypt 對密碼長度有限制嗎? 有,72 byte,超出的部分會被悄悄截斷而不是拒絕。如果你允許很長的通行短語,或是在交給 bcrypt 之前先把輸入雜湊過,這一點就很重要,因為截斷發生時不會有任何錯誤提示。

沒有使用者密碼,要怎麼升級密碼雜湊? 在登入時做。使用者驗證的那一刻,明文短暫地在你手上,所以先用舊的雜湊值驗證,再立刻用目前的演算法重新雜湊。逐筆記錄所使用的演算法,讓新舊共存,並訂下一個日期:過了那天,剩下的舊雜湊作廢,相關使用者必須重設密碼。