基于HLS的多媒体防盗方案调研

基于HLS的多媒体防盗方案调研

为什么要加密视频

视频加密是为了让要保护的视频不能轻易被下载,即使下载到了也是加密后的内容,其它人解开加密后的内容需要付出非常大的代价。即便如此,也无法严格保护视频不被录制。

常见的防盗技术

  • 防盗链:只能合法的通过系统认证的用户才能访问到资源,其实就是资源访问鉴权。
  • 加密视频:通过对称加密算法加密视频内容,合法用户获取到解密视频的**,并获取到解密的视频内容,在客户端解密播放。

播放方案

根据经验播放mp3、mp4文件的时候,H5 video或audio播放器通过设置header中的content-range向流媒体服务部分获取一段媒体数据,播放和seek一样,每次获取数据字节数可能比较大,视网络状况不同,等待的时间不同,字节越大缓冲等待的时间越久。参考大型视频网站的做法,发现他们的是将视频文件切片播放的。也就是将大的视频文件分成N小段,比如按Apple推荐标准每10s切成一段。这样的优势是打开视频加载速度快,另外播放第n段的时候,播放器会下载n+1段,n+2段不会下载,大大缓解服务器和带宽的压力。

 

因此为了提高播放性能,并且防止资源被盗,我们将音视频进行切片,并生成**,用**对音视频数据进行加密。m3u8支持分片,并建立m3u8格式的索引,可用于直播或点播场景。

 

因此我们采用的ts切片,浏览器播放的流媒体传输协议是HLS。

  • HLS:Apple 推出的基于 HTTP 协议的 MP4 分片传输协议,可用于点播和直播场景。每下载一个分片都需要发生一次 HTTP 请求,所以严格来说 HLS 不能称为流媒体传输协议。

流媒体加密原理

流媒体传输协议是把音视频流拆分成连续的小块之后再传输。流媒体加密技术的关键在于,对每一分配采用对称加密算法。服务端加密,通过验证的用户才能使用播放器进行解密。解密的过程是通过m3u8播放列表提供的参数,获取到**key,通key进行解密播放。现有常见的加密技术分为对称加密和非对称加密,对称加解密计算较快,效率较高,适用于流媒体对延时有要求的场景。HLS提供的对称加密算法有AES-128等。

我们的方案

我们的目标是用户只能在我们系统观看视频,但是不能下载(盗用)视频。前文提到为了提高播放性能,我们采用了切片;为了基本的视频安全,我们利用HLS支持的AES-128对视频进行加密。这样做还是不够的,因为播放器需要通过m3u8播放列表提供的参数向服务器发起http请求获取到**,这样通过抓包或者浏览器 F12 network可以看到请求URL和响应体。也就是说能观看的用户可以拿到我们加密的key和视频切片,通过FFmpeg或相关技术应该是可以将切片合成完整视频的。因此这样做还不够安全。我们的方案是将**key进行二次加密,播放器拿到key以后需要先解密一次得到实际的加***,然后再将**给到播放器播放。这样即便拿到接口响应的key也不能拿来解密或用于一般播放器播放。

方案及如何保护**

我们的场景是在Web前端也就是浏览器播放,必须借助浏览器生态支持的技术,并且没有开发我们自己的播放器,有的厂商是有生成自己的视频格式的,需要厂商自己的播放器才能打得开,这样相比会更安全。但是我们目前只能利用hls.js或者video.js来播放HLS。video.js 使用的是videojs-contrib-hls插件,据说videojs-contrib-hls使用的hls.js,其实最终使用的还是hls.js。

video.js

video.js 提供了 XHR 拦截器,对获取key的URI进行替换,这样的好处是说,系统外用户拿到m3u8不能直接用其他播放器打开,但是合法用户还是能拦截或查看到替换后访问的URI。假如m3u8播放列表类似这样:

基于HLS的多媒体防盗方案调研

处理URI的代码片段如下:

<link media="all" rel="stylesheet" href="https://unpkg.com/[email protected]/dist/video-js.css">
<script src="https://unpkg.com/[email protected]/dist/video.js"></script>

