【问题标题】:Translate Windows RC4 CryptDeriveKey to PHP for openssl将 Windows RC4 CryptDeriveKey 转换为 PHP for openssl
【发布时间】:2019-05-25 23:45:26
【问题描述】:

这是我们一直在尝试做的遗留系统翻译的第二个组成部分。我们已成功匹配 Windows ::CryptHashData 生成的初始二进制密码/密钥。

该密码/密钥被传递给 ::CryptDeriveKey,在那里它执行多个步骤来创建 ::CryptEncrypt 使用的最终密钥。我的研究使我找到了 CryptDeriveKey 文档,其中清楚地描述了为 ::CryptEncrypt 派生密钥所需的步骤,但到目前为止,我还无法在 PHP 端解密文件。 https://docs.microsoft.com/en-us/windows/desktop/api/wincrypt/nf-wincrypt-cryptderivekey

根据 ::CryptDeriveKey 文档,对于我们特定的旧密钥大小,可能存在一些额外的未记录步骤,这些步骤可能无法很好地理解。当前的 Windows ::CryptDeriveKey 默认设置为 ZERO SALT,这显然与 NO_SALT 有所不同。在此处查看盐值功能: https://docs.microsoft.com/en-us/windows/desktop/SecCrypto/salt-value-functionality

我们遗留系统的 CryptAPI 参数如下:

提供者类型:PROV_RSA_FULL

提供者名称:MS_DEF_PROV

算法 ID CALG_RC4

说明RC4流加密算法

密钥长度:40 位。

盐长度:88 位。零盐

特别注意:但是,带零值盐的 40 位对称密钥不等同于不带盐的 40 位对称密钥。为了互操作性,必须在没有盐的情况下创建密钥。这个问题是由一个默认情况导致的,这种情况只发生在恰好 40 位的密钥上。

我不打算导出密钥,而是重现创建最终加密密钥的过程,该密钥传递给 RC4 加密算法的 ::CryptEncrypt 并让它与 openssl_decrypt 一起使用。

这是当前适用于加密的 Windows 代码。

try {
    BOOL bSuccess;
    bSuccess = ::CryptAcquireContextA(&hCryptProv, 
                                      CE_CRYPTCONTEXT, 
                                      MS_DEF_PROV_A, 
                                      PROV_RSA_FULL, 
                                      CRYPT_MACHINE_KEYSET);

    ::CryptCreateHash(hCryptProv, 
                      CALG_MD5, 
                      0, 
                      0, 
                      &hSaveHash);

    ::CryptHashData(hSaveHash, 
                    baKeyRandom, 
                    (DWORD)sizeof(baKeyRandom), 
                    0);

    ::CryptHashData(hSaveHash, 
                    (LPBYTE)T2CW(pszSecret), 
                    (DWORD)_tcslen(pszSecret) * sizeof(WCHAR), 
                     0);

    ::CryptDeriveKey(hCryptProv, 
                     CALG_RC4, 
                     hSaveHash, 
                     0, 
                     &hCryptKey);

    // Now Encrypt the value
    BYTE * pData = NULL;
    DWORD dwSize = (DWORD)_tcslen(pszToEncrypt) * sizeof(WCHAR); 
    // will be a wide str
    DWORD dwReqdSize = dwSize;

    ::CryptEncrypt(hCryptKey, 
                   NULL, 
                   TRUE, 
                   0, 
                   (LPBYTE)NULL, 
                   &dwReqdSize, 0);

    dwReqdSize = max(dwReqdSize, dwSize);

    pData = new BYTE[dwReqdSize];

    memcpy(pData, T2CW(pszToEncrypt), dwSize);

    if (!::CryptEncrypt(hCryptKey, 
                        NULL, 
                        TRUE, 
                        0, 
                        pData, 
                        &dwSize, 
                        dwReqdSize)) {

            printf("%l\n", hCryptKey);
            printf("error during CryptEncrypt\n");
            }

    if (*pbstrEncrypted)
    ::SysFreeString(*pbstrEncrypted);
    *pbstrEncrypted = ::SysAllocStringByteLen((LPCSTR)pData, dwSize);
    delete[] pData;
    hr = S_OK;
}

这是尝试复制文档中描述的 ::CryptDeriveKey 函数的 PHP 代码。

设 n 为所需的派生密钥长度,以字节为单位。派生密钥是 CryptDeriveKey 完成哈希计算后哈希值的前 n 个字节。如果哈希不是 SHA-2 系列的成员,并且所需的密钥用于 3DES 或 AES,则密钥派生如下:

  1. 通过重复常量 0x36 64 次形成一个 64 字节的缓冲区。设 k 为由输入参数 hBaseData 表示的哈希值的长度。将缓冲区的前 k 字节设置为缓冲区前 k 字节与输入参数 hBaseData 表示的哈希值的 XOR 运算的结果。

  2. 通过重复常量 0x5C 64 次形成一个 64 字节的缓冲区。将缓冲区的前 k 字节设置为缓冲区前 k 字节与输入参数 hBaseData 表示的哈希值的异或运算的结果。

  3. 使用与用于计算由 hBaseData 参数表示的哈希值相同的哈希算法对步骤 1 的结果进行哈希处理。

  4. 使用与用于计算由 hBaseData 参数表示的哈希值相同的哈希算法对步骤 2 的结果进行哈希处理。

  5. 将步骤 3 的结果与步骤 4 的结果连接起来。

  6. 使用第 5 步结果的前 n 个字节作为派生密钥。

