mirror of
https://github.com/teest114514/chatlog_alpha.git
synced 2026-03-22 19:00:19 +08:00
300 lines
7.1 KiB
Go
300 lines
7.1 KiB
Go
package chatlog
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/rs/zerolog/log"
|
||
"github.com/sjzar/chatlog/pkg/util/dat2img"
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
var (
|
||
batchDecryptCmd = &cobra.Command{
|
||
Use: "batch-decrypt",
|
||
Short: "批量解密已存在的.dat图片文件",
|
||
Long: `扫描指定目录下的所有.dat文件,并批量解密保存为普通图片格式`,
|
||
Example: `chatlog batch-decrypt --data-dir "E:\xwechat_files\wxid_sp86q2lhlm6f22_fffc" --data-key "66363764393236353832316536663530" --platform windows --version 4`,
|
||
Run: BatchDecrypt,
|
||
}
|
||
|
||
// 批量解密参数
|
||
batchDataDir string
|
||
batchDataKey string
|
||
batchImgKey string
|
||
batchPlatform string
|
||
batchVersion int
|
||
batchRecursive bool
|
||
batchDryRun bool
|
||
batchConcurrency int
|
||
)
|
||
|
||
func init() {
|
||
rootCmd.AddCommand(batchDecryptCmd)
|
||
|
||
// 必需参数
|
||
batchDecryptCmd.Flags().StringVar(&batchDataDir, "data-dir", "", "微信数据目录路径")
|
||
batchDecryptCmd.Flags().StringVar(&batchDataKey, "data-key", "", "数据密钥")
|
||
batchDecryptCmd.Flags().StringVar(&batchImgKey, "img-key", "", "图片密钥")
|
||
batchDecryptCmd.Flags().StringVar(&batchPlatform, "platform", "windows", "平台 (windows/darwin)")
|
||
batchDecryptCmd.Flags().IntVar(&batchVersion, "version", 4, "微信版本 (3/4)")
|
||
|
||
// 可选参数
|
||
batchDecryptCmd.Flags().BoolVar(&batchRecursive, "recursive", true, "递归扫描子目录")
|
||
batchDecryptCmd.Flags().BoolVar(&batchDryRun, "dry-run", false, "仅显示将要处理的文件,不实际解密")
|
||
batchDecryptCmd.Flags().IntVar(&batchConcurrency, "concurrency", 4, "并发处理数量")
|
||
|
||
// 标记必需参数
|
||
batchDecryptCmd.MarkFlagRequired("data-dir")
|
||
batchDecryptCmd.MarkFlagRequired("data-key")
|
||
}
|
||
|
||
func BatchDecrypt(cmd *cobra.Command, args []string) {
|
||
// 验证参数
|
||
if batchDataDir == "" {
|
||
log.Error().Msg("data-dir is required")
|
||
return
|
||
}
|
||
if batchDataKey == "" {
|
||
log.Error().Msg("data-key is required")
|
||
return
|
||
}
|
||
|
||
// 设置图片密钥(如果提供)
|
||
if batchImgKey != "" {
|
||
dat2img.SetAesKey(batchImgKey)
|
||
log.Info().Str("img_key", batchImgKey).Msg("使用提供的图片密钥")
|
||
} else {
|
||
// 如果没有提供图片密钥,尝试使用数据密钥
|
||
dat2img.SetAesKey(batchDataKey)
|
||
log.Info().Str("data_key", batchDataKey).Msg("使用数据密钥作为图片密钥")
|
||
}
|
||
|
||
// 设置XOR密钥(微信4.x版本)
|
||
if batchVersion == 4 {
|
||
log.Info().Msg("扫描并设置XOR密钥...")
|
||
_, err := dat2img.ScanAndSetXorKey(batchDataDir)
|
||
if err != nil {
|
||
log.Warn().Err(err).Msg("设置XOR密钥失败,将使用默认值")
|
||
}
|
||
}
|
||
|
||
log.Info().
|
||
Str("data_dir", batchDataDir).
|
||
Str("platform", batchPlatform).
|
||
Int("version", batchVersion).
|
||
Bool("recursive", batchRecursive).
|
||
Bool("dry_run", batchDryRun).
|
||
Int("concurrency", batchConcurrency).
|
||
Msg("开始批量解密")
|
||
|
||
// 扫描.dat文件
|
||
datFiles, err := scanDatFiles(batchDataDir, batchRecursive)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("扫描.dat文件失败")
|
||
return
|
||
}
|
||
|
||
if len(datFiles) == 0 {
|
||
log.Info().Msg("未找到任何.dat文件")
|
||
return
|
||
}
|
||
|
||
log.Info().Int("count", len(datFiles)).Msg("找到.dat文件")
|
||
|
||
// 执行批量解密
|
||
startTime := time.Now()
|
||
stats := processBatchDecrypt(datFiles, batchConcurrency, batchDryRun)
|
||
duration := time.Since(startTime)
|
||
|
||
// 输出统计信息
|
||
log.Info().
|
||
Int("total", stats.Total).
|
||
Int("success", stats.Success).
|
||
Int("failed", stats.Failed).
|
||
Int("skipped", stats.Skipped).
|
||
Dur("duration", duration).
|
||
Msg("批量解密完成")
|
||
|
||
if stats.Failed > 0 {
|
||
log.Warn().Int("failed_count", stats.Failed).Msg("部分文件解密失败,请检查日志")
|
||
}
|
||
}
|
||
|
||
// 扫描.dat文件
|
||
func scanDatFiles(dataDir string, recursive bool) ([]string, error) {
|
||
var datFiles []string
|
||
|
||
walkFunc := func(path string, info os.FileInfo, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 跳过目录
|
||
if info.IsDir() {
|
||
return nil
|
||
}
|
||
|
||
// 检查是否为.dat文件
|
||
if strings.HasSuffix(strings.ToLower(path), ".dat") {
|
||
datFiles = append(datFiles, path)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
if recursive {
|
||
err := filepath.Walk(dataDir, walkFunc)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
} else {
|
||
entries, err := os.ReadDir(dataDir)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if !entry.IsDir() && strings.HasSuffix(strings.ToLower(entry.Name()), ".dat") {
|
||
datFiles = append(datFiles, filepath.Join(dataDir, entry.Name()))
|
||
}
|
||
}
|
||
}
|
||
|
||
return datFiles, nil
|
||
}
|
||
|
||
// 批量解密统计信息
|
||
type BatchStats struct {
|
||
Total int
|
||
Success int
|
||
Failed int
|
||
Skipped int
|
||
}
|
||
|
||
// 处理批量解密
|
||
func processBatchDecrypt(datFiles []string, concurrency int, dryRun bool) BatchStats {
|
||
stats := BatchStats{
|
||
Total: len(datFiles),
|
||
}
|
||
|
||
// 创建信号量控制并发
|
||
semaphore := make(chan struct{}, concurrency)
|
||
results := make(chan BatchResult, len(datFiles))
|
||
|
||
// 启动工作协程
|
||
for _, datFile := range datFiles {
|
||
go func(file string) {
|
||
semaphore <- struct{}{} // 获取信号量
|
||
defer func() { <-semaphore }() // 释放信号量
|
||
|
||
result := processSingleFile(file, dryRun)
|
||
results <- result
|
||
}(datFile)
|
||
}
|
||
|
||
// 收集结果
|
||
for i := 0; i < len(datFiles); i++ {
|
||
result := <-results
|
||
switch result.Status {
|
||
case "success":
|
||
stats.Success++
|
||
case "failed":
|
||
stats.Failed++
|
||
case "skipped":
|
||
stats.Skipped++
|
||
}
|
||
|
||
// 输出进度
|
||
if (i+1)%10 == 0 || i == len(datFiles)-1 {
|
||
log.Info().
|
||
Int("processed", i+1).
|
||
Int("total", len(datFiles)).
|
||
Int("success", stats.Success).
|
||
Int("failed", stats.Failed).
|
||
Int("skipped", stats.Skipped).
|
||
Msg("批量解密进度")
|
||
}
|
||
}
|
||
|
||
return stats
|
||
}
|
||
|
||
// 单个文件处理结果
|
||
type BatchResult struct {
|
||
File string
|
||
Status string // success, failed, skipped
|
||
Error error
|
||
}
|
||
|
||
// 处理单个文件
|
||
func processSingleFile(datFile string, dryRun bool) BatchResult {
|
||
// 生成输出文件路径
|
||
outputPath := strings.TrimSuffix(datFile, filepath.Ext(datFile))
|
||
|
||
// 检查是否已存在解密文件
|
||
extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".mp4"}
|
||
for _, ext := range extensions {
|
||
if _, err := os.Stat(outputPath + ext); err == nil {
|
||
return BatchResult{
|
||
File: datFile,
|
||
Status: "skipped",
|
||
}
|
||
}
|
||
}
|
||
|
||
if dryRun {
|
||
log.Debug().Str("file", datFile).Msg("将处理文件")
|
||
return BatchResult{
|
||
File: datFile,
|
||
Status: "success",
|
||
}
|
||
}
|
||
|
||
// 读取.dat文件
|
||
data, err := os.ReadFile(datFile)
|
||
if err != nil {
|
||
return BatchResult{
|
||
File: datFile,
|
||
Status: "failed",
|
||
Error: err,
|
||
}
|
||
}
|
||
|
||
// 解密文件
|
||
decryptedData, ext, err := dat2img.Dat2Image(data)
|
||
if err != nil {
|
||
log.Debug().Err(err).Str("file", datFile).Msg("解密失败")
|
||
return BatchResult{
|
||
File: datFile,
|
||
Status: "failed",
|
||
Error: err,
|
||
}
|
||
}
|
||
|
||
// 保存解密后的文件
|
||
outputPath = outputPath + "." + ext
|
||
err = os.WriteFile(outputPath, decryptedData, 0644)
|
||
if err != nil {
|
||
return BatchResult{
|
||
File: datFile,
|
||
Status: "failed",
|
||
Error: err,
|
||
}
|
||
}
|
||
|
||
log.Debug().
|
||
Str("dat_file", datFile).
|
||
Str("output_file", outputPath).
|
||
Str("format", ext).
|
||
Int("size", len(decryptedData)).
|
||
Msg("文件解密成功")
|
||
|
||
return BatchResult{
|
||
File: datFile,
|
||
Status: "success",
|
||
}
|
||
}
|