Files
zWorkFlow/decrypt_wxwork_db.py
2026-05-17 06:24:08 +08:00

177 lines
5.1 KiB
Python

"""
Decrypt WXWork databases encrypted with wxSQLite3 AES-128-CBC.
This handles the database page format. A 16-byte raw key is still required,
either from wxwork_keys.json or via --key.
"""
import argparse
import json
import os
import shutil
import sys
from key_utils import get_key_info, strip_key_metadata
from wxwork_crypto import (
decrypt_wxwork_database,
is_plain_sqlite_page,
is_wxsqlite3_aes128_page1,
verify_sqlite_file,
verify_wxsqlite3_aes128_key,
)
def _app_paths():
from config import _app_base_dir, _config_file_path
return _app_base_dir(), _config_file_path()
def _load_config():
base, config_file = _app_paths()
cfg = {}
if os.path.exists(config_file):
with open(config_file, encoding="utf-8") as f:
cfg = json.load(f)
db_dir = cfg.get("wxwork_db_dir", "")
if not db_dir or not os.path.isdir(db_dir):
from find_wxwork_keys import auto_detect_wxwork_db_dir
detected = auto_detect_wxwork_db_dir()
if detected:
db_dir = detected
else:
raise RuntimeError("wxwork_db_dir is not configured")
keys_file = cfg.get("wxwork_keys_file", "wxwork_keys.json")
if not os.path.isabs(keys_file):
keys_file = os.path.join(base, keys_file)
out_dir = cfg.get("wxwork_decrypted_dir", "wxwork_decrypted")
if not os.path.isabs(out_dir):
out_dir = os.path.join(base, out_dir)
return {
"db_dir": db_dir,
"keys_file": keys_file,
"out_dir": out_dir,
"global_key": cfg.get("wxwork_db_key", ""),
}
def _parse_key_hex(value):
value = (value or "").strip()
if value.startswith("x'") and value.endswith("'"):
value = value[2:-1]
if len(value) != 32:
raise ValueError("WXWork wxSQLite3 AES-128 key must be 32 hex chars")
return bytes.fromhex(value)
def _load_keys(keys_file):
if not os.path.exists(keys_file):
return {}
with open(keys_file, encoding="utf-8") as f:
return strip_key_metadata(json.load(f))
def _iter_db_files(db_dir):
for root, dirs, files in os.walk(db_dir):
dirs[:] = [d for d in dirs if d not in ("-journal",)]
for name in files:
if not name.endswith(".db") or name.endswith("-wal") or name.endswith("-shm"):
continue
path = os.path.join(root, name)
rel = os.path.relpath(path, db_dir)
yield rel, path
def main(argv=None):
parser = argparse.ArgumentParser(description="Decrypt WXWork wxSQLite3 AES-128 databases")
parser.add_argument("--key", help="16-byte raw key as 32 hex chars")
args = parser.parse_args(argv)
cfg = _load_config()
db_dir = cfg["db_dir"]
out_dir = cfg["out_dir"]
keys_file = cfg["keys_file"]
keys = _load_keys(keys_file)
global_key = None
key_arg = args.key or cfg.get("global_key")
if key_arg:
global_key = _parse_key_hex(key_arg)
print("=" * 60)
print(" WXWork Database Decryptor")
print("=" * 60)
print(f"DB dir: {db_dir}")
print(f"Output: {out_dir}")
if keys:
print(f"Loaded {len(keys)} per-DB keys from {keys_file}")
elif global_key:
print("Using global key from argument/config")
else:
print(f"No key available. Run find_wxwork_keys.py or pass --key.")
return 1
os.makedirs(out_dir, exist_ok=True)
success = 0
copied = 0
failed = 0
for rel, path in sorted(_iter_db_files(db_dir)):
out_path = os.path.join(out_dir, rel)
with open(path, "rb") as f:
page1 = f.read(4096)
if is_plain_sqlite_page(page1):
os.makedirs(os.path.dirname(out_path), exist_ok=True)
shutil.copy2(path, out_path)
copied += 1
print(f"COPY: {rel} (plain SQLite)")
continue
if not is_wxsqlite3_aes128_page1(page1):
failed += 1
print(f"SKIP: {rel} (unknown encrypted format)")
continue
key = global_key
key_info = get_key_info(keys, rel) if keys else None
if key_info:
try:
key = _parse_key_hex(key_info["enc_key"])
except (KeyError, ValueError) as exc:
failed += 1
print(f"FAIL: {rel} (bad key entry: {exc})")
continue
if key is None:
failed += 1
print(f"SKIP: {rel} (no key)")
continue
if not verify_wxsqlite3_aes128_key(key, page1):
failed += 1
print(f"FAIL: {rel} (key validation failed)")
continue
try:
decrypt_wxwork_database(path, out_path, key)
tables = verify_sqlite_file(out_path)
success += 1
table_preview = ", ".join(tables[:5])
suffix = f" tables: {table_preview}" if table_preview else " no tables"
print(f"OK: {rel} ({suffix})")
except Exception as exc:
failed += 1
print(f"FAIL: {rel} ({exc})")
print(f"\nResult: {success} decrypted, {copied} copied, {failed} failed")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())