处理使用 EXT-X-MEDIA-SEQUENCE 作为AES的iv的ts文件

main v1.18.0
orestonce 2023-06-02 20:02:54 +08:00
parent 7b1fa07cdf
commit 1519511baf
4 changed files with 92 additions and 26 deletions

View File

@ -7,6 +7,7 @@ import (
"crypto/cipher"
"crypto/sha256"
"crypto/tls"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
@ -31,6 +32,7 @@ import (
type TsInfo struct {
Name string
Url string
Seq uint64 // 如果是aes加密并且没有iv, 这个seq需要充当iv
}
type GetProgress_Resp struct {
@ -180,6 +182,7 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
return resp
}
}
beginSeq := parseBeginSeq(m3u8Body)
// 获取m3u8地址的内容体
encInfo, err := this.getEncryptInfo(req.M3u8Url, string(m3u8Body))
if err != nil {
@ -188,7 +191,7 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
return resp
}
this.SetProgressBarTitle("[3/6]获取ts列表")
tsList, errMsg := getTsList(req.M3u8Url, string(m3u8Body))
tsList, errMsg := getTsList(beginSeq, req.M3u8Url, string(m3u8Body))
if errMsg != "" {
resp.ErrMsg = "获取ts列表错误: " + errMsg
return resp
@ -278,6 +281,16 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
return resp
}
func parseBeginSeq(body []byte) uint64 {
data := M3u8Parse(string(body))
seq := data.GetPart(`#EXT-X-MEDIA-SEQUENCE`).TextFull
u, err := strconv.ParseUint(seq, 10, 64)
if err != nil {
return 0
}
return u
}
var gOldEnv *downloadEnv
var gOldEnvLocker sync.Mutex
@ -350,6 +363,10 @@ func (this *downloadEnv) getEncryptInfo(m3u8Url string, html string) (info *Encr
if uri == "" {
return nil, nil
}
method := keyPart.KeyValue["METHOD"]
if method == EncryptMethod_NONE {
return nil, nil
}
keyUrl, errMsg := resolveRefUrl(m3u8Url, uri)
if errMsg != "" {
return nil, errors.New(errMsg)
@ -359,12 +376,19 @@ func (this *downloadEnv) getEncryptInfo(m3u8Url string, html string) (info *Encr
if err != nil {
return nil, err
}
iv, err := hex.DecodeString(strings.TrimPrefix(keyPart.KeyValue["IV"], "0x"))
if method == EncryptMethod_AES128 && len(res) != 16 { // Aes 128
return nil, errors.New("getEncryptInfo invalid key " + strconv.Quote(string(res)))
}
var iv []byte
ivs := keyPart.KeyValue["IV"]
if ivs != "" {
iv, err = hex.DecodeString(strings.TrimPrefix(ivs, "0x"))
if err != nil {
return nil, err
}
}
return &EncryptInfo{
Method: keyPart.KeyValue["METHOD"],
Method: method,
Key: res,
Iv: iv,
}, nil
@ -379,7 +403,7 @@ func splitLineWithTrimSpace(s string) []string {
return tmp
}
func getTsList(m38uUrl string, body string) (tsList []TsInfo, errMsg string) {
func getTsList(beginSeq uint64, m38uUrl string, body string) (tsList []TsInfo, errMsg string) {
index := 0
for _, line := range splitLineWithTrimSpace(body) {
@ -394,6 +418,7 @@ func getTsList(m38uUrl string, body string) (tsList []TsInfo, errMsg string) {
tsList = append(tsList, TsInfo{
Name: fmt.Sprintf("%05d.ts", index), // ts视频片段命名规则
Url: after,
Seq: beginSeq + uint64(index-1),
})
}
}
@ -417,7 +442,7 @@ func (this *downloadEnv) downloadTsFile(ts TsInfo, download_dir string, encInfo
// 解密出视频 ts 源文件
if encInfo != nil {
//解密 ts 文件算法aes 128 cbc pack5
origData, err = AesDecrypt(origData, encInfo)
origData, err = AesDecrypt(ts.Seq, origData, encInfo)
if err != nil {
return err
}
@ -540,30 +565,29 @@ func isDirExists(path string) bool {
// ============================== 加解密相关 ==============================
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(crypted []byte, encInfo *EncryptInfo) ([]byte, error) {
func AesDecrypt(seq uint64, crypted []byte, encInfo *EncryptInfo) ([]byte, error) {
block, err := aes.NewCipher(encInfo.Key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
iv := encInfo.Iv
if len(iv) == 0 {
if len(encInfo.Key) > blockSize {
return nil, errors.New(fmt.Sprint("AesDecrypt invalid size ", blockSize, " ", len(encInfo.Key)))
if encInfo.Method == EncryptMethod_AES128 {
iv = make([]byte, 16)
binary.BigEndian.PutUint64(iv[8:], seq)
} else {
return nil, errors.New("AesDecrypt method not support " + strconv.Quote(encInfo.Method))
}
iv = encInfo.Key[:blockSize]
}
blockMode := cipher.NewCBCDecrypter(block, iv)
origData := make([]byte, len(crypted))
blockMode.CryptBlocks(origData, crypted)
origData = PKCS7UnPadding(origData)
return origData, nil
length := len(origData)
unpadding := int(origData[length-1])
if length-unpadding < 0 {
return nil, fmt.Errorf(`invalid length of unpadding %v - %v`, length, unpadding)
}
return origData[:(length - unpadding)], nil
}
func getFileSha256(targetFile string) (v string) {

View File

@ -1,6 +1,7 @@
package m3u8d
import (
"bytes"
"embed"
"io/fs"
"net/http"
@ -50,7 +51,7 @@ func TestGetTsList(t *testing.T) {
}
func tGetTsList(m3u8Url string, m3u8Content string, expectTs0Url string) {
list, errMsg := getTsList(m3u8Url, m3u8Content)
list, errMsg := getTsList(0, m3u8Url, m3u8Content)
if errMsg != "" {
panic(errMsg)
}
@ -88,6 +89,7 @@ func TestFull(t *testing.T) {
M3u8Url: m3u8Url,
SaveDir: saveDir,
FileName: "all",
ThreadCount: 8,
})
if resp2.ErrMsg != "" {
panic(resp2.ErrMsg)
@ -118,3 +120,17 @@ func TestGetFileName(t *testing.T) {
t.Fail()
}
}
func TestCloseOldEnv(t *testing.T) {
encInfo := EncryptInfo{
Method: EncryptMethod_AES128,
Key: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6},
Iv: nil,
}
before := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 4}
after, err := AesDecrypt(1, before, &encInfo)
checkErr(err)
if bytes.Equal(after, []byte{69, 46, 52, 180, 68, 205, 99, 220, 193, 44, 116, 174, 96, 196, 199, 87, 214, 77, 67, 5, 37, 8, 139, 146, 229, 120, 164, 76, 107, 0, 204, 0}) == false {
panic("expect bytes failed")
}
}

View File

@ -11,9 +11,15 @@ type EncryptInfo struct {
Iv []byte
}
// https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.2.4
const (
EncryptMethod_NONE = `NONE`
EncryptMethod_AES128 = `AES-128`
EncryptMethod_SIMPLE_AES = `SAMPLE-AES` // TODO
)
type M3u8Content struct {
PartList []M3u8Part `json:",omitempty"`
TsList []string `json:",omitempty"`
}
type M3u8Part struct {

View File

@ -16,7 +16,7 @@ func TestM3u8Parse(t *testing.T) {
#EXT-X-KEY:METHOD=AES-128,URI="/20230502/xthms/2000kb/hls/key.key"
`)
part := info.GetPart("#EXT-X-KEY")
if part.KeyValue["METHOD"] != "AES-128" {
if part.KeyValue["METHOD"] != EncryptMethod_AES128 {
panic("method")
}
if part.KeyValue["URI"] != "/20230502/xthms/2000kb/hls/key.key" {
@ -48,3 +48,23 @@ func checkErr(err error) {
panic(err)
}
}
func TestM3u8Parse2(t *testing.T) {
seq1 := parseBeginSeq([]byte(`#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0`))
if seq1 != 0 {
panic(seq1)
}
seq2 := parseBeginSeq([]byte(`#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:2`))
if seq2 != 2 {
panic(seq2)
}
}