Skip to content

Commit ddc5c4a

Browse files
committed
fix(decrypt): 加强解密结果校验并过滤内部数据库
- 新增可用 SQLite 校验,解密失败时返回更明确提示并清理无效输出 - 统一过滤 key_info、FTS 索引库和内部缓存库,修正数据库扫描与账号统计 - 补充解密流和数据库过滤相关测试
1 parent d0d51c0 commit ddc5c4a

File tree

11 files changed

+462
-30
lines changed

11 files changed

+462
-30
lines changed

src/wechat_decrypt_tool/chat_helpers.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from .app_paths import get_output_databases_dir
1616
from .logging_config import get_logger
17-
from .sqlite_diagnostics import collect_sqlite_diagnostics, format_sqlite_diagnostics
17+
from .sqlite_diagnostics import collect_sqlite_diagnostics, format_sqlite_diagnostics, is_usable_sqlite_db
1818

1919
try:
2020
import zstandard as zstd # type: ignore
@@ -29,13 +29,7 @@
2929

3030

3131
def _is_valid_decrypted_sqlite(path: Path) -> bool:
32-
try:
33-
if not path.exists() or (not path.is_file()):
34-
return False
35-
with path.open("rb") as f:
36-
return f.read(len(_SQLITE_HEADER)) == _SQLITE_HEADER
37-
except Exception:
38-
return False
32+
return is_usable_sqlite_db(path)
3933

4034

4135
def _list_decrypted_accounts() -> list[str]:
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
6+
_IGNORED_SOURCE_DATABASE_NAMES = frozenset({"key_info.db"})
7+
_INDEX_DATABASE_NAMES = frozenset({"chat_search_index.db", "chat_search_index.tmp.db"})
8+
_INDEX_DATABASE_SUFFIXES = ("_fts.db",)
9+
_INTERNAL_OUTPUT_DATABASE_NAMES = frozenset(
10+
{
11+
"chat_search_index.db",
12+
"chat_search_index.tmp.db",
13+
"session_last_message.db",
14+
}
15+
)
16+
17+
18+
def normalize_database_file_name(file_name: str | Path) -> str:
19+
return Path(str(file_name or "")).name.strip().lower()
20+
21+
22+
def is_index_database_file(file_name: str | Path) -> bool:
23+
lower_name = normalize_database_file_name(file_name)
24+
if not lower_name:
25+
return False
26+
if lower_name in _INDEX_DATABASE_NAMES:
27+
return True
28+
return lower_name.endswith(_INDEX_DATABASE_SUFFIXES)
29+
30+
31+
def should_skip_source_database(file_name: str | Path) -> bool:
32+
lower_name = normalize_database_file_name(file_name)
33+
if not lower_name:
34+
return True
35+
if lower_name in _IGNORED_SOURCE_DATABASE_NAMES:
36+
return True
37+
return is_index_database_file(lower_name)
38+
39+
40+
def should_include_in_database_count(file_name: str | Path) -> bool:
41+
lower_name = normalize_database_file_name(file_name)
42+
if not lower_name.endswith(".db"):
43+
return False
44+
if should_skip_source_database(lower_name):
45+
return False
46+
if lower_name in _INTERNAL_OUTPUT_DATABASE_NAMES:
47+
return False
48+
return True
49+
50+
51+
def list_countable_database_names(account_dir: Path) -> list[str]:
52+
if not account_dir.exists():
53+
return []
54+
55+
db_files = [
56+
path.name
57+
for path in account_dir.glob("*.db")
58+
if path.is_file() and should_include_in_database_count(path.name)
59+
]
60+
db_files.sort()
61+
return db_files

src/wechat_decrypt_tool/media_helpers.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from .app_paths import get_output_databases_dir
2020
from .logging_config import get_logger
21+
from .sqlite_diagnostics import is_usable_sqlite_db
2122

2223
logger = get_logger(__name__)
2324

@@ -28,13 +29,7 @@
2829

2930

3031
def _is_valid_decrypted_sqlite(path: Path) -> bool:
31-
try:
32-
if not path.exists() or (not path.is_file()):
33-
return False
34-
with path.open("rb") as f:
35-
return f.read(len(_SQLITE_HEADER)) == _SQLITE_HEADER
36-
except Exception:
37-
return False
32+
return is_usable_sqlite_db(path)
3833

3934

4035
def _list_decrypted_accounts() -> list[str]:

