Это второй компонент перевода устаревшей системы, который мы пытались сделать. Нам удалось точно сопоставить исходный двоичный пароль / ключ, который генерирует 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 бит. ZERO_SALT
Специальное примечание: 40-битный симметричный ключ с нулевой солью, однако, не эквивалентен 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;
}
Вот код PHP, который пытается скопировать функцию :: CryptDeriveKey, как описано в документации.
Пусть n будет необходимой длиной производного ключа в байтах. Полученный ключ — это первые n байтов значения хеша после того, как CryptDeriveKey завершил вычисление хеша. Если хэш не является членом семейства SHA-2 и требуемый ключ предназначен для 3DES или AES, ключ получается следующим образом:
Создайте 64-байтовый буфер, повторяя константу 0x36 64 раза. Пусть k будет длиной значения хеша, которое представлено входным параметром hBaseData. Установите первые k байтов буфера для результата операции XOR первых k байтов буфера со значением хеш-функции, которое представлено входным параметром hBaseData.
Создайте 64-байтовый буфер, повторяя константу 0x5C 64 раза. Установите первые k байтов буфера в качестве результата операции XOR первых k байтов буфера со значением хеш-функции, которое представлено входным параметром hBaseData.
Хэшируйте результат шага 1, используя тот же алгоритм хеширования, который использовался для вычисления значения хеш-функции, представленного параметром hBaseData.
Хэшируйте результат шага 2, используя тот же алгоритм хеширования, который использовался для вычисления значения хеш-функции, представленного параметром hBaseData.
Объедините результат шага 3 с результатом шага 4.
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, который мы создали, что мешает ему создать тот же ключ и расшифровать зашифрованный файл Windows CryptoAPI с помощью openssl_decrypt?
Где и как мы можем создать 88-битную нулевую соль, которая требуется для 40-битного ключа?
Каковы правильные параметры openssl_decrypt, которые бы приняли этот ключ и расшифровали то, что было сгенерировано :: CryptDeriveKey?
Да, мы знаем, что это небезопасно и не используется для паролей или PII. Мы хотели бы отойти от этого старого и небезопасного метода, но нам нужно сделать этот промежуточный шаг — сначала перевести оригинальное шифрование на PHP для обеспечения взаимодействия с существующими развернутыми системами. Любая помощь или руководство будут оценены.
На тот случай, если кто-нибудь еще пойдет по этому пути, вот ответы на все вопросы выше.
Вы можете реплицировать :: CryptDeriveKey на PHP, используя openssl, но есть некоторые предварительные условия, которые должны быть выполнены в первую очередь на стороне Windows.
CryptDeriveKey ДОЛЖЕН быть установлен в CRYPT_NO_SALT следующим образом:
::CrypeDeriveKey(hCryptProv, CALG_RC4, hSaveHash, CRYPT_NO_SALT, &hCryptKey)
Это позволит вам создать ключ из вашего хэша и сгенерировать соответствующий ключ в PHP, который будет работать на openssl. Если вы не установите параметры соли, вы получите ключ, созданный с помощью неизвестного запатентованного алгоритма соли, который не может быть сопоставлен с другой системой.
Причина, по которой вы должны установить CRYPT_NO_SALT, заключается в том, что и CryptAPI, и openssl имеют собственные алгоритмы соли, и нет никакого способа заставить их соответствовать. Так что солить надо отдельно. Здесь есть более подробная информация об этой функциональной ценности соли: 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
Вот пример экспортированного ключевого блоба
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, это первые биты «keylength» вашего пароля CryptHashData. Достаточно просто сказать, но настоящая боль, чтобы добраться до. Удачи и надеюсь, что это поможет кому-то еще с устаревшими проблемами перевода Windows.
Других решений пока нет …