使用 openssl_decrypt() 和密码(注意 key/iv)

Use openssl_decrypt() with passphase (not key/iv)

我正在尝试解密一些已在 PHP 脚本中使用密码和 aes-256-cbc 方法加密的数据。

这是我加密原始数据的方法

printf "Hello" |   openssl enc -e -base64 -A -aes-256-cbc -k "MYPASSWORD"

// output
U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=

当我尝试在命令行中解密时它工作正常

printf "U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=" |   openssl enc -d -base64 -A -aes-256-cbc -k "MYPASSWORD"

// output
Hello

但是当我在我的 PHP 脚本中使用 openssl_decrypt() 时,它 不起作用!!

$result = openssl_decrypt("U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=", 'AES-256-CBC', "MYPASSWORD");
var_dump($result);

//output
bool(false)

我附加以下几行以获取错误

while ($msg = openssl_error_string())
    echo $msg . "<br />\n";

它returns:

error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt

我知道我应该使用 key/iv 对,但我无法使用任何 salt 从我的密码短语中提取它。 我怎样才能使下面的命令起作用?

printf "U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=" | openssl enc -d -base64 -A -aes-256-cbc -K ??????????????? -iv ????????????????

// expected output !!!
Hello

编辑:

我尝试使用 -p 参数获取 key/iv 但它不起作用

printf "U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=" | openssl enc -d -base64 -A -aes-256-cbc -k "MYPASSWORD" -p
salt=9D5AE06E8A2B627C
key=8ACC4E30E9128FBB0763DDDA8998A7141DFDC77B9DADF0A5FC65E67E2A8313FA
iv =4150125DCCD36F73A9F08F3020151A04
Hello

printf "U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=" | openssl enc -d -base64 -A -aes-256-cbc -K 8ACC4E30E9128FBB0763DDDA8998A7141DFDC77B9DADF05E67E2A8313FA -iv 4150125DCCD36F73A9F08F3020151A04
    bad decrypt
140735954895816:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:evp_enc.c:529:
??G?"r!C???&C&??

这里的问题是您没有使用 EVP_BytesToKey。这是用于从您的密码派生密钥和 IV 的 OpenSSL KDF。

请注意,它是不安全的。您应该更喜欢将十六进制密钥和 IV 直接传递给 openssl enc.

通过 -k 选项(在您的情况下 "MYPASSWORD")用作 openssl enc 参数的密码(或密码短语)与 key PHP 函数 openssl_decrypt() 期望的参数。 openssl enc-k 选项是一个任意长度的密码短语,实际的 256 位加密密钥将从中导出。这也是 PHP openssl_decrypt() 函数需要的关键。此加密密钥为 256 位,因为您选择了 aes-256.

您可以通过在调用 openssl enc 时添加 -p 选项来了解导出的加密密钥是什么。这还会打印 iv,这是您需要与 PHP openssl_decrypt() 函数一起使用的另一个参数。例如:

printf "Hello" |  openssl enc -e -base64 -A -aes-256-cbc -k "MYPASSWORD" -nosalt -p
key=E0FAC2DD2C00FFE30F27A6D14568CB4F12EB84676A3A2BFB172A444C3BBB831F
iv =5A79774BB4B326EED949E6871FC27697
sp0z18QezUO8tSy7tgjOEw==

这些打印的 keyiv 值是您需要输入到 PHP openssl_decrypt() 函数调用中的值,如下所示:

$ciphertext = 'sp0z18QezUO8tSy7tgjOEw==';
$key = hex2bin('E0FAC2DD2C00FFE30F27A6D14568CB4F12EB84676A3A2BFB172A444C3BBB831F');
$iv = hex2bin('5A79774BB4B326EED949E6871FC27697');
$result = openssl_decrypt($ciphertext, 'AES-256-CBC', $key, 0, $iv);
var_dump($result);

运行 PHP 脚本现在成功:

$ php decrypt.php 
string(5) "Hello"

您可能已经注意到 运行 宁 openssl enc 时额外的 -nosalt 选项。 Salt用于在密钥推导过程中添加一些randomness/uniqueness,而-nosalt省略了该步骤。因此,keyivciphertext 在每个 运行 中都是相同的(如果使用相同的密码和明文),您应该能够准确地重现输出。如果您不使用 -nosalt,您的实验仍然有效,但每个 运行 的 keyivciphertext 值将不同,您也会必须摆脱 openssl 添加为 header 的盐——有关详细信息,请参阅此答案的下方。

