跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • M3u8 文件解析和 TS 文件加解密,不知道对不对
未分類
29 8 月 2020

M3u8 文件解析和 TS 文件加解密,不知道对不对

M3u8 文件解析和 TS 文件加解密,不知道对不对

資深大佬 : yuyujulin 7

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);     } }  

大佬有話說 (0)

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具