比較密碼加密演算法
| 演算法 | 可調整參數 | 記憶體強度 | 抗 GPU/ASIC | 密碼儲存 |
|---|---|---|---|---|
| UnixCrypt | 無 | 低 | 無 | 不適合 |
| MD5 | 無 | 低 | 無 | 不適合 |
| PBKDF2 | iterations | 低 | 弱 | 一般安全性 |
| bcrypt | cost factor | 中 | 部分 | 一般網站,推薦使用 |
| scrypt | N, r, p | 高 | 高 | 高安全性,錢包,加密金鑰 |
| Argon2 | time, mem, parallelism | 高 | 最高 | 最高安全性,密碼系統 |
java profiler
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>
<!-- scrypt, Argon2 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.82</version>
</dependency>
</dependencies>
</project>
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.generators.SCrypt;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.mindrot.jbcrypt.BCrypt;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
public class PasswordHashBenchmarkWithVerify {
private static final String PASSWORD = "MyPassword123!";
private static final SecureRandom RANDOM = new SecureRandom();
public static void main(String[] args) throws Exception {
System.out.println("=== Hash + Verify Benchmark ===");
System.out.println("Password: " + PASSWORD + "\n");
// 先做一次 warm-up (JVM/JIT)
warmUp();
System.out.println("---- PBKDF2 (HmacSHA256) ----");
pbkdf2HashAndVerify();
System.out.println("---- bcrypt (jBCrypt) ----");
bcryptHashAndVerify();
System.out.println("---- scrypt (BouncyCastle) ----");
scryptHashAndVerify();
System.out.println("---- Argon2id (BouncyCastle) ----");
argon2HashAndVerify();
}
private static void warmUp() {
// 簡單 warmup 幾次,讓 JIT 編譯熱起來
for (int i = 0; i < 3; i++) {
try {
pbkdf2Once();
bcryptOnce();
scryptOnce();
argon2Once();
} catch (Exception ignored) {}
}
}
// ---------- PBKDF2 ----------
private static void pbkdf2HashAndVerify() throws Exception {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
int iterations = 65536;
int keyLen = 256; // bits
long t0 = System.nanoTime();
PBEKeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), salt, iterations, keyLen);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
long t1 = System.nanoTime();
String stored = String.format("PBKDF2$%d$%s$%s",
iterations,
Base64.getEncoder().encodeToString(salt),
Base64.getEncoder().encodeToString(hash));
System.out.println("Stored: " + stored);
System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");
// verify
long v0 = System.nanoTime();
boolean ok = verifyPBKDF2(PASSWORD, stored);
long v1 = System.nanoTime();
System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
}
private static boolean verifyPBKDF2(String password, String stored) throws Exception {
// stored format: PBKDF2$iterations$base64salt$base64hash
String[] parts = stored.split("\\$");
int iterations = Integer.parseInt(parts[1]);
byte[] salt = Base64.getDecoder().decode(parts[2]);
byte[] expected = Base64.getDecoder().decode(parts[3]);
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, expected.length * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] computed = skf.generateSecret(spec).getEncoded();
return MessageDigest.isEqual(computed, expected);
}
private static void pbkdf2Once() throws Exception {
byte[] salt = new byte[8];
RANDOM.nextBytes(salt);
PBEKeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), salt, 1000, 128);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
skf.generateSecret(spec).getEncoded();
}
// ---------- bcrypt ----------
private static void bcryptHashAndVerify() {
int cost = 12; // cost factor
long t0 = System.nanoTime();
String hash = BCrypt.hashpw(PASSWORD, BCrypt.gensalt(cost));
long t1 = System.nanoTime();
System.out.println("Stored: " + hash);
System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");
long v0 = System.nanoTime();
boolean ok = BCrypt.checkpw(PASSWORD, hash);
long v1 = System.nanoTime();
System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
}
private static void bcryptOnce() {
BCrypt.hashpw(PASSWORD, BCrypt.gensalt(8));
}
// ---------- scrypt ----------
private static void scryptHashAndVerify() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
int N = 16384; // CPU/memory cost (2^14)
int r = 8;
int p = 1;
int keyLen = 32;
long t0 = System.nanoTime();
byte[] derived = SCrypt.generate(PASSWORD.getBytes(StandardCharsets.UTF_8), salt, N, r, p, keyLen);
long t1 = System.nanoTime();
String stored = String.format("scrypt$%d$%d$%d$%s$%s",
N, r, p,
Base64.getEncoder().encodeToString(salt),
Base64.getEncoder().encodeToString(derived));
System.out.println("Stored: " + stored);
System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");
long v0 = System.nanoTime();
boolean ok = verifyScrypt(PASSWORD, stored);
long v1 = System.nanoTime();
System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
}
private static boolean verifyScrypt(String password, String stored) {
// stored format: scrypt$N$r$p$base64salt$base64hash
String[] parts = stored.split("\\$");
int N = Integer.parseInt(parts[1]);
int r = Integer.parseInt(parts[2]);
int p = Integer.parseInt(parts[3]);
byte[] salt = Base64.getDecoder().decode(parts[4]);
byte[] expected = Base64.getDecoder().decode(parts[5]);
byte[] computed = SCrypt.generate(password.getBytes(StandardCharsets.UTF_8), salt, N, r, p, expected.length);
return MessageDigest.isEqual(computed, expected);
}
private static void scryptOnce() {
byte[] salt = new byte[8];
RANDOM.nextBytes(salt);
SCrypt.generate(PASSWORD.getBytes(StandardCharsets.UTF_8), salt, 1024, 8, 1, 16);
}
// ---------- Argon2id ----------
private static void argon2HashAndVerify() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
int iterations = 3;
int memoryKB = 64 * 1024; // 64 MB represented as KB here
int parallelism = 1;
int hashLen = 32;
Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withSalt(salt)
.withParallelism(parallelism)
.withMemoryAsKB(memoryKB)
.withIterations(iterations)
.build();
Argon2BytesGenerator gen = new Argon2BytesGenerator();
long t0 = System.nanoTime();
gen.init(params);
byte[] hash = new byte[hashLen];
gen.generateBytes(PASSWORD.getBytes(StandardCharsets.UTF_8), hash);
long t1 = System.nanoTime();
String stored = String.format("argon2id$%d$%d$%d$%s$%s",
iterations, memoryKB, parallelism,
Base64.getEncoder().encodeToString(salt),
Base64.getEncoder().encodeToString(hash));
System.out.println("Stored: " + stored);
System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");
long v0 = System.nanoTime();
boolean ok = verifyArgon2(PASSWORD, stored);
long v1 = System.nanoTime();
System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
}
private static boolean verifyArgon2(String password, String stored) {
// stored format: argon2id$iterations$memoryKB$parallelism$base64salt$base64hash
String[] parts = stored.split("\\$");
int iterations = Integer.parseInt(parts[1]);
int memoryKB = Integer.parseInt(parts[2]);
int parallelism = Integer.parseInt(parts[3]);
byte[] salt = Base64.getDecoder().decode(parts[4]);
byte[] expected = Base64.getDecoder().decode(parts[5]);
Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withSalt(salt)
.withParallelism(parallelism)
.withMemoryAsKB(memoryKB)
.withIterations(iterations)
.build();
Argon2BytesGenerator gen = new Argon2BytesGenerator();
gen.init(params);
byte[] computed = new byte[expected.length];
gen.generateBytes(password.getBytes(StandardCharsets.UTF_8), computed);
return MessageDigest.isEqual(computed, expected);
}
private static void argon2Once() {
byte[] salt = new byte[8];
RANDOM.nextBytes(salt);
Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withSalt(salt)
.withParallelism(1)
.withMemoryAsKB(32)
.withIterations(1)
.build();
Argon2BytesGenerator gen = new Argon2BytesGenerator();
gen.init(params);
byte[] out = new byte[16];
gen.generateBytes(PASSWORD.getBytes(StandardCharsets.UTF_8), out);
}
}
執行結果
=== Hash + Verify Benchmark ===
Password: MyPassword123!
---- PBKDF2 (HmacSHA256) ----
Stored: PBKDF2$65536$uBq2UzPm/rU5/5QOOHgWwA==$0Ri1XVl8++OB5Pz7pXAXEAJ32M8i8degzp6PZPnyd5o=
Hash time: 61 ms
Verify: true (time: 52 ms)
---- bcrypt (jBCrypt) ----
Stored: $2a$12$FjXjsIfL7MQ8zz8oyNfx.O6sBoSkhG6hcMK/brtWvfFOlfFx0WD/a
Hash time: 215 ms
Verify: true (time: 213 ms)
---- scrypt (BouncyCastle) ----
Stored: scrypt$16384$8$1$C6G1/j3+24iT7CHfW8/Ifg==$HC7JX3pdmDTKk2tCMUdTQ/ezgyey/hLx2ZCAoePzdHs=
Hash time: 22 ms
Verify: true (time: 25 ms)
---- Argon2id (BouncyCastle) ----
Stored: argon2id$3$65536$1$6HpyoNfAlJQ32cFAmflEHQ==$vZ+hNK/DnNpj1Urbj0W76zT2nvm0qDV14HFMbW9O9J0=
Hash time: 136 ms
Verify: true (time: 133 ms)