← 回首頁

中文密碼打到第 24 個字,bcrypt 就不再讀了

兩個看起來完全不同的密碼可以是同一個密碼。截斷以位元組計算,所以它會切在一個字的中間——而 24 個中文字就已經用完全部額度。

一條由淡藍色方塊組成的色帶由左延伸到右,其中一個特別大的方塊被乾淨地切成兩半,切口之後的全部碎裂成小碎片。

使用者設了一段很長的密碼。幾個月後他把結尾打錯,照樣登入成功。沒有東西壞掉, 沒有人回報 bug,而他以為在保護帳號的那組密碼,比他自己選的那組短。

bcrypt 讀 72 個位元組,其餘丟掉

在 Node v25.9.0 上用 bcrypt 6.0.0 與 bcryptjs 3.0.3 實測:兩者都接受 任意長度的密碼,雜湊前 72 個位元組,把後面全部丟掉。不丟例外、不發警告, 回傳值裡也沒有任何跡象。

邊界很精確。拿一個 71 位元組的密碼加一個字元,它是不同的密碼;拿一個 72 位元組的密碼加一個字元,它是同一個密碼。

第二句話就是這篇文章的全部。

兩個不同的密碼,一個雜湊

下面有兩個輸入框。預設值的前 72 個位元組相同,之後不同。 這裡不做雜湊、不送出、不儲存,計算全部在你的瀏覽器裡。

bcrypt 實際讀到的部分

不要輸入你真的在用的密碼。這裡不做雜湊、不送出、不儲存,計算全部在你的瀏覽器裡——但一個你曾經打進網頁的密碼,就是一個該換掉的密碼。

位元組 75 · 字 25 · 超過 72 位元組——後面的被丟掉

我的密碼是正確的馬電池釘書針正確的馬電池釘書針真密

位元組 75 · 字 25 · 超過 72 位元組——後面的被丟掉

我的密碼是正確的馬電池釘書針正確的馬電池釘書針真寇

對 bcrypt 而言,這兩個是同一個密碼嗎?

同一個密碼。用其中一個註冊,可以用另一個登入。

幾個字會填滿 72 位元組

  • a 72 個字
  • é 36 個字
  • 密 24 個字
  • 🔐 18 個字

上限是 72 位元組。行為於 2026-08-07 在 Node v25.9.0 + bcrypt 6.0.0 上實測。

那行判定不是在模擬 bcrypt,它就只是比對前 72 個位元組——因為那正是 bcrypt 對 輸入做的全部事情。當它說「同一個密碼」,意思是你可以用其中一個註冊、用另一個登入。

上限算的是位元組,而文字不是位元組

bcrypt 建立在 Blowfish 的金鑰排程上,它吃的是原始位元組。你的密碼要先編碼才會變成 位元組,而 UTF-8 不是對每個字元一視同仁:

每字位元組 塞得下幾個
英文字母 1 72
帶重音的拉丁字母(é) 2 36
常用中文字(密) 3 24
Emoji(🔐) 4 18

英文使用者要刻意才撞得到 72。中文使用者用一句完全正常的話就到了—— 二十四個字是一個句子,不是耐力測驗。

同一個上限,在一個語言裡是冷知識,在另一個語言裡是正在發生的問題。而這正是那種 會安然通過一個英語團隊程式碼審查的錯誤。

切點不理會字元邊界

這一段是我看了兩次才相信的。

密 編碼成 e5 af 86,寇 編碼成 e5 af 87,前兩個位元組相同。把它們各自放在 一個切點落在第 72 位元組之後的密碼結尾,bcrypt 從兩者都只留下 e5 af:

"a" × 70 + 密   →  73 位元組
"a" × 70 + 寇   →  73 位元組
前 72 個位元組   →  完全相同

用第一個做雜湊、拿第二個去驗證,回傳 true。對照組 天(e5 a4 a9)在第二個 位元組就不同,正確地驗證失敗。

也就是說,兩個結尾完全不同、任何人都不會說它們一樣的密碼,是同一組憑證。 而被留下的那個尾巴根本不是合法的 UTF-8——它是三分之二個字。

一個在這裡不成立的傳說

你會讀到「bcrypt 會在 NUL 位元組處停下來,像 C 字串那樣」。在 bcrypt 6.0.0 上不會: 把 "secret" + NUL + "ignored-tail" 做成雜湊,再拿 "secret" 去驗證,回傳 false。

寫進來是因為它與上面每一項是同一種形狀的主張——一個在上百串留言裡被自信重複的 位元組層行為——而它對多數 Node 專案真正安裝的那個函式庫來說是錯的。 量你手上的那一個。

該怎麼做

問題不在那個上限,在那份沉默。

  1. 在註冊時就擋下超長密碼,並且說出來。 被告知「太長了」的使用者會換一組; 被靜默截斷的使用者,會相信一組不存在的密碼。
  2. 用位元組算長度,不要用字元數。 password.length 在任何一個字元需要超過一個 位元組的語言裡都是錯的數字——而且是往「讓壞輸入通過」的方向錯。
  3. 如果你的使用者寫中文、日文或韓文,你真正的上限是 24 個字。 那不是一個值得 放進註腳的理論值,那是人們真的會打的長度。

如果你要的是一個完全沒有長度上限的密碼欄位,那是換一個 KDF 的理由,不是把 bcrypt 調長的理由。但多數系統需要的不是那個——是不要再對自己存下來的密碼說謊。

常見追問

密碼太長時 bcrypt 會報錯嗎?

在 Node 上不會。實測 bcrypt 6.0.0 與 bcryptjs 3.0.3,兩者都接受任意長度的密碼, 雜湊前 72 個位元組,其餘丟掉,不丟任何例外。其他語言的某些實作確實會拒絕過長輸入, 所以這是一個要逐個函式庫確認的行為,不能假設。

為什麼是 72 個位元組而不是 72 個字?

上限來自 bcrypt 底層的 Blowfish 金鑰排程,它吃的是原始位元組。文字要先編碼才會 變成位元組,而 UTF-8 給英文字母一個位元組、給多數帶重音的拉丁字母兩個、給常用 中文字三個、給 emoji 四個。上限是固定的,塞得下多少文字不是。

切點落在一個字中間會怎樣?

bcrypt 保留塞得下的那幾個位元組,把那個字剩下的部分丟掉。任何編碼開頭與那幾個 位元組相同的其他字,就會產生一模一樣的密碼——密與寇的開頭都是 e5 af, 所以切點落在第二個位元組之後時,兩者會撞在一起。

NUL 位元組會終止密碼嗎?

在這裡實測的版本不會。那個說法來自建立在 C 字串處理上的實作,值得實測而不是照抄: 在 bcrypt 6.0.0 上,含有 NUL 位元組的密碼不會通過「只有前半段」的驗證。

那到底該怎麼做?

在註冊時就擋下超長密碼並且說出來,不要靜默截斷。用位元組算長度,不要用字元數。 如果你的使用者會用中文寫密碼,實際的上限是 24 個字,不是 72。