在現代密碼學中,ChaCha20(RFC 8439)已全面取代舊時代的 RC4,成為現代串流加密(Stream Cipher)的主流。今日我們就來透過使用Java JCA的實際範例來理解加解密的完整過程。
ChaCha20 本質上是一個確定的金鑰流產生器(Deterministic Keystream Generator)。它以 64 Bytes(512 bits)為一個 Block 單位,透過以下三個輸入參數生成對應的金鑰流(Keystream):
金鑰流 = ChaCha20Block(Secret Key, Nonce, Counter)
1。在本地端算出金鑰流 K 後,加密與解密完全依賴經典的 XOR 位元運算:
加密的過程中會使用到IV/nonce對方解密時怎麼知道你的IV/nonce呢?答案是IV / Nonce 通常會直接「明文附在密文前面」一起傳送給對方,常見的做法是拼接在密文開頭 (Prefix)
[ 12 Bytes IV (明文) ] + [ 加密後的密文 (Ciphertext) ]
簡單的五步驟就可以對明文進行加密,解密也是相同的只要只要在步驟4設定為`DECRYPT_MODE即可
Java 從 JDK 11 (JEP 329) 開始原生支援 ChaCha20 演算法。以下範本示範了標準的 JCA 五階段生命週期,並包含實務中 Payload 打包與拆解的完整流程。
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.ChaCha20ParameterSpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
public class ChaCha20Demo {
private static final String ALGORITHM = "ChaCha20";
private static final int KEY_SIZE_BITS = 256;
private static final int NONCE_SIZE_BYTES = 12; // RFC 8439 標準 12-byte Nonce
private static final int INITIAL_COUNTER = 1; // 加密訊息預設 Counter 起始值為 1
public static void main(String[] args) throws Exception {
String originalText = "Hello IronMan! 這是一段 ChaCha20 串流加密測試訊息。";
// Step 1: 產生 256-bit ChaCha20 主金鑰
SecretKey secretKey = generateKey();
// Step 2: 執行加密,產出封裝好的 Payload [ Nonce (12B) + Ciphertext ]
byte[] encryptedPayload = encrypt(originalText, secretKey);
System.out.println("加密後的 Payload (Base64): " + Base64.getEncoder().encodeToString(encryptedPayload));
// Step 3: 執行解密
String decryptedText = decrypt(encryptedPayload, secretKey);
System.out.println("解密後的明文內容: " + decryptedText);
}
/**
* 階段 1:產生 256-bit ChaCha20 對稱金鑰
*/
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
keyGen.init(KEY_SIZE_BITS);
return keyGen.generateKey();
}
/**
* 階段 2~5:加密流程
*/
public static byte[] encrypt(String plaintext, SecretKey key) throws Exception {
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
// 階段 3a: 生成強強度的隨機 Nonce (IV)
byte[] nonce = new byte[NONCE_SIZE_BYTES];
SecureRandom.getInstanceStrong().nextBytes(nonce);
// 階段 3b: 封裝參數 Specification (Nonce + Counter)
ChaCha20ParameterSpec paramSpec = new ChaCha20ParameterSpec(nonce, INITIAL_COUNTER);
// 階段 2: 取得 Cipher 實例
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 階段 4: 初始化 Cipher 為 ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, key, paramSpec);
// 階段 5: 執行最後 XOR 運算,產出密文
byte[] ciphertext = cipher.doFinal(plaintextBytes);
// 工程打包: 將 [ Nonce (12 Bytes) ] 拼接在 [ 密文 ] 前端傳輸
ByteBuffer payload = ByteBuffer.allocate(nonce.length + ciphertext.length);
payload.put(nonce);
payload.put(ciphertext);
return payload.array();
}
/**
* 解密流程
*/
public static String decrypt(byte[] payload, SecretKey key) throws Exception {
if (payload.length < NONCE_SIZE_BYTES) {
throw new IllegalArgumentException("Payload 長度不足,無法提取 Nonce!");
}
// 1. 拆解 Payload:切出前 12 Bytes 的 Nonce
byte[] nonce = Arrays.copyOfRange(payload, 0, NONCE_SIZE_BYTES);
// 2. 拆解 Payload:提取剩餘的密文
byte[] ciphertext = Arrays.copyOfRange(payload, NONCE_SIZE_BYTES, payload.length);
// 3. 使用提取出的 Nonce 與約定好的 Initial Counter 初始化解密器
ChaCha20ParameterSpec paramSpec = new ChaCha20ParameterSpec(nonce, INITIAL_COUNTER);
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 4. 設定模式為 DECRYPT_MODE
cipher.init(Cipher.DECRYPT_MODE, key, paramSpec);
// 5. 本地計算相同的金鑰流並執行反向 XOR 解密
byte[] decryptedBytes = cipher.doFinal(ciphertext);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
}
在 Java 呼叫 cipher.doFinal(plaintext) 時,你不需要手動指定金鑰流要多長,因為 Cipher 引擎會根據你傳入的明文長度,動態生成對應長度的金鑰流。
以 ChaCha20 為例,它的底層運作方式如下:
ChaCha20 屬於以區塊為單位生成金鑰流(Block-based Keystream Generator)的串流密碼。它的內部算法每次會產出 64 Bytes(512 bits) 的金鑰流區塊。
counter 的核心作用是標記當前正在生成「第幾個 64-Byte 金鑰流區塊」,具體效益如下:
ChaCha20 內部的數學運算狀態由 16 個 32-bit 的數字組成(包含 Constant、Key、Nonce 與 Counter)。
counter = 1
counter = 2
counter = 3
因為 Counter 每 64 Bytes 就會自動 $+1$,這保證了即使 Key 與 Nonce 完全不變,第 1 個區塊與第 2 個區塊算出來的金鑰流也完全不同。
傳統的串流密碼(如 RC4)如果要解密第 1,000,000 個 Byte 的資料,必須先從第 1 個 Byte 依序計算到第 999,999 個 Byte。
但 ChaCha20 只要知道 offset,就能直接推算出對應的 counter 值。例如:
要讀取第 $1280$ 個 Byte(位於第 21 個區塊,因為 $1280 / 64 = 20$),直接把 counter 設定為 21,就能跳過前面的資料,單獨計算該區塊進行解密。
1?在 RFC 7539 / RFC 8439 標準規範中,ChaCha20 加密資料時的初始 Counter 通常設為 1(0 往往被留給帶認證的 ChaCha20-Poly1305 模式用來生成 Poly1305 的 One-Time Key)。