提示

返回博客列表

HLS AES-128 加密流怎么下:从 #EXT-X-KEY 到解密合并,我把整条链路拆给你看

第一次碰到加密 HLS 的时候,我完全没意识到出了什么事。m3u8 下下来了,几百个 .ts 分片也都下下来了,合并完之后用播放器打开——画面花成马赛克,声音像坏掉的收音机。我第一反应是分片没下载全,回去一个个校验 MD5,全对。折腾到快凌晨才注意到 m3u8 里那行我一直当注释跳过的东西:

```

EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key/abc123",IV=0x9c7db8778570d05c3f8f2a1b4e6d9f30

```

这行不起眼的标签,就是"分片全是密文"的原因。后来我发现,很多人卡在这一步不是因为技术多难,而是因为没人告诉他们:HLS 的 AES-128 加密根本不是 DRM,它只是给每个分片上了把锁,钥匙就挂在旁边。这篇把我后来摸清的整条链路写下来——从看懂那行标签,到手动解密,再到用 ffmpeg 和 yt-dlp 一把梭,最后是那些让我栽过跟头的细节。

TL;DR:HLS 的 AES-128 是分片级对称加密,密钥以 HTTP(S) URL 形式写在 m3u8 的 #EXT-X-KEY 里。完整流程 = 拿 m3u8 → 提取 key URI 下载 16 字节密钥 → 逐片 AES-128-CBC 解密 → 按序合并。ffmpeg 能全自动完成(前提是它能访问到 key URI,必要时加 -referer / -headers),yt-dlp 也内建支持。真正的难点不在解密本身,而在:密钥 URL 带鉴权、IV 的处理方式、密钥轮换,以及分辨 AES-128 和 SAMPLE-AES(后者才是真 DRM,别混为一谈)

目录

一、先搞清楚:HLS 加密到底是什么级别的东西

我得先纠正一个流传很广的误解:HLS 的 AES-128 不是 DRM

这个区别决定了你要走哪条路:

HLS AES-128 真 DRM(FairPlay / Widevine / PlayReady)
加密方式 分片对称加密(AES-128-CBC) 内容密钥受许可证与硬件保护
密钥在哪 m3u8 里的一个 URL,HTTP 就能拿到 需向许可证服务器申请,返回的是加密过的密钥
解密难度 拿到密钥后 openssl 一行 需要 CDM、设备级密钥,基本不可行
ffmpeg 能直接处理 不能
典型标签 METHOD=AES-128 METHOD=SAMPLE-AESKEYFORMAT=com.apple.streamingkeydelivery
密钥长度 正好 16 字节 不适用

所以看到 METHOD=AES-128,恭喜,这是"能下"的那一类。看到 SAMPLE-AEScom.apple.streamingkeydeliverycom.widevine.alpha,那就是另一回事了,本文的方法不适用,直接看第八节。

还有个很实用的土办法判断:打开 Network 面板,看有没有一个请求返回体正好 16 字节的二进制。有的话基本就是 AES-128;如果看到的是 skd:// 开头的 URI 或者请求发往 license 服务器,那就是 DRM。

二、读懂 #EXT-X-KEY 这一行

一个典型加密 m3u8 长这样:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="https://cdn.example.com/keys/9f2a.key",IV=0x9c7db8778570d05c3f8f2a1b4e6d9f30
#EXTINF:10.000,
segment0.ts
#EXTINF:10.000,
segment1.ts

字段含义:

字段 含义 注意点
METHOD 加密方法 NONE(不加密)/ AES-128 / SAMPLE-AES(DRM)
URI 密钥地址 下载下来必须正好 16 字节
IV 初始化向量 十六进制,带 0x 前缀,可省略
KEYFORMAT 密钥格式 默认 identity;其他值基本都是 DRM
KEYFORMATVERSIONS 版本 一般不用管

三个容易忽略的点,每一个我都踩过:

  1. #EXT-X-KEY 可以出现多次。流中途换密钥(密钥轮换)时,新的 key 标签会出现在对应分片前面,只作用于它后面的分片。只处理第一个 key 的话,后半段会解出乱码——表现为"前 5 分钟正常,后面全花",排查起来特别费时间。

  2. IV 可以省略。省略时按 HLS 规范用分片的媒体序列号#EXT-X-MEDIA-SEQUENCE 起始值 + 分片序号)作为隐式 IV,转成 16 字节大端整数。手动解密时若 m3u8 没有 IV,你必须自己算,不能传空也不能传全零(全零只在序列号为 0 时对)。

  3. URI 可以是相对路径,要相对于 m3u8 的 URL 解析。只拼域名部分会 404,而且 404 响应体往往是个 HTML 错误页——拿它当密钥解密会得到一堆花屏,很难联想到是 key 下错了。

