bcrypt 是一種專為密碼儲存設計的單向雜湊演算法(hashing algorithm)。 由美國電腦科學家 Niels Provos 及 David Mazières 根據 Blowfish 加密演算法所設計的密碼雜湊函式,於1999年在USENIX中展示。並加入了:
Salt — 防止字典攻擊與 rainbow table 攻擊
工作因子(cost factor) — 運算次數 =
2^cost,控制計算複雜度,讓暴力破解更慢
| Cost | 大約耗時(現代CPU) | 用途建議 |
|---|---|---|
| 8 | 約 50ms | 測試環境 |
| 10 | 約 100–200ms | 一般 Web 登入 |
| 12 | 約 300–500ms | 高安全性需求 |
| 14+ | >1 秒 | 金融級或低頻操作 |
bcrypt 運作流程
注意密碼最多只支援 72 bytes
- 輸入:
明文密碼(例如 "MyPassword123!"),最多 72 bytes
工作因子(cost,例如 10)
產生隨機 Salt(16 bytes), 是一個 16 bytes(128 bits) salt value
進行多輪 Blowfish 加密運算,運算次數 = 2^cost ex: cost=10 → 1024 次加密循環 cost=12 → 4096 次循環
輸出:產生一個 60 字元的 hash 字串,裡面包含版本, cost factor, salt及 hash 結果
bcrypt 格式
#格式
$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]
# sample
$2a$10$E6h8dSmDsC8Kz.bJb3SuPO0h9msLniD9pD8bZjq4nLZdTIm.CDzMy
$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
格式說明
$2a$ → bcrypt 版本
10$ → cost factor
E6h8dSmDsC8Kz.bJb3SuPO → Base64 編碼的 salt
0h9msLniD9pD8bZjq4nLZdTIm.CDzMy → hash 結果
$2a$ → bcrypt 版本
12$ → cost factor
R9h/cIPz0gi.URNNX3kh2O → Base64 編碼的 salt
PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW → hash 結果
Version
$2$ (1999)
最初始的 bcrypt 版本
在 OpenBSD 密碼檔案裡面用以下幾種方式定義使用哪一種加密方式
$1$: MD5-based crypt ('md5crypt')$2$: Blowfish-based crypt ('bcrypt')$sha1$: SHA-1-based crypt ('sha1crypt')$5$: SHA-256-based crypt ('sha256crypt')$6$: SHA-512-based crypt ('sha512crypt')
$2a$
初始版本的 bcrypt,沒有定義如何處理 non-ASCII 字元,也沒有處理 null terminator。這個版本的 bcrypt 限制
字串必須要用UTF-8 encoding
必須要包含 null terminator
$2x$, $2y$ (June 2011)
因 2011/6 在 php 版本的 crypt_blowfish 發現了一個 bug,在 8th bit 設定時,會處理錯誤。故建議把 $2a$更新為 $2x$ ,同時做了 $2y$。但 2x/2y 都沒有被廣泛採用
$2b$ (February 2014)
OpenBSD 版本的 bcrypt 發現了一個 bug,是使用 unsigned 8-bit 數值儲存密碼長度。超過 255 bytes 的密碼,會以少於 72 bytes 的長度切割。 ex: 260 bytes 被以 4 bytes 切割。
OpenBSD 修正這個問題,並改版為 $2b$
Java Sample
有兩個 java library,org.mindrot.jbcrypt 版本比較舊,沒有支援新版的演算法,且已經沒有在維護。at.favre.lib.crypto.bcrypt 有支援新的演算法,且有支援超過 72 bytes 密碼的處理方式。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.maxkit.test</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<version>0.10.2</version>
</dependency>
</dependencies>
</project>
MindrotBcrypt.java
import org.mindrot.jbcrypt.BCrypt;
public class MindrotBcrypt {
public static void main(String[] args) {
String password = "MyPassword123!";
// cost factor (越大越慢,預設常見是 10~12)
int cost = 12;
// 產生 salt
String salt = BCrypt.gensalt(cost);
System.out.println("Salt: " + salt);
// Hash 密碼
String hashed = BCrypt.hashpw(password, salt);
System.out.println("BCrypt Hash: " + hashed);
// 驗證密碼
boolean matched = BCrypt.checkpw(password, hashed);
System.out.println("Password matched? " + matched);
// 測試錯誤密碼
boolean matchedWrong = BCrypt.checkpw("WrongPassword", hashed);
System.out.println("Wrong password verified? " + matchedWrong);
}
}
執行結果
Salt: $2a$12$.ZzlOaYpDmUSvFCq/FJaM.
BCrypt Hash: $2a$12$.ZzlOaYpDmUSvFCq/FJaM.lEUlNjTrXH8JuUAN96fq80yiKSyV0Q.
Password matched? true
Wrong password verified? false
BcryptFavreBcrypt.java
import at.favre.lib.crypto.bcrypt.BCrypt;
public class BcryptFavreBcrypt {
public static void main(String[] args) {
String password = "MyPassword123!";
int cost = 12;
// 產生 hash
String bcryptHashString = BCrypt.withDefaults().hashToString(cost, password.toCharArray());
System.out.println("Hash: " + bcryptHashString);
// 驗證密碼
BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString);
if (result.verified) {
System.out.println("Password verified OK");
} else {
System.out.println("Password verification failed");
}
// 錯誤密碼測試
BCrypt.Result resultWrong = BCrypt.verifyer().verify("WrongPassword".toCharArray(), bcryptHashString);
System.out.println("Wrong password verified? " + resultWrong.verified);
//////
// 產生 2y hash
String bcryptHashString2Y = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(cost, password.toCharArray());
System.out.println("2Y Hash: " + bcryptHashString2Y);
// 驗證密碼
BCrypt.Result result2Y = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString2Y);
if (result2Y.verified) {
System.out.println("2Y Password verified OK");
} else {
System.out.println("2Y Password verification failed");
}
// 產生 2b hash
String bcryptHashString2B = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(cost, password.toCharArray());
System.out.println("2B Hash: " + bcryptHashString2B);
// 驗證密碼
BCrypt.Result result2B = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString2B);
if (result2B.verified) {
System.out.println("2B Password verified OK");
} else {
System.out.println("2B Password verification failed");
}
}
}
執行結果
Hash: $2a$12$BUc45rHJsnLWDkFW8CJN4.5QB.KUSg2ofIcejoeJ24OTmznz8BD8a
Password verified OK
Wrong password verified? false
2Y Hash: $2y$12$vRH74gxo2LZN/wbFj4oKdO6RrGgOgWTHxrhw.OUoMt9ZT1WqPT8Uq
2Y Password verified OK
2B Hash: $2y$12$7y.j7ocPnDqcbspURCddzeTn25HWgtQR1LHy20Wo./N81ygBd7m92
2B Password verified OK
沒有留言:
張貼留言