Skip to content

Commit c86bead

Browse files
yanicklandryclaude
andcommitted
fix(scanner): preserve all memory keys with _by_salt fallback
Previously, scan_keys discarded any key found in WeChat's process memory that didn't match a known DB file's salt. If DB files were inaccessible during `wx init` (permission issues, path resolution failures), zero keys were saved and decryption silently failed. Apply the same logic as khipuchat's wechat-key-extract.c: save all (key, salt) pairs found in memory unconditionally. Keys that match a DB file by salt are stored under the relative DB path as before. Keys with no match are stored under `_by_salt/<salt_hex>` so they are never lost. The daemon's DbCache::get_with_mode gains a fallback: if a DB's rel_key is not in all_keys, it reads the DB file's first 16 bytes to get the salt and retries the lookup as `_by_salt/<salt>`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 08af894 commit c86bead

4 files changed

Lines changed: 90 additions & 39 deletions

File tree

src/daemon/cache.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,6 @@ impl DbCache {
200200
}
201201

202202
pub async fn get_with_mode(&self, rel_key: &str) -> Result<Option<CacheResolve>> {
203-
let enc_key_hex = match self.all_keys.get(rel_key) {
204-
Some(k) => k.clone(),
205-
None => return Ok(None),
206-
};
207-
208203
let db_path = self.db_dir.join(
209204
rel_key
210205
.replace('\\', std::path::MAIN_SEPARATOR_STR)
@@ -214,6 +209,22 @@ impl DbCache {
214209
return Ok(None);
215210
}
216211

212+
let enc_key_hex = match self.all_keys.get(rel_key) {
213+
Some(k) => k.clone(),
214+
None => {
215+
// Fallback: key was saved as "_by_salt/<salt>" (khipuchat-style) when
216+
// the DB file wasn't accessible during `wx init`. Try to find it now by
217+
// reading the DB file's actual salt.
218+
let salt = crate::scanner::read_db_salt(&db_path);
219+
match salt.and_then(|s| {
220+
self.all_keys.get(&format!("_by_salt/{}", s)).cloned()
221+
}) {
222+
Some(k) => k,
223+
None => return Ok(None),
224+
}
225+
}
226+
};
227+
217228
let wal_path = wal_path_for(&db_path);
218229
let db_mt = mtime_nanos(&db_path);
219230
let wal_mt = if wal_path.exists() {

src/scanner/linux.rs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/// 通过 /proc/<pid>/mem 读取内存内容,
55
/// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
66
use anyhow::{Context, Result};
7+
use std::collections::HashMap;
78
use std::io::{Read, Seek, SeekFrom};
89
use std::path::Path;
910

@@ -89,21 +90,33 @@ pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
8990
}
9091
eprintln!("找到 {} 个候选密钥", raw_keys.len());
9192

93+
// 与 khipuchat 脚本行为一致:所有在内存中找到的密钥都保留,
94+
// 无法匹配 DB 文件的用 _by_salt/<salt> 保存,daemon 会做 salt 回退查找。
95+
let db_salt_map: HashMap<&str, &str> = db_salts
96+
.iter()
97+
.map(|(salt, name)| (salt.as_str(), name.as_str()))
98+
.collect();
99+
92100
let mut entries = Vec::new();
93101
for (key_hex, salt_hex) in &raw_keys {
94-
for (db_salt, db_name) in &db_salts {
95-
if salt_hex == db_salt {
96-
entries.push(KeyEntry {
97-
db_name: db_name.clone(),
98-
enc_key: key_hex.clone(),
99-
salt: salt_hex.clone(),
100-
});
101-
break;
102-
}
103-
}
102+
let db_name = db_salt_map
103+
.get(salt_hex.as_str())
104+
.map(|s| s.to_string())
105+
.unwrap_or_else(|| format!("_by_salt/{}", salt_hex));
106+
entries.push(KeyEntry {
107+
db_name,
108+
enc_key: key_hex.clone(),
109+
salt: salt_hex.clone(),
110+
});
104111
}
105112

106-
eprintln!("匹配到 {}/{} 个密钥", entries.len(), raw_keys.len());
113+
let matched = entries.iter().filter(|e| !e.db_name.starts_with("_by_salt/")).count();
114+
eprintln!(
115+
"匹配到 {}/{} 个密钥(另有 {} 个按 salt 保存)",
116+
matched,
117+
raw_keys.len(),
118+
entries.len() - matched
119+
);
107120
Ok(entries)
108121
}
109122

src/scanner/macos.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
/// 2. WeChat 需要进行 ad-hoc 签名
1111
/// 3. 在内存中搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
1212
use anyhow::{bail, Context, Result};
13+
use std::collections::HashMap;
1314
use std::path::Path;
1415

1516
use super::{collect_db_salts, KeyEntry};
@@ -141,22 +142,34 @@ pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
141142
let raw_keys = scan_memory(task)?;
142143
eprintln!("找到 {} 个候选密钥", raw_keys.len());
143144

144-
// 5. 将密钥与数据库 salt 匹配
145+
// 5. 将密钥与数据库 salt 匹配,无法匹配的用 _by_salt/<salt> 保存
146+
// 与 khipuchat 脚本行为一致:所有在内存中找到的密钥都保留,
147+
// 避免因 DB 文件权限或路径问题导致密钥丢失。
148+
let db_salt_map: HashMap<&str, &str> = db_salts
149+
.iter()
150+
.map(|(salt, name)| (salt.as_str(), name.as_str()))
151+
.collect();
152+
145153
let mut entries = Vec::new();
146154
for (key_hex, salt_hex) in &raw_keys {
147-
for (db_salt, db_name) in &db_salts {
148-
if salt_hex == db_salt {
149-
entries.push(KeyEntry {
150-
db_name: db_name.clone(),
151-
enc_key: key_hex.clone(),
152-
salt: salt_hex.clone(),
153-
});
154-
break;
155-
}
156-
}
155+
let db_name = db_salt_map
156+
.get(salt_hex.as_str())
157+
.map(|s| s.to_string())
158+
.unwrap_or_else(|| format!("_by_salt/{}", salt_hex));
159+
entries.push(KeyEntry {
160+
db_name,
161+
enc_key: key_hex.clone(),
162+
salt: salt_hex.clone(),
163+
});
157164
}
158165

159-
eprintln!("匹配到 {}/{} 个密钥", entries.len(), raw_keys.len());
166+
let matched = entries.iter().filter(|e| !e.db_name.starts_with("_by_salt/")).count();
167+
eprintln!(
168+
"匹配到 {}/{} 个密钥(另有 {} 个按 salt 保存)",
169+
matched,
170+
raw_keys.len(),
171+
entries.len() - matched
172+
);
160173
Ok(entries)
161174
}
162175

