来自 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(它的设计使得意外搞砸变得更难)
我在 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(它的设计使得意外搞砸变得更难)