package com.example.demo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Test;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
/**
* TS 文件加解密。 包含如下两套加解密方式:
* 1. AES/CBC/PKCS7Padding 标准 Java 加解密方式
* 2. AES/CBC/NoPadding 加手动 PKCS7Padding 方式。当前 Stream 采用这种方式。
* <p>
* AES-CBC-128 加密
*/
public class MediaFileCryptoUtils {
// 算法名称
private static final String KEY_ALG = "AES";
/**
* 加解密算法 /模式 /填充方式。PKCS7Padding
*/
private static final String AES_CBC_PKCS7PADDING = "AES/CBC/PKCS7Padding";
/**
* 加解密算法 /模式 /填充方式。
* 这里虽然是 NoPadding,但实际最后一个数据块会手动做 PKCS7Padding
*/
private static final String AES_CBC_NOPADDING = "AES/CBC/NoPadding";
/**
* AES 加密数据块分组长度必须为 128 比特( bit 位),
* 密钥长度可以是 128 比特、192 比特、256 比特中的任意一个(如果数据块不足密钥长度时,会补齐)。
*/
private static final long CIPHER_BLOCK_SIZE = 16;
// 每次读取的缓冲区长度,必须为 CIPHER_BLOCK_SIZE 的倍数
private static final int BUFFER_SIZE = 1024;
// 加密后的 ts 文件块大小
private static final int TS_BLOCK_SIZE = 188;
static {
Security.addProvider(new BouncyCastleProvider());
}
private static Cipher getCipher(byte[] keyBytes, byte[] ivBytes, String transformation, int encryptMode) {
try {
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(encryptMode, new SecretKeySpec(keyBytes, KEY_ALG), new IvParameterSpec(ivBytes));
return cipher;
} catch (NoSuchAlgorithmException | NoSuchPaddingException
| InvalidAlgorithmParameterException | InvalidKeyException e) {
throw new RuntimeException("Error occurred while getting cipher", e);
}
}
/**
* 用给定的 key 和 iv 加密指定 TS 文件并将结果写入到指定的输出流
*
* @param keyString 秘钥字符串,例如 "362ed0938ef220d8"
* @param ivHexString 初始向量的十六进制字符串,前面有 0x 开头,例如 "0x04401234f48591766c1a3bc51ab173f0"
* @param sourceTS 源 TS 文件路径
* @param os 要输出到的流
*/
public static void encryptTS(String keyString, String ivHexString, String sourceTS, OutputStream os) {
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = Hex.decode(ivHexString.substring(2));
encryptTS(keyBytes, ivBytes, sourceTS, os);
}
public static void encryptTsWithManualPadding(String keyString, String ivHexString, String sourceTS, OutputStream os) {
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = Hex.decode(ivHexString.substring(2));
encryptTsWithManualPadding(keyBytes, ivBytes, sourceTS, os);
}
/**
* 用给定的 key 和 iv 加密指定 TS 文件并将结果写入到指定的输出流。
* <p>
* AES-CBC 对文件加密的标准 Java 写法。
*
* @param keyBytes 秘钥
* @param ivBytes 初始向量
* @param sourceTS 源 TS 文件路径
* @param os 输出流
*/
public static void encryptTS(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
// 初始化 cipher, 同一个文件要用一个 Cipher
Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_PKCS7PADDING, Cipher.ENCRYPT_MODE);
File plainFile = new File(sourceTS);
try (FileInputStream fis = new FileInputStream(plainFile)) {
byte[] buffer = new byte[BUFFER_SIZE];
int length = -1;
int count = 0;
while ((length = fis.read(buffer)) != -1) {
System.out.println("count: " + count++ + ", length: " + length);
byte[] encryptedData;
// 可读大小为 0,表示当前已读到的数据是最后一块数据
if (fis.available() == 0) {
encryptedData = cipher.doFinal(buffer, 0, length);
} else {
encryptedData = cipher.update(buffer, 0, length);
}
os.write(encryptedData);
}
} catch (IOException | BadPaddingException | IllegalBlockSizeException e) {
throw new RuntimeException("Error occurred while encrypting ts", e);
}
}
/**
* 用给定的 key 和 iv 加密指定 TS 文件并将结果写入到指定的输出流。
* <p>
* Stream 里面 TS 加密的 Java 实现,所有数据块采用 AES_CBC_NOPADDING,最后一个数据块需要手动加上 PKCS7Padding 。
*
* @param keyBytes 秘钥
* @param ivBytes 初始向量
* @param sourceTS 源 TS 文件路径
* @param os 输出流
*/
public static void encryptTsWithManualPadding(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
// 初始化 cipher, 同一个文件要用一个 Cipher
Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_NOPADDING, Cipher.ENCRYPT_MODE);
File plainFile = new File(sourceTS);
try (FileInputStream fis = new FileInputStream(plainFile)) {
long totalLength = plainFile.length();
int paddingLength = (int) (CIPHER_BLOCK_SIZE - totalLength % CIPHER_BLOCK_SIZE);
byte[] buffer = new byte[BUFFER_SIZE];
int length = -1;
while ((length = fis.read(buffer)) != -1) {
byte[] plainData = buffer;
// 可读大小为 0,表示当前已读到的数据是最后一块数据, 且需要 padding
if (fis.available() == 0 && paddingLength != 0) {
plainData = new byte[length + paddingLength];
System.arraycopy(buffer, 0, plainData, 0, length);
// PCKS7 填充,在填充字节上都填相同的数据,比如数据缺少 4 字节,所以所有字节上都填 4
for (int i = length; i < plainData.length; i++) {
plainData[i] = (byte) paddingLength;
}
}
/**
*这里不要使用 cipher.doFinal 因为 CBC 是循环加密,要把上一个加密快的结果作为下一次加密的 iv 。
* 即使是最后一个数据块也不需要使用 cipher.doFinal,因为上面针对最后一个数据块手动进行了 PKCS7 填充
*/
byte[] encryptedData = cipher.update(plainData);
os.write(encryptedData);
}
} catch (IOException e) {
throw new RuntimeException("Error occurred while encrypting ts with manual padding", e);
}
}
/**
* 用给定的 key 和 iv 解密指定 TS 文件并将结果写入到指定的输出流
*
* @param keyString 秘钥字符串,例如 "362ed0938ef220d8"
* @param ivHexString 初始向量的十六进制字符串,前面有 0x 开头,例如 "0x04401234f48591766c1a3bc51ab173f0"
* @param sourceTS 源 TS 文件路径
* @param os 要输出到的流
*/
public static void decryptTS(String keyString, String ivHexString, String sourceTS, OutputStream os) {
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = Hex.decode(ivHexString.substring(2));
decryptTS(keyBytes, ivBytes, sourceTS, os);
}
public static void decryptTsWithManualPadding(String keyString, String ivHexString, String sourceTS, OutputStream os) {
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = Hex.decode(ivHexString.substring(2));
decryptTsWithManualPadding(keyBytes, ivBytes, sourceTS, os);
}
/**
* 用给定的 key 和 iv 解密指定 TS 文件并将结果写入到指定的输出流。
* <p>
* AES-CBC 对文件解密的标准 Java 写法。
*
* @param keyBytes 秘钥
* @param ivBytes 初始向量
* @param sourceTS 源 TS 文件路径
* @param os 输出流
*/
public static void decryptTS(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
// 初始化 cipher, 同一个文件要用一个 Cipher
Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_PKCS7PADDING, Cipher.DECRYPT_MODE);
File encryptedFile = new File(sourceTS);
try (FileInputStream fis = new FileInputStream(encryptedFile)) {
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = fis.read(buffer)) != -1) {
byte[] plainData;
if (fis.available() == 0) {
plainData = cipher.doFinal(buffer, 0, length);
} else {
plainData = cipher.update(buffer, 0, length);
}
os.write(plainData);
}
} catch (IOException | BadPaddingException | IllegalBlockSizeException e) {
throw new RuntimeException("Error occurred while decrypting ts", e);
}
}
/**
* 用给定的 key 和 iv 解密指定 TS 文件并将结果写入到指定的输出流。
* <p>
* Stream 里面 TS 解密的 Java 实现,所有数据块采用 AES_CBC_NOPADDING,最后一个数据块需要手动去除 padding 。
*
* @param keyBytes 秘钥
* @param ivBytes 初始向量
* @param sourceTS 源 TS 文件路径
* @param os 输出流
*/
public static void decryptTsWithManualPadding(byte[] keyBytes, byte[] ivBytes, String sourceTS, OutputStream os) {
// 初始化 cipher, 同一个文件要用一个 Cipher
Cipher cipher = getCipher(keyBytes, ivBytes, AES_CBC_NOPADDING, Cipher.DECRYPT_MODE);
File encryptedFile = new File(sourceTS);
try (FileInputStream fis = new FileInputStream(encryptedFile)) {
byte[] buffer = new byte[BUFFER_SIZE];
int totalLength = fis.available();
int length;
while ((length = fis.read(buffer)) != -1) {
byte[] plainData = cipher.update(buffer);
int plainDataLength = plainData.length; // 默认为解密后的数据长度
if (fis.available() == 0) {
// 最后一个解密出来的数据数据块,要去掉 Padding 的数据
// 计算 padding 长度
int paddingLength = totalLength % TS_BLOCK_SIZE;
// 去掉无用的 padding
plainDataLength = length - paddingLength;
}
os.write(plainData, 0, plainDataLength);
}
} catch (IOException e) {
throw new RuntimeException("Error occurred while decrypting ts with manual padding", e);
}
}
@Test
public void testEncryptFile() {
try (FileOutputStream fos = new FileOutputStream(new File("D:\\196.ets"))) {
encryptTS("7db4fd4359bb25b0", "0xb70cbefa3168efd2d0984abc8181ecff",
"D:\\196.ts", fos);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testDecryptFile() {
try (FileOutputStream fos = new FileOutputStream(new File("D:\\9.ts"))) {
decryptTS("7db4fd4359bb25b0", "0xb70cbefa3168efd2d0984abc8181ecff",
"D:\\record-crypt\\06987ff1-0357-45b1-a6b8-f062e989c82d\\videoHD\\9.ts", fos);
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.util.CollectionUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class M3u8Parser {
/**
* m3u8 文件头指令:m3u8 文件头。必须在文件第一行。
*/
private static final String DIRECTIVE_HEADER = "#EXTM3U";
/**
* 码流信息指令:带宽、分辨率,解码器等键值对信息。后一行跟对应码流的 m3u8 文件位置。
*/
private static final String DIRECTIVE_STREAM_INF = "#EXT-X-STREAM-INF";
/**
* 音频,视频轨道信息指令:时长(秒),标题,其他额外信息(如 logo )以键值对显示。后一行跟对应 ts 的文件位置
*/
private static final String DIRECTIVE_TRACK_INF = "#EXTINF";
/**
* 列表终止标识指令
*/
private static final String DIRECTIVE_ENDLIST = "#EXT-X-ENDLIST";
/**
* m3u8 文件包含的最小行数
*/
private static final int M3U8_MIN_LINES = 2;
public List<String> getAllTsPaths(String indexM3u8) {
File indexM3u8File = new File(indexM3u8);
if (!indexM3u8File.exists()) {
throw new IllegalArgumentException("File not found");
}
if (!indexM3u8File.isFile()) {
throw new IllegalArgumentException(indexM3u8File + " is not a file");
}
String basePath = indexM3u8File.getParentFile().getAbsolutePath();
Set<String> tsSet = parseIndexM3u8(basePath, indexM3u8File);
if (CollectionUtils.isEmpty(tsSet)) {
throw new IllegalArgumentException("No TS in specified m3u8 file");
}
return tsSet.stream().map(tsName -> basePath + File.separator + tsName).collect(Collectors.toList());
}
private Set<String> parseIndexM3u8(String basePath, File indexM3u8File) {
// index m3u8 文件比较小,一次性读完
List<String> indexM3u8Lines = readAllLines(indexM3u8File);
validateM3u8(indexM3u8Lines);
for (int i = 1; i < indexM3u8Lines.size(); i++) {
String line = indexM3u8Lines.get(i);
if (line.startsWith(DIRECTIVE_STREAM_INF)) {
// 遇到第一个码流信息,取码流之后的一行就是子 m3u8 文件的位置,当前第一个码流信息就够了
String subM3u8 = basePath + File.separator + indexM3u8Lines.get(i + 1);
return parseSubM3u8(subM3u8);
}
}
throw new IllegalArgumentException("Not a valid m3u8 file: no ts info");
}
private Set<String> parseSubM3u8(String subM3u8) {
// sub m3u8 文件可能会比较大,每读一行就解析一行
try (FileReader fr = new FileReader(new File(subM3u8));
BufferedReader bf = new BufferedReader(fr)) {
Set<String> tracks = new LinkedHashSet<>();
String line;
while ((line = bf.readLine()) != null) {
if (line.startsWith(DIRECTIVE_TRACK_INF)) {
// 当前行是轨道信息,就再读一行
line = bf.readLine();
if (line != null) {
tracks.add(line);
}
}
if (line.startsWith(DIRECTIVE_ENDLIST)) {
break;
}
}
return tracks;
} catch (IOException e) {
throw new IllegalArgumentException("Error occurred while parsing sub m3u8 file", e);
}
}
private void validateM3u8(List<String> indexM3u8Lines) {
if (indexM3u8Lines.size() < M3U8_MIN_LINES) {
throw new IllegalArgumentException("Invalid m3u8 file: insufficient lines");
}
if (!DIRECTIVE_HEADER.equals(indexM3u8Lines.get(0))) {
throw new IllegalArgumentException("Invalid m3u8 file: invalid m3u8 header");
}
}
public List<String> readAllLines(File file) {
List<String> lines = new ArrayList<>();
try (FileReader fr = new FileReader(file);
BufferedReader bf = new BufferedReader(fr)) {
String line;
while ((line = bf.readLine()) != null) {
lines.add(line);
}
return lines;
} catch (IOException e) {
throw new RuntimeException("Error occurred while reading file", e);
}
}
@Test
public void testM3u8Parser() {
M3u8Parser m3u8Parser = new M3u8Parser();
m3u8Parser.getAllTsPaths("D:\\record-crypt\\40ac6397-5116-4b44-8cb9-a2f70d8d68fa\\videoHD\\index.m3u8").stream().forEach(System.out::println);
}
}