src/scanner/windows.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/// - VirtualQueryEx: 枚举内存区域
77
/// - ReadProcessMemory: 读取内存内容
88
use anyhow::{Context, Result};
9+
use std::collections::HashMap;
910
use std::path::Path;
1011
use windows::Win32::Foundation::{CloseHandle, HANDLE};
1112
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
@@ -79,20 +80,33 @@ pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
7980
let _ = CloseHandle(process);
8081
}
8182

83+
// 与 khipuchat 脚本行为一致:所有在内存中找到的密钥都保留,
84+
// 无法匹配 DB 文件的用 _by_salt/<salt> 保存,daemon 会做 salt 回退查找。
85+
let db_salt_map: HashMap<&str, &str> = db_salts
86+
.iter()
87+
.map(|(salt, name)| (salt.as_str(), name.as_str()))
88+
.collect();
89+
8290
let mut entries = Vec::new();
8391
for (key_hex, salt_hex) in &raw_keys {
84-
for (db_salt, db_name) in &db_salts {
85-
if salt_hex == db_salt {
86-
entries.push(KeyEntry {
87-
db_name: db_name.clone(),
88-
enc_key: key_hex.clone(),
89-
salt: salt_hex.clone(),
90-
});
91-
break;
92-
}
93-
}
92+
let db_name = db_salt_map
93+
.get(salt_hex.as_str())
94+
.map(|s| s.to_string())
95+
.unwrap_or_else(|| format!("_by_salt/{}", salt_hex));
96+
entries.push(KeyEntry {
97+
db_name,
98+
enc_key: key_hex.clone(),
99+
salt: salt_hex.clone(),
100+
});
94101
}
95-
eprintln!("匹配到 {}/{} 个密钥", entries.len(), raw_keys.len());
102+
103+
let matched = entries.iter().filter(|e| !e.db_name.starts_with("_by_salt/")).count();
104+
eprintln!(
105+
"匹配到 {}/{} 个密钥(另有 {} 个按 salt 保存)",
106+
matched,
107+
raw_keys.len(),
108+
entries.len() - matched
109+
);
96110
Ok(entries)
97111
}
98112

0 commit comments

Comments
 (0)