Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Python
__pycache__/
.ruff_cache/
*.py[cod]
*$py.class
*.egg-info/
Expand Down
62 changes: 27 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,28 @@ Note: Make sure you have Node.js installed first. You can ask your agent to set

### Step 1 — Initialize

Make sure WeChat is running, then:
Start with a read-only check. It does not read WeChat process memory:

```bash
# macOS/Linux: may need sudo for memory scanning
sudo wechat-cli init
wechat-cli doctor
wechat-cli init --dry-run
```

`init` now shows a plan by default and does not automatically scan process memory. Prefer an audited helper whose key document is piped only over stdin:

# Windows: run in a terminal with sufficient privileges
wechat-cli init
```bash
trusted-wechat-key-helper | wechat-cli keys import --stdin
```

This auto-detects your WeChat data directory, extracts encryption keys, and saves config to `~/.wechat-cli/`.
Every imported key must pass HMAC verification against its real database first page before state is saved under `~/.wechat-cli/`. A new config/key generation is fully written and read back before `active.json` is switched. Windows uses CurrentUser DPAPI, macOS uses the login Keychain, and Linux uses Secret Service; initialization fails closed when no system key store is available.

Process-memory scanning is a high-risk compatibility path. It requires both `--scan-memory --confirm-memory-scan`, plus an exact accepted capability profile binding platform, client version, valid publisher signature, and scanner algorithm. Unknown clients stop before process-memory read access is requested. No scanner profile is accepted yet, so current builds will not perform a memory scan.

When upgrading from a release with `~/.wechat-cli/all_keys.json`, run `wechat-cli init --force` to migrate. The CLI no longer silently accepts a legacy plaintext key file by default.

![init-claude-code-1](image/init-claude-code-1.png)

On macOS, you'll need to run the `sudo` command and enter your password:
On macOS/Linux, the scanner helper may ask for an elevation password:

![init-claude-code-2](image/init-claude-code-2.png)

Expand All @@ -123,30 +130,7 @@ Without this permission, the tool cannot access WeChat's data directory and key

On some macOS systems, `init` may fail with `task_for_pid failed` even when running with `sudo`. This is due to macOS security restrictions on process memory access.

**WeChat CLI will automatically attempt to fix this** by re-signing WeChat with the required entitlement (original entitlements are preserved). Just follow the on-screen instructions:

1. The tool will re-sign WeChat automatically
2. Quit WeChat completely (not just minimize)
3. Reopen WeChat and log in
4. Run `sudo wechat-cli init` again

If auto re-signing fails, you can do it manually:

```bash
# Quit WeChat first, then:
sudo codesign --force --sign - --entitlements /dev/stdin /Applications/WeChat.app <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
EOF
```

> **Heads up:** Re-signing WeChat is safe and will **not** cause account issues or bans. However, it may affect WeChat's auto-update mechanism. If you notice any feature not working properly, or want to update WeChat to the latest version, simply re-download and reinstall WeChat from the [official website](https://mac.weixin.qq.com/) — no need to re-run `init`, your existing config and keys will continue to work.
WeChat CLI fails closed and leaves `WeChat.app` untouched. It never re-signs, injects into, or modifies the third-party app. Any debugging entitlement must be assessed and applied explicitly by the device administrator. The bundled arm64 scanner is a historical artifact and is rejected until rebuilt from the current C source with the redaction artifact gate; no x86_64 artifact is currently shipped.

### Step 2 — Use It

Expand Down Expand Up @@ -337,8 +321,8 @@ The `--type` option (on `history` and `search`):

| Platform | Status | Notes |
|----------|--------|-------|
| macOS (Apple Silicon) | ✅ Supported | Bundled arm64 binary |
| macOS (Intel) | ✅ Supported | x86_64 binary needed |
| macOS (Apple Silicon) | ⚠️ Rebuild required | Historical arm64 artifact is rejected by the safety gate |
| macOS (Intel) | ❌ Not shipped | No x86_64 artifact is present |
| Windows | ✅ Supported | Reads Weixin.exe process memory |
| Linux | ✅ Supported | Reads /proc/pid/mem, requires root |

