Files
chatlog_alpha/cmd/chatlog/cmd_batch_decrypt.go
T
lx1056758714-glitch 3d04a7f3eb 同步本地代码
2025-12-13 17:30:38 +08:00

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",
}
}