iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Software Development

我是Java工程師,關於密碼學我想懂的不多系列 第 22 篇

Day21 - 現代認證加密 (AEAD):AES-GCM 與 ChaCha20-Poly1305

  • 分享至 

  • xImage
  •  

前言

在前面的章節中,我們分別探討了負責「資料機密性(Confidentiality)」的對稱加密(AES、ChaCha20),以及負責「資料完整性與真實性(Integrity & Authenticity)」的訊息鑑別碼(MAC)。

在過去,開發者如果需要同時保護資料不被偷看且不被竄改,必須手動將加密演算法與 MAC 結合。然而,這種手動組合的方式極易引發嚴重的密碼學安全漏洞。為了從根本解決這個問題,現代密碼學推出了認證加密(Authenticated Encryption with Associated Data,簡稱 AEAD)。

一、 為什麼我們需要 AEAD?傳統組合方式的陷阱

單純加密(如 AES)只能「防偷看」,不能「防篡改」。早期如果想同時達成這兩個目標,工程師必須手動把「加密演算法」與「MAC 演算法」組裝在一起。

然而,這兩種機制的執行順序至關重要,傳統上有三種組合方式,前兩種都藏有致命陷阱:
https://ithelp.ithome.com.tw/upload/images/20261004/20128084C1JjbBVaWe.png

二、 既然 EtM 數學上安全,為什麼我們還需要 AEAD?

儘管 Encrypt-then-MAC (EtM) 在密碼學理論上被證明是安全的,但在實際工程開發中,手動組裝這套流程對一般工程師而言極度繁瑣且充滿陷阱:

工程實作中常見的 4 大致命漏洞:

  1. 違反金鑰分離原則:開發者常圖方便,拿同一把 key 同時做加密與 MAC(K11 = K2),破壞密碼學安全假設。
  2. 遺漏 IV / Nonce 或標頭資料:計算 MAC 時漏掉了 IV 或訊息標頭(Header),導致攻擊者可以替換 IV 進行位元反轉攻擊。
  3. 時序攻擊漏洞(Timing Attack):在驗證 MAC 標籤時,使用了語言內建的 String.equals() 或 Arrays.equals(),未採用固定時間比對(Constant-Time Comparison)。
  4. 解密順序顛倒:工程師寫程式時不小心「先呼叫解密,再檢查 MAC」,導致 EtM 的安全優勢瞬間崩塌。

三、 AEAD 的核心要素與附加資料 (AAD)

1.什麼是附加資料 (AAD, Additional Authenticated Data)?

在實際的網路傳輸中,許多資料標頭(Header,如 IP 位址、通訊協定版本、封包序號)必須以明文傳輸(否則路由器無法轉發),但這些標頭絕不能被中間人竄改。

AAD 就是專門為這類「不需要保密,但需要驗證真實性」的資料設計的:

  • AEAD 演算法只會加密明文(Plaintext),產出密文(Ciphertext)。
  • 但 AEAD 在生成認證標籤(Auth Tag)時,會將 密文 + AAD 一併納入計算。
  • 若攻擊者在傳輸中竄改了明文標頭(AAD),解密時的 Auth Tag 驗證依然會失敗。

AEAD 的加密過程接收四個輸入,並產出兩個輸出:

               +-------------------------------------------+
  Plaintext -->|                                           |--> Ciphertext
        Key -->|              AEAD Encryption              |
      Nonce -->|                                           |
        AAD -->| (Additional Authenticated Data, 不加密)   |--> Auth Tag
               +-------------------------------------------+

2.AEAD 帶來了兩大革命性改進:

(1) 原子化操作(Atomic Operation):
AEAD 將「加密」與「鑑別標籤(Auth Tag)計算」高度封裝在單一演算法(如 AES-GCM、ChaCha20-Poly1305)與 API 中。
在 Java 呼叫 cipher.doFinal() 時,底層保證:要麼成功還原明文,要麼驗證失敗直接拋出 AEADBadTagException**,中間不留任何供攻擊者利用的喘息空間。
(2) 原生支援附加資料認證(Associated Data, AAD):
在真實網路傳輸中,許多資料(例如 TCP/IP 標頭、HTTP Header、路由資訊)不能被加密(否則路由器無法傳送),但又
絕對不能被篡改**。
AEAD 允許傳入 AAD(Associated Data):AAD 保持明文傳送,但會與密文一起被納入 Auth Tag 的計算中,完美解決了「公開標頭防篡改」的需求。

四、 兩大現代主流 AEAD 演算法

目前主流的 AEAD 演算法主要有兩款,分別代表了 NIST 與 RFC 體系:

1. AES-GCM (Galois/Counter Mode) — NIST FIPS 標準

  • 機制:結合了 CTR 模式加密 與 GMAC(基於伽羅瓦體 $\mathbb{GF}(2^{128})$ 的認證碼)。
  • 優點:硬體加速極佳。現代 x86 與 ARM 處理器都有專屬的 AES-NI 與 PCLMULQDQ 指令集,AES-GCM 的運算吞吐量極高。
  • 規範:符合 NIST SP 800-38D,是 FIPS 140-3 官方核可的標準 AEAD。
  • 致命弱點:Nonce 極度敏感。若使用相同金鑰且重複使用了 Nonce,GMAC 的認證金鑰會瞬間洩漏,導致攻擊者可任意偽造密文。