三、手动走一遍:下载、取密钥、解密、合并

理解原理最好的办法是手动来一次。假设:

M3U8_URL="https://cdn.example.com/vod/12345/index.m3u8"

3.1 拿 m3u8

curl -s "$M3U8_URL" -H "Referer: https://www.example.com/" -o index.m3u8
head -20 index.m3u8

3.2 提取 key URI 和 IV

shell 正则对转义不健壮,我更倾向用 python 解析:

import re
from urllib.parse import urljoin

M3U8 = "https://cdn.example.com/vod/12345/index.m3u8"
text = open("index.m3u8", encoding="utf-8").read()

m = re.search(r'#EXT-X-KEY:([^\n]+)', text)
attrs = dict(
    kv.split('=', 1) for kv in
    [x.strip() for x in m.group(1).split(',')] if '=' in kv
)
key_uri = attrs.get('URI', '').strip('"')
iv = attrs.get('IV', '')
key_url = urljoin(M3U8, key_uri)      # 关键:处理相对路径
print("KEY:", key_url)
print("IV :", iv)

3.3 下载密钥并确认长度

curl -s "$KEY_URL" -o video.key
stat -c%s video.key     # Linux:必须是 16
xxd -p video.key        # 看十六进制,后面要用

这一步一定要确认是 16 字节。如果不是(返回了 HTML 错误页、JSON、或者 32 字节),说明 key URL 需要鉴权或者你下错了东西。直接拿去解密必然全花,而且报错信息不会告诉你原因。

3.4 下载分片

mkdir -p segs
i=0
grep -v '^#' index.m3u8 | grep -v '^$' | while read -r url; do
  full=$(python3 -c "from urllib.parse import urljoin;import sys;print(urljoin('$M3U8_URL', sys.argv[1]))" "$url")
  printf -v name '%05d.ts' "$i"
  curl -s "$full" -o "segs/$name"
  i=$((i+1))
done

3.5 逐片解密

mkdir -p dec
KEY_HEX=$(xxd -p video.key | tr -d '\n')

