m3u8.sqlite转mp4(txkt,文末附程序下载地址)

557 阅读6分钟

中秋节放假,本想好好休息一下,女朋友说手机里下载了的腾讯课堂的课程能不能够传到电脑上面去,因为手机的空间不够了,我心想这不是很简单吗。。 果然,事情没我想的那么简单,找到了腾讯课堂的视频缓存目录

/Android/data/com.tencent.edu/files/tencentedu/video/txdownload/

发现里面的文件格式全部都是 xxx.m3u8.sqlite,并不是我们常见的可播放文件 于是转而百度搜索了一下如何把.m3u8.sqlite 转为mp4,在这片文章上面找到了可行的方案 m3u8.sqlite转mp4

根据文章上面的描述,先读取sqlite数据库文件,然后根据第一行获取文件的元数据信息,根据第二行获取文件的解密秘钥。然后其余的行则是真实的视频内容。

这里的第一行第二行在不同的排序规则下数据其实是不一样的,所以判断规则可以这样来判断: 1、如果url中没有start参数那么说明这不是视频内容数据,是属于1,2行内容 2、如果url没有start并且没有sign,那么说明是秘钥,如果有sign,那么说明是文件元数据

再根据上面的逻辑处理好之后对真是的文件内容进行解密和拼接

本以为万事大吉,可没想到,仅仅只有一个文件能够解密成功,其他的文件竟然都无法解密。查看了一下报错原因,发现竟然是数据库文件无法解析,于是使用编辑器打开对比了一下不能解析的sqlite文件 在这里插入图片描述 上图中左边是正常的能解析的文件,右边是不能接下的,可以看到不能解析的sqlite头文件被加密了,但是具体的加密方式却不得而知。 于是换了一个思路,通过对比发现这些文件都只是头部被加密了,内容部分并没有进行加密,于是冒出了一个想法,既然头部被加密了,那我把被加密的头部换成一个正常的头部是不是就可以了呢?试试证明,确实没错。 最近做了一些优化,做了一个界面化的程序 下载地址:convert.exe 最终效果: 在这里插入图片描述

具体的代码如下: 代码很粗略,能解决女朋友就行,哈哈哈

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"database/sql"
	"fmt"
	_ "github.com/mattn/go-sqlite3"
	"io"
	"io/ioutil"
	"net/url"
	"os"
	"strings"
)

func main() {
	//当前文件所在的路径
	rootPath := "./"
	//rootPath := "D:\\IdeaWorkspace\\go-study\\src\\test\\"
	//获取一个可用的sqlite文件头部
	//head := readHead(rootPath)
	//使用固定头部
	var head = []byte{  0x53 ,0x51 ,0x4C ,0x69 ,0x74 ,0x65 ,0x20 ,0x66 ,0x6F ,0x72 ,0x6D ,0x61 ,0x74 ,0x20 ,0x33 ,0x00 ,0x10 ,0x00 ,0x01 ,0x01 ,0x00 ,0x40 ,0x20 ,0x20 ,0x00 ,0x00 ,0x00 ,0x73 ,0x00 ,0x00 ,0xEE ,0xBD,0x00 ,0x00 ,0x00 ,0x06 ,0x00 ,0x00 ,0x00 ,0x07 ,0x00 ,0x00 ,0x00 ,0x02 ,0x00 ,0x00 ,0x00 ,0x04,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x01 ,0x00 ,0x00 ,0x00 ,0x00,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x64,0x00 ,0x2E ,0x28 ,0x6B ,0x0D ,0x0F ,0xF8 ,0x00 ,0x04 ,0x0E ,0xF4 ,0x00 ,0x0F ,0x71 ,0x0F ,0xC7,0x0E ,0xF4 ,0x0F ,0x44 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00}

	//输出的mp文件路径
	outPath := rootPath + "out/"
	
	
	pathExists, _ := PathExists(outPath)
	if !pathExists {
		os.Mkdir(outPath, 0666)
	}
	infos, _ := ioutil.ReadDir(rootPath)

	for _, file := range infos {
		suffix := strings.HasSuffix(file.Name(), "m3u8.sqlite")
		if suffix && !file.IsDir() {
			fmt.Println("开始处理文件:", file.Name(), "头部", head)
			rewriteHead(head, rootPath, file.Name())
			fmt.Println("校验头部:", getHead(rootPath+file.Name()))
			fmt.Println("开始处转换文件:", file.Name())
			GetMp4(rootPath, file.Name(), outPath)
		}
	}

}

