From 1519511baf4d56acf8afaf3dd0691fe65dafe733 Mon Sep 17 00:00:00 2001 From: orestonce Date: Fri, 2 Jun 2023 20:02:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BD=BF=E7=94=A8=20EXT-X-ME?= =?UTF-8?q?DIA-SEQUENCE=20=E4=BD=9C=E4=B8=BAAES=E7=9A=84iv=E7=9A=84ts?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- download.go | 64 +++++++++++++++++++++++++++++++++--------------- download_test.go | 24 +++++++++++++++--- m3u8.go | 8 +++++- m3u8_test.go | 22 ++++++++++++++++- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/download.go b/download.go index 7779a90..bf7af14 100644 --- a/download.go +++ b/download.go @@ -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 err != nil { - return nil, err + 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) { diff --git a/download_test.go b/download_test.go index d64afd0..e8be219 100644 --- a/download_test.go +++ b/download_test.go @@ -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) } @@ -85,9 +86,10 @@ func TestFull(t *testing.T) { panic(err) } resp2 := RunDownload(RunDownload_Req{ - M3u8Url: m3u8Url, - SaveDir: saveDir, - FileName: "all", + 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") + } +} diff --git a/m3u8.go b/m3u8.go index 75d1111..05d3d7e 100644 --- a/m3u8.go +++ b/m3u8.go @@ -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 { diff --git a/m3u8_test.go b/m3u8_test.go index e22a646..0b181a3 100644 --- a/m3u8_test.go +++ b/m3u8_test.go @@ -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) + } + +}