iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Software Development

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

Day19 - 訊息鑑別碼 (Message Authention Code,MAC)

  • 分享至 

  • xImage
  •  

前言

在前幾天的內容中,我們學習了 AES 與 3DES 等對稱加密演算法。許多初學者常有一種誤解:「只要資料經過加密,中間人看不懂,系統就是安全的。」

這在密碼學中是極度危險的盲點。在 Day 08 中,我們介紹了單向雜湊函數(Hash Function),知道它可以為資料生成「數位指紋」以檢查資料是否被修改。然而,在真實的網路傳輸環境中,單純使用雜湊函數是無法防範惡意攻擊者竄改資料的,亦即在Day01談到的鑑別性 (Authentication,防假冒)。

一、 為什麼單向雜湊 (Hash) 還不夠?

假設 Bob 要傳送一份合約 M 給 Alice,為了確保資料完整性,Alice 附帶了雜湊值 H(M):

[Bob] ─── (明文 M + 雜湊值 H(M)) ───> [中間人 Hacker] ───> [Alice]

如果在傳輸過程中存在惡意中間人 Eve:

  1. Hacker 攔截封包,將明文 M 修改為竄改後的 M'(例如將轉帳金額 1,000 改為 100,000)。
  2. Hacker 重新計算 H(M'),並將原有的雜湊值替換掉。
  3. Hacker 將 (M', H(M')) 發送給 Alice。

