加密与签名技术之消息认证码

57 阅读8分钟

概述

消息认证码(MAC)用于验证数据完整性和真实性,确保消息未被篡改且来自预期发送者。

目录

  1. HMAC
  2. CMAC
  3. GMAC
  4. Poly1305
  5. 性能对比

HMAC

原理

HMAC (Hash-based Message Authentication Code) 使用哈希函数(如SHA-256)和密钥生成MAC。

算法:

HMAC(K, m) = H((K ⊕ opad) || H((K ⊕ ipad) || m))

技术规格

属性
哈希函数SHA-256, SHA-384, SHA-512等
密钥长度建议等于哈希输出长度
输出长度等于哈希输出长度
安全级别

应用场景

  1. API认证:请求签名验证
  2. JWT:HMAC-SHA256签名
  3. OAuth:请求签名
  4. 消息完整性:数据传输验证
  5. 密码派生:密钥派生函数
  6. TLS:某些场景使用

性能影响

  • 计算速度:快(取决于哈希函数)
  • 内存占用:低
  • CPU使用率:低
  • 吞吐量:高

Java实现示例

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
​
public class HMACExample {
    
    /**
     * 生成HMAC
     */
    public static String generateHMAC(String data, String key, String algorithm) throws Exception {
        Mac mac = Mac.getInstance(algorithm);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
        mac.init(secretKeySpec);
        byte[] macBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(macBytes);
    }
    
    /**
     * HMAC-SHA256(最常用)
     */
    public static String hmacSHA256(String data, String key) throws Exception {
        return generateHMAC(data, key, "HmacSHA256");
    }
    
    /**
     * HMAC-SHA512
     */
    public static String hmacSHA512(String data, String key) throws Exception {
        return generateHMAC(data, key, "HmacSHA512");
    }
    
    /**
     * 验证HMAC
     */
    public static boolean verifyHMAC(String data, String receivedMAC, String key, String algorithm) 
            throws Exception {
        String computedMAC = generateHMAC(data, key, algorithm);
        return computedMAC.equals(receivedMAC);
    }
    
    /**
     * 生成随机密钥
     */
    public static String generateKey(int keyLengthBytes) {
        byte[] key = new byte[keyLengthBytes];
        SecureRandom random = new SecureRandom();
        random.nextBytes(key);
        return Base64.getEncoder().encodeToString(key);
    }
    
    /**
     * API请求签名示例
     */
    public static class APISignature {
        /**
         * 签名API请求
         */
        public static String signRequest(String method, String path, String body, 
                                        String timestamp, String key) throws Exception {
            // 构造签名字符串
            String signString = method + "\n" + path + "\n" + body + "\n" + timestamp;
            return hmacSHA256(signString, key);
        }
        
        /**
         * 验证API请求签名
         */
        public static boolean verifyRequest(String method, String path, String body, 
                                           String timestamp, String receivedSignature, 
                                           String key) throws Exception {
            String computedSignature = signRequest(method, path, body, timestamp, key);
            return computedSignature.equals(receivedSignature);
        }
    }
    