PHP 版本的 ::CryptDeriveKey。

function cryptoDeriveKey($key){

    //Put the hash key into an array
    $hashKey1 = str_split($key,2);
    $count = count($hashKey1);
    $hashKeyInt = array();

    for ($i=0; $i<$count; $i++){
        $hashKeyInt[$i] = hexdec($hashKey1[$i]);
    }
    $hashKey = $hashKeyInt;

    //Let n be the required derived key length, in bytes.  CALG_RC4 = 40 bits key or 88 salt bytes
    $n = 40/8;

    //Let k be the length of the hash value that is represented by the input parameter hBaseData
    $k = 16;

    //Step 1 Form a 64-byte buffer by repeating the constant 0x36 64 times   
    $arraya = array_fill(0, 64, 0x36);

    //Set the first k bytes of the buffer to the result of an XOR operation of the first k bytes of the buffer with the hash value 
    for ($i=0; $i<$k; $i++){
        $arraya[$i] = $arraya[$i] ^ $hashKey[$i];
    }

    //Hash the result of step 1 by using the same hash algorithm as hBaseData
    $arrayPacka = pack('c*', ...$arraya);
    $hashArraya = md5($arrayPacka);

    //Put the hash string back into the array
    $hashKeyArraya = str_split($hashArraya,2);
    $count = count($hashKeyArraya);
    $hashKeyInta = array();
    for ($i=0; $i<$count; $i++){
        $hashKeyInta[$i] = hexdec($hashKeyArraya[$i]);
    }

    //Step 2 Form a 64-byte buffer by repeating the constant 0x5C 64 times. 
    $arrayb = array_fill(0, 64, 0x5C);

    //Set the first k bytes of the buffer to the result of an XOR operation of the first k bytes of the buffer with the hash value
    for ($i=0; $i<$k; $i++){
        $arrayb[$i] =  $arrayb[$i] ^ $hashKey[$i];
    }

    //Hash the result of step 2 by using the same hash algorithm as hBaseData    
    $arrayPackb = pack('c*', ...$arrayb);
    $hashArrayb = md5($arrayPackb);

    //Put the hash string back into the array
    $hashKeyArrayb = str_split($hashArrayb,2);
    $count = count($hashKeyArrayb);
    $hashKeyIntb = array();
    for ($i=0; $i<$count; $i++){
        $hashKeyIntb[$i] = hexdec($hashKeyArrayb[$i]);
    }

    //Concatenate the result of step 3 with the result of step 4.
    $combined = array_merge($hashKeyInta, $hashKeyIntb);

    //Use the first n bytes of the result of step 5 as the derived key.
    $finalKey = array();
    for ($i=0; $i <$n; $i++){
        $finalKey[$i] =  $combined[$i];
    }
    $key = $finalKey;

    return $key;
}

PHP解密函数

function decryptRC4($encrypted, $key){
    $opts = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
    $cypher = ‘rc4-40’;
    $decrypted = openssl_decrypt($encrypted, $cypher, $key, $opts);
    return $decrypted; 
}

所以这里有几个大问题:

有没有人能够在另一个系统上成功地复制 ::CryptDeriveKey 和 RC4?

有谁知道我们创建的 PHP 脚本中缺少什么,阻止它创建相同的密钥并使用 openssl_decrypt 解密 Windows CryptoAPI 加密文件?

我们在哪里以及如何创建 40 位密钥所需的 88 位零盐?

什么是正确的 openssl_decrypt 参数,可以接受这个密钥并解密 ::CryptDeriveKey 生成的内容?

是的,我们知道这不安全,并且不会用于密码或 PII。我们希望摆脱这种陈旧且不安全的方法,但我们需要先将原始加密转换为 PHP 的临时步骤,以便与现有部署的系统进行互操作。任何帮助或指导将不胜感激。