Expand All @@ -348,10 +332,18 @@ The `--type` option (on `history` and `search`):

WeChat stores chat data in SQLCipher-encrypted SQLite databases locally. WeChat CLI:

1. **Extracts keys** — scans WeChat process memory for encryption keys (`init`)
2. **Decrypts on-the-fly** — transparent page-level AES-256-CBC decryption with caching
1. **Obtains keys under policy** — imports audited-helper output through stdin and validates it against real database pages; scanning is limited to accepted clients
2. **Decrypts on-the-fly** — transparent page-level AES-256-CBC decryption in a private per-process cache removed on normal exit
3. **Queries locally** — all data stays on your machine, no network access

### Key and cache security boundary

- Initialization is stored under `~/.wechat-cli/generations/<id>/`; `active.json` changes only after config and key readback succeeds.
- `doctor`, `init --dry-run`, and default `init` never read WeChat process memory; unknown clients fail closed before process-memory read access.
- Initialization and scanner logs report matching state, database paths, and counts without echoing complete keys.
- Windows uses CurrentUser DPAPI and explicit NTFS ACLs; macOS uses the login Keychain; Linux uses Secret Service. The default is fail-closed when no system key store exists. `WECHAT_CLI_ALLOW_PLAINTEXT_KEYS=1` explicitly enables the compatibility plaintext file backend.
- Decryption uses a stable DB/WAL snapshot and verifies SQLCipher page HMACs, SQLite WAL checksums, and the last commit frame. Plaintext cache directories hold an exclusive lease; startup removes only well-formed stale runs that are confirmed unlocked.

---

## 📄 License
Expand Down
66 changes: 29 additions & 37 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,28 @@ pip install -e .

### 第一步 — 初始化

确保微信正在运行,然后
先执行只读检查;它不会读取微信进程内存

```bash
# macOS/Linux: 可能需要 sudo 权限
sudo wechat-cli init
wechat-cli doctor
wechat-cli init --dry-run
```

`init` 默认也只显示计划,不再自动扫描进程内存。推荐由经过审计的独立 helper 生成密钥文档,并仅通过标准输入导入:

# Windows: 在有足够权限的终端中运行
wechat-cli init
```bash
trusted-wechat-key-helper | wechat-cli keys import --stdin
```

这一步会自动检测微信数据目录、提取加密密钥,并保存到 `~/.wechat-cli/`。
导入时每个密钥都必须通过对应真实数据库首页的 HMAC 校验,之后才会保存到 `~/.wechat-cli/`。新配置与密钥先写入独立 generation,完整读回后才原子切换。Windows 使用 CurrentUser DPAPI,macOS 使用登录钥匙串,Linux 使用 Secret Service;系统密钥库不可用时默认拒绝写入明文密钥。

内存扫描属于高风险兼容路径,必须同时使用 `--scan-memory --confirm-memory-scan`,并且客户端平台、精确版本、有效发布者签名和扫描算法必须存在于已验收能力清单;未知版本会在申请进程内存读取权限前停止。目前能力清单没有已验收版本,因此不会执行内存扫描。

从旧版本升级且已有 `~/.wechat-cli/all_keys.json` 时,执行 `wechat-cli init --force` 迁移;默认不会继续静默使用旧版明文密钥文件。

![init-claude-code-1](image/init-claude-code-1.png)

如果是 mac,需要执行 sudo 命令,然后需要输入密码
macOS/Linux 扫描 helper 请求提权时,系统可能要求输入密码

![init-claude-code-code-2](image/init-claude-code-2.png)

Expand All @@ -121,30 +128,7 @@ wechat-cli init

在某些 macOS 系统上,即使使用了 `sudo`,`init` 也可能报 `task_for_pid failed`。这是 macOS 的安全策略限制了进程内存访问。

**WeChat CLI 会自动尝试修复此问题**——对微信重新签名以获取必要权限(会保留微信原有权限)。按提示操作即可:

1. 工具会自动对微信重新签名
2. 完全退出微信(不是最小化)
3. 重新打开微信并登录
4. 再次执行 `sudo wechat-cli init`