    /**
     * 完整示例
     */
    public static void main(String[] args) throws Exception {
        String data = "这是要认证的消息";
        String key = "secret-key-12345";
        
        System.out.println("=== HMAC生成和验证 ===");
        String hmac = hmacSHA256(data, key);
        System.out.println("数据: " + data);
        System.out.println("密钥: " + key);
        System.out.println("HMAC-SHA256: " + hmac);
        
        boolean isValid = verifyHMAC(data, hmac, key, "HmacSHA256");
        System.out.println("验证结果: " + isValid);
        
        // 测试数据被篡改
        String tamperedData = "这是被篡改的消息";
        boolean isTamperedValid = verifyHMAC(tamperedData, hmac, key, "HmacSHA256");
        System.out.println("篡改后验证: " + isTamperedValid);
        
        System.out.println("\n=== 不同哈希算法对比 ===");
        String[] algorithms = {"HmacSHA256", "HmacSHA384", "HmacSHA512"};
        for (String alg : algorithms) {
            String mac = generateHMAC(data, key, alg);
            System.out.println(alg + ": " + mac.substring(0, 30) + "...");
        }
        
        System.out.println("\n=== API请求签名示例 ===");
        String apiKey = generateKey(32);
        String method = "POST";
        String path = "/api/v1/users";
        String body = "{"name":"test"}";
        String timestamp = String.valueOf(System.currentTimeMillis());
        
        String signature = APISignature.signRequest(method, path, body, timestamp, apiKey);
        System.out.println("请求签名: " + signature);
        
        boolean isValidRequest = APISignature.verifyRequest(
            method, path, body, timestamp, signature, apiKey);
        System.out.println("请求验证: " + isValidRequest);
        
        System.out.println("\n=== 性能测试 ===");
        String testData = "性能测试数据" + "x".repeat(1000);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            hmacSHA256(testData, key);
        }
        long end = System.currentTimeMillis();
        System.out.println("HMAC-SHA256 (10000次): " + (end - start) + "ms");
    }
}

安全建议

推荐:

  • 使用HMAC-SHA256(性能与安全性平衡)
  • 密钥长度至少等于哈希输出长度
  • 使用密码学安全的随机数生成密钥
  • 使用安全比较(防止时序攻击)

⚠️ 注意:

  • 密钥必须保密
  • 不同消息使用不同密钥或nonce
  • 定期轮换密钥

CMAC

原理

CMAC (Cipher-based Message Authentication Code) 基于分组密码(如AES)生成MAC。

技术规格

属性
底层密码AES, DES等
密钥长度128/192/256位(AES)
输出长度等于块大小(AES为128位)
安全级别

应用场景

  1. 金融系统:某些支付协议
  2. 需要基于分组密码的MAC
  3. 与AES加密配合使用

性能影响

  • 计算速度:中等(取决于底层密码)
  • 内存占用:低
  • CPU使用率:中等

Java实现示例

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
​
public class CMACExample {
    
    /**
     * 生成AES-CMAC
     */
    public static String generateCMAC(String data, byte[] key) throws Exception {
        Mac mac = Mac.getInstance("AESCMAC");
        SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
        mac.init(keySpec);
        byte[] macBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(macBytes);
    }
    
    /**
     * 验证CMAC
     */
    public static boolean verifyCMAC(String data, String receivedMAC, byte[] key) throws Exception {
        String computedMAC = generateCMAC(data, key);
        return computedMAC.equals(receivedMAC);
    }
    
    /**
     * 生成AES密钥
     */
    public static byte[] generateAESKey(int keyLengthBits) {
        byte[] key = new byte[keyLengthBits / 8];
        SecureRandom random = new SecureRandom();
        random.nextBytes(key);
        return key;
    }
    
    public static void main(String[] args) throws Exception {
        System.out.println("=== AES-CMAC ===");
        
        // 注意:AESCMAC可能需要Java 11+或BouncyCastle
        try {
            byte[] key = generateAESKey(256);
            String data = "测试数据";
            
            String cmac = generateCMAC(data, key);
            System.out.println("数据: " + data);
            System.out.println("CMAC: " + cmac);
            
            boolean isValid = verifyCMAC(data, cmac, key);
            System.out.println("验证结果: " + isValid);
        } catch (Exception e) {
            System.out.println("CMAC需要Java 11+或BouncyCastle库支持");
            System.out.println("错误: " + e.getMessage());
        }
    }
}

GMAC

原理

GMAC (Galois Message Authentication Code) 是GCM模式的认证部分,基于Galois域运算。

技术规格

属性
与GCM结合
密钥长度128/192/256位(AES)
标签长度通常128位
安全级别

应用场景

  1. AES-GCM加密:自动提供认证
  2. 需要认证加密的场景
  3. 高性能认证需求

性能影响

  • 计算速度:快(硬件加速)
  • 内存占用:低
  • CPU使用率:低(硬件加速时)

