Loading...
墨滴

雨夜冰之恋

2021/08/25  阅读:71  主题:红绯

m3u8收费视频防盗加密方式

1 场景

视频采用m3u8 ts切片方式,采用切片默认的aes-128加密方式不安全,key值也是16位明文的,随便下个video.js插件就可以破解(其它播放插件也可以),让后端做来源判断或者加请求标识个人感觉安全系数也不是很大。

2 m3u8视频防盗方案

  • 方式一: 后端对m3u8返回内容进行加密处理,前端请求m3u8时可以包括加SIGN验证,根据视频id加请求参数验证等,以便后端判断请求是否给你返回内容。

  • 关于方式一解密(加解密方案前后端自己定即可,以下均是修改插件源码并以标明位置函数,实测前后端以调通)

  //插件:video.js - 7.0.5(此版本默认不包含hls)
  
    //处理请求添加字段证明身份
    function _createXHR(options{
      ...
        var failureResponse = {
            bodyundefined,
            headers: {},
            statusCode0,
            method: method,
            url: uri,
            rawRequest: xhr
        };
        
        // 自定义 - 开始
        if (options.uri.indexOf('自定义地址') > -1) {
            /**
             * 
             * 加密算法得出加密值
             * 
             */

            headers = { "SIGN": 加密值, 'Content-Type''application/x-www-form-urlencoded' };
            method = "POST";
        } else {
            method = xhr.method = options.method || "GET";
            headers = xhr.headers = options.headers || {};
        }
        // 自定义 - 结束

        if ("json" in options && options.json !== false) {
            isJson = true;
            headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
            if (method !== "GET" && method !== "HEAD") {
                headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
                body = JSON.stringify(options.json === true ? body : options.json);
            }
        }
      ...
    }
  
  
  //插件:videojs-contrib-hls.js - 5.9.0
  
  var handleKeyResponse = function handleKeyResponse(segment, finishProcessingFn{
    ...
      if (errorObj) {
                            return finishProcessingFn(errorObj, segment);
                        }
                        
                        // 解密二次加密后的key - 开始
                        if(请求key返回的正确code值){
                            if (response.key !== "") {
                                /**
                                 * 
                                 * 咔咔咔写自己的解密
                                 * 
                                 */


                                var view = new DataView(解密得到的值);
                                segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
                                return finishProcessingFn(null, segment);
                            }
                        }else{
                            // 用于初始化判断key长度
                            if (response.byteLength !== 16) {
                                return finishProcessingFn({
                                    status: request.status,
                                    message'Invalid HLS key at URL: ' + request.uri,
                                    code: REQUEST_ERRORS.FAILURE,
                                    xhr: request
                                }, segment);
                            }
                        }
                        // 解密二次加密后的key - 结束

                        // 将原有的注释
                        // var view = new DataView(response);
                        // segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
                        // return finishProcessingFn(null, segment);
    ...
  }
  
  haveMetadata = function (xhr, url{
    ...
      // any in-flight request is now finished
                        request = null;
                        loader.state = 'HAVE_METADATA';
                        
                        // 解析m3u8正确格式 - 开始
                        if (typeof (req.responseText) == "string") {
                            /**
                             * 解密m3u8得到的值
                             */

                            var _responseText = 解密后的内容;
                        } else {
                            var _responseText = req.responseText;
                        }
                        // 解析m3u8正确格式 - 结束

                        parser = new _m3u8Parser2['default'].Parser();
                        parser.push(_responseText);
                        parser.end();
    ...
  }
  
  loader.start = function ({
    ...
      if (error) {
                                loader.error = {
                                    status: req.status,
                                    message'HLS playlist request error at URL: ' + srcUrl,
                                    responseText: req.responseText,
                                    // MEDIA_ERR_NETWORK
                                    code2
                                };
                                if (loader.state === 'HAVE_NOTHING') {
                                    loader.started = false;
                                }
                                return loader.trigger('error');
                            }


                            // 解析m3u8正确格式 - 开始
                            if (typeof (req.responseText) == "string") {
                                /**
                                 * 解密m3u8得到的值
                                 */

                                var _responseText = 解密后的内容;
                            } else {
                                var _responseText = req.responseText;
                            }
                            // 解析m3u8正确格式 - 结束
                            
                            parser = new _m3u8Parser2['default'].Parser();
                            parser.push(_responseText);
                            parser.end();
    ...
  }
  
  • 方式二:如果感觉第一种麻烦可以让后端对key进行特殊二次加密(当然前提是前端知道后端是如何加密的以方便如何解密)

  • 方式二解密:

  
  // video.js 7.15.0 (这个版本自带hls功能)
  
    var handleKeyResponse = function handleKeyResponse(segment, objects, finishProcessingFn{
        return function (error, request{
            var response = request.response;
            var errorObj = handleErrors(error, request);

            if (errorObj) {
                return finishProcessingFn(errorObj, segment);
            }

            /**
             * 
             * 1、咔咔咔写自己拿到key返回内容进行解密
             * 2、最后得到的是一个16位字符串
             * 3、必须将字符串转换为ArrayBuffer
             * 注:假如某个节视频key解密后为 2y7d9iwu8rc3fa0x
             */


            //字符串转字符串ArrayBuffer
            function str2ab(s,f{
                var b = new Blob([s],{type:'text/plain'});
                var r = new FileReader();
                r.readAsArrayBuffer(b);
                r.onload = function (){if(f)f.call(null,r.result)}
            }

            // 此处是为了演示,实际应该填写每次解密后的字符串变量
            str2ab('2y7d9iwu8rc3fa0x',function(ab){
                //ab为ArrayBuffer
                var view = new DataView(ab);

                var bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);

                console.log(bytes);

                for (var i = 0; i < objects.length; i++) {
                    objects[i].bytes = bytes;
                }

                return finishProcessingFn(null, segment);
            });


            // 将原来的方法注释掉
            // var view = new DataView(new Int8Array(hexAesStr('516aa4d465a4085baddcac890cbcbf7e')).buffer);
            // var bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);

            // for (var i = 0; i < objects.length; i++) {
            //     objects[i].bytes = bytes;
            // }

            // return finishProcessingFn(null, segment);
        };
    };

雨夜冰之恋

2021/08/25  阅读:71  主题:红绯

作者介绍

雨夜冰之恋