// // main.swift // Helper // // Created by Serhiy Mytrovtsiy on 17/11/2022 // Using Swift 5.0 // Running on macOS 13.0 // // Copyright © 2022 Serhiy Mytrovtsiy. All rights reserved. // import Foundation let helper = Helper() helper.run() class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { private let listener: NSXPCListener private let smcQueue = DispatchQueue(label: "eu.exelban.Stats.SMC.Helper.smcQueue") private var connections = [NSXPCConnection]() private var shouldQuit = false private var shouldQuitCheckInterval = 1.0 private var smc: String? = nil override init() { self.listener = NSXPCListener(machServiceName: "eu.exelban.Stats.SMC.Helper") super.init() self.listener.delegate = self } public func run() { let args = CommandLine.arguments.dropFirst() if !args.isEmpty && args.first == "uninstall" { NSLog("detected uninstall command") if let val = args.last, let pid: pid_t = Int32(val) { while kill(pid, 0) == 0 { usleep(50000) } } self.uninstallHelper() exit(0) } self.listener.resume() while !self.shouldQuit { RunLoop.current.run(until: Date(timeIntervalSinceNow: self.shouldQuitCheckInterval)) } } func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { do { let isValid = try CodesignCheck.codeSigningMatches(pid: newConnection.processIdentifier) if !isValid { NSLog("invalid connection, dropping") return false } } catch { NSLog("error checking code signing: \(error)") return false } newConnection.exportedInterface = NSXPCInterface(with: HelperProtocol.self) newConnection.exportedObject = self newConnection.invalidationHandler = { if let connectionIndex = self.connections.firstIndex(of: newConnection) { self.connections.remove(at: connectionIndex) } if self.connections.isEmpty { self.shouldQuit = true } } self.connections.append(newConnection) newConnection.resume() return true } private func uninstallHelper() { let process = Process() process.launchPath = "/bin/launchctl" process.qualityOfService = QualityOfService.userInitiated process.arguments = ["unload", "/Library/LaunchDaemons/eu.exelban.Stats.SMC.Helper.plist"] process.launch() process.waitUntilExit() if process.terminationStatus != .zero { NSLog("termination code: \(process.terminationStatus)") } NSLog("unloaded from launchctl") do { try FileManager.default.removeItem(at: URL(fileURLWithPath: "/Library/LaunchDaemons/eu.exelban.Stats.SMC.Helper.plist")) } catch let err { NSLog("plist deletion: \(err)") } NSLog("property list deleted") do { try FileManager.default.removeItem(at: URL(fileURLWithPath: "/Library/PrivilegedHelperTools/eu.exelban.Stats.SMC.Helper")) } catch let err { NSLog("helper deletion: \(err)") } NSLog("smc helper deleted") } } extension Helper { func version(completion: (String) -> Void) { completion(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0") } func setSMCPath(_ path: String) { self.smc = path } func setFanMode(id: Int, mode: Int, completion: (String?) -> Void) { smcQueue.sync { guard let smc = self.smc else { completion("missing smc tool") return } let result = syncShell("\(smc) fan \(id) -m \(mode)") if let error = result.error, !error.isEmpty { NSLog("error set fan mode: \(error)") completion(nil) return } completion(result.output) } } func setFanSpeed(id: Int, value: Int, completion: (String?) -> Void) { smcQueue.sync { guard let smc = self.smc else { completion("missing smc tool") return } let result = syncShell("\(smc) fan \(id) -v \(value)") if let error = result.error, !error.isEmpty { NSLog("error set fan speed: \(error)") completion(nil) return } completion(result.output) } } func resetFanControl(completion: (String?) -> Void) { smcQueue.sync { guard let smc = self.smc else { completion("missing smc tool") return } let result = syncShell("\(smc) reset") if let error = result.error, !error.isEmpty { NSLog("error reset fan control: \(error)") completion(nil) return } completion(result.output) } } func powermetrics(_ samplers: [String], completion: @escaping (String?) -> Void) { let result = syncShell("powermetrics -n 1 -s \(samplers.joined(separator: ",")) --sample-rate 1000") if let error = result.error, !error.isEmpty { NSLog("error call powermetrics: \(error)") completion(nil) return } completion(result.output) } public func syncShell(_ args: String) -> (output: String?, error: String?) { let task = Process() task.launchPath = "/bin/sh" task.arguments = ["-c", args] let outputPipe = Pipe() let errorPipe = Pipe() defer { outputPipe.fileHandleForReading.closeFile() errorPipe.fileHandleForReading.closeFile() } task.standardOutput = outputPipe task.standardError = errorPipe do { try task.run() } catch let err { return (nil, "syncShell: \(err.localizedDescription)") } let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: outputData, encoding: .utf8) let error = String(data: errorData, encoding: .utf8) return (output, error) } func uninstall() { let process = Process() process.launchPath = "/Library/PrivilegedHelperTools/eu.exelban.Stats.SMC.Helper" process.qualityOfService = QualityOfService.userInitiated process.arguments = ["uninstall", String(getpid())] process.launch() exit(0) } } // https://github.com/duanefields/VirtualKVM/blob/master/VirtualKVM/CodesignCheck.swift let kSecCSDefaultFlags = 0 enum CodesignCheckError: Error { case message(String) } struct CodesignCheck { public static func codeSigningMatches(pid: pid_t) throws -> Bool { return try self.codeSigningCertificatesForSelf() == self.codeSigningCertificates(forPID: pid) } private static func codeSigningCertificatesForSelf() throws -> [SecCertificate] { guard let secStaticCode = try secStaticCodeSelf() else { return [] } return try codeSigningCertificates(forStaticCode: secStaticCode) } private static func codeSigningCertificates(forPID pid: pid_t) throws -> [SecCertificate] { guard let secStaticCode = try secStaticCode(forPID: pid) else { return [] } return try codeSigningCertificates(forStaticCode: secStaticCode) } private static func executeSecFunction(_ secFunction: () -> (OSStatus) ) throws { let osStatus = secFunction() guard osStatus == errSecSuccess else { throw CodesignCheckError.message(String(describing: SecCopyErrorMessageString(osStatus, nil))) } } private static func secStaticCodeSelf() throws -> SecStaticCode? { var secCodeSelf: SecCode? try executeSecFunction { SecCodeCopySelf(SecCSFlags(rawValue: 0), &secCodeSelf) } guard let secCode = secCodeSelf else { throw CodesignCheckError.message("SecCode returned empty from SecCodeCopySelf") } return try secStaticCode(forSecCode: secCode) } private static func secStaticCode(forPID pid: pid_t) throws -> SecStaticCode? { var secCodePID: SecCode? try executeSecFunction { SecCodeCopyGuestWithAttributes(nil, [kSecGuestAttributePid: pid] as CFDictionary, [], &secCodePID) } guard let secCode = secCodePID else { throw CodesignCheckError.message("SecCode returned empty from SecCodeCopyGuestWithAttributes") } return try secStaticCode(forSecCode: secCode) } private static func secStaticCode(forSecCode secCode: SecCode) throws -> SecStaticCode? { var secStaticCodeCopy: SecStaticCode? try executeSecFunction { SecCodeCopyStaticCode(secCode, [], &secStaticCodeCopy) } guard let secStaticCode = secStaticCodeCopy else { throw CodesignCheckError.message("SecStaticCode returned empty from SecCodeCopyStaticCode") } return secStaticCode } private static func isValid(secStaticCode: SecStaticCode) throws { try executeSecFunction { SecStaticCodeCheckValidity(secStaticCode, SecCSFlags(rawValue: kSecCSDoNotValidateResources | kSecCSCheckNestedCode), nil) } } private static func secCodeInfo(forStaticCode secStaticCode: SecStaticCode) throws -> [String: Any]? { try isValid(secStaticCode: secStaticCode) var secCodeInfoCFDict: CFDictionary? try executeSecFunction { SecCodeCopySigningInformation(secStaticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &secCodeInfoCFDict) } guard let secCodeInfo = secCodeInfoCFDict as? [String: Any] else { throw CodesignCheckError.message("CFDictionary returned empty from SecCodeCopySigningInformation") } return secCodeInfo } private static func codeSigningCertificates(forStaticCode secStaticCode: SecStaticCode) throws -> [SecCertificate] { guard let secCodeInfo = try secCodeInfo(forStaticCode: secStaticCode), let secCertificates = secCodeInfo[kSecCodeInfoCertificates as String] as? [SecCertificate] else { return [] } return secCertificates } }