func rewriteHead(head []byte, rootpath string, name string) {
	//head[95] = byte(100)
	nfile, err := os.OpenFile(rootpath+name, os.O_RDWR|os.O_CREATE, 0666)
	fmt.Println("文件", name, "打开并重写头部", err)
	n, err := nfile.WriteAt(head, 0)
	fmt.Println("重写是否发生异常", err, n)
	nfile.Close()
}

func readHead(rootpath string) []byte {
	var filename string
	infos, _ := ioutil.ReadDir(rootpath)
	for _, file := range infos {
		suffix := strings.HasSuffix(file.Name(), "m3u8.sqlite")
		if suffix && !file.IsDir() {
			fmt.Println("开始获取文件头:", file.Name())
			gdb, e := sql.Open("sqlite3", rootpath+file.Name())
			if e == nil {
				rows, e1 := gdb.Query("SELECT * FROM caches")
				if e1 == nil && rows.Next() {
					filename = file.Name()
					break
				} else {
					fmt.Println("文件", file.Name(), "头部有问题")
				}
			} else {
				fmt.Println("文件", file.Name(), "头部有问题")
			}
			gdb.Close()
		}
	}
	fmt.Println("正确头部文件", filename, "文件打开")
	return getHead(rootpath + filename)

}

func getHead(path string) []byte {
	head := make([]byte, 128)
	file, _ := os.Open(path)
	io.ReadAtLeast(file, head[:], 128)
	file.Close()
	return head
}

func GetMp4(path string, name string, outpath string) []byte {
	var gdb *sql.DB
	fmt.Println("打开数据库:", path+name)
	gdb, e := sql.Open("sqlite3", path+name)
	if e != nil {
		fmt.Println(e)
		return nil
	}
	defer gdb.Close()
	rows, _ := gdb.Query("SELECT * FROM caches")
	var temp = make(map[string][]byte, 10000)
	var aeskey []byte
	var content string
	for rows.Next() {
		var key string
		var value []byte
		e = rows.Scan(&key, &value)
		//fmt.Println("urlkey:",key,e)
		u := url.URL{}
		parse, _ := u.Parse(key)
		start := parse.Query().Get("start")
		end := parse.Query().Get("end")
		sign := parse.Query().Get("sign")
		if strings.Contains(key, "?") {
			k := "start:" + start + "end:" + end
			fmt.Println("缓存key:", k)
			temp[k] = value
		} else {
			fmt.Println("没有缓存", key)
		}
		if start == "" {
			if sign != "" {
				s := string(value)
				content = s

			} else {
				aeskey = value
				fmt.Println("获取秘钥的key:", key)
			}
		}
	}
	//fmt.Println("秘钥",aeskey)
	//fmt.Println("解析信息",content)
	
	var urlList []string
	for _, e := range strings.Split(content, "\n") {
		if e != "" && strings.Contains(e, "start") && strings.Contains(e, "end") {
			s := "http://www.baidu.com" + e
			u := url.URL{}
			parse, _ := u.Parse(s)
			start := parse.Query().Get("start")
			end := parse.Query().Get("end")
			k := "start:" + start + "end:" + end
			urlList = append(urlList, k)
		}
	}
	fmt.Println("开始解密")
	file, _ := os.Create(outpath + name + ".mp4")
	for _, str := range urlList {
		fmt.Println("解密:", str)
		video := temp[str]
		if len(video) == 0 {
			fmt.Println("待解密内容为空", str)
			continue
		}
		res, _ := AesDecrypt(video, aeskey)
		//fmt.Println("解密位置:",str, "  文件大小:",len(temp[str])," 解密之后 :",len(res))

		// 查找文件末尾的偏移量
		n, _ := file.Seek(0, io.SeekEnd)
		// 从末尾的偏移量开始写入内容
		_, _ = file.WriteAt(res, n)

	}
	file.Close()
	fmt.Println(name, "解密完成")
	gdb.Close()
	return aeskey
}

func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padtext...)
}

func PKCS5UnPadding(origData []byte) []byte {
	length := len(origData)
	unpadding := int(origData[length-1])
	return origData[:(length - unpadding)]
}

//加密
func AesEncrypt(origData, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	origData = PKCS5Padding(origData, blockSize)
	blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
	crypted := make([]byte, len(origData))
	blockMode.CryptBlocks(crypted, origData)
	return crypted, nil
}
//解密
func AesDecrypt(crypted, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	blockSize := block.BlockSize()
	blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
	origData := make([]byte, len(crypted))
	blockMode.CryptBlocks(origData, crypted)
	origData = PKCS5UnPadding(origData)
	return origData, nil
}

//判断路径是否存在
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

最近修改了程序,使用固定的头部区替换文件的头部,避免有些人的文件列表里面找不到一个可用的头部的情况。

可执行程序下载地址:convert.exe

运行的时候直接双击安装就可以使用了