每天 Shaarli

一天内的所有链接,汇聚在一个页面上。

September 18, 2024

Note: HLS 加密视频分片加密(m3u8+ts+key)

HLS 加密 (HTTP Live Streaming + AES-128) 确实是非常实用的,尤其在需要保护视频内容的 Web 媒体流播放中,它是一个常见且强大的解决方案。以下是对 HLS 加密工作原理、实施步骤以及一些实用示例的详细介绍。

什么是 HLS?
HLS(HTTP Live Streaming) 是 Apple 开发的一种基于 HTTP 的流媒体协议。它通过将视频文件切割成许多小的分段文件(segment),并通过 .m3u8 播放列表 文件来管理分段文件。HLS 可以动态调整视频的比特率,根据网络状况或设备能力提供不同质量的视频流。

HLS 加密(AES-128 加密)
HLS 协议支持 AES-128 对称加密,这种加密方式可以为每个 HLS 的分段文件提供安全保护。加密后的视频分段文件只有在具备相应的解密密钥时才能播放。

AES-128 是一种常见的对称加密算法,对称加密意味着同一个密钥用于加密和解密视频文件。使用 AES-128 加密的 HLS 方案通常用于视频保护,防止未经授权的用户下载或播放视频。

HLS 加密的工作原理
HLS 加密主要有以下几个步骤:

视频切片:FFmpeg 将视频文件切成多个分段(.ts 文件)。
生成密钥:每个视频分段文件都会使用 AES-128 加密算法进行加密,密钥保存在一个独立的文件中。
生成播放列表(.m3u8):播放列表文件中会包含每个分段的地址,以及用来解密分段的密钥 URI 地址。
视频播放:播放器通过访问 .m3u8 文件,获取分段和密钥 URI,解密分段并播放视频。
实现 HLS 加密的步骤
我们将使用 FFmpeg 来生成加密的 HLS 视频流。需要准备一个视频文件和 OpenSSL 用于生成密钥。

  1. 生成 AES-128 密钥
    使用 OpenSSL 来生成 128 位的密钥文件。执行以下命令生成一个 16 字节(128 位)的随机密钥:
openssl rand 16 > enc.key

enc.key 是我们生成的加密密钥文件,实际用于加密 HLS 的分段。

  1. 创建密钥信息文件
    我们需要创建一个密钥信息文件(key_info_file.txt),它包含 HLS 播放列表中如何引用和访问密钥的相关信息。

密钥信息文件格式如下:

key_uri
/path/to/enc.key
IV

key_uri:播放器访问密钥的 URL 地址。可以是服务器上的文件路径,例如 https://example.com/encryption_key
/path/to/enc.key:密钥的本地文件路径。这个路径用于 FFmpeg 在加密时找到密钥文件。
IV(初始化向量):这是一个可选项,如果不指定,FFmpeg 会自动生成。它用于增强加密的安全性。
示例 key_info_file.txt:

https://example.com/encryption_key
/path/to/enc.key
00000000000000000000000000000000
  1. 使用 FFmpeg 进行 HLS 加密
    现在,我们将使用 FFmpeg 来生成加密的 HLS 视频。使用以下命令:
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -strict -2 -hls_time 10 -hls_key_info_file key_info_file.txt -hls_playlist_type vod -hls_segment_filename 'segment_%03d.ts' output.m3u8

参数说明:

-i input.mp4:输入文件。
-c
libx264:使用 H.264 编码器。
-c
aac:使用 AAC 音频编码器。
-strict -2:确保兼容 AAC 编码。
-hls_time 10:每个 HLS 分段的时长为 10 秒。
-hls_key_info_file key_info_file.txt:使用我们之前创建的密钥信息文件进行加密。
-hls_playlist_type vod:生成点播类型的播放列表。
-hls_segmentfilename 'segment%03d.ts':指定输出的 HLS 分段文件名模式,生成如 segment_001.ts, segment_002.ts 的分段文件。
output.m3u8:输出的 HLS 播放列表文件。

  1. HLS 文件结构
    生成的文件结构通常如下:

