fix: image_scan_helper 自动检测 task_for_pid 权限不足时通过 osascript 提权运行

macOS SIP 下 ad-hoc 签名的 debugger entitlement 不被信任,导致
image_scan_helper 调用 task_for_pid 返回 kr=5。现在先尝试直接运行,
检测到权限错误后自动切换到 osascript with administrator privileges
方式运行 helper,后续调用跳过直接运行直接走提权路径。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Felix
2026-03-13 23:22:27 +11:00
parent 0a603116ef
commit 5acd3d86c8

View File

@@ -23,6 +23,7 @@ export class KeyServiceMac {
private machVmRegion: any = null private machVmRegion: any = null
private machVmReadOverwrite: any = null private machVmReadOverwrite: any = null
private machPortDeallocate: any = null private machPortDeallocate: any = null
private _needsElevation = false
private getHelperPath(): string { private getHelperPath(): string {
const isPackaged = app.isPackaged const isPackaged = app.isPackaged
@@ -634,30 +635,27 @@ export class KeyServiceMac {
ciphertext: Buffer, ciphertext: Buffer,
onProgress?: (message: string) => void onProgress?: (message: string) => void
): Promise<string | null> { ): Promise<string | null> {
// 优先通过 image_scan_helper 子进程调用(有 debugger entitlement // 优先通过 image_scan_helper 子进程调用
try { try {
const helperPath = this.getImageScanHelperPath() const helperPath = this.getImageScanHelperPath()
const ciphertextHex = ciphertext.toString('hex') const ciphertextHex = ciphertext.toString('hex')
const result = await new Promise<string | null>((resolve, reject) => {
const child = spawn(helperPath, [String(pid), ciphertextHex], { stdio: ['ignore', 'pipe', 'pipe'] }) // 1) 直接运行 helper有正式签名的 debugger entitlement 时可用)
let stdout = '' if (!this._needsElevation) {
child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString() }) const direct = await this._spawnScanHelper(helperPath, pid, ciphertextHex, false)
child.stderr.on('data', (chunk: Buffer) => { console.log('[image_scan_helper]', chunk.toString().trim()) }) if (direct.key) return direct.key
child.on('error', reject) if (direct.permissionError) {
child.on('close', () => { console.warn('[KeyServiceMac] task_for_pid 权限不足,切换到 osascript 提权模式')
try { this._needsElevation = true
const lines = stdout.split(/\r?\n/).map(x => x.trim()).filter(Boolean) onProgress?.('需要管理员权限,请在弹出的对话框中输入密码...')
const last = lines[lines.length - 1] }
if (!last) { resolve(null); return } }
const payload = JSON.parse(last)
resolve(payload?.success && payload?.aesKey ? payload.aesKey : null) // 2) 通过 osascript 以管理员权限运行 helperSIP 下 ad-hoc 签名无法获取 task_for_pid
} catch { if (this._needsElevation) {
resolve(null) const elevated = await this._spawnScanHelper(helperPath, pid, ciphertextHex, true)
if (elevated.key) return elevated.key
} }
})
setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, 30_000)
})
if (result) return result
} catch (e: any) { } catch (e: any) {
console.warn('[KeyServiceMac] image_scan_helper unavailable, fallback to Mach API:', e?.message) console.warn('[KeyServiceMac] image_scan_helper unavailable, fallback to Mach API:', e?.message)
} }
@@ -757,6 +755,45 @@ export class KeyServiceMac {
} }
} }
private _spawnScanHelper(
helperPath: string, pid: number, ciphertextHex: string, elevated: boolean
): Promise<{ key: string | null; permissionError: boolean }> {
return new Promise((resolve, reject) => {
let child: ReturnType<typeof spawn>
if (elevated) {
const shellCmd = `'${helperPath}' ${pid} ${ciphertextHex}`
child = spawn('osascript', ['-e', `do shell script ${JSON.stringify(shellCmd)} with administrator privileges`],
{ stdio: ['ignore', 'pipe', 'pipe'] })
} else {
child = spawn(helperPath, [String(pid), ciphertextHex], { stdio: ['ignore', 'pipe', 'pipe'] })
}
const tag = elevated ? '[image_scan_helper:elevated]' : '[image_scan_helper]'
let stdout = '', stderr = ''
child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString() })
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString()
console.log(tag, chunk.toString().trim())
})
child.on('error', reject)
child.on('close', () => {
const permissionError = !elevated && stderr.includes('task_for_pid failed')
try {
const lines = stdout.split(/\r?\n/).map(x => x.trim()).filter(Boolean)
const last = lines[lines.length - 1]
if (!last) { resolve({ key: null, permissionError }); return }
const payload = JSON.parse(last)
resolve({
key: payload?.success && payload?.aesKey ? payload.aesKey : null,
permissionError
})
} catch {
resolve({ key: null, permissionError })
}
})
setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, elevated ? 60_000 : 30_000)
})
}
private async findWeChatPid(): Promise<number | null> { private async findWeChatPid(): Promise<number | null> {
const { execSync } = await import('child_process') const { execSync } = await import('child_process')
try { try {