【问题标题】:PHP7.4: OpenSSL AES-CFB encryption different to PythonPHP7.4:OpenSSL AES-CFB 加密不同于 Python
【发布时间】:2020-11-09 09:30:14
【问题描述】:

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

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

这是我要复制的代码,然后是 PHP 代码尝试用不同的方法复制结果。 有些东西告诉我它与 PHP 的“字节处理”有关,因为 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

【问题讨论】:

    标签: python php encryption aes


    【解决方案1】:

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

    Python 代码中的hexdigest 方法也将结果作为十六进制字符串返回。但是,在 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"))
      

      那么输出b0016a55aes-128-cfb8的PHP代码结果一致。

    • 选项 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位(=一个字节)和CFB表示整个块( 16 个字节)。在 Python 中,每步的位数用segment_size 指定。

    ...-cfb8 在 PHP 中的对应物是 Python 中的 segment_size = 8...-cfb 在 PHP 中的对应物是 segment_size = 128 在 Python 中。

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

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


    另请注意:

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

    【讨论】:

    • 这就像一个魅力!我将 PHP 中的密码更改为 AES-256-CFB,现在它给出了相同的结果。
    猜你喜欢
    • 2021-12-20
    • 2017-04-12
    • 2021-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-02
    • 2019-02-15
    • 1970-01-01
    相关资源
    最近更新 更多