"""自动密钥提取:合成数据库/内存,不读取真实账号的内存。""" import ctypes import os import tempfile import unittest from pathlib import Path from unittest import mock import wxwork_key as scanner from test_crypto_roundtrip import build_encrypted_db class AutomaticKeyScannerTest(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.path = str(Path(temporary.name) / "message.db") self.key = bytes(range(1, 17)) Path(self.path).write_bytes(build_encrypted_db(self.key)) def test_binary_patterns_and_chunk_boundaries_return_hex_and_stop_early(self): patterns = [self.key + b"\x01\x00\x00\x00sAlT", b"\x10\x00\x00\x00" + self.key, b"raw:" + self.key, self.key.hex().encode()] for pattern in patterns: with self.subTest(pattern_length=len(pattern)): memory = b"-" * 59 + pattern + b"-" * 512 pm = mock.Mock() pm.regions.return_value = [(0, len(memory))] pm.read.side_effect = lambda base, size: memory[base:base+size] with mock.patch.object(scanner, "_ProcessMemory", return_value=pm), mock.patch.object(scanner, "_CHUNK", 64): result = scanner._scan_pid(123, [self.path]) self.assertEqual(result, {self.path: [self.key.hex()]}) self.assertLessEqual(pm.read.call_count, 2 + 2 * ((len(memory) + 63) // 64)) pm.close.assert_called_once_with() def test_completed_accounts_skip_remaining_processes(self): with mock.patch.object(scanner, "find_wxwork_pids", return_value=[1, 2, 3]), \ mock.patch.object(scanner, "_scan_pid", return_value={self.path: [self.key.hex()]}) as scan: result = scanner.extract_keys_from_running([self.path], timeout=50) self.assertEqual(result, {self.path: [self.key.hex()]}) self.assertEqual(scan.call_count, 1) def test_deadline_preserves_partial_results_and_closes_memory(self): pm = mock.Mock() pm.regions.return_value = [(0, 64), (64, 64)] pm.read.return_value = self.key + b"\x01\x00\x00\x00sAlT" with mock.patch.object(scanner, "_ProcessMemory", return_value=pm), \ mock.patch.object(scanner.time, "monotonic", side_effect=[0, 0, 20]): result = scanner._scan_pid(123, [self.path, "absent.db"], deadline=10) self.assertEqual(result, {self.path: [self.key.hex()]}) pm.close.assert_called_once_with() def test_expired_budget_does_not_start_scanning(self): with mock.patch.object(scanner, "find_wxwork_pids", return_value=[1]), \ mock.patch.object(scanner, "_scan_pid") as scan: self.assertEqual(scanner.extract_keys_from_running([self.path], timeout=0), {}) scan.assert_not_called() def test_native_memory_api_reads_own_test_buffer_at_full_pointer_width(self): payload = b"synthetic-native-memory-probe" buffer = ctypes.create_string_buffer(payload) pm = scanner._ProcessMemory(os.getpid()) try: address = ctypes.addressof(buffer) self.assertEqual(pm.read(address, len(payload)), payload) self.assertTrue(any(base <= address < base + size for base, size in pm.regions(rw_only=True))) finally: pm.close() def test_fast_salt_pass_reaches_later_regions_before_expensive_fallback(self): pm = mock.Mock() memory = b"-" * 192 + self.key + b"\x01\x00\x00\x00sAlT" pm.regions.return_value = [(0, len(memory))] pm.read.side_effect = lambda base, size: memory[base:base + size] with mock.patch.object(scanner, "_ProcessMemory", return_value=pm), \ mock.patch.object(scanner, "_CHUNK", 64), \ mock.patch.object(scanner, "_candidates_codec", side_effect=AssertionError("fallback ran too early")): result = scanner._scan_pid(123, [self.path]) self.assertEqual(result, {self.path: [self.key.hex()]}) self.assertEqual(pm.read.call_count, 4)