基於HLS的多媒體防盜方案調研

Mickle_zhang發表於2020-11-11

基於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播放列表類似這樣:

image.png

處理URI的程式碼片段如下:

<link media="all" rel="stylesheet" href="https://unpkg.com/video.js@7.1.0/dist/video-js.css">
<script src="https://unpkg.com/video.js@7.1.0/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

 

相關文章