import Foundation import PuterHelperProtocol import Security private final class HelperService: NSObject, PuterPrivilegedHelperProtocol { func version(withReply reply: @escaping (Int) -> Void) { reply(PuterHelperConstants.protocolVersion) } func setFanTargets(_ targets: [NSNumber: NSNumber], withReply reply: @escaping (Bool, String?) -> Void) { guard let validated = PuterFanTargetValidator.validate(targets) else { reply(false, "Fan targets failed validation.") return } do { for (fan, rpm) in validated { try runSMC(["fan", "\(fan)", "-m", "1"]) try runSMC(["fan", "\(fan)", "-v", "\(rpm)"]) } reply(true, nil) } catch { reply(false, error.localizedDescription) } } func restoreAutomaticFanControl(withReply reply: @escaping (Bool, String?) -> Void) { do { try runSMC(["reset"]) reply(true, nil) } catch { reply(false, error.localizedDescription) } } private func runSMC(_ arguments: [String]) throws { let helperURL = URL(fileURLWithPath: CommandLine.arguments[0]).standardizedFileURL let toolURL = helperURL.deletingLastPathComponent().appendingPathComponent("smc") guard FileManager.default.isExecutableFile(atPath: toolURL.path) else { throw HelperError.smcUnavailable } let process = Process() let errorPipe = Pipe() process.executableURL = toolURL process.arguments = arguments process.standardOutput = FileHandle.nullDevice process.standardError = errorPipe try process.run() let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() guard process.terminationStatus == 0 else { throw HelperError.commandFailed(String(decoding: errorData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)) } } } private enum HelperError: LocalizedError { case smcUnavailable case commandFailed(String) var errorDescription: String? { switch self { case .smcUnavailable: "The bundled SMC backend is unavailable." case .commandFailed(let detail): detail.isEmpty ? "The SMC command failed." : detail } } } private final class HelperDelegate: NSObject, NSXPCListenerDelegate { private let service = HelperService() func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool { guard ClientCodeValidator.isTrusted(pid: connection.processIdentifier) else { return false } connection.exportedInterface = NSXPCInterface(with: PuterPrivilegedHelperProtocol.self) connection.exportedObject = service connection.resume() return true } } private enum ClientCodeValidator { static func isTrusted(pid: pid_t) -> Bool { var clientCode: SecCode? let attributes = [kSecGuestAttributePid: NSNumber(value: pid)] as CFDictionary guard SecCodeCopyGuestWithAttributes(nil, attributes, [], &clientCode) == errSecSuccess, let clientCode, let clientInfo = signingInfo(for: clientCode), clientInfo.identifier == "dev.soconnor.puter" else { return false } var helperCode: SecCode? guard SecCodeCopySelf([], &helperCode) == errSecSuccess, let helperCode, let helperInfo = signingInfo(for: helperCode), let clientTeam = clientInfo.team, let helperTeam = helperInfo.team, clientTeam == helperTeam else { return false } var requirement: SecRequirement? let text = "anchor apple generic and identifier \"dev.soconnor.puter\"" guard SecRequirementCreateWithString(text as CFString, [], &requirement) == errSecSuccess, let requirement else { return false } return SecCodeCheckValidity(clientCode, [], requirement) == errSecSuccess } private static func signingInfo(for code: SecCode) -> (identifier: String, team: String?)? { var staticCode: SecStaticCode? guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode else { return nil } var information: CFDictionary? guard SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &information) == errSecSuccess, let values = information as? [CFString: Any], let identifier = values[kSecCodeInfoIdentifier] as? String else { return nil } return (identifier, values[kSecCodeInfoTeamIdentifier] as? String) } } private let delegate = HelperDelegate() private let listener = NSXPCListener(machServiceName: PuterHelperConstants.machServiceName) listener.delegate = delegate listener.resume() RunLoop.current.run()