如果自动签名失败,可以手动执行:

```bash
# 先退出微信,然后:
sudo codesign --force --sign - --entitlements /dev/stdin /Applications/WeChat.app <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
EOF
```

> **温馨提示:** 重新签名是安全的,**不会**导致封号或账号异常。但可能影响微信的部分功能或自动更新。如果发现任何功能异常(如搜一搜无法使用),或想更新到微信最新版,直接从[微信官网](https://mac.weixin.qq.com/)重新下载安装即可,**无需重新执行 init**,已有的配置和密钥不受影响。
WeChat CLI 会失败关闭并保留原始 `WeChat.app`,不会自动重签、注入或修改第三方应用。调试权限如确有必要,必须由设备管理员独立评估并显式操作。当前仓库内置的 arm64 扫描器是历史产物;在 macOS 上从当前 C 源码重建并通过脱敏产物门禁前,CLI 会拒绝执行它。仓库目前没有 x86_64 产物。

### 第二步 — 开始使用

Expand Down Expand Up @@ -335,21 +319,29 @@ wechat-cli new-messages # 后续: 仅返回上次以来的新

| 平台 | 状态 | 说明 |
|------|------|------|
| macOS (Apple Silicon) | ✅ 支持 | 内置 arm64 二进制 |
| macOS (Intel) | ✅ 支持 | 需要 x86_64 二进制 |
| Windows | ✅ 支持 | 读取 Weixin.exe 进程内存 |
| Linux | ✅ 支持 | 读取 /proc/pid/mem,需要 root |
| macOS (Apple Silicon) | ⚠️ 需重建 | 历史 arm64 产物被安全门禁拒绝,须从当前源码重建 |
| macOS (Intel) | ❌ 未提供 | 仓库没有 x86_64 二进制 |
| Windows | ⚠️ 查询/导入可用 | 内存扫描须命中已验收的精确版本能力清单 |
| Linux | ⚠️ 查询/导入可用 | 内存扫描须命中已验收的精确版本能力清单 |

---

## 🔧 工作原理

微信将聊天数据存储在本地的 SQLCipher 加密 SQLite 数据库中。WeChat CLI:

1. **提取密钥** — 扫描微信进程内存获取加密密钥(`init`)
2. **即时解密** — 透明页级 AES-256-CBC 解密,带缓存
1. **受控取得密钥** — 默认使用受信 helper 标准输入导入并以真实数据库页校验;内存扫描仅限已验收版本
2. **即时解密** — 透明页级 AES-256-CBC 解密;解密缓存只存于本次进程的私有目录,正常退出时删除
3. **本地查询** — 所有数据留在本机,无需网络访问

### 密钥与缓存安全边界

- 初始化结果保存在 `~/.wechat-cli/generations/<id>/`,`active.json` 只在配置与密钥全部读回成功后切换。
- `doctor`、`init --dry-run` 和默认 `init` 不会读取微信进程内存;未知客户端版本在申请进程内存读取权限前失败关闭。
- 初始化和扫描日志只显示匹配状态、数据库路径和密钥数量,不回显完整密钥。
- Windows 使用 CurrentUser DPAPI 并显式收紧 NTFS ACL;macOS 使用登录钥匙串;Linux 使用 Secret Service。若系统密钥库不可用,默认失败关闭。仅可通过 `WECHAT_CLI_ALLOW_PLAINTEXT_KEYS=1` 显式启用兼容明文文件后端。
- 解密前先复制稳定的 DB/WAL 快照;SQLCipher 页 HMAC、SQLite WAL checksum 与最后提交帧均通过后才应用。明文缓存位于带独占租约的 `cache/run-*`;启动时只清理确认无人持锁的规范遗留目录。

---

## 📄 开源协议
Expand Down
87 changes: 58 additions & 29 deletions npm/scripts/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"""Build wechat-cli standalone binaries with PyInstaller."""

import os
import shutil
import subprocess
import sys
from pathlib import Path
Expand All @@ -20,6 +19,37 @@
}


def current_platform():
import platform as host