output.m3u8:HLS 播放列表文件。
segment_001.ts, segment_002.ts, ...:加密的视频分段文件。
enc.key:加密用的密钥文件(服务器需要安全存储)。

  1. 播放加密的 HLS
    播放器(如 VLC、Safari 或一些 HLS 支持的 HTML5 播放器)在播放时会自动读取 .m3u8 文件中的信息,访问指定的 key_uri 获取密钥并解密视频分段。

如果密钥服务器要求用户身份验证(如登录验证),可以进一步保护密钥文件。

  1. 解密 HLS 流
    播放器使用 .m3u8 文件中的 URI 获取密钥并解密 HLS 流。如果你想手动解密某个 .ts 视频分段,可以使用 OpenSSL 解密:
openssl aes-128-cbc -d -in encrypted_segment.ts -out decrypted_segment.ts -K <hex_key> -iv <hex_iv>

-K <hex_key>:这是你的 AES 密钥(以十六进制表示)。
-iv <hex_iv>:初始化向量(IV),如果你没有指定,FFmpeg 会自动生成。
总结:
通过 HLS 和 AES-128 加密,你可以有效保护视频内容,防止未经授权的下载和播放。关键点在于:

生成密钥并确保密钥文件的安全。
创建密钥信息文件以供 FFmpeg 使用。
加密视频分段并生成 HLS 播放列表。
密钥分发与访问控制:通过安全的密钥 URI 和服务器验证,确保只有授权用户能够解密视频。
这个流程比较适合 Web 环境下的流媒体播放,广泛应用于需要保护内容的场景,比如在线教育、付费视频、流媒体平台等。

如果密钥文件 enc.key 被暴露并且可以直接访问,那么任何人都能下载这个密钥并用它解密加密的 HLS 分段文件。这确实会对安全性构成威胁。因此,密钥的保护至关重要。为了确保加密方案的安全性,有几个策略可以用来保护密钥,防止未经授权的访问。

  1. 密钥存储在安全的服务器上
    密钥文件不应该直接与视频文件一起存储在同一目录下或同一公开服务器上。应该将密钥文件存储在受保护的服务器或需要身份验证的服务器上,只有经过身份验证的用户才能访问该密钥。

密钥 URI 应指向一个受保护的地址。只有当客户端(例如浏览器或播放器)通过身份验证后,才能获取该密钥并解密视频。
具体实施方法:
HTTPS:确保密钥传输过程使用 HTTPS 加密协议,这样在传输过程中,密钥不会被中途截获。
身份验证:设置服务端的身份验证机制,只有通过用户登录验证或访问令牌验证的用户才能访问密钥文件。
示例:
在 key_info_file.txt 中的密钥 URI 可以是这样:

https://secure.example.com/get_key?id=video123
/path/to/enc.key
00000000000000000000000000000000

而不是直接将 enc.key 文件暴露在一个公开的 URL 上。

当用户请求密钥时,服务器可以通过用户认证、令牌验证或其他机制来确保只有合法用户能够获取密钥。

  1. 使用动态密钥和短时有效的密钥
    为了进一步增加安全性,可以使用动态密钥生成,并限制密钥的有效时间。这样每次视频播放时,密钥都会不同,减少了密钥被重复使用的风险。

如何实现:
短时有效密钥:每次视频播放或特定时间段内生成一个新的密钥。密钥的有效时间可以很短,例如 5-10 分钟,过了这个时间后密钥失效。下次播放时用户必须重新获取密钥。
动态密钥生成:通过服务器动态生成密钥,避免长时间存储在某个固定位置。用户每次播放视频时,都需要通过身份验证获取新的密钥。

  1. 结合身份验证系统
    你可以将 HLS 密钥的访问控制与用户身份验证系统(如 OAuth 或 JWT 令牌)结合起来。这种方式确保只有经过授权的用户才能访问密钥:

OAuth 或 JWT 验证:在用户登录后,服务器为用户生成一个短时效的令牌。只有携带这个令牌的请求才能访问密钥文件。
服务器端验证:通过用户的身份验证系统来验证密钥的访问权限,确保只有付费用户、注册用户或特定权限的用户可以获取密钥。
示例:
用户请求播放视频时,播放器首先访问一个授权 API,获取视频播放的授权 token 和密钥 URI。例如:

https://secure.example.com/get_key?id=video123&token=abcdef123456

服务器根据用户的身份或权限生成一个带有时效限制的密钥,并将其提供给播放器。播放器可以通过这个密钥来解密视频。

  1. 防止密钥缓存
    为了防止密钥被恶意缓存或窃取,你还可以通过以下方式进一步加强安全:

禁止 HTTP 缓存:确保在提供密钥时,服务器不允许客户端缓存密钥。可以通过在 HTTP 响应头中添加 Cache-Control: no-store 来确保密钥不会被缓存。
限制密钥使用次数:服务器可以为每个密钥设定使用次数限制,确保每个密钥只能使用一次或几次,之后即失效。

  1. 结合 DRM 解决方案
    如果需要更高级别的安全性,许多专业的视频平台会使用 DRM(数字版权管理) 技术,如 Google Widevine 或 Microsoft PlayReady。这些 DRM 技术可以在客户端和服务器之间建立更强的加密和身份验证机制,防止密钥被泄露或滥用。

DRM 的特点是:

加密过程更加复杂,使用非对称密钥加密,客户端无法直接访问解密密钥。
需要支持 DRM 的播放器来播放内容。
对应的 DRM 密钥管理服务器(KMS)可以动态管理密钥和权限。

  1. 限制密钥的访问条件
    可以通过一些额外的机制来限制密钥的访问,比如:

IP 地址限制:只允许特定 IP 地址访问密钥,比如仅允许来自某些 CDN 或服务器的请求访问密钥。
时效限制:密钥只能在特定时间内有效。
HTTPS 保护:密钥文件的传输必须使用 HTTPS 协议,确保传输过程的安全性。
总结
尽管 AES-128 HLS 加密本身很安全,但关键在于如何保护密钥文件。具体策略包括:

密钥存储在安全的服务器:通过 HTTPS 和身份验证保护密钥。
使用动态密钥和短时效密钥:减少密钥暴露的机会。
结合身份验证系统:确保只有经过验证的用户能够访问密钥。
防止密钥缓存和滥用:确保密钥不能被重复使用或缓存。
结合 DRM 解决方案:如果需要更高的安全性,可以使用专业的 DRM 保护方案。
通过这些策略,你可以有效保护视频的加密密钥,防止未经授权的用户访问和解密视频。

Note: 使用 FFmpeg 转码压缩成 VP9 格式的 WebM 视频 性价比最高的配置
  1. 2K (2048x1080)

    ffmpeg -i input.mp4 -vf scale=2048:-1 -c:v libvpx-vp9 -b:v 4M -crf 24 -row-mt 1 -threads 4 -pass 1 -an -f null /dev/null && ffmpeg -i input.mp4 -vf scale=2048:-1 -c:v libvpx-vp9 -b:v 4M -crf 24 -row-mt 1 -threads 4 -pass 2 -c:a libopus -b:a 128k output_2k.webm
  2. 1920x1080 (Full HD)

    ffmpeg -i input.mp4 -vf scale=1920:-1 -c:v libvpx-vp9 -b:v 3M -crf 25 -row-mt 1 -threads 4 -pass 1 -an -f null /dev/null && ffmpeg -i input.mp4 -vf scale=1920:-1 -c:v libvpx-vp9 -b:v 3M -crf 25 -row-mt 1 -threads 4 -pass 2 -c:a libopus -b:a 128k output_1080p.webm
  3. 1280x720 (HD)

    ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libvpx-vp9 -b:v 2M -crf 26 -row-mt 1 -threads 4 -pass 1 -an -f null /dev/null && ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libvpx-vp9 -b:v 2M -crf 26 -row-mt 1 -threads 4 -pass 2 -c:a libopus -b:a 96k output_720p.webm
  4. 800x600 (SVGA)

    ffmpeg -i input.mp4 -vf scale=800:-1 -c:v libvpx-vp9 -b:v 1M -crf 28 -row-mt 1 -threads 4 -pass 1 -an -f null /dev/null && ffmpeg -i input.mp4 -vf scale=800:-1  -c:v libvpx-vp9 -b:v 1M -crf 28 -row-mt 1 -threads 4 -pass 2 -c:a libopus -b:a 64k output_600p.webm
  5. 480x320 (低分辨率)

    ffmpeg -i input.mp4 -vf scale=480:-1 -c:v libvpx-vp9 -b:v 600k -crf 30 -row-mt 1 -threads 4 -pass 1 -an -f null /dev/null && ffmpeg -i input.mp4 -vf scale=480:-1 -c:v libvpx-vp9 -b:v 600k -crf 30 -row-mt 1 -threads 4 -pass 2 -c:a libopus -b:a 48k output_320p.webm
  6. WebM 格式(使用 VP9)最快最小配置命令:

    ffmpeg.exe -i input.mkv -vf "unsharp=3:3:0.6:3:3:0.6,eq=contrast=0.98,hue=h=0,hqdn3d,scale=w=480:h=-1" -threads 6 -c:v libvpx-vp9 -speed 4 -crf 45 -b:v 50k -r 18 -pass 1 -an -map_metadata -1 -fflags +genpts -y -f null NUL && ffmpeg.exe -i input.mkv -vf "unsharp=3:3:0.6:3:3:0.6,eq=contrast=0.98,hue=h=0,hqdn3d,scale=w=480:h=-1" -threads 6 -c:v libvpx-vp9 -speed 4 -crf 45 -b:v 50k -r 18 -pass 2 -c:a libopus -b:a 32k -map_metadata -1 -fflags +genpts -y -loglevel info output_320p_min_fast.webm
Note: nodejs+koa2怎么允许CORS?

安装koa2-cors:

npm install koa2-cors
const Koa = require('koa');
const cors = require('koa2-cors');
const app = new Koa();

app.use(cors({
    origin: (ctx) => {
        // 限制特定域名或根据条件动态设置
        if (ctx.request.header.origin === 'http://allowed-origin.com') {
            return 'http://allowed-origin.com';
        }
        return '*'; // 允许所有域名
    },
    allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],  // 限制允许的请求方法
    allowHeaders: ['Content-Type', 'Authorization', 'Accept'], // 指定允许的请求头
    credentials: true,  // 是否允许发送cookie
}));

// 其他中间件
app.use(async ctx => {
    ctx.body = 'CORS enabled!';
});

app.listen(3000);

高级注意事项:

安全性考虑:

生产环境中尽量避免使用'*'作为origin值,特别是如果credentials设置为true时,这会导致安全隐患,确保origin只允许可信域名。
动态控制:

根据不同请求动态返回CORS设置,特别是针对不同的API,可以使用自定义逻辑来动态决定是否允许跨域请求。
复杂请求:

某些带有自定义请求头或非简单方法(如PUT、DELETE)的请求是“预检请求”,确保在返回响应头时正确处理Access-Control-Allow-Headers和Access-Control-Allow-Methods。
这样就可以在Koa2中灵活地控制和管理CORS设置。

使用'*'作为origin值

const Koa = require('koa');
const cors = require('koa2-cors');
const app = new Koa();

app.use(cors({
    origin: '*',  // 允许所有域名
    allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],  // 允许的请求方法
    allowHeaders: ['Content-Type', 'Authorization', 'Accept'],  // 允许的请求头
    credentials: false,  // 开发环境下不需要cookie,设置为false
}));

app.use(async ctx => {
    ctx.body = 'CORS enabled with * origin!';
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});