如何解密AES-128加密的m3u8视频文件?

问题描述 投票:0回答:6

我尝试解密 AES-128 加密的 m3u8 视频文件,例如这个:

m3u8 文件:

#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:NO
#EXT-X-VERSION:2
#EXT-X-FAXS-CM:MII6lAYJKoZIhvcNAQcCoII6hTCCOoECAQExCzAJBgUrDgMCGgUAM... very long key...
#EXT-X-KEY:METHOD=AES-128,URI="faxs://faxs.adobe.com",IV=0X99b74007b6254e4bd1c6e03631cad15b
#EXT-X-TARGETDURATION:8
#EXTINF:8,
video.mp4Frag1Num0.ts
#EXTINF:8,
video.mp4Frag1Num1.ts
...

我尝试过使用 openssl :

openssl aes-128-cbc -d -kfile key.txt -iv 99b74007b6254e4bd1c6e03631cad15b -nosalt -in video_enc.ts -out video_dec.ts

key.txt 包含很长的密钥 -->

bad decrypt
1074529488:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:evp_enc.c:539:

我做错了什么?

encryption openssl aes m3u8
6个回答
7
投票

这可能有点黑客行为,但给定 .m3u8 文件的 URL,它将下载并解密构成流的文件:

#!/usr/bin/env bash
curl "$1" -s | awk 'BEGIN {c=0} $0 ~ "EXT-X-KEY" {urlpos=index($0,"URI=")+5; ivpos=index($0,"IV="); keyurl=substr($0, urlpos, ivpos-urlpos-2); iv=substr($0, ivpos+5); print "key=`curl -s '\''"keyurl"'\'' | hexdump -C | head -1 | sed \"s/00000000//;s/|.*//;s/ //g\"`"; print "iv="iv} $0 !~ "-KEY" && $0 ~ "http" {printf("curl -s '\''"$0"'\'' | openssl aes-128-cbc -K $key -iv $iv -d >seg%05i.ts\n", c++)}' | bash

此脚本生成第二个脚本,用于提取密钥和初始化向量并在下载时使用它们进行解密。 它需要curl、awk、hexdump、sed 和openssl 才能运行。 它可能会在未加密的流或使用 AES-128 以外的流上阻塞(是否支持任何其他加密?)。

您将得到一堆文件:seg00000.ts、seg00001.ts 等。使用 tsMuxeR (https://www.videohelp.com/software/tsMuxeR) 将这些文件合并到一个文件中(简单的串联并没有对我不起作用...这是我首先尝试的):

(echo "MUXOPT --no-pcr-on-video-pid --new-audio-pes --vbr  --vbv-len=500"; (echo -n "V_MPEG4/ISO/AVC, "; for i in seg*.ts; do echo -n "\"$i\"+"; done; echo ", fps=30, insertSEI, contSPS, track=258") | sed "s/+,/,/"; (echo -n "A_AAC, "; for i in seg*.ts; do echo -n "\"$i\"+"; done; echo ", track=257") | sed "s/+,/,/") >video.meta
tsMuxeR video.meta video.ts

(轨道 ID 和帧速率可能需要调整...通过将下载的文件之一传递到 tsMuxeR 来获取要使用的值。)

然后使用 ffmpeg 重新混合为更广泛理解的内容:

ffmpeg -i video.ts -vcodec copy -acodec copy video.m4v

6
投票

为了解密加密的视频流,您需要加密密钥。 该密钥不是流的一部分。应单独获取。

EXT-X-FAXS-CM 标头包含 DRM 元数据,而不是密钥。

这是 Adobe Media Server 开发人员指南的摘录: Adobe Access Server 受保护的变体播放列表还需要包含 #EXT-X-FAXS-CM 标签。变体播放列表中 #EXT-X-FAXS-CM 标签的值是引用单个流之一的 DRM 元数据的相对 URI。在客户端,变体播放列表中的 #EXT-X-FAXS-CM 标签将是用于创建 DRM 会话。相同的 DRM 会话将用于变体播放列表内的所有加密 M3U8 文件。

完整指南可以在这里找到: http://help.adobe.com/en_US/adobemediaserver/devguide/WS5262178513756206-4b6aabd1378392bb59-7fe8.html

还提到faxs://faxs.adobe.com URI 用于本地密钥服务。 因此从设备本地获取密钥。


1
投票

虽然现有答案中的一些 bash 脚本可以帮助您完成部分(甚至全部)任务,但取决于您尝试从哪个站点下载,您可能会遇到其他障碍(不同的身份验证方法、自定义许可证服务器安装等) .)

我发现 streamlink 是最强大的解决方案,它还可以让您直接流式传输(而不是下载),如果这就是您所追求的,并且它已经为您完成了所有特定于站点的工作有关站点的长列表(请参阅插件部分,但请记住它正在积极开发中,最新版本是在六月,因此对于一些较新的站点,您必须

git clone
并从源代码安装)。


1
投票

在许多情况下,VLC 会很乐意将 .m3u8 视频转换为未加密的 .ts 或 .mp4。在 VLC 图形界面中,转到媒体 > 转换/保存。


0
投票

即使此文件包含 AES 加密数据,

openssl
也不知道
m3u8
格式。 然而FFmpeg也许能够处理它。


0
投票

首先使用网络浏览器检查您尝试加载的网站上的网络流量。找到播放列表,因为您需要它来进行解码。在某些情况下,当您发出密钥请求时,您的 cookie 和可能的其他标头对于获取有效密钥很重要,因此请右键单击浏览器检查器中获取的网络项目,然后选择“复制为 cURL”。它将提供很多东西,包括可能有用的标题,并且看起来像这样:

curl -H 'User-Agent: Mozilla/5.0... Firefox/128.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'Origin: https://www.example.com' ... \
  https://example.com/path/to/playlist.m3u8

