diff --git a/SwiftPrivilegedHelperApplication/Scripts/CodeSignUpdate.sh b/SwiftPrivilegedHelperApplication/Scripts/CodeSignUpdate.sh deleted file mode 100755 index ec1dadf..0000000 --- a/SwiftPrivilegedHelperApplication/Scripts/CodeSignUpdate.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/bash - -# CodeSignUpdate.sh -# SwiftPrivilegedHelperApplication -# -# Created by Erik Berglund. -# Copyright © 2018 Erik Berglund. All rights reserved. - -set -e - -### -### CUSTOM VARIABLES -### - -bundleIdentifierApplication="com.github.erikberglund.SwiftPrivilegedHelperApplication" -bundleIdentifierHelper="com.github.erikberglund.SwiftPrivilegedHelper" - -### -### STATIC VARIABLES -### - -infoPlist="${INFOPLIST_FILE}" - -if [[ $( /usr/libexec/PlistBuddy -c "Print NSPrincipalClass" "${infoPlist}" 2>/dev/null ) == "NSApplication" ]]; then - target="application" -else - target="helper" -fi - -oidAppleDeveloperIDCA="1.2.840.113635.100.6.2.6" -oidAppleDeveloperIDApplication="1.2.840.113635.100.6.1.13" -oidAppleMacAppStoreApplication="1.2.840.113635.100.6.1.9" -oidAppleWWDRIntermediate="1.2.840.113635.100.6.2.1" - -### -### FUNCTIONS -### - -function appleGeneric { - printf "%s" "anchor apple generic" -} - -function appleDeveloperID { - printf "%s" "certificate leaf[field.${oidAppleMacAppStoreApplication}] /* exists */ or certificate 1[field.${oidAppleDeveloperIDCA}] /* exists */ and certificate leaf[field.${oidAppleDeveloperIDApplication}] /* exists */" -} - -function appleMacDeveloper { - printf "%s" "certificate 1[field.${oidAppleWWDRIntermediate}]" -} - -function identifierApplication { - printf "%s" "identifier \"${bundleIdentifierApplication}\"" -} - -function identifierHelper { - printf "%s" "identifier \"${bundleIdentifierHelper}\"" -} - - -function developerID { - developmentTeamIdentifier="${DEVELOPMENT_TEAM}" - if ! [[ ${developmentTeamIdentifier} =~ ^[A-Z0-9]{10}$ ]]; then - printf "%s\n" "Invalid Development Team Identifier: ${developmentTeamIdentifier}" - exit 1 - fi - - printf "%s" "certificate leaf[subject.OU] = ${developmentTeamIdentifier}" -} - -function macDeveloper { - macDeveloperCN="${EXPANDED_CODE_SIGN_IDENTITY_NAME}" - if ! [[ ${macDeveloperCN} =~ ^Mac\ Developer:\ .*\ \([A-Z0-9]{10}\)$ ]]; then - printf "%s\n" "Invalid Mac Developer CN: ${macDeveloperCN}" - exit 1 - fi - - printf "%s" "certificate leaf[subject.CN] = \"${macDeveloperCN}\"" -} - -function updateSMPrivilegedExecutables { - /usr/libexec/PlistBuddy -c 'Delete SMPrivilegedExecutables' "${infoPlist}" - /usr/libexec/PlistBuddy -c 'Add SMPrivilegedExecutables dict' "${infoPlist}" - /usr/libexec/PlistBuddy -c 'Add SMPrivilegedExecutables:'"${bundleIdentifierHelper}"' string '"$( sed -E 's/\"/\\\"/g' <<< ${1})"'' "${infoPlist}" -} - -function updateSMAuthorizedClients { - /usr/libexec/PlistBuddy -c 'Delete SMAuthorizedClients' "${infoPlist}" - /usr/libexec/PlistBuddy -c 'Add SMAuthorizedClients array' "${infoPlist}" - /usr/libexec/PlistBuddy -c 'Add SMAuthorizedClients: string '"$( sed -E 's/\"/\\\"/g' <<< ${1})"'' "${infoPlist}" -} - -### -### MAIN SCRIPT -### - -case "${ACTION}" in - "build") - appString=$( identifierApplication ) - appString="${appString} and $( appleGeneric )" - appString="${appString} and $( macDeveloper )" - appString="${appString} and $( appleMacDeveloper )" - appString="${appString} /* exists */" - - helperString=$( identifierHelper ) - helperString="${helperString} and $( appleGeneric )" - helperString="${helperString} and $( macDeveloper )" - helperString="${helperString} and $( appleMacDeveloper )" - helperString="${helperString} /* exists */" - ;; - "install") - appString=$( appleGeneric ) - appString="${appString} and $( identifierApplication )" - appString="${appString} and ($( appleDeveloperID )" - appString="${appString} and $( developerID ))" - - helperString=$( appleGeneric ) - helperString="${helperString} and $( identifierHelper )" - helperString="${helperString} and ($( appleDeveloperID )" - helperString="${helperString} and $( developerID ))" - ;; - *) - printf "%s\n" "Unknown Xcode Action: ${ACTION}" - exit 1 - ;; -esac - -case "${target}" in - "helper") - updateSMAuthorizedClients "${appString}" - ;; - "application") - updateSMPrivilegedExecutables "${helperString}" - ;; - *) - printf "%s\n" "Unknown Target: ${target}" - exit 1 - ;; -esac diff --git a/SwiftPrivilegedHelperApplication/Scripts/CodeSignUpdate.swift b/SwiftPrivilegedHelperApplication/Scripts/CodeSignUpdate.swift new file mode 100644 index 0000000..33c7992 --- /dev/null +++ b/SwiftPrivilegedHelperApplication/Scripts/CodeSignUpdate.swift @@ -0,0 +1,405 @@ +/* + CodeSignUpdate.swift + + Created by Chip Jarred on 8/29/20. + Copyright © 2020 Chip Jarred. All rights reserved. + + Based on CodeSignUpdate.sh shell script written by Erik Berglund + + The copyright above and the following MIT License apply only to this Swift + source file and should not be construed as expanding the license or overriding + the copyright of the creator(s) or owners of the project or source code + repository containing it. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + + +import Foundation + +let oidAppleDeveloperIDCA="1.2.840.113635.100.6.2.6" +let oidAppleDeveloperIDApplication="1.2.840.113635.100.6.1.13" +let oidAppleMacAppStoreApplication="1.2.840.113635.100.6.1.9" +let oidAppleWWDRIntermediate="1.2.840.113635.100.6.2.1" + + +let alphabetStr = "abcdefghijklmnopqrstuvwxyz" +let lowerAlpha = [Character](alphabetStr) +let upperAlpha = [Character](alphabetStr.uppercased()) +let digits = [Character]("0123456789") +let devIDChars = upperAlpha + digits +let alphaNumericChars = upperAlpha + lowerAlpha + digits + +// ----------------------------------------- +struct Environment +{ + // ----------------------------------------- + static subscript(key: String) -> String? + { + // ----------------------------------------- + get + { + if let value = key.withCString({ getenv($0) }) { + return String(cString: value) + } + return nil + } + + // ----------------------------------------- + set + { + key.withCString + { key in + if let value = newValue { + _ = value.withCString { setenv(key, $0, 1) } + } + else { unsetenv(key) } + } + } + } +} + +let helpStr = +""" +Place the following lines *BEFORE* CodeSignUpdate in your Run Script phase: + + export MAIN_BUNDLE_ID= + export HELPER_BUNDLE_ID= + +For example, assuming CodeSignUpdate is installed in /usr/local/bin: + + export MAIN_BUNDLE_ID="com.skynet.central" + export HELPER_BUNDLE_ID="com.skynet.terminator" + /usr/local/bin/CodeSignUpdate +""" + +let bundleIDFormatHelp = +""" +Bundle identifiers should be in reverse domain format. For example: + + com.skynet.terminator + com.github.paranoidMacDev.WhosOutToGetYouApp +""" + +// ------------------------------------- +func emitError(_ msg: String) -> Never +{ + print("error: \(msg)") + exit(1) +} + +// ------------------------------------- +func environmentVarNotSet(_ variable: String, addHelp: Bool = false) -> Never +{ + let message = "\(variable) environment variable not set\n\(helpStr)" + + (addHelp ? "\n\(helpStr)" : "") + + emitError(message) +} + +// ------------------------------------- +func getBundleIDFrom(_ environmentVariable: String) -> String +{ + guard let bundleID = Environment[environmentVariable] else { + environmentVarNotSet(environmentVariable, addHelp: true) + } + + let bundleIDChars = alphaNumericChars + ["."] + + guard bundleID.count > 1 else { + emitError("Bundle ID is empty\n\(bundleIDFormatHelp)") + } + + guard bundleID.reduce(true, { $0 && bundleIDChars.contains($1) }) else + { + emitError( + "Bundle ID contains illegal characters: \(bundleID)\n" + + "\(bundleIDFormatHelp)" + ) + } + + guard bundleID.first! != "." && bundleID.last! != "." else + { + emitError( + "Bundle ID must not start or end with period: \(bundleID)\n" + + "\(bundleIDFormatHelp)" + ) + } + + return bundleID +} + + +let bundleIdentifierApplication = getBundleIDFrom("MAIN_BUNDLE_ID") +let bundleIdentifierHelper = getBundleIDFrom("HELPER_BUNDLE_ID") + +guard let infoPListFile = Environment["INFOPLIST_FILE"] else { + environmentVarNotSet("INFOPLIST_FILE") +} + +var infoPlist: [String: AnyObject] = +{ + let pListURL = URL(fileURLWithPath: infoPListFile) + guard let pListDict = NSDictionary(contentsOf: pListURL) + as? Dictionary + else { + emitError("Unable to read plist data from \(infoPListFile)") + } + return pListDict +}() + +let target: String = infoPlist["NSPrincipalClass"] as? String == "NSApplication" + ? "application" + : "helper" + +// ------------------------------------- +func isValidDeveloperID(_ s: S) -> Bool +{ + guard s.count == 10 else { return false } + return s.reduce(true) { $0 && devIDChars.contains($1) } +} + +// ------------------------------------- +func isValidDeveloperCN(_ s: String, withPrefix prefix: String) -> Bool +{ + // +4 accounts for 2 spaces, plus open and close parentheses + guard s.hasPrefix(prefix), s.count > prefix.count + 4 else { return false } + + guard let openParen = s.lastIndex(of: "(") else { return false } + guard let closeParen = s.lastIndex(of: ")") else { return false } + + guard s.distance(from: closeParen, to: s.endIndex) == 1 else { + return false + } + + guard s.distance(from: openParen, to: closeParen) == 11 else { + return false + } + + return isValidDeveloperID(s[s.index(after: openParen).. Bool +{ + let appleDeveloper = "Apple Development:" + let macDeveloper = "Mac Developer:" + + if isValidDeveloperCN(s, withPrefix: appleDeveloper) { return true } + if isValidDeveloperCN(s, withPrefix: macDeveloper) { return true } + + return false +} + +// ------------------------------------- +func isDeveloperIDCN() -> Bool +{ + guard let macDeveloperCN = Environment["EXPANDED_CODE_SIGN_IDENTITY_NAME"] + else + { + environmentVarNotSet("EXPANDED_CODE_SIGN_IDENTITY_NAME") + } + + let developerID = "Developer ID Application:" + + return isValidDeveloperCN(macDeveloperCN, withPrefix: developerID) +} + +// ------------------------------------- +func appendAppleGeneric(to s: inout String) { + s += "anchor apple generic" +} + +// ------------------------------------- +func appendAppleDeveloperID(to s: inout String) +{ + let exists = "/* exists */" + + s += "certificate leaf[field.\(oidAppleMacAppStoreApplication)] " + + exists + + " or certificate 1[field.\(oidAppleDeveloperIDCA)] " + + exists + + " and certificate leaf[field.\(oidAppleDeveloperIDApplication)] " + + exists +} + +// ------------------------------------- +func appendAppleMacDeveloper(to s: inout String) { + s += "certificate 1[field.\(oidAppleWWDRIntermediate)]" +} + +// ------------------------------------- +func appendApplicationBundleIdentifier(to s: inout String) { + s += "identifier \"\(bundleIdentifierApplication)\"" +} + +// ------------------------------------- +func appendHelperBundleIdentifier(to s: inout String) { + s += "identifier \"\(bundleIdentifierHelper)\"" +} + +// ------------------------------------- +func appendDeveloperID(to s: inout String) +{ + guard let devTeamID = Environment["DEVELOPMENT_TEAM"] else { + environmentVarNotSet("DEVELOPMENT_TEAM") + } + + guard isValidDeveloperID(devTeamID) else { + emitError("Invalid Development Team Identifier: \(devTeamID)") + } + + s += "certificate leaf[subject.OU] = \(devTeamID)" +} + +// ------------------------------------- +func appendMacDeveloper(to s: inout String) +{ + guard let macDeveloperCN = Environment["EXPANDED_CODE_SIGN_IDENTITY_NAME"] + else + { + environmentVarNotSet("EXPANDED_CODE_SIGN_IDENTITY_NAME") + } + + guard isValidDeveloperCN(macDeveloperCN) else { + emitError("Invalid Mac Developer CN: \(macDeveloperCN)") + } + + s += "certificate leaf[subject.CN] = \"\(macDeveloperCN)\"" +} + +// ------------------------------------- +func updateSMPriviledgedExecutables( + in plistDict: inout [String: AnyObject], + with s: String) +{ + assert(target == "application") + guard let prodBundleID = Environment["PRODUCT_BUNDLE_IDENTIFIER"] else { + environmentVarNotSet("PRODUCT_BUNDLE_IDENTIFIER") + } + + guard prodBundleID == bundleIdentifierApplication else + { + emitError( + "PRODUCT_BUNDLE_IDENTIFIER does not match MAIN_BUNDLE_ID\n" + + " PRODUCT_BUNDLE_IDENTIFIER = \(prodBundleID)\n" + + " MAIN_BUNDLE_ID = \(bundleIdentifierApplication)" + ) + } + + plistDict.removeValue(forKey: "SMPrivilegedExecutables") + let newExecutables: [String: String] = [bundleIdentifierHelper: s] + plistDict["SMPrivilegedExecutables"] = newExecutables as NSDictionary +} + +// ------------------------------------- +func updateSMAuthorizedClients( + in plistDict: inout [String: AnyObject], + with s: String) +{ + assert(target == "helper") + guard let prodBundleID = plistDict["CFBundleIdentifier"] as? String else + { + emitError( + "Helper info property list, \(infoPListFile), is missing " + + "\"CFBundleIdentifier\" key, or it not a string" + ) + } + guard prodBundleID == bundleIdentifierHelper else + { + emitError( + "Bundle id in info propery list, \(infoPListFile), does not" + + " match HELPER_BUNDLE_ID\n" + + " plists CFBundleIdentifier = \(prodBundleID)\n" + + " HELPER_BUNDLE_ID = \(bundleIdentifierHelper)" + ) + } + + guard let prodBundleName = plistDict["CFBundleName"] as? String else + { + emitError( + "Helper info property list, \(infoPListFile), is missing " + + "\"CFBundleName\" key, or it not a string" + ) + } + guard prodBundleName == bundleIdentifierHelper else + { + emitError( + "Bundle name in info propery list, \(infoPListFile), does " + + "not match HELPER_BUNDLE_ID\n" + + " plists CFBundleName = \(prodBundleName)\n" + + " HELPER_BUNDLE_ID = \(bundleIdentifierHelper)" + ) + } + + plistDict.removeValue(forKey: "SMAuthorizedClients") + let newClients: [String] = [s] + plistDict["SMAuthorizedClients"] = newClients as NSArray +} + +var appString = "" +var helperString = "" + +if (isDeveloperIDCN()) { + appendAppleGeneric(to: &appString) + appString += " and " + appendApplicationBundleIdentifier(to: &appString) + appString += " and " + appendAppleDeveloperID(to: &appString) + appString += " and " + appendDeveloperID(to: &appString) + + appendAppleGeneric(to: &helperString) + helperString += " and " + appendHelperBundleIdentifier(to: &helperString) + helperString += " and " + appendAppleDeveloperID(to: &helperString) + helperString += " and " + appendDeveloperID(to: &helperString) +} +else { + appendApplicationBundleIdentifier(to: &appString) + appString += " and " + appendAppleGeneric(to: &appString) + appString += " and " + appendMacDeveloper(to: &appString) + appString += " and " + appendAppleMacDeveloper(to: &appString) + appString += " /* exists */" + + appendHelperBundleIdentifier(to: &helperString) + helperString += " and " + appendAppleGeneric(to: &helperString) + helperString += " and " + appendMacDeveloper(to: &helperString) + helperString += " and " + appendAppleMacDeveloper(to: &helperString) + helperString += " /* exists */" +} + +if target == "helper" { + updateSMAuthorizedClients(in: &infoPlist, with: appString) +} +else if target == "application" { + updateSMPriviledgedExecutables(in: &infoPlist, with: helperString) +} +else { emitError("Unknown Target: \(target)") } + +(infoPlist as NSDictionary).write(toFile: infoPListFile, atomically: true) + diff --git a/SwiftPrivilegedHelperApplication/Scripts/README.md b/SwiftPrivilegedHelperApplication/Scripts/README.md new file mode 100644 index 0000000..cdd5e4e --- /dev/null +++ b/SwiftPrivilegedHelperApplication/Scripts/README.md @@ -0,0 +1,25 @@ +# CodeSignUpdate + +By [Chip Jarred](https://github.com/chipjarred) + +`CodeSignUpdate.swift` is based on and replaces the `CodeSignUpdate.sh` shell script created by Erik Berglund. I found [Erik's project](https://github.com/erikberglund/SwiftPrivilegedHelper) extremely useful in helping solve some problem code signing issues with my helper tool, and decided to contribute some further improvements that could have made my trouble-shooting even shorter. + +`CodeSignUpdate.swift` is intended to be used as a Swift script in a *Run Script* build phase, though you could, of course, actually compile it. It's job is the same as Erik's shell script: To fill in the correct code signing certificate information in the plists of both a main application and the helper tool it uses for priviledge escalation. His script was, in my opinion, a nice improvement on the way you'd have to do that task before, which was do manually invoke Apple's `SMJobBlessUtility.py` script, which may not have been so bad in the days when Xcode did in-project builds, but with builds done deep inside your Library folder, specyfing the app path plus the paths for both of your plists was kind of pain, and even that was an improvement over doing every single step yourself. I hope you'll agree that this Swift version is another step toward making building an app with a priviledged helper tool just a little easier. + +So, apart from the implementation language, what's different? + +The shell script used `stdout` as a means of string building, and so couldn't use it to give useful error information in the build log. If things didn't work, you'd just get a build error saying that the script terminated with exit code 1 with no other information. It did generate a few error messages, but most of them were in a context in which `stdout` was being redirected to build the string, and since those errors would be followed by a call to `exit`, they would be lost. + +Because `CodeSignUpdate.swift` is written in Swift, it is able to build strings without redirecting I/O, and so is able to generate useful error messages that you can actually see. It also generates more detailed and, I hope, useful messages for things the original didn't check to help you track down and fix whatever is causing it to fail. + +It also eliminates the hard-coding of bundle ids in the script itself. You provide them by `export`ing them as shell variables before calling the script in your *Run Script* phase, which should run after *Dependencies*, but before *Compile Sources*. For example + + export MAIN_BUNDLE_ID="com.github.erikberglund.SwiftPrivilegedHelperApplication" + export HELPER_BUNDLE_ID="com.github.erikberglund.SwiftPrivilegedHelper" + swift "${SRCROOT}"/Scripts/CodeSignUpdate.swift + +If you forget to include the `export` commands, the script will remind you. + +This Swift version accomplishes the job of editing the plists very differently than the shell script version did. This particular difference isn't necessarily better. It's just a matter of each version using the most convenient tools available to it to get the job done. The shell script called the `/usr/libexec/PlistBuddy` tool and `sed` to edit the plists. The Swift version reads the plists into dictionaries, which it modifies itself, then writes back out to the plists. + +Since it has to read and process the info.plists anyway, it checks that the bundle IDs in them are consistent with each other. If they weren't that could cause problems in the actual code signing phase. It checks that your helper tool's bundle id and name match the `HELPER_BUNDLE_ID` environment variable you set, and for your main app, it checks that your the `MAIN_BUNDLE_ID` matches the `PRODUCT_BUNDLE_ID` environment variable that Xcode generates from your build settings. diff --git a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/Helper.swift b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/Helper.swift index 8785c15..ee15fb1 100644 --- a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/Helper.swift +++ b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/Helper.swift @@ -53,7 +53,8 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { } // Set the protocol that the calling application conforms to. - connection.remoteObjectInterface = NSXPCInterface(with: AppProtocol.self) + connection.remoteObjectInterface = + NSXPCInterface(with: HelperToolControllerProtocol.self) // Set the protocol that the helper conforms to. connection.exportedInterface = NSXPCInterface(with: HelperProtocol.self) @@ -82,8 +83,29 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { func getVersion(completion: (String) -> Void) { completion(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0") } + + func withAuthorization( + authData: NSData?, + forCommand selector: Selector, + completion: (NSNumber) -> Void, + doCommand command:() -> Void) + { + /* + Check the passed authorization, if the user need to authenticate to + use this command the user might be prompted depending on the settings + and/or cached authentication. + */ + guard self.verifyAuthorization(authData, forCommand: selector) else { + completion(kAuthorizationFailedExitCode) + return + } - func runCommandLs(withPath path: String, completion: @escaping (NSNumber) -> Void) { + command() + } + + func runCommandLs( + withPath path: String, + completion: @escaping (NSNumber) -> Void) { // For security reasons, all commands should be hardcoded in the helper let command = "/bin/ls" @@ -93,16 +115,76 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { self.runTask(command: command, arguments: arguments, completion: completion) } - func runCommandLs(withPath path: String, authData: NSData?, completion: @escaping (NSNumber) -> Void) { - - // Check the passed authorization, if the user need to authenticate to use this command the user might be prompted depending on the settings and/or cached authentication. - - guard self.verifyAuthorization(authData, forCommand: #selector(HelperProtocol.runCommandLs(withPath:authData:completion:))) else { - completion(kAuthorizationFailedExitCode) - return + let lsSelector = + #selector(HelperProtocol.runCommandLs(withPath:authData:completion:)) + func runCommandLs( + withPath path: String, + authData: NSData?, + completion: @escaping (NSNumber) -> Void) + { + withAuthorization( + authData: authData, + forCommand: lsSelector, + completion: completion) + { + self.runCommandLs(withPath: path, completion: completion) + } + } + + let launchDaemonsURL = URL( + fileURLWithPath: "/Library/LaunchDaemons", + isDirectory: true + ) + let uninstallSelector = + #selector(HelperProtocol.runCommandUninstall(authData:completion:)) + func runCommandUninstall(completion: @escaping (NSNumber) -> Void) + { + let exectablePath = ProcessInfo.processInfo.arguments[0] + let plistName = + URL(fileURLWithPath: exectablePath).lastPathComponent + ".plist" + let plistURL = launchDaemonsURL.appendingPathComponent(plistName) + let executableURL = URL(fileURLWithPath: exectablePath) + + let fm = FileManager.default + trace("Deleting \(executableURL.path)") + let execRemoved = removeFile(executableURL, with: fm) + trace("Deleting \(plistURL.path)") + let plistRemoved = removeFile(plistURL, with: fm) + + + let exitCode = execRemoved && plistRemoved ? 0 : -1 + completion(exitCode as NSNumber) + + trace("Qutting helper tool") + shouldQuit = true + } + + private func removeFile(_ url: URL, with fm: FileManager) -> Bool + { + do { try fm.removeItem(at: url) } + catch + { + log( + stdErr: "Failed to remove helper at \(url.path) with " + + "error: \(error)" + ) + return false + } + + return true + } + + func runCommandUninstall( + authData: NSData?, + completion: @escaping (NSNumber) -> Void) + { + withAuthorization( + authData: authData, + forCommand: uninstallSelector, + completion: completion) + { + self.runCommandUninstall(completion: completion) } - - self.runCommandLs(withPath: path, completion: completion) } // MARK: - @@ -121,9 +203,7 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { do { try HelperAuthorization.verifyAuthorization(authData, forCommand: command) } catch { - if let remoteObject = self.connection()?.remoteObjectProxy as? AppProtocol { - remoteObject.log(stdErr: "Authentication Error: \(error)") - } + log(stdErr: "Authentication Error: \(error)") return false } return true @@ -140,7 +220,7 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { let stdOutHandler = { (file: FileHandle!) -> Void in let data = file.availableData guard let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return } - if let remoteObject = self.connection()?.remoteObjectProxy as? AppProtocol { + if let remoteObject = self.connection()?.remoteObjectProxy as? HelperToolControllerProtocol { remoteObject.log(stdOut: output as String) } } @@ -150,7 +230,7 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { let stdErrHandler = { (file: FileHandle!) -> Void in let data = file.availableData guard let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return } - if let remoteObject = self.connection()?.remoteObjectProxy as? AppProtocol { + if let remoteObject = self.connection()?.remoteObjectProxy as? HelperToolControllerProtocol { remoteObject.log(stdErr: output as String) } } @@ -167,4 +247,38 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol { task.launch() } + + private var controller: HelperToolControllerProtocol? { + self.connection()?.remoteObjectProxy as? HelperToolControllerProtocol + } + + private func log(stdOut s: String) + { + if let remoteObject = controller { + remoteObject.log(stdOut: s) + } + } + + private func log(stdErr s: String) + { + if let remoteObject = controller { + remoteObject.log(stdErr: s) + } + } + + private func trace( + _ s: @autoclosure () -> String, + file: StaticString = #file, + line: UInt = #line) + { + #if DEBUG + var useFileInfo: Bool { false } + if useFileInfo { + log(stdOut: "\(file):\(line): \(s())") + } + else { + log(stdOut: s()) + } + #endif + } } diff --git a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperAuthorization.swift b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperAuthorization.swift index 45a4445..5b3dd44 100644 --- a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperAuthorization.swift +++ b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperAuthorization.swift @@ -10,9 +10,34 @@ import Foundation enum HelperAuthorizationError: Error { case message(String) + + init(authorizationError: OSStatus) + { + self = .message( + SecCopyErrorMessageString(authorizationError, nil) as String? + ?? "Unknown error" + ) + } } class HelperAuthorization { + + static func makeRight( + command: Selector, + description: String) -> HelperAuthorizationRight + { + let customRule: [String: Any] = [ + kAuthorizationRightKeyClass: "user", + kAuthorizationRightKeyGroup: "admin", + kAuthorizationRightKeyVersion: 1 + ] + + return HelperAuthorizationRight( + command: command, + description: description, + ruleCustom: customRule + ) + } // MARK: - // MARK: Variables @@ -20,9 +45,14 @@ class HelperAuthorization { // FIXME: Add all functions that require authentication here. static let authorizationRights = [ - HelperAuthorizationRight(command: #selector(HelperProtocol.runCommandLs(withPath:authData:completion:)), - description: "SwiftPrivilegedHelper wants to run the command /bin/ls", - ruleCustom: [kAuthorizationRightKeyClass: "user", kAuthorizationRightKeyGroup: "admin", kAuthorizationRightKeyVersion: 1]) + makeRight( + command: #selector(HelperProtocol.runCommandLs(withPath:authData:completion:)), + description: "SwiftPrivilegedHelper wants to run the command /bin/ls" + ), + makeRight( + command: #selector(HelperProtocol.runCommandUninstall(authData:completion:)), + description: "SwiftPrivilegedHelper wants to uninstall helper tool" + ), ] // MARK: - @@ -178,9 +208,12 @@ class HelperAuthorization { var authItem = AuthorizationItem(name: authRightName, valueLength: 0, value: UnsafeMutableRawPointer(bitPattern: 0), flags: 0) // Create the AuthorizationRights for using the AuthorizationItem - var authRights = AuthorizationRights(count: 1, items: &authItem) + try withUnsafeMutablePointer(to: &authItem) + { + var authRights = AuthorizationRights(count: 1, items: $0) - // Check if the user is authorized for the AuthorizationRights. If not the user might be asked for an admin credential. - try executeAuthorizationFunction { AuthorizationCopyRights(authRef, &authRights, nil, [.extendRights, .interactionAllowed], nil) } + // Check if the user is authorized for the AuthorizationRights. If not the user might be asked for an admin credential. + try executeAuthorizationFunction { AuthorizationCopyRights(authRef, &authRights, nil, [.extendRights, .interactionAllowed], nil) } + } } } diff --git a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperProtocol.swift b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperProtocol.swift index 8f927a3..15c047f 100644 --- a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperProtocol.swift +++ b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelper/HelperProtocol.swift @@ -13,4 +13,9 @@ protocol HelperProtocol { func getVersion(completion: @escaping (String) -> Void) func runCommandLs(withPath: String, completion: @escaping (NSNumber) -> Void) func runCommandLs(withPath: String, authData: NSData?, completion: @escaping (NSNumber) -> Void) + + func runCommandUninstall(completion: @escaping (NSNumber) -> Void) + func runCommandUninstall( + authData: NSData?, + completion: @escaping (NSNumber) -> Void) } diff --git a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication.xcodeproj/project.pbxproj b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication.xcodeproj/project.pbxproj index 6edf202..dfeeb5f 100644 --- a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication.xcodeproj/project.pbxproj +++ b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 45D3E674297CBCB100E6E569 /* HelperToolController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D3E673297CBCB100E6E569 /* HelperToolController.swift */; }; 532308FA2163A87600A456CF /* ExtensionsNSTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532308F92163A87600A456CF /* ExtensionsNSTextView.swift */; }; 5332883321660B2400028C27 /* HelperAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE3960215E3068007310F5 /* HelperAuthorization.swift */; }; 5332883421660D7A00028C27 /* HelperAuthorizationRight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE3964215E3A85007310F5 /* HelperAuthorizationRight.swift */; }; @@ -18,8 +19,8 @@ 53604C20215E134500071149 /* Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C1F215E134500071149 /* Helper.swift */; }; 53604C22215E13E200071149 /* HelperConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C21215E13E200071149 /* HelperConstants.swift */; }; 53604C26215E185D00071149 /* HelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C25215E185D00071149 /* HelperProtocol.swift */; }; - 53604C2A215E1B0100071149 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C29215E1B0100071149 /* AppProtocol.swift */; }; - 53604C2C215E1C2700071149 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C29215E1B0100071149 /* AppProtocol.swift */; }; + 53604C2A215E1B0100071149 /* HelperToolControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C29215E1B0100071149 /* HelperToolControllerProtocol.swift */; }; + 53604C2C215E1C2700071149 /* HelperToolControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C29215E1B0100071149 /* HelperToolControllerProtocol.swift */; }; 53604C2E215E1CFB00071149 /* com.github.erikberglund.SwiftPrivilegedHelper in Copy Helper */ = {isa = PBXBuildFile; fileRef = 53604C18215E131D00071149 /* com.github.erikberglund.SwiftPrivilegedHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 53C6B95E216767F0001A80FD /* CodesignCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C6B95921676219001A80FD /* CodesignCheck.swift */; }; 53EE395F215E2631007310F5 /* HelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53604C25215E185D00071149 /* HelperProtocol.swift */; }; @@ -61,7 +62,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 532308F42163910700A456CF /* CodeSignUpdate.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = CodeSignUpdate.sh; sourceTree = ""; }; + 4569C94524FB6F72005B473B /* CodeSignUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeSignUpdate.swift; sourceTree = ""; }; + 4569C94624FB7175005B473B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 45D3E673297CBCB100E6E569 /* HelperToolController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolController.swift; sourceTree = ""; }; 532308F92163A87600A456CF /* ExtensionsNSTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsNSTextView.swift; sourceTree = ""; }; 53604C03215E12F300071149 /* SwiftPrivilegedHelperApplication.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftPrivilegedHelperApplication.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53604C06215E12F300071149 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -75,7 +78,7 @@ 53604C23215E14C900071149 /* Helper-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Helper-Info.plist"; sourceTree = ""; }; 53604C24215E150C00071149 /* Helper-Launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Helper-Launchd.plist"; sourceTree = ""; }; 53604C25215E185D00071149 /* HelperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperProtocol.swift; sourceTree = ""; }; - 53604C29215E1B0100071149 /* AppProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = ""; }; + 53604C29215E1B0100071149 /* HelperToolControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolControllerProtocol.swift; sourceTree = ""; }; 53C6B95921676219001A80FD /* CodesignCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodesignCheck.swift; sourceTree = ""; }; 53EE3960215E3068007310F5 /* HelperAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperAuthorization.swift; sourceTree = ""; }; 53EE3964215E3A85007310F5 /* HelperAuthorizationRight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperAuthorizationRight.swift; sourceTree = ""; }; @@ -102,7 +105,8 @@ 532308F12163908D00A456CF /* Scripts */ = { isa = PBXGroup; children = ( - 532308F42163910700A456CF /* CodeSignUpdate.sh */, + 4569C94524FB6F72005B473B /* CodeSignUpdate.swift */, + 4569C94624FB7175005B473B /* README.md */, ); path = Scripts; sourceTree = ""; @@ -132,10 +136,11 @@ isa = PBXGroup; children = ( 53604C06215E12F300071149 /* AppDelegate.swift */, - 53604C29215E1B0100071149 /* AppProtocol.swift */, + 53604C29215E1B0100071149 /* HelperToolControllerProtocol.swift */, 53604C08215E12F400071149 /* Assets.xcassets */, 53604C0A215E12F400071149 /* MainMenu.xib */, 53604C0D215E12F400071149 /* Info.plist */, + 45D3E673297CBCB100E6E569 /* HelperToolController.swift */, ); path = SwiftPrivilegedHelperApplication; sourceTree = ""; @@ -219,11 +224,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1000; - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1140; ORGANIZATIONNAME = "Erik Berglund"; TargetAttributes = { 53604C02215E12F300071149 = { CreatedOnToolsVersion = 10.0; + LastSwiftMigration = 1140; SystemCapabilities = { com.apple.Sandbox = { enabled = 0; @@ -232,6 +238,7 @@ }; 53604C17215E131D00071149 = { CreatedOnToolsVersion = 10.0; + LastSwiftMigration = 1140; }; }; }; @@ -282,7 +289,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}\"/Scripts/CodeSignUpdate.sh\n"; + shellScript = "export MAIN_BUNDLE_ID=\"com.github.erikberglund.SwiftPrivilegedHelperApplication\"\nexport HELPER_BUNDLE_ID=\"com.github.erikberglund.SwiftPrivilegedHelper\"\nswift \"${SRCROOT}\"/Scripts/CodeSignUpdate.swift\n"; }; 5361D213216254BC0036E296 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -299,7 +306,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}\"/Scripts/CodeSignUpdate.sh\n"; + shellScript = "export MAIN_BUNDLE_ID=\"com.github.erikberglund.SwiftPrivilegedHelperApplication\"\nexport HELPER_BUNDLE_ID=\"com.github.erikberglund.SwiftPrivilegedHelper\"\nswift \"${SRCROOT}\"/Scripts/CodeSignUpdate.swift\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -310,11 +317,12 @@ files = ( 53604C07215E12F300071149 /* AppDelegate.swift in Sources */, 535731AF215E1E5A009FDE00 /* HelperConstants.swift in Sources */, - 53604C2A215E1B0100071149 /* AppProtocol.swift in Sources */, + 53604C2A215E1B0100071149 /* HelperToolControllerProtocol.swift in Sources */, 53EE395F215E2631007310F5 /* HelperProtocol.swift in Sources */, 53EE3965215E3A85007310F5 /* HelperAuthorizationRight.swift in Sources */, 532308FA2163A87600A456CF /* ExtensionsNSTextView.swift in Sources */, 53EE3961215E3068007310F5 /* HelperAuthorization.swift in Sources */, + 45D3E674297CBCB100E6E569 /* HelperToolController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -323,7 +331,7 @@ buildActionMask = 2147483647; files = ( 53C6B95E216767F0001A80FD /* CodesignCheck.swift in Sources */, - 53604C2C215E1C2700071149 /* AppProtocol.swift in Sources */, + 53604C2C215E1C2700071149 /* HelperToolControllerProtocol.swift in Sources */, 53604C1B215E131E00071149 /* main.swift in Sources */, 53604C26215E185D00071149 /* HelperProtocol.swift in Sources */, 53604C22215E13E200071149 /* HelperConstants.swift in Sources */, @@ -485,7 +493,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.github.erikberglund.SwiftPrivilegedHelperApplication; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -504,7 +512,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.github.erikberglund.SwiftPrivilegedHelperApplication; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -528,7 +536,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.github.erikberglund.SwiftPrivilegedHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -552,7 +560,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.github.erikberglund.SwiftPrivilegedHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/AppDelegate.swift b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/AppDelegate.swift index 0f3c816..e435a8e 100644 --- a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/AppDelegate.swift +++ b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/AppDelegate.swift @@ -7,10 +7,9 @@ // import Cocoa -import ServiceManagement @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { +class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - // MARK: IBOutlets @@ -33,13 +32,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { // MARK: - // MARK: Variables - private var currentHelperConnection: NSXPCConnection? - @objc dynamic private var currentHelperAuthData: NSData? private let currentHelperAuthDataKeyPath: String @objc dynamic private var helperIsInstalled = false + { + didSet + { + self.buttonInstallHelper?.title = helperIsInstalled + ? "Uninstall Helper" + : "Install Helper" + } + } private let helperIsInstalledKeyPath: String + + @objc dynamic var installButtonTitle: String { + helperIsInstalled ? "Uninstall Helper" : "Install Helper" + } + private let installButtonTitleKeyPath: String + // MARK: - // MARK: Computed Variables @@ -66,6 +77,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { override init() { self.currentHelperAuthDataKeyPath = NSStringFromSelector(#selector(getter: self.currentHelperAuthData)) self.helperIsInstalledKeyPath = NSStringFromSelector(#selector(getter: self.helperIsInstalled)) + self.installButtonTitleKeyPath = NSStringFromSelector(#selector(getter: self.installButtonTitle)) super.init() } @@ -73,22 +85,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { self.configureBindings() } - func applicationDidFinishLaunching(_ aNotification: Notification) { - - // Update the current authorization database right - // This will prmpt the user for authentication if something needs updating. + var helperToolController: HelperToolController! = nil + + func applicationDidFinishLaunching(_ aNotification: Notification) + { + resetHelperToolController() + // Check if the current embedded helper tool is installed on the machine. + updateHelperStatus() + } + + private func resetHelperToolController() + { do { - try HelperAuthorization.authorizationRightsUpdateDatabase() - } catch { - self.textViewOutput.appendText("Failed to update the authorization database rights with error: \(error)") + helperToolController = try HelperToolController( + toolName: HelperConstants.machServiceName + ) + helperToolController.logStdOut = { + self.textViewOutput.appendText($0) + } + helperToolController.logStdErr = { + self.textViewOutput.appendText($0) + } } - - // Check if the current embedded helper tool is installed on the machine. - - self.helperStatus { installed in - OperationQueue.main.addOperation { - self.textFieldHelperInstalled.stringValue = (installed) ? "Yes" : "No" + catch { + self.textViewOutput.appendText(error.localizedDescription) + } + } + + private func updateHelperStatus() + { + self.helperToolController.helperStatus + { installed in + DispatchQueue.main.async + { + self.textFieldHelperInstalled.stringValue = (installed) + ? "Yes" + : "No" self.setValue(installed, forKey: self.helperIsInstalledKeyPath) } } @@ -96,21 +129,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { // MARK: - // MARK: Initialization - + func configureBindings() { // Button: Install Helper - self.buttonInstallHelper.bind(.enabled, - to: self, - withKeyPath: self.helperIsInstalledKeyPath, - options: [.continuouslyUpdatesValue: true, - .valueTransformerName: NSValueTransformerName.negateBooleanTransformerName]) + self.buttonInstallHelper.isEnabled = true + self.buttonInstallHelper.bind( + .title, + to: self, + withKeyPath: self.installButtonTitleKeyPath, + options: [.continuouslyUpdatesValue: true] + ) // Button: Run Command - self.buttonRunCommand.bind(.enabled, - to: self, - withKeyPath: self.helperIsInstalledKeyPath, - options: [.continuouslyUpdatesValue: true]) + self.buttonRunCommand.bind( + .enabled, + to: self, + withKeyPath: self.helperIsInstalledKeyPath, + options: [.continuouslyUpdatesValue: true] + ) } @@ -119,167 +156,132 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { @IBAction func buttonInstallHelper(_ sender: Any) { do { - if try self.helperInstall() { - OperationQueue.main.addOperation { - self.textViewOutput.appendText("Helper installed successfully.") - self.textFieldHelperInstalled.stringValue = "Yes" - self.setValue(true, forKey: self.helperIsInstalledKeyPath) - } + if helperIsInstalled { + try uninstallHelper() return - } else { - OperationQueue.main.addOperation { - self.textFieldHelperInstalled.stringValue = "No" - self.textViewOutput.appendText("Failed install helper with unknown error.") - } } - } catch { - OperationQueue.main.addOperation { - self.textViewOutput.appendText("Failed to install helper with error: \(error)") + else if try installHelper() { return } + } + catch + { + DispatchQueue.main.async + { + self.textViewOutput.appendText( + "Failed to install helper with error: \(error)" + ) } } - OperationQueue.main.addOperation { + DispatchQueue.main.async + { self.textFieldHelperInstalled.stringValue = "No" self.setValue(false, forKey: self.helperIsInstalledKeyPath) } } - - @IBAction func buttonDestroyCachedAuthorization(_ sender: Any) { - self.currentHelperAuthData = nil - self.textFieldAuthorizationCached.stringValue = "No" - self.buttonDestroyCachedAuthorization.isEnabled = false - } - - @IBAction func buttonRunCommand(_ sender: Any) { - guard - let inputPath = self.inputPath, - let helper = self.helper(nil) else { return } - - if self.checkboxRequireAuthentication.state == .on { - do { - guard let authData = try self.currentHelperAuthData ?? HelperAuthorization.emptyAuthorizationExternalFormData() else { - self.textViewOutput.appendText("Failed to get the empty authorization external form") - return - } - - helper.runCommandLs(withPath: inputPath, authData: authData) { (exitCode) in - OperationQueue.main.addOperation { - - // Verify that authentication was successful - - guard exitCode != kAuthorizationFailedExitCode else { - self.textViewOutput.appendText("Authentication Failed") - return - } - - self.textViewOutput.appendText("Command exit code: \(exitCode)") - if self.checkboxCacheAuthentication.state == .on, self.currentHelperAuthData == nil { - self.currentHelperAuthData = authData - self.textFieldAuthorizationCached.stringValue = "Yes" - self.buttonDestroyCachedAuthorization.isEnabled = true - } - - } - } - } catch { - self.textViewOutput.appendText("Command failed with error: \(error)") - } - } else { - helper.runCommandLs(withPath: inputPath) { (exitCode) in - self.textViewOutput.appendText("Command exit code: \(exitCode)") + + private func installHelper() throws -> Bool + { + if try self.helperToolController.install() + { + DispatchQueue.main.async + { + self.textViewOutput.appendText( + "Helper installed successfully." + ) + self.textFieldHelperInstalled.stringValue = "Yes" + self.setValue(true, forKey: self.helperIsInstalledKeyPath) } + return true } - } - // MARK: - - // MARK: AppProtocol Methods - - func log(stdOut: String) { - guard !stdOut.isEmpty else { return } - OperationQueue.main.addOperation { - self.textViewOutput.appendText(stdOut) + DispatchQueue.main.async + { + self.textFieldHelperInstalled.stringValue = "No" + self.textViewOutput.appendText( + "Failed install helper with unknown error." + ) } + return false } - - func log(stdErr: String) { - guard !stdErr.isEmpty else { return } - OperationQueue.main.addOperation { - self.textViewOutput.appendText(stdErr) + + private func uninstallHelper() throws + { + try helperToolController.withAuthorizedHelper( + cachedAuthentication: nil) + { helper, authData in + helper.runCommandUninstall(authData: authData) + { exitCode in + DispatchQueue.main.async + { + guard exitCode != kAuthorizationFailedExitCode else { + self.textViewOutput.appendText("Authentication Failed") + return + } + + if exitCode == 0 + { + self.currentHelperAuthData = nil + self.textFieldAuthorizationCached.stringValue = "No" + self.buttonDestroyCachedAuthorization.isEnabled = false + self.helperIsInstalled = false + } + self.textViewOutput.appendText("Uninstall exit code: \(exitCode)") + } + } } } + + private func runAuthorizedCommand(inputPath: String) throws + { + try helperToolController.withAuthorizedHelper( + cachedAuthentication: self.currentHelperAuthData) + { helper, authData in + helper.runCommandLs(withPath: inputPath, authData: authData) + { (exitCode) in + DispatchQueue.main.async + { + // Verify that authentication was successful + guard exitCode != kAuthorizationFailedExitCode else { + self.textViewOutput.appendText("Authentication Failed") + return + } - // MARK: - - // MARK: Helper Connection Methods - - func helperConnection() -> NSXPCConnection? { - guard self.currentHelperConnection == nil else { - return self.currentHelperConnection - } - - let connection = NSXPCConnection(machServiceName: HelperConstants.machServiceName, options: .privileged) - connection.exportedInterface = NSXPCInterface(with: AppProtocol.self) - connection.exportedObject = self - connection.remoteObjectInterface = NSXPCInterface(with: HelperProtocol.self) - connection.invalidationHandler = { - self.currentHelperConnection?.invalidationHandler = nil - OperationQueue.main.addOperation { - self.currentHelperConnection = nil + self.textViewOutput.appendText("Command exit code: \(exitCode)") + if self.checkboxCacheAuthentication.state == .on, self.currentHelperAuthData == nil + { + self.currentHelperAuthData = authData + self.textFieldAuthorizationCached.stringValue = "Yes" + self.buttonDestroyCachedAuthorization.isEnabled = true + } + } } } - - self.currentHelperConnection = connection - self.currentHelperConnection?.resume() - - return self.currentHelperConnection } - func helper(_ completion: ((Bool) -> Void)?) -> HelperProtocol? { - - // Get the current helper connection and return the remote object (Helper.swift) as a proxy object to call functions on. - - guard let helper = self.helperConnection()?.remoteObjectProxyWithErrorHandler({ error in - self.textViewOutput.appendText("Helper connection was closed with error: \(error)") - if let onCompletion = completion { onCompletion(false) } - }) as? HelperProtocol else { return nil } - return helper - } - - func helperStatus(completion: @escaping (_ installed: Bool) -> Void) { - - // Comppare the CFBundleShortVersionString from the Info.plist in the helper inside our application bundle with the one on disk. - - let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + HelperConstants.machServiceName) - guard - let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any], - let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String, - let helper = self.helper(completion) else { - completion(false) - return - } - - helper.getVersion { installedHelperVersion in - completion(installedHelperVersion == helperVersion) - } + @IBAction func buttonDestroyCachedAuthorization(_ sender: Any) { + self.currentHelperAuthData = nil + self.textFieldAuthorizationCached.stringValue = "No" + self.buttonDestroyCachedAuthorization.isEnabled = false } - func helperInstall() throws -> Bool { - - // Install and activate the helper inside our application bundle to disk. - - var cfError: Unmanaged? - var authItem = AuthorizationItem(name: kSMRightBlessPrivilegedHelper, valueLength: 0, value:UnsafeMutableRawPointer(bitPattern: 0), flags: 0) - var authRights = AuthorizationRights(count: 1, items: &authItem) - + @IBAction func buttonRunCommand(_ sender: Any) { guard - let authRef = try HelperAuthorization.authorizationRef(&authRights, nil, [.interactionAllowed, .extendRights, .preAuthorize]), - SMJobBless(kSMDomainSystemLaunchd, HelperConstants.machServiceName as CFString, authRef, &cfError) else { - if let error = cfError?.takeRetainedValue() { throw error } - return false + let inputPath = self.inputPath, + let helper = self.helperToolController.helper(nil) + else { return } + + if self.checkboxRequireAuthentication.state == .on + { + do { try runAuthorizedCommand(inputPath: inputPath) } + catch { + self.textViewOutput.appendText( + "Command failed with error: \(error)" + ) + } + } else { + helper.runCommandLs(withPath: inputPath) { (exitCode) in + self.textViewOutput.appendText("Command exit code: \(exitCode)") + } } - - self.currentHelperConnection?.invalidate() - self.currentHelperConnection = nil - - return true } } diff --git a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/Base.lproj/MainMenu.xib b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/Base.lproj/MainMenu.xib index ee89ae8..694886e 100644 --- a/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/Base.lproj/MainMenu.xib +++ b/SwiftPrivilegedHelperApplication/SwiftPrivilegedHelperApplication/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -638,7 +638,7 @@ - + @@ -693,16 +693,16 @@ - - + + - + - + - + @@ -710,7 +710,7 @@ - + @@ -718,7 +718,7 @@ - + @@ -739,7 +739,7 @@ - + - + - + - + @@ -769,12 +769,12 @@ - + - + @@ -792,7 +792,7 @@ - + @@ -800,17 +800,17 @@ - + - + @@ -818,7 +818,7 @@