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