操作演示
如果是没加密的 一条命令就够了
ffmpeg -i http://aa.bb.com/play.m3u8 file.mp4
但被包含的资源一般m3u8的访问需要权限
同时m3u8中的ts也通过aes128加密 密码则也被包含起来
手动下载方式
1
|
video_m3u8_url=http://aa.bb.com/xx/001/play.m3u8
|
获取key
如果被加密了则 在 play.m3u8 中前几行可看到
1
|
#EXT-X-KEY:METHOD=AES-128,URI="https://aa.bb.com/yy/001/key",IV=0x5901307461D2D72A5EA7F1B2269C33DC
|
可得 ts_iv=5901307461D2D72A5EA7F1B2269C33DC && ts_key_url=https://aa.bb.com/yy/001/key
那就需要先获取 key
通常访问key也是需要鉴权的(验证登录)
1
|
curl $ts_key_url | hexdump -v -e '16/1 "%02x"'
|
可得 ts_key=d1d4295da4f2b1259f30df5a02337762
下载ts
然后是下载 在 play.m3u8 中的ts文件
有一行行的 ts文件名 001.ts 002.ts … 00N.ts
可得 ts文件下载地址为
1
2
3
|
http://aa.bb.com/xx/v23/001.ts
http://aa.bb.com/xx/v23/002.ts
http://aa.bb.com/xx/v23/00N.ts
|
解密
全部下载完成后 对每个ts文件进行解密
1
2
3
|
openssl aes-128-cbc -d -in 001.ts -out g001.ts -nosalt -K $ts_key -iv $ts_iv
openssl aes-128-cbc -d -in 002.ts -out g002.ts -nosalt -K $ts_key -iv $ts_iv
openssl aes-128-cbc -d -in 00N.ts -out g00N.ts -nosalt -K $ts_key -iv $ts_iv
|
合并文件
最后将解密后的文件合并
制作待合并文件清单
vlist.txt
1
2
3
4
|
file 'g001.ts'
file 'g002.ts'
...
file 'g00N.ts'
|
可以合成mp4的或ts的
1
2
3
4
5
|
#合成mp4文件
ffmpeg.exe -f concat -i vlist.txt -c copy output.mp4
或者用linux的cat合并ts
cat *.ts > cv23.ts
|
合并时,注意文件名顺序
顺序不对会导致合并的视频时间顺序错误
java代码方式下载
解密说明
在通过java解析时, 对解密流程比较耗心费力,着重讲下该部分。
如果是通过ffmpeg命令去转换m3u8为mp4
那ffmpeg会自动解密
或者使用 openssl 命令去解密ts文件 传入的是两个字符串
但网上下载的key是个二进制 以及如何用java解密比较迷惑。
最初没办法,用java调用系统命令 hexdump 计算key
然后改写了m3u8文件 又调用ffmpeg 去转换为mp4
之后又花功夫重新梳理关系 有了答案
ffmpeg 先是分割视频 并对每一段ts 使用了openssl去加密
一般都是搜索关键字 java openssl m3u8
网友都说让用ffmpeg去解密。。。
然后搜 java aes 再和 key 去解密,又绕远了。。。
划重点
在ffmpeg加密视频时 使用的时 aes 128 填充方式为PKCS7Padding
java默认不支持该方式 , 所以需要另外加载一个jar
gradle引入 implementation "org.bouncycastle:bcprov-jdk15on:1.64"
另一个困惑点
网上资料都是 openssl -k key-xxx iv iv-xxx
命令中都是字符串形式,在java解密代码中都是byte形式
二者如何转换
openssl生成的key iv 都是16个byte的数据
而字符串则是 每个byte转换为16进制的字符
每个byte有8位,每个16进制数有4位, 即1个byte会转为2位的16进制字符
所以通常 key iv都是32的字符串, 解析的时候,每两位转换成1个byte即可
工具类
工具类用于解析aes的key
以及将加密的ts文件解密
DecodeUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
package fluffy.mo.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.Security;
public class DecodeUtil {
static {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
//"abcd".getBytes(StandardCharsets.UTF_8);
}
public static byte[] decode(byte[] encryp, String key, String iv) throws Exception {
byte[] ivb = hexStr2Byte(iv);
byte[] kb = hexStr2Byte(key);
return decode(encryp, kb, ivb);
}
public static byte[] decode(byte[] encryp, byte[] keyb, byte[] ivb) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
SecretKeySpec keySpec = new SecretKeySpec(keyb, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivb);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] original = cipher.doFinal(encryp);
return original;
}
public static String sha1sum(String str) {
return sha1sum(str.getBytes(StandardCharsets.UTF_8));
}
public static String sha1sum(File file) throws Exception {
byte[] bytes = new FileInputStream(file).readAllBytes();
return sha1sum(bytes);
}
public static String sha1sum(byte[] bytes) {
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(bytes);
byte[] md = mdTemp.digest();
return byte2HexStr(md);
} catch (Exception e) {
return null;
}
}
/**
* 16进制字符串转二进制
*/
public static byte[] hexStr2Byte(String hexStr) {
if (hexStr.length() < 1) { // 字符串应当 长度为双数,字符都是[0-9或A-F]
return null;
} else {
int byteSize = hexStr.length() / 2;
byte[] result = new byte[byteSize];
for (int i = 0; i < byteSize; ++i) {
int strStart = i * 2;
int high = Integer.parseInt(hexStr.substring(strStart, strStart + 1), 16);
int low = Integer.parseInt(hexStr.substring(strStart + 1, strStart + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
}
private static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'};
public static String byte2HexStr2(byte[] bs) {
// 1byte有8个二进制位 而1个16进制数要占用4个二进制
// 所以2个16进制字符对应一个byte , 长度是字节的2倍
int j = bs.length;
char buf[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = bs[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
}
/**
* 二进制转16进制字符串
*/
public static String byte2HexStr(byte[] bs) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bs.length; ++i) {
String hex = Integer.toHexString(bs[i] & 0xFF);
if (hex.length() < 2) {
hex = "0" + hex;
}
sb.append(hex);
}
return sb.toString();
}
}
|
整理后
1
2
3
4
|
"cat /etc/passwd" #| "grep /bin/bash" #| "wc -l" !
"ls /usr" #| "wc -l" !
val fileNum = "ls /usr" #| "wc -l" !!
println(s"文件数量为: $fileNum")
|
文章作者
duansheli
上次更新
2019-12-25
(325c7b3)