Java实现示例

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
​
public class GMACExample {
    
    /**
     * AES-GCM加密(包含GMAC认证)
     */
    public static class EncryptedData {
        public String encryptedData;
        public String iv;
        public String tag;
        
        public EncryptedData(String encryptedData, String iv, String tag) {
            this.encryptedData = encryptedData;
            this.iv = iv;
            this.tag = tag;
        }
    }
    
    /**
     * AES-GCM加密(自动生成认证标签)
     */
    public static EncryptedData encryptWithAuth(String plaintext, SecretKeySpec key) throws Exception {
        byte[] iv = new byte[12];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);
        
        GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec);
        
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        
        return new EncryptedData(
            Base64.getEncoder().encodeToString(ciphertext),
            Base64.getEncoder().encodeToString(iv),
            "" // GCM标签包含在密文中
        );
    }
    
    /**
     * AES-GCM解密(自动验证GMAC)
     */
    public static String decryptWithAuth(EncryptedData encrypted, SecretKeySpec key) throws Exception {
        byte[] iv = Base64.getDecoder().decode(encrypted.iv);
        byte[] ciphertext = Base64.getDecoder().decode(encrypted.encryptedData);
        
        GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);
        
        // 如果认证失败,会抛出异常
        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext, StandardCharsets.UTF_8);
    }
    
    public static void main(String[] args) throws Exception {
        System.out.println("=== AES-GCM with GMAC ===");
        
        byte[] keyBytes = new byte[32];
        SecureRandom random = new SecureRandom();
        random.nextBytes(keyBytes);
        SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
        
        String plaintext = "需要加密和认证的数据";
        EncryptedData encrypted = encryptWithAuth(plaintext, key);
        
        System.out.println("原文: " + plaintext);
        System.out.println("IV: " + encrypted.iv);
        System.out.println("密文: " + encrypted.encryptedData);
        
        String decrypted = decryptWithAuth(encrypted, key);
        System.out.println("解密: " + decrypted);
        
        // 尝试篡改密文(会认证失败)
        try {
            EncryptedData tampered = new EncryptedData(
                Base64.getEncoder().encodeToString("篡改的密文".getBytes()),
                encrypted.iv,
                encrypted.tag
            );
            decryptWithAuth(tampered, key);
            System.out.println("⚠️ 篡改未被检测到!");
        } catch (Exception e) {
            System.out.println("✓ 篡改被检测到: " + e.getMessage());
        }
    }
}

Poly1305

原理

Poly1305是基于模运算的消息认证码,通常与ChaCha20结合使用(ChaCha20-Poly1305)。

技术规格

属性
输出长度128位
密钥长度256位
安全级别
常用组合ChaCha20-Poly1305

应用场景

  1. ChaCha20加密:ChaCha20-Poly1305认证加密
  2. TLS:某些TLS实现
  3. VPN:WireGuard协议
  4. 高性能场景:软件实现性能好

性能影响

  • 计算速度:非常快(软件实现)
  • 内存占用:低
  • CPU使用率:低

Java实现示例

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
​
public class Poly1305Example {
    
    /**
     * ChaCha20-Poly1305加密(包含Poly1305认证)
     */
    public static String encryptWithAuth(String plaintext, SecretKeySpec key) throws Exception {
        byte[] nonce = new byte[12];
        SecureRandom random = new SecureRandom();
        random.nextBytes(nonce);
        
        IvParameterSpec ivSpec = new IvParameterSpec(nonce);
        Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305");
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        
        // 组合nonce和密文
        byte[] combined = new byte[12 + ciphertext.length];
        System.arraycopy(nonce, 0, combined, 0, 12);
        System.arraycopy(ciphertext, 0, combined, 12, ciphertext.length);
        
        return Base64.getEncoder().encodeToString(combined);
    }
    
