Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
WECHAT_DATA_GLOB = os.path.expanduser(
"~/Documents/xwechat_files/*/db_storage"
)
if os.environ.get('WECHAT_DATA_ROOT'):
root = os.path.expandvars(os.path.expanduser(os.environ['WECHAT_DATA_ROOT']))
WECHAT_DATA_GLOB = os.path.join(root, '*', 'db_storage')
else:
raise RuntimeError(f"Unsupported platform: {platform.system()}")
16 changes: 16 additions & 0 deletions docs/windows-data-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Moved Windows data directories

If WeChat's `xwechat_files` directory lives outside Documents, set its parent
account root before extracting, decrypting, running the doctor, or querying:

```powershell
$env:WECHAT_DATA_ROOT = 'D:\WeChat\xwechat_files'
& .venv\Scripts\python.exe scripts\windows\extract_raw_key.py
& .venv\Scripts\python.exe scripts\windows\decrypt_all.py
& .venv\Scripts\python.exe scripts\common\doctor.py --json
```

This directory contains the account folders, each with a `db_storage` directory.
All Windows discovery paths honor the same override. An explicit missing override
does not fall back to another account under Documents. Without an override the
existing Documents discovery behavior is preserved.
12 changes: 10 additions & 2 deletions scripts/windows/decrypt_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ def decrypt_db(raw: bytes, src: str, dst: str) -> bool:
return True


def find_storage_roots():
root = os.environ.get('WECHAT_DATA_ROOT')
if root:
root = os.path.expandvars(os.path.expanduser(root))
return glob.glob(os.path.join(root, '*', 'db_storage'))
return glob.glob(os.path.expanduser(r"~/Documents/xwechat_files/*/db_storage"))


def main():
if len(sys.argv) > 1:
raw_hex = sys.argv[1].strip()
Expand All @@ -79,9 +87,9 @@ def main():
if len(raw) != 32 or len(raw_hex) != 64:
raise SystemExit("ERR: raw key must be exactly 64 hexadecimal characters")

src_roots = glob.glob(os.path.expanduser(r"~/Documents/xwechat_files/*/db_storage"))
src_roots = find_storage_roots()
if not src_roots:
print("ERR: no encrypted db_storage under ~/Documents/xwechat_files/*/"); sys.exit(1)
print("ERR: no encrypted db_storage found; set WECHAT_DATA_ROOT to the xwechat_files directory if moved"); sys.exit(1)
src_root = max(src_roots, key=os.path.getmtime)
account = os.path.basename(os.path.dirname(src_root)) # {wxid}_{device}
dst_root = os.path.join(DECRYPTED_DIR, account, "db_storage")
Expand Down
10 changes: 9 additions & 1 deletion scripts/windows/extract_raw_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ def capture_key(device, pid, page1, seconds):
return capture.raw_key


def find_message_dbs():
root = os.environ.get('WECHAT_DATA_ROOT')
if root:
root = os.path.expandvars(os.path.expanduser(root))
return glob.glob(os.path.join(root, '*', 'db_storage', 'message', 'message_0.db'))
return glob.glob(r"C:\Users\*\Documents\xwechat_files\*\db_storage\message\message_0.db")


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("seconds", nargs="?", type=int, default=90)
Expand All @@ -239,7 +247,7 @@ def main(argv=None):
import frida
from Crypto.Cipher import AES # Check dependencies before closing WeChat.

dbs = glob.glob(r"C:\Users\*\Documents\xwechat_files\*\db_storage\message\message_0.db")
dbs = find_message_dbs()
if not dbs:
raise FileNotFoundError("message_0.db not found")
with open(dbs[0], "rb") as source:
Expand Down
45 changes: 45 additions & 0 deletions tests/test_windows_data_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import importlib
import importlib.util
import os
from pathlib import Path


def _script(name):
path = Path(__file__).parents[1] / 'scripts' / 'windows' / (name + '.py')
spec = importlib.util.spec_from_file_location('path_test_' + name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def test_override_is_shared_by_query_extraction_and_decryption(monkeypatch, tmp_path):
import config, platform
root = tmp_path / 'Moved 微信 files'
storage = root / 'synthetic_account' / 'db_storage'
message = storage / 'message' / 'message_0.db'
message.parent.mkdir(parents=True)
message.write_bytes(b'synthetic')
with monkeypatch.context() as patch:
patch.setenv('WECHAT_DATA_ROOT', str(root))
patch.setattr(platform, 'system', lambda: 'Windows')
importlib.reload(config)
assert config.WECHAT_DATA_GLOB == os.path.join(str(root), '*', 'db_storage')
assert _script('extract_raw_key').find_message_dbs() == [str(message)]
assert _script('decrypt_all').find_storage_roots() == [str(storage)]
importlib.reload(config)


def test_override_does_not_silently_fall_back_to_another_account(monkeypatch, tmp_path):
monkeypatch.setenv('WECHAT_DATA_ROOT', str(tmp_path / 'missing'))
assert _script('extract_raw_key').find_message_dbs() == []
assert _script('decrypt_all').find_storage_roots() == []


def test_default_query_path_remains_supported(monkeypatch):
import config, platform
with monkeypatch.context() as patch:
patch.delenv('WECHAT_DATA_ROOT', raising=False)
patch.setattr(platform, 'system', lambda: 'Windows')
importlib.reload(config)
assert config.WECHAT_DATA_GLOB == os.path.expanduser('~/Documents/xwechat_files/*/db_storage')
importlib.reload(config)