mirror of
https://github.com/jackwener/wx-cli.git
synced 2026-10-09 00:25:46 +00:00
feat: Rust 完整重写 wx-cli(单一二进制,支持 macOS/Linux/Windows)
实现所有核心模块: - src/crypto/: SQLCipher 4 页解密 + WAL 应用(AES-256-CBC) - src/scanner/: 三平台内存扫描(macOS Mach VM / Linux /proc/mem / Windows ReadProcessMemory) - src/daemon/: tokio 异步 daemon,Unix socket IPC,mtime-aware DB 缓存,WAL 监听推送 - src/cli/: clap CLI,自动启动 daemon,完整命令实现 - src/config.rs: 跨平台配置加载,兼容 Python 版 config.json 格式 - src/ipc.rs: 换行符分隔 JSON 协议,与 Python 版兼容 - .github/workflows/release.yml: 四平台自动构建发布 cargo build --release 验证通过,生成 4.8MB macOS arm64 单一二进制
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
pub mod wal;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use aes::Aes256;
|
||||
use cbc::Decryptor;
|
||||
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
|
||||
use std::path::Path;
|
||||
|
||||
pub const PAGE_SZ: usize = 4096;
|
||||
pub const SALT_SZ: usize = 16;
|
||||
pub const RESERVE_SZ: usize = 80; // IV(16) + HMAC(64)
|
||||
|
||||
/// SQLite 文件头魔数(16字节)
|
||||
pub const SQLITE_HDR: &[u8] = b"SQLite format 3\x00";
|
||||
|
||||
type Aes256CbcDec = Decryptor<Aes256>;
|
||||
|
||||
/// 解密单个 SQLCipher 4 页
|
||||
///
|
||||
/// - `enc_key`: 32字节 AES 密钥
|
||||
/// - `page_data`: 原始加密页面数据(PAGE_SZ 字节)
|
||||
/// - `pgno`: 页码(从1开始)
|
||||
///
|
||||
/// 返回解密后的完整页面(PAGE_SZ 字节)
|
||||
pub fn decrypt_page(enc_key: &[u8; 32], page_data: &[u8], pgno: u32) -> Result<Vec<u8>> {
|
||||
if page_data.len() < PAGE_SZ {
|
||||
bail!("页面数据不足 {} 字节", PAGE_SZ);
|
||||
}
|
||||
|
||||
// IV 位于页面末尾 RESERVE_SZ 区域的前16字节
|
||||
let iv_offset = PAGE_SZ - RESERVE_SZ;
|
||||
let iv: &[u8; 16] = page_data[iv_offset..iv_offset + 16]
|
||||
.try_into()
|
||||
.expect("IV 长度固定为 16");
|
||||
|
||||
let mut result = vec![0u8; PAGE_SZ];
|
||||
|
||||
if pgno == 1 {
|
||||
// 第一页:跳过 salt(16字节),解密 [SALT_SZ..PAGE_SZ-RESERVE_SZ]
|
||||
let enc = &page_data[SALT_SZ..PAGE_SZ - RESERVE_SZ];
|
||||
let dec = aes_cbc_decrypt(enc_key, iv, enc)?;
|
||||
// 写入 SQLite 文件头
|
||||
result[..16].copy_from_slice(SQLITE_HDR);
|
||||
// 写入解密数据(从第16字节开始)
|
||||
result[16..PAGE_SZ - RESERVE_SZ].copy_from_slice(&dec);
|
||||
// 末尾 RESERVE_SZ 字节补零
|
||||
// (已经是零,无需显式操作)
|
||||
} else {
|
||||
// 其他页:解密 [0..PAGE_SZ-RESERVE_SZ]
|
||||
let enc = &page_data[..PAGE_SZ - RESERVE_SZ];
|
||||
let dec = aes_cbc_decrypt(enc_key, iv, enc)?;
|
||||
result[..PAGE_SZ - RESERVE_SZ].copy_from_slice(&dec);
|
||||
// 末尾 RESERVE_SZ 字节补零
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// AES-256-CBC 解密(不去除 padding,SQLCipher 不使用 PKCS#7 padding)
|
||||
fn aes_cbc_decrypt(key: &[u8; 32], iv: &[u8; 16], data: &[u8]) -> Result<Vec<u8>> {
|
||||
if data.is_empty() || data.len() % 16 != 0 {
|
||||
bail!("密文长度不是 AES 块大小的倍数: {}", data.len());
|
||||
}
|
||||
let mut buf = data.to_vec();
|
||||
// 使用 raw 模式不处理 padding
|
||||
Aes256CbcDec::new(key.into(), iv.into())
|
||||
.decrypt_blocks_mut(unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
buf.as_mut_ptr() as *mut aes::cipher::Block<Aes256>,
|
||||
buf.len() / 16,
|
||||
)
|
||||
});
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// 完整解密一个 SQLCipher 数据库文件
|
||||
///
|
||||
/// 读取 `db_path`,按 PAGE_SZ 分页解密,写入 `out_path`
|
||||
pub fn full_decrypt(db_path: &Path, out_path: &Path, enc_key: &[u8; 32]) -> Result<()> {
|
||||
let data = std::fs::read(db_path)?;
|
||||
if data.is_empty() {
|
||||
bail!("数据库文件为空: {}", db_path.display());
|
||||
}
|
||||
|
||||
if let Some(parent) = out_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let total_pages = (data.len() + PAGE_SZ - 1) / PAGE_SZ;
|
||||
let mut out = Vec::with_capacity(data.len());
|
||||
|
||||
for pgno in 1..=total_pages {
|
||||
let offset = (pgno - 1) * PAGE_SZ;
|
||||
let end = std::cmp::min(offset + PAGE_SZ, data.len());
|
||||
let mut page = data[offset..end].to_vec();
|
||||
// 不足一页则补零
|
||||
if page.len() < PAGE_SZ {
|
||||
page.resize(PAGE_SZ, 0);
|
||||
}
|
||||
let dec = decrypt_page(enc_key, &page, pgno as u32)?;
|
||||
out.extend_from_slice(&dec);
|
||||
}
|
||||
|
||||
std::fs::write(out_path, &out)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use anyhow::Result;
|
||||
use std::io::{SeekFrom, Seek, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use super::{decrypt_page, PAGE_SZ};
|
||||
|
||||
pub const WAL_HDR_SZ: usize = 32;
|
||||
pub const WAL_FRAME_HDR: usize = 24;
|
||||
|
||||
/// 将 WAL 文件中的变更应用到已解密的数据库文件
|
||||
///
|
||||
/// WAL 格式(SQLite 标准,SQLCipher 4 的 WAL 帧也被加密):
|
||||
/// - WAL header (32 bytes): magic(4) + format(4) + page_sz(4) + ckpt_seq(4) + salt1(4) + salt2(4) + cksum1(4) + cksum2(4)
|
||||
/// - 每帧:frame_header(24 bytes) + page_data(PAGE_SZ bytes)
|
||||
/// - frame_header: pgno(4) + commit_pgcnt(4) + salt1(4) + salt2(4) + cksum1(4) + cksum2(4)
|
||||
pub fn apply_wal(wal_path: &Path, out_path: &Path, enc_key: &[u8; 32]) -> Result<()> {
|
||||
if !wal_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let wal_data = std::fs::read(wal_path)?;
|
||||
if wal_data.len() <= WAL_HDR_SZ {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 读取 WAL 头中的 salt1 / salt2
|
||||
let s1 = u32::from_be_bytes(wal_data[16..20].try_into().unwrap());
|
||||
let s2 = u32::from_be_bytes(wal_data[20..24].try_into().unwrap());
|
||||
|
||||
let frame_size = WAL_FRAME_HDR + PAGE_SZ;
|
||||
let frame_area = &wal_data[WAL_HDR_SZ..];
|
||||
|
||||
// 打开输出文件做随机写
|
||||
let mut db_file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(out_path)?;
|
||||
|
||||
let mut pos = 0usize;
|
||||
while pos + frame_size <= frame_area.len() {
|
||||
let fh = &frame_area[pos..pos + WAL_FRAME_HDR];
|
||||
let page_data = &frame_area[pos + WAL_FRAME_HDR..pos + frame_size];
|
||||
|
||||
let pgno = u32::from_be_bytes(fh[0..4].try_into().unwrap());
|
||||
let fs1 = u32::from_be_bytes(fh[8..12].try_into().unwrap());
|
||||
let fs2 = u32::from_be_bytes(fh[12..16].try_into().unwrap());
|
||||
|
||||
pos += frame_size;
|
||||
|
||||
// 跳过无效页码
|
||||
if pgno == 0 || pgno > 1_000_000 {
|
||||
continue;
|
||||
}
|
||||
// salt 不匹配的帧属于已检查点或旧事务
|
||||
if fs1 != s1 || fs2 != s2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut page_buf = page_data.to_vec();
|
||||
if page_buf.len() < PAGE_SZ {
|
||||
page_buf.resize(PAGE_SZ, 0);
|
||||
}
|
||||
|
||||
let dec = decrypt_page(enc_key, &page_buf, pgno)?;
|
||||
let file_offset = (pgno as u64 - 1) * PAGE_SZ as u64;
|
||||
db_file.seek(SeekFrom::Start(file_offset))?;
|
||||
db_file.write_all(&dec)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user