2026/05/18

bcrypt

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

  1. 輸入:
  • 明文密碼(例如 "MyPassword123!"),最多 72 bytes

  • 工作因子(cost,例如 10)

  1. 產生隨機 Salt(16 bytes), 是一個 16 bytes(128 bits) salt value

  2. 進行多輪 Blowfish 加密運算,運算次數 = 2^cost ex: cost=10 → 1024 次加密循環 cost=12 → 4096 次循環

  3. 輸出:產生一個 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

References

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

2026/05/04

Java Atomic Variables

java.util.concurrent.atomic 定義了對單一變數進行 atomic operations 的類別,所有類別都有 get 與 set methods,可讀寫 volatile variables。set 可確定會在 get 的前面先處理。

對於物件管理,如果有兩個 thread 同時做 get/set,會導致資料異常,所以通常會在 set method 加上 synchronized 來限制一次只有一個 thread 進入該 method

例如

Counter 類別的變數 c,會因為多個 thread 同時使用而異常

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

所以會改寫為

class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}

synchronized 可解決 multi-thread 問題,但會產生效能問題。

以 AtomicInteger 改寫

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

Atomic Operation

non-blocking 演算法提供了 compare-and-swap CAS 方法,可確保資料完整性。

CAS operation 使用以下三個 operands

  1. M 要操作 operate 的記憶體位置

  2. A 該變數期待的既有原始數值

  3. B 需要被設定的新數值

CAS operation 可自動 atomically 從 M 到 B 更新數值,只會在 M 確認為 A 時,更新為 B

因此可不使用 synchronization lock,完成資料的更新,沒有 thread 會被 suspended,也不需要 context switching。


常用的 Atomic Variables 有 AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference,分別代表 int, long, boolean, object reference 這些數值可自動被安全地更新,主要 methods 有

  • get()

    等同於讀取 volatile variable,可直接取得其他 thread 更新的數值

  • incrementAndGet()

  • set()

    等同於寫入 volatile variable

  • lazySet()

    最終會寫入變數,但不保證可立即被其他 thread 取得。速度比 set 更快。可用在 cache,或被 gc 的資料

  • compareAndSet()

    成功就回傳 true

  • weakCompareAndSetPlain(), weakCompareAndSetVolatile()

    weakCompareAndSet() 已被 deprecated

References

# Atomic Variables Java Tutorial

An Introduction to Atomic Variables in Java | Baeldung

java.util.concurrent.atomic (Java SE 25 & JDK 25)

Atomic Variables in Java with Examples - GeeksforGeeks

2026/04/27

FFMForeign Function and Memory (FFM) API

為了使用外部函式庫,java 需要跟原生的 native code 進行互動,傳統是使用 Java Native Interface JNI 進行開發,但 JNI 的開發過程繁複,需要定義 native method,然後從 Java Source Code 生成 C 的 header file,再用C 語言實作,然後才能編譯並連結原生開發的函式庫。

Foreign Function & Memory API(FFM API)參考了其他語言的方法,實作外部函式庫的 Interface,提供更安全有效率的方法,存取本地端的記憶體及函式,取代了 JNI 的功能。這是自 JDK 22 開始的一組新的 API,用來取代 JNI。能夠呼叫 C/C++/Rust 的非 Java 原生函式庫,可直接操作記憶體。

JNI sample

JNIDemo.java

public class JNIDemo {
    static {
        System.loadLibrary("JNIDemo");
    }

    public static void main(String[] args) {
        new JNIDemo().printHelloWorld();
    }

    private native void printHelloWorld();
}

這裡面,有一個 private native void printHelloWorld(),這就是還沒有實作的原生函式。

用以下指令,編譯 class 並產生 header file

javac -h . JNIDemo.java

JNIDemo.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */

#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNIDemo
 * Method:    printHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIDemo_printHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

編譯 C library

因為在 macos 先查詢 JAVA_HOME 目錄

$ /usr/libexec/java_home
/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home

編譯

JAVA_HOME=/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home
gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -dynamiclib JNIDemo.c -o libJNIDemo.dylib

會產生 libJNIDemo.dylib

執行

$ java -cp . -Djava.library.path=. JNIDemo
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by JNIDemo in an unnamed module (file:/Users/charley/Downloads/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Hello World!

FFM

FFM API 能夠讓 Java 程式在不使用 JNI 的情況下

  • 更安全地存取記憶體

  • 直接呼叫 C function

  • 呼叫 C/系統的 library

FFM 核心元件:

  • Linker:對應 C 語言呼叫約定的連結器,用來呼叫 native 函式

  • SymbolLookup: 在 native library 或 process 中查找符號(例如 printf

  • MemorySegment: 表示一塊連續的記憶體(on-heap 或 off-heap)

  • Arena:管理 MemorySegment 的生命週期(自動釋放資源)

  • ValueLayout:描述原生型別的記憶體布局,例如 JAVA_INT, C_DOUBLE

  • FunctionDescriptor:描述 native 函式的簽章(參數與回傳型別)

測試1

測試呼叫 strlen

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;

public class FFMStrlenDemo {
    public static void main(String[] args) throws Throwable {
        // 1. 取得系統的 Linker(如 SystemV ABI 或 Windows ABI)
        Linker linker = Linker.nativeLinker();

        // 2. 找出 symbol (在 libc 裡)
        SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
        MemorySegment strlenAddr = stdlib.find("strlen").orElseThrow();

        // 3. 建立 Java 對應的 MethodHandle
        MethodHandle strlen = linker.downcallHandle(
            strlenAddr,
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );

        // 4. 建立要傳入的字串記憶體
        // try (Arena arena = Arena.ofConfined()) {
        //     MemorySegment str = arena.allocateUtf8String("Hello FFM API!");
        //     long len = (long) strlen.invoke(str);
        //     System.out.println("Length = " + len);
        // }
        try (Arena arena = Arena.ofConfined()) {
            // 手動建立 UTF-8 null-terminated string
            byte[] bytes = "Hello FFM API!".getBytes(StandardCharsets.UTF_8);
            MemorySegment str = arena.allocate(bytes.length + 1, 1);
            str.asSlice(0, bytes.length).copyFrom(MemorySegment.ofArray(bytes));
            str.set(ValueLayout.JAVA_BYTE, bytes.length, (byte) 0); // '\0'

            long len = (long) strlen.invoke(str);
            System.out.println("Length = " + len);
        }

        // allocate set/get 測試
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT);
            seg.set(ValueLayout.JAVA_INT, 0, 42); // 寫入 42
            int value = seg.get(ValueLayout.JAVA_INT, 0);
            System.out.println("Read value: " + value);
        } // arena 自動釋放記憶體
    }
}

檢查 allocate API

標準 API 是 java.lang.foreign.Arena.allocateUtf8String(String)

但測試,結果是 allocate

# javap -classpath $JAVA_HOME/lib/modules java.lang.foreign.Arena | grep allocate

public abstract java.lang.foreign.MemorySegment allocate(long, long);

這表示目前這個 openjdk 還是使用舊版的 allocate,缺少 allocateUtf8String

編譯,執行

javac FFMStrlenDemo.java
java --enable-native-access=ALL-UNNAMED FFMStrlenDemo

結果

Length = 14
Read value: 42

測試2

sum.c

// sum.c
#include <stdio.h>

int sum(int a, int b) {
    return a + b;
}

編譯為 library

## linux
# gcc -shared -fPIC -o libsum.so sum.c

## macos
gcc -shared -fPIC -o libsum.dylib sum.c

SumFFMDemo.java 放在同一個目錄

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class SumFFMDemo {
    public static void main(String[] args) throws Throwable {
        // 載入我們的動態函式庫
        System.loadLibrary("sum"); // 對應 libsum.so / sum.dll

        // 建立 linker
        Linker linker = Linker.nativeLinker();

        // 查找 symbol
        SymbolLookup lookup = SymbolLookup.libraryLookup("libsum.dylib", Arena.global());
        MemorySegment funcAddr = lookup.find("sum").orElseThrow();

        // 建立函式描述: int (int, int)
        FunctionDescriptor fd = FunctionDescriptor.of(
                ValueLayout.JAVA_INT, // return type
                ValueLayout.JAVA_INT, // param a
                ValueLayout.JAVA_INT  // param b
        );

        // 建立 method handle
        MethodHandle sum = linker.downcallHandle(funcAddr, fd);

        // 呼叫 native 函式
        int result = (int) sum.invoke(12, 30);
        System.out.println("sum(12, 30) = " + result);
    }
}

編譯

javac SumFFMDemo.java
java --enable-native-access=ALL-UNNAMED SumFFMDemo
# java -Djava.library.path=. --enable-native-access=ALL-UNNAMED SumFFMDemo

結果

sum(12, 30) = 42