18bili
V2EX  ›  React

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

By 18bili at 2024 年 9 月 11 日 · 9305 次点击

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 条附言  ·  2024 年 9 月 11 日
当然,上面列的步骤是方便兄弟们理解加解密过程,我这边也有现成的已经加密好的文件可直接用于本地解密播放测试,愿意试试的老哥可以联系我,小飞机:@niupi66666 ,VX:aW1uZXdiaWU2NjY=( base64 )
58 条回复  •  2025-11-15 16:47:19 +08:00
yuzo555
   1
yuzo555  
   2024 年 9 月 11 日
正确的做法是转码时让 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 命令中的参数。
18bili
   2
18bili  
OP
   2024 年 9 月 11 日
@yuzo555 你好我知道这种官方标准加密方式的,直接 ffmpeg 就行,前端不需要任何处理,弊端就是任何人能拿到文件链接的情况下都能播放,不能够满足我这边的需求。
18bili
   3
18bili  
OP
   2024 年 9 月 11 日
ffmpeg 官方加密的方式,即使有防盗链,也可以通过批量下载的方式拿到可播放的文件。
dullwit
   4
dullwit  
   2024 年 9 月 11 日
DRM ?
LuckyLauncher
   5
LuckyLauncher  
   2024 年 9 月 11 日
这个是自己加密前端实现解密?那不就相当于没加密?
18bili
   6
18bili  
OP
   2024 年 9 月 11 日
@LuckyLauncher 不能这么说吧,玩 next.js 一把梭的时候各种数据库相关的配置不也是存放在前端项目的.env 里,只要解密 key 不暴露即可。可以理解为前后端约定好了一套加解密规则,后端加密数据,前端取到后按约定好的规则进行数据解密。
LuckyLauncher
   7
LuckyLauncher  
   2024 年 9 月 11 日
@newbie111 #6 你在说些啥?你把.env 放到前端让人可以访问到?你把地址放出来等一会你看看你数据库还在不在
LuckyLauncher
   8
LuckyLauncher  
   2024 年 9 月 11 日
@newbie111 #6
“只要解密 key 不暴露即可”
“前端取到后按约定好的规则进行数据解密”
你就说你的前端解密需不需要 key
dzdh
   9
dzdh  
   2024 年 9 月 11 日
通通加密后。你 m3u8 的解密密钥怎么下发。写死到网页里?通过接口获取?

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

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

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

个人感觉,把后端鉴权做好就够了,保证走你自己加密的 js 那一套 web 流程。
EridanusSora
   24
EridanusSora  
   2024 年 9 月 11 日 via Android
视频加密的正确方法:上 DRM ,比如 widevine
easydou
   25
easydou  
   2024 年 9 月 11 日 via Android
加密 m3u8 的话,也可以用国内的一些厂商产品,比如保利威,阿里云等。如果自己写的话,最好打包成 wasm ,这样破解难度高,解密速度也比直接 js 解密要快
fly9i
   26
fly9i  
   2024 年 9 月 11 日
一般常规做法是在 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  
   2024 年 9 月 11 日
@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  
   2024 年 9 月 11 日 via Android
用 ffmpeg 加密之后,得到 m3u8 文件,把其中的解密 key 加密,前端播放前解密,这样对方拿到 m3u8 也无法播放,不过问题是解密是在前端,对方研究代码也能找到破解的方法,代价是花费更多的时间
puzzle9
   29
puzzle9  
   2024 年 9 月 11 日
m3u8
要不 你自己重新实现下解密流程 像那种视频网站一样
增加下破解难度而已
joewang1988
   30
joewang1988  
   2024 年 9 月 11 日
做完了,怎么交付?
joewang1988
   31
joewang1988  
   2024 年 9 月 11 日
收款账号信息

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  
   2024 年 9 月 11 日
还有个骚操作办法,先视频预处理对 rgb 通道进行异或,前端播放时根据 key 还原,速度非常快对性能影响很小,虽然没加密但是对方没 key 播放出来就是花屏
Oldletter
   33
Oldletter  
   2024 年 9 月 11 日
BaiLinfeng
   34
BaiLinfeng  
   2024 年 9 月 11 日
