update dat2img (#11)
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/sjzar/chatlog/internal/chatlog/mcp"
|
||||
"github.com/sjzar/chatlog/internal/chatlog/wechat"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
"github.com/sjzar/chatlog/pkg/util/dat2img"
|
||||
)
|
||||
|
||||
// Manager 管理聊天日志应用
|
||||
@@ -96,6 +97,11 @@ func (m *Manager) StartService() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是 4.0 版本,更新下 xorkey
|
||||
if m.ctx.Version == 4 {
|
||||
go dat2img.ScanAndSetXorKey(m.ctx.DataDir)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
m.ctx.SetHTTPEnabled(true)
|
||||
|
||||
|
||||
@@ -1,31 +1,53 @@
|
||||
package dat2img
|
||||
|
||||
// copy from: https://github.com/tujiaw/wechat_dat_to_image
|
||||
// Implementation based on:
|
||||
// - https://github.com/tujiaw/wechat_dat_to_image
|
||||
// - https://github.com/LC044/WeChatMsg/blob/6535ed0/wxManager/decrypt/decrypt_dat.py
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Format defines the header and extension for different image types
|
||||
type Format struct {
|
||||
Header []byte
|
||||
Ext string
|
||||
}
|
||||
|
||||
var (
|
||||
// Common image format definitions
|
||||
JPG = Format{Header: []byte{0xFF, 0xD8, 0xFF}, Ext: "jpg"}
|
||||
PNG = Format{Header: []byte{0x89, 0x50, 0x4E, 0x47}, Ext: "png"}
|
||||
GIF = Format{Header: []byte{0x47, 0x49, 0x46, 0x38}, Ext: "gif"}
|
||||
TIFF = Format{Header: []byte{0x49, 0x49, 0x2A, 0x00}, Ext: "tiff"}
|
||||
BMP = Format{Header: []byte{0x42, 0x4D}, Ext: "bmp"}
|
||||
Formats = []Format{JPG, PNG, GIF, TIFF, BMP}
|
||||
|
||||
// WeChat v4 related constants
|
||||
V4XorKey byte = 0x37 // Default XOR key for WeChat v4 dat files
|
||||
V4DatHeader = []byte{0x07, 0x08, 0x56, 0x31} // WeChat v4 dat file header
|
||||
JpgTail = []byte{0xFF, 0xD9} // JPG file tail marker
|
||||
)
|
||||
|
||||
// Dat2Image converts WeChat dat file data to image data
|
||||
// Returns the decoded image data, file extension, and any error encountered
|
||||
func Dat2Image(data []byte) ([]byte, string, error) {
|
||||
|
||||
if len(data) < 4 {
|
||||
return nil, "", fmt.Errorf("data length is too short: %d", len(data))
|
||||
}
|
||||
|
||||
// Check if this is a WeChat v4 dat file
|
||||
if len(data) >= 6 && bytes.Equal(data[:4], V4DatHeader) {
|
||||
return Dat2ImageV4(data)
|
||||
}
|
||||
|
||||
// For older WeChat versions, use XOR decryption
|
||||
findFormat := func(data []byte, header []byte) bool {
|
||||
xorBit := data[0] ^ header[0]
|
||||
for i := 0; i < len(header); i++ {
|
||||
@@ -37,20 +59,21 @@ func Dat2Image(data []byte) ([]byte, string, error) {
|
||||
}
|
||||
|
||||
var xorBit byte
|
||||
var find bool
|
||||
var found bool
|
||||
var ext string
|
||||
for _, format := range Formats {
|
||||
if find = findFormat(data, format.Header); find {
|
||||
if found = findFormat(data, format.Header); found {
|
||||
xorBit = data[0] ^ format.Header[0]
|
||||
ext = format.Ext
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !find {
|
||||
if !found {
|
||||
return nil, "", fmt.Errorf("unknown image type: %x %x", data[0], data[1])
|
||||
}
|
||||
|
||||
// Apply XOR decryption
|
||||
out := make([]byte, len(data))
|
||||
for i := range data {
|
||||
out[i] = data[i] ^ xorBit
|
||||
@@ -58,3 +81,222 @@ func Dat2Image(data []byte) ([]byte, string, error) {
|
||||
|
||||
return out, ext, nil
|
||||
}
|
||||
|
||||
// calculateXorKeyV4 calculates the XOR key for WeChat v4 dat files
|
||||
// by analyzing the file tail against known JPG ending bytes (FF D9)
|
||||
func calculateXorKeyV4(data []byte) (byte, error) {
|
||||
if len(data) < 2 {
|
||||
return 0, fmt.Errorf("data too short to calculate XOR key")
|
||||
}
|
||||
|
||||
// Get the last two bytes of the file
|
||||
fileTail := data[len(data)-2:]
|
||||
|
||||
// Assuming it's a JPG file, the tail should be FF D9
|
||||
xorKeys := make([]byte, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
xorKeys[i] = fileTail[i] ^ JpgTail[i]
|
||||
}
|
||||
|
||||
// Verify that both bytes yield the same XOR key
|
||||
if xorKeys[0] == xorKeys[1] {
|
||||
return xorKeys[0], nil
|
||||
}
|
||||
|
||||
// If inconsistent, return the first byte as key with a warning
|
||||
return xorKeys[0], fmt.Errorf("inconsistent XOR key, using first byte: 0x%x", xorKeys[0])
|
||||
}
|
||||
|
||||
// ScanAndSetXorKey scans a directory for "_t.dat" files to calculate and set
|
||||
// the global XOR key for WeChat v4 dat files
|
||||
// Returns the found key and any error encountered
|
||||
func ScanAndSetXorKey(dirPath string) (byte, error) {
|
||||
// Walk the directory recursively
|
||||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only process "_t.dat" files (thumbnail files)
|
||||
if !strings.HasSuffix(info.Name(), "_t.dat") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read file content
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if it's a WeChat v4 dat file
|
||||
if len(data) < 6 || !bytes.Equal(data[:4], V4DatHeader) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse file header
|
||||
if len(data) < 15 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get XOR encryption length
|
||||
xorEncryptLen := binary.LittleEndian.Uint32(data[10:14])
|
||||
|
||||
// Get data after header
|
||||
fileData := data[15:]
|
||||
|
||||
// Skip if there's no XOR-encrypted part
|
||||
if xorEncryptLen == 0 || uint32(len(fileData)) <= uint32(len(fileData))-xorEncryptLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get XOR-encrypted part
|
||||
xorData := fileData[uint32(len(fileData))-xorEncryptLen:]
|
||||
|
||||
// Calculate XOR key
|
||||
key, err := calculateXorKeyV4(xorData)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set global XOR key
|
||||
V4XorKey = key
|
||||
|
||||
// Stop traversal after finding a valid key
|
||||
return filepath.SkipAll
|
||||
})
|
||||
|
||||
if err != nil && err != filepath.SkipAll {
|
||||
return V4XorKey, fmt.Errorf("error scanning directory: %v", err)
|
||||
}
|
||||
|
||||
return V4XorKey, nil
|
||||
}
|
||||
|
||||
// Dat2ImageV4 processes WeChat v4 dat image files
|
||||
// WeChat v4 uses a combination of AES-ECB and XOR encryption
|
||||
func Dat2ImageV4(data []byte) ([]byte, string, error) {
|
||||
if len(data) < 15 {
|
||||
return nil, "", fmt.Errorf("data length is too short for WeChat v4 format: %d", len(data))
|
||||
}
|
||||
|
||||
// Parse dat file header:
|
||||
// - 6 bytes: 0x07085631 (dat file identifier)
|
||||
// - 4 bytes: int (little-endian) AES-ECB128 encryption length
|
||||
// - 4 bytes: int (little-endian) XOR encryption length
|
||||
// - 1 byte: 0x01 (unknown)
|
||||
|
||||
// Read AES encryption length
|
||||
aesEncryptLen := binary.LittleEndian.Uint32(data[6:10])
|
||||
// Read XOR encryption length
|
||||
xorEncryptLen := binary.LittleEndian.Uint32(data[10:14])
|
||||
|
||||
// Data after header
|
||||
fileData := data[15:]
|
||||
|
||||
// AES encrypted part (max 1KB)
|
||||
// Round up to multiple of 16 bytes for AES block size
|
||||
aesEncryptLen0 := (aesEncryptLen)/16*16 + 16
|
||||
if aesEncryptLen0 > uint32(len(fileData)) {
|
||||
aesEncryptLen0 = uint32(len(fileData))
|
||||
}
|
||||
|
||||
// Decrypt AES part
|
||||
aesDecryptedData, err := decryptAESECB(fileData[:aesEncryptLen0], []byte("cfcd208495d565ef"))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("AES decrypt error: %v", err)
|
||||
}
|
||||
|
||||
// Prepare result buffer
|
||||
var result []byte
|
||||
|
||||
// Add decrypted AES part (remove padding if necessary)
|
||||
if len(aesDecryptedData) > int(aesEncryptLen) {
|
||||
result = append(result, aesDecryptedData[:aesEncryptLen]...)
|
||||
} else {
|
||||
result = append(result, aesDecryptedData...)
|
||||
}
|
||||
|
||||
// Add unencrypted middle part
|
||||
middleStart := aesEncryptLen0
|
||||
middleEnd := uint32(len(fileData)) - xorEncryptLen
|
||||
if middleStart < middleEnd {
|
||||
result = append(result, fileData[middleStart:middleEnd]...)
|
||||
}
|
||||
|
||||
// Process XOR-encrypted part (file tail)
|
||||
if xorEncryptLen > 0 && middleEnd < uint32(len(fileData)) {
|
||||
xorData := fileData[middleEnd:]
|
||||
|
||||
// Apply XOR decryption using global key
|
||||
xorDecrypted := make([]byte, len(xorData))
|
||||
for i := range xorData {
|
||||
xorDecrypted[i] = xorData[i] ^ V4XorKey
|
||||
}
|
||||
|
||||
result = append(result, xorDecrypted...)
|
||||
}
|
||||
|
||||
// Identify image type from decrypted data
|
||||
imgType := ""
|
||||
for _, format := range Formats {
|
||||
if len(result) >= len(format.Header) && bytes.Equal(result[:len(format.Header)], format.Header) {
|
||||
imgType = format.Ext
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if imgType == "" {
|
||||
return nil, "", fmt.Errorf("unknown image type after decryption")
|
||||
}
|
||||
|
||||
return result, imgType, nil
|
||||
}
|
||||
|
||||
// decryptAESECB decrypts data using AES in ECB mode
|
||||
func decryptAESECB(data, key []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create AES cipher
|
||||
cipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure data length is a multiple of block size
|
||||
if len(data)%aes.BlockSize != 0 {
|
||||
return nil, fmt.Errorf("data length is not a multiple of block size")
|
||||
}
|
||||
|
||||
decrypted := make([]byte, len(data))
|
||||
|
||||
// ECB mode requires block-by-block decryption
|
||||
for bs, be := 0, aes.BlockSize; bs < len(data); bs, be = bs+aes.BlockSize, be+aes.BlockSize {
|
||||
cipher.Decrypt(decrypted[bs:be], data[bs:be])
|
||||
}
|
||||
|
||||
// Handle PKCS#7 padding
|
||||
padding := int(decrypted[len(decrypted)-1])
|
||||
if padding > 0 && padding <= aes.BlockSize {
|
||||
// Validate padding
|
||||
valid := true
|
||||
for i := len(decrypted) - padding; i < len(decrypted); i++ {
|
||||
if decrypted[i] != byte(padding) {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if valid {
|
||||
return decrypted[:len(decrypted)-padding], nil
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user