for f in segs/*.ts; do
  base=$(basename "$f")
  idx=$((10#${base%.ts}))     # 去掉前导零
  if [ -n "$IV" ]; then
    USE_IV="${IV#0x}"         # 去掉 0x 前缀
  else
    USE_IV=$(printf '%032x' "$idx")   # 隐式 IV:序号转 16 字节大端
  fi
  openssl aes-128-cbc -d -in "$f" -out "dec/$base" -K "$KEY_HEX" -iv "$USE_IV"
done

注意 openssl-K-iv 都要十六进制字符串,不是原始字节。xxd -p 输出的正好是 hex,直接喂进去即可。

3.6 合并

ls dec/*.ts | sort | sed 's/^/file /' > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy output.mp4

手动走完这一遍,你会对整个机制有非常具体的认识。之后再遇到问题,就知道该查哪一环——是 key 不对、IV 不对,还是分片顺序乱了。

四、ffmpeg 一把梭:自动处理与自定义请求头

绝大多数时候不需要手动来。ffmpeg 内建支持 HLS AES-128:

ffmpeg -i "https://cdn.example.com/vod/12345/index.m3u8" -c copy output.mp4

它会自动读 #EXT-X-KEY、下载密钥、解密、合并。前提是ffmpeg 能成功访问 key URL。如果 key URL 需要 Referer 或 Cookie,你会看到:

[https @ 0x55f9c0] HTTP error 403 Forbidden
Error opening input file ...

加请求头:

ffmpeg -referer "https://www.example.com/" \
  -i "https://cdn.example.com/vod/12345/index.m3u8" \
  -c copy output.mp4

需要自定义多个头时用 -headers,多个头之间用 CRLF(\r\n)分隔。bash 里这么写最稳:

HEADERS=$(printf 'Referer: %s\r\nCookie: %s\r\nUser-Agent: %s\r\n' \
  "https://www.example.com/" "session=xxx" "Mozilla/5.0")
ffmpeg -headers "$HEADERS" -i "$M3U8_URL" -c copy output.mp4

两个很常用的附加参数:

# 分片用了非标准扩展名(.m4s / 无扩展名 / .jpg)时 ffmpeg 会拒绝加载
-allowed_extensions ALL

# 调试用:把每次 key 请求、分片请求都打出来
-v verbose

我排查问题时的标准起手式就是加 -v verbose,看它有没有去请求 key、请求返回了多少字节。如果日志里压根没出现 key 请求,说明 m3u8 解析有问题;如果出现了但返回 403,就是鉴权问题;如果返回了但大小不是 16 字节,就是 key URL 不对。三种情况三种修法。

五、yt-dlp 的姿势

yt-dlp 内建 HLS 支持,包括 AES-128 解密,通常一条命令就够:

yt-dlp "https://www.example.com/watch/12345" -o "%(title)s.%(ext)s"

需要额外请求头时:

yt-dlp --referer "https://www.example.com/" \
  --add-header "Cookie:session=xxx" \
  "https://cdn.example.com/vod/12345/index.m3u8"

几个我常用的组合:

# 先看有哪些格式,不下(排错第一步)
yt-dlp -F "URL"

# 只看 HLS 相关的原生 URL,配合 ffmpeg 手动处理
yt-dlp -F "URL" | grep -i m3u8

# 强制走 ffmpeg 而不是原生下载器(某些流更稳)
yt-dlp --downloader ffmpeg "URL"

# 保留中间分片用于排查
yt-dlp --keep-fragments "URL"

--keep-fragments 这个选项在我排查"合并后花屏"时帮了大忙:保留原始分片后,我可以单独对每个分片做解密测试,快速定位是某一片坏了还是全流程都错了。

六、密钥 URL 带鉴权时怎么办

这是实际场景里最常见也最烦的情况:分片能下,密钥下不来(403/401),于是合出来全是花屏。

6.1 先确认到底是哪种失败

别猜,用 curl 直接问一次:

curl -s -o key.bin -w 'HTTP=%{http_code} SIZE=%{size_download}\n' \
  "$KEY_URL" -H "Referer: https://www.example.com/"

判读:

输出 说明 下一步
HTTP=200 SIZE=16 正常 问题不在 key,去查 IV 或分片顺序
HTTP=403 缺 Referer / Cookie / UA 补齐请求头(方案 A/B)
HTTP=200 SIZE=1234 返回了网页或 JSON URL 不对,或需要签名参数
HTTP=401 需要登录态 用你自己的登录 Cookie(前提是你有权访问)

6.2 方案 A:给 ffmpeg 补请求头

前面第四节写过,最简单。适用于 key 只缺 Referer/Cookie 的情况。

6.3 方案 B:yt-dlp 加头

yt-dlp --referer "https://www.example.com/" \
  --add-header "Cookie:session=xxx;token=yyy" \
  "URL"

6.4 方案 C:把 key 下到本地,改写 m3u8(我最常用的招)

当 key URL 带一次性签名、或者 ffmpeg 怎么都带不上正确的头时,这招最省事:

# 1) 用能成功的方式(浏览器复制的 curl / 带完整 Cookie)把 key 存到本地
curl -s "$KEY_URL" -H "Cookie: session=xxx" -o video.key
stat -c%s video.key    # 确认 16

# 2) 把 m3u8 里的 key URI 改成本地文件
sed -E 's|URI="[^"]*"|URI="video.key"|g' index.m3u8 > local.m3u8

# 3) 让 ffmpeg 读本地 m3u8(注意 protocol_whitelist 要放开 file)
ffmpeg -allowed_extensions ALL \
  -protocol_whitelist file,http,https,tcp,tls,crypto \
  -i local.m3u8 -c copy output.mp4

-protocol_whitelist 这串必须带上 file(否则本地文件不让读)和 crypto(否则解密协议被拦)。我第一次用这招时就是漏了 crypto,报错 Protocol 'crypto' not on whitelist,查了半天。

如果连分片也要走本地,把分片也下下来、m3u8 里的分片名改成相对路径即可,同样走这套。

6.5 方案 D:分片全下 + 手动解密

ffmpeg 和 yt-dlp 都搞不定的时候,回到第三节的手动流程。yt-dlp 用 --keep-fragments 保留分片,或者你自己 curl 批量下,然后按 3.5 逐片解密、3.6 合并。慢,但是可控,出问题能定位到具体哪一片。

七、七个坑

按我遇到的频率排序。

1. 密钥不是 16 字节
症状:合并后全花屏,且 ffmpeg 不报错。原因几乎都是 key 请求返回了错误页(HTML/JSON/重定向)。对策:curl -w '%{size_download}' 确认长度,永远是排查 AES-128 的第一步。

2. 密钥轮换只处理了第一个
症状:前几分钟正常,后面花屏。原因:m3u8 里有多个 #EXT-X-KEY,只用了第一个。对策:解析时按出现位置分段处理,每个 key 作用于它之后的片段。ffmpeg 会自动处理,手写的脚本容易漏。

3. 隐式 IV 算错
症状:第一片能解,后面全花。原因:m3u8 没写 IV,你按分片序号从 0 开始算,但该流的 #EXT-X-MEDIA-SEQUENCE 不是 0。正确算法是 media_sequence + 分片索引,不是单纯的分片索引:

iv_int = media_sequence + seg_index
iv_bytes = iv_int.to_bytes(16, 'big')      # 大端 16 字节
iv_hex = iv_bytes.hex()

这个坑在小项目里很常见,因为大部分测试流的 MEDIA-SEQUENCE 恰好是 0,代码跑通了就以为对了,上到直播流(序列号很大)就崩。

4. key URI 是相对路径没解析
症状:key 请求 404,但你复制 URL 到浏览器又能打开(因为你复制的是拼接后的)。对策:一律用 urljoin(m3u8_url, key_uri)

5. 把 SAMPLE-AES 当 AES-128 处理
症状:key 拿到了(甚至能拿到 16 字节),但解密出来还是花屏。原因:SAMPLE-AES 是逐样本加密,不是整片 AES-128-CBC,openssl 那套不适用。对策:看 KEYFORMAT,非 identity 就是 DRM,趁早收手。

6. 分片合并顺序错
症状:画面能看但剧情跳跃、音画错位。原因:按文件名字典序排序(1.ts, 10.ts, 2.ts)而不是数值序,或者没按 m3u8 里的顺序。对策:下载时用 %05d 补零命名,合并时按 m3u8 原始顺序生成列表。

7. openssl 报 bad decrypt
症状:个别分片(常常是最后一片)解密失败。原因:padding 校验。可以先试试加 -nopad

openssl aes-128-cbc -d -nopad -in seg.ts -out seg_dec.ts -K "$KEY" -iv "$IV"

不过更常见的原因是 key 或 IV 错了——先确认前六条再怀疑 padding。

八、怎么快速判断你面对的是不是真 DRM

给你一个我自己的检查顺序,基本 30 秒内能定:

  1. 看 m3u8 里的 METHODAES-128 → 可下;SAMPLE-AES → DRM。
  2. KEYFORMAT。没有或 identity → 正常;com.apple.streamingkeydelivery / com.widevine.alpha / com.microsoft.playready → DRM。
  3. 看 URI 协议https://... → 正常;skd://... → 一定是 DRM。
  4. 看浏览器行为。DevTools Console 里搜 requestMediaKeySystemAccess,有调用就是走了 EME(DRM 的标准接口)。
  5. 看播放器兼容。某浏览器能播另一个不能,通常是 DRM 方案不同。

如果确认是真 DRM,我的建议是放弃下载这条路,不是技术洁癖,而是:硬件级密钥拿不到、即使拿到也涉及规避技术保护措施,法律风险远大于收益。这类内容要么走官方离线下载(很多平台自带),要么就不下。

九、合规与温馨提示

最后说几句必须说的话。

HLS 的 AES-128 加密本质上是传输层的内容保护,它不是版权标记,也不改变内容的权利归属。你能解密,不代表你有权传播。

具体建议:

  • 本文的方法适用于你自己拥有版权或已获授权的内容:自己录的会议、公司内训、自己拍的素材、已购买且允许离线保存的课程。
  • 如果密钥需要登录态才能获取,说明平台把它限定给了登录用户。用你自己账号下载你自己有权观看的内容,通常在服务条款允许的个人使用范围内;但把下载下来的内容二次分发、公开传播,基本都会越界。
  • 不要为了拿 key 去破解签名算法、撞别人的会话、或者绕过付费墙。这类操作和"解密自己的分片"性质完全不同。
  • 企业环境里跑批量下载前,确认带宽和存储的使用规定——我就见过有人把转码机跑满带宽,结果整个办公室断网被叫去谈话的。

技术本身是中性的,边界在使用方式。希望这篇帮你省下我当年花掉的那个凌晨。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部