src/wechat_decrypt_tool/routers/chat.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from ..media_helpers import _resolve_account_db_storage_dir, _try_find_decrypted_resource
7171
from .. import chat_edit_store
7272
from ..app_paths import get_output_dir
73+
from ..database_filters import list_countable_database_names
7374
from ..key_store import remove_account_keys_from_store
7475
from ..path_fix import PathFixRoute
7576
from ..session_last_message import (
@@ -3892,7 +3893,7 @@ async def list_chat_accounts():
38923893
@router.get("/api/chat/account_info", summary="获取当前账号信息")
38933894
def get_chat_account_info(account: Optional[str] = None):
38943895
account_dir = _resolve_account_dir(account)
3895-
db_files = sorted([p.name for p in account_dir.glob("*.db") if p.is_file()])
3896+
db_files = list_countable_database_names(account_dir)
38963897

38973898
session_db = account_dir / "session.db"
38983899
session_updated_at = 0

src/wechat_decrypt_tool/routers/decrypt.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
from ..logging_config import get_logger
1717
from ..path_fix import PathFixRoute
1818
from ..key_store import upsert_account_keys_in_store
19-
from ..wechat_decrypt import WeChatDatabaseDecryptor, decrypt_wechat_databases, scan_account_databases_from_path
19+
from ..wechat_decrypt import (
20+
WeChatDatabaseDecryptor,
21+
build_decrypt_summary_message,
22+
decrypt_wechat_databases,
23+
scan_account_databases_from_path,
24+
)
2025

2126
logger = get_logger(__name__)
2227

@@ -463,7 +468,11 @@ async def generate_progress():
463468
"success_count": success_count,
464469
"failure_count": total_databases - success_count,
465470
"output_directory": str(base_output_dir.absolute()),
466-
"message": f"解密完成: 成功 {success_count}/{total_databases}",
471+
"message": build_decrypt_summary_message(
472+
success_count=success_count,
473+
total_databases=total_databases,
474+
diagnostic_warning_count=diagnostic_warning_count,
475+
),
467476
"processed_files": processed_files,
468477
"failed_files": failed_files,
469478
"account_results": account_results,

src/wechat_decrypt_tool/sqlite_diagnostics.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,40 @@ def collect_sqlite_diagnostics(
123123
return diagnostics
124124

125125

126+
def is_usable_sqlite_db(path: str | Path) -> bool:
127+
db_path = Path(path)
128+
if not db_path.exists() or (not db_path.is_file()):
129+
return False
130+
131+
try:
132+
if int(db_path.stat().st_size) <= len(SQLITE_HEADER):
133+
return False
134+
except Exception:
135+
return False
136+
137+
try:
138+
with db_path.open("rb") as f:
139+
if f.read(len(SQLITE_HEADER)) != SQLITE_HEADER:
140+
return False
141+
except Exception:
142+
return False
143+
144+
conn: sqlite3.Connection | None = None
145+
try:
146+
conn = sqlite3.connect(str(db_path))
147+
conn.execute("PRAGMA schema_version").fetchone()
148+
row = conn.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1").fetchone()
149+
return row is not None
150+
except Exception:
151+
return False
152+
finally:
153+
if conn is not None:
154+
try:
155+
conn.close()
156+
except Exception:
157+
pass
158+
159+
126160
def sqlite_diagnostics_status(diagnostics: Mapping[str, Any]) -> str:
127161
if not diagnostics:
128162
return "not_run"

src/wechat_decrypt_tool/wechat_decrypt.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
2222

2323
from .app_paths import get_output_databases_dir
24+
from .database_filters import should_skip_source_database
2425
from .sqlite_diagnostics import collect_sqlite_diagnostics, sqlite_diagnostics_status
2526

2627
# 注意:不再支持默认密钥,所有密钥必须通过参数传入
@@ -64,6 +65,59 @@ def _derive_account_name_from_path(path: Path) -> str:
6465
return "unknown_account"
6566

6667

68+
def _build_decrypt_failure_message(result: dict) -> str:
69+
failed_pages = int(result.get("failed_pages") or 0)
70+
successful_pages = int(result.get("successful_pages") or 0)
71+
diagnostic_status = str(result.get("diagnostic_status") or "").strip()
72+
diagnostics = dict(result.get("diagnostics") or {})
73+
74+
detail = (
75+
diagnostics.get("quick_check_error")
76+
or diagnostics.get("connect_error")
77+
or diagnostics.get("table_list_error")
78+
or diagnostics.get("page_count_error")
79+
or diagnostics.get("quick_check")
80+
or diagnostic_status
81+
)
82+
detail_text = " ".join(str(detail or "").split()).strip()
83+
84+
if failed_pages > 0 and successful_pages == 0:
85+
if detail_text:
86+
return f"数据库校验未通过,密钥可能不匹配当前账号: {detail_text}"
87+
return "数据库校验未通过,密钥可能不匹配当前账号"
88+
89+
if diagnostic_status and diagnostic_status != "ok":
90+
if detail_text:
91+
return f"解密输出不是有效的 SQLite 数据库: {detail_text}"
92+
return "解密输出不是有效的 SQLite 数据库"
93+
94+
if failed_pages > 0:
95+
return "解密输出包含页失败,结果不完整"
96+
97+
return ""
98+
99+
100+
def build_decrypt_summary_message(*, success_count: int, total_databases: int, diagnostic_warning_count: int) -> str:
101+
success_count = int(success_count or 0)
102+
total_databases = int(total_databases or 0)
103+
diagnostic_warning_count = int(diagnostic_warning_count or 0)
104+
105+
if total_databases <= 0:
106+
return "未找到可解密的数据库"
107+
108+
if success_count <= 0:
109+
if diagnostic_warning_count > 0:
110+
return "解密失败:数据库校验未通过,密钥可能不匹配当前账号。"
111+
return "解密失败:未能成功解密任何数据库。"
112+
113+
if success_count < total_databases:
114+
if diagnostic_warning_count > 0:
115+
return f"解密部分成功:成功 {success_count}/{total_databases},其余数据库校验未通过。"
116+
return f"解密部分成功:成功 {success_count}/{total_databases}。"
117+
118+
return f"解密完成: 成功 {success_count}/{total_databases}"
119+
120+
67121
def _resolve_db_storage_roots(storage_path: Path) -> list[Path]:
68122
try:
69123
target = storage_path.resolve()
@@ -158,7 +212,7 @@ def scan_account_databases_from_path(db_storage_path: str) -> dict:
158212
for file_name in files:
159213
if not file_name.endswith(".db"):
160214
continue
161-
if file_name in ["key_info.db"]:
215+
if should_skip_source_database(file_name):
162216
continue
163217
db_path = os.path.join(root, file_name)
164218
databases.append(
@@ -266,7 +320,8 @@ def _append_failed_page(page_num: int, reason: str, error: str = "") -> None:
266320
result["failed_page_samples"].append(item)
267321

268322
def _finalize(success: bool, error: str = "") -> bool:
269-
result["success"] = bool(success)
323+
normalized_success = bool(success)
324+
result["success"] = normalized_success
270325
if error:
271326
result["error"] = " ".join(str(error).split()).strip()
272327

@@ -281,6 +336,19 @@ def _finalize(success: bool, error: str = "") -> bool:
281336
result["diagnostics"] = diagnostics
282337
result["diagnostic_status"] = sqlite_diagnostics_status(diagnostics)
283338

339+
if normalized_success:
340+
failure_message = _build_decrypt_failure_message(result)
341+
if failure_message:
342+
normalized_success = False
343+
result["success"] = False
344+
if not result["error"]:
345+
result["error"] = failure_message
346+
if output_file.exists():
347+
try:
348+
output_file.unlink()
349+
except Exception as exc:
350+
logger.warning("删除无效解密输出失败: %s, 错误: %s", output_file, exc)
351+
284352
payload = {
285353
"db_name": result["db_name"],
286354
"db_path": result["db_path"],
@@ -307,7 +375,7 @@ def _finalize(success: bool, error: str = "") -> bool:
307375
log_fn = logger.warning
308376
log_fn("[decrypt.diagnostic] %s", json.dumps(payload, ensure_ascii=False, sort_keys=True))
309377
self.last_result = result
310-
return bool(success)
378+
return bool(result["success"])
311379

312380
logger.info(f"开始解密数据库: {db_path}")
313381

@@ -693,7 +761,11 @@ def decrypt_wechat_databases(db_storage_path: str = None, key: str = None) -> di
693761
# 返回结果
694762
result = {
695763
"status": "success" if success_count > 0 else "error",
696-
"message": f"解密完成: 成功 {success_count}/{total_databases}",
764+
"message": build_decrypt_summary_message(
765+
success_count=success_count,
766+
total_databases=total_databases,
767+
diagnostic_warning_count=diagnostic_warning_count,
768+
),
697769
"total_databases": total_databases,
698770
"successful_count": success_count,
699771
"failed_count": total_databases - success_count,

src/wechat_decrypt_tool/wechat_detection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from ctypes import wintypes
1414
from datetime import datetime
1515

16+
from .database_filters import should_skip_source_database
17+
1618

1719
def get_wx_db(msg_dir: str = None,
1820
db_types: Union[List[str], str] = None,
@@ -59,8 +61,7 @@ def get_wx_db(msg_dir: str = None,
5961
for file_name in files:
6062
if not file_name.endswith(".db"):
6163
continue
62-
# 排除不需要解密的数据库
63-
if file_name in ["key_info.db"]:
64+
if should_skip_source_database(file_name):
6465
continue
6566
db_type = re.sub(r"\d*\.db$", "", file_name)
6667
if db_types and db_type not in db_types: # 如果指定db_type,则过滤掉其他db_type
@@ -672,8 +673,7 @@ def collect_account_databases(data_dir: str, account_name: str) -> List[Dict[str
672673
if not file_name.endswith('.db'):
673674
continue
674675

675-
# 排除不需要解密的数据库
676-
if file_name in ["key_info.db"]:
676+
if should_skip_source_database(file_name):
677677
continue
678678

679679
db_path = os.path.join(root, file_name)

0 commit comments

Comments
 (0)