PHP7.4:OpenSSL AES-CFB 加密不同于 Python

PHP7.4: OpenSSL AES-CFB encryption different to Python

我正在尝试使用 PHP7.4 复制一段 python 代码,该代码使用 Pycryptodome 进行 AES-128-CFB 加密。 为此,我使用了 PHP 的 openssl_encrypt 内置函数。 我尝试了几个配置参数和 CFB 模式,但我一直得到不同的结果。 我发现 pycryptodomes CFB 实现似乎使用了 8 位段大小,这应该是 PHP 的 openssl 实现中的 aes-128-cfb8 模式。

IV 被故意固定为 0,所以请忽略它不安全的事实。

这是我要复制的代码,后面是 PHP 代码,试图用不同的方法复制结果。 有些东西告诉我它与 PHP 的 'byte handling' 有关,因为 python 区分字节字符串(由 .encode('utf-8') 返回)和字符串。 最后你可以看到两个代码的输出:

Python代码:

import hashlib
from Crypto.Cipher import AES

key = 'testKey'
IV = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
ENC_KEY = hashlib.md5(key.encode('utf-8')).hexdigest()

print('key: "' + key + '"')
print('hashedKey: ' + ENC_KEY)
obj = AES.new(ENC_KEY.encode("utf8"), AES.MODE_CFB, IV.encode("utf8"))
test_data = 'test'
print('encrypting "' + test_data + '"')
encData = obj.encrypt(test_data.encode("utf8"))
print('encData: ' + encData.hex())

PHP代码:

function encTest($testStr, $ENC_KEY)
{
    $iv = hex2bin('00000000000000000000000000000000');

    echo "aes-128-cfb8-1: ".bin2hex(openssl_encrypt($testStr, 'aes-128-cfb8', $ENC_KEY, OPENSSL_RAW_DATA, $iv))."\n";
    echo "aes-128-cfb1-1: ".bin2hex(openssl_encrypt($testStr, 'aes-128-cfb1', $ENC_KEY, OPENSSL_RAW_DATA, $iv))."\n";
    echo "aes-128-cfb-1: ".bin2hex(openssl_encrypt($testStr, 'aes-128-cfb', $ENC_KEY, OPENSSL_RAW_DATA, $iv))."\n";
    echo "\n";

    echo "aes-128-cfb8-2: ".bin2hex(openssl_encrypt($testStr, 'aes-128-cfb8', $ENC_KEY, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv))."\n";
    echo "aes-128-cfb1-2: ".bin2hex(openssl_encrypt($testStr, 'aes-128-cfb1', $ENC_KEY, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv))."\n";
    echo "aes-128-cfb-2: ".bin2hex(openssl_encrypt($testStr, 'aes-128-cfb', $ENC_KEY, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv))."\n";
    echo "\n";

    echo "aes-128-cfb8-3: ".bin2hex(openssl_encrypt(utf8_encode($testStr), 'aes-128-cfb8', utf8_encode($ENC_KEY), OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv))."\n";
    echo "aes-128-cfb1-3: ".bin2hex(openssl_encrypt(utf8_encode($testStr), 'aes-128-cfb1', utf8_encode($ENC_KEY), OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv))."\n";
    echo "aes-128-cfb-3: ".bin2hex(openssl_encrypt(utf8_encode($testStr), 'aes-128-cfb', utf8_encode($ENC_KEY), OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv))."\n";
    echo "\n";

    echo "aes-128-cfb8-4: ".bin2hex(openssl_encrypt(utf8_encode($testStr), 'aes-128-cfb8', utf8_encode($ENC_KEY), OPENSSL_RAW_DATA, $iv))."\n";
    echo "aes-128-cfb1-4: ".bin2hex(openssl_encrypt(utf8_encode($testStr), 'aes-128-cfb1', utf8_encode($ENC_KEY), OPENSSL_RAW_DATA, $iv))."\n";
    echo "aes-128-cfb-4: ".bin2hex(openssl_encrypt(utf8_encode($testStr), 'aes-128-cfb', utf8_encode($ENC_KEY), OPENSSL_RAW_DATA, $iv))."\n";
    echo "\n";
}

$key = "testKey";
$ENC_KEY = hash('md5', utf8_encode($key));
echo "ENC_KEY: ".$ENC_KEY."\n";

$test = "test";
echo "encrypting \"".$test."\"\n";
encTest($test, $ENC_KEY);

Python 输出(encData 应该被复制):

key: "testKey"
hashedKey: 24afda34e3f74e54b61a8e4cbe921650
encrypting "test"

