将Windows RC4 CryptDeriveKey翻译为PHP的openssl。

4
这是我们一直在努力进行的遗留系统翻译的第二个组件。我们已经成功匹配了Windows :: CryptHashData生成的初始二进制密码/密钥。
该密码/密钥传递到:: CryptDeriveKey,其中执行多个步骤以创建由:: CryptEncrypt使用的最终密钥。我的研究导致了CryptDeriveKey文档,其中清楚地描述了为:: CryptEncrypt派生密钥所需的步骤,但到目前为止,我还没有能够使它在PHP端解密文件。 https://learn.microsoft.com/en-us/windows/desktop/api/wincrypt/nf-wincrypt-cryptderivekey 根据:: CryptDeriveKey文档,可能存在一些额外的未记录步骤,用于我们特定的遗留密钥大小,这些步骤可能不是很清楚。当前的Windows :: CryptDeriveKey默认设置为ZERO SALT,这显然与NO_SALT有所不同。请参见此处的盐值功能: https://learn.microsoft.com/en-us/windows/desktop/SecCrypto/salt-value-functionality 我们遗留系统上CryptAPI的参数如下:
提供者类型: PROV_RSA_FULL
提供者名称: MS_DEF_PROV
算法ID: CALG_RC4
描述: RC4流加密算法
密钥长度: 40位。
盐长度: 88位。 ZERO_SALT
特别注意: 值为零的盐与40位对称密钥不带盐不等效。为实现互操作性,必须创建不带盐的密钥。这个问题只会出现在正好有40位的密钥的默认条件下。
我不打算导出密钥,而是复制创建传递给::CryptEncrypt作为RC4加密算法的最终加密密钥的过程,并使其与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表示的哈希值进行XOR操作的结果。

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

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

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

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

::CryptDeriveKey的PHP版本。

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; 
}

以下是重要问题:

有没有人成功地在另一个系统上复制了使用RC4的::CryptDeriveKey?

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

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

哪些是正确的openssl_decrypt参数,可以接受由::CryptDeriveKey生成的密钥并解密数据?

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

1个回答

2

如果有人也想探索这条路,以下是上面所有问题的答案。

你可以使用openssl在PHP中复制::CryptDeriveKey,但必须先满足Windows端的某些前提条件。

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

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

这将允许您从哈希中创建一个密钥,并在PHP中生成一个匹配的密钥,可用于openssl。如果您不设置任何盐参数,则会得到一个使用未知专有盐算法创建的密钥,无法在另一个系统上匹配。
之所以必须设置CRYPT_NO_SALT,是因为CryptAPI和openssl都具有专有的盐算法,无法使它们匹配。因此,您应该单独进行加盐。有关此盐值功能的更多详细信息,请参见https://learn.microsoft.com/en-us/windows/desktop/SecCrypto/salt-value-functionality 以下是创建等效passkey供openssl使用的PHP脚本的外观。
<?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://learn.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个字节!!
在下面的openssl enc命令中使用来自BLOB的导出密钥值作为十六进制密钥值。
openssl enc -d -rc4-40 -in testFile-NO_SALT-enc.txt -out testFile-NO_SALT-dec.txt -K "Hex Key Value" -nosalt -nopad

这将解密使用CryptEncrypt在Windows机器上加密的文件。
如您所见,当您将CryptDeriveKey设置为CRYPT_NO_SALT时,您只需要使用CryptHashData密码的前“keylength”位作为openssl密码或密钥。虽然很简单,但实际操作起来很麻烦。祝好运,希望能帮助其他遇到旧版Windows翻译问题的人。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接