来自BouncyCastle的AES256-CBC密文在BC中解密,但PHP openssl_decrypt失败并且具有相同的输入

时间:2017-03-29 06:18:31

标签: aes php-openssl

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

然后我编写了一个测试程序(作为一个经过检测的Android单元测试),它将所有三个值硬编码为base64编码的字符串。当我在Android下运行以下代码时,它会成功解密。

完全相同的base64编码的密文,密钥和iv在PHP(5.6.30)下失败。我很难理解为什么。在这两种情况下,密文,密钥和iv都以硬编码的base64编码字符串开头,具有完全相同的值。

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

Android(SpongyCastle)代码:

@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);
        }
    }

1 个答案:

答案 0 :(得分:0)

好的,找到了明确的答案&amp;确认它有效。

php.net上openssl_decrypt的官方文档很糟糕,而且单独发表的前几条评论介于“不完整”和“误导”之间。

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

除非明确指定OPENSSL_RAW_DATA作为选项,否则

$ data被假定为base64编码。源代码本身清楚地说明了这一点:

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)最初加密的字符串的值 - “唉!无尽的悲伤!”

至于为什么官方文档如此糟糕,PHP的开发人员显然决定悄悄地弃用openssl函数,期望一旦PHP 7.2成为主流,每个人都将使用libsodium。将libsodium添加到PHP 7.2以后的默认安装是很棒的,但不幸的是,它不会对共享的Web托管帐户做任何人(谁缺少管理员,更不用说root,shell访问服务器而无法安装自己的延伸至少要到2018年初才能好转。

换句话说,如果您在2017年末之后的某个时间通过Google发现此帖,同时尝试解决您使用openssl和PHP时遇到的一些问题,请检查您的网络服务器是否具有PHP 7.2。

  • 如果没有,既没有升级到PHP 7.2+也没有在服务器上安装libsodium是可行的选择,请继续我的解决方案。

  • 如果确实如此,请忘记openssl_ *功能曾经存在,并直接进入libsodium(这设计使得意外搞砸更加困难)