diff --git a/docs/mac-disable-sip.md b/docs/mac-disable-sip.md index aae6d55..8b7c868 100644 --- a/docs/mac-disable-sip.md +++ b/docs/mac-disable-sip.md @@ -1,5 +1,62 @@ -# macOS 数据访问说明(兼容入口) +# macOS 关闭 SIP 教程 -完整内容已移到[macOS 数据访问与系统权限](./platform/macos.md)。 +SIP(System Integrity Protection,系统完整性保护)是 macOS 的系统安全机制。关闭 SIP 会降低系统安全性,只建议在确实需要读取或调试本地微信数据时临时关闭;操作完成后,建议重新开启。 -保留此文件是为了兼容应用内已经发布的帮助链接。请不要把“关闭 SIP”当作默认安装步骤;只有当当前连接页面明确要求时才处理,并在完成后恢复系统安全设置。 +> 只在连接页面明确提示需要关闭 SIP 时才处理。首次连接失败时,先确认微信版本、账号目录和登录时机,再按本文操作。关闭 SIP 不是 TraceMemo 的常规安装步骤,也不应长期保持关闭。 + +## 准备 + +- 一台 Mac 电脑,Intel 芯片和 Apple Silicon 芯片均可。 +- 需要进入 macOS 恢复模式。 +- 请先保存正在编辑的文件,并预留一次重启时间。 + +## 关闭 SIP + +### Intel Mac + +1. 关机。 +2. 按下开机键后,立刻按住 `Command + R`。 +3. 保持按住,直到进入 macOS 恢复模式。 + +### Apple Silicon Mac(M1/M2/M3/M4/M5) + +1. 关机。 +2. 长按开机键不放。 +3. 直到出现启动选项界面后松开。 +4. 选择"选项",进入 macOS 恢复模式。 + +### 在恢复模式中执行命令 + +1. 进入恢复模式后,点击顶部菜单栏的 **Utilities(实用工具)**。 +2. 选择 **Terminal(终端)**。 +3. 在终端中输入: + +```bash +csrutil disable +``` + +4. 按回车执行。 +5. 看到关闭成功提示后,重启电脑。 + +## 确认是否生效 + +重启回到正常桌面后,打开"终端",执行: + +```bash +csrutil status +``` + +看到 `System Integrity Protection status: disabled.` 才算关闭成功。 + +若仍显示 `enabled`,说明没有生效。常见原因是没在恢复模式里执行,或系统刚做过大版本更新—— +macOS 大版本更新会把 SIP 重置回开启状态,此前关过也会失效,需要重新按上面的步骤操作。 + +## 重新开启 SIP + +拿到数据库密钥后,建议重新进入恢复模式,在终端中执行: + +```bash +csrutil enable +``` + +然后重启电脑,恢复系统安全设置。 diff --git a/docs/platform/macos.md b/docs/platform/macos.md index fedd8bb..585a0be 100644 --- a/docs/platform/macos.md +++ b/docs/platform/macos.md @@ -8,7 +8,7 @@ TraceMemo 需要读取微信本地数据。macOS 会根据系统版本、微信 1. 先启动 TraceMemo,阅读连接页面显示的当前前置条件。 2. 确认微信数据目录指向当前账号。 -3. 只在页面明确要求时处理系统授权或 SIP;按页面提示完成密钥获取后,恢复你平时使用的安全设置。 +3. 只在页面明确要求时处理系统授权或 SIP;关闭 SIP 的具体步骤见[关闭 SIP 教程](../mac-disable-sip.md),按页面提示完成密钥获取后,恢复你平时使用的安全设置。 4. 返回应用重新检测账号、数据库和图片资源状态。 不要直接复制网上针对其他微信版本的命令。系统授权失败时,记录 macOS 版本、微信版本和页面错误,再按[排障文档](../user-guide/troubleshooting.md#连接微信失败)处理。 diff --git a/src/main/key-service-mac.ts b/src/main/key-service-mac.ts index f274c60..4131230 100644 --- a/src/main/key-service-mac.ts +++ b/src/main/key-service-mac.ts @@ -134,6 +134,15 @@ export function parseXkeyHelperOutput(output: string): DatabaseKeyResult { return mapXkeyHelperFailure(rawError) } +export const SIP_ENABLED_ERROR = + 'macOS 系统完整性保护(SIP)已开启,无法自动获取数据库密钥。请先关闭 SIP,或改用手动粘贴。' + +export function parseSipEnabled(statusOutput: string): boolean { + const status = statusOutput.match(/status:\s*([a-z]+)/i)?.[1]?.toLowerCase() + if (status) return status === 'enabled' + return statusOutput.toLowerCase().includes('enabled') +} + export class KeyServiceMac { private getMacKeyRuntimeDir(): string { return path.join(app.getPath('userData'), 'key-runtime') @@ -306,7 +315,7 @@ export class KeyServiceMac { private async isSipEnabled(): Promise { try { const { stdout } = await execFileAsync('/usr/bin/csrutil', ['status']) - return stdout.toLowerCase().includes('enabled') + return parseSipEnabled(stdout) } catch { return false } @@ -346,10 +355,7 @@ export class KeyServiceMac { return { success: false, error: '自动获取密钥目前仅支持 macOS' } } if (await this.isSipEnabled()) { - return { - success: false, - error: '当前系统还未完成连接环境准备,请按页面提示完成设置。' - } + return { success: false, code: 'SIP_ENABLED', error: SIP_ENABLED_ERROR } } try { diff --git a/src/renderer/src/components/DatabaseConnectionPage.tsx b/src/renderer/src/components/DatabaseConnectionPage.tsx index d638288..43d42e0 100644 --- a/src/renderer/src/components/DatabaseConnectionPage.tsx +++ b/src/renderer/src/components/DatabaseConnectionPage.tsx @@ -491,14 +491,8 @@ export function DatabaseConnectionPage({