system = host.system().lower()
machine = host.machine().lower()
if system == "darwin":
return "darwin-arm64" if machine == "arm64" else "darwin-x64"
if system == "windows" and machine in {"amd64", "x86_64"}:
return "win32-x64"
if system == "linux":
return "linux-arm64" if machine in {"arm64", "aarch64"} else "linux-x64"
raise RuntimeError(f"Unsupported build host: {system}-{machine}")


def build_macos_scanner(platform):
source = ROOT / "wechat_cli" / "bin" / "find_all_keys_macos.c"
arch = "arm64" if platform == "darwin-arm64" else "x86_64"
output_dir = ROOT / "build" / "native" / platform
output_dir.mkdir(parents=True, exist_ok=True)
output = output_dir / f"find_all_keys_macos.{arch}"
subprocess.check_call([
"/usr/bin/clang", "-O2", "-arch", arch, str(source),
"-framework", "Foundation", "-o", str(output),
])
payload = output.read_bytes()
if b"[REDACTED]" not in payload or b"%-25s %-66s %s" in payload:
output.unlink(missing_ok=True)
raise RuntimeError("macOS scanner failed the redaction artifact gate")
return output


def ensure_pyinstaller():
try:
import PyInstaller # noqa: F401
Expand All @@ -31,7 +61,10 @@ def ensure_pyinstaller():


def build_platform(platform: str):
info = PLATFORM_MAP[platform]
host_platform = current_platform()
if platform != host_platform:
print(f"[-] Refusing cross-labelled build: host={host_platform}, target={platform}")
return False
os_name, arch = platform.split("-")
ext = ".exe" if os_name == "win32" else ""
binary_name = f"wechat-cli{ext}"
Expand All @@ -54,12 +87,14 @@ def build_platform(platform: str):
"--clean",
]

# Bundle C binaries for key extraction
bin_dir = ROOT / "wechat_cli" / "bin"
if bin_dir.exists():
for f in bin_dir.iterdir():
if not f.name.startswith(".") and f.is_file():
cmd.extend(["--add-binary", f"{f}:wechat_cli/bin"])
# macOS scanner must be rebuilt from the current source on the matching host.
if os_name == "darwin":
try:
scanner = build_macos_scanner(platform)
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
print(f"[-] macOS scanner build/gate failed: {error}")
return False
cmd.extend(["--add-binary", f"{scanner}:wechat_cli/bin"])

# Hidden imports
hidden = ["pysqlcipher3", "sqlcipher3", "Cryptodome", "zstandard"]
Expand Down Expand Up @@ -91,29 +126,23 @@ def main():
platforms = sys.argv[1:]
else:
# Default: build for current platform only
import platform as _pf
current = f"{_pf.system().lower()}-{_pf.machine()}"
# Normalize
if current == "darwin-arm64":
platforms = ["darwin-arm64"]
elif current == "darwin-x86_64" or current == "darwin-amd64":
platforms = ["darwin-x64"]
else:
# Try to match
platforms = []
for p in PLATFORM_MAP:
os_name, arch = p.split("-")
if os_name in current and (arch in current or
(arch == "x64" and ("x86_64" in current or "amd64" in current))):
platforms = [p]
break
if not platforms:
print(f"Cannot determine platform from '{current}'")
print(f"Usage: {sys.argv[0]} [platform...]")
print(f" Platforms: {', '.join(PLATFORM_MAP.keys())}")
sys.exit(1)
try:
platforms = [current_platform()]
except RuntimeError as error:
print(error)
print(f"Usage: {sys.argv[0]} [platform...]")
print(f" Platforms: {', '.join(PLATFORM_MAP.keys())}")
sys.exit(1)

print(f"[+] Building for: {', '.join(platforms)}")
unknown = [name for name in platforms if name not in PLATFORM_MAP]
if unknown:
print(f"[-] Unknown platform(s): {', '.join(unknown)}")
sys.exit(1)
host_platform = current_platform()
if any(name != host_platform for name in platforms):
print(f"[-] Refusing cross-labelled build: host={host_platform}, targets={platforms}")
sys.exit(1)
ensure_pyinstaller()

results = {}
Expand Down
Loading