【问题讨论】:

    标签: php hash salt cryptoapi php-openssl


    【解决方案1】:

    以防万一其他人在这条路上徘徊,这里是上述所有问题的答案。

    您可以使用 openssl 在 PHP 上复制 ::CryptDeriveKey,但首先必须在 Windows 端满足一些先决条件。

    CryptDeriveKey 必须设置为 CRYPT_NO_SALT,如下所示:

    ::CrypeDeriveKey(hCryptProv, CALG_RC4, hSaveHash, CRYPT_NO_SALT, &hCryptKey)
    

    这将允许您从散列创建一个密钥,并在 PHP 中生成一个可以在 openssl 上工作的匹配密钥。如果您不设置任何盐参数,您将获得一个使用未知专有盐算法创建的密钥,该算法无法在另一个系统上匹配。

    你必须设置 CRYPT_NO_SALT 的原因是因为 CryptAPI 和 openssl 都有专有的 salt 算法,没有办法让它们匹配。所以你应该分开腌制。这里有关于这个盐值功能的更多细节:https://docs.microsoft.com/en-us/windows/desktop/SecCrypto/salt-value-functionality

    下面是 PHP 脚本创建等效密码以供 openssl 使用的外观。

    <?php
    $random = pack('c*', 87,194,...........);
    $origSecret = 'ASCII STRING OF CHARACTERS AS PASSWORD'; 
    
    //Need conversion to match format of Windows CString or wchar_t*
    //Windows will probably be UTF-16LE and LAMP will be UTF-8
    $secret = iconv('UTF-8','UTF-16LE', $origSecret);
    
    //Create hash key from Random and Secret
    //This is basically a hash and salt process.
    $hash = hash_init("md5");
    hash_update($hash, $random);
    hash_update($hash, $secret);
    $key = hash_final($hash);
    
    $key = cryptoDeriveKey($key);
    //Convert the key hex array to a hex string for openssl_decrypt
    $count = count($key);
    $maxchars = 2;
    for ($i=0; $i<$count; $i++){
        $key .= str_pad(dechex($key[$i]), $maxchars, "0", STR_PAD_LEFT);
    }
    

    重要提示:OpenSSL 期望密钥是从散列派生的原始十六进制值,不幸的是 openssl_decrypt() 需要与字符串或密码相同的值。因此,此时您必须进行十六进制到字符串的转换。这里有一篇很棒的文章,说明了为什么你必须这样做。 http://php.net/manual/en/function.openssl-encrypt.php

    $opts = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
    //Convert key hex string to a string for openssl_decrypt
    //Leave it as it is for openssl command line.
    $key = hexToStr($key);
    $cipher = 'rc4-40';
    $encrypted = “the data you want to encrypt or decrypt”;
    $decrypted = openssl_decrypt($encrypted, $cipher, $key, $opts);  
    
    echo $decrypted;  //This is the final information you’re looking for
    
    
    function cryptoDeriveKey($key){
    //convert the key into hex byte array as int
        $hashKey1 = str_split($key,2);
        $count = count($hashKey1);
        $hashKeyInt = array();
        for ($i=0; $i<$count; $i++){
            $hashKeyInt[$i] = hexdec($hashKey1[$i]);
        }
        $hashKey = $hashKeyInt;
        //Let n be the required derived key length, in bytes.  CALG_RC4 = 40 bits key with 88 salt bits
        $n = 40/8;
        //Chop the key down to the first 40 bits or 5 bytes.
        $finalKey = array();
        for ($i=0; $i <$n; $i++){
            $finalKey[$i] =  $hashKey[$i];
        }
        return $finalKey;
    }
    
    
    function hexToStr($hex){
        $string='';
        for ($i=0; $i < strlen($hex)-1; $i+=2){
            $string .= chr(hexdec($hex[$i].$hex[$i+1]));
        }
    return $string;
    }
    ?>
    

    如果您在使用上述代码后无法获取正确的值,您可以尝试从 CryptoAPI 导出您的密钥值并使用 openssl 命令行对其进行测试。

    首先您必须设置 CryptDeriveKey 以允许使用 CRYPT_EXPORTABLE 和 CRYPT_NO_SALT 导出密钥

    ::CrypeDeriveKey(hCryptProv, CALG_RC4, hSaveHash, CRYPT_EXPORTABLE | CRYPT_NO_SALT, &hCryptKey)
    

    如果您想知道如何从导出的密钥中显示 PLAINTEXTKEYBLOB,请点击此链接。 https://docs.microsoft.com/en-us/windows/desktop/seccrypto/example-c-program--importing-a-plaintext-key

    这是一个导出的密钥 blob 示例 0x08 0x02 0x00 0x00 0x01 0x68 0x00 0x00 0x05 0x00 0x00 0x00 0xAA 0xBB 0xCC 0xDD 0xEE

    0x08 0x02 0x00 0x00 0x01 0x68 0x00 0x00 //BLOB标头几乎完全匹配 0x05 0x00 0x00 0x00 //以字节为单位的密钥长度是正确的5个字节 0xAA 0xBB 0xCC 0xDD 0xEE //我们创建的哈希键的前5个字节!!

    使用您从 BLOB 导出的键值作为下面 openssl enc 命令中的十六进制键值。

    openssl enc -d -rc4-40 -in testFile-NO_SALT-enc.txt -out testFile-NO_SALT-dec.txt -K "Hex Key Value" -nosalt -nopad
    

    这将解密在 Windows 机器上使用 CryptEncrypt 加密的文件。

    如您所见,当您将 CryptDeriveKey 设置为 CRYPT_NO_SALT 时,您需要的 openssl 密码或密钥就是 CryptHashData 密码的第一个“keylength”位。说起来很简单,但做起来真的很痛苦。祝你好运,希望这可以帮助解决遗留 Windows 翻译问题的其他人。

    【讨论】:

      猜你喜欢
      • 2017-10-15
      • 2019-07-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-22
      • 2018-03-02
      • 2020-07-17
      • 1970-01-01
      相关资源
      最近更新 更多