{isMac ? ( <> - {isIntelMac - ? 'Intel Mac 首次使用需要先准备连接环境,按页面提示完成即可。' - : 'macOS Apple 芯片首次获取密钥需要关闭 SIP,并在监听期间点击微信登录。'}{' '} - + macOS 首次获取密钥需要关闭 SIP,并在监听期间点击微信登录。{' '} + 查看说明
第 4 步之前不要登录微信,只需保留微信登录页面窗口;授权后再点「登录」。 diff --git a/tests/component/database-connection.test.tsx b/tests/component/database-connection.test.tsx index 3c40b4c..c3a7dca 100644 --- a/tests/component/database-connection.test.tsx +++ b/tests/component/database-connection.test.tsx @@ -143,7 +143,9 @@ describe('DatabaseConnectionPage', () => { expect(screen.getByRole('button', { name: '开始获取密钥' })).toBeVisible() expect(screen.queryByText(/管理员授权|电脑密码/)).not.toBeInTheDocument() - expect(screen.queryByText(/Frida|Python|SIP|重新签名/)).not.toBeInTheDocument() + // SIP is a user prerequisite, not an implementation detail: seeing it in the + // UI is expected whenever the key cannot be captured without it. + expect(screen.queryByText(/Frida|Python|重新签名/)).not.toBeInTheDocument() }) it('renders a discovered nickname and avatar before connection', () => { diff --git a/tests/component/database-key-controls.test.tsx b/tests/component/database-key-controls.test.tsx index 88bb303..492667c 100644 --- a/tests/component/database-key-controls.test.tsx +++ b/tests/component/database-key-controls.test.tsx @@ -84,7 +84,8 @@ describe('database key controls', () => { expect(screen.getByText('Intel Mac 自动获取')).toBeVisible() expect(screen.getByText(/按页面提示操作即可/)).toBeVisible() - expect(screen.queryByText(/Frida|Python|SIP|签名/)).not.toBeInTheDocument() + // SIP is a user prerequisite, not an implementation detail. + expect(screen.queryByText(/Frida|Python|签名/)).not.toBeInTheDocument() await user.click(screen.getByRole('button', { name: '自动获取密钥' })) expect(onDetect).toHaveBeenCalledOnce() }) diff --git a/tests/unit/key-service-mac.test.ts b/tests/unit/key-service-mac.test.ts index 251843e..5927724 100644 --- a/tests/unit/key-service-mac.test.ts +++ b/tests/unit/key-service-mac.test.ts @@ -7,9 +7,11 @@ vi.mock('electron', () => ({ })) import { + SIP_ENABLED_ERROR, buildAppleSiliconXkeyInvocation, buildXkeyHelperArguments, mapXkeyHelperFailure, + parseSipEnabled, parseXkeyHelperOutput, resolveXkeyHelperMode } from '../../src/main/key-service-mac' @@ -133,3 +135,28 @@ describe('parseXkeyHelperOutput', () => { }) }) }) + +describe('parseSipEnabled', () => { + it.each<[string, boolean]>([ + ['System Integrity Protection status: enabled.', true], + ['System Integrity Protection status: disabled.', false], + ['System Integrity Protection status: unknown (Custom Configuration).', false], + ['System Integrity Protection status: enabled (Apple Internal).', true], + ['System Integrity Protection status: disabled (Apple Internal).', false] + ])('reads "%s" as %s', (statusOutput, expected) => { + expect(parseSipEnabled(statusOutput)).toBe(expected) + }) + + it('only reads the status field, not the word enabled elsewhere in the output', () => { + expect( + parseSipEnabled('System Integrity Protection status: disabled.\nKernel Extensions: enabled') + ).toBe(false) + }) +}) + +describe('SIP blocking message', () => { + it('names the prerequisite and the next action', () => { + expect(SIP_ENABLED_ERROR).toContain('SIP') + expect(SIP_ENABLED_ERROR).toMatch(/关闭 SIP|手动粘贴/) + }) +})