这是想做啥黑科技
Ipsum
   35
Ipsum  
   2024 年 9 月 11 日
@BaiLinfeng 我感觉是想放毛片在国内 cdn 上躲避审查。
BaiLinfeng
   36
BaiLinfeng  
   2024 年 9 月 11 日
@Ipsum 还可以这样玩的吗?有 demo 吗,我看看,那么骚的操作
azhangbing
   37
azhangbing  
   2024 年 9 月 11 日
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  
   2024 年 9 月 11 日
上午写完的,但联系不上,还能拿到 100u 么 [狗头]
Demo:[https://decipher-m3u8.yaavi.me/]( https://decipher-m3u8.yaavi.me/)
aycclm
   39
aycclm  
   2024 年 9 月 11 日
你们真的是卷死了 [手动狗头]
100u 还真给做了
jjl19960910
   40
jjl19960910  
   2024 年 9 月 11 日 via Android
@Ipsum #35 hh 笑死
18bili
   41
18bili  
OP
   2024 年 9 月 11 日
抱歉各位,我这里有 12 小时时差,现在是早上 6 点 46 ,刚起床,已经有好几个人加我,我在确认。
k9982874
   42
k9982874  
   2024 年 9 月 11 日 via Android
问问 ai 用 js 怎么 sha256 加密解密文件不就完事了。。
Yaavi
   43
Yaavi  
   2024 年 9 月 11 日
@newbie111 已收到 100u ,感谢老哥~
Yaavi
   44
Yaavi  
   2024 年 9 月 11 日   ❤️ 1
18bili
   45
18bili  
OP
   2024 年 9 月 11 日
抱歉 31 楼和其他加我的几位老哥,为节约大家时间,我优先加了在申请备注中提示“已按方案完成”的 Yaavi 老哥,验证后完美运行。31 楼老哥代码我刚跑了下,对比 Yaavi 老哥提供的代码,同样的 url ,会提示跨域,就没继续往下看了,感谢大家的帮忙,已转 yaavi 。
18bili
   46
18bili  
OP
   2024 年 9 月 11 日
感谢 v 站,藏龙卧虎果然不虚。
joewang1988
   47
joewang1988  
   2024 年 9 月 11 日
@newbie111

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

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

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

https://decipher-m3u8.yaavi.me/assets/index-DCANJRc2.js
3IOhG7M0knRu5UlC
   51
3IOhG7M0knRu5UlC  
   2024 年 9 月 11 日 via Android
掩耳盗铃
Erniu
   52
Erniu  
   2024 年 9 月 11 日
看了 github 源码,我知道了密钥是 1234567890ABCDEF ,谁能教教我,有什么方法能在只有混淆后代码的情况下,通过调试一步一步找到密钥?
07aPzknB16ui9Cp3
   53
07aPzknB16ui9Cp3  
   2024 年 9 月 11 日
@Yaavi 想提个问,简单看了一下代码,看起来 decryptTSAES 是拿去的 fetch 下来的全量 ArrayBuffer ,但是我个人理解对于 TS 这样的视频文件不是流式处理不会出问题吗
07aPzknB16ui9Cp3
   54
07aPzknB16ui9Cp3  
   2024 年 9 月 11 日
没事了,突然意识到是 m3u8 ,那么本身就是分片
Yaavi
   55
Yaavi  
   2024 年 9 月 11 日
@joewang1988 没问题 -vsme- 含两头的 -
sunchuo
   56
sunchuo  
   2024 年 9 月 11 日
@Yaavi star 了。感谢。
caola
   57
caola  
   2024 年 9 月 12 日
解密的密钥 key 放在 wasm 里面,多弄几个变量混淆一下密钥,还可以加入其他的参数进行校验(例如:服务器给出一个加密后的时间戳,wasm 解密这个参数后校验这个时间在一定的范围内就允许解密操作),然后再编译成二进制。一般能破解的概率小很多了。
js 把加密的数据和参数传入 wasm 解密后的数据反回给 js 。
maemo
   58
maemo  
   2025 年 11 月 15 日
各位大佬,有没有机会看看这个需求 https://www.v2ex.com/t/1172986
© 2026 V2EX · 128ms · 3.9.8.5