Skip to content

Commit c28ac62

Browse files
committed
fix(key): match image key account with xwechat suffix
1 parent 2d46898 commit c28ac62

2 files changed

Lines changed: 85 additions & 8 deletions

File tree

src/wechat_decrypt_tool/key_service.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ def _summarize_key_payload(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
6363
}
6464

6565

66+
def _image_key_account_match_variants(value: Any) -> set[str]:
67+
"""Return account names that should be considered equivalent for image key matching.
68+
69+
Windows WeChat 4.x stores account data under a folder such as
70+
``wxid_o6wp2aat9mu312_8d63`` while wx_key may report the account as
71+
``wxid_o6wp2aat9mu312``. The trailing four-hex folder suffix is not part
72+
of the logical account id, so both names must match. Do not strip
73+
arbitrary suffixes: names like ``wxid_demo_extra`` may be a distinct
74+
account in tests or legacy data.
75+
"""
76+
raw = str(value or "").strip().lower()
77+
if not raw:
78+
return set()
79+
80+
variants = {raw}
81+
suffix_match = re.match(r"^(wxid_[^_\s]+)_[0-9a-f]{4}$", raw, flags=re.IGNORECASE)
82+
if suffix_match:
83+
variants.add(suffix_match.group(1).lower())
84+
return variants
85+
86+
6687
def _resolve_wxid_dir_for_image_key(
6788
account: Optional[str] = None,
6889
*,
@@ -779,23 +800,43 @@ async def get_image_key_integrated_workflow(
779800
if local_keys:
780801
# 如果指定了账号,尝试在本地结果中找匹配的
781802
if target_account_wxid:
803+
target_account_variants = _image_key_account_match_variants(target_account_wxid)
782804
for k in local_keys:
783805
local_wxid = str(k.get("wxid") or "").strip().lower()
784-
if local_wxid and local_wxid == target_account_wxid:
806+
local_account_variants = _image_key_account_match_variants(local_wxid)
807+
if local_account_variants and (local_account_variants & target_account_variants):
785808
logger.info(
786-
"[image_key] 本地算法精确匹配成功:target_wxid=%s payload=%s",
809+
"[image_key] 本地算法账号匹配成功:target_wxid=%s target_variants=%s local_variants=%s payload=%s",
787810
target_account_wxid,
811+
sorted(target_account_variants),
812+
sorted(local_account_variants),
788813
_summarize_key_payload(k),
789814
)
790-
upsert_account_keys_in_store(
791-
account=str(k.get("wxid") or "").strip(),
792-
image_xor_key=k['xor_key'],
793-
image_aes_key=k['aes_key']
794-
)
815+
if local_wxid != target_account_wxid:
816+
aliases = []
817+
for alias in [local_wxid, str(account or "").strip()]:
818+
if alias and alias not in aliases:
819+
aliases.append(alias)
820+
upsert_account_keys_in_store(
821+
account=target_account_wxid,
822+
image_xor_key=k['xor_key'],
823+
image_aes_key=k['aes_key'],
824+
aliases=aliases,
825+
)
826+
else:
827+
upsert_account_keys_in_store(
828+
account=str(k.get("wxid") or "").strip(),
829+
image_xor_key=k['xor_key'],
830+
image_aes_key=k['aes_key']
831+
)
832+
k = dict(k)
833+
k.setdefault("matched_wxid", target_account_wxid)
834+
k.setdefault("match_variants", sorted(local_account_variants | target_account_variants))
795835
return k
796836
logger.info(
797-
"[image_key] 本地算法未匹配到目标账号:target_wxid=%s local_wxids=%s",
837+
"[image_key] 本地算法未匹配到目标账号:target_wxid=%s target_variants=%s local_wxids=%s",
798838
target_account_wxid,
839+
sorted(target_account_variants),
799840
[str(item.get("wxid") or "").strip() for item in local_keys],
800841
)
801842
else:

tests/test_key_service_image_key_account_match.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,42 @@ def test_local_image_keys_require_exact_account_match(self) -> None:
287287
image_aes_key="BBBBBBBBBBBBBBBB",
288288
)
289289

290+
def test_local_image_keys_match_xwechat_folder_suffix(self) -> None:
291+
with mock.patch.object(
292+
key_service,
293+
"try_get_local_image_keys",
294+
return_value=[
295+
{"wxid": "wxid_demo", "xor_key": "0x2C", "aes_key": "AAAAAAAAAAAAAAAA"},
296+
],
297+
), mock.patch.object(
298+
key_service,
299+
"_resolve_account_dir",
300+
return_value=Path("D:/tmp/output/databases/wxid_demo"),
301+
), mock.patch.object(
302+
key_service,
303+
"_resolve_account_wxid_dir",
304+
return_value=Path("D:/tmp/xwechat_files/wxid_demo_8d63"),
305+
), mock.patch.object(
306+
key_service,
307+
"upsert_account_keys_in_store",
308+
) as upsert_mock, mock.patch.object(
309+
key_service,
310+
"fetch_and_save_remote_keys",
311+
new=mock.AsyncMock(side_effect=AssertionError("remote should not be called")),
312+
):
313+
result = asyncio.run(key_service.get_image_key_integrated_workflow("wxid_demo"))
314+
315+
self.assertEqual(result["wxid"], "wxid_demo")
316+
self.assertEqual(result["matched_wxid"], "wxid_demo_8d63")
317+
self.assertEqual(result["xor_key"], "0x2C")
318+
self.assertEqual(result["aes_key"], "AAAAAAAAAAAAAAAA")
319+
upsert_mock.assert_called_once_with(
320+
account="wxid_demo_8d63",
321+
image_xor_key="0x2C",
322+
image_aes_key="AAAAAAAAAAAAAAAA",
323+
aliases=["wxid_demo"],
324+
)
325+
290326
def test_fetch_remote_keys_can_use_db_storage_path_without_decrypted_output(self) -> None:
291327
with TemporaryDirectory() as temp_dir:
292328
wxid_dir = Path(temp_dir) / "xwechat_files" / "wxid_v4mbduwqtzpt22"

0 commit comments

Comments
 (0)