feat: add Linux support with cross-platform memory scanning

- Add Linux memory scanner (`find_all_keys_linux.py`) using `/proc/<pid>/mem`,
  same approach as Windows/macOS — no GDB, no function offsets, no restart needed
- Extract Windows-specific code to `find_all_keys_windows.py`
- Make `find_all_keys.py` a platform dispatcher (Windows / Linux)
- Add `key_utils.py` for cross-platform path matching (`/` vs `\` in all_keys.json)
- Update `config.py` with Linux auto-detection of db_storage paths
- Update all consumers (decrypt_db, monitor, monitor_web, mcp_server) to use
  `get_key_info()` for platform-agnostic key lookup

Tested on remote Linux container: 15/15 DBs scanned, decrypted, and verified.
This commit is contained in:
PeanutSplash
2026-03-06 15:52:06 +08:00
committed by ylytdeng
parent 5879b58239
commit f9c338b48d
12 changed files with 1197 additions and 762 deletions

View File

@@ -10,8 +10,9 @@ import hmac as hmac_mod
from datetime import datetime
from Crypto.Cipher import AES
from mcp.server.fastmcp import FastMCP
import zstandard as zstd
from decode_image import ImageResolver
import zstandard as zstd
from decode_image import ImageResolver
from key_utils import get_key_info, key_path_variants, strip_key_metadata
# ============ 加密常量 ============
PAGE_SZ = 4096
@@ -49,8 +50,8 @@ if not DECODED_IMAGE_DIR:
elif not os.path.isabs(DECODED_IMAGE_DIR):
DECODED_IMAGE_DIR = os.path.join(SCRIPT_DIR, DECODED_IMAGE_DIR)
with open(KEYS_FILE) as f:
ALL_KEYS = json.load(f)
with open(KEYS_FILE) as f:
ALL_KEYS = strip_key_metadata(json.load(f))
# ============ 解密函数 ============
@@ -149,7 +150,7 @@ class DBCache:
tmp_path = info["path"]
if not os.path.exists(tmp_path):
continue
rel_path = rel_key.replace('/', os.sep)
rel_path = rel_key.replace('\\', os.sep)
db_path = os.path.join(DB_DIR, rel_path)
wal_path = db_path + "-wal"
try:
@@ -174,12 +175,13 @@ class DBCache:
except OSError:
pass
def get(self, rel_key):
if rel_key not in ALL_KEYS:
return None
rel_path = rel_key.replace('/', os.sep)
db_path = os.path.join(DB_DIR, rel_path)
wal_path = db_path + "-wal"
def get(self, rel_key):
key_info = get_key_info(ALL_KEYS, rel_key)
if not key_info:
return None
rel_path = rel_key.replace('\\', '/').replace('/', os.sep)
db_path = os.path.join(DB_DIR, rel_path)
wal_path = db_path + "-wal"
if not os.path.exists(db_path):
return None
@@ -195,8 +197,8 @@ class DBCache:
return c_path
tmp_path = self._cache_path(rel_key)
enc_key = bytes.fromhex(ALL_KEYS[rel_key]["enc_key"])
full_decrypt(db_path, tmp_path, enc_key)
enc_key = bytes.fromhex(key_info["enc_key"])
full_decrypt(db_path, tmp_path, enc_key)
if os.path.exists(wal_path):
decrypt_wal(wal_path, tmp_path, enc_key)
self._cache[rel_key] = (db_mtime, wal_mtime, tmp_path)
@@ -248,7 +250,7 @@ def get_contact_names():
pass
# 实时解密
path = _cache.get("contact/contact.db")
path = _cache.get("contact\\contact.db")
if path:
try:
_contact_names, _contact_full = _load_contacts_from(path)
@@ -330,11 +332,12 @@ def _parse_message_content(content, local_type, is_group):
# 消息 DB 的 rel_keys排除 fts/resource/media/biz
MSG_DB_KEYS = sorted([
k for k in ALL_KEYS
if k.startswith("message/message_") and k.endswith(".db")
and "fts" not in k and "resource" not in k
])
MSG_DB_KEYS = sorted([
k for k in ALL_KEYS
if any(v.startswith("message/") for v in key_path_variants(k))
and any(v.endswith(".db") for v in key_path_variants(k))
and "fts" not in k and "resource" not in k
])
def _find_msg_table_for_user(username):
@@ -379,7 +382,7 @@ def get_recent_sessions(limit: int = 20) -> str:
Args:
limit: 返回的会话数量默认20
"""
path = _cache.get("session/session.db")
path = _cache.get("session\\session.db")
if not path:
return "错误: 无法解密 session.db"
@@ -635,7 +638,7 @@ def get_new_messages() -> str:
"""获取自上次调用以来的新消息。首次调用返回最近的会话状态。"""
global _last_check_state
path = _cache.get("session/session.db")
path = _cache.get("session\\session.db")
if not path:
return "错误: 无法解密 session.db"