杰瑞科技汇

PHP与Java的AES加密如何实现互通?

AES 加密有几个关键参数必须保持一致,否则加密和解密无法匹配:

PHP与Java的AES加密如何实现互通?-图1
(图片来源网络,侵删)
  1. 算法模式: 最常用的是 AES/CBC/PKCS5PaddingAES/GCM/PKCS5Padding,GCM 模式更现代,提供了认证加密功能,推荐使用。
  2. 密钥: 一个固定长度的字符串,AES 支持 128, 192, 256 位密钥,对应 16, 24, 32 字节的长度。
  3. 初始化向量: 一个随机的“盐”,长度通常是 16 字节。IV 必须是随机的,并且需要和密文一起传输,但它本身不是秘密。
  4. 填充方式: PKCS5Padding 或 PKCS7Padding(两者在大多数情况下效果相同)。

下面我们分别以 AES/CBC/PKCS5Padding更推荐的 AES/GCM/PKCS5Padding 为例,展示 PHP 和 Java 的实现。


使用 AES/CBC 模式 (需要手动处理 IV)

这是最经典的模式,实现简单,但需要注意 IV 的处理。

Java (AES/CBC) 实现

在 Java 中,我们使用 javax.crypto 包。

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AesCbcExample {
    // AES 密钥,必须是 16, 24, 或 32 字节长
    private static final String SECRET_KEY = "ThisIsASecretKey12345"; // 32 bytes for AES-256
    // IV 长度必须与块大小相同,对于 AES 是 16 字节
    private static final String INIT_VECTOR = "RandomInitVector"; // 16 bytes
    public static String encrypt(String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(INIT_VECTOR.getBytes(StandardCharsets.UTF_8));
            SecretKeySpec skeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(value.getBytes());
            // 将 IV 和密文拼接,然后进行 Base64 编码
            // 这样 Java 和 PHP 都能正确解析
            return Base64.getEncoder().encodeToString(iv.getIV()) + ":" + Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
    public static String decrypt(String encrypted) {
        try {
            // 分离 IV 和密文
            String[] parts = encrypted.split(":");
            if (parts.length != 2) {
                throw new IllegalArgumentException("Invalid encrypted string format");
            }
            byte[] iv = Base64.getDecoder().decode(parts[0]);
            byte[] cipherText = Base64.getDecoder().decode(parts[1]);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            SecretKeySpec skeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivParameterSpec);
            byte[] original = cipher.doFinal(cipherText);
            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
    public static void main(String[] args) {
        String originalString = "Hello, World! This is a secret message.";
        System.out.println("Original: " + originalString);
        String encryptedString = encrypt(originalString);
        System.out.println("Encrypted: " + encryptedString);
        String decryptedString = decrypt(encryptedString);
        System.out.println("Decrypted: " + decryptedString);
    }
}

关键点:

PHP与Java的AES加密如何实现互通?-图2
(图片来源网络,侵删)
  • Cipher.getInstance("AES/CBC/PKCS5Padding"): 指定了算法、模式和填充。
  • IV 处理: 我们将 IV 和密文用 分隔,然后都进行 Base64 编码,这是跨语言兼容的关键,因为 IV 不是秘密,需要传递给解密方。
  • Base64.getEncoder().encodeToString(): 用于将二进制数据转换为可传输的字符串。

PHP (AES/CBC) 实现

在 PHP 中,我们使用 openssl_encryptopenssl_decrypt 函数。

<?php
class AesCbcExample
{
    // 必须与 Java 中的密钥完全一致
    private const SECRET_KEY = 'ThisIsASecretKey12345'; // 32 bytes for AES-256
    // 必须与 Java 中的 IV 完全一致
    private const INIT_VECTOR = 'RandomInitVector'; // 16 bytes
    /**
     * 加密
     * @param string $data
     * @return string
     */
    public static function encrypt(string $data): string
    {
        $encrypted = openssl_encrypt(
            $data,
            'aes-256-cbc', // 算法: AES-256, 模式: CBC, 填充: PKCS7 (PHP默认)
            self::SECRET_KEY,
            0, // options
            self::INIT_VECTOR
        );
        if ($encrypted === false) {
            throw new \RuntimeException("Encryption failed.");
        }
        // 将 IV 和密文用 : 分隔,都进行 Base64 编码
        // 与 Java 的输出格式保持一致
        return base64_encode(self::INIT_VECTOR) . ':' . base64_encode($encrypted);
    }
    /**
     * 解密
     * @param string $encrypted
     * @return string
     */
    public static function decrypt(string $encrypted): string
    {
        // 分离 IV 和密文
        $parts = explode(':', $encrypted);
        if (count($parts) !== 2) {
            throw new \InvalidArgumentException("Invalid encrypted string format.");
        }
        $iv = base64_decode($parts[0]);
        $cipherText = base64_decode($parts[1]);
        $decrypted = openssl_decrypt(
            $cipherText,
            'aes-256-cbc',
            self::SECRET_KEY,
            0,
            $iv
        );
        if ($decrypted === false) {
            throw new \RuntimeException("Decryption failed.");
        }
        return $decrypted;
    }
}
// --- 测试 ---
$originalString = "Hello, World! This is a secret message.";
echo "Original: " . $originalString . PHP_EOL;
$encryptedString = AesCbcExample::encrypt($originalString);
echo "Encrypted: " . $encryptedString . PHP_EOL;
$decryptedString = AesCbcExample::decrypt($encryptedString);
echo "Decrypted: " . $decryptedString . PHP_EOL;
?>

关键点:

  • openssl_encrypt($data, 'aes-256-cbc', ...): 'aes-256-cbc' 指定了算法(AES-256)和模式(CBC),PHP 默认使用 PKCS7 填充,与 Java 的 PKCS5Padding 兼容。
  • IV 处理: 与 Java 一样,我们手动拼接 Base64 编码的 IV 和密文,并用 分隔,确保格式统一。

使用 AES/GCM 模式 (更推荐,自带认证)

GCM (Galois/Counter Mode) 是认证加密模式,它不仅能加密,还能验证数据在传输过程中是否被篡改,它将 IV 和认证标签打包在一起,比 CBC 更安全。

Java (AES/GCM) 实现

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AesGcmExample {
    // AES-256 密钥
    private static final String SECRET_KEY = "ThisIsASecretKey12345"; // 32 bytes
    // GCM 推荐的 IV 长度是 12 字节
    private static final int IV_LENGTH_BYTE = 12;
    // GCM 认证标签长度是 16 字节
    private static final int TAG_LENGTH_BIT = 128;
    public static String encrypt(String value) {
        try {
            // 生成随机 IV
            byte[] iv = new byte[IV_LENGTH_BYTE];
            SecureRandom random = new SecureRandom();
            random.nextBytes(iv);
            SecretKeySpec skeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "AES");
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // GCM 通常不使用填充
            GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, gcmParameterSpec);
            byte[] encrypted = cipher.doFinal(value.getBytes());
            // 将 IV, 密文和认证标签拼接 (cipherText 包含了密文和标签)
            // 然后进行 Base64 编码
            return Base64.getEncoder().encodeToString(iv) + ":" + Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
    public static String decrypt(String encrypted) {
        try {
            String[] parts = encrypted.split(":");
            if (parts.length != 2) {
                throw new IllegalArgumentException("Invalid encrypted string format");
            }
            byte[] iv = Base64.getDecoder().decode(parts[0]);
            byte[] cipherTextWithTag = Base64.getDecoder().decode(parts[1]);
            SecretKeySpec skeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "AES");
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, gcmParameterSpec);
            // 如果数据被篡改,cipher.doFinal 会抛出 AEADBadTagException
            byte[] original = cipher.doFinal(cipherTextWithTag);
            return new String(original);
        } catch (AEADBadTagException ex) {
            System.err.println("Decryption failed: Data is corrupted or tampered with.");
            return null; // 或者抛出异常
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
    public static void main(String[] args) {
        String originalString = "Hello, World! This is a secret GCM message.";
        System.out.println("Original: " + originalString);
        String encryptedString = encrypt(originalString);
        System.out.println("Encrypted: " + encryptedString);
        String decryptedString = decrypt(encryptedString);
        System.out.println("Decrypted: " + decryptedString);
        // 测试篡改数据
        String tamperedString = encryptedString.substring(0, 20) + "X" . encryptedString.substring(21);
        System.out.println("\nTampered String: " . tamperedString);
        String decryptedTampered = decrypt(tamperedString);
        System.out.println("Decrypted Tampered: " . (decryptedTampered != null ? decryptedTampered : "Decryption failed as expected."));
    }
}

关键点:

PHP与Java的AES加密如何实现互通?-图3
(图片来源网络,侵删)
  • 随机 IV: GCM 模式下,每次加密都必须使用一个全新的、随机的 IV,不能像 CBC 那样使用固定的 IV。
  • GCMParameterSpec: 用于指定 IV 和认证标签的长度。
  • 认证标签: cipher.doFinal() 的结果包含了密文和 16 字节的认证标签,Java 会自动处理它们。
  • 篡改检测: 如果解密时数据被篡改,会抛出 AEADBadTagException,这是 GCM 安全性的重要体现。

PHP (AES/GCM) 实现

PHP 7.1+ 开始支持 AES-GCM。

<?php
class AesGcmExample
{
    // 必须与 Java 中的密钥一致
    private const SECRET_KEY = 'ThisIsASecretKey12345'; // 32 bytes
    // GCM 推荐的 IV 长度是 12 字节
    private const IV_LENGTH = 12;
    /**
     * 加密
     * @param string $data
     * @return string
     * @throws Exception
     */
    public static function encrypt(string $data): string
    {
        // 生成随机 IV
        $iv = random_bytes(self::IV_LENGTH);
        $tag = ''; // 初始化认证变量
        // openssl_encrypt 会将认证标签填充到这个变量中
        $encrypted = openssl_encrypt(
            $data,
            'aes-256-gcm', // 算法: AES-256, 模式: GCM
            self::SECRET_KEY,
            0, // options
            $iv,
            $tag // 用于接收认证标签
        );
        if ($encrypted === false) {
            throw new \RuntimeException("Encryption failed.");
        }
        // 将 IV, 密文和认证标签拼接
        // PHP 的 openssl_encrypt 返回的密文不包含标签,需要手动拼接
        return base64_encode($iv) . ':' . base64_encode($encrypted) . ':' . base64_encode($tag);
    }
    /**
     * 解密
     * @param string $encrypted
     * @return string
     * @throws Exception
     */
    public static function decrypt(string $encrypted): string
    {
        $parts = explode(':', $encrypted);
        if (count($parts) !== 3) {
            throw new \InvalidArgumentException("Invalid encrypted string format.");
        }
        $iv = base64_decode($parts[0]);
        $cipherText = base64_decode($parts[1]);
        $tag = base64_decode($parts[2]);
        $decrypted = openssl_decrypt(
            $cipherText,
            'aes-256-gcm',
            self::SECRET_KEY,
            0,
            $iv,
            $tag // 将认证标签传递给解密函数
        );
        if ($decrypted === false) {
            // 如果认证失败,openssl_decrypt 会返回 false
            throw new \RuntimeException("Decryption failed. Data may be corrupted or tampered with.");
        }
        return $decrypted;
    }
}
// --- 测试 ---
$originalString = "Hello, World! This is a secret GCM message.";
echo "Original: " . $originalString . PHP_EOL;
$encryptedString = AesGcmExample::encrypt($originalString);
echo "Encrypted: " . $encryptedString . PHP_EOL;
$decryptedString = AesGcmExample::decrypt($encryptedString);
echo "Decrypted: " . $decryptedString . PHP_EOL;
// 测试篡改数据
$tamperedString = substr($encryptedString, 0, 30) . "X" . substr($encryptedString, 31);
echo "\nTampered String: " . $tamperedString . PHP_EOL;
try {
    $decryptedTampered = AesGcmExample::decrypt($tamperedString);
    echo "Decrypted Tampered: " . $decryptedTampered . PHP_EOL;
} catch (\RuntimeException $e) {
    echo "Decryption Tampered: " . $e->getMessage() . PHP_EOL;
}
?>

关键点:

  • random_bytes(self::IV_LENGTH): 生成随机的 IV。
  • $tag 参数: openssl_encrypt 的第六个参数是一个引用变量,用于接收生成的认证标签。
  • 输出格式: 我们将 IV、密文和认证标签三部分用 分隔并 Base64 编码,这与 Java 的输出格式略有不同(Java 的密文+标签是连在一起的),但逻辑一致。
  • 解密: 将接收到的三部分数据分离,并把认证标签 $tag 传递给 openssl_decrypt,如果标签不匹配,解密会失败并返回 false

总结与最佳实践

特性 AES/CBC AES/GCM (推荐)
安全性 良好,但易受填充预言攻击和 IV 重放攻击影响。 非常高,提供认证加密,防止篡改。
IV 必须固定,且需要保密(虽然通常认为不保密也可,但最佳实践是保密)。 必须随机,每次加密都不同,无需保密。
密文结构 Base64(IV) : Base64(密文) Base64(IV) : Base64(密文) : Base64(认证标签) (PHP) 或 Base64(IV) : Base64(密文+标签) (Java)
跨语言 实现简单,但需手动处理 IV。 实现稍复杂,需正确处理随机 IV 和认证标签。
错误处理 解密失败通常返回乱码或抛出异常。 解密失败会明确抛出异常(Java: AEADBadTagException, PHP: openssl_decrypt 返回 false),能检测到篡改。

最终建议:

  1. 优先选择 AES/GCM: 它更安全,功能更强大(自带认证),是现代加密的首选。
  2. 保持参数一致: 确保 Java 和 PHP 使用的密钥、IV(如果是 CBC)、算法模式完全相同。
  3. 正确处理 IV:
    • CBC: 使用一个预定义的、安全的 IV。
    • GCM: 每次加密都生成一个新的随机 IV,并将其与密文一起传给解密方。
  4. 使用 Base64 编码: 因为 IV、密文和标签都是二进制数据,为了在网络或文本环境中传输,必须进行 Base64 编码。
  5. 妥善保管密钥: 密钥是安全的核心,切勿硬编码在代码中或提交到版本控制系统,应使用环境变量、密钥管理服务等方式安全地存储和获取密钥。
分享:
扫描分享到社交APP
上一篇
下一篇