當 Alice 收到資料後計算 H(M'),發現與封包中的雜湊值完全吻合!Alice 會以為資料完好無損,進而採信被竄改的內容。

問題根源:雜湊函數是公開且不需要金鑰的。任何人(包含攻擊者)都能自由計算任何訊息的雜湊值。因此,雜湊只能防範「非人為的傳送錯誤(如網路雜訊)」,無法防範「有心人的惡意竄改」。

二、 什麼是訊息鑑別碼 (MAC)?

訊息鑑別碼(MAC)是一種結合了共享金鑰(Secret Key)與訊息內容的密碼學技術:

Tag = MAC(K, M)

  • K:Alice 與 Bob 事先約定好的共享金鑰(秘密)。
  • M:要傳輸的訊息。
  • Tag:生成的認證標籤(Authentication Tag)。
[Bob] ─── (明文 M + 標籤 Tag) ───> [中間人 Hacker] ───> [Alice]

當 Eve 再次嘗試攔截並修改 M 為 M' 時,因為 Eve 不知道金鑰 K,他無法計算出對應 M' 的合法 Tag。當 Alice 收到資料並使用自己的金鑰 K 計算 MAC(K, M') 時,會發現與傳送過來的 Tag 不符,進而立刻發現資料遭到竄改並予以拋棄。

MAC 提供的兩大安全保障

  1. 資料完整性 (Integrity):確保訊息在傳送過程中沒有被變更。
  2. 來源真實性 (Authenticity):確保訊息確實來自持有共享金鑰 K 的合法傳送者。

三、 MAC 的三大工程使用場景

根據系統對「保密」與「效能」的需求不同,MAC 主要有以下三種應用模式:

場景 1:明文 + MAC(內容無須保密,但極害怕篡改與假冒)

  • 核心邏輯:「明文傳輸」不等於「資料不重要」!許多資料(如 API Webhook、JWT 憑證、韌體更新檔)本身不需要隱藏內容,甚至需要公開讓第三方或前端直接讀取,但對防篡改與來源認證的要求極高。
  • 經典範例:
  • API Webhook (如 LINE Bot / GitHub / 綠界金流):通知內容為明文 JSON,但 Header 附帶 HMAC 標籤,確保請求確實由原廠發出,而非駭客偽造的扣款通知。
  • JWT (JSON Web Token):{"user_id": 1002, "role": "admin"} 以明文 Base64 傳送,末尾附帶 MAC 標籤。使用者若擅自改為 "role": "admin",後端驗證 MAC 失敗會立刻剔除。
  • 韌體更新檔:作業系統更新包公開下載(無須加密),但附帶 MAC / 數位簽章,防止半路被植入木馬。

場景 2:傳統「加密 + MAC」組合(Encrypt-then-MAC)

  • 核心邏輯:在採用現代 AEAD 模式前,經典的密碼學實踐要求「先加密,再對密文做 MAC」。
  • 運作流程:
  1. 使用金鑰 K1 將明文加密為密文 C。
  2. 使用金鑰 K2 對密文計算標籤 T = MAC(K2, IV || C)。
  3. 接收端必須先驗證 T 是否正確,通過後才允許解密 C。若 T 錯誤則直接丟棄,完全不觸碰解密引擎,從根本杜絕 Padding Oracle 攻擊。

場景 3:現代整合方案(AEAD 模式,如 AES-GCM / ChaCha20-Poly1305)

  • 核心邏輯:現代工程不再手動分步做「加密 + HMAC」,而是直接採用 AEAD(Authenticated Encryption with Associated Data)。
  • 運作流程:加密引擎在產出密文的同時,自動在末尾附帶 128-bit Auth Tag。解密時底層自動進行驗證,若遭篡改直接拋出異常。

四、 主流 MAC 機制解析:HMAC、Poly1305 與 GHASH (GMAC)

在現代密碼學工程中,三大主流 MAC 運算機制各自代表了不同的數學路線與硬體優化哲學:

https://ithelp.ithome.com.tw/upload/images/20261004/201280842pNrpN10Qx.png

五、 Java JCA 實戰:javax.crypto.Mac

在 Java 中,處理 MAC 運算的核心類別是 javax.crypto.Mac。其生命週期與前面學過的 Cipher 極為相似:getInstance() -> init() -> update() -> doFinal()。

以下為使用 HMAC-SHA256 產生標籤與進行安全驗證的完整範例:

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

public class HmacExample {

    private static final String ALGORITHM = "HmacSHA256";

    // 1. 生成 HMAC 專用金鑰
    public static SecretKey generateKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
        keyGen.init(256, new SecureRandom());
        return keyGen.generateKey();
    }

    // 2. 計算 HMAC Tag
    public static byte[] computeMac(byte[] data, SecretKey key) throws Exception {
        Mac mac = Mac.getInstance(ALGORITHM);
        mac.init(key);
        return mac.doFinal(data);
    }

    // 3. 驗證 HMAC Tag (防範時序攻擊)
    public static boolean verifyMac(byte[] data, SecretKey key, byte[] expectedMac) throws Exception {
        byte[] actualMac = computeMac(data, key);
        // 關鍵:必須使用 MessageDigest.isEqual 進行等時比對
        return MessageDigest.isEqual(actualMac, expectedMac);
    }

    public static void main(String[] args) {
        try {
            String message = "Action=Transfer&Amount=1000&To=Alice";
            SecretKey key = generateKey();

            // 產生 MAC 標籤
            byte[] tag = computeMac(message.getBytes(), key);
            System.out.println("原始訊息: " + message);
            System.out.println("HMAC-SHA256 Tag (Base64): " + Base64.getEncoder().encodeToString(tag));

            // 驗證成功案例
            boolean isValid = verifyMac(message.getBytes(), key, tag);
            System.out.println("原始訊息驗證結果: " + (isValid ? "成功 (未被竄改)" : "失敗"));

            // 模擬中間人竄改資料
            String tamperedMessage = "Action=Transfer&Amount=100000&To=Alice";
            boolean isTamperedValid = verifyMac(tamperedMessage.getBytes(), key, tag);
            System.out.println("竄改訊息驗證結果: " + (isTamperedValid ? "成功" : "失敗 (成功攔截竄改)"));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

需注意的資安陷阱:時序攻擊(Timing Attack)

在比對 MAC 標籤時,**嚴禁使用一般的 String.equals() 或 Arrays.equals()**!

  • 普通比對 (equals):採用短路機制,一旦發現第一個 Byte 不匹配就會立刻返回 false(耗時極短);匹配的 Byte 越多,耗時越長。攻擊者可藉由微秒級的時間差,逐個位元推算出正確的 Tag(即時序攻擊 Side-Channel Attack)。
  • 固定時間比對 (MessageDigest.isEqual):無論中間結果如何,永遠跑完所有 Byte 的完整比對,不給攻擊者利用時間差推算資料的機會。

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

尚未有邦友留言

立即登入留言