Расшифровка hls потока с помощью openssl_decrypt
Что использовать для расшифровки hls стрима, краткий гайд для работы с функцией openssl_decrypt() в в PHP
С чего все началось
Один из моих проектов выкачивает видео с одного достаточно популярного сайта, но уже несколько потерявшего свое былое величие. С недавних пор этот сайт начал отдавать видео, зашифрованное с помощью алгоритма AES-128. Пользователи начали жаловаться на то, что скачанное видео не воспроизводится, что вылилось в срочную доработку кода "качалки" видео.
Что имеем на руках
Итак, у нас есть обычный плейлист m3u8 потока в виде:
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PROGRAM-DATE-TIME:2019-04-18T04:29:04.871Z
#EXT-X-KEY:METHOD=AES-128,URI="hlsEncryptionKey?stream_name=LhSX5NKOZDnZEBlbXQjdyq86CWWVjx-wqvhT_oQ7UJ-0HUweUwmPYb0WiocgyTwTAjZ4R7sF16l48l5LHUzx7A",IV=0x07a6ed33e15beca7f64e57e5137f7fde
#EXTINF:2.035,
k0_chunk_1555561741998417630_0_a.ts
#EXT-X-PROGRAM-DATE-TIME:2019-04-18T04:29:06.907Z
#EXT-X-KEY:METHOD=AES-128,URI="hlsEncryptionKey?stream_name=LhSX5NKOZDnZEBlbXQjdyq86CWWVjx-wqvhT_oQ7UJ-0HUweUwmPYb0WiocgyTwTAjZ4R7sF16l48l5LHUzx7A",IV=0xb51d4f0db93191e12fd7cb405596f57c
#EXTINF:3.198,
k0_chunk_1555561742000754678_1_a.ts
#EXT-X-PROGRAM-DATE-TIME:2019-04-18T04:29:10.104Z
#EXT-X-KEY:METHOD=AES-128,URI="hlsEncryptionKey?stream_name=LhSX5NKOZDnZEBlbXQjdyq86CWWVjx-wqvhT_oQ7UJ-0HUweUwmPYb0WiocgyTwTAjZ4R7sF16l48l5LHUzx7A",IV=0xc24dd0563a70e8b70b499f173d7e11eb
#EXTINF:3.212,
k0_chunk_1555561742002319677_2_a.ts
#EXT-X-PROGRAM-DATE-TIME:2019-04-18T04:29:13.316Z
#EXT-X-KEY:METHOD=AES-128,URI="hlsEncryptionKey?stream_name=LhSX5NKOZDnZEBlbXQjdyq86CWWVjx-wqvhT_oQ7UJ-0HUweUwmPYb0WiocgyTwTAjZ4R7sF16l48l5LHUzx7A",IV=0xfe07b0f27f9bb09a156208859cf7a9fd
#EXTINF:3.224,
k0_chunk_1555561747042459190_3_a.ts
#EXT-X-PROGRAM-DATE-TIME:2019-04-18T04:29:16.540Z
#EXT-X-KEY:METHOD=AES-128,URI="hlsEncryptionKey?stream_name=LhSX5NKOZDnZEBlbXQjdyq86CWWVjx-wqvhT_oQ7UJ-0HUweUwmPYb0WiocgyTwTAjZ4R7sF16l48l5LHUzx7A",IV=0x6ce77aa4e50f4724395e90536da47234
#EXTINF:3.228,
Раньше в плейлисте были указаны только чанки (куски) потока, пример k0_chunk_1555561741998417630_0_a.ts. Именно после их объединения и и получался на выходе один большой файл. Теперь можно заметить, что появилась новая строка с указанием метода шифрования, ключа и дополнительных параметров.
#EXT-X-KEY:METHOD=AES-128,URI="hlsEncryptionKey?stream_name=LhSX5NKOZDnZEBlbXQjdyq86CWWVjx-wqvhT_oQ7UJ-0HUweUwmPYb0WiocgyTwTAjZ4R7sF16l48l5LHUzx7A",IV=0x07a6ed33e15beca7f64e57e5137f7fde
Алгоритм шифрования - AES-128, более подробно о нем можно почитать на Хабре. Нам же он понадобится для указания метода в функции дешифрования.
Можно было бы использовать функцию mcrypt_decrypt() или попробовать расшифровать с помощью:
openssl aes-128 -d -in encrypted_segment.ts -out decrypted_segment.ts -nosalt -iv <iv_hex> -K <key_hex>
Но mcrypt_decrypt() устарела и в PHP 7.2 была удалена. Вместо нее воспользуемся openssl_decrypt().
openssl_decrypt ( string $data , string $method , string $key [, int $options = 0 [, string $iv = "" [, string $tag = "" [, string $aad = "" ]]]] ) : string
- data - Данные для расшифровки.
- method - Метод шифрования. Список доступных методов можно получить с помощью функции openssl_get_cipher_methods().
- key - Ключ.
- options - options можно задать одной из констант: OPENSSL_RAW_DATA, OPENSSL_ZERO_PADDING.
- iv - Ненулевой инициализирующий вектор.
- tag - Тег аутентификации в режиме шифрования AEAD. Если он некорректен, то аутентификация завершится неудачей и функция вернет FALSE.
- aad - Дополнительные аутентификационные данные.
//обращаю ваше внимание что ненулевые вектора мы вытаскиваем без 0x, это очень важно (!)
preg_match_all('/.*URI="(.*)".*IV=0x(.*).*/', $playlist, $cryptoData);
$cryptoData[0] = [...//массив ключей для каждого чанка, в моем случае он одинаков для всех];
$cryptoData[1] = [...//массив инициализирующих ненулевых векторов];
$chunkList = [...//массив с именами чанков];
$key = $this->getEncriptionKey($baseUrl . $cryptoData[0][0]);
foreach ($chunkList as $countChunk => $chunk) {
try {
//дергаем шифрованный файл чанка
$chunkResponse = $this->guzzle->get($baseUrl . $chunk,
["cookies" => $cookies ])->getBody()->getContents();
} catch (ClientException $e) {
continue;
}
/**
* алгоритм шифрования, достаем из того же плейлиста m3u8
* но для правильной работы убран "-"
*/
$method = "AES128";
//"ломаем" с помощью openssl_decrypt()
$chunkResponseDecrypt = openssl_decrypt(
$chunkResponse,
$method,
$key,
OPENSSL_RAW_DATA,
hex2bin($cryptoData[1][$count])
);
//сохраняем расшифрованный чанк
file_put_contents($tmpDir . $chunk, $chunkResponseDecrypt);
}
...
//загружаем ключ для дешифровки
public function getEncriptionKey($urlKey)
{
//я использую Guzzle Client
$client = new Client();
$key = $client->get($urlKey);
return $key->getBody()->getContents();
}
Обратите внимание на то, что ненулевые инициализирующие вектора мы преобразуем из шестнадцатеричных данных в двоичные, с помощью функции hex2bin(). Когда я просто пытался расшифровать без преобразования векторов, то видео было без звука, тоже важно это заметить. Второе, на что нужно обратить внимание, если не обрезать 0x в строке с вектором, то звук также может отсутствовать, да и в принципе видео, в моем случае, было "битым" в некоторых местах.
Подведем итоги
- функция mcrypt_decrypt() устарела и удалена
- используем openssl_decrypt()
- обрезаем у ненулевых инициализирующих векторов 0x
- не забываем преобразовать ненулевые вектора с помощью hex2bin()
Yrstal14.02.2024
Здравствуйте у меня такой вопрос. Можно ли с hls или с m3u8 стриминга потока узнать оригинальную ссылку стрима? Либо ip адрес первоначального стримера?