    /**
     * ChaCha20-Poly1305解密(自动验证Poly1305)
     */
    public static String decryptWithAuth(String encryptedData, SecretKeySpec key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(encryptedData);
        
        byte[] nonce = new byte[12];
        System.arraycopy(combined, 0, nonce, 0, 12);
        byte[] ciphertext = new byte[combined.length - 12];
        System.arraycopy(combined, 12, ciphertext, 0, ciphertext.length);
        
        IvParameterSpec ivSpec = new IvParameterSpec(nonce);
        Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305");
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        
        // 如果认证失败,会抛出异常
        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext, StandardCharsets.UTF_8);
    }
    
    public static void main(String[] args) throws Exception {
        System.out.println("=== ChaCha20-Poly1305 ===");
        
        // 注意:需要Java 11+
        try {
            byte[] keyBytes = new byte[32];
            SecureRandom random = new SecureRandom();
            random.nextBytes(keyBytes);
            SecretKeySpec key = new SecretKeySpec(keyBytes, "ChaCha20");
            
            String plaintext = "需要加密和认证的数据";
            String encrypted = encryptWithAuth(plaintext, key);
            System.out.println("原文: " + plaintext);
            System.out.println("加密: " + encrypted);
            
            String decrypted = decryptWithAuth(encrypted, key);
            System.out.println("解密: " + decrypted);
            
            // 尝试篡改
            try {
                String tampered = encrypted.substring(0, encrypted.length() - 5) + "XXXXX";
                decryptWithAuth(tampered, key);
                System.out.println("⚠️ 篡改未被检测到!");
            } catch (Exception e) {
                System.out.println("✓ 篡改被检测到: " + e.getMessage());
            }
        } catch (Exception e) {
            System.out.println("需要Java 11+支持ChaCha20-Poly1305");
            System.out.println("错误: " + e.getMessage());
        }
    }
}

性能对比

MAC算法性能对比

算法计算速度输出长度密钥要求推荐度
HMAC-SHA256256位可变⭐⭐⭐⭐⭐
HMAC-SHA512512位可变⭐⭐⭐⭐
AES-CMAC中等128位128/192/256位⭐⭐⭐
GMAC128位128/192/256位⭐⭐⭐⭐
Poly1305很快128位256位⭐⭐⭐⭐

选择建议

通用场景:

  • 首选:HMAC-SHA256

与AES-GCM配合:

  • GMAC(自动包含)

与ChaCha20配合:

  • Poly1305(自动包含)

需要基于分组密码:

  • CMAC

HMAC vs 数字签名

对比表

特性HMAC数字签名
密钥类型对称密钥非对称密钥对
密钥分发需要安全通道公钥可公开
计算速度慢(签名)
验证速度快(验证)
不可否认性
适用场景API认证、消息完整性数字证书、代码签名

选择建议

使用HMAC:

  • API认证
  • 消息完整性验证
  • 双方共享密钥的场景

使用数字签名:

  • 需要不可否认性
  • 公钥可公开的场景
  • 数字证书、代码签名

安全注意事项

常见攻击

  1. 密钥泄露:密钥必须保密
  2. 重放攻击:使用时间戳或nonce
  3. 时序攻击:使用安全比较
  4. 密钥重用:不同场景使用不同密钥

最佳实践

/**
 * 安全比较MAC(防止时序攻击)
 */
public static boolean secureCompare(byte[] a, byte[] b) {
    if (a.length != b.length) {
        return false;
    }
    int result = 0;
    for (int i = 0; i < a.length; i++) {
        result |= a[i] ^ b[i];
    }
    return result == 0;
}

总结

推荐选择

通用消息认证:

  • 首选:HMAC-SHA256

认证加密:

  • AES-GCM(包含GMAC)
  • ChaCha20-Poly1305(包含Poly1305)

特殊需求:

  • CMAC:需要基于分组密码的MAC

选择决策树

需要消息认证?
├─ 与加密结合?→ GCM/GMAC 或 ChaCha20-Poly1305
├─ API认证?→ HMAC-SHA256
├─ 需要分组密码MAC?→ CMAC
└─ 通用场景?→ HMAC-SHA256