2026/06/22

密碼 hash 演算法比較

比較密碼加密演算法

演算法 可調整參數 記憶體強度 抗 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)

2026/06/15

Argon2

Argon2 是一種密碼雜湊 (password hashing) 演算法,也是 2015 年密碼雜湊競賽(PHC, Password Hashing Competition) 的獲勝者。它被設計來安全地儲存密碼,抵抗暴力破解和 GPU / ASIC 攻擊。

特性

  1. Memory-hard:需要大量記憶體計算,增加硬體破解成本。

  2. Time-cost adjustable:可設定運算次數(iterations),增加雜湊延遲。

  3. Parallelism:支援多執行緒並行計算。

  4. 三種變體

    • Argon2d:可抵抗 GPU 破解,記憶體存取取決於輸入密碼,但對側信道攻擊 side‑channel attack 敏感。

    • Argon2i:可抵抗側信道攻擊,使用固定存取模式,但稍慢。

    • Argon2id:混合模式,推薦用於大多數應用(兼顧 GPU 攻擊與側信道安全)。

側信道攻擊 side‑channel attack

側信道攻擊不是直接破解演算法數學弱點,而是利用系統在執行時「洩漏的額外資訊」來恢復密碼、金鑰。這些洩漏資訊稱為「側信道」,常見類型有:

  • 時間(Timing):根據運算花費時間推斷內部邏輯或資料(例如不同輸入導致不同記憶體存取時間)。

  • 快取/記憶體訪問模式(Cache / Memory access):透過觀察 CPU cache 命中/未命中或記憶體訪問順序推測密碼相關資料。

  • 電力(Power analysis):量測設備耗電曲線(特別在智慧卡、嵌入式裝置)來推斷內部運算。

  • 電磁輻射(EM):接收設備發出的電磁洩漏訊號。

  • 分支/投機執行漏洞(Spectre/Speculative exec):利用微架構行為取得不該看到的記憶體內容。

  • I/O 或錯誤回應差異:例如錯誤訊息長短或序列差異可洩漏資訊。

側信道攻擊常見條件:攻擊者和目標在同一台機器或共用硬體資源(例如 cloud 共宿主、虛擬機、瀏覽器 JS 高解析計時)或在物理近距離(電力/EM)時更容易成功。遠端網路時延雜訊大,攻擊難度上升但並非不可能(需大量樣本與高解析度計時)。

若攻擊者只有透過網路遠端(無共用硬體且無高解析計時),側信道攻擊較難。

三種變體

在記憶體存取模式上不同

  • Argon2d

    • 記憶體存取依賴輸入(data‑dependent)

    • 優點:對 GPU / ASIC 破解更抗(memory‑hard),但容易洩漏記憶體訪問模式,因此對於有本地或共宿主攻擊者(能觀察 cache/access pattern)的系統 較不安全

  • Argon2i

    • 記憶體存取獨立於輸入(data‑independent),以固定/預定方式存取記憶體。

    • 優點:較能抵抗側信道攻擊(尤其是記憶體訪問/快取型);缺點:通常較慢,對某些攻擊(GPU 的大量平行 brute force)耐性較弱。

  • Argon2id

    • 混合模式:先用 Argon2i 的 data‑independent pass 再用 Argon2d 的 data‑dependent pass(或類似組合)。

    • 建議:大多數情況使用 Argon2id —— 在兼顧側信道與 GPU 抗性間取得平衡,是常見推薦選擇。

java example

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <version>1.82</version>
        </dependency>

Argon2BouncyCastleExample.java

import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Arrays;

public class Argon2BouncyCastleExample {