复制所有

-H
标头和生成的 URL,并将它们作为参数粘贴到下面的脚本中:

./rip-crypto-m3u8 -H 'User-Agent: Mozilla/5.0... Firefox/128.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'Origin: https://www.example.com' ... \
  https://example.com/path/to/playlist.m3u8

这是对我有用的脚本:

#!/usr/bin/perl
use strict;
use warnings;
use WWW::Curl::Easy;
use Getopt::Long;
use File::Basename;
use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
use IO::Uncompress::Inflate qw(inflate $InflateError);
use URI;

# Command-line options
my $url;
my @headers;

GetOptions(
    'H=s' => \@headers, # Multiple -H options for headers
) or die "Usage: $0 -H 'Header: Value' <url>\n";

# Get the URL
$url = pop @ARGV or die "Usage: $0 -H 'Header: Value' <url>\n";

# Fetch URL with headers
sub fetch_url {
    my ($url, $headers) = @_;
    my $curl = WWW::Curl::Easy->new;

    # Set CURL options
    $curl->setopt(CURLOPT_URL, $url);
    $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
    $curl->setopt(CURLOPT_FAILONERROR, 1);
    $curl->setopt(CURLOPT_ENCODING, '');  # Accept any encoding

    # Prepare headers
    my @curl_headers;
    my $user_agent;
    for my $header (@$headers) {
        my ($name, $value) = split /:\s*/, $header, 2;
        if (lc $name eq 'user-agent') {
            $user_agent = $value;
            $curl->setopt(CURLOPT_USERAGENT, $value);
        } else {
            push @curl_headers, "$name: $value";
        }
    }
    $curl->setopt(CURLOPT_HTTPHEADER, \@curl_headers) if @curl_headers;

    # Capture response body and headers
    my $response_body = '';
    my $response_headers = '';

    # Define write callback for body
    my $body_callback = sub {
        my ($data, $size, $nmemb) = @_;
        $response_body .= $data;
        return length($data);
    };

    # Define write callback for headers
    my $header_callback = sub {
        my ($data, $size, $nmemb) = @_;
        $response_headers .= $data;
        return length($data);
    };

    $curl->setopt(CURLOPT_WRITEFUNCTION, $body_callback);
    $curl->setopt(CURLOPT_HEADERFUNCTION, $header_callback);

    # Perform the request
    my $retcode = $curl->perform;

    # Check for errors
    if ($retcode != 0) {
        die "Error fetching $url: " . $curl->strerror($retcode) . " (" . $curl->errbuf . ")\n";
    }

    # Decode based on Content-Encoding
    if ($response_headers =~ /Content-Encoding:\s*(\S+)/i) {
        my $encoding = lc $1;
        if ($encoding eq 'gzip') {
            my $decoded;
            gunzip(\$response_body => \$decoded) or die "gunzip failed: $GunzipError\n";
            $response_body = $decoded;
        } elsif ($encoding eq 'deflate') {
            my $decoded;
            inflate(\$response_body => \$decoded) or die "inflate failed: $InflateError\n";
            $response_body = $decoded;
        }
        # Removed Brotli ('br') handling
    }

    return $response_body;
}

# Fetch the playlist
my $playlist = fetch_url($url, \@headers);

# Verify playlist content
if (!defined($playlist) || $playlist eq '') {
    die "Playlist content is empty or couldn't be retrieved.\n";
}

# Initialize key and IV
my ($key, $iv) = ('', '');

# Process each line with debugging
my $line_number = 0;
for my $line (split /\n/, $playlist) {
    $line_number++;
    print "Processing line $line_number: $line\n";  # Debug

    if ($line =~ /EXT-X-KEY/) {
        # Extract key URL and IV
        my ($key_url) = $line =~ /URI="([^"]+)"/;
        ($iv) = $line =~ /IV=0x([0-9A-Fa-f]+)/;

        unless ($key_url && $iv) {
            die "Failed to parse key URL or IV from line $line_number: $line\n";
        }

        # Resolve relative key URLs
        my $key_uri = URI->new_abs($key_url, $url);
        my $resolved_key_url = $key_uri->as_string;

        print "Resolved key URL: $resolved_key_url\n";    # Debug
        print "Extracted IV: $iv\n";                      # Debug

        # Fetch key content and convert to hex
        my $key_content = fetch_url($resolved_key_url, \@headers);
        unless (defined $key_content && length $key_content) {
            die "Failed to fetch key content from $resolved_key_url\n";
        }
        $key = unpack("H*", $key_content);
        print "Extracted key: $key\n";                     # Debug
    }
    elsif ($line !~ /-KEY/ && $line =~ /^https?:\/\//) {  # Match http and https
        # Extract filename without query
        my ($segment_url) = $line =~ /^([^?]+)/;
        my $output_file = basename($segment_url);

        unless ($output_file) {
            die "Failed to extract filename from URL: $line\n";
        }

        print "Fetching segment from URL: $line\n";        # Debug
        print "Output file will be: $output_file\n";       # Debug

        # Fetch segment content
        my $segment_content = fetch_url($line, \@headers);
        unless (defined $segment_content && length $segment_content) {
            die "Failed to fetch segment content from $line\n";
        }

        # Decrypt and save segment
        open my $fh, '|-', "openssl aes-128-cbc -K $key -iv $iv -d > \"$output_file\""
            or die "Couldn't open openssl for writing: $!\n";
        binmode $fh;  # Binary mode
        print $fh $segment_content;
        close $fh or warn "Failed to close filehandle for $output_file: $!\n";

        print "Decrypted segment saved to $output_file\n";  # Debug
    }
}

© www.soinside.com 2019 - 2024. All rights reserved.