2. ChaCha20-Poly1305 — RFC 8439 規範

  • 機制:結合了 ChaCha20 串流加密 與 Poly1305 MAC。
  • 優點:純軟體運算速度極快,在缺乏 AES 硬體加速指令集的裝置(如舊款手機、IoT 設備)上表現優異。
  • 規範:IETF RFC 8439,是 TLS 1.3 與 WireGuard VPN 的核心加密選項。

五、 Java JCA 實戰

在 Java 中,處理 AEAD 不需要額外的第三方套件(JDK 9+ 內建完整支援)。當呼叫 cipher.doFinal() 時,JCA 會自動將產出的 Auth Tag 附加在密文的最末端;解密時,JCA 也會自動從密文末端切出 Auth Tag 進行驗證。

1. AES-GCM 實作範例

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.SecureRandom;
import java.util.Base64;

public class AesGcmExample {

    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int TAG_LENGTH_BITS = 128; // 推薦 128 bits (16 bytes) Auth Tag
    private static final int IV_LENGTH_BYTES = 12;  // GCM 推薦標準 IV 長度為 96 bits (12 bytes)

    public static byte[] encrypt(byte[] plaintext, SecretKey key, byte[] iv, byte[] aad) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);

        // 若有 AAD 資料,必須在 doFinal 前呼叫 updateAAD
        if (aad != null && aad.length > 0) {
            cipher.updateAAD(aad);
        }

        // 產出的 ciphertext 包含了:[密文內容] + [16 Bytes 的 Auth Tag]
        return cipher.doFinal(plaintext);
    }

    public static byte[] decrypt(byte[] cipherTextWithTag, SecretKey key, byte[] iv, byte[] aad) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
        cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);

        if (aad != null && aad.length > 0) {
            cipher.updateAAD(aad);
        }

        // 若 AAD 被竄改或密文/Tag 不符,doFinal 會直接拋出 AEADBadTagException
        return cipher.doFinal(cipherTextWithTag);
    }

    public static void main(String[] args) {
        try {
            // 準備資料
            String plaintext = "敏感交易資料:轉帳 $5,000 至 Bob";
            String headerAAD = "Packet-Seq-No: 10042"; // 不需要加密但需要鑑別的 Header
        
            KeyGenerator keyGen = KeyGenerator.getInstance("AES");
            keyGen.init(256);
            SecretKey key = keyGen.generateKey();

            byte[] iv = new byte[IV_LENGTH_BYTES];
            new SecureRandom().nextBytes(iv); // 隨機生成 12-byte IV

            // 1. 加密
            byte[] ciphertext = encrypt(plaintext.getBytes(), key, iv, headerAAD.getBytes());
            System.out.println("密文 + Auth Tag (Base64): " + Base64.getEncoder().encodeToString(ciphertext));

            // 2. 正常解密
            byte[] decrypted = decrypt(ciphertext, key, iv, headerAAD.getBytes());
            System.out.println("解密結果: " + new String(decrypted));

            // 3. 模擬中間人竄改 AAD Header
            String tamperedAAD = "Packet-Seq-No: 10043";
            System.out.print("嘗試使用被竄改的 AAD 解密: ");
            decrypt(ciphertext, key, iv, tamperedAAD.getBytes()); // 將觸發例外

        } catch (javax.crypto.AEADBadTagException e) {
            System.out.println("驗證失敗!資料或 Header 遭到竄改 (AEADBadTagException)");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. ChaCha20-Poly1305 實作範例

在 Java 11+ 中,ChaCha20-Poly1305 的呼叫方式與 AES-GCM 非常相似,唯一的差異在於初始化參數使用的是 IvParameterSpec:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.security.SecureRandom;

public class ChaChaPolyExample {

    public static void main(String[] args) throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance("ChaCha20");
        keyGen.init(256);
        SecretKey key = keyGen.generateKey();

        byte[] nonce = new byte[12]; // RFC 8439 規定 12 Bytes Nonce
        new SecureRandom().nextBytes(nonce);

        String plaintext = "Hello ChaCha20-Poly1305!";
        byte[] aad = "Protocol-Version-2".getBytes();

        // 加密
        Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305");
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(nonce));
        cipher.updateAAD(aad);
        byte[] cipherTextWithTag = cipher.doFinal(plaintext.getBytes());

        // 解密
        Cipher decryptCipher = Cipher.getInstance("ChaCha20-Poly1305");
        decryptCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(nonce));
        decryptCipher.updateAAD(aad);
        byte[] decrypted = decryptCipher.doFinal(cipherTextWithTag);

        System.out.println("ChaCha20-Poly1305 解密結果: " + new String(decrypted));
    }
}

上一篇
Day20 - 手動打造安全的經典模式:Encrypt-then-MAC 結合 HKDF 金鑰分離
系列文
我是Java工程師,關於密碼學我想懂的不多 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言