另一种选择是让 PHP 代码在调用 openssl_decrypt() 之前从密码短语中导出 keyiv。为此,您必须检查 code of the enc tool 以查找您正在使用的 openssl 版本。在那里您可以看到使用了哪个密钥派生函数——这取决于您使用的 openssl 的版本以及您提供的选项——以及它是否在 PHP 绑定中可用openssl.


更新,回复您的评论,您在其中添加了您只有密文和密码可用的信息,并且该密文是使用 crypto-js 创建的。

查看crypto-js的源代码,其中提到in a comment in the source file evpkdf.js即"the key derivation function is meant to conform with EVP_BytesToKey",这与大多数openssl版本使用的功能相同。因此,您应该能够使用 openssl enc 工具通过使用 -p 选项来提取 keyiv,如下所示:

$printf "U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=" | openssl enc -d -base64 -A -aes-256-cbc -k "MYPASSWORD" -p
salt=9D5AE06E8A2B627C
key=8ACC4E30E9128FBB0763DDDA8998A7141DFDC77B9DADF0A5FC65E67E2A8313FA
iv =4150125DCCD36F73A9F08F3020151A04

(您现在也在另一条评论中确认了这一点),然后在调用 PHP 函数时使用它们,如上所述。请注意,您必须分别对每个密文执行此操作,因为 salt(以及 keyiv)的选择不同,每次加密由 crypto-js 随机选择行动。要直接在 PHP 中执行此操作,请参阅我之前的评论:所需的功能似乎在其 decrypt 模块中不可用。

您可以通过在解密时将 keyiv 填入 openssl enc 来验证它是否有效。但是,有一个障碍。使用盐时,openssl 方法是在输出中包含该盐,如您在此处所见:

$ printf "U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=" |  openssl base64 -d -A | hexdump -C
00000000  53 61 6c 74 65 64 5f 5f  9d 5a e0 6e 8a 2b 62 7c  |Salted__.Z.n.+b||
00000010  7e 33 bb 56 2f fe 5e fe  1d c7 c8 a9 1f f0 c5 27  |~3.V/.^........'|
00000020

输出的前 16 个字节是 "magic" 个字节 Salted__,然后是盐。如果您使用密码短语,该盐通常会被该工具读取,但如果您直接使用 keyiv 解密,它就会妨碍您。因此,在解密时将字节作为密文输入 openssl enc 之前,您必须删除 header ,例如使用 tail 像这样:

printf "U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=" |  openssl base64 -d -A | tail -c +17 | openssl enc -d -aes-256-cbc -K 8ACC4E30E9128FBB0763DDDA8998A7141DFDC77B9DADF0A5FC65E67E2A8313FA -iv 4150125DCCD36F73A9F08F3020151A04
Hello

这个one-liner先做base64解码,然后去掉前16个字节,然后将结果送入openssl enc,不再需要-base64选项,因为已经处理好了。

在PHP中:

$ciphertext = 'U2FsdGVkX1+dWuBuiitifH4zu1Yv/l7+HcfIqR/wxSc=';
$ciphertext_decoded = base64_decode($ciphertext);
$ciphertext_nosalt = base64_encode(substr($ciphertext_decoded, 16));

$key = hex2bin('8ACC4E30E9128FBB0763DDDA8998A7141DFDC77B9DADF0A5FC65E67E2A8313FA');
$iv = hex2bin('4150125DCCD36F73A9F08F3020151A04');

$result = openssl_decrypt($ciphertext_nosalt, 'AES-256-CBC', $key, 0, $iv);
var_dump($result);

综上所述,您可能最好放弃 openssl enccrypto-js 使用的密钥派生,它依赖于 OpenSSL EVP_ByesToKey 实现的专有机制功能。甚至 openssl enc now warns about this being deprecated .

相反,开始使用像 PBKDF2 这样的标准算法。这是由 openssl enc 的更新版本支持的,我在 crypto-js 和 PHP crypto 模块的源代码中也发现了它(但我自己从未使用过它们).如果您有一个需要保留的加密数据数据库,您可以 re-encrypt 它的内容一次,使用旧方法解密并使用 PKDBF2 方法加密。确保将盐分开存储,而不是与密文一起存储为一个 blob。