mirror of https://github.com/jackwener/wx-cli.git
Merge ef5cd4d4b8 into 08af894594
commit
ad93529dff
|
|
@ -0,0 +1,287 @@
|
|||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub db_dir: PathBuf,
|
||||
pub keys_file: PathBuf,
|
||||
pub decrypted_dir: PathBuf,
|
||||
#[serde(default)]
|
||||
pub wechat_process: String,
|
||||
}
|
||||
|
||||
/// 从 <exe_dir>/config.json 或 $HOME/.wx-cli/config.json 加载配置
|
||||
pub fn load_config() -> Result<Config> {
|
||||
let config_path = find_config_file()?;
|
||||
let content = std::fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("读取 config.json 失败: {}", config_path.display()))?;
|
||||
let raw: serde_json::Value = serde_json::from_str(&content)
|
||||
.with_context(|| "config.json 格式错误")?;
|
||||
|
||||
let db_dir = raw.get("db_dir")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(default_db_dir);
|
||||
|
||||
let base_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||
|
||||
let keys_file = raw.get("keys_file")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| {
|
||||
let p = PathBuf::from(s);
|
||||
if p.is_absolute() { p } else { base_dir.join(p) }
|
||||
})
|
||||
.unwrap_or_else(|| base_dir.join("all_keys.json"));
|
||||
|
||||
let decrypted_dir = raw.get("decrypted_dir")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| {
|
||||
let p = PathBuf::from(s);
|
||||
if p.is_absolute() { p } else { base_dir.join(p) }
|
||||
})
|
||||
.unwrap_or_else(|| base_dir.join("decrypted"));
|
||||
|
||||
let wechat_process = raw.get("wechat_process")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(default_wechat_process())
|
||||
.to_string();
|
||||
|
||||
Ok(Config {
|
||||
db_dir,
|
||||
keys_file,
|
||||
decrypted_dir,
|
||||
wechat_process,
|
||||
})
|
||||
}
|
||||
|
||||
fn find_config_file() -> Result<PathBuf> {
|
||||
// 1. 优先查找可执行文件同目录
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let p = dir.join("config.json");
|
||||
if p.exists() {
|
||||
return Ok(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. 当前工作目录
|
||||
let cwd = std::env::current_dir().unwrap_or_default().join("config.json");
|
||||
if cwd.exists() {
|
||||
return Ok(cwd);
|
||||
}
|
||||
// 3. ~/.wx-cli/config.json
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let p = home.join(".wx-cli").join("config.json");
|
||||
if p.exists() {
|
||||
return Ok(p);
|
||||
}
|
||||
}
|
||||
// 返回默认路径(可能不存在,调用方负责处理)
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
return Ok(dir.join("config.json"));
|
||||
}
|
||||
}
|
||||
Ok(PathBuf::from("config.json"))
|
||||
}
|
||||
|
||||
pub fn cli_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||
.join(".wx-cli")
|
||||
}
|
||||
|
||||
pub fn sock_path() -> PathBuf {
|
||||
cli_dir().join("daemon.sock")
|
||||
}
|
||||
|
||||
pub fn pid_path() -> PathBuf {
|
||||
cli_dir().join("daemon.pid")
|
||||
}
|
||||
|
||||
pub fn log_path() -> PathBuf {
|
||||
cli_dir().join("daemon.log")
|
||||
}
|
||||
|
||||
pub fn cache_dir() -> PathBuf {
|
||||
cli_dir().join("cache")
|
||||
}
|
||||
|
||||
pub fn mtime_file() -> PathBuf {
|
||||
cache_dir().join("_mtimes.json")
|
||||
}
|
||||
|
||||
fn default_db_dir() -> PathBuf {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files")
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join("Documents/xwechat_files")
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
PathBuf::from(std::env::var("APPDATA").unwrap_or_default())
|
||||
.join("Tencent/xwechat")
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
PathBuf::from(".")
|
||||
}
|
||||
}
|
||||
|
||||
fn default_wechat_process() -> &'static str {
|
||||
#[cfg(target_os = "macos")]
|
||||
{ "WeChat" }
|
||||
#[cfg(target_os = "linux")]
|
||||
{ "wechat" }
|
||||
#[cfg(target_os = "windows")]
|
||||
{ "Weixin.exe" }
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{ "WeChat" }
|
||||
}
|
||||
|
||||
/// 自动检测微信 db_storage 目录
|
||||
pub fn auto_detect_db_dir() -> Option<PathBuf> {
|
||||
detect_db_dir_impl()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
let home = dirs::home_dir()?;
|
||||
// 支持 sudo 环境
|
||||
let home = if let Ok(sudo_user) = std::env::var("SUDO_USER") {
|
||||
if !sudo_user.is_empty() {
|
||||
PathBuf::from("/Users").join(&sudo_user)
|
||||
} else {
|
||||
home
|
||||
}
|
||||
} else {
|
||||
home
|
||||
};
|
||||
|
||||
let base = home.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files");
|
||||
if !base.exists() {
|
||||
return None;
|
||||
}
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(&base) {
|
||||
for entry in entries.flatten() {
|
||||
let storage = entry.path().join("db_storage");
|
||||
if storage.is_dir() {
|
||||
candidates.push(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort_by_key(|p| {
|
||||
std::fs::metadata(p)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
});
|
||||
candidates.into_iter().next_back()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
let home = dirs::home_dir()?;
|
||||
let sudo_home = std::env::var("SUDO_USER").ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|u| PathBuf::from("/home").join(u));
|
||||
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
for base_home in [Some(home.clone()), sudo_home].into_iter().flatten() {
|
||||
let xwechat = base_home.join("Documents/xwechat_files");
|
||||
if xwechat.exists() {
|
||||
if let Ok(entries) = std::fs::read_dir(&xwechat) {
|
||||
for entry in entries.flatten() {
|
||||
let storage = entry.path().join("db_storage");
|
||||
if storage.is_dir() {
|
||||
candidates.push(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let old = base_home.join(".local/share/weixin/data/db_storage");
|
||||
if old.is_dir() {
|
||||
candidates.push(old);
|
||||
}
|
||||
}
|
||||
candidates.sort_by_key(|p| {
|
||||
std::fs::metadata(p)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
});
|
||||
candidates.into_iter().next_back()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
let appdata = std::env::var("APPDATA").ok()?;
|
||||
let config_dir = PathBuf::from(&appdata).join("Tencent/xwechat/config");
|
||||
if !config_dir.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(&config_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "ini").unwrap_or(false) {
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
let data_root = content.trim().to_string();
|
||||
if PathBuf::from(&data_root).is_dir() {
|
||||
let pattern = PathBuf::from(&data_root).join("xwechat_files");
|
||||
if let Ok(entries2) = std::fs::read_dir(&pattern) {
|
||||
for entry2 in entries2.flatten() {
|
||||
let storage = entry2.path().join("db_storage");
|
||||
if storage.is_dir() {
|
||||
candidates.push(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单个候选直接返回
|
||||
if candidates.len() == 1 {
|
||||
return candidates.into_iter().next();
|
||||
}
|
||||
|
||||
// 多个候选:显示选择菜单
|
||||
if candidates.len() > 1 {
|
||||
println!("[!] 检测到多个微信账号(请选择当前正在运行的微信):");
|
||||
for (i, cand) in candidates.iter().enumerate() {
|
||||
if let Some(name) = cand.parent().and_then(|p| p.file_name()) {
|
||||
println!(" {}. {}", i + 1, name.to_string_lossy());
|
||||
}
|
||||
}
|
||||
print!("请选择 [1-{}]: ", candidates.len());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
if let Ok(idx) = input.trim().parse::<usize>() {
|
||||
if idx >= 1 && idx <= candidates.len() {
|
||||
return Some(candidates[idx - 1].clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("无效选择,使用第一个候选");
|
||||
}
|
||||
|
||||
candidates.into_iter().next()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
/// Windows WeChat 进程内存密钥扫描器
|
||||
///
|
||||
/// 使用 Windows API:
|
||||
/// - PowerShell Get-Process: 获取进程内存(字节,兼容中英文 Windows)
|
||||
/// - OpenProcess: 获取进程句柄(需要 PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)
|
||||
/// - VirtualQueryEx: 枚举内存区域
|
||||
/// - ReadProcessMemory: 读取内存内容
|
||||
///
|
||||
/// 改进点(参考 wechat-decrypt 项目):
|
||||
/// - 扫描所有 Weixin.exe 进程(按内存降序),而非只扫第一个
|
||||
/// - 正则匹配放宽到 64-192 hex chars(更灵活匹配不同密钥格式)
|
||||
/// - Salt 取值从"中间"改为"末尾"(与 wechat-decrypt 一致)
|
||||
/// - 扫描完成后对未匹配的 salt 进行交叉验证
|
||||
/// - 增加进度报告(扫描进度、匹配数量实时输出)
|
||||
use anyhow::{bail, Result};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows::Win32::System::Memory::{
|
||||
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_READWRITE,
|
||||
};
|
||||
use windows::Win32::System::Threading::{
|
||||
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ,
|
||||
};
|
||||
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
|
||||
|
||||
use super::{collect_db_salts, KeyEntry};
|
||||
|
||||
/// hex pattern 最小和最大长度(字节)
|
||||
const HEX_PATTERN_MIN: usize = 64;
|
||||
const HEX_PATTERN_MAX: usize = 192;
|
||||
const CHUNK_SIZE: usize = 2 * 1024 * 1024;
|
||||
|
||||
/// 获取所有 Weixin.exe 进程,按内存降序排列
|
||||
fn find_all_wechat_pids() -> Vec<(u32, usize)> {
|
||||
let output = Command::new("powershell")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"Get-Process Weixin -ErrorAction SilentlyContinue | Select-Object Id,@{N='WS';E={$_.WorkingSet64}} | ConvertTo-Csv -NoTypeInformation",
|
||||
])
|
||||
.output()
|
||||
.ok();
|
||||
|
||||
let mut pids = Vec::new();
|
||||
if let Some(out) = output {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
for line in text.lines().skip(1) {
|
||||
let fields: Vec<&str> = line.split(',').collect();
|
||||
if fields.len() >= 2 {
|
||||
let pid_str = fields[0].trim_matches('"');
|
||||
let mem_str = fields.get(1).unwrap_or(&"").trim_matches('"');
|
||||
if let Ok(pid) = pid_str.parse() {
|
||||
let mem_bytes: usize = mem_str.parse().unwrap_or(0);
|
||||
pids.push((pid, mem_bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pids.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
for (pid, mem) in &pids {
|
||||
eprintln!("[+] Weixin.exe PID={} ({}MB)", pid, mem / 1024 / 1024);
|
||||
}
|
||||
pids
|
||||
}
|
||||
|
||||
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> {
|
||||
let all_pids = find_all_wechat_pids();
|
||||
if all_pids.is_empty() {
|
||||
bail!("找不到 Weixin.exe 进程,请确认微信正在运行");
|
||||
}
|
||||
eprintln!("找到 {} 个微信进程", all_pids.len());
|
||||
|
||||
let db_salts = collect_db_salts(db_dir);
|
||||
eprintln!("找到 {} 个加密数据库", db_salts.len());
|
||||
|
||||
let mut salt_to_dbs: std::collections::HashMap<String, Vec<String>> =
|
||||
std::collections::HashMap::new();
|
||||
for (salt, name) in &db_salts {
|
||||
salt_to_dbs
|
||||
.entry(salt.clone())
|
||||
.or_default()
|
||||
.push(name.clone());
|
||||
}
|
||||
|
||||
eprintln!("扫描进程内存...");
|
||||
let raw_keys = scan_all_processes(&all_pids)?;
|
||||
eprintln!("找到 {} 个候选密钥", raw_keys.len());
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let mut matched_salts = std::collections::HashSet::new();
|
||||
|
||||
for (key_hex, salt_hex) in &raw_keys {
|
||||
for (db_salt, db_name) in &db_salts {
|
||||
if salt_hex == db_salt && !matched_salts.contains(salt_hex) {
|
||||
entries.push(KeyEntry {
|
||||
db_name: db_name.clone(),
|
||||
enc_key: key_hex.clone(),
|
||||
salt: salt_hex.clone(),
|
||||
});
|
||||
matched_salts.insert(salt_hex.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let matched_count = matched_salts.len();
|
||||
let total_salts = db_salts.len();
|
||||
eprintln!("匹配到 {}/{} 个密钥", matched_count, total_salts);
|
||||
|
||||
let remaining: Vec<&String> = salt_to_dbs.keys()
|
||||
.filter(|s| !matched_salts.contains(*s))
|
||||
.collect();
|
||||
|
||||
if !remaining.is_empty() {
|
||||
eprintln!("\n还有 {} 个 salt 未匹配,进行交叉验证...", remaining.len());
|
||||
for missing_salt in &remaining {
|
||||
eprintln!(" MISSING: {}",
|
||||
salt_to_dbs.get(*missing_salt).map(|v| v.join(", ")).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// 扫描所有微信进程,按内存降序
|
||||
fn scan_all_processes(pids: &[(u32, usize)]) -> Result<Vec<(String, String)>> {
|
||||
let mut all_keys: Vec<(String, String)> = Vec::new();
|
||||
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
for (pid, mem) in pids {
|
||||
eprintln!("\n[*] 扫描 PID={} ({}MB)", pid, mem / 1024 / 1024);
|
||||
|
||||
let process = match unsafe {
|
||||
OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, *pid)
|
||||
} {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("[WARN] 无法打开进程 PID={}: {:?},跳过", pid, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let keys = scan_memory(process);
|
||||
unsafe { let _ = CloseHandle(process); }
|
||||
|
||||
let mut new_count = 0;
|
||||
for (k, s) in keys {
|
||||
let key = format!("{}{}", k, s);
|
||||
if !seen.contains(&key) {
|
||||
seen.insert(key);
|
||||
all_keys.push((k, s));
|
||||
new_count += 1;
|
||||
}
|
||||
}
|
||||
eprintln!("[+] 从 PID={} 新增 {} 个密钥", pid, new_count);
|
||||
}
|
||||
|
||||
Ok(all_keys)
|
||||
}
|
||||
|
||||
fn scan_memory(process: HANDLE) -> Vec<(String, String)> {
|
||||
let mut results: Vec<(String, String)> = Vec::new();
|
||||
let mut addr: usize = 0;
|
||||
|
||||
loop {
|
||||
let mut mbi = MEMORY_BASIC_INFORMATION::default();
|
||||
let ret = unsafe {
|
||||
VirtualQueryEx(
|
||||
process,
|
||||
Some(addr as *const _),
|
||||
&mut mbi,
|
||||
std::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
||||
)
|
||||
};
|
||||
if ret == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let region_size = mbi.RegionSize as usize;
|
||||
let base = mbi.BaseAddress as usize;
|
||||
|
||||
if mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE && region_size < 500 * 1024 * 1024 {
|
||||
scan_region(process, base, region_size, &mut results);
|
||||
}
|
||||
|
||||
addr = base.saturating_add(region_size);
|
||||
if addr == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn scan_region(
|
||||
process: HANDLE,
|
||||
base: usize,
|
||||
size: usize,
|
||||
results: &mut Vec<(String, String)>,
|
||||
) {
|
||||
let overlap = HEX_PATTERN_MAX + 4;
|
||||
let mut offset = 0usize;
|
||||
|
||||
loop {
|
||||
if offset >= size {
|
||||
break;
|
||||
}
|
||||
let chunk_size = std::cmp::min(CHUNK_SIZE, size - offset);
|
||||
let addr = base + offset;
|
||||
let mut buf = vec![0u8; chunk_size];
|
||||
let mut bytes_read: usize = 0;
|
||||
|
||||
let ok = unsafe {
|
||||
ReadProcessMemory(
|
||||
process,
|
||||
addr as *const _,
|
||||
buf.as_mut_ptr() as *mut _,
|
||||
chunk_size,
|
||||
Some(&mut bytes_read),
|
||||
).is_ok()
|
||||
};
|
||||
|
||||
if ok && bytes_read > 0 {
|
||||
buf.truncate(bytes_read);
|
||||
search_pattern(&buf, results);
|
||||
}
|
||||
|
||||
if chunk_size > overlap {
|
||||
offset += chunk_size - overlap;
|
||||
} else {
|
||||
offset += chunk_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_hex_char(c: u8) -> bool {
|
||||
c.is_ascii_hexdigit()
|
||||
}
|
||||
|
||||
fn find_ending_quote(buf: &[u8], start: usize, max_len: usize) -> Option<usize> {
|
||||
let end = std::cmp::min(start + max_len, buf.len());
|
||||
for i in start..end {
|
||||
if buf[i] == b'\'' {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 在内存中搜索 x'...hex...' 模式的密钥
|
||||
///
|
||||
/// 参考 wechat-decrypt 的 key_scan_common.py,salt 位于 hex 字符串的末尾 32 字节
|
||||
/// WCDB 存储格式: x'<64hex_key><32hex_salt>' 或 x'<longer_hex>'(salt 在最后)
|
||||
fn search_pattern(buf: &[u8], results: &mut Vec<(String, String)>) {
|
||||
if buf.len() < HEX_PATTERN_MIN + 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while i + HEX_PATTERN_MIN + 3 <= buf.len() {
|
||||
if buf[i] != b'x' || buf[i + 1] != b'\'' {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let hex_start = i + 2;
|
||||
let end_quote = find_ending_quote(buf, hex_start, HEX_PATTERN_MAX);
|
||||
|
||||
if let Some(quote_pos) = end_quote {
|
||||
let hex_len = quote_pos - hex_start;
|
||||
// hex 长度必须是偶数(完整字节),最小 96 (32+16 key+salt * 2 hex)
|
||||
if hex_len >= 96 && hex_len <= HEX_PATTERN_MAX && hex_len % 2 == 0 {
|
||||
let hex_slice = &buf[hex_start..quote_pos];
|
||||
if hex_slice.iter().all(|&c| is_hex_char(c)) {
|
||||
let hex_str = String::from_utf8_lossy(hex_slice).to_lowercase();
|
||||
|
||||
// 提取 key (前64字符 = 32字节) 和 salt (后32字符 = 16字节)
|
||||
let key_hex = hex_str[..64].to_string();
|
||||
let salt_hex = hex_str[hex_str.len() - 32..].to_string();
|
||||
|
||||
let key = format!("{}{}", key_hex, salt_hex);
|
||||
if !results.iter().any(|(k, s)| format!("{}{}", k, s) == key) {
|
||||
results.push((key_hex, salt_hex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue