diff --git a/.gitignore b/.gitignore index d8cc5fe..5379ef4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ TEST app/data/avatar app/data/image2 app/data/emoji +app/DataBase/Msg/* *.db *.pyc *.log diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 0dc18c4..4978b11 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,13 +4,15 @@ - - @@ -680,7 +672,9 @@ - @@ -699,24 +693,9 @@ file://$PROJECT_DIR$/app/decrypt/decrypt.py - 105 + 103 - - file://$PROJECT_DIR$/app/person.py - 100 - - - file://$PROJECT_DIR$/app/person.py - 98 - - - file://$PROJECT_DIR$/app/person.py - 99 - diff --git a/app/DataBase/hard_link.py b/app/DataBase/hard_link.py new file mode 100644 index 0000000..34236ec --- /dev/null +++ b/app/DataBase/hard_link.py @@ -0,0 +1,78 @@ +import binascii +import os.path +import sqlite3 +import threading +import xml.etree.ElementTree as ET + +lock = threading.Lock() +DB = None +cursor = None +db_path = "./app/Database/Msg/HardLinkImage.db" +root_path = 'FileStorage/MsgAttach/' +if os.path.exists(db_path): + DB = sqlite3.connect(db_path, check_same_thread=False) + # '''创建游标''' + cursor = DB.cursor() + + +def init_database(): + global DB + global cursor + if not DB: + if os.path.exists(db_path): + DB = sqlite3.connect(db_path, check_same_thread=False) + # '''创建游标''' + cursor = DB.cursor() + + +def get_image_by_md5(md5: bytes): + sql = ''' + select Md5Hash,MD5,FileName,HardLinkImageID.Dir as DirName1,HardLinkImageID2.Dir as DirName2 + from HardLinkImageAttribute + join HardLinkImageID on HardLinkImageAttribute.DirID1 = HardLinkImageID.DirID + join HardLinkImageID as HardLinkImageID2 on HardLinkImageAttribute.DirID2 = HardLinkImageID2.DirID + where MD5 = ?; + ''' + try: + lock.acquire(True) + cursor.execute(sql, [md5, ]) + result = cursor.fetchone() + return result + finally: + lock.release() + + +def get_md5_from_xml(content): + # 解析XML + root = ET.fromstring(content) + # 提取md5的值 + md5_value = root.find(".//img").get("md5") + # print(md5_value) + return md5_value + + +def get_image(content, thumb=False): + md5 = get_md5_from_xml(content) + # md5 = 'bc37a58c32cb203ee9ac587b068e5853' + result = get_image_by_md5(binascii.unhexlify(md5)) + if result: + # print(result) + dir1 = result[3] + dir2 = result[4] + data_image = result[2] + dir0 = 'Thumb' if thumb else 'Image' + dat_image = os.path.join(root_path, dir1, dir0, dir2, data_image) + return dat_image + + +# 6b02292eecea118f06be3a5b20075afc_t + +if __name__ == '__main__': + msg_root_path = './Msg/' + db_path = "./Msg/HardLinkImage.db" + init_database() + content = '''\n\t\n\t\n\t\n\n''' + print(get_image(content)) + print(get_image(content, thumb=False)) + result = get_md5_from_xml(content) + print(result) diff --git a/app/DataBase/output_pc.py b/app/DataBase/output_pc.py index 34bf547..c1e3940 100644 --- a/app/DataBase/output_pc.py +++ b/app/DataBase/output_pc.py @@ -6,6 +6,9 @@ from PyQt5.QtCore import pyqtSignal, QThread from . import msg from ..log import log +if not os.path.exists('./data/聊天记录'): + os.mkdir('./data/聊天记录') + class Output(QThread): """ diff --git a/app/components/bubble_message.py b/app/components/bubble_message.py index 7c982c3..fc26e4b 100644 --- a/app/components/bubble_message.py +++ b/app/components/bubble_message.py @@ -8,7 +8,7 @@ from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QVBoxLayo class MessageType: Text = 1 - Image = 2 + Image = 3 class TextMessage(QLabel): @@ -104,20 +104,28 @@ class OpenImageThread(QThread): class ImageMessage(QLabel): - def __init__(self, avatar, parent=None): + def __init__(self, image, image_link='', max_width=480, max_height=720, parent=None): + """ + param:image 图像路径或者QPixmap对象 + param:image_link='' 点击图像打开的文件路径 + """ super().__init__(parent) self.image = QLabel(self) - if isinstance(avatar, str): - self.setPixmap(QPixmap(avatar)) - self.image_path = avatar - elif isinstance(avatar, QPixmap): - self.setPixmap(avatar) - self.setMaximumWidth(480) - self.setMaximumHeight(720) - self.setScaledContents(True) + + if isinstance(image, str): + self.setPixmap(QPixmap(image)) + self.image_path = image + elif isinstance(image, QPixmap): + self.setPixmap(image) + if image_link: + self.image_path = image_link + self.setMaximumWidth(max_width) + self.setMaximumHeight(max_height) + # self.setScaledContents(True) def mousePressEvent(self, event): if event.buttons() == Qt.LeftButton: # 左键按下 + print('打开图像', self.image_path) self.open_image_thread = OpenImageThread(self.image_path) self.open_image_thread.start() diff --git a/app/data/icons/404.png b/app/data/icons/404.png new file mode 100644 index 0000000..d16f272 Binary files /dev/null and b/app/data/icons/404.png differ diff --git a/app/decrypt/dat2pic.py b/app/decrypt/dat2pic.py new file mode 100644 index 0000000..f66abd6 --- /dev/null +++ b/app/decrypt/dat2pic.py @@ -0,0 +1,91 @@ + +import os +# 图片字节头信息, +# [0][1]为jpg头信息, +# [2][3]为png头信息, +# [4][5]为gif头信息 +pic_head = [0xff, 0xd8, 0x89, 0x50, 0x47, 0x49] +# 解密码 +decode_code = 0 + + +def get_code(file_path): + """ + 自动判断文件类型,并获取dat文件解密码 + :param file_path: dat文件路径 + :return: 如果文件为jpg/png/gif格式,则返回解密码,否则返回-1 + """ + if os.path.isdir(file_path): + return -1, -1 + if file_path[-4:] != ".dat": + return -1, -1 + dat_file = open(file_path, "rb") + dat_read = dat_file.read(2) + print(dat_read) + head_index = 0 + while head_index < len(pic_head): + # 使用第一个头信息字节来计算加密码 + # 第二个字节来验证解密码是否正确 + code = dat_read[0] ^ pic_head[head_index] + idf_code = dat_read[1] ^ code + head_index = head_index + 1 + if idf_code == pic_head[head_index]: + dat_file.close() + return head_index, code + head_index = head_index + 1 + + print("not jpg, png, gif") + return -1, -1 + + +def decode_dat(file_path, out_path): + """ + 解密文件,并生成图片 + :param file_path: dat文件路径 + :return: 无 + """ + file_type, decode_code = get_code(file_path) + + if decode_code == -1: + return + if file_type == 1: + pic_name = file_path.split("\\")[-1][:-4] + ".jpg" + elif file_type == 3: + pic_name = file_path[:-4] + ".png" + elif file_type == 5: + pic_name = file_path[:-4] + ".gif" + else: + pic_name = file_path[:-4] + ".jpg" + + dat_file = open(file_path, "rb") + file_outpath = os.path.join(out_path, pic_name) + print(pic_name) + print(file_outpath) + pic_write = open(file_outpath, "wb") + for dat_data in dat_file: + for dat_byte in dat_data: + pic_data = dat_byte ^ decode_code + pic_write.write(bytes([pic_data])) + print(pic_name + "完成") + dat_file.close() + pic_write.close() + + +def find_datfile(dir_path, out_path): + """ + 获取dat文件目录下所有的文件 + :param dir_path: dat文件目录 + :return: 无 + """ + files_list = os.listdir(dir_path) + for file_name in files_list: + file_path = dir_path + "\\" + file_name + decode_dat(file_path, out_path) + + +if __name__ == "__main__": + path = r"D:\download\wechat\WeChat Files\wxid_0o18ef858vnu22\FileStorage\MsgAttach\febd8caf62dd403a7212aef63fd55910\Thumb\2023-11" + outpath = "D:\\test" + if not os.path.exists(outpath): + os.mkdir(outpath) + find_datfile(path, outpath) \ No newline at end of file diff --git a/app/decrypt/get_wx_info.py b/app/decrypt/get_wx_info.py index 395110d..52f844f 100644 --- a/app/decrypt/get_wx_info.py +++ b/app/decrypt/get_wx_info.py @@ -65,7 +65,7 @@ def read_info(version_list, is_logging=False): if len(wechat_process) == 0: error = "[-] WeChat No Run" if is_logging: print(error) - return error + return -1 for process in wechat_process: tmp_rd = {} @@ -76,8 +76,9 @@ def read_info(version_list, is_logging=False): bias_list = version_list.get(tmp_rd['version'], None) if not isinstance(bias_list, list): error = f"[-] WeChat Current Version {tmp_rd['version']} Is Not Supported" - if is_logging: print(error) - return error + if is_logging: + print(error) + return -2 wechat_base_address = 0 for module in process.memory_maps(grouped=False): @@ -86,8 +87,9 @@ def read_info(version_list, is_logging=False): break if wechat_base_address == 0: error = f"[-] WeChat WeChatWin.dll Not Found" - if is_logging: print(error) - return error + if is_logging: + print(error) + return -3 Handle = ctypes.windll.kernel32.OpenProcess(0x1F0FFF, False, process.pid) @@ -128,7 +130,7 @@ def get_info(): with open(VERSION_LIST_PATH, "r", encoding="utf-8") as f: VERSION_LIST = json.load(f) - result = read_info(VERSION_LIST) # 读取微信信息 + result = read_info(VERSION_LIST, True) # 读取微信信息 return result diff --git a/app/person.py b/app/person.py index 32c8dd4..e52aa89 100644 --- a/app/person.py +++ b/app/person.py @@ -55,6 +55,10 @@ def singleton(cls): class MePC: def __init__(self): self.avatar = QPixmap(Icon.Default_avatar_path) + self.wxid = '' + self.wx_dir = '' + self.name = '' + self.mobile = '' def set_avatar(self, img_bytes): if not img_bytes: diff --git a/app/ui_pc/Icon.py b/app/ui_pc/Icon.py index 88a0f0e..9a569b1 100644 --- a/app/ui_pc/Icon.py +++ b/app/ui_pc/Icon.py @@ -3,6 +3,7 @@ from PyQt5.QtGui import QIcon class Icon: Default_avatar_path = './app/data/icons/default_avatar.svg' + Default_image_path = './app/data/icons/404.png' MainWindow_Icon = QIcon('./app/data/icons/logo.svg') Default_avatar = QIcon(Default_avatar_path) Output = QIcon('./app/data/icons/output.svg') diff --git a/app/ui_pc/chat/chat_info.py b/app/ui_pc/chat/chat_info.py index 96b3d0b..d5a7958 100644 --- a/app/ui_pc/chat/chat_info.py +++ b/app/ui_pc/chat/chat_info.py @@ -1,9 +1,12 @@ +import traceback + from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout -from app.DataBase import msg +from app.DataBase import msg, hard_link from app.components.bubble_message import BubbleMessage, ChatWidget, Notice from app.person import MePC +from app.util import get_abs_path class ChatInfo(QWidget): @@ -84,7 +87,7 @@ class ChatInfo(QWidget): is_send = message[4] avatar = MePC().avatar if is_send else self.contact.avatar timestamp = message[5] - if type_ == 1 or type_ == 3: + if type_ == 1: if self.is_5_min(timestamp): time_message = Notice(self.last_str_time) self.last_str_time = str_time @@ -96,8 +99,23 @@ class ChatInfo(QWidget): is_send ) self.chat_window.add_message_item(bubble_message, 0) + elif type_ == 3: + if self.is_5_min(timestamp): + time_message = Notice(self.last_str_time) + self.last_str_time = str_time + self.chat_window.add_message_item(time_message, 0) + image_path = hard_link.get_image(content=str_content, thumb=False) + image_path = get_abs_path(image_path) + bubble_message = BubbleMessage( + image_path, + avatar, + type_, + is_send + ) + self.chat_window.add_message_item(bubble_message, 0) except: print(message) + traceback.print_exc() class ShowChatThread(QThread): diff --git a/app/ui_pc/mainview.py b/app/ui_pc/mainview.py index 4826281..3448422 100644 --- a/app/ui_pc/mainview.py +++ b/app/ui_pc/mainview.py @@ -85,6 +85,10 @@ class MainWinController(QMainWindow, mainwindow.Ui_MainWindow): wxid = dic.get('wxid') if wxid: me = MePC() + me.wxid = dic.get('wxid') + me.name = dic.get('name') + me.mobile = dic.get('mobile') + me.wx_dir = dic.get('wx_dir') self.set_my_info(wxid) else: QMessageBox.information( @@ -158,11 +162,6 @@ class MainWinController(QMainWindow, mainwindow.Ui_MainWindow): self.avatar.scaled(60, 60) me = MePC() me.set_avatar(img_bytes) - dic = { - 'wxid': wxid - } - with open('./app/data/info.json', 'w', encoding='utf-8') as f: - f.write(json.dumps(dic)) self.myavatar.setScaledContents(True) self.myavatar.setPixmap(self.avatar) diff --git a/app/ui_pc/tool/pc_decrypt/decryptUi.py b/app/ui_pc/tool/pc_decrypt/decryptUi.py index 3fed138..d4b0b8d 100644 --- a/app/ui_pc/tool/pc_decrypt/decryptUi.py +++ b/app/ui_pc/tool/pc_decrypt/decryptUi.py @@ -53,14 +53,14 @@ class Ui_Dialog(object): self.lineEdit = QtWidgets.QLineEdit(Dialog) self.lineEdit.setStyleSheet("background:transparent;\n" "\n" - "border-radius:5px;\n" - "border-top: 0px solid #b2e281;\n" - "border-bottom: 2px solid black;\n" - "border-right: 0px solid #b2e281;\n" - "border-left: 0px solid #b2e281;\n" + " border-radius:5px;\n" + " border-top: 0px solid #b2e281;\n" + " border-bottom: 2px solid black;\n" + " border-right: 0px solid #b2e281;\n" + " border-left: 0px solid #b2e281;\n" "\n" - " \n" - "border-style:outset\n" + "\n" + " border-style:outset\n" " ") self.lineEdit.setFrame(False) self.lineEdit.setObjectName("lineEdit") @@ -72,7 +72,7 @@ class Ui_Dialog(object): self.label_6.setObjectName("label_6") self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1) self.label_key = QtWidgets.QLabel(Dialog) - self.label_key.setMaximumSize(QtCore.QSize(200, 16777215)) + self.label_key.setMaximumSize(QtCore.QSize(400, 16777215)) self.label_key.setText("") self.label_key.setObjectName("label_key") self.gridLayout.addWidget(self.label_key, 5, 1, 1, 1) @@ -101,7 +101,7 @@ class Ui_Dialog(object): self.label_8.setObjectName("label_8") self.gridLayout.addWidget(self.label_8, 6, 0, 1, 1) self.label_db_dir = QtWidgets.QLabel(Dialog) - self.label_db_dir.setMaximumSize(QtCore.QSize(200, 300)) + self.label_db_dir.setMaximumSize(QtCore.QSize(400, 300)) self.label_db_dir.setText("") self.label_db_dir.setObjectName("label_db_dir") self.gridLayout.addWidget(self.label_db_dir, 6, 1, 1, 1) diff --git a/app/ui_pc/tool/pc_decrypt/decryptUi.ui b/app/ui_pc/tool/pc_decrypt/decryptUi.ui index dff82c5..9049e6c 100644 --- a/app/ui_pc/tool/pc_decrypt/decryptUi.ui +++ b/app/ui_pc/tool/pc_decrypt/decryptUi.ui @@ -127,7 +127,7 @@ - 200 + 400 16777215 @@ -189,7 +189,7 @@ - 200 + 400 300 diff --git a/app/ui_pc/tool/pc_decrypt/pc_decrypt.py b/app/ui_pc/tool/pc_decrypt/pc_decrypt.py index e7226e6..99cbb33 100644 --- a/app/ui_pc/tool/pc_decrypt/pc_decrypt.py +++ b/app/ui_pc/tool/pc_decrypt/pc_decrypt.py @@ -1,3 +1,4 @@ +import json import os.path import time import traceback @@ -35,11 +36,13 @@ class DecryptControl(QWidget, decryptUi.Ui_Dialog): def get_info(self): try: result = get_wx_info.get_info() + print(result) if result == -1: QMessageBox.critical(self, "错误", "请登录微信") elif result == -2: QMessageBox.critical(self, "错误", "微信版本不匹配\n请更新微信版本为:3.9.8.15") - # print(result) + elif result == -3: + QMessageBox.critical(self, "错误", "WeChat WeChatWin.dll Not Found") else: self.ready = True self.info = result[0] @@ -52,7 +55,7 @@ class DecryptControl(QWidget, decryptUi.Ui_Dialog): self.lineEdit.setFocus() self.checkBox.setChecked(True) self.get_wxidSignal.emit(self.info['wxid']) - if self.wx_dir and os.path.exists(os.path.join(self.wx_dir, self.info['wxid'])): + if self.wx_dir and os.path.exists(os.path.join(self.wx_dir)): self.label_ready.setText('已就绪') except Exception as e: print(e) @@ -71,12 +74,16 @@ class DecryptControl(QWidget, decryptUi.Ui_Dialog): directory = QtWidgets.QFileDialog.getExistingDirectory( self, "选取微信安装目录——能看到All Users文件夹", "C:/") # 起始路径 - if directory: - self.label_db_dir.setText(directory) - self.wx_dir = directory - self.checkBox_2.setChecked(True) - if self.ready: - self.label_ready.setText('已就绪') + db_dir = os.path.join(directory, 'Msg') + if not os.path.exists(db_dir): + QMessageBox.critical(self, "错误", "文件夹选择错误\n一般以wxid_xxx结尾") + return + + self.label_db_dir.setText(directory) + self.wx_dir = directory + self.checkBox_2.setChecked(True) + if self.ready: + self.label_ready.setText('已就绪') def decrypt(self): if not self.ready: @@ -88,11 +95,12 @@ class DecryptControl(QWidget, decryptUi.Ui_Dialog): if self.lineEdit.text() == 'None': QMessageBox.critical(self, "错误", "请填入wxid") return + db_dir = os.path.join(self.wx_dir, 'Msg') if self.ready: - if not os.path.exists(os.path.join(self.wx_dir, self.info['wxid'])): - QMessageBox.critical(self, "错误", "文件夹选择错误\n一般以WeChat Files结尾") + if not os.path.exists(db_dir): + QMessageBox.critical(self, "错误", "文件夹选择错误\n一般以wxid_xxx结尾") return - db_dir = os.path.join(self.wx_dir, self.info['wxid'], 'Msg') + self.thread2 = DecryptThread(db_dir, self.info['key']) self.thread2.maxNumSignal.connect(self.setProgressBarMaxNum) self.thread2.signal.connect(self.progressBar_view) @@ -103,6 +111,7 @@ class DecryptControl(QWidget, decryptUi.Ui_Dialog): # print("enter clicked") # 中间可以添加处理逻辑 # QMessageBox.about(self, "解密成功", "数据库文件存储在app/DataBase/Msg文件夹下") + self.DecryptSignal.emit('ok') # self.close() @@ -121,6 +130,14 @@ class DecryptControl(QWidget, decryptUi.Ui_Dialog): def btnExitClicked(self): # print("Exit clicked") + dic = { + 'wxid': self.info['wxid'], + 'wx_dir': self.wx_dir, + 'name': self.info['name'], + 'mobile': self.info['mobile'] + } + with open('./app/data/info.json', 'w', encoding='utf-8') as f: + f.write(json.dumps(dic)) self.DecryptSignal.emit('ok') self.close() diff --git a/app/util/__init__.py b/app/util/__init__.py index e69de29..ed49344 100644 --- a/app/util/__init__.py +++ b/app/util/__init__.py @@ -0,0 +1 @@ +from .path import get_abs_path diff --git a/app/util/path.py b/app/util/path.py new file mode 100644 index 0000000..ab32b28 --- /dev/null +++ b/app/util/path.py @@ -0,0 +1,11 @@ +import os + +from app.person import MePC + + +def get_abs_path(path): + return os.path.join(os.getcwd(), 'app/data/icons/404.png') + if path: + return os.path.join(MePC().wx_dir, path) + else: + return os.path.join(os.getcwd(), 'app/data/icons/404.png') diff --git a/doc/images/path_select.png b/doc/images/path_select.png new file mode 100644 index 0000000..b8e7b1d Binary files /dev/null and b/doc/images/path_select.png differ diff --git a/doc/电脑端使用教程.md b/doc/电脑端使用教程.md index b7aaf05..9071ee1 100644 --- a/doc/电脑端使用教程.md +++ b/doc/电脑端使用教程.md @@ -40,7 +40,8 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple ![](./images/setting.png) - 点击**设置微信路径**按钮,选择该文件夹路径 + 点击**设置微信路径**按钮,选择该文件夹路径下的带有wxid_xxx的路径 + ![](./images/path_select.png) 5. 获取到密钥和微信路径之后点击开始解密 diff --git a/readme.md b/readme.md index 290ded6..b42b8a9 100644 --- a/readme.md +++ b/readme.md @@ -88,7 +88,9 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 可以到微信->设置->文件管理查看 ![](./doc/images/setting.png) - 点击**设置微信路径**按钮,选择该文件夹路径 + 点击**设置微信路径**按钮,选择该文件夹路径下的带有wxid_xxx的路径 + ![](./doc/images/path_select.png) + 5. 获取到密钥和微信路径之后点击开始解密 6. 解密后的数据库文件保存在./app/DataBase/Msg路径下