mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
improvement(log): 补充图片密钥与媒体解密链路调试日志
- 为解密页关键阶段补充调试日志 - 为图片密钥获取流程补充请求、命中和回退日志 - 为媒体密钥保存与批量解密补充有效密钥日志 - 对 AES 信息做摘要输出,便于排查且避免完整泄露
This commit is contained in:
+135
-4
@@ -487,6 +487,44 @@ const manualKeyErrors = reactive({
|
||||
})
|
||||
|
||||
const normalizeAccountId = (value) => String(value || '').trim()
|
||||
const summarizeAesForLog = (value) => {
|
||||
const raw = String(value || '').trim()
|
||||
if (!raw) return ''
|
||||
if (raw.length <= 8) return raw
|
||||
return `${raw.slice(0, 4)}...${raw.slice(-4)}(len=${raw.length})`
|
||||
}
|
||||
const summarizeKeyStateForLog = (xorKey, aesKey) => ({
|
||||
xor_key: String(xorKey || '').trim(),
|
||||
aes_key: summarizeAesForLog(aesKey),
|
||||
has_xor: !!String(xorKey || '').trim(),
|
||||
has_aes: !!String(aesKey || '').trim()
|
||||
})
|
||||
const formatLogError = (error) => {
|
||||
if (!error) return ''
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: String(error.name || 'Error'),
|
||||
message: String(error.message || ''),
|
||||
stack: String(error.stack || '')
|
||||
}
|
||||
}
|
||||
if (typeof error === 'object') {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(error))
|
||||
} catch {}
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
const logDecryptDebug = (phase, details = {}) => {
|
||||
if (process.client && typeof window !== 'undefined') {
|
||||
try {
|
||||
window.wechatDesktop?.logDebug?.('decrypt-page', phase, details)
|
||||
} catch {}
|
||||
}
|
||||
try {
|
||||
console.info(`[decrypt-page] ${phase}`, details)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const normalizeXorKey = (value) => {
|
||||
const raw = String(value || '').trim()
|
||||
@@ -508,6 +546,7 @@ const normalizeAesKey = (value) => {
|
||||
const prefillKeysForAccount = async (account) => {
|
||||
const acc = normalizeAccountId(account)
|
||||
if (!acc) return
|
||||
logDecryptDebug('prefill:start', { account: acc })
|
||||
try {
|
||||
const resp = await getSavedKeys({ account: acc })
|
||||
if (!resp || resp.status !== 'success') return
|
||||
@@ -527,22 +566,46 @@ const prefillKeysForAccount = async (account) => {
|
||||
if (aesKey && !String(manualKeys.aes_key || '').trim()) {
|
||||
manualKeys.aes_key = aesKey
|
||||
}
|
||||
logDecryptDebug('prefill:done', {
|
||||
request_account: acc,
|
||||
response_account: String(resp.account || '').trim(),
|
||||
db_key_present: !!dbKey,
|
||||
...summarizeKeyStateForLog(
|
||||
String(keys.image_xor_key || '').trim(),
|
||||
String(keys.image_aes_key || '').trim()
|
||||
),
|
||||
applied: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key)
|
||||
})
|
||||
} catch (e) {
|
||||
// ignore
|
||||
logDecryptDebug('prefill:error', { account: acc, error: formatLogError(e) })
|
||||
}
|
||||
}
|
||||
|
||||
const tryAutoFetchImageKeys = async (account) => {
|
||||
const acc = normalizeAccountId(account)
|
||||
if (!acc) return
|
||||
if (String(manualKeys.xor_key || '').trim() || String(manualKeys.aes_key || '').trim()) return
|
||||
if (String(manualKeys.xor_key || '').trim() || String(manualKeys.aes_key || '').trim()) {
|
||||
logDecryptDebug('auto-fetch:skip-existing', {
|
||||
account: acc,
|
||||
keys: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
warning.value = '正在通过云端/本地算法自动获取图片密钥,请稍候...'
|
||||
logDecryptDebug('auto-fetch:start', { account: acc })
|
||||
try {
|
||||
const imgRes = await getImageKey({
|
||||
account: acc,
|
||||
db_storage_path: String(formData.db_storage_path || '').trim()
|
||||
})
|
||||
logDecryptDebug('auto-fetch:response', {
|
||||
account: acc,
|
||||
status: imgRes?.status,
|
||||
errmsg: String(imgRes?.errmsg || ''),
|
||||
data_account: String(imgRes?.data?.account || '').trim(),
|
||||
keys: summarizeKeyStateForLog(imgRes?.data?.xor_key, imgRes?.data?.aes_key)
|
||||
})
|
||||
|
||||
if (imgRes && imgRes.status === 0) {
|
||||
if (imgRes.data?.xor_key) manualKeys.xor_key = imgRes.data.xor_key
|
||||
@@ -554,6 +617,7 @@ const tryAutoFetchImageKeys = async (account) => {
|
||||
}
|
||||
} catch (e) {
|
||||
warning.value = '网络请求失败,请手动填写图片密钥。'
|
||||
logDecryptDebug('auto-fetch:error', { account: acc, error: formatLogError(e) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,13 +625,27 @@ const ensureKeysForAccount = async (account) => {
|
||||
const acc = normalizeAccountId(account)
|
||||
if (!acc) return
|
||||
|
||||
logDecryptDebug('ensure-keys:start', {
|
||||
account: acc,
|
||||
previous_account: activeKeyAccount.value,
|
||||
current_manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key)
|
||||
})
|
||||
if (activeKeyAccount.value && activeKeyAccount.value !== acc) {
|
||||
logDecryptDebug('ensure-keys:switch-account', {
|
||||
from: activeKeyAccount.value,
|
||||
to: acc,
|
||||
cleared_keys: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key)
|
||||
})
|
||||
clearManualKeys()
|
||||
}
|
||||
|
||||
activeKeyAccount.value = acc
|
||||
await prefillKeysForAccount(acc)
|
||||
await tryAutoFetchImageKeys(acc)
|
||||
logDecryptDebug('ensure-keys:done', {
|
||||
account: acc,
|
||||
manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key)
|
||||
})
|
||||
}
|
||||
|
||||
const handleGetDbKey = async () => {
|
||||
@@ -640,6 +718,11 @@ const applyManualKeys = () => {
|
||||
}
|
||||
|
||||
const clearManualKeys = () => {
|
||||
logDecryptDebug('keys:clear', {
|
||||
active_account: activeKeyAccount.value,
|
||||
manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key),
|
||||
applied: summarizeKeyStateForLog(mediaKeys.xor_key, mediaKeys.aes_key)
|
||||
})
|
||||
manualKeys.xor_key = ''
|
||||
manualKeys.aes_key = ''
|
||||
manualKeyErrors.xor_key = ''
|
||||
@@ -769,6 +852,10 @@ const handleDecrypt = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
logDecryptDebug('decrypt:start', {
|
||||
db_storage_path: String(formData.db_storage_path || '').trim(),
|
||||
db_key_length: String(formData.key || '').trim().length
|
||||
})
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
warning.value = ''
|
||||
@@ -799,6 +886,10 @@ const handleDecrypt = async () => {
|
||||
if (match) mediaAccount.value = match[1]
|
||||
}
|
||||
} catch (e) {}
|
||||
logDecryptDebug('decrypt:completed-fallback', {
|
||||
media_account: mediaAccount.value,
|
||||
accounts: Object.keys(result.account_results || {})
|
||||
})
|
||||
|
||||
currentStep.value = 1
|
||||
await ensureKeysForAccount(mediaAccount.value)
|
||||
@@ -877,6 +968,10 @@ const handleDecrypt = async () => {
|
||||
if (match) mediaAccount.value = match[1]
|
||||
}
|
||||
} catch (e) {}
|
||||
logDecryptDebug('decrypt:completed-sse', {
|
||||
media_account: mediaAccount.value,
|
||||
accounts: Object.keys(data.account_results || {})
|
||||
})
|
||||
|
||||
try {
|
||||
eventSource.close()
|
||||
@@ -929,6 +1024,10 @@ const decryptAllImages = async () => {
|
||||
mediaDecryptResult.value = null
|
||||
error.value = ''
|
||||
warning.value = ''
|
||||
logDecryptDebug('media-decrypt:start', {
|
||||
account: mediaAccount.value,
|
||||
keys: summarizeKeyStateForLog(mediaKeys.xor_key, mediaKeys.aes_key)
|
||||
})
|
||||
|
||||
// 重置进度
|
||||
resetMediaDecryptProgress()
|
||||
@@ -973,9 +1072,20 @@ const decryptAllImages = async () => {
|
||||
decryptProgress.fail_count = data.fail_count
|
||||
mediaDecryptResult.value = data
|
||||
mediaDecrypting.value = false
|
||||
logDecryptDebug('media-decrypt:complete', {
|
||||
account: mediaAccount.value,
|
||||
total: data.total,
|
||||
success_count: data.success_count,
|
||||
skip_count: data.skip_count,
|
||||
fail_count: data.fail_count
|
||||
})
|
||||
closeMediaDecryptEventSource()
|
||||
} else if (data.type === 'error') {
|
||||
error.value = data.message
|
||||
logDecryptDebug('media-decrypt:error-event', {
|
||||
account: mediaAccount.value,
|
||||
message: data.message
|
||||
})
|
||||
mediaDecrypting.value = false
|
||||
closeMediaDecryptEventSource()
|
||||
}
|
||||
@@ -1016,19 +1126,30 @@ const goToMediaDecryptStep = async () => {
|
||||
warning.value = ''
|
||||
// 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示)
|
||||
const ok = applyManualKeys()
|
||||
logDecryptDebug('media-step:apply-manual', {
|
||||
account: mediaAccount.value,
|
||||
ok,
|
||||
manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key),
|
||||
applied: summarizeKeyStateForLog(mediaKeys.xor_key, mediaKeys.aes_key),
|
||||
errors: { ...manualKeyErrors }
|
||||
})
|
||||
if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return
|
||||
|
||||
// 用户已输入 XOR 时,自动保存一次,避免下次重复输入(失败不影响继续)
|
||||
if (mediaKeys.xor_key) {
|
||||
try {
|
||||
const aesVal = String(mediaKeys.aes_key || '').trim()
|
||||
logDecryptDebug('media-step:save-keys', {
|
||||
account: mediaAccount.value,
|
||||
keys: summarizeKeyStateForLog(mediaKeys.xor_key, aesVal)
|
||||
})
|
||||
await saveMediaKeys({
|
||||
account: mediaAccount.value || null,
|
||||
xor_key: mediaKeys.xor_key,
|
||||
aes_key: aesVal ? aesVal : null
|
||||
})
|
||||
} catch (e) {
|
||||
// ignore
|
||||
logDecryptDebug('media-step:save-keys-error', { account: mediaAccount.value, error: formatLogError(e) })
|
||||
}
|
||||
}
|
||||
currentStep.value = 2
|
||||
@@ -1040,6 +1161,10 @@ const skipToChat = async () => {
|
||||
const ok = applyManualKeys()
|
||||
if (ok && mediaKeys.xor_key) {
|
||||
const aesVal = String(mediaKeys.aes_key || '').trim()
|
||||
logDecryptDebug('skip-chat:save-keys', {
|
||||
account: mediaAccount.value,
|
||||
keys: summarizeKeyStateForLog(mediaKeys.xor_key, aesVal)
|
||||
})
|
||||
await saveMediaKeys({
|
||||
account: mediaAccount.value || null,
|
||||
xor_key: mediaKeys.xor_key,
|
||||
@@ -1047,7 +1172,7 @@ const skipToChat = async () => {
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
logDecryptDebug('skip-chat:save-keys-error', { account: mediaAccount.value, error: formatLogError(e) })
|
||||
}
|
||||
navigateTo('/chat')
|
||||
}
|
||||
@@ -1056,6 +1181,7 @@ const skipToChat = async () => {
|
||||
onMounted(async () => {
|
||||
if (process.client && typeof window !== 'undefined') {
|
||||
const selectedAccount = sessionStorage.getItem('selectedAccount')
|
||||
logDecryptDebug('mounted:selected-account-raw', { raw: selectedAccount || '' })
|
||||
if (selectedAccount) {
|
||||
try {
|
||||
const account = JSON.parse(selectedAccount)
|
||||
@@ -1068,9 +1194,14 @@ onMounted(async () => {
|
||||
}
|
||||
// 清除sessionStorage
|
||||
sessionStorage.removeItem('selectedAccount')
|
||||
logDecryptDebug('mounted:selected-account-parsed', {
|
||||
account_name: String(account.account_name || '').trim(),
|
||||
data_dir: String(account.data_dir || '').trim()
|
||||
})
|
||||
await ensureKeysForAccount(mediaAccount.value)
|
||||
} catch (e) {
|
||||
console.error('解析账户信息失败:', e)
|
||||
logDecryptDebug('mounted:selected-account-error', { error: formatLogError(e) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,24 @@ from .media_helpers import _resolve_account_dir, _resolve_account_wxid_dir
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _summarize_aes_key(value: Any) -> str:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if len(raw) <= 8:
|
||||
return raw
|
||||
return f"{raw[:4]}...{raw[-4:]}(len={len(raw)})"
|
||||
|
||||
|
||||
def _summarize_key_payload(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
payload = payload or {}
|
||||
return {
|
||||
"wxid": str(payload.get("wxid") or "").strip(),
|
||||
"xor_key": str(payload.get("xor_key") or "").strip(),
|
||||
"aes_key": _summarize_aes_key(payload.get("aes_key")),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_wxid_dir_for_image_key(
|
||||
account: Optional[str] = None,
|
||||
*,
|
||||
@@ -40,6 +58,7 @@ def _resolve_wxid_dir_for_image_key(
|
||||
if explicit_wxid_dir:
|
||||
candidate = Path(explicit_wxid_dir).expanduser()
|
||||
if candidate.exists() and candidate.is_dir():
|
||||
logger.info("[image_key] 使用显式 wxid_dir: %s", str(candidate))
|
||||
return candidate
|
||||
raise FileNotFoundError(f"指定的 wxid_dir 不存在或不是目录: {candidate}")
|
||||
|
||||
@@ -50,19 +69,42 @@ def _resolve_wxid_dir_for_image_key(
|
||||
if db_storage_dir.name.lower() == "db_storage":
|
||||
candidate = db_storage_dir.parent
|
||||
if candidate.exists() and candidate.is_dir():
|
||||
logger.info(
|
||||
"[image_key] 通过 db_storage_path 反推出 wxid_dir: db_storage_path=%s wxid_dir=%s",
|
||||
str(db_storage_dir),
|
||||
str(candidate),
|
||||
)
|
||||
return candidate
|
||||
nested_db_storage = db_storage_dir / "db_storage"
|
||||
if nested_db_storage.exists() and nested_db_storage.is_dir():
|
||||
logger.info(
|
||||
"[image_key] db_storage_path 指向 wxid_dir,自动使用其子目录: wxid_dir=%s",
|
||||
str(db_storage_dir),
|
||||
)
|
||||
return db_storage_dir
|
||||
logger.info(
|
||||
"[image_key] 提供的 db_storage_path 无法解析 wxid_dir: %s",
|
||||
explicit_db_storage_path,
|
||||
)
|
||||
|
||||
if account:
|
||||
try:
|
||||
account_dir = _resolve_account_dir(account)
|
||||
wx_id_dir = _resolve_account_wxid_dir(account_dir)
|
||||
if wx_id_dir:
|
||||
logger.info(
|
||||
"[image_key] 通过已解密账号目录解析 wxid_dir: account=%s account_dir=%s wxid_dir=%s",
|
||||
str(account).strip(),
|
||||
str(account_dir),
|
||||
str(wx_id_dir),
|
||||
)
|
||||
return wx_id_dir
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
"[image_key] 无法通过已解密账号目录解析 wxid_dir: account=%s error=%s",
|
||||
str(account).strip(),
|
||||
str(e),
|
||||
)
|
||||
|
||||
raise FileNotFoundError("无法定位该账号的 wxid_dir,请传入有效的 db_storage_path 或先完成数据库解密")
|
||||
|
||||
@@ -180,11 +222,13 @@ def get_wechat_internal_global_config(wx_dir: Path, file_name1) -> bytes:
|
||||
def try_get_local_image_keys() -> List[Dict[str, Any]]:
|
||||
"""尝试通过本地算法提取图片密钥 (无需 Hook)"""
|
||||
if wx_key is None or not hasattr(wx_key, 'get_image_key'):
|
||||
logger.info("[image_key] 本地算法不可用:wx_key.get_image_key 缺失")
|
||||
return []
|
||||
|
||||
try:
|
||||
res_json = wx_key.get_image_key()
|
||||
if not res_json:
|
||||
logger.info("[image_key] 本地算法返回空结果")
|
||||
return []
|
||||
|
||||
data = json.loads(res_json)
|
||||
@@ -202,6 +246,11 @@ def try_get_local_image_keys() -> List[Dict[str, Any]]:
|
||||
"xor_key": f"0x{int(xor_key):02X}",
|
||||
"aes_key": aes_key
|
||||
})
|
||||
logger.info(
|
||||
"[image_key] 本地算法完成:accounts=%s results=%s",
|
||||
len(accounts),
|
||||
[_summarize_key_payload(item) for item in results],
|
||||
)
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"本地提取图片密钥失败: {e}")
|
||||
@@ -234,6 +283,14 @@ async def get_image_key_integrated_workflow(
|
||||
except Exception:
|
||||
target_account_wxid = account
|
||||
target_account_wxid = str(target_account_wxid or "").strip().lower()
|
||||
logger.info(
|
||||
"[image_key] 开始集成流程:request_account=%s target_wxid=%s local_key_count=%s db_storage_path=%s wxid_dir=%s",
|
||||
str(account or "").strip(),
|
||||
target_account_wxid,
|
||||
len(local_keys),
|
||||
str(db_storage_path or "").strip(),
|
||||
str(wxid_dir or "").strip(),
|
||||
)
|
||||
|
||||
if local_keys:
|
||||
# 如果指定了账号,尝试在本地结果中找匹配的
|
||||
@@ -241,17 +298,29 @@ async def get_image_key_integrated_workflow(
|
||||
for k in local_keys:
|
||||
local_wxid = str(k.get("wxid") or "").strip().lower()
|
||||
if local_wxid and local_wxid == target_account_wxid:
|
||||
logger.info(
|
||||
"[image_key] 本地算法精确匹配成功:target_wxid=%s payload=%s",
|
||||
target_account_wxid,
|
||||
_summarize_key_payload(k),
|
||||
)
|
||||
upsert_account_keys_in_store(
|
||||
account=str(k.get("wxid") or "").strip(),
|
||||
image_xor_key=k['xor_key'],
|
||||
image_aes_key=k['aes_key']
|
||||
)
|
||||
return k
|
||||
logger.info(
|
||||
"[image_key] 本地算法未匹配到目标账号:target_wxid=%s local_wxids=%s",
|
||||
target_account_wxid,
|
||||
[str(item.get("wxid") or "").strip() for item in local_keys],
|
||||
)
|
||||
else:
|
||||
# 如果没指定账号,返回第一个发现的并存入 store (如果有的话)
|
||||
k = local_keys[0]
|
||||
logger.info(f"本地算法提取成功 (未指定账号,返回首个): {k['wxid']}")
|
||||
# logger.info(local_keys)
|
||||
logger.info(
|
||||
"[image_key] 未指定账号,返回本地首个结果:payload=%s",
|
||||
_summarize_key_payload(k),
|
||||
)
|
||||
upsert_account_keys_in_store(
|
||||
account=k['wxid'],
|
||||
image_xor_key=k['xor_key'],
|
||||
@@ -260,7 +329,7 @@ async def get_image_key_integrated_workflow(
|
||||
return k
|
||||
|
||||
# 2. 本地提取失败或不匹配,尝试远程解析
|
||||
logger.info("本地算法提取未命中,尝试远程 API 解析...")
|
||||
logger.info("[image_key] 本地算法未命中,尝试远程 API 解析")
|
||||
return await fetch_and_save_remote_keys(
|
||||
account,
|
||||
wxid_dir=wxid_dir,
|
||||
@@ -284,13 +353,25 @@ async def fetch_and_save_remote_keys(
|
||||
url = "https://view.free.c3o.re/api/key"
|
||||
data = {"weixinIDFolder": wxid}
|
||||
|
||||
logger.info(f"正在为账号 {wxid} 获取云端备选图片密钥...")
|
||||
logger.info(
|
||||
"[image_key] 准备请求远程密钥:request_account=%s resolved_account=%s wxid_dir=%s db_storage_path=%s",
|
||||
str(account or "").strip(),
|
||||
wxid,
|
||||
str(wx_id_dir),
|
||||
str(db_storage_path or "").strip(),
|
||||
)
|
||||
|
||||
try:
|
||||
blob1_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1="global_config")
|
||||
blob2_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1="global_config.crc")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"读取微信内部文件失败: {e}")
|
||||
logger.info(
|
||||
"[image_key] 远程请求输入文件已读取:wxid=%s global_config_bytes=%s crc_bytes=%s",
|
||||
wxid,
|
||||
len(blob1_bytes),
|
||||
len(blob2_bytes),
|
||||
)
|
||||
|
||||
files = {
|
||||
'fileBytes': ('file', blob1_bytes, 'application/octet-stream'),
|
||||
@@ -298,7 +379,7 @@ async def fetch_and_save_remote_keys(
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
logger.info("向云端 API 发送请求...")
|
||||
logger.info("[image_key] 向云端 API 发送请求:url=%s wxid=%s", url, wxid)
|
||||
response = await client.post(url, data=data, files=files)
|
||||
|
||||
if response.status_code != 200:
|
||||
@@ -307,6 +388,15 @@ async def fetch_and_save_remote_keys(
|
||||
config = response.json()
|
||||
if not config:
|
||||
raise RuntimeError("云端解析失败: 返回数据为空")
|
||||
logger.info(
|
||||
"[image_key] 收到远程响应:status_code=%s keys=%s nick_name=%s",
|
||||
response.status_code,
|
||||
{
|
||||
"xor_key": str(config.get("xorKey", config.get("xor_key", ""))),
|
||||
"aes_key": _summarize_aes_key(config.get("aesKey", config.get("aes_key", ""))),
|
||||
},
|
||||
str(config.get("nickName", config.get("nick_name", ""))),
|
||||
)
|
||||
|
||||
# 新 API 的字段兼容处理
|
||||
xor_raw = str(config.get("xorKey", config.get("xor_key", "")))
|
||||
@@ -326,6 +416,12 @@ async def fetch_and_save_remote_keys(
|
||||
image_xor_key=xor_hex_str,
|
||||
image_aes_key=aes_val
|
||||
)
|
||||
logger.info(
|
||||
"[image_key] 远程密钥已保存:account=%s xor_key=%s aes_key=%s",
|
||||
wxid,
|
||||
xor_hex_str,
|
||||
_summarize_aes_key(aes_val),
|
||||
)
|
||||
|
||||
return {
|
||||
"wxid": wxid,
|
||||
|
||||
@@ -2,12 +2,23 @@ from typing import Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from ..logging_config import get_logger
|
||||
from ..key_store import get_account_keys_from_store
|
||||
from ..key_service import get_db_key_workflow, get_image_key_integrated_workflow
|
||||
from ..media_helpers import _load_media_keys, _resolve_account_dir
|
||||
from ..path_fix import PathFixRoute
|
||||
|
||||
router = APIRouter(route_class=PathFixRoute)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _summarize_aes_key(value: str) -> str:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if len(raw) <= 8:
|
||||
return raw
|
||||
return f"{raw[:4]}...{raw[-4:]}(len={len(raw)})"
|
||||
|
||||
|
||||
@router.get("/api/keys", summary="获取账号已保存的密钥")
|
||||
@@ -23,6 +34,13 @@ async def get_saved_keys(account: Optional[str] = None):
|
||||
# 账号可能尚未解密;仍允许从全局 store 读取(如果传入了 account)
|
||||
account_name = str(account or "").strip() or None
|
||||
|
||||
logger.info(
|
||||
"[keys] get_saved_keys start: request_account=%s resolved_account=%s account_dir=%s",
|
||||
str(account or "").strip(),
|
||||
str(account_name or ""),
|
||||
str(account_dir) if account_dir else "",
|
||||
)
|
||||
|
||||
keys: dict = {}
|
||||
if account_name:
|
||||
keys = get_account_keys_from_store(account_name)
|
||||
@@ -45,6 +63,14 @@ async def get_saved_keys(account: Optional[str] = None):
|
||||
"image_aes_key": str(keys.get("image_aes_key") or "").strip(),
|
||||
"updated_at": str(keys.get("updated_at") or "").strip(),
|
||||
}
|
||||
logger.info(
|
||||
"[keys] get_saved_keys done: account=%s db_key_present=%s xor_key=%s aes_key=%s updated_at=%s",
|
||||
str(account_name or ""),
|
||||
bool(result["db_key"]),
|
||||
result["image_xor_key"],
|
||||
_summarize_aes_key(result["image_aes_key"]),
|
||||
result["updated_at"],
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -101,11 +127,24 @@ async def get_image_key(
|
||||
4. 解析返回流,自动存入本地数据库
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
"[keys] get_image_key start: request_account=%s db_storage_path=%s wxid_dir=%s",
|
||||
str(account or "").strip(),
|
||||
str(db_storage_path or "").strip(),
|
||||
str(wxid_dir or "").strip(),
|
||||
)
|
||||
result = await get_image_key_integrated_workflow(
|
||||
account,
|
||||
db_storage_path=db_storage_path,
|
||||
wxid_dir=wxid_dir,
|
||||
)
|
||||
logger.info(
|
||||
"[keys] get_image_key done: request_account=%s response_account=%s xor_key=%s aes_key=%s",
|
||||
str(account or "").strip(),
|
||||
str(result.get("wxid") or "").strip(),
|
||||
str(result.get("xor_key") or "").strip(),
|
||||
_summarize_aes_key(str(result.get("aes_key") or "").strip()),
|
||||
)
|
||||
|
||||
return {
|
||||
"status": 0,
|
||||
@@ -118,6 +157,12 @@ async def get_image_key(
|
||||
}
|
||||
}
|
||||
except FileNotFoundError as e:
|
||||
logger.exception(
|
||||
"[keys] get_image_key file missing: request_account=%s db_storage_path=%s wxid_dir=%s",
|
||||
str(account or "").strip(),
|
||||
str(db_storage_path or "").strip(),
|
||||
str(wxid_dir or "").strip(),
|
||||
)
|
||||
return {
|
||||
"status": -1,
|
||||
"errmsg": f"文件缺失: {str(e)}",
|
||||
@@ -126,6 +171,12 @@ async def get_image_key(
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
logger.exception(
|
||||
"[keys] get_image_key failed: request_account=%s db_storage_path=%s wxid_dir=%s",
|
||||
str(account or "").strip(),
|
||||
str(db_storage_path or "").strip(),
|
||||
str(wxid_dir or "").strip(),
|
||||
)
|
||||
return {
|
||||
"status": -1,
|
||||
"errmsg": f"获取失败: {str(e)}",
|
||||
|
||||
@@ -27,6 +27,26 @@ logger = get_logger(__name__)
|
||||
router = APIRouter(route_class=PathFixRoute)
|
||||
|
||||
|
||||
def _summarize_aes_key(value: Optional[str]) -> str:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if len(raw) <= 8:
|
||||
return raw
|
||||
return f"{raw[:4]}...{raw[-4:]}(len={len(raw)})"
|
||||
|
||||
|
||||
def _summarize_media_keys(*, xor_key: Optional[str] = None, aes_key: Optional[str] = None) -> dict:
|
||||
xor_str = str(xor_key or "").strip()
|
||||
aes_str = str(aes_key or "").strip()
|
||||
return {
|
||||
"xor_key": xor_str,
|
||||
"aes_key": _summarize_aes_key(aes_str),
|
||||
"has_xor": bool(xor_str),
|
||||
"has_aes": bool(aes_str),
|
||||
}
|
||||
|
||||
|
||||
class MediaKeysSaveRequest(BaseModel):
|
||||
"""媒体密钥保存请求模型(用户手动提供)"""
|
||||
|
||||
@@ -52,6 +72,12 @@ async def save_media_keys_api(request: MediaKeysSaveRequest):
|
||||
- aes_key: AES密钥(可选,至少16个字符;V4-V2需要)
|
||||
"""
|
||||
account_dir = _resolve_account_dir(request.account)
|
||||
logger.info(
|
||||
"[media] save_media_keys start: request_account=%s resolved_account=%s keys=%s",
|
||||
str(request.account or "").strip(),
|
||||
account_dir.name,
|
||||
_summarize_media_keys(xor_key=request.xor_key, aes_key=request.aes_key),
|
||||
)
|
||||
|
||||
# 解析XOR密钥
|
||||
try:
|
||||
@@ -76,6 +102,11 @@ async def save_media_keys_api(request: MediaKeysSaveRequest):
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(
|
||||
"[media] save_media_keys done: account=%s keys=%s",
|
||||
account_dir.name,
|
||||
_summarize_media_keys(xor_key=f"0x{xor_int:02X}", aes_key=aes_str[:16] if aes_str else ""),
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -99,6 +130,12 @@ async def decrypt_all_media(request: MediaDecryptRequest):
|
||||
"""
|
||||
account_dir = _resolve_account_dir(request.account)
|
||||
wxid_dir = _resolve_account_wxid_dir(account_dir)
|
||||
logger.info(
|
||||
"[media] decrypt_all start: request_account=%s resolved_account=%s provided_keys=%s",
|
||||
str(request.account or "").strip(),
|
||||
account_dir.name,
|
||||
_summarize_media_keys(xor_key=request.xor_key, aes_key=request.aes_key),
|
||||
)
|
||||
|
||||
if not wxid_dir:
|
||||
raise HTTPException(
|
||||
@@ -125,12 +162,28 @@ async def decrypt_all_media(request: MediaDecryptRequest):
|
||||
# 如果未提供密钥,尝试从缓存加载
|
||||
if xor_key_int is None or aes_key16 is None:
|
||||
cached = _load_media_keys(account_dir)
|
||||
logger.info(
|
||||
"[media] decrypt_all cache lookup: account=%s cached_keys=%s",
|
||||
account_dir.name,
|
||||
_summarize_media_keys(
|
||||
xor_key=f"0x{int(cached.get('xor')):02X}" if cached.get("xor") is not None else "",
|
||||
aes_key=str(cached.get("aes") or "").strip(),
|
||||
),
|
||||
)
|
||||
if xor_key_int is None:
|
||||
xor_key_int = cached.get("xor")
|
||||
if aes_key16 is None:
|
||||
aes_str = str(cached.get("aes") or "").strip()
|
||||
if len(aes_str) >= 16:
|
||||
aes_key16 = aes_str[:16].encode("ascii", errors="ignore")
|
||||
logger.info(
|
||||
"[media] decrypt_all effective_keys: account=%s keys=%s",
|
||||
account_dir.name,
|
||||
_summarize_media_keys(
|
||||
xor_key=f"0x{int(xor_key_int):02X}" if xor_key_int is not None else "",
|
||||
aes_key=(aes_key16 or b"").decode("ascii", errors="ignore") if aes_key16 else "",
|
||||
),
|
||||
)
|
||||
|
||||
if xor_key_int is None:
|
||||
raise HTTPException(
|
||||
@@ -267,6 +320,12 @@ async def decrypt_all_media_stream(
|
||||
|
||||
account_dir = _resolve_account_dir(account)
|
||||
wxid_dir = _resolve_account_wxid_dir(account_dir)
|
||||
logger.info(
|
||||
"[media] decrypt_all_stream start: request_account=%s resolved_account=%s provided_keys=%s",
|
||||
str(account or "").strip(),
|
||||
account_dir.name,
|
||||
_summarize_media_keys(xor_key=xor_key, aes_key=aes_key),
|
||||
)
|
||||
|
||||
if not wxid_dir:
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': '未找到微信数据目录'})}\n\n"
|
||||
@@ -292,12 +351,28 @@ async def decrypt_all_media_stream(
|
||||
# 如果未提供密钥,尝试从缓存加载
|
||||
if xor_key_int is None or aes_key16 is None:
|
||||
cached = _load_media_keys(account_dir)
|
||||
logger.info(
|
||||
"[media] decrypt_all_stream cache lookup: account=%s cached_keys=%s",
|
||||
account_dir.name,
|
||||
_summarize_media_keys(
|
||||
xor_key=f"0x{int(cached.get('xor')):02X}" if cached.get("xor") is not None else "",
|
||||
aes_key=str(cached.get("aes") or "").strip(),
|
||||
),
|
||||
)
|
||||
if xor_key_int is None:
|
||||
xor_key_int = cached.get("xor")
|
||||
if aes_key16 is None:
|
||||
aes_str = str(cached.get("aes") or "").strip()
|
||||
if len(aes_str) >= 16:
|
||||
aes_key16 = aes_str[:16].encode("ascii", errors="ignore")
|
||||
logger.info(
|
||||
"[media] decrypt_all_stream effective_keys: account=%s keys=%s",
|
||||
account_dir.name,
|
||||
_summarize_media_keys(
|
||||
xor_key=f"0x{int(xor_key_int):02X}" if xor_key_int is not None else "",
|
||||
aes_key=(aes_key16 or b"").decode("ascii", errors="ignore") if aes_key16 else "",
|
||||
),
|
||||
)
|
||||
|
||||
if xor_key_int is None:
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': '未找到XOR密钥,请先使用 wx_key 获取并保存/填写'})}\n\n"
|
||||
|
||||
Reference in New Issue
Block a user