weflow-cli 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/weflow-cli-electron.cjs +59 -0
- package/bin/weflow-cli.ts +2311 -0
- package/cli.cjs +3 -0
- package/dist/bin/weflow-cli.d.ts +3 -0
- package/dist/bin/weflow-cli.d.ts.map +1 -0
- package/dist/bin/weflow-cli.js +2270 -0
- package/dist/bin/weflow-cli.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +690 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/core/dbPathService.d.ts +40 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -0
- package/dist/src/core/dbPathService.js +324 -0
- package/dist/src/core/dbPathService.js.map +1 -0
- package/dist/src/core/keyService.d.ts +59 -0
- package/dist/src/core/keyService.d.ts.map +1 -0
- package/dist/src/core/keyService.js +368 -0
- package/dist/src/core/keyService.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +62 -0
- package/dist/src/core/ntCore.d.ts.map +1 -0
- package/dist/src/core/ntCore.js +191 -0
- package/dist/src/core/ntCore.js.map +1 -0
- package/dist/src/core/sqlcipherCore.d.ts +119 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
- package/dist/src/core/sqlcipherCore.js +693 -0
- package/dist/src/core/sqlcipherCore.js.map +1 -0
- package/dist/src/core/wcdbCore.d.ts +738 -0
- package/dist/src/core/wcdbCore.d.ts.map +1 -0
- package/dist/src/core/wcdbCore.js +4545 -0
- package/dist/src/core/wcdbCore.js.map +1 -0
- package/dist/src/core/wechatClient.d.ts +23 -0
- package/dist/src/core/wechatClient.d.ts.map +1 -0
- package/dist/src/core/wechatClient.js +166 -0
- package/dist/src/core/wechatClient.js.map +1 -0
- package/dist/src/services/chatService.d.ts +25 -0
- package/dist/src/services/chatService.d.ts.map +1 -0
- package/dist/src/services/chatService.js +291 -0
- package/dist/src/services/chatService.js.map +1 -0
- package/dist/src/services/configService.d.ts +43 -0
- package/dist/src/services/configService.d.ts.map +1 -0
- package/dist/src/services/configService.js +156 -0
- package/dist/src/services/configService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +30 -0
- package/dist/src/services/exportService.d.ts.map +1 -0
- package/dist/src/services/exportService.js +273 -0
- package/dist/src/services/exportService.js.map +1 -0
- package/dist/src/services/wechat-formatter.d.ts +69 -0
- package/dist/src/services/wechat-formatter.d.ts.map +1 -0
- package/dist/src/services/wechat-formatter.js +248 -0
- package/dist/src/services/wechat-formatter.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +28 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -0
- package/dist/src/services/wechatMessageService.js +341 -0
- package/dist/src/services/wechatMessageService.js.map +1 -0
- package/dist/src/services/wereadService.d.ts +206 -0
- package/dist/src/services/wereadService.d.ts.map +1 -0
- package/dist/src/services/wereadService.js +145 -0
- package/dist/src/services/wereadService.js.map +1 -0
- package/dist/src/services/whitelistService.d.ts +20 -0
- package/dist/src/services/whitelistService.d.ts.map +1 -0
- package/dist/src/services/whitelistService.js +60 -0
- package/dist/src/services/whitelistService.js.map +1 -0
- package/dist/src/types.d.ts +140 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/errors.d.ts +25 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +43 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/pathUtils.d.ts +5 -0
- package/dist/src/utils/pathUtils.d.ts.map +1 -0
- package/dist/src/utils/pathUtils.js +16 -0
- package/dist/src/utils/pathUtils.js.map +1 -0
- package/dist/src/utils/pythonRunner.d.ts +27 -0
- package/dist/src/utils/pythonRunner.d.ts.map +1 -0
- package/dist/src/utils/pythonRunner.js +62 -0
- package/dist/src/utils/pythonRunner.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +14 -0
- package/dist/src/utils/talkerUtils.d.ts.map +1 -0
- package/dist/src/utils/talkerUtils.js +73 -0
- package/dist/src/utils/talkerUtils.js.map +1 -0
- package/mcp-server/index.ts +757 -0
- package/package.json +72 -0
- package/resources/key/win32/x64/wx_key.dll +0 -0
- package/resources/wcdb/win32/x64/SDL2.dll +0 -0
- package/resources/wcdb/win32/x64/WCDB.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
- package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
- package/scripts/_batch_link_sources.py +73 -0
- package/scripts/_utils.py +531 -0
- package/scripts/annual_report.py +796 -0
- package/scripts/auto_tag.py +128 -0
- package/scripts/biz_daily.py +896 -0
- package/scripts/chat_report.py +397 -0
- package/scripts/chat_stats.py +702 -0
- package/scripts/classify_daily.py +411 -0
- package/scripts/compile_wiki.py +258 -0
- package/scripts/create_reading_notes.py +370 -0
- package/scripts/enrich_backlinks.py +163 -0
- package/scripts/export_chat_html.py +766 -0
- package/scripts/extract_3x_key.py +86 -0
- package/scripts/extract_todos.py +404 -0
- package/scripts/fav_server.py +392 -0
- package/scripts/fix_topics.py +111 -0
- package/scripts/generate_ai_report.py +367 -0
- package/scripts/generate_html.py +1372 -0
- package/scripts/generate_review.py +170 -0
- package/scripts/mcp_bridge.py +398 -0
- package/scripts/nt_decrypt.py +562 -0
- package/scripts/pipeline.py +145 -0
- package/scripts/promote_all.py +310 -0
- package/scripts/promote_ideas.py +297 -0
- package/scripts/rag_chat.py +191 -0
- package/scripts/scan_decrypt_4x.py +329 -0
- package/scripts/semantic_search.py +431 -0
- package/scripts/sync_fav.py +146 -0
- package/scripts/sync_weread.py +192 -0
- package/scripts/vault_rag.py +139 -0
- package/scripts/vault_search.py +141 -0
- package/src/core/dbPathService.ts +348 -0
- package/src/core/keyService.ts +409 -0
- package/src/core/ntCore.ts +224 -0
- package/src/core/sqlcipherCore.ts +764 -0
- package/src/core/wcdbCore.ts +4568 -0
- package/src/core/wechatClient.ts +204 -0
- package/src/lz4.d.ts +10 -0
- package/src/services/chatService.ts +296 -0
- package/src/services/configService.ts +197 -0
- package/src/services/exportService.ts +305 -0
- package/src/services/wechat-formatter.ts +348 -0
- package/src/services/wechatMessageService.ts +383 -0
- package/src/services/wereadService.ts +308 -0
- package/src/services/whitelistService.ts +72 -0
- package/src/types.ts +141 -0
- package/src/utils/errors.ts +42 -0
- package/src/utils/pathUtils.ts +16 -0
- package/src/utils/pythonRunner.ts +81 -0
- package/src/utils/talkerUtils.ts +84 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
WeChat 4.x Database Key Scanner & Decryptor
|
|
4
|
+
Scans Weixin.exe process memory for WCDB key+salt pairs and tests decryption.
|
|
5
|
+
"""
|
|
6
|
+
import ctypes
|
|
7
|
+
from ctypes import wintypes, c_void_p, c_size_t, create_string_buffer, byref, sizeof
|
|
8
|
+
import re
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac as hmac_lib
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from Crypto.Cipher import AES
|
|
17
|
+
except ImportError:
|
|
18
|
+
print("Install pycryptodome: pip install pycryptodome")
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
# ========== Constants ==========
|
|
22
|
+
PROCESS_VM_READ = 0x0010
|
|
23
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
24
|
+
MEM_COMMIT = 0x1000
|
|
25
|
+
PAGE_NOACCESS = 0x01
|
|
26
|
+
PAGE_GUARD = 0x100
|
|
27
|
+
PAGE_SIZE = 4096
|
|
28
|
+
EXPECTED_MAGIC = b'SQLite format 3\x00'
|
|
29
|
+
|
|
30
|
+
# ========== Memory Structures ==========
|
|
31
|
+
class MEMORY_BASIC_INFORMATION(ctypes.Structure):
|
|
32
|
+
_fields_ = [
|
|
33
|
+
('BaseAddress', ctypes.c_void_p),
|
|
34
|
+
('AllocationBase', ctypes.c_void_p),
|
|
35
|
+
('AllocationProtect', wintypes.DWORD),
|
|
36
|
+
('PartitionId', wintypes.WORD),
|
|
37
|
+
('RegionSize', ctypes.c_size_t),
|
|
38
|
+
('State', wintypes.DWORD),
|
|
39
|
+
('Protect', wintypes.DWORD),
|
|
40
|
+
('Type', wintypes.DWORD),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
kernel32 = ctypes.windll.kernel32
|
|
44
|
+
ReadProcessMemory = kernel32.ReadProcessMemory
|
|
45
|
+
ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
|
|
46
|
+
ReadProcessMemory.restype = wintypes.BOOL
|
|
47
|
+
|
|
48
|
+
VirtualQueryEx = kernel32.VirtualQueryEx
|
|
49
|
+
VirtualQueryEx.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.c_void_p, ctypes.c_size_t]
|
|
50
|
+
VirtualQueryEx.restype = ctypes.c_size_t
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_weixin_pid():
|
|
54
|
+
"""Find Weixin.exe process ID."""
|
|
55
|
+
import pymem
|
|
56
|
+
import pymem.process
|
|
57
|
+
for proc in pymem.process.list_processes():
|
|
58
|
+
try:
|
|
59
|
+
name = proc.szExeFile
|
|
60
|
+
if isinstance(name, bytes):
|
|
61
|
+
name = name.decode('utf-8', errors='ignore')
|
|
62
|
+
if name.lower() == 'weixin.exe':
|
|
63
|
+
return proc.th32ProcessID
|
|
64
|
+
except:
|
|
65
|
+
pass
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def scan_memory_keys(pid):
|
|
70
|
+
"""Scan process memory for x'<64hex_key><32hex_salt>' patterns."""
|
|
71
|
+
hProcess = kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
|
|
72
|
+
if not hProcess:
|
|
73
|
+
print(f"Failed to open PID {pid}")
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
pattern = re.compile(rb"x'([0-9a-fA-F]{64})([0-9a-fA-F]{32})'")
|
|
77
|
+
keys_found = []
|
|
78
|
+
|
|
79
|
+
address = 0x10000
|
|
80
|
+
region_count = 0
|
|
81
|
+
|
|
82
|
+
print(f"Scanning process {pid} memory...")
|
|
83
|
+
|
|
84
|
+
while address < 0x7FFFFFFFFFFF:
|
|
85
|
+
mbi = MEMORY_BASIC_INFORMATION()
|
|
86
|
+
result = VirtualQueryEx(hProcess, ctypes.c_void_p(address), ctypes.byref(mbi), sizeof(mbi))
|
|
87
|
+
if result == 0:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
region_count += 1
|
|
91
|
+
if region_count % 1000 == 0:
|
|
92
|
+
print(f" ... {region_count} regions scanned, {len(keys_found)} keys found")
|
|
93
|
+
|
|
94
|
+
region_addr = mbi.BaseAddress or 0
|
|
95
|
+
region_size = mbi.RegionSize or 0
|
|
96
|
+
|
|
97
|
+
if (mbi.State == MEM_COMMIT and
|
|
98
|
+
region_size > 256 and region_size < 200 * 1024 * 1024 and
|
|
99
|
+
mbi.Protect not in (0, PAGE_NOACCESS, PAGE_GUARD)):
|
|
100
|
+
|
|
101
|
+
pos = region_addr
|
|
102
|
+
end = region_addr + region_size
|
|
103
|
+
while pos < end:
|
|
104
|
+
chunk_size = min(65536, end - pos)
|
|
105
|
+
buf = create_string_buffer(chunk_size)
|
|
106
|
+
bytesRead = c_size_t(0)
|
|
107
|
+
ok = ReadProcessMemory(hProcess, ctypes.c_void_p(pos), buf, chunk_size, byref(bytesRead))
|
|
108
|
+
if ok and bytesRead.value > 0:
|
|
109
|
+
data = buf.raw[:bytesRead.value]
|
|
110
|
+
for m in pattern.finditer(data):
|
|
111
|
+
abs_addr = pos + m.start()
|
|
112
|
+
key_hex = m.group(1).decode()
|
|
113
|
+
salt_hex = m.group(2).decode()
|
|
114
|
+
keys_found.append((key_hex, salt_hex, abs_addr))
|
|
115
|
+
pos += chunk_size
|
|
116
|
+
|
|
117
|
+
address = region_addr + region_size
|
|
118
|
+
|
|
119
|
+
kernel32.CloseHandle(hProcess)
|
|
120
|
+
|
|
121
|
+
# Deduplicate
|
|
122
|
+
seen = set()
|
|
123
|
+
unique_keys = []
|
|
124
|
+
for k, s, a in keys_found:
|
|
125
|
+
pair = (k, s)
|
|
126
|
+
if pair not in seen:
|
|
127
|
+
seen.add(pair)
|
|
128
|
+
unique_keys.append((k, s, a))
|
|
129
|
+
|
|
130
|
+
print(f"Found {len(unique_keys)} unique key+salt pairs in {region_count} regions")
|
|
131
|
+
return unique_keys
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def try_decrypt_page0(enc_data, key_bytes):
|
|
135
|
+
"""Try to decrypt page 0 with given key using various parameters."""
|
|
136
|
+
|
|
137
|
+
# Parameter combinations to try
|
|
138
|
+
kdf_configs = [
|
|
139
|
+
# (use_pbkdf2, sha_name, iterations, salt_source)
|
|
140
|
+
(False, None, 0, 'none'), # Raw key, no KDF
|
|
141
|
+
(True, 'sha512', 256000, 'file'), # SQLCipher 4 standard
|
|
142
|
+
(True, 'sha1', 64000, 'file'), # SQLCipher 3 standard
|
|
143
|
+
(True, 'sha256', 256000, 'file'),
|
|
144
|
+
(True, 'sha512', 64000, 'file'),
|
|
145
|
+
(True, 'sha1', 256000, 'file'),
|
|
146
|
+
(True, 'sha512', 4000, 'file'),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
file_salt = enc_data[:16] # First 16 bytes of file (zero for MSG0.db)
|
|
150
|
+
|
|
151
|
+
for use_pbkdf2, sha, iters, salt_src in kdf_configs:
|
|
152
|
+
if use_pbkdf2:
|
|
153
|
+
salt = file_salt if salt_src == 'file' else bytes(16)
|
|
154
|
+
try:
|
|
155
|
+
dk = hashlib.pbkdf2_hmac(sha, key_bytes, salt, iters, dklen=32)
|
|
156
|
+
except:
|
|
157
|
+
continue
|
|
158
|
+
else:
|
|
159
|
+
dk = key_bytes
|
|
160
|
+
|
|
161
|
+
for rsv in [48, 80, 64, 32, 96]:
|
|
162
|
+
if rsv >= PAGE_SIZE:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
iv_offset = PAGE_SIZE - rsv
|
|
166
|
+
iv = enc_data[iv_offset:iv_offset + 16]
|
|
167
|
+
if len(iv) < 16:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
enc_len = PAGE_SIZE - 16 - rsv # For page 0: skip 16-byte salt
|
|
171
|
+
encrypted = enc_data[16:16 + enc_len]
|
|
172
|
+
|
|
173
|
+
if len(encrypted) % 16 != 0:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
cipher = AES.new(dk, AES.MODE_CBC, iv)
|
|
178
|
+
decrypted = cipher.decrypt(encrypted)
|
|
179
|
+
if decrypted[:16] == EXPECTED_MAGIC:
|
|
180
|
+
return {
|
|
181
|
+
'key': key_bytes.hex(),
|
|
182
|
+
'pbkdf2': use_pbkdf2,
|
|
183
|
+
'sha': sha,
|
|
184
|
+
'iterations': iters,
|
|
185
|
+
'reserved': rsv,
|
|
186
|
+
'iv_offset': iv_offset,
|
|
187
|
+
}
|
|
188
|
+
except:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def decrypt_full(enc_path, out_path, key_bytes, params):
|
|
195
|
+
"""Decrypt entire database using discovered parameters."""
|
|
196
|
+
with open(enc_path, 'rb') as f:
|
|
197
|
+
enc = f.read()
|
|
198
|
+
|
|
199
|
+
rsv = params['reserved']
|
|
200
|
+
use_pbkdf2 = params['pbkdf2']
|
|
201
|
+
sha = params['sha']
|
|
202
|
+
iters = params['iterations']
|
|
203
|
+
|
|
204
|
+
file_salt = enc[:16]
|
|
205
|
+
|
|
206
|
+
if use_pbkdf2:
|
|
207
|
+
dk = hashlib.pbkdf2_hmac(sha, key_bytes, file_salt, iters, dklen=32)
|
|
208
|
+
else:
|
|
209
|
+
dk = key_bytes
|
|
210
|
+
|
|
211
|
+
result = bytearray()
|
|
212
|
+
# Write SQLite header
|
|
213
|
+
result.extend(b'SQLite format 3\x00')
|
|
214
|
+
|
|
215
|
+
num_pages = len(enc) // PAGE_SIZE
|
|
216
|
+
|
|
217
|
+
for pg_num in range(num_pages):
|
|
218
|
+
page_start = pg_num * PAGE_SIZE
|
|
219
|
+
page_data = enc[page_start:page_start + PAGE_SIZE]
|
|
220
|
+
|
|
221
|
+
if pg_num == 0:
|
|
222
|
+
# Page 0: salt(16) + encrypted_data + reserved
|
|
223
|
+
iv_offset = PAGE_SIZE - rsv
|
|
224
|
+
iv = page_data[iv_offset:iv_offset + 16]
|
|
225
|
+
enc_data = page_data[16:PAGE_SIZE - rsv]
|
|
226
|
+
else:
|
|
227
|
+
# Other pages: encrypted_data + reserved
|
|
228
|
+
iv_offset = PAGE_SIZE - rsv
|
|
229
|
+
iv = page_data[iv_offset:iv_offset + 16]
|
|
230
|
+
enc_data = page_data[:PAGE_SIZE - rsv]
|
|
231
|
+
|
|
232
|
+
cipher = AES.new(dk, AES.MODE_CBC, iv)
|
|
233
|
+
decrypted = cipher.decrypt(enc_data)
|
|
234
|
+
result.extend(decrypted)
|
|
235
|
+
|
|
236
|
+
# Write reserved area
|
|
237
|
+
reserved = page_data[PAGE_SIZE - rsv:PAGE_SIZE]
|
|
238
|
+
result.extend(reserved)
|
|
239
|
+
|
|
240
|
+
if pg_num % 1000 == 0 and pg_num > 0:
|
|
241
|
+
print(f" ... decrypted {pg_num}/{num_pages} pages")
|
|
242
|
+
|
|
243
|
+
with open(out_path, 'wb') as f:
|
|
244
|
+
f.write(result)
|
|
245
|
+
print(f"Decrypted database written to: {out_path}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def main():
|
|
249
|
+
import argparse
|
|
250
|
+
parser = argparse.ArgumentParser(description='WeChat 4.x Database Decryptor')
|
|
251
|
+
parser.add_argument('--db', help='Path to encrypted database (e.g., MSG0.db)')
|
|
252
|
+
parser.add_argument('--out', help='Output path for decrypted database')
|
|
253
|
+
parser.add_argument('--scan-only', action='store_true', help='Only scan for keys, no decryption')
|
|
254
|
+
parser.add_argument('--key', help='Specific key hex to try (64 chars)')
|
|
255
|
+
parser.add_argument('--salt', help='Specific salt hex to try with key (32 chars)')
|
|
256
|
+
args = parser.parse_args()
|
|
257
|
+
|
|
258
|
+
pid = find_weixin_pid()
|
|
259
|
+
if not pid:
|
|
260
|
+
print("Weixin.exe not running. Start WeChat 4.x first.")
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
print(f"Found Weixin.exe PID: {pid}")
|
|
263
|
+
|
|
264
|
+
# Scan memory for keys
|
|
265
|
+
keys = scan_memory_keys(pid)
|
|
266
|
+
print(f"\nFound {len(keys)} key+salt pairs:")
|
|
267
|
+
for key_hex, salt_hex, addr in keys:
|
|
268
|
+
print(f" {key_hex[:32]}... | {salt_hex} @ {hex(addr)}")
|
|
269
|
+
|
|
270
|
+
if args.scan_only:
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
# If specific key+salt provided, use it
|
|
274
|
+
if args.key:
|
|
275
|
+
keys.insert(0, (args.key, args.salt or '00' * 16, 0))
|
|
276
|
+
|
|
277
|
+
# Database to decrypt
|
|
278
|
+
db_path = args.db
|
|
279
|
+
if not db_path:
|
|
280
|
+
# Try to find MSG0.db
|
|
281
|
+
wx_docs = os.path.expandvars(r'%USERPROFILE%\Documents\WeChat Files')
|
|
282
|
+
if os.path.isdir(wx_docs):
|
|
283
|
+
for wxid_dir in os.listdir(wx_docs):
|
|
284
|
+
msg_path = os.path.join(wx_docs, wxid_dir, 'Msg', 'Multi', 'MSG0.db')
|
|
285
|
+
if os.path.exists(msg_path):
|
|
286
|
+
db_path = msg_path
|
|
287
|
+
print(f"\nAuto-detected database: {db_path}")
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
if not db_path or not os.path.exists(db_path):
|
|
291
|
+
print("No database file found. Use --db to specify path.")
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
|
|
294
|
+
out_path = args.out or db_path.replace('.db', '_decrypted.db')
|
|
295
|
+
|
|
296
|
+
with open(db_path, 'rb') as f:
|
|
297
|
+
enc_data = f.read()
|
|
298
|
+
|
|
299
|
+
print(f"\nTesting {len(keys)} keys against {db_path}...")
|
|
300
|
+
print(f"File size: {len(enc_data)} bytes, {len(enc_data) // PAGE_SIZE} pages")
|
|
301
|
+
print(f"File salt (first 16 bytes): {enc_data[:16].hex()}")
|
|
302
|
+
|
|
303
|
+
for idx, (key_hex, salt_hex, addr) in enumerate(keys):
|
|
304
|
+
key_bytes = bytes.fromhex(key_hex)
|
|
305
|
+
result = try_decrypt_page0(enc_data, key_bytes)
|
|
306
|
+
if result:
|
|
307
|
+
print(f"\n*** SUCCESS with key #{idx + 1} ***")
|
|
308
|
+
print(f" Key: {key_hex}")
|
|
309
|
+
print(f" Memory salt: {salt_hex}")
|
|
310
|
+
print(f" PBKDF2: {result['pbkdf2']}")
|
|
311
|
+
if result['pbkdf2']:
|
|
312
|
+
print(f" Hash: {result['sha']}")
|
|
313
|
+
print(f" Iterations: {result['iterations']}")
|
|
314
|
+
print(f" Reserved: {result['reserved']} bytes")
|
|
315
|
+
|
|
316
|
+
decrypt_full(db_path, out_path, key_bytes, result)
|
|
317
|
+
break
|
|
318
|
+
elif idx % 5 == 0:
|
|
319
|
+
print(f" ... tested {idx + 1}/{len(keys)} keys")
|
|
320
|
+
else:
|
|
321
|
+
print("\nNo key could decrypt the database.")
|
|
322
|
+
print("Possible reasons:")
|
|
323
|
+
print(" - Database uses a different encryption format")
|
|
324
|
+
print(" - Key is not in the x'<hex>' format in memory")
|
|
325
|
+
print(" - Database was created with a key from a different session")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
if __name__ == '__main__':
|
|
329
|
+
main()
|