【问题标题】:AES256-CBC Ciphertext from BouncyCastle decrypts in BC, but PHP openssl_decrypt fails w/same inputs来自 BouncyCastle 的 AES256-CBC 密文在 BC 中解密,但 PHP openssl_decrypt 因输入相同而失败
【发布时间】:2017-08-22 11:36:19
【问题描述】:

我在 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

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

对于它的价值,这是我用来加密原始字符串的代码,以及我在 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);
        }
    }

【问题讨论】:

  • 阅读文档 php.net/manual/es/function.openssl-decrypt.php 中的 cmets,看起来 IV 和密码应该是二进制的(不是 base64)。我猜你首先需要base64_decode
  • 嗯...我从第一条评论中得到了相反的印象,它说“$data 可以是描述中所说的 raw 或 base64。如果没有设置 $option(这是,如果值为0 传入此参数),数据将被假定为 base64 编码。"
  • 是的,很混乱,但是在第二条评论中,作者明确表示参数字符串$password必须是二进制形式

标签: aes php-openssl


【解决方案1】:

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

php.net 上的 openssl_decrypt 官方文档很糟糕,前几个孤立的 cmets 介于​​“不完整”和“误导”之间。

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

$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;
}

但是,$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 找到这篇文章,同时尝试解决您在使用 openssl 和 PHP 时遇到的一些问题,请检查您的 Web 服务器是否有可用的 PHP 7.2。

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

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

【讨论】:

    猜你喜欢
    • 2017-09-28
    • 1970-01-01
    • 2017-02-24
    • 1970-01-01
    • 1970-01-01
    • 2021-10-23
    • 2015-08-22
    • 1970-01-01
    • 2020-05-21
    相关资源
    最近更新 更多