V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
newbie111
V2EX  ›  React

100u 有偿请前端老哥实现解密播放 m3u8 文件

  •  
  •   newbie111 · 6 天前 · 4976 次点击

    1.通过以下命令对影片的明文 m3u8 和 ts 文件进行加密( aes-ecb ):

    openssl enc -aes-128-ecb -in decrypt/index.m3u8 -out encrypt/index.m3u8 -K $(echo -n "你自己任意指定的 16 位加密 key" | xxd -p)
    
    openssl enc -aes-128-ecb -in decrypt/001.ts -out encrypt/001.ts -K $(echo -n "你自己任意指定的 16 位加密 key" | xxd -p)
    

    2.前端页面请求到 m3u8 播放链接时解密文件并播放。

    这里我上传了一个不到 10 秒的未加密素材供测试,当然你也可以自己找个 mp4 文件切片成 m3u8 然后本地测试解密播放,先提前谢过各位前端老哥了,一旦成功解密播放,立即支付 100u 表达谢意。我的理解是 hls.js 或 crypto.js 之类的库来解密,但我不是专业前端所以改起来有困难。。。 https://drive.google.com/drive/folders/1QPD_E6C34tND0hICVrWIemxjRL-RLnad?usp=sharing

    第 1 条附言  ·  6 天前
    当然,上面列的步骤是方便兄弟们理解加解密过程,我这边也有现成的已经加密好的文件可直接用于本地解密播放测试,愿意试试的老哥可以联系我,小飞机:@niupi66666 ,VX:aW1uZXdiaWU2NjY=( base64 )
    57 条回复
    yuzo555
        1
    yuzo555  
       6 天前
    正确的做法是转码时让 ffmpeg 自动处理,而不是转码后自己去处理。以下方法来自 GPT-4o:

    要使用 FFmpeg 将 MP4 格式的视频转码为 HLS 流并使用 HLS 标准加密( AES-128 ),你可以使用以下命令:

    ffmpeg -i input.mp4 \
    -c:v libx264 -c:a aac \
    -hls_time 10 \
    -hls_key_info_file key_info_file.txt \
    -hls_playlist_type vod \
    output.m3u8

    在使用这个命令之前,你需要准备一个 key_info_file.txt 文件,其中包含加密密钥信息。该文件的格式如下:

    key_uri
    key_file_path
    key_iv

    key_uri:在播放列表中引用密钥的 URI (通常是密钥的 URL )。
    key_file_path:包含实际密钥的文件路径。
    key_iv:初始化向量( IV ),如果不指定,FFmpeg 会自动生成。
    例如:

    https://example.com/keys/key.bin
    ./key.bin
    0123456789abcdef0123456789abcdef

    你还需要生成一个 16 字节的密钥,并将其保存到 key.bin 文件中。可以使用以下命令生成随机密钥:

    openssl rand 16 > key.bin
    请确保 key_info_file.txt 和 key.bin 文件的路径正确,并根据你的需求调整 FFmpeg 命令中的参数。
    newbie111
        2
    newbie111  
    OP
       6 天前
    @yuzo555 你好我知道这种官方标准加密方式的,直接 ffmpeg 就行,前端不需要任何处理,弊端就是任何人能拿到文件链接的情况下都能播放,不能够满足我这边的需求。
    newbie111
        3
    newbie111  
    OP
       6 天前
    ffmpeg 官方加密的方式,即使有防盗链,也可以通过批量下载的方式拿到可播放的文件。
    dullwit
        4
    dullwit  
       6 天前
    DRM ?
    LuckyLauncher
        5
    LuckyLauncher  
       6 天前
    这个是自己加密前端实现解密?那不就相当于没加密?
    newbie111
        6
    newbie111  
    OP
       6 天前
    @LuckyLauncher 不能这么说吧,玩 next.js 一把梭的时候各种数据库相关的配置不也是存放在前端项目的.env 里,只要解密 key 不暴露即可。可以理解为前后端约定好了一套加解密规则,后端加密数据,前端取到后按约定好的规则进行数据解密。
    LuckyLauncher
        7
    LuckyLauncher  
       6 天前
    @newbie111 #6 你在说些啥?你把.env 放到前端让人可以访问到?你把地址放出来等一会你看看你数据库还在不在
    LuckyLauncher
        8
    LuckyLauncher  
       6 天前
    @newbie111 #6
    “只要解密 key 不暴露即可”
    “前端取到后按约定好的规则进行数据解密”
    你就说你的前端解密需不需要 key
    dzdh
        9
    dzdh  
       6 天前
    通通加密后。你 m3u8 的解密密钥怎么下发。写死到网页里?通过接口获取?

    还是仅仅防一下小白而已?
    dzdh
        10
    dzdh  
       6 天前
    感觉还是服务端做鉴权靠谱
    helone
        11
    helone  
       6 天前
    100u 怕是有点少
    wbrobot
        12
    wbrobot  
       6 天前
    你这种就是请人定做一个 hls.js 嘛,获取到视频流之前前端解密一下,再拼成新的视频交给播放器播放。
    你直接说定制 hls.js 就行了,什么算法都能实现,甚至不用全文件加密,只需要很短的 aes 随机替换 ts 中间的几个字节,盗播的人都播不了。
    registerrr
        13
    registerrr  
       6 天前
    没盯上你的,用 ffmpeg 自带的加密也足够了。真盯上你的,只要你的前端有解密逻辑,有什么破不了的,都能破。防防小白而已
    paradox8599
        14
    paradox8599  
       6 天前 via Android
    @newbie111 不是吧,nextjs 的 env 只有 `NEXT_PUBLIC_` 开头的才会 1 暴露给前端呀
    yb2313
        15
    yb2313  
       6 天前
    是我想的那种网站吗, 让我看看
    qq78660651
        16
    qq78660651  
       6 天前
    你想要实现的功能 和 alist 的 crypt 的文件加密功能相似呀,可以参考他的代码。
    下载学习资料,加密后上传云盘,云盘存储的是加密后的文件,然后云盘播放时,实时解码观看,等于小带宽换大带宽。
    相当于客户端负责加密和解密,文件在云端是加密存储的。
    purringpal
        17
    purringpal  
       6 天前 via iPhone
    如果是特定用户才能看,你就把密钥分发给他们,至于前端加解密,聊胜于无吧
    lyxxxh2
        18
    lyxxxh2  
       6 天前
    @newbie111

    "不也是存放在前端项目的.env 里,只要解密 key 不暴露即可"
    `next.js`我没用过,但听过,`next.js` 严格来说是后端。

    跟 laravel 差不多, `.env`客户不知道。
    如果我在模板引擎:
    ```
    <script>
    key = {{ env('key') }}
    </script>
    ```
    客户浏览器可以看得到吧。
    ***
    或许你说我不定义,那 js 怎么拿 key 解密?
    除非你有个登录,让用户绑定 key 。
    SingeeKing
        19
    SingeeKing  
       6 天前 via iPhone
    这种我做过,基于 hls.js 改的,但是 100u 确实少了点
    wen20
        20
    wen20  
       6 天前
    .ts 都是绝对路径的话,可以试下。
    ppddtt
        21
    ppddtt  
       6 天前
    这样做没有意义,本质上还是本地解密,客户端包含密钥信息
    dyllen
        22
    dyllen  
       6 天前   ❤️ 1
    @newbie111 你这话好多问题,前端项目的.env 文件并不在前端,那是在服务器保存的,并不会暴露。人家说的前端指浏览器端,不是你这前端项目。aes 加密,浏览器播放端要解密,必定需要密钥,别人就能拿到。
    tool2dx
        23
    tool2dx  
       6 天前
    还不如学 youtube ,直接把视频接口从 get 都改成 post 。

    个人感觉,把后端鉴权做好就够了,保证走你自己加密的 js 那一套 web 流程。
    EridanusSora
        24
    EridanusSora  
       6 天前 via Android
    视频加密的正确方法:上 DRM ,比如 widevine
    easydou
        25
    easydou  
       6 天前 via Android
    加密 m3u8 的话,也可以用国内的一些厂商产品,比如保利威,阿里云等。如果自己写的话,最好打包成 wasm ,这样破解难度高,解密速度也比直接 js 解密要快
    fly9i
        26
    fly9i  
       6 天前
    一般常规做法是在 m3u8 不加密,ts 文件加密,每个 ts 文件密钥可以不一样,加密一般是用 aes-128-cbc 。
    m3u8 中配置一条
    #EXT-X-KEY:METHOD=AES-128,URI="https://example.com/path/to/key",IV=0xabcdef0123456789abcdef0123456789

    类似这样的数据,hls.js 每次请求 ts 前会先先请求密钥,ts 解密也是库里自动了。
    thinkingbullet
        27
    thinkingbullet  
       6 天前
    @yuzo555 ffmpeg -i video.mp4 -codec:v libx264 -codec:a aac -strict -2 -f hls -hls_key_info_file key_info.txt -hls_segment_type mpegts -hls_encryption_algorithm AES-128 -hls_key_url http://example.com/path/to/key playlist.m3u8
    gpt3.5 的答案是这个,不知道谁真谁假
    abusizhishen
        28
    abusizhishen  
       6 天前 via Android
    用 ffmpeg 加密之后,得到 m3u8 文件,把其中的解密 key 加密,前端播放前解密,这样对方拿到 m3u8 也无法播放,不过问题是解密是在前端,对方研究代码也能找到破解的方法,代价是花费更多的时间
    puzzle9
        29
    puzzle9  
       6 天前
    m3u8
    要不 你自己重新实现下解密流程 像那种视频网站一样
    增加下破解难度而已
    joewang1988
        30
    joewang1988  
       6 天前
    做完了,怎么交付?
    joewang1988
        31
    joewang1988  
       6 天前
    收款账号信息

    Address :TD8j8Z76JP4t9PZPdtspZVbgwnFt2FDAUf
    Token:USDT
    Network:Tron


    原理

    使用 hlsjs 的 custom loader 在请求到 m3u8 和 ts 文件时,分别进行相对应的解密操作。

    以下是完整代码




    <html>

    <head>
    <title>Hls.js demo - basic usage</title>
    </head>

    <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.15/hls.min.js"></script>
    <script src="./crypto-js.min.js"></script>

    <center>
    <h1>Hls.js demo - basic usage</h1>
    <video height="600" id="video" controls></video>
    </center>

    <script>





    async function process(playlist) {

    const key = '1111111111111111';

    const keyBytes = CryptoJS.enc.Utf8.parse(key);

    console.log(playlist);

    const byteArray = new Uint8Array(playlist);
    const wordArray = CryptoJS.lib.WordArray.create(byteArray);


    // Decrypt the data
    const decrypted = CryptoJS.AES.decrypt(
    { ciphertext: wordArray },
    keyBytes,
    { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
    );

    console.log(decrypted)
    // Convert decrypted data to text
    const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
    console.log(decryptedText);
    return decryptedText;
    }



    // 转换 decrypted 为 ArrayBuffer
    function wordArrayToArrayBuffer(wordArray) {
    // 计算需要的长度
    const length = wordArray.sigBytes;
    const words = wordArray.words;
    const bytes = new Uint8Array(length);

    // 将 wordArray 的每个 word 转换为 byte
    for (let i = 0; i < length; i++) {
    bytes[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xFF;
    }

    return bytes.buffer;
    }

    async function processData(data) {
    console.log(data);

    const key = '1111111111111111';

    const keyBytes = CryptoJS.enc.Utf8.parse(key);

    console.log(data);

    const byteArray = new Uint8Array(data);
    const wordArray = CryptoJS.lib.WordArray.create(byteArray);


    // Decrypt the data
    const decrypted = CryptoJS.AES.decrypt(
    { ciphertext: wordArray },
    keyBytes,
    { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
    );

    console.log(decrypted)

    const arrayBuffer = wordArrayToArrayBuffer(decrypted);

    return arrayBuffer;
    }

    class CustomLoader extends Hls.DefaultConfig.loader {
    constructor(config) {
    super(config);
    var load = this.load.bind(this);
    //var xhrSetup = this.xhrSetup.bind(this);



    this.load = function (context, config, callbacks) {
    context.responseType = 'arraybuffer';


    if (context.type == 'manifest') {
    var onSuccess = callbacks.onSuccess;
    callbacks.onSuccess = function (response, stats, context, networkDetails) {
    console.log(networkDetails)
    process(response.data)
    .then(data => {
    response.data = data;
    onSuccess(response, stats, context);
    })
    .catch(err => {
    console.error(err);
    })

    };
    } else {
    const onSuccess = callbacks.onSuccess;
    callbacks.onSuccess = function (response, stats, context) {


    processData(response.data)
    .then(data => {
    response.data = data;
    onSuccess(response, stats, context);
    })
    .catch(err => {
    console.error(err);
    })


    };
    }
    load(context, config, callbacks);
    };
    }
    }

    // Create the Hls instance with the custom fetch loader
    const myHls = new Hls({
    //debug: true,
    loader: CustomLoader,
    xhrSetup: function (xhr, url) {


    xhr.open('GET', url, true);
    xhr.setRequestHeader('id', 1)
    xhr.setRequestHeader('token', 456)
    xhr.responseType = '111'; // do send cookies
    console.log("XXXXXXX")
    }

    });

    const video = document.getElementById('video');
    myHls.loadSource('./index.e.m3u8');
    myHls.attachMedia(video);
    myHls.on(Hls.Events.MANIFEST_PARSED, function () {
    video.play();
    });

    myHls.on(Hls.Events.ERROR, function (event, data) {
    if (data.fatal) {
    console.error('HLS.js Error:', data);
    }
    });

    </script>
    </body>

    </html>
    rekulas
        32
    rekulas  
       6 天前
    还有个骚操作办法,先视频预处理对 rgb 通道进行异或,前端播放时根据 key 还原,速度非常快对性能影响很小,虽然没加密但是对方没 key 播放出来就是花屏
    Oldletter
        33
    Oldletter  
       6 天前
    BaiLinfeng
        34
    BaiLinfeng  
       6 天前
    这是想做啥黑科技
    Ipsum
        35
    Ipsum  
       6 天前
    @BaiLinfeng 我感觉是想放毛片在国内 cdn 上躲避审查。
    BaiLinfeng
        36
    BaiLinfeng  
       6 天前
    @Ipsum 还可以这样玩的吗?有 demo 吗,我看看,那么骚的操作
    azhangbing
        37
    azhangbing  
       6 天前
    31 楼正确 你只需要重写 class CustomFragmentLoader extends Hls.DefaultConfig.loader 的 load 方法就行了

    load(context, config, callbacks) {
    const onSuccessOriginal = callbacks.onSuccess;

    callbacks.onSuccess = (response, stats, context) => {
    if (context.frag && context.frag.url && context.frag.url.includes('.ts')) {
    const encryptedData = new Uint8Array(response.data);
    const filenameBase = this.extractFilename(context.frag.url);



    const decryptedData = xorDecrypt(encryptedData, this.keyBytes);
    response.data = decryptedData.buffer;


    }
    onSuccessOriginal(response, stats, context);
    };

    super.load(context, config, callbacks);
    }
    Yaavi
        38
    Yaavi  
       6 天前
    上午写完的,但联系不上,还能拿到 100u 么 [狗头]
    Demo:[https://decipher-m3u8.yaavi.me/]( https://decipher-m3u8.yaavi.me/)
    aycclm
        39
    aycclm  
       6 天前
    你们真的是卷死了 [手动狗头]
    100u 还真给做了
    jjl19960910
        40
    jjl19960910  
       6 天前 via Android
    @Ipsum #35 hh 笑死
    newbie111
        41
    newbie111  
    OP
       6 天前
    抱歉各位,我这里有 12 小时时差,现在是早上 6 点 46 ,刚起床,已经有好几个人加我,我在确认。
    k9982874
        42
    k9982874  
       6 天前 via Android
    问问 ai 用 js 怎么 sha256 加密解密文件不就完事了。。
    Yaavi
        43
    Yaavi  
       6 天前
    @newbie111 已收到 100u ,感谢老哥~
    Yaavi
        44
    Yaavi  
       6 天前   ❤️ 1
    newbie111
        45
    newbie111  
    OP
       6 天前
    抱歉 31 楼和其他加我的几位老哥,为节约大家时间,我优先加了在申请备注中提示“已按方案完成”的 Yaavi 老哥,验证后完美运行。31 楼老哥代码我刚跑了下,对比 Yaavi 老哥提供的代码,同样的 url ,会提示跨域,就没继续往下看了,感谢大家的帮忙,已转 yaavi 。
    newbie111
        46
    newbie111  
    OP
       6 天前
    感谢 v 站,藏龙卧虎果然不虚。
    joewang1988
        47
    joewang1988  
       6 天前
    @newbie111

    会提示跨域 --- 你把 m3u8 文件和 ts 文件放在同一个 domain 下就不会有跨域问题了。

    我贴代码也是做个社会性测试。

    呵呵 果然是不公平啊
    joewang1988
        48
    joewang1988  
       6 天前
    @azhangbing 感谢
    joewang1988
        49
    joewang1988  
       6 天前
    @Yaavi 看你代码写得不错。方便留个 wx 不 有前端的活。200 刀一个。
    pxiphx891
        50
    pxiphx891  
       6 天前
    我理解,解密密钥应该在这个 js 文件里,谁能教教我,怎么找到解密密钥?我打断点找了半天也没找着

    https://decipher-m3u8.yaavi.me/assets/index-DCANJRc2.js
    GooMS
        51
    GooMS  
       6 天前 via Android
    掩耳盗铃
    pxiphx891
        52
    pxiphx891  
       6 天前
    看了 github 源码,我知道了密钥是 1234567890ABCDEF ,谁能教教我,有什么方法能在只有混淆后代码的情况下,通过调试一步一步找到密钥?
    wuzzispacelake
        53
    wuzzispacelake  
       6 天前
    @Yaavi 想提个问,简单看了一下代码,看起来 decryptTSAES 是拿去的 fetch 下来的全量 ArrayBuffer ,但是我个人理解对于 TS 这样的视频文件不是流式处理不会出问题吗
    wuzzispacelake
        54
    wuzzispacelake  
       6 天前
    没事了,突然意识到是 m3u8 ,那么本身就是分片
    Yaavi
        55
    Yaavi  
       6 天前
    @joewang1988 没问题 -vsme- 含两头的 -
    sunchuo
        56
    sunchuo  
       6 天前
    @Yaavi star 了。感谢。
    caola
        57
    caola  
       5 天前
    解密的密钥 key 放在 wasm 里面,多弄几个变量混淆一下密钥,还可以加入其他的参数进行校验(例如:服务器给出一个加密后的时间戳,wasm 解密这个参数后校验这个时间在一定的范围内就允许解密操作),然后再编译成二进制。一般能破解的概率小很多了。
    js 把加密的数据和参数传入 wasm 解密后的数据反回给 js 。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2402 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 15:00 · PVG 23:00 · LAX 08:00 · JFK 11:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.