来自 BouncyCastle 的 AES256-CBC 密文在 BC 中解密,但 PHP openssl_decrypt 失败 w/same 输入

AES256-CBC Ciphertext from BouncyCastle decrypts in BC, but PHP openssl_decrypt fails w/same inputs

我在 Android 下使用 BouncyCastle(技术上,SpongyCastle)使用 256 位 AES(CBC 模式)加密了一个字符串,并记录了密文、密钥和 iv 的 base64 编码值。

然后我编写了一个测试程序(作为检测的 Android 单元测试),将所有三个值硬编码为 base64 编码的字符串。当我 运行 下面的代码在 Android.

下它解密成功

完全相同的 base64 编码密文、密钥和 iv 在 PHP (5.6.30) 下失败。 我正在努力理解原因。在这两种情况下,密文、密钥和 iv 都以具有完全相同值的硬编码 base64 编码字符串开始。

除其他外,它抱怨密钥长度(32 字节)无效(我上次检查时,32 * 8 == 256)。

Android(海绵城堡)代码:

@Test
    public void crossplatformTest() {
        String ciphertext64 = "gfcC6t1BarndpzMuvYj2JFpWHqlWSJMhTtxPN7QjyEg=";
        String key64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
        String iv64="AAECAwQFBgcICQoLDA0ODw==";

        byte[] ciphertext = Base64.decode(ciphertext64, Base64.DEFAULT);
        byte[] key = Base64.decode(key64, Base64.DEFAULT);
        byte[] iv = Base64.decode(iv64, Base64.DEFAULT);

        byte[] decrypted = Crypto.decryptWithAesCBC(ciphertext, key, iv);
        // the following assertion succeeds.
        assertEquals("Ugh! Endless grief!", new String(decrypted, StandardCharsets.UTF_8));
    }

PHP (OpenSSL) 代码:

<?php
    $ciphertext64 = "gfcC6t1BarndpzMuvYj2JFpWHqlWSJMhTtxPN7QjyEg=";
    $key64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
    $iv64="AAECAwQFBgcICQoLDA0ODw=="; 

    // UPDATE: THE LINE BELOW IS THE PROBLEM. SEE ANSWER FOR SOLUTION.
    $decrypted = openssl_decrypt($ciphertext64, 'aes-256-cbc', $key64, 0, $iv64);
    if ($decrypted == false) {
        while ($msg = openssl_error_string())
            echo "<p>$msg</p>\n";
        echo("key length=" . strlen(base64_decode($key64, true))."<BR>\n");
        echo("iv length=" . strlen(base64_decode($iv64, true))."<BR>\n");
    }

PHP的输出:

error:0607A082:digital envelope routines:EVP_CIPHER_CTX_set_key_length:invalid key length
error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt

key length=32
iv length=16

根据 openssl_decrypt 似乎存在于 php.net (http://php.net/manual/en/function.openssl-decrypt.php) 的少量文档,openssl_decrypt 需要 base64 编码的密文、密钥和 iv 字符串.

对于它的价值,这是我用来加密原始字符串的代码,以及我在 Android 上用来解密它的代码:

public static byte[] decryptWithAesCBC(byte[] ciphertext, byte[] key, byte[] iv) {
        try {
            PaddedBufferedBlockCipher aes = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
            CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(key), iv);
            aes.init(false, ivAndKey);
            return cipherData(aes, ciphertext);
        }
        catch (InvalidCipherTextException e) {
            throw new RuntimeException(e);
        }
    }

private static byte[] cipherData(PaddedBufferedBlockCipher cipher, byte[] data) throws InvalidCipherTextException {
        int minSize = cipher.getOutputSize(data.length);
        byte[] outBuf = new byte[minSize];
        int length1 = cipher.processBytes(data, 0, data.length, outBuf, 0);
        int length2 = cipher.doFinal(outBuf, length1);
        int actualLength = length1 + length2;
        byte[] ciphertext = new byte[actualLength];
        for (int x=0; x < actualLength; x++) {
            ciphertext[x] = outBuf[x];
        }
        return ciphertext;
    }

public static byte[] encryptWithAesCBC(byte[] plaintext, byte[] key, byte[] iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "SC");

            PaddedBufferedBlockCipher aes = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
            CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(key), iv);
            aes.init(true, ivAndKey);
            return cipherData(aes, plaintext);
        }
        catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException | InvalidCipherTextException e) {
            throw new RuntimeException(e);
        }
    }

好的,找到明确的答案并确认它有效。

openssl_decrypt 在 php.net 的官方文档很糟糕,前几条孤立的评论介于 "incomplete" 和 "misleading" 之间。

根据openssl_decrypt最权威的文档(ext/openssl/openssl.c在PHP 5.6.30本身的源代码中):

$data 被假定为 base64 编码,除非 OPENSSL_RAW_DATA 被明确指定为一个选项。源代码本身说明了这一点:

if (!(options & OPENSSL_RAW_DATA)) {
    base64_str = (char*)php_base64_decode((unsigned char*)data, data_len, &base64_str_len);
    if (!base64_str) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to base64 decode the input");
        RETURN_FALSE;
    }
    data_len = base64_str_len;
    data = base64_str;
}

HOWEVER,$password 和 $iv 的值直接转换为 (unsigned char *).

因此,正确的用法是:

$ciphertext64 = "gfcC6t1BarndpzMuvYj2JFpWHqlWSJMhTtxPN7QjyEg=";
$key64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
$iv64="AAECAwQFBgcICQoLDA0ODw==";

$key = base64_decode($key64, true);
$iv = base64_decode($iv64, true);

$decrypted = openssl_decrypt($ciphertext64, 'aes-256-cbc', $key, 0, $iv);

... 输出我最初在 Android 上使用 BouncyCastle (SpongyCastle) 加密的字符串的值 -- "Ugh! Endless grief!"

至于为什么官方文档如此糟糕,PHP的开发人员显然已经决定悄悄弃用openssl函数,期望一旦PHP 7.2 成为主流,以后每个人都会使用 libsodium。在 PHP 7.2 以后的默认安装中添加 libsodium 很棒,但不幸的是,它不会对任何拥有共享网络托管帐户的人(缺少管理员,更不用说 root,shell 访问服务器并且无法安装自己的扩展)至少在 2018 年初之前会很好。

换句话说,如果您在 2017 年底之后的某个时候通过 Google 发现了这个 post,同时试图解决您在使用 openssl 和 PHP 时遇到的问题,请查看您的网络服务器是否有 PHP 7.2 可用。

  • 如果没有,并且升级到 PHP 7.2+ 和在服务器上安装 libsodium 都不可行,请继续我的解决方案。

  • 如果是这样,忘记 openssl_* 函数曾经存在,直接进入 libsodium(它的设计使得意外搞砸变得更难)