    // 產生隨機 salt
    private static byte[] generateSalt(int length) {
        byte[] salt = new byte[length];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    // 將密碼 hash
    public static String hashPassword(String password, byte[] salt) {
        // 設定參數
        Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withIterations(3)       // 運算次數
                .withMemoryAsKB(65536)   // 記憶體使用量 KB (64 MB)
                .withParallelism(2);     // 併行線程數

        Argon2BytesGenerator generator = new Argon2BytesGenerator();
        generator.init(builder.build());

        byte[] hash = new byte[32]; // 32 bytes hash
        generator.generateBytes(password.toCharArray(), hash, 0, hash.length);

        // 回傳 base64 編碼
        return Base64.getEncoder().encodeToString(hash);
    }

    // 驗證密碼
    public static boolean verifyPassword(String password, String expectedHashBase64, byte[] salt) {
        String hashToCheck = hashPassword(password, salt);
        return Arrays.equals(Base64.getDecoder().decode(hashToCheck), Base64.getDecoder().decode(expectedHashBase64));
    }

    public static void main(String[] args) {
        String password = "MyPassword123!";
        byte[] salt = generateSalt(16); // 16 bytes salt

        // hash
        String hash = hashPassword(password, salt);
        System.out.println("Salt (Base64): " + Base64.getEncoder().encodeToString(salt));
        System.out.println("Hash (Base64): " + hash);

        // 驗證
        boolean ok = verifyPassword(password, hash, salt);
        System.out.println("Password verified: " + ok);

        // 測試錯誤密碼
        boolean fail = verifyPassword("wrongPassword", hash, salt);
        System.out.println("Wrong password verified: " + fail);
    }
}

執行結果

Salt (Base64): Uivew8iBmhiZaz7Wv2nSDw==
Hash (Base64): Ldm2DksAcnhUIHyJ4v4uzJJwLd5A2YIhTMn3277/tDU=
Password verified: true
Wrong password verified: false

References

Argon2 - 維基百科,自由的百科全書

2026/06/01

scrypt

scrypt 是一種password-based key derivation演算法。是加拿大計算機科學家暨計算機安全研究人員 Colin Percival 於2009年所發明的密鑰派生函數,當初設計用在他所創立的Tarsnap服務上。設計時考慮到大規模的客製硬體攻擊而刻意設計需要大量記憶體運算,可防止 GPU/ASIC 大規模破解。2016年,scrypt 演算法發佈在RFC 7914。scrypt的簡化版被用在數個密碼貨幣的工作量證明(Proof-of-Work)上。

scrypt需要使用大量記憶體的原因來自於產生大量 pseudorandom 資料作為演算法計算的基礎。一旦這些資料被產生後,演算法將會以偽隨機性的順序讀取這些資料產生結果。因此最直接的實做方式將會需要大量記憶體將這些資料儲存在記憶體內供演算法計算。由於偽隨機性資料是透過演算法產生,在實作上也可以在需要存取時再計算以降低記憶體使用量。但由於計算成本很高,這個實作方法將大幅降低演算法的速度。

主要的參數:

透過 r, p 參數,調整記憶體使用量

參數 說明
N 迭代次數,耗費 CPU 與時間成本(必須是 2 的次方)
r 記憶體使用量(block size)
p 並行度(parallelism)
salt 隨機值,避免彩虹表攻擊
dkLen 輸出 hash 長度,通常是 32 bytes / 64 bytes

java sample

pom.xml

        <!-- scrypt -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <version>1.82</version>
        </dependency>

ScryptBouncyCastleExample.java

import org.bouncycastle.crypto.generators.SCrypt;
import java.security.SecureRandom;
import java.util.Base64;

public class ScryptBouncyCastleExample {

    // scrypt 參數
    private static final int N = 16384; // CPU cost
    private static final int r = 8;     // Memory cost
    private static final int p = 1;     // Parallelism
    private static final int KEY_LENGTH = 32;

    // 產生隨機 salt
    public static byte[] generateSalt() {
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    // Hash 密碼
    public static String hashPassword(String password, byte[] salt) {
        byte[] hash = SCrypt.generate(password.getBytes(), salt, N, r, p, KEY_LENGTH);
        return Base64.getEncoder().encodeToString(hash);
    }

    // 驗證密碼
    public static boolean verifyPassword(String password, byte[] salt, String storedHash) {
        String hashToVerify = hashPassword(password, salt);
        return hashToVerify.equals(storedHash);
    }

    public static void main(String[] args) {
        String password = "MyPassword123!";

        // 產生 salt
        byte[] salt = generateSalt();

        // 生成 hash
        String hashed = hashPassword(password, salt);
        System.out.println("Salt (Base64): " + Base64.getEncoder().encodeToString(salt));
        System.out.println("Hashed password: " + hashed);

        // 驗證
        boolean match = verifyPassword(password, salt, hashed);
        System.out.println("Password match? " + match);

        boolean wrong = verifyPassword("wrongPassword", salt, hashed);
        System.out.println("Password match with wrong password? " + wrong);
    }
}

執行結果

Salt (Base64): +VF7o5rmURY++GxWmKARhw==
Hashed password: PON5gVuqeiOM4MDszfY4DHBjBacpMdObu07WhdTaoyo=
Password match? true
Password match with wrong password? false

References

scrypt - 維基百科,自由的百科全書