encData: 117c1974

PHP 输出:

key: "testKey"
hashedKey: 24afda34e3f74e54b61a8e4cbe921650
encrypting "test"

aes-128-cfb8-1: b0016a55
aes-128-cfb1-1: bac44c56
aes-128-cfb-1: b0f1c27a

aes-128-cfb8-2: b0016a55
aes-128-cfb1-2: bac44c56
aes-128-cfb-2: b0f1c27a

aes-128-cfb8-3: b0016a55
aes-128-cfb1-3: bac44c56
aes-128-cfb-3: b0f1c27a

aes-128-cfb8-4: b0016a55
aes-128-cfb1-4: bac44c56
aes-128-cfb-4: b0f1c27a

在 PHP 代码中(更准确地说是 openssl_encrypt),明确指定了 AES 变体,例如与当前 aes-128-... 的情况一样,即 PHP 使用 AES-128。太长的键被截断,太短的键用 0 值填充。由于 PHP 代码中的 hash 方法 returns 其结果为十六进制字符串,因此 16 字节的 MD5 哈希由 32 个字符(32 字节)表示,即在当前情况下 PHP 使用密钥的前 16 个字节 (AES-128)。

Python 代码中的 hexdigest 方法也 returns 结果为十六进制字符串。但是,在 Python 代码中(更准确地说是 PyCryptodome),AES 变体由密钥大小指定,即 Python 代码使用完整的 32 字节密钥,因此使用 AES-256。

不同的密钥和 AES 变体是导致不同结果的主要原因。要解决此问题,必须在两个代码中使用相同的密钥和 AES 变体:

  • 选项 1 也是在 Python 代码中使用 AES-128。这可以通过以下更改来实现:

    obj = AES.new(ENC_KEY[:16].encode("utf8"), AES.MODE_CFB, IV.encode("utf8"))
    

    则输出b0016a55与PHP代码的结果一致aes-128-cfb8.

  • 选项 2 也是在 PHP 代码中使用 AES-256。这可以通过将 aes-128... 替换为 aes-256... 来完成 然后输出是

    aes-256-cfb8-1: 117c1974
    aes-256-cfb1-1: 54096db1
    aes-256-cfb-1 : 11bfdaa9
    

并且,正如预期的那样,aes-128-cfb8 的输出 117c1974 与 Python 代码的原始值匹配。


CFB 模式将块密码更改为流密码。从而在每个加密步骤中加密n位,称为CFBn。有关确切的详细信息。 here.

术语CFBn(或cfbn)也用于PHP,即CFB1表示一位加密,CFB8表示8位加密(= 一个字节)和整个块(16 字节)的 CFB。在Python中,每步的位数用segment_size指定。

即PHP中...-cfb8的对应项是Python中的segment_size = 8,PHP中...-cfb的对应项是[=]中的segment_size = 128 115=].

以下假设两个代码中使用相同的密钥和相同的 AES 变体。

由于 segment_size = 8 是默认值,因此 Python 代码的结果与 PHP 代码的 ...-cfb8 相同。如果选择Python代码中的segement_size = 128,结果与PHP代码中的...-cfb相同。但是,在 PyCryptodome 中 segment_size 必须是 8 的整数倍,否则错误消息 'segment_size' 必须是正数并显示 8 位的倍数 。因此,PyCryptodome 不支持 CFB1 模式。


另请注意:

  • 摘要的结果也可以在两种代码中返回二进制而不是十六进制字符串。为此,PHP方法的第三个参数hash must be set to TRUE (default: FALSE). In Python, simply use the digest方法代替了hexdigest.
  • 在 PHP 代码中,对于像 CFB 这样的流密码模式,填充被自动禁用,因此 OPENSSL_ZERO_PADDING 标志(可用于显式禁用填充)没有区别。
  • utf8_encode 允许您将 ISO-8859-1 编码转换为 UTF-8,但由于 $ENC_KEY 由字母数字字符(十六进制编码)组成,因此这没有任何效果。但是,一般来说,任意二进制数据(例如摘要的结果)不得采用 UTF8 编码,因为这会损坏数据。还有其他编码用于此目的,例如 Base64。如果摘要的结果以二进制形式返回(见第1点),则可能没有进行UTF8编码。
  • 在 CFB 模式的上下文中,旧版 PyCrypto 库中存在一个错误,该错误要求明文的长度是段大小的整数倍。否则会出现以下错误:输入字符串的长度必须是段大小 16 的倍数。