在前幾天的內容中,我們學習了 AES 與 3DES 等對稱加密演算法。許多初學者常有一種誤解:「只要資料經過加密,中間人看不懂,系統就是安全的。」
這在密碼學中是極度危險的盲點。在 Day 08 中,我們介紹了單向雜湊函數(Hash Function),知道它可以為資料生成「數位指紋」以檢查資料是否被修改。然而,在真實的網路傳輸環境中,單純使用雜湊函數是無法防範惡意攻擊者竄改資料的,亦即在Day01談到的鑑別性 (Authentication,防假冒)。
假設 Bob 要傳送一份合約 M 給 Alice,為了確保資料完整性,Alice 附帶了雜湊值 H(M):
[Bob] ─── (明文 M + 雜湊值 H(M)) ───> [中間人 Hacker] ───> [Alice]
如果在傳輸過程中存在惡意中間人 Eve:
當 Alice 收到資料後計算 H(M'),發現與封包中的雜湊值完全吻合!Alice 會以為資料完好無損,進而採信被竄改的內容。
問題根源:雜湊函數是公開且不需要金鑰的。任何人(包含攻擊者)都能自由計算任何訊息的雜湊值。因此,雜湊只能防範「非人為的傳送錯誤(如網路雜訊)」,無法防範「有心人的惡意竄改」。
訊息鑑別碼(MAC)是一種結合了共享金鑰(Secret Key)與訊息內容的密碼學技術:
Tag = MAC(K, M)
[Bob] ─── (明文 M + 標籤 Tag) ───> [中間人 Hacker] ───> [Alice]
當 Eve 再次嘗試攔截並修改 M 為 M' 時,因為 Eve 不知道金鑰 K,他無法計算出對應 M' 的合法 Tag。當 Alice 收到資料並使用自己的金鑰 K 計算 MAC(K, M') 時,會發現與傳送過來的 Tag 不符,進而立刻發現資料遭到竄改並予以拋棄。
根據系統對「保密」與「效能」的需求不同,MAC 主要有以下三種應用模式:
{"user_id": 1002, "role": "admin"} 以明文 Base64 傳送,末尾附帶 MAC 標籤。使用者若擅自改為 "role": "admin",後端驗證 MAC 失敗會立刻剔除。在現代密碼學工程中,三大主流 MAC 運算機制各自代表了不同的數學路線與硬體優化哲學:

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();
}
}
}
在比對 MAC 標籤時,**嚴禁使用一般的 String.equals() 或 Arrays.equals()**!
equals):採用短路機制,一旦發現第一個 Byte 不匹配就會立刻返回 false(耗時極短);匹配的 Byte 越多,耗時越長。攻擊者可藉由微秒級的時間差,逐個位元推算出正確的 Tag(即時序攻擊 Side-Channel Attack)。MessageDigest.isEqual):無論中間結果如何,永遠跑完所有 Byte 的完整比對,不給攻擊者利用時間差推算資料的機會。