<video-js id="player">
    <source src="//video/index.m3u8" type="application/x-mpegURL" />
</video-js>
<script>
    var player = videojs("player");
    var keyPrefix = "key://";
    var urlTpl = "https://domain.com/path/{key}";
    // player.ready
    player.on("loadstart", function (e) {
        player.tech().hls.xhr.beforeRequest = function(options) {
            // required for detecting only the key requests
            if (!options.uri.startsWith(keyPrefix)) { return; }
            options.headers = options.headers || {};
            optopns.headers["Custom-Header"] = "value";
            options.uri = urlTpl.replace("{key}", options.uri.substring(keyPrefix.length));
        };
    });
</script>

显然这个方法不能起到理想的防盗效果。

hls.js

HLS只允许将**与播放器可以检索的内容链接起来,但它没有指定如何保护**的特定方法。如果需要对内容和**执行访问控制,我们可以像处理其他想保护的资源一样,因为播放器通过HTTP链接访问它。例如,可以使用某种登录令牌来保护**,该令牌将在请求头中传递。不幸的是,hls.js还不支持为请求key配置客户化请求头。这里通常的做法是在配置中传递一个loader,基于现有的loader实现自己的加载器,然后将自定义的HTTP头放入其中,或者对返回的**数据做处理(比如解密)。另外hls.js自定义分片请求的URL,可以参考这篇文章

 

我们在hls.js的配置项李找到了对loader的介绍

(default: standard XMLHttpRequest-based URL loader)

Override standard URL loader by a custom one. Use composition and wrap internal implementation which could be exported by Hls.DefaultConfig.loader. Could be useful for P2P or stubbing (testing).

Use this, if you want to overwrite both the fragment and the playlist loader.

Note: If fLoader or pLoader are used, they overwrite loader!

我们试着通过改写这个loader方法来满足我们的业务场景:

var configure = {
        //MEU8加载器
        loader : function() {
            const loader = new Hls.DefaultConfig.loader(configure);
            this.abort = () => loader.abort();
            this.destroy = () => loader.destroy();

            this.load = (context, config, callbacks) => {
                const { type } = context;
                const onSuccess = callbacks.onSuccess;
                callbacks.onSuccess = (response, stats, context1, networkDetails) => {
                    if (type !== "manifest" && context1.url.endsWith(".key")) {
                        console.log(context1.url)
                        // 这里对返回的**数据进行处理
                        // response.data = dealWithKeyData(response.data));
                    }
                    onSuccess(response, stats, context, networkDetails);
                };
                loader.load(context, config, callbacks);
            };
        }
    };

    if(Hls.isSupported()) {
      var hls = new Hls(configure);
      hls.loadSource(videoSrcInHls);
      hls.attachMedia(video);
      hls.on(Hls.Events.MANIFEST_PARSED,function() {
        video.play();
      });
    } else {
      addSourceToVideo(video, videoSrcInMp4, 'video/mp4');
      video.play();
    }

**的其他保护

m3u8的播放列表以上面截图为例,请注意#EXT-X-KEY这个参数提供了加***的URI。播放器将从该位置检索**以解密媒体段。为了防止**被窃听,应该通过HTTPS对其进行服务。可能还需要实现某些身份验证机制,以限制谁可以访问**。在这种情况下,所有段都使用相同的**加密。如果暴露特定**,则定期更改加***以最小化影响可能是有益的,这被称为**旋转。针对动态更换**这点,需要后端能力支撑。设想生成**以后需要对切片进行加密,更换一次**需要等待较长时间,不一定能满足播放实时性要求。

参考文章

https://onetdev.medium.com/custom-key-acquisition-for-encrypted-hls-in-videojs-59e495f78e52

http://hlsbook.net/how-to-encrypt-hls-video-with-ffmpeg/

https://github.com/videojs/videojs-contrib-hls/issues/1337

https://doc.xuwenliang.com/docs/video_audio/3422

https://imweb.io/topic/59819d7bf8b6c96352a593ff

https://github.com/video-dev/hls.js/issues/1437

https://zhuanlan.zhihu.com/p/102125509