Files
WeChat-Channels-Video-File-…/decrypt_wechat_video_gui.py
Evil0ctal da298fbde7 Add Files
2025-10-15 05:44:59 -07:00

518 lines
18 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
微信视频号解密工具 - 图形界面版本
使用 tkinter 提供友好的图形界面
Author: Evil0ctal
GitHub: https://github.com/Evil0ctal/WeChat-Channels-Video-File-Decryption
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import threading
import os
import sys
from pathlib import Path
# 导入 CLI 模块的函数
from decrypt_wechat_video_cli import (
read_keystream_from_file,
read_keystream_from_string,
decrypt_video
)
class DecryptionGUI:
"""解密工具 GUI 主类"""
def __init__(self, root):
self.root = root
self.root.title("微信视频号解密工具")
self.root.geometry("800x700")
self.root.resizable(True, True)
# 设置应用图标(如果有的话)
try:
# 可以添加图标
pass
except:
pass
# 变量
self.keystream_file_var = tk.StringVar(value="keystream_131072_bytes.txt")
self.encrypted_file_var = tk.StringVar(value="wx_encrypted.mp4")
self.output_file_var = tk.StringVar(value="wx_decrypted.mp4")
self.keystream_data = None
self.is_decrypting = False
# 创建 UI
self.create_widgets()
# 检查默认密钥流文件
self.check_default_keystream()
def create_widgets(self):
"""创建界面组件"""
# 主容器
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置网格权重
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# 标题
title_label = ttk.Label(
main_frame,
text="🎬 微信视频号解密工具",
font=("Arial", 18, "bold")
)
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
# 第一部分:密钥流
row = 1
ttk.Label(main_frame, text="密钥流文件:", font=("Arial", 11)).grid(
row=row, column=0, sticky=tk.W, pady=5
)
ttk.Entry(main_frame, textvariable=self.keystream_file_var, width=50).grid(
row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5
)
ttk.Button(main_frame, text="选择文件", command=self.browse_keystream).grid(
row=row, column=2, pady=5
)
# 密钥流状态
row += 1
self.keystream_status_label = ttk.Label(
main_frame,
text="等待加载密钥流...",
foreground="gray"
)
self.keystream_status_label.grid(
row=row, column=1, sticky=tk.W, pady=(0, 10)
)
# 或者直接输入密钥流
row += 1
ttk.Label(main_frame, text="或粘贴密钥流:", font=("Arial", 11)).grid(
row=row, column=0, sticky=tk.W, pady=5
)
self.hex_input = scrolledtext.ScrolledText(
main_frame,
height=3,
width=50,
wrap=tk.WORD,
font=("Courier", 9)
)
self.hex_input.grid(row=row, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5, padx=5)
row += 1
ttk.Button(
main_frame,
text="从文本加载密钥流",
command=self.load_keystream_from_text
).grid(row=row, column=1, sticky=tk.W, pady=(0, 15))
# 分隔线
row += 1
ttk.Separator(main_frame, orient='horizontal').grid(
row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10
)
# 第二部分:加密文件
row += 1
ttk.Label(main_frame, text="加密视频文件:", font=("Arial", 11)).grid(
row=row, column=0, sticky=tk.W, pady=5
)
ttk.Entry(main_frame, textvariable=self.encrypted_file_var, width=50).grid(
row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5
)
ttk.Button(main_frame, text="选择文件", command=self.browse_encrypted).grid(
row=row, column=2, pady=5
)
# 第三部分:输出文件
row += 1
ttk.Label(main_frame, text="输出文件名:", font=("Arial", 11)).grid(
row=row, column=0, sticky=tk.W, pady=5
)
ttk.Entry(main_frame, textvariable=self.output_file_var, width=50).grid(
row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5
)
ttk.Button(main_frame, text="另存为", command=self.browse_output).grid(
row=row, column=2, pady=5
)
# 分隔线
row += 1
ttk.Separator(main_frame, orient='horizontal').grid(
row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10
)
# 解密按钮
row += 1
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=row, column=0, columnspan=3, pady=10)
self.decrypt_button = ttk.Button(
button_frame,
text="🚀 开始解密",
command=self.start_decryption,
width=20
)
self.decrypt_button.grid(row=0, column=0, padx=5)
self.open_folder_button = ttk.Button(
button_frame,
text="📂 打开文件夹",
command=self.open_output_folder,
state=tk.DISABLED,
width=20
)
self.open_folder_button.grid(row=0, column=1, padx=5)
ttk.Button(
button_frame,
text="❓ 帮助",
command=self.show_help,
width=15
).grid(row=0, column=2, padx=5)
# 日志输出区域
row += 1
ttk.Label(main_frame, text="操作日志:", font=("Arial", 11)).grid(
row=row, column=0, sticky=tk.W, pady=(10, 5)
)
row += 1
self.log_text = scrolledtext.ScrolledText(
main_frame,
height=15,
width=80,
wrap=tk.WORD,
font=("Courier", 9),
state=tk.DISABLED
)
self.log_text.grid(
row=row, column=0, columnspan=3,
sticky=(tk.W, tk.E, tk.N, tk.S),
pady=5
)
# 配置日志文本标签
self.log_text.tag_config("success", foreground="green")
self.log_text.tag_config("error", foreground="red")
self.log_text.tag_config("warning", foreground="orange")
self.log_text.tag_config("info", foreground="blue")
# 状态栏
row += 1
self.status_label = ttk.Label(
main_frame,
text="就绪",
relief=tk.SUNKEN,
anchor=tk.W
)
self.status_label.grid(
row=row, column=0, columnspan=3,
sticky=(tk.W, tk.E), pady=(10, 0)
)
# 配置行列权重使文本框可扩展
main_frame.rowconfigure(row - 1, weight=1)
# 欢迎信息
self.log("欢迎使用微信视频号解密工具!", "info")
self.log("作者: Evil0ctal", "info")
self.log("项目地址: https://github.com/Evil0ctal/WeChat-Channels-Video-File-Decryption\n", "info")
def log(self, message, tag=None):
"""添加日志"""
self.log_text.config(state=tk.NORMAL)
if tag:
self.log_text.insert(tk.END, message + "\n", tag)
else:
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
self.root.update_idletasks()
def update_status(self, message):
"""更新状态栏"""
self.status_label.config(text=message)
self.root.update_idletasks()
def check_default_keystream(self):
"""检查默认密钥流文件"""
keystream_file = self.keystream_file_var.get()
if os.path.exists(keystream_file):
self.keystream_data = read_keystream_from_file(keystream_file, verbose=False)
if self.keystream_data:
size_kb = len(self.keystream_data) / 1024
self.keystream_status_label.config(
text=f"✅ 已加载密钥流 ({size_kb:.2f} KB)",
foreground="green"
)
self.log(f"✅ 自动加载密钥流文件: {keystream_file}", "success")
else:
self.keystream_status_label.config(
text="❌ 密钥流文件格式错误",
foreground="red"
)
else:
self.keystream_status_label.config(
text="⚠️ 未找到默认密钥流文件",
foreground="orange"
)
def browse_keystream(self):
"""选择密钥流文件"""
filename = filedialog.askopenfilename(
title="选择密钥流文件",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if filename:
self.keystream_file_var.set(filename)
self.keystream_data = read_keystream_from_file(filename, verbose=False)
if self.keystream_data:
size_kb = len(self.keystream_data) / 1024
self.keystream_status_label.config(
text=f"✅ 已加载密钥流 ({size_kb:.2f} KB)",
foreground="green"
)
self.log(f"✅ 加载密钥流文件: {filename}", "success")
else:
self.keystream_status_label.config(
text="❌ 密钥流文件格式错误",
foreground="red"
)
self.log(f"❌ 加载密钥流失败: {filename}", "error")
def load_keystream_from_text(self):
"""从文本框加载密钥流"""
hex_string = self.hex_input.get("1.0", tk.END).strip()
if not hex_string:
messagebox.showwarning("警告", "请粘贴十六进制密钥流!")
return
self.keystream_data = read_keystream_from_string(hex_string, verbose=False)
if self.keystream_data:
size_kb = len(self.keystream_data) / 1024
self.keystream_status_label.config(
text=f"✅ 已加载密钥流 ({size_kb:.2f} KB)",
foreground="green"
)
self.log(f"✅ 从文本加载密钥流成功 ({size_kb:.2f} KB)", "success")
# 保存到文件
save_file = "keystream_131072_bytes.txt"
with open(save_file, 'w') as f:
f.write(hex_string)
self.log(f"✅ 密钥流已保存到: {save_file}", "info")
else:
self.keystream_status_label.config(
text="❌ 密钥流格式错误",
foreground="red"
)
self.log("❌ 密钥流格式错误,请检查输入", "error")
def browse_encrypted(self):
"""选择加密文件"""
filename = filedialog.askopenfilename(
title="选择加密视频文件",
filetypes=[("MP4 视频", "*.mp4"), ("所有文件", "*.*")]
)
if filename:
self.encrypted_file_var.set(filename)
self.log(f"✅ 选择加密文件: {filename}", "info")
def browse_output(self):
"""选择输出文件"""
filename = filedialog.asksaveasfilename(
title="保存解密视频",
defaultextension=".mp4",
filetypes=[("MP4 视频", "*.mp4"), ("所有文件", "*.*")]
)
if filename:
self.output_file_var.set(filename)
self.log(f"✅ 输出文件: {filename}", "info")
def start_decryption(self):
"""开始解密(在线程中执行)"""
if self.is_decrypting:
messagebox.showwarning("警告", "正在解密中,请等待...")
return
# 验证输入
if not self.keystream_data:
messagebox.showerror("错误", "请先加载密钥流文件或粘贴密钥流!")
return
encrypted_file = self.encrypted_file_var.get()
if not os.path.exists(encrypted_file):
messagebox.showerror("错误", f"加密文件不存在:\n{encrypted_file}")
return
output_file = self.output_file_var.get()
if not output_file:
messagebox.showerror("错误", "请指定输出文件名!")
return
# 禁用按钮
self.decrypt_button.config(state=tk.DISABLED)
self.is_decrypting = True
# 在新线程中执行解密
thread = threading.Thread(target=self.decrypt_worker, args=(encrypted_file, output_file))
thread.daemon = True
thread.start()
def decrypt_worker(self, encrypted_file, output_file):
"""解密工作线程"""
try:
self.log("\n" + "=" * 70, "info")
self.log("🚀 开始解密...", "info")
self.log("=" * 70 + "\n", "info")
self.update_status("正在解密...")
# 验证密钥流大小
keystream_size = len(self.keystream_data)
self.log(f"📊 密钥流大小: {keystream_size:,} bytes ({keystream_size / 1024:.2f} KB)", "info")
if keystream_size != 131072:
self.log(f"⚠️ 警告: 密钥流大小不是标准的 131072 bytes", "warning")
# 文件信息
file_size = os.path.getsize(encrypted_file)
self.log(f"📁 加密文件: {encrypted_file}", "info")
self.log(f"📊 文件大小: {file_size:,} bytes ({file_size / 1024 / 1024:.2f} MB)", "info")
self.log(f"💾 输出文件: {output_file}\n", "info")
# 调用 CLI 模块的解密函数
self.log("🔓 开始 XOR 解密...", "info")
success = decrypt_video(
encrypted_file,
self.keystream_data,
output_file,
verbose=False # 我们自己处理日志输出
)
# 验证结果
if success:
self.log("\n" + "=" * 70, "success")
self.log("🎉 解密成功!", "success")
self.log("=" * 70 + "\n", "success")
output_size = os.path.getsize(output_file)
self.log(f"✅ 解密文件: {output_file}", "success")
self.log(f"📊 文件大小: {output_size:,} bytes ({output_size / 1024 / 1024:.2f} MB)", "success")
self.log(f"📍 完整路径: {os.path.abspath(output_file)}\n", "info")
self.update_status("解密完成!")
# 启用打开文件夹按钮
self.open_folder_button.config(state=tk.NORMAL)
# 显示成功对话框
result = messagebox.askyesno(
"解密成功",
f"视频解密完成!\n\n文件: {output_file}\n\n是否打开文件所在文件夹?"
)
if result:
self.open_output_folder()
else:
self.log("\n" + "=" * 70, "warning")
self.log("⚠️ 解密完成,但可能存在问题", "warning")
self.log("=" * 70 + "\n", "warning")
self.log("请检查:", "warning")
self.log("1. 密钥流是否正确", "warning")
self.log("2. decode_key 是否匹配此视频", "warning")
self.log("3. 加密文件是否完整\n", "warning")
self.update_status("解密完成(可能有问题)")
messagebox.showwarning(
"警告",
"解密完成,但未检测到有效的 MP4 签名。\n请检查密钥流和文件是否正确。"
)
except Exception as e:
self.log(f"\n❌ 解密失败: {e}", "error")
self.update_status("解密失败")
messagebox.showerror("错误", f"解密失败:\n{e}")
finally:
# 恢复按钮
self.decrypt_button.config(state=tk.NORMAL)
self.is_decrypting = False
def open_output_folder(self):
"""打开输出文件所在文件夹"""
output_file = self.output_file_var.get()
if os.path.exists(output_file):
folder = os.path.dirname(os.path.abspath(output_file))
if sys.platform == "darwin": # macOS
os.system(f'open "{folder}"')
elif sys.platform == "win32": # Windows
os.system(f'explorer "{folder}"')
else: # Linux
os.system(f'xdg-open "{folder}"')
else:
messagebox.showwarning("警告", "输出文件不存在!")
def show_help(self):
"""显示帮助信息"""
help_text = """
微信视频号解密工具 - 使用说明
📝 使用步骤:
1⃣ 获取密钥流
方式一:使用浏览器生成
- 访问项目 GitHub Pages 或启动本地服务器
- 在页面中输入 decode_key
- 点击"生成密钥流"
- 点击"导出密钥流"下载文件
方式二:直接粘贴
- 将密钥流十六进制字符串粘贴到文本框
- 点击"从文本加载密钥流"
2⃣ 选择加密文件
- 点击"选择文件"选择加密的 MP4 视频
3⃣ 指定输出文件
- 输入输出文件名默认wx_decrypted.mp4
4⃣ 开始解密
- 点击"开始解密"按钮
- 等待解密完成
🔧 技术原理:
- 加密算法Isaac64 PRNG
- 加密范围:视频前 128 KB
- 解密方式XOR 运算
- 关键步骤:密钥流必须 reverse()
📌 注意事项:
- 每个视频有唯一的 decode_key
- 密钥流大小应为 131,072 bytes (128 KB)
- 解密后文件头应包含 'ftyp' 签名
🔗 项目地址:
https://github.com/Evil0ctal/WeChat-Channels-Video-File-Decryption
👨‍💻 作者Evil0ctal
"""
messagebox.showinfo("使用帮助", help_text)
def main():
"""主函数"""
root = tk.Tk()
app = DecryptionGUI(root)
root.mainloop()
if __name__ == "__main__":
main()