├── .gitignore ├── BuildScripts └── PropertyListModifier.swift ├── Config.xcconfig ├── LICENSE ├── README.md ├── Shared ├── AllowedCommand.swift ├── CodeInfo.swift ├── HelperToolInfoPropertyList.swift ├── HelperToolLaunchdPropertyList.swift └── SharedConstants.swift ├── SwiftAuthorizationApp ├── AppConfig.xcconfig ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── HelperToolMonitor.swift ├── Info.plist └── ViewController.swift ├── SwiftAuthorizationHelperTool ├── AllowedCommandRunner.swift ├── HelperToolConfig.xcconfig ├── Info.plist ├── Uninstaller.swift ├── Updater.swift └── main.swift ├── SwiftAuthorizationSample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ └── joshkaplan.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | SwiftAuthorizationSample.xcodeproj/xcuserdata/ 3 | -------------------------------------------------------------------------------- /BuildScripts/PropertyListModifier.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun --sdk macosx swift 2 | // 3 | // PropertyListModifier.swift 4 | // SwiftAuthorizationSample 5 | // 6 | // Created by Josh Kaplan on 2021-10-23 7 | // 8 | 9 | // This script generates all of the property list requirements needed by SMJobBless and XPC Mach Services in conjunction 10 | // with user defined variables specified in the xcconfig files. This scripts adds/modifies/removes entries to: 11 | // - App's info property list 12 | // - Helper tool's info property list 13 | // - Helper tool's launchd property list 14 | // 15 | // For this sample, this script is run both at the beginning and end of the build process for both targets. When run at 16 | // the end it deletes all of the property list requirement changes it applied. For your own project you may not find it 17 | // useful to do this cleanup task at the end of the build process. 18 | // 19 | // Additionally this script can auto-increment the helper tools's version number whenever the source code changes 20 | // - SMJobBless will only successfully install a new helper tool over an existing one if its version is greater 21 | // - In order to track changes, a BuildHash entry will be added to the helper tool's Info property list 22 | // - This sample is configured by default to perform this auto-increment 23 | // 24 | // All of these options are configured by passing in command line arguments to this script. See ScriptTask for details. 25 | 26 | import Foundation 27 | import CryptoKit 28 | 29 | /// Errors raised throughout this script. 30 | enum ScriptError: Error { 31 | case general(String) 32 | case wrapped(String, Error) 33 | } 34 | 35 | // MARK: helper functions to read environment variables 36 | 37 | /// Attempts to read an environment variable, throws an error if it is not present. 38 | /// 39 | /// - Parameters: 40 | /// - name: Name of the environment variable. 41 | /// - description: A description of what was trying to be read; used in the error message if one is thrown. 42 | /// - isUserDefined: Whether the environment variable is user defined; used to modify the error message if one is thrown. 43 | func readEnvironmentVariable(name: String, description: String, isUserDefined: Bool) throws -> String { 44 | if let value = ProcessInfo.processInfo.environment[name] { 45 | return value 46 | } else { 47 | var message = "Unable to determine \(description), missing \(name) environment variable." 48 | if isUserDefined { 49 | message += " This is a user-defined variable. Please check that the xcconfig files are present and " + 50 | "configured in the project settings." 51 | } 52 | throw ScriptError.general(message) 53 | } 54 | } 55 | 56 | /// Attempts to read an environment variable as a URL. 57 | func readEnvironmentVariableAsURL(name: String, description: String, isUserDefined: Bool) throws -> URL { 58 | let value = try readEnvironmentVariable(name: name, description: description, isUserDefined: isUserDefined) 59 | 60 | return URL(fileURLWithPath: value) 61 | } 62 | 63 | // MARK: property list keys 64 | 65 | // Helper tool - info 66 | /// Key for entry in helper tool's info property list. 67 | let SMAuthorizedClientsKey = "SMAuthorizedClients" 68 | /// Key for bundle identifier. 69 | let CFBundleIdentifierKey = kCFBundleIdentifierKey as String 70 | /// Key for bundle version. 71 | let CFBundleVersionKey = kCFBundleVersionKey as String 72 | /// Custom key for an entry in the helper tool's info plist that contains a hash of source files. Used to detect when the build changes. 73 | let BuildHashKey = "BuildHash" 74 | 75 | // Helper tool - launchd 76 | /// Key for entry in helper tool's launchd property list. 77 | let LabelKey = "Label" 78 | /// Key for XPC mach service used by the helper tool. 79 | let MachServicesKey = "MachServices" 80 | 81 | // App - info 82 | /// Key for entry in app's info property list. 83 | let SMPrivilegedExecutablesKey = "SMPrivilegedExecutables" 84 | 85 | // MARK: code signing requirements 86 | 87 | /// A requirement that the organizational unit for the leaf certificate match the development team identifier. 88 | /// 89 | /// From Apple's documentation: "In Apple issued developer certificates, this field contains the developer’s Team Identifier." 90 | /// 91 | /// The leaf certificate is the one which corresponds to your developer certificate. The certificates above it in the chain are Apple's. 92 | /// Depending on whether this build is signed for debug or release the leaf certificate *will* differ, but the organizational unit, represented by `subject.OU` in 93 | /// the function, will remain the same. 94 | func organizationalUnitRequirement() throws -> String { 95 | // In order for this requirement to actually work, the signed app or helper tool needs to have a certificate chain 96 | // which will contain the organizational unit (subject.OU). While it'd be great if we could just examine the signed 97 | // app/helper tool after that's been done, that's of course not possible as we need to generate this requirement 98 | // *during* the process for each. 99 | // 100 | // Note: In practice this certificate chain won't exist when self signing using "Sign to Run Locally". 101 | // 102 | // There's no to precise way to determine if the subject.OU will be present, but in practice we can check for the 103 | // subject.CN (CN stands for common name) by seeing if there is a meaningful value for the CODE_SIGN_IDENTITY 104 | // build variable. This could still fail because we're checking for *this* target's common name, but creating an 105 | // identity for the *other* target - so if Xcode isn't configured the same for both targets an issue is likely to 106 | // arise. 107 | // 108 | // Note: The reason to use the organizational unit for the code requirement instead of the common name is because 109 | // the organizational unit will be consistent between the Apple Development and Developer ID builds, while the 110 | // common name will not be — simplifying the development workflow. 111 | let commonName = ProcessInfo.processInfo.environment["CODE_SIGN_IDENTITY"] 112 | if commonName == nil || commonName == "-" { 113 | throw ScriptError.general("Signing Certificate must be Development. Sign to Run Locally is not supported.") 114 | } 115 | 116 | let developmentTeamId = try readEnvironmentVariable(name: "DEVELOPMENT_TEAM", 117 | description: "development team for code signing", 118 | isUserDefined: false) 119 | guard developmentTeamId.range(of: #"^[A-Z0-9]{10}$"#, options: .regularExpression) != nil else { 120 | if developmentTeamId == "-" { 121 | throw ScriptError.general("Development Team for code signing is not set") 122 | } else { 123 | throw ScriptError.general("Development Team for code signing is invalid: \(developmentTeamId)") 124 | } 125 | } 126 | let certificateString = "certificate leaf[subject.OU] = \"\(developmentTeamId)\"" 127 | 128 | return certificateString 129 | } 130 | 131 | /// Requirement that Apple is part of the certificate chain, mean it was signed by an Apple issued certificate 132 | let appleGenericRequirement = "anchor apple generic" 133 | 134 | /// Creates a `SMAuthorizedClients` entry representing the app which must go inside the helper tool's info property list. 135 | func SMAuthorizedClientsEntry() throws -> (key: String, value: [String]) { 136 | let appIdentifierRequirement = "identifier \"\(try TargetType.app.bundleIdentifier())\"" 137 | // Create requirement that the app must be its current version or later. This mitigates downgrade attacks where an 138 | // older version of the app had a security vulnerability fixed in later versions. The attacker could then 139 | // intentionally install and run an older version of the app and exploit its vulnerability in order to talk to 140 | // the helper tool. 141 | let appVersion = try readEnvironmentVariable(name: "APP_VERSION", 142 | description: "app version", 143 | isUserDefined: true) 144 | let appVersionRequirement = "info[\(CFBundleVersionKey)] >= \"\(appVersion)\"" 145 | let requirements = [appleGenericRequirement, 146 | appIdentifierRequirement, 147 | appVersionRequirement, 148 | try organizationalUnitRequirement()] 149 | let value = [requirements.joined(separator: " and ")] 150 | 151 | return (SMAuthorizedClientsKey, value) 152 | } 153 | 154 | /// Creates a `SMPrivilegedExecutables` entry representing the helper tool which must go inside the app's info property list. 155 | func SMPrivilegedExecutablesEntry() throws -> (key: String, value: [String : String]) { 156 | let helperToolIdentifierRequirement = "identifier \"\(try TargetType.helperTool.bundleIdentifier())\"" 157 | let requirements = [appleGenericRequirement, helperToolIdentifierRequirement, try organizationalUnitRequirement()] 158 | let value = [try TargetType.helperTool.bundleIdentifier() : requirements.joined(separator: " and ")] 159 | 160 | return (SMPrivilegedExecutablesKey, value) 161 | } 162 | 163 | /// Creates a `Label` entry which must go inside the helper tool's launchd property list. 164 | func LabelEntry() throws -> (key: String, value: String) { 165 | return (key: LabelKey, value: try TargetType.helperTool.bundleIdentifier()) 166 | } 167 | 168 | // MARK: property list manipulation 169 | 170 | /// Reads the property list at the provided path. 171 | /// 172 | /// - Parameters: 173 | /// - atPath: Where the property list is located. 174 | /// - Returns: Tuple containing entries and the format of the on disk property list. 175 | func readPropertyList(atPath path: URL) throws -> (entries: NSMutableDictionary, 176 | format: PropertyListSerialization.PropertyListFormat) { 177 | let onDiskPlistData: Data 178 | do { 179 | onDiskPlistData = try Data(contentsOf: path) 180 | } catch { 181 | throw ScriptError.wrapped("Unable to read property list at: \(path)", error) 182 | } 183 | 184 | do { 185 | var format = PropertyListSerialization.PropertyListFormat.xml 186 | let plist = try PropertyListSerialization.propertyList(from: onDiskPlistData, 187 | options: .mutableContainersAndLeaves, 188 | format: &format) 189 | if let entries = plist as? NSMutableDictionary { 190 | return (entries: entries, format: format) 191 | } 192 | else { 193 | throw ScriptError.general("Unable to cast parsed property list") 194 | } 195 | } 196 | catch { 197 | throw ScriptError.wrapped("Unable to parse property list", error) 198 | } 199 | } 200 | 201 | /// Writes (or overwrites) a property list at the provided path. 202 | /// 203 | /// - Parameters: 204 | /// - atPath: Where the property list should be written. 205 | /// - entries: All entries to be written to the property list, this does not append - it overwrites anything existing. 206 | /// - format:The format to use when writing entries to `atPath`. 207 | func writePropertyList(atPath path: URL, 208 | entries: NSDictionary, 209 | format: PropertyListSerialization.PropertyListFormat) throws { 210 | let plistData: Data 211 | do { 212 | plistData = try PropertyListSerialization.data(fromPropertyList: entries, 213 | format: format, 214 | options: 0) 215 | } catch { 216 | throw ScriptError.wrapped("Unable to serialize property list in order to write to path: \(path)", error) 217 | } 218 | 219 | do { 220 | try plistData.write(to: path) 221 | } 222 | catch { 223 | throw ScriptError.wrapped("Unable to write property list to path: \(path)", error) 224 | } 225 | } 226 | 227 | /// Updates the property list with the provided entries. 228 | /// 229 | /// If an existing entry exists for the given key it will be overwritten. If the property file does not exist, it will be created. 230 | func updatePropertyListWithEntries(_ newEntries: [String : AnyHashable], atPath path: URL) throws { 231 | let (entries, format) : (NSMutableDictionary, PropertyListSerialization.PropertyListFormat) 232 | if FileManager.default.fileExists(atPath: path.path) { 233 | (entries, format) = try readPropertyList(atPath: path) 234 | } else { 235 | (entries, format) = ([:], PropertyListSerialization.PropertyListFormat.xml) 236 | } 237 | for (key, value) in newEntries { 238 | entries.setValue(value, forKey: key) 239 | } 240 | try writePropertyList(atPath: path, entries: entries, format: format) 241 | } 242 | 243 | /// Updates the property list by removing the provided keys (if present) or deletes the file if there are now no entries. 244 | func removePropertyListEntries(forKeys keys: [String], atPath path: URL) throws { 245 | let (entries, format) = try readPropertyList(atPath: path) 246 | for key in keys { 247 | entries.removeObject(forKey: key) 248 | } 249 | 250 | if entries.count > 0 { 251 | try writePropertyList(atPath: path, entries: entries, format: format) 252 | } else { 253 | try FileManager.default.removeItem(at: path) 254 | } 255 | } 256 | 257 | /// The path of the info property list for this target. 258 | func infoPropertyListPath() throws -> URL { 259 | return try readEnvironmentVariableAsURL(name: "INFOPLIST_FILE", 260 | description: "info property list path", 261 | isUserDefined: true) 262 | } 263 | 264 | /// The path of the launchd property list for the helper tool. 265 | func launchdPropertyListPath() throws -> URL { 266 | try readEnvironmentVariableAsURL(name: "LAUNCHDPLIST_FILE", 267 | description: "launchd property list path", 268 | isUserDefined: true) 269 | } 270 | 271 | // MARK: automatic bundle version updating 272 | 273 | /// Hashes Swift source files in the helper tool's directory as well as the shared directory. 274 | /// 275 | /// - Returns: hash value, hex encoded 276 | func hashSources() throws -> String { 277 | // Directories to hash source files in 278 | let sourcePaths: [URL] = [ 279 | try infoPropertyListPath().deletingLastPathComponent(), 280 | try readEnvironmentVariableAsURL(name: "SHARED_DIRECTORY", 281 | description: "shared source directory path", 282 | isUserDefined: true) 283 | ] 284 | 285 | // Enumerate over and hash Swift source files 286 | var sha256 = SHA256() 287 | for sourcePath in sourcePaths { 288 | if let enumerator = FileManager.default.enumerator(at: sourcePath, includingPropertiesForKeys: []) { 289 | for case let fileURL as URL in enumerator { 290 | if fileURL.pathExtension == "swift" { 291 | do { 292 | sha256.update(data: try Data(contentsOf: fileURL)) 293 | } catch { 294 | throw ScriptError.wrapped("Unable to hash \(fileURL)", error) 295 | } 296 | } 297 | } 298 | } else { 299 | throw ScriptError.general("Could not create enumerator for: \(sourcePath)") 300 | } 301 | } 302 | let digestHex = sha256.finalize().compactMap{ String(format: "%02x", $0) }.joined() 303 | 304 | return digestHex 305 | } 306 | 307 | /// Represents the value corresponding to the key `CFBundleVersionKey` in the info property list. 308 | enum BundleVersion { 309 | case major(UInt) 310 | case majorMinor(UInt, UInt) 311 | case majorMinorPatch(UInt, UInt, UInt) 312 | 313 | init?(version: String) { 314 | let versionParts = version.split(separator: ".") 315 | if versionParts.count == 1, 316 | let major = UInt(versionParts[0]) { 317 | self = .major(major) 318 | } 319 | else if versionParts.count == 2, 320 | let major = UInt(versionParts[0]), 321 | let minor = UInt(versionParts[1]) { 322 | self = .majorMinor(major, minor) 323 | } 324 | else if versionParts.count == 3, 325 | let major = UInt(versionParts[0]), 326 | let minor = UInt(versionParts[1]), 327 | let patch = UInt(versionParts[2]) { 328 | self = .majorMinorPatch(major, minor, patch) 329 | } 330 | else { 331 | return nil 332 | } 333 | } 334 | 335 | var version: String { 336 | switch self { 337 | case .major(let major): 338 | return "\(major)" 339 | case .majorMinor(let major, let minor): 340 | return "\(major).\(minor)" 341 | case .majorMinorPatch(let major, let minor, let patch): 342 | return "\(major).\(minor).\(patch)" 343 | } 344 | } 345 | 346 | func increment() -> BundleVersion { 347 | switch self { 348 | case .major(let major): 349 | return .major(major + 1) 350 | case .majorMinor(let major, let minor): 351 | return .majorMinor(major, minor + 1) 352 | case .majorMinorPatch(let major, let minor, let patch): 353 | return .majorMinorPatch(major, minor, patch + 1) 354 | } 355 | } 356 | } 357 | 358 | /// Reads the `CFBundleVersion` value from the passed in dictionary. 359 | func readBundleVersion(propertyList: NSMutableDictionary) throws -> BundleVersion { 360 | if let value = propertyList[CFBundleVersionKey] as? String { 361 | if let version = BundleVersion(version: value) { 362 | return version 363 | } else { 364 | throw ScriptError.general("Invalid value for \(CFBundleVersionKey) in property list") 365 | } 366 | } else { 367 | throw ScriptError.general("Could not find version, \(CFBundleVersionKey) missing in property list") 368 | } 369 | } 370 | 371 | /// Reads the `BuildHash` value from the passed in dictionary. 372 | func readBuildHash(propertyList: NSMutableDictionary) throws -> String? { 373 | return propertyList[BuildHashKey] as? String 374 | } 375 | 376 | /// Reads the info property list, determines if the build has changed based on stored hash values, and increments the build version if it has. 377 | func incrementBundleVersionIfNeeded(infoPropertyListPath: URL) throws { 378 | let propertyList = try readPropertyList(atPath: infoPropertyListPath) 379 | let previousBuildHash = try readBuildHash(propertyList: propertyList.entries) 380 | let currentBuildHash = try hashSources() 381 | if currentBuildHash != previousBuildHash { 382 | let version = try readBundleVersion(propertyList: propertyList.entries) 383 | let newVersion = version.increment() 384 | 385 | propertyList.entries[BuildHashKey] = currentBuildHash 386 | propertyList.entries[CFBundleVersionKey] = newVersion.version 387 | 388 | try writePropertyList(atPath: infoPropertyListPath, 389 | entries: propertyList.entries, 390 | format: propertyList.format) 391 | } 392 | } 393 | 394 | // MARK: Xcode target 395 | 396 | /// The two build targets used as part of this sample. 397 | enum TargetType: String { 398 | case app = "APP_BUNDLE_IDENTIFIER" 399 | case helperTool = "HELPER_TOOL_BUNDLE_IDENTIFIER" 400 | 401 | func bundleIdentifier() throws -> String { 402 | return try readEnvironmentVariable(name: self.rawValue, 403 | description: "bundle identifier for \(self)", 404 | isUserDefined: true) 405 | } 406 | } 407 | 408 | /// Determines whether this script is running for the app or the helper tool. 409 | func determineTargetType() throws -> TargetType { 410 | let bundleId = try readEnvironmentVariable(name: "PRODUCT_BUNDLE_IDENTIFIER", 411 | description: "bundle id", 412 | isUserDefined: false) 413 | 414 | let appBundleIdentifier = try TargetType.app.bundleIdentifier() 415 | let helperToolBundleIdentifier = try TargetType.helperTool.bundleIdentifier() 416 | if bundleId == appBundleIdentifier { 417 | return TargetType.app 418 | } else if bundleId == helperToolBundleIdentifier { 419 | return TargetType.helperTool 420 | } else { 421 | throw ScriptError.general("Unexpected bundle id \(bundleId) encountered. This means you need to update the " + 422 | "user defined variables APP_BUNDLE_IDENTIFIER and/or " + 423 | "HELPER_TOOL_BUNDLE_IDENTIFIER in Config.xcconfig.") 424 | } 425 | } 426 | 427 | // MARK: tasks 428 | 429 | /// The tasks this script can perform. They're provided as command line arguments to this script. 430 | typealias ScriptTask = () throws -> Void 431 | let scriptTasks: [String : ScriptTask] = [ 432 | /// Update the property lists as needed to satisfy the requirements of SMJobBless 433 | "satisfy-job-bless-requirements" : satisfyJobBlessRequirements, 434 | /// Clean up changes made to property lists to satisfy the requirements of SMJobBless 435 | "cleanup-job-bless-requirements" : cleanupJobBlessRequirements, 436 | /// Specifies MachServices entry in the helper tool's launchd property list to enable XPC 437 | "specify-mach-services" : specifyMachServices, 438 | /// Cleans up changes made to Mach Services in the helper tool's launchd property list 439 | "cleanup-mach-services" : cleanupMachServices, 440 | /// Auto increment the bundle version number; only intended for the helper tool. 441 | "auto-increment-version" : autoIncrementVersion 442 | ] 443 | 444 | /// Determines what tasks this script should undertake in based on passed in arguments. 445 | func determineScriptTasks() throws -> [ScriptTask] { 446 | if CommandLine.arguments.count > 1 { 447 | var matchingTasks = [ScriptTask]() 448 | for index in 1.. URL { 31 | var path: CFURL? 32 | let status = SecCodeCopyPath(try copyCurrentStaticCode(), SecCSFlags(), &path) 33 | guard status == errSecSuccess, let path = path as URL? else { 34 | throw CodeInfoError.codeLocationNotRetrievable(status) 35 | } 36 | 37 | return path 38 | } 39 | 40 | /// Determines if the public keys of this helper tool and the executable corresponding to the passed in `URL` match. 41 | /// 42 | /// - Parameter executable: On disk location of an executable. 43 | /// - Throws: If unable to compare the public keys for the on disk representations of both this helper tool and the executable for the provided URL. 44 | /// - Returns: If the public keys of their leaf certificates (which is the Developer ID certificate) match. 45 | static func doesPublicKeyMatch(forExecutable executable: URL) throws -> Bool { 46 | // Only perform this comparison if the executable's static code has a valid signature 47 | let executableStaticCode = try createStaticCode(forExecutable: executable) 48 | let checkFlags = SecCSFlags(rawValue: kSecCSStrictValidate | kSecCSCheckAllArchitectures) 49 | guard SecStaticCodeCheckValidity(executableStaticCode, checkFlags, nil) == errSecSuccess else { 50 | return false 51 | } 52 | 53 | let currentKeyData = try copyLeafCertificateKeyData(staticCode: try copyCurrentStaticCode()) 54 | let executableKeyData = try copyLeafCertificateKeyData(staticCode: executableStaticCode) 55 | 56 | return currentKeyData == executableKeyData 57 | } 58 | 59 | /// Convenience wrapper around `SecStaticCodeCreateWithPath`. 60 | /// 61 | /// - Parameter executable: On disk location of an executable. 62 | /// - Throws: If unable to create the static code. 63 | /// - Returns: Static code instance corresponding to the provided `URL`. 64 | static func createStaticCode(forExecutable executable: URL) throws -> SecStaticCode { 65 | var staticCode: SecStaticCode? 66 | let status = SecStaticCodeCreateWithPath(executable as CFURL, SecCSFlags(), &staticCode) 67 | guard status == errSecSuccess, let staticCode = staticCode else { 68 | throw CodeInfoError.externalStaticCodeNotRetrievable(status) 69 | } 70 | 71 | return staticCode 72 | } 73 | 74 | /// Convenience wrapper around `SecCodeCopySelf` and `SecCodeCopyStaticCode`. 75 | /// 76 | /// - Throws: If unable to create a copy of the on disk representation of this code. 77 | /// - Returns: Static code instance corresponding to the executable running this code. 78 | static func copyCurrentStaticCode() throws -> SecStaticCode { 79 | var currentCode: SecCode? 80 | let copySelfStatus = SecCodeCopySelf(SecCSFlags(), ¤tCode) 81 | guard copySelfStatus == errSecSuccess, let currentCode = currentCode else { 82 | throw CodeInfoError.helperToolStaticCodeNotRetrievable(copySelfStatus) 83 | } 84 | 85 | var currentStaticCode: SecStaticCode? 86 | let staticCodeStatus = SecCodeCopyStaticCode(currentCode, SecCSFlags(), ¤tStaticCode) 87 | guard staticCodeStatus == errSecSuccess, let currentStaticCode = currentStaticCode else { 88 | throw CodeInfoError.helperToolStaticCodeNotRetrievable(staticCodeStatus) 89 | } 90 | 91 | return currentStaticCode 92 | } 93 | 94 | /// Returns the leaf certificate in the code's certificate chain. 95 | /// 96 | /// For a Developer ID signed app, this practice this corresponds to the Developer ID certificate. 97 | /// 98 | /// - Parameter staticCode: On disk representation. 99 | /// - Throws: If unable to determine the certificate. 100 | /// - Returns: The leaf certificate. 101 | static func copyLeafCertificate(staticCode: SecStaticCode) throws -> SecCertificate { 102 | var info: CFDictionary? 103 | let flags = SecCSFlags(rawValue: kSecCSSigningInformation) 104 | guard SecCodeCopySigningInformation(staticCode, flags, &info) == errSecSuccess, 105 | let info = info as NSDictionary?, 106 | let certificates = info[kSecCodeInfoCertificates as String] as? [SecCertificate], 107 | let leafCertificate = certificates.first else { 108 | throw CodeInfoError.leafCertificateNotRetrievable 109 | } 110 | 111 | return leafCertificate 112 | } 113 | 114 | /// Returns the signing key in data form for the leaf certificate in the certificate chain. 115 | /// 116 | /// - Parameter staticCode: On disk representation. 117 | /// - Throws: If unable to copy the data. 118 | /// - Returns: Signing key in data form for the leaf certificate in the certificate chain. 119 | private static func copyLeafCertificateKeyData(staticCode: SecStaticCode) throws -> Data { 120 | guard let leafKey = SecCertificateCopyKey(try copyLeafCertificate(staticCode: staticCode)), 121 | let leafKeyData = SecKeyCopyExternalRepresentation(leafKey, nil) as Data? else { 122 | throw CodeInfoError.signingKeyDataNotRetrievable 123 | } 124 | 125 | return leafKeyData 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Shared/HelperToolInfoPropertyList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelperToolInfoPropertyList.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-23 6 | // 7 | 8 | import Foundation 9 | import EmbeddedPropertyList 10 | 11 | /// Read only representation of the helper tool's info property list. 12 | struct HelperToolInfoPropertyList: Decodable { 13 | /// Value for `SMAuthorizedClients`. 14 | let authorizedClients: [String] 15 | /// Value for `CFBundleVersion`. 16 | let version: BundleVersion 17 | /// Value for `CFBundleIdentifier`. 18 | let bundleIdentifier: String 19 | 20 | // Used by the decoder to map the names of the entries in the property list to the property names of this struct 21 | private enum CodingKeys: String, CodingKey { 22 | case authorizedClients = "SMAuthorizedClients" 23 | case version = "CFBundleVersion" 24 | case bundleIdentifier = "CFBundleIdentifier" 25 | } 26 | 27 | /// An immutable in memory representation of the property list by attempting to read it from the helper tool. 28 | static var main: HelperToolInfoPropertyList { 29 | get throws { 30 | try PropertyListDecoder().decode(HelperToolInfoPropertyList.self, 31 | from: try EmbeddedPropertyListReader.info.readInternal()) 32 | } 33 | } 34 | 35 | /// Creates an immutable in memory representation of the property list by attempting to read it from the helper tool. 36 | /// 37 | /// - Parameter url: Location of the helper tool on disk. 38 | init(from url: URL) throws { 39 | self = try PropertyListDecoder().decode(HelperToolInfoPropertyList.self, 40 | from: try EmbeddedPropertyListReader.info.readExternal(from: url)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Shared/HelperToolLaunchdPropertyList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelperToolLaunchdPropertyList.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-23 6 | // 7 | 8 | import Foundation 9 | import EmbeddedPropertyList 10 | 11 | /// Read only representation of the helper tool's embedded launchd property list. 12 | struct HelperToolLaunchdPropertyList: Decodable { 13 | /// Value for `MachServices`. 14 | let machServices: [String : Bool] 15 | /// Value for `Label`. 16 | let label: String 17 | 18 | // Used by the decoder to map the names of the entries in the property list to the property names of this struct 19 | private enum CodingKeys: String, CodingKey { 20 | case machServices = "MachServices" 21 | case label = "Label" 22 | } 23 | 24 | /// An immutable in memory representation of the property list by attempting to read it from the helper tool. 25 | static var main: HelperToolLaunchdPropertyList { 26 | get throws { 27 | try PropertyListDecoder().decode(HelperToolLaunchdPropertyList.self, 28 | from: try EmbeddedPropertyListReader.launchd.readInternal()) 29 | } 30 | } 31 | 32 | /// Creates an immutable in memory representation of the property list by attempting to read it from the helper tool. 33 | /// 34 | /// - Parameter url: Location of the helper tool on disk. 35 | init(from url: URL) throws { 36 | self = try PropertyListDecoder().decode(HelperToolLaunchdPropertyList.self, 37 | from: try EmbeddedPropertyListReader.launchd.readExternal(from: url)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Shared/SharedConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedConstants.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-23 6 | // 7 | 8 | import Foundation 9 | import Authorized 10 | import Blessed 11 | import SecureXPC 12 | import EmbeddedPropertyList 13 | 14 | /// Encapsulates a set of constants used throughout the app and helper tool. 15 | struct SharedConstants { 16 | /// Errors preventing shared constants from being created. 17 | enum SharedConstantsError: Error { 18 | /// The helper tool's launchd property list's value for `MachServices` array has no elements. 19 | case missingMachServiceName 20 | /// The app's info property list is missing its `SMPrivilegedExecutables` key or has no entries in the correct format. 21 | case missingSMPrivilegedExecutables 22 | } 23 | 24 | // MARK: True constants 25 | 26 | /// Authorization right used to force user to authenticate as admin before performing an action. 27 | static let exampleRight = AuthorizationRight(name: "com.example.SwiftAuthorizationSample.secure-action") 28 | /// XPC route to run an allowed command as root. 29 | static let allowedCommandRoute = XPCRoute.named("process") 30 | .withMessageType(AllowedCommandMessage.self) 31 | .withReplyType(AllowedCommandReply.self) 32 | .throwsType(AllowedCommandError.self) 33 | /// XPC route to uninstall the helper tool. 34 | static let uninstallRoute = XPCRoute.named("uninstall") 35 | /// XPC route to update the helper tool. 36 | static let updateRoute = XPCRoute.named("update") 37 | .withMessageType(URL.self) 38 | 39 | // MARK: Derived constants 40 | 41 | /// The label of the helper tool. This is required by `SMJobBless` to match its filename. 42 | let helperToolLabel: String 43 | /// The version of the helper tool. If this is being accessed by the helper tool this is its own version. If this is being accessed by the app it is the version of 44 | /// the bundled helper tool. 45 | let helperToolVersion: BundleVersion 46 | /// The name of the Mach service registered by the helper tool. If there are multiple registered Mach services, this is set to the name of the first one. If using the 47 | /// build script that's part of this project, then this will have the same value as `helperToolLabel`, but no such requirement exists and this code makes no 48 | /// such assumption. 49 | let machServiceName: String 50 | /// Location of the helper tool's launchd property list generated by the system as part of `SMJobBless`. 51 | /// 52 | /// In practice this is where `SMJobBless` will place the launchd property list, but this behavior is not officially documented. 53 | let blessedPropertyListLocation: URL 54 | /// Location of the helper tool if it has been blessed via `SMJobBless`. 55 | /// 56 | /// In practice this is where `SMJobBless` will install the helper tool, but this behavior is not officially documented. 57 | let blessedLocation: URL 58 | 59 | #if APP 60 | /// Location of the helper tool within the app bundle. 61 | let bundledLocation: URL 62 | #endif 63 | 64 | /// Initializes a set of constants used throughout the app and helper tool. 65 | init() throws { 66 | #if APP 67 | guard let helperToolLabel = (Bundle.main.infoDictionary?["SMPrivilegedExecutables"] 68 | as? [String : Any])?.first?.key else { 69 | throw SharedConstantsError.missingSMPrivilegedExecutables 70 | } 71 | self.helperToolLabel = helperToolLabel 72 | self.bundledLocation = URL(fileURLWithPath: "Contents/Library/LaunchServices/\(helperToolLabel)", 73 | relativeTo: Bundle.main.bundleURL).absoluteURL 74 | let launchdPropertyList = try HelperToolLaunchdPropertyList(from: self.bundledLocation) 75 | let infoPropertyList = try HelperToolInfoPropertyList(from: self.bundledLocation) 76 | #else 77 | #if HELPER_TOOL 78 | let launchdPropertyList = try HelperToolLaunchdPropertyList.main 79 | let infoPropertyList = try HelperToolInfoPropertyList.main 80 | self.helperToolLabel = launchdPropertyList.label 81 | #else 82 | fatalError(""" 83 | No Swift compiler directive was set for this executable. In this sample this is set in the AppConfig.xcconfig \ 84 | and HelperToolConfig.xcconfig configuration files. 85 | """) 86 | #endif 87 | #endif 88 | 89 | self.helperToolVersion = infoPropertyList.version 90 | self.blessedPropertyListLocation = URL(fileURLWithPath: "/Library/LaunchDaemons/\(helperToolLabel).plist") 91 | self.blessedLocation = URL(fileURLWithPath: "/Library/PrivilegedHelperTools/\(helperToolLabel)") 92 | 93 | // Important: If the Mach service name has been changed, for the app until that new version of the helper tool 94 | // is installed via SMJobBless this will not result in the correct name being found. 95 | guard let machServiceName = launchdPropertyList.machServices.first?.key else { 96 | throw SharedConstantsError.missingMachServiceName 97 | } 98 | self.machServiceName = machServiceName 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/AppConfig.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // AppConfig.xcconfig 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-23 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | #include "Config.xcconfig" 12 | 13 | // The display name for the sample app (for a non-sample app you would not necessarily want to define this here). 14 | DISPLAY_NAME = Authorization Sample 15 | 16 | // The name of the .app bundle created by the build process. Often matches the display name, but does not need to. 17 | TARGET_NAME = Swift Authorization Sample 18 | 19 | // The directory containing the source code and property lists for the main App. 20 | TARGET_DIRECTORY = SwiftAuthorizationApp 21 | 22 | 23 | // ** There should not be a need to modify anything below this line in this configuration file ** // 24 | 25 | 26 | // Bundle identifier used both in the info property list and so the build script knows which target it is running for. 27 | // If you want to change the bundle identifier, change the value for APP_BUNDLE_IDENTIFIER in Config.xcconfig. 28 | PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER) 29 | 30 | // The product name match the name of the .app. 31 | PRODUCT_NAME = $(TARGET_NAME) 32 | 33 | // It is not possible to use the Authorization framework from a sandboxed app. 34 | ENABLE_APP_SANDBOX = NO 35 | 36 | // Location of the info property list. 37 | INFOPLIST_FILE = $(TARGET_DIRECTORY)/Info.plist 38 | 39 | // Tells Xcode Archive the app is the target which should be the installable artifact (unlike the helper tool). 40 | SKIP_INSTALL = NO 41 | 42 | // Used by the shared code to know which target it's being built for 43 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP 44 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-21 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 13 | return true 14 | } 15 | 16 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 68 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 156 | 157 | 158 | 159 | 160 | 161 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/HelperToolMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelperToolMonitor.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-23 6 | // 7 | 8 | import Foundation 9 | import EmbeddedPropertyList 10 | 11 | /// Monitors the on disk location of the helper tool and its launchd property list. 12 | /// 13 | /// Whenever those files change, the helper tool's embedded info property list is read and the launchd status is queried (via the public interface to launchctl). This 14 | /// means this monitor has a limitation that if *only* the launchd registration changes then this monitor will not automatically pick up this changed. However, if 15 | /// `determineStatus()` is called it will always reflect the latest state including querying launchd status. 16 | class HelperToolMonitor { 17 | /// Encapsulates the installation status at approximately a moment in time. 18 | /// 19 | /// The individual properties of this struct can't be queried all at once, so it is possible for this to reflect a state that never truly existed simultaneously. 20 | struct InstallationStatus { 21 | 22 | /// Status of the helper tool executable as exists on disk. 23 | enum HelperToolExecutable { 24 | /// The helper tool exists in its expected location. 25 | /// 26 | /// Associated value is the helper tool's bundle version. 27 | case exists(BundleVersion) 28 | /// No helper tool was found. 29 | case missing 30 | } 31 | 32 | /// The helper tool is registered with launchd (according to launchctl). 33 | let registeredWithLaunchd: Bool 34 | /// The property list used by launchd exists on disk. 35 | let registrationPropertyListExists: Bool 36 | /// Whether an on disk representation of the helper tool exists in its "blessed" location. 37 | let helperToolExecutable: HelperToolExecutable 38 | } 39 | 40 | /// Directories containing installed helper tools and their registration property lists. 41 | private let monitoredDirectories: [URL] 42 | /// Mapping of monitored directories to corresponding dispatch sources. 43 | private var dispatchSources = [URL : DispatchSourceFileSystemObject]() 44 | /// Queue to receive callbacks on. 45 | private let directoryMonitorQueue = DispatchQueue(label: "directorymonitor", attributes: .concurrent) 46 | /// Name of the privileged executable being monitored 47 | private let constants: SharedConstants 48 | 49 | /// Creates the monitor. 50 | /// - Parameter constants: Constants defining needed file paths. 51 | init(constants: SharedConstants) { 52 | self.constants = constants 53 | self.monitoredDirectories = [constants.blessedLocation.deletingLastPathComponent(), 54 | constants.blessedPropertyListLocation.deletingLastPathComponent()] 55 | } 56 | 57 | /// Starts the monitoring process. 58 | /// 59 | /// If it's already been started, this will have no effect. This function is not thread safe. 60 | /// - Parameter changeOccurred: Called when the helper tool or registration property list file is created, deleted, or modified. 61 | func start(changeOccurred: @escaping (InstallationStatus) -> Void) { 62 | if dispatchSources.isEmpty { 63 | for monitoredDirectory in monitoredDirectories { 64 | let fileDescriptor = open((monitoredDirectory as NSURL).fileSystemRepresentation, O_EVTONLY) 65 | let dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, 66 | eventMask: .write, 67 | queue: directoryMonitorQueue) 68 | dispatchSources[monitoredDirectory] = dispatchSource 69 | dispatchSource.setEventHandler { 70 | changeOccurred(self.determineStatus()) 71 | } 72 | dispatchSource.setCancelHandler { 73 | close(fileDescriptor) 74 | self.dispatchSources.removeValue(forKey: monitoredDirectory) 75 | } 76 | dispatchSource.resume() 77 | } 78 | } 79 | } 80 | 81 | /// Stops the monitoring process. 82 | /// 83 | /// If the process wa never started, this will have no effect. This function is not thread safe. 84 | func stop() { 85 | for source in dispatchSources.values { 86 | source.cancel() 87 | } 88 | } 89 | 90 | /// Determines the installation status of the helper tool 91 | /// - Returns: The status of the helper tool installation. 92 | func determineStatus() -> InstallationStatus { 93 | // Sleep for 50ms because on disk file changes, which triggers this call, can occur before launchctl knows about 94 | // the (de)registration 95 | Thread.sleep(forTimeInterval: 0.05) 96 | 97 | // Registered with launchd 98 | let process = Process() 99 | process.launchPath = "/bin/launchctl" 100 | process.arguments = ["print", "system/\(constants.helperToolLabel)"] 101 | process.qualityOfService = QualityOfService.userInitiated 102 | process.standardOutput = nil 103 | process.standardError = nil 104 | process.launch() 105 | process.waitUntilExit() 106 | let registeredWithLaunchd = (process.terminationStatus == 0) 107 | 108 | // Registration property list exists on disk 109 | let registrationPropertyListExists = FileManager.default 110 | .fileExists(atPath: constants.blessedPropertyListLocation.path) 111 | 112 | let helperToolExecutable: InstallationStatus.HelperToolExecutable 113 | do { 114 | let infoPropertyList = try HelperToolInfoPropertyList(from: constants.blessedLocation) 115 | helperToolExecutable = .exists(infoPropertyList.version) 116 | } catch { 117 | helperToolExecutable = .missing 118 | } 119 | 120 | return InstallationStatus(registeredWithLaunchd: registeredWithLaunchd, 121 | registrationPropertyListExists: registrationPropertyListExists, 122 | helperToolExecutable: helperToolExecutable) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | $(TARGET_NAME) 7 | CFBundleIdentifier 8 | $(PRODUCT_BUNDLE_IDENTIFIER) 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | $(DISPLAY_NAME) 13 | CFBundleVersion 14 | $(APP_VERSION) 15 | LSApplicationCategoryType 16 | public.app-category.developer-tools 17 | NSMainStoryboardFile 18 | Main 19 | NSPrincipalClass 20 | NSApplication 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftAuthorizationApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-21 6 | // 7 | 8 | import Cocoa 9 | import Authorized 10 | import Blessed 11 | import EmbeddedPropertyList 12 | import SecureXPC 13 | 14 | class ViewController: NSViewController { 15 | // Defined in the Storyboard 16 | @IBOutlet weak var installedField: NSTextField! 17 | @IBOutlet weak var versionField: NSTextField! 18 | @IBOutlet weak var uninstallButton: NSButton! 19 | @IBOutlet weak var installOrUpdateButton: NSButton! 20 | @IBOutlet weak var commandPopup: NSPopUpButton! 21 | @IBOutlet weak var runButton: NSButton! 22 | @IBOutlet weak var outputText: NSTextView! 23 | 24 | // Initialized in viewDidLoad() 25 | 26 | /// Monitors the helper tool, if installed. 27 | private var monitor: HelperToolMonitor! 28 | /// The location of the helper tool bundlded with this app. 29 | private var bundledLocation: URL! 30 | /// The version of the helper tool bundled with this app (not necessarily the version installed). 31 | private var bundledHelperToolVersion: BundleVersion! 32 | /// Used to communicate with the helper tool. 33 | private var xpcClient: XPCClient! 34 | 35 | /// Authorization instance used for the run of this app. This needs a persistent scope because once the authorization instance is deinitialized it will no longer be 36 | /// valid system-wide, including in other processes such as the helper tool. 37 | private var authorization: Authorization? 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | // Command pop options 43 | for command in AllowedCommand.allCases { 44 | let menuItem = NSMenuItem(title: command.displayName, action: nil, keyEquivalent: "") 45 | menuItem.representedObject = command 46 | commandPopup.menu?.addItem(menuItem) 47 | } 48 | 49 | // Output text field 50 | outputText.font = NSFont.userFixedPitchFont(ofSize: 11) 51 | 52 | // Have this button call this target when clicked, the specific function called will differ 53 | self.installOrUpdateButton.target = self 54 | 55 | // Initialize variables using shared constants 56 | let sharedConstants: SharedConstants 57 | do { 58 | sharedConstants = try SharedConstants() 59 | } catch { 60 | fatalError(""" 61 | One or more property list configuration issues exist. Please check the PropertyListModifier.swift script \ 62 | is run as part of the build process for both the app and helper tool targets. This script will \ 63 | automatically create all of the necessary configurations. 64 | Issue: \(error) 65 | """) 66 | } 67 | self.xpcClient = XPCClient.forMachService(named: sharedConstants.machServiceName) 68 | self.bundledLocation = sharedConstants.bundledLocation 69 | self.bundledHelperToolVersion = sharedConstants.helperToolVersion 70 | self.monitor = HelperToolMonitor(constants: sharedConstants) 71 | self.updateInstallationStatus(self.monitor.determineStatus()) 72 | self.monitor.start(changeOccurred: self.updateInstallationStatus) 73 | } 74 | 75 | override func viewWillAppear() { 76 | // Have the window title and menu items reflect the display name of the app 77 | if let displayName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String { 78 | if let window = self.view.window { 79 | window.title = displayName 80 | } 81 | 82 | let menu = NSMenu() 83 | let menuItemOne = NSMenuItem() 84 | menuItemOne.submenu = NSMenu() 85 | let aboutItem = NSMenuItem(title: "About \(displayName)", 86 | action: #selector(ViewController.openGithubPage(_:)), 87 | keyEquivalent: "") 88 | let quitItem = NSMenuItem(title: "Quit \(displayName)", 89 | action: #selector(NSApplication.terminate(_:)), 90 | keyEquivalent: "q") 91 | menuItemOne.submenu?.items = [aboutItem, NSMenuItem.separator(), quitItem] 92 | menu.items = [menuItemOne] 93 | NSApplication.shared.mainMenu = menu 94 | } 95 | } 96 | 97 | @objc func openGithubPage(_ sender: Any?) { 98 | if let url = URL(string: "https://github.com/trilemma-dev/SwiftAuthorizationSample") { 99 | NSWorkspace.shared.open(url) 100 | } 101 | } 102 | 103 | /// Updates the Installation section of the UI. 104 | /// 105 | /// This gets called by the `HelperToolMonitor` when changes occur. 106 | func updateInstallationStatus(_ status: HelperToolMonitor.InstallationStatus) { 107 | DispatchQueue.main.async { 108 | // 8 possible combinations of installation status 109 | if status.registeredWithLaunchd { 110 | if status.registrationPropertyListExists { 111 | // Registered: yes | Registration file: yes | Helper tool: yes 112 | if case .exists(let installedHelperToolVersion) = status.helperToolExecutable { 113 | self.installedField.stringValue = "Yes" 114 | if self.bundledHelperToolVersion > installedHelperToolVersion { 115 | self.installOrUpdateButton.isEnabled = true 116 | self.installOrUpdateButton.title = "Update" 117 | self.installOrUpdateButton.action = #selector(ViewController.update) 118 | let tooltip = "Update helper tool to version \(self.bundledHelperToolVersion.rawValue)" 119 | self.installOrUpdateButton.toolTip = tooltip 120 | } else { 121 | self.installOrUpdateButton.isEnabled = false 122 | self.installOrUpdateButton.title = "Update" 123 | self.installOrUpdateButton.action = nil 124 | let tooltip = "Bundled helper tool version \(self.bundledHelperToolVersion.rawValue) is " + 125 | "not greater than installed version \(installedHelperToolVersion.rawValue)" 126 | self.installOrUpdateButton.toolTip = tooltip 127 | } 128 | self.uninstallButton.isEnabled = true 129 | self.versionField.stringValue = installedHelperToolVersion.rawValue 130 | self.runButton.isEnabled = true 131 | } else { // Registered: yes | Registration file: yes | Helper tool: no 132 | self.installedField.stringValue = "No (helper tool missing)" 133 | self.installOrUpdateButton.isEnabled = true 134 | self.installOrUpdateButton.title = "Install" 135 | self.installOrUpdateButton.action = #selector(ViewController.install) 136 | self.installOrUpdateButton.toolTip = "Install version \(self.bundledHelperToolVersion.rawValue)" 137 | self.uninstallButton.isEnabled = false 138 | self.versionField.stringValue = "—" 139 | self.runButton.isEnabled = false 140 | } 141 | } else { 142 | // Registered: yes | Registration file: no | Helper tool: yes 143 | if case .exists(let installedHelperToolVersion) = status.helperToolExecutable { 144 | self.installedField.stringValue = "No (registration file missing)" 145 | self.installOrUpdateButton.isEnabled = true 146 | self.installOrUpdateButton.title = "Install" 147 | self.installOrUpdateButton.action = #selector(ViewController.install) 148 | self.installOrUpdateButton.toolTip = "Install version \(self.bundledHelperToolVersion.rawValue)" 149 | self.uninstallButton.isEnabled = false 150 | self.versionField.stringValue = installedHelperToolVersion.rawValue 151 | self.runButton.isEnabled = false 152 | } else { // Registered: yes | Registration file: no | Helper tool: no 153 | self.installedField.stringValue = "No (helper tool and registration file missing)" 154 | self.installOrUpdateButton.isEnabled = true 155 | self.installOrUpdateButton.title = "Install" 156 | self.installOrUpdateButton.action = #selector(ViewController.install) 157 | self.installOrUpdateButton.toolTip = "Install version \(self.bundledHelperToolVersion.rawValue)" 158 | self.uninstallButton.isEnabled = false 159 | self.versionField.stringValue = "—" 160 | self.runButton.isEnabled = false 161 | } 162 | } 163 | } else { 164 | if status.registrationPropertyListExists { 165 | // Registered: no | Registration file: yes | Helper tool: yes 166 | if case .exists(let installedHelperToolVersion) = status.helperToolExecutable { 167 | self.installedField.stringValue = "No (helper tool and registration file exist)" 168 | self.installOrUpdateButton.isEnabled = true 169 | self.installOrUpdateButton.title = "Install" 170 | self.installOrUpdateButton.action = #selector(ViewController.install) 171 | self.installOrUpdateButton.toolTip = "Install version \(self.bundledHelperToolVersion.rawValue)" 172 | self.uninstallButton.isEnabled = false 173 | self.versionField.stringValue = installedHelperToolVersion.rawValue 174 | self.runButton.isEnabled = false 175 | } else { // Registered: no | Registration file: yes | Helper tool: no 176 | self.installedField.stringValue = "No (registration file exists)" 177 | self.installOrUpdateButton.isEnabled = true 178 | self.installOrUpdateButton.title = "Install" 179 | self.installOrUpdateButton.action = #selector(ViewController.install) 180 | self.installOrUpdateButton.toolTip = "Install version \(self.bundledHelperToolVersion.rawValue)" 181 | self.uninstallButton.isEnabled = false 182 | self.versionField.stringValue = "—" 183 | self.runButton.isEnabled = false 184 | } 185 | } else { 186 | // Registered: no | Registration file: no | Helper tool: yes 187 | if case .exists(let installedHelperToolVersion) = status.helperToolExecutable { 188 | self.installedField.stringValue = "No (helper tool exists)" 189 | self.installOrUpdateButton.isEnabled = true 190 | self.installOrUpdateButton.title = "Install" 191 | self.installOrUpdateButton.action = #selector(ViewController.install) 192 | let tooltip = "Install helper tool version \(self.bundledHelperToolVersion.rawValue)" 193 | self.installOrUpdateButton.toolTip = tooltip 194 | self.uninstallButton.isEnabled = false 195 | self.versionField.stringValue = installedHelperToolVersion.rawValue 196 | self.runButton.isEnabled = false 197 | } else { // Registered: no | Registration file: no | Helper tool: no 198 | self.installedField.stringValue = "No" 199 | self.installOrUpdateButton.isEnabled = true 200 | self.installOrUpdateButton.title = "Install" 201 | self.installOrUpdateButton.action = #selector(ViewController.install) 202 | self.installOrUpdateButton.toolTip = "Install version \(self.bundledHelperToolVersion.rawValue)" 203 | self.uninstallButton.isEnabled = false 204 | self.versionField.stringValue = "—" 205 | self.runButton.isEnabled = false 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | /// Attempts to install the helper tool, requiring user authorization. 213 | @objc func install(_ sender: NSButton) { 214 | do { 215 | try PrivilegedHelperManager.shared 216 | .authorizeAndBless(message: "Do you want to install the sample helper tool?") 217 | } catch AuthorizationError.canceled { 218 | // No user feedback needed, user canceled 219 | } catch { 220 | self.showModal(title: "Install Failed", error: error) 221 | } 222 | } 223 | 224 | /// Attempts to update the helper tool by having the helper tool perform a self update. 225 | @objc func update(_ sender: NSButton) { 226 | self.xpcClient.sendMessage(self.bundledLocation, to: SharedConstants.updateRoute) { response in 227 | if case .failure(let error) = response { 228 | switch error { 229 | case .connectionInterrupted: 230 | break // It's expected the connection is interrupted as part of updating the client 231 | default: 232 | self.showModal(title: "Update Failed", error: error) 233 | } 234 | } 235 | } 236 | } 237 | 238 | /// Attempts to uninstall the helper tool by having the helper tool uninstall itself. 239 | @IBAction func uninstall(_ sender: NSButton) { 240 | self.xpcClient.send(to: SharedConstants.uninstallRoute) { response in 241 | if case .failure(let error) = response { 242 | switch error { 243 | case .connectionInterrupted: 244 | break // It's expected the connection is interrupted as part of uninstalling the client 245 | default: 246 | self.showModal(title: "Uninstall Failed", error: error) 247 | } 248 | } 249 | } 250 | } 251 | 252 | /// Show a modal to the user. In practice used to communicate an error. 253 | private func showModal(title: String, error: Error) { 254 | DispatchQueue.main.async { 255 | if let window = self.view.window { 256 | let alert = NSAlert() 257 | alert.messageText = title 258 | // Handler error represents an error thrown by a closure registered with the server 259 | if let error = error as? XPCError, case .handlerError(let handlerError) = error { 260 | alert.informativeText = handlerError.localizedDescription 261 | } else { 262 | alert.informativeText = error.localizedDescription 263 | } 264 | alert.addButton(withTitle: "OK") 265 | alert.beginSheetModal(for: window, completionHandler: nil) 266 | _ = NSApp.runModal(for: window) 267 | } 268 | } 269 | } 270 | 271 | /// Requests the helper tool to run the command currently selected in the popup. 272 | @IBAction func run(_ sender: NSButton) { 273 | self.outputText.string = "" // Immediately clear the output, response will be returned async 274 | 275 | guard let command = commandPopup.selectedItem?.representedObject as? AllowedCommand else { 276 | fatalError("Command popup contained unexpected item") 277 | } 278 | 279 | let message: AllowedCommandMessage 280 | if command.requiresAuth { 281 | // If it hasn't been done yet, define the example right used to self-restrict this command 282 | do { 283 | if !(try SharedConstants.exampleRight.isDefined()) { 284 | let rules: Set = [CannedAuthorizationRightRules.authenticateAsAdmin] 285 | let description = "\(ProcessInfo.processInfo.processName) would like to perform a secure action." 286 | try SharedConstants.exampleRight.createOrUpdateDefinition(rules: rules, descriptionKey: description) 287 | } 288 | } catch { 289 | DispatchQueue.main.async { 290 | self.outputText.textColor = NSColor.systemRed 291 | self.outputText.string = error.localizedDescription 292 | } 293 | return 294 | } 295 | 296 | if let authorization = self.authorization { 297 | message = .authorizedCommand(command, authorization) 298 | } else { 299 | do { 300 | let authorization = try Authorization() 301 | self.authorization = authorization 302 | message = .authorizedCommand(command, authorization) 303 | } catch { 304 | DispatchQueue.main.async { 305 | self.outputText.textColor = NSColor.systemRed 306 | self.outputText.string = error.localizedDescription 307 | } 308 | return 309 | } 310 | } 311 | } else { 312 | message = .standardCommand(command) 313 | } 314 | 315 | self.xpcClient.sendMessage(message, 316 | to: SharedConstants.allowedCommandRoute, 317 | withResponse: self.displayAllowedCommandResponse(_:)) 318 | } 319 | 320 | /// Displays the response of requesting the helper tool run the command. 321 | private func displayAllowedCommandResponse(_ result: Result) { 322 | DispatchQueue.main.async { 323 | switch result { 324 | case let .success(reply): 325 | if let standardOutput = reply.standardOutput { 326 | self.outputText.textColor = NSColor.textColor 327 | self.outputText.string = standardOutput 328 | } else if let standardError = reply.standardError { 329 | self.outputText.textColor = NSColor.systemRed 330 | self.outputText.string = standardError 331 | } else { 332 | self.outputText.string = "" 333 | } 334 | case let .failure(error): 335 | self.outputText.textColor = NSColor.systemRed 336 | // Handler error represents an error thrown by a closure registered with the server 337 | if case .handlerError(let handlerError) = error { 338 | self.outputText.string = handlerError.localizedDescription 339 | } else { 340 | self.outputText.string = error.localizedDescription 341 | } 342 | } 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /SwiftAuthorizationHelperTool/AllowedCommandRunner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllowedProcessRunner.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-24 6 | // 7 | 8 | import Foundation 9 | import Authorized 10 | 11 | /// Runs an allowed command. 12 | enum AllowedCommandRunner { 13 | /// Runs the allowed command and replies with the results. 14 | /// 15 | /// If authorization is needed, the user will be prompted. 16 | /// 17 | /// - Parameter message: Message containing the command to run, and if applicable the authorization. 18 | /// - Returns: The results of running the command. 19 | static func run(message: AllowedCommandMessage) throws -> AllowedCommandReply { 20 | // Prompt user to authorize if the client requested it 21 | if case .authorizedCommand(_, let authorization) = message { 22 | let rights = try authorization.requestRights([SharedConstants.exampleRight], 23 | environment: [], 24 | options: [.interactionAllowed, .extendRights]) 25 | guard rights.contains(where: { $0.name == SharedConstants.exampleRight.name }) else { 26 | throw AllowedCommandError.authorizationFailed 27 | } 28 | } else if message.command.requiresAuth { // Authorization is required, but the client did not request it 29 | throw AllowedCommandError.authorizationNotRequested 30 | } 31 | 32 | // Launch process and wait for it to finish 33 | let process = Process() 34 | process.launchPath = message.command.launchPath 35 | process.arguments = message.command.arguments 36 | process.qualityOfService = QualityOfService.userInitiated 37 | let outputPipe = Pipe() 38 | defer { outputPipe.fileHandleForReading.closeFile() } 39 | process.standardOutput = outputPipe 40 | let errorPipe = Pipe() 41 | defer { errorPipe.fileHandleForReading.closeFile() } 42 | process.standardError = errorPipe 43 | process.launch() 44 | process.waitUntilExit() 45 | 46 | // Convert a pipe's data to a string if there was non-whitespace output 47 | let pipeAsString = { (pipe: Pipe) -> String? in 48 | let output = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8)? 49 | .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" 50 | return output.isEmpty ? nil : output 51 | } 52 | 53 | return AllowedCommandReply(terminationStatus: process.terminationStatus, 54 | standardOutput: pipeAsString(outputPipe), 55 | standardError: pipeAsString(errorPipe)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SwiftAuthorizationHelperTool/HelperToolConfig.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // HelperToolConfig.xcconfig 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-23 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | #include "Config.xcconfig" 12 | 13 | // The directory containing the source code and property lists for the helper tool. 14 | TARGET_DIRECTORY = SwiftAuthorizationHelperTool 15 | 16 | 17 | // ** There should not be a need to modify anything below this line in this configuration file ** // 18 | 19 | 20 | // Bundle identifier used both in the info property list and so the build script knows which target it is running for. 21 | // If you want to change the bundle identifier, change the value for HELPER_TOOL_BUNDLE_IDENTIFIER in Config.xcconfig. 22 | PRODUCT_BUNDLE_IDENTIFIER = $(HELPER_TOOL_BUNDLE_IDENTIFIER) 23 | 24 | // Name of the executable created by the build process. To satisfy SMJobBless this must match the bundle identifier. 25 | TARGET_NAME = $(HELPER_TOOL_BUNDLE_IDENTIFIER) 26 | 27 | // The product name match the name of executable. 28 | PRODUCT_NAME = $(HELPER_TOOL_BUNDLE_IDENTIFIER) 29 | 30 | // Property list locations 31 | INFOPLIST_FILE = $(TARGET_DIRECTORY)/Info.plist 32 | LAUNCHDPLIST_FILE = $(TARGET_DIRECTORY)/launchd.plist 33 | 34 | // Inlines the property list files into the helper tool's binary. 35 | // Note that CREATE_INFOPLIST_SECTION_IN_BINARY = YES can't be used to inline the info property list because this step 36 | // occurs immediately *before* any scripts are run, preventing the property list from being modified. 37 | OTHER_LDFLAGS = -sectcreate __TEXT __info_plist $(INFOPLIST_FILE) -sectcreate __TEXT __launchd_plist $(LAUNCHDPLIST_FILE) 38 | 39 | // Tells Xcode Archive the helper tool shouldn't be an installable artifact (the app should be). 40 | SKIP_INSTALL = YES 41 | 42 | // Used by the shared code to know which target it's being built for 43 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = HELPER_TOOL 44 | -------------------------------------------------------------------------------- /SwiftAuthorizationHelperTool/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleInfoDictionaryVersion 6 | 6.0 7 | CFBundleVersion 8 | 1.0.0 9 | 10 | 11 | -------------------------------------------------------------------------------- /SwiftAuthorizationHelperTool/Uninstaller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Uninstaller.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-24 6 | // 7 | 8 | import Foundation 9 | 10 | /// A self uninstaller which performs the logical equivalent of the non-existent `SMJobUnbless`. 11 | /// 12 | /// Because Apple does not provide an API to perform an "unbless" operation, the technique used here relies on a few key behaviors: 13 | /// - To deregister the helper tool with launchd, the `launchctl` command line utility which ships with macOS is used 14 | /// - The `unload` command used is publicly documented 15 | /// - An assumption that this helper tool when installed is located at `/Library/PrivilegedHelperTools/` 16 | /// - While this location is not documented in `SMJobBless`, it is used in Apple's EvenBetterAuthorizationSample `Uninstall.sh` script 17 | /// - This is used to determine if this helper tool is in fact running from the blessed location 18 | /// - To remove the `launchd` property list, its location is assumed to be `/Library/LaunchDaemons/.plist` 19 | /// - While this location is not documented in `SMJobBless`, it is used in Apple's EvenBetterAuthorizationSample `Uninstall.sh` script 20 | enum Uninstaller { 21 | /// Errors that prevent the uninstall from succeeding. 22 | enum UninstallError: Error { 23 | /// Uninstall will not be performed because this code is not running from the blessed location. 24 | case notRunningFromBlessedLocation(location: URL) 25 | /// Attempting to unload using `launchctl` failed. 26 | case launchctlFailure(statusCode: Int32) 27 | /// The argument provided must be a process identifier, but was not. 28 | case notProcessId(invalidArgument: String) 29 | } 30 | 31 | /// Command line argument that identifies to `main` to call the `uninstallFromCommandLine(...)` function. 32 | static let commandLineArgument = "uninstall" 33 | 34 | /// Indirectly uninstalls this helper tool. Calling this function will terminate this process unless an error is throw. 35 | /// 36 | /// Uninstalls this helper tool by relaunching itself not via XPC such that the installation can occur succesfully. 37 | /// 38 | /// - Throws: If unable to determine the on disk location of this running code. 39 | static func uninstallFromXPC() throws { 40 | NSLog("uninstall requested, PID \(getpid())") 41 | let process = Process() 42 | process.launchPath = try CodeInfo.currentCodeLocation().path 43 | process.qualityOfService = QualityOfService.utility 44 | process.arguments = [commandLineArgument, String(getpid())] 45 | process.launch() 46 | NSLog("about to exit...") 47 | exit(0) 48 | } 49 | 50 | /// Directly uninstalls this executable. Calling this function will terminate this process unless an error is thrown. 51 | /// 52 | /// Depending on the the arguments provided to this function, it may wait on another process to exit before performing the uninstall. 53 | /// 54 | /// - Parameter arguments: The command line arguments; the first argument should always be `uninstall`. 55 | /// - Throws: If unable to perform the uninstall, including because the provided arguments are invalid. 56 | static func uninstallFromCommandLine(withArguments arguments: [String]) throws -> Never { 57 | if arguments.count == 1 { 58 | try uninstallImmediately() 59 | } else { 60 | guard let pid: pid_t = Int32(arguments[1]) else { 61 | throw UninstallError.notProcessId(invalidArgument: arguments[1]) 62 | } 63 | try uninstallAfterProcessExits(pid: pid) 64 | } 65 | } 66 | 67 | /// Uninstalls this helper tool after waiting for a process (in practice this helper tool launched via XPC) to terminate. 68 | /// 69 | /// - Parameter pid: Identifier for the process which must terminate before performing uninstall. In practice this is identifier is for is a previous run of this helper tool. 70 | private static func uninstallAfterProcessExits(pid: pid_t) throws -> Never { 71 | // When passing 0 as the second argument, no signal is sent, but existence and permission checks are still 72 | // performed. This checks for the existence of a process ID. If 0 is returned the process still exists, so loop 73 | // until 0 is no longer returned. 74 | while kill(pid, 0) == 0 { // in practice this condition almost always evaluates to false on its first check 75 | usleep(50 * 1000) // sleep for 50ms 76 | NSLog("PID \(getpid()) waited 50ms for PID \(pid)") 77 | } 78 | NSLog("PID \(getpid()) done waiting for PID \(pid)") 79 | 80 | try uninstallImmediately() 81 | } 82 | 83 | /// Uninstalls this helper tool. 84 | /// 85 | /// This function will not work if called when this helper tool was started by an XPC call because `launchctl` will be unable to unload. 86 | /// 87 | /// If the uninstall fails when deleting either the `launchd` property list for this executable or the on disk representation of this helper tool then the uninstall 88 | /// will be an incomplete state; however, it will no longer be started by `launchd` (and in turn not accessible via XPC) and so will be mostly uninstalled even 89 | /// though some on disk portions will remain. 90 | /// 91 | /// - Throws: Due to one of: unable to determine the on disk location of this running code, that location is not the blessed location, `launchctl` can't 92 | /// unload this helper tool, the `launchd` property list for this helper tool can't be deleted, or the on disk representation of this helper tool can't be deleted. 93 | private static func uninstallImmediately() throws -> Never { 94 | let sharedConstants = try SharedConstants() 95 | let currentLocation = try CodeInfo.currentCodeLocation() 96 | guard currentLocation == sharedConstants.blessedLocation else { 97 | throw UninstallError.notRunningFromBlessedLocation(location: currentLocation) 98 | } 99 | 100 | // Equivalent to: launchctl unload /Library/LaunchDaemons/.plist 101 | let process = Process() 102 | process.launchPath = "/bin/launchctl" 103 | process.qualityOfService = QualityOfService.utility 104 | process.arguments = ["unload", sharedConstants.blessedPropertyListLocation.path] 105 | process.launch() 106 | NSLog("about to wait for launchctl...") 107 | process.waitUntilExit() 108 | let terminationStatus = process.terminationStatus 109 | guard terminationStatus == 0 else { 110 | throw UninstallError.launchctlFailure(statusCode: terminationStatus) 111 | } 112 | NSLog("unloaded from launchctl") 113 | 114 | // Equivalent to: rm /Library/LaunchDaemons/.plist 115 | try FileManager.default.removeItem(at: sharedConstants.blessedPropertyListLocation) 116 | NSLog("property list deleted") 117 | 118 | // Equivalent to: rm /Library/PrivilegedHelperTools/ 119 | try FileManager.default.removeItem(at: sharedConstants.blessedLocation) 120 | NSLog("helper tool deleted") 121 | NSLog("uninstall completed, exiting...") 122 | exit(0) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /SwiftAuthorizationHelperTool/Updater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Updater.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-24 6 | // 7 | 8 | import Foundation 9 | import EmbeddedPropertyList 10 | 11 | /// An in-place updater for the helper tool. 12 | /// 13 | /// To keep things simple, this updater only works if `launchd` property lists do not change between versions. 14 | enum Updater { 15 | /// Replaces itself with the helper tool located at the provided `URL` so long as security, launchd, and version requirements are met. 16 | /// 17 | /// - Parameter helperTool: Path to the helper tool. 18 | /// - Throws: If the helper tool file can't be read, public keys can't be determined, or `launchd` property lists can't be compared. 19 | static func updateHelperTool(atPath helperTool: URL) throws { 20 | guard try CodeInfo.doesPublicKeyMatch(forExecutable: helperTool) else { 21 | NSLog("update failed: security requirements not met") 22 | return 23 | } 24 | 25 | guard try launchdPropertyListsMatch(forHelperTool: helperTool) else { 26 | NSLog("update failed: launchd property list has changed") 27 | return 28 | } 29 | 30 | let (isNewer, currentVersion, otherVersion) = try isHelperToolNewerVersion(atPath: helperTool) 31 | guard isNewer else { 32 | NSLog("update failed: not a newer version. current: \(currentVersion), other: \(otherVersion).") 33 | return 34 | } 35 | 36 | try Data(contentsOf: helperTool).write(to: CodeInfo.currentCodeLocation(), options: .atomicWrite) 37 | NSLog("update succeeded: current version \(currentVersion) exiting...") 38 | exit(0) 39 | } 40 | 41 | /// Determines if the helper tool located at the provided `URL` is actually an update. 42 | /// 43 | /// - Parameter helperTool: Path to the helper tool. 44 | /// - Throws: If unable to read the info property lists of this helper tool or the one located at `helperTool`. 45 | /// - Returns: If the helper tool at the location specified by `helperTool` is newer than the one running this code and the versions of both. 46 | private static func isHelperToolNewerVersion( 47 | atPath helperTool: URL 48 | ) throws -> (isNewer: Bool, current: BundleVersion, other: BundleVersion) { 49 | let current = try HelperToolInfoPropertyList.main.version 50 | let other = try HelperToolInfoPropertyList(from: helperTool).version 51 | 52 | return (other > current, current, other) 53 | } 54 | 55 | /// Determines if the `launchd` property list used by this helper tool and the executable located at the provided `URL` are byte-for-byte identical. 56 | /// 57 | /// This matters because only the helper tool itself is being updated, the property list generated for `launchd` will not be updated as part of this update 58 | /// process. 59 | /// 60 | /// - Parameter helperTool: Path to the helper tool. 61 | /// - Throws: If unable to read the `launchd` property lists of this helper tool or the one located at `helperTool`. 62 | /// - Returns: If the two `launchd` property lists match. 63 | private static func launchdPropertyListsMatch(forHelperTool helperTool: URL) throws -> Bool { 64 | try EmbeddedPropertyListReader.launchd.readInternal() == 65 | EmbeddedPropertyListReader.launchd.readExternal(from: helperTool) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /SwiftAuthorizationHelperTool/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // SwiftAuthorizationSample 4 | // 5 | // Created by Josh Kaplan on 2021-10-21 6 | // 7 | 8 | import Foundation 9 | import SecureXPC 10 | 11 | NSLog("starting helper tool. PID \(getpid()). PPID \(getppid()).") 12 | NSLog("version: \(try HelperToolInfoPropertyList.main.version.rawValue)") 13 | 14 | // Command line arguments were provided, so process them 15 | if CommandLine.arguments.count > 1 { 16 | // Remove the first argument, which represents the name (typically the full path) of this helper tool 17 | var arguments = CommandLine.arguments 18 | _ = arguments.removeFirst() 19 | NSLog("run with arguments: \(arguments)") 20 | 21 | if let firstArgument = arguments.first { 22 | if firstArgument == Uninstaller.commandLineArgument { 23 | try Uninstaller.uninstallFromCommandLine(withArguments: arguments) 24 | } else { 25 | NSLog("argument not recognized: \(firstArgument)") 26 | } 27 | } 28 | } else if getppid() == 1 { // Otherwise if started by launchd, start up server 29 | NSLog("parent is launchd, starting up server") 30 | 31 | let server = try XPCServer.forMachService() 32 | server.registerRoute(SharedConstants.allowedCommandRoute, handler: AllowedCommandRunner.run(message:)) 33 | server.registerRoute(SharedConstants.uninstallRoute, handler: Uninstaller.uninstallFromXPC) 34 | server.registerRoute(SharedConstants.updateRoute, handler: Updater.updateHelperTool(atPath:)) 35 | server.setErrorHandler { error in 36 | if case .connectionInvalid = error { 37 | // Ignore invalidated connections as this happens whenever the client disconnects which is not a problem 38 | } else { 39 | NSLog("error: \(error)") 40 | } 41 | } 42 | server.startAndBlock() 43 | } else { // Otherwise started via command line without arguments, print out help info 44 | print("Usage: \(try CodeInfo.currentCodeLocation().lastPathComponent) ") 45 | print("\nCommands:") 46 | print("\t\(Uninstaller.commandLineArgument)\tUnloads and deletes from disk this helper tool and configuration.") 47 | } 48 | -------------------------------------------------------------------------------- /SwiftAuthorizationSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 995AF20C274A586600F855D1 /* CodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B5727243FD800F7D92D /* CodeInfo.swift */; }; 11 | 99662095272557FF00ECE5C7 /* EmbeddedPropertyList in Frameworks */ = {isa = PBXBuildFile; productRef = 99662094272557FF00ECE5C7 /* EmbeddedPropertyList */; }; 12 | 996620972725580A00ECE5C7 /* EmbeddedPropertyList in Frameworks */ = {isa = PBXBuildFile; productRef = 996620962725580A00ECE5C7 /* EmbeddedPropertyList */; }; 13 | 998D9C342865D623006224C4 /* Authorized in Frameworks */ = {isa = PBXBuildFile; productRef = 998D9C332865D623006224C4 /* Authorized */; }; 14 | 99A67B332723B2D200F7D92D /* SecureXPC in Frameworks */ = {isa = PBXBuildFile; productRef = 99A67B322723B2D200F7D92D /* SecureXPC */; }; 15 | 99A67B392723B2FC00F7D92D /* Blessed in Frameworks */ = {isa = PBXBuildFile; productRef = 99A67B382723B2FC00F7D92D /* Blessed */; }; 16 | 99A67B3C2723B30800F7D92D /* Blessed in Frameworks */ = {isa = PBXBuildFile; productRef = 99A67B3B2723B30800F7D92D /* Blessed */; }; 17 | 99A67B3E2723B30D00F7D92D /* SecureXPC in Frameworks */ = {isa = PBXBuildFile; productRef = 99A67B3D2723B30D00F7D92D /* SecureXPC */; }; 18 | 99A67B4827240DB100F7D92D /* HelperToolMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B4727240DB100F7D92D /* HelperToolMonitor.swift */; }; 19 | 99A67B4A2724111200F7D92D /* HelperToolLaunchdPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B492724111200F7D92D /* HelperToolLaunchdPropertyList.swift */; }; 20 | 99A67B4B2724111200F7D92D /* HelperToolLaunchdPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B492724111200F7D92D /* HelperToolLaunchdPropertyList.swift */; }; 21 | 99A67B4D2724125200F7D92D /* HelperToolInfoPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B4C2724125200F7D92D /* HelperToolInfoPropertyList.swift */; }; 22 | 99A67B4E2724125200F7D92D /* HelperToolInfoPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B4C2724125200F7D92D /* HelperToolInfoPropertyList.swift */; }; 23 | 99A67B502724134800F7D92D /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B4F2724134800F7D92D /* SharedConstants.swift */; }; 24 | 99A67B512724134800F7D92D /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B4F2724134800F7D92D /* SharedConstants.swift */; }; 25 | 99A67B53272439E400F7D92D /* AllowedCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B52272439E400F7D92D /* AllowedCommand.swift */; }; 26 | 99A67B54272439E400F7D92D /* AllowedCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B52272439E400F7D92D /* AllowedCommand.swift */; }; 27 | 99A67B5627243F6E00F7D92D /* Uninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B5527243F6E00F7D92D /* Uninstaller.swift */; }; 28 | 99A67B5827243FD800F7D92D /* CodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B5727243FD800F7D92D /* CodeInfo.swift */; }; 29 | 99A67B5A2724413300F7D92D /* AllowedCommandRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B592724413300F7D92D /* AllowedCommandRunner.swift */; }; 30 | 99A67B5C2724416C00F7D92D /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99A67B5B2724416C00F7D92D /* Updater.swift */; }; 31 | 99F409CF27216B210010500C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F409CE27216B210010500C /* AppDelegate.swift */; }; 32 | 99F409D127216B210010500C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F409D027216B210010500C /* ViewController.swift */; }; 33 | 99F409D327216B230010500C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 99F409D227216B230010500C /* Assets.xcassets */; }; 34 | 99F409D627216B230010500C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 99F409D427216B230010500C /* Main.storyboard */; }; 35 | 99F409E427216BFF0010500C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F409E327216BFF0010500C /* main.swift */; }; 36 | 99F409FA272308C50010500C /* com.example.SwiftAuthorizationApp.helper in CopyFiles */ = {isa = PBXBuildFile; fileRef = 99F409E127216BFF0010500C /* com.example.SwiftAuthorizationApp.helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXCopyFilesBuildPhase section */ 40 | 99F409DF27216BFF0010500C /* CopyFiles */ = { 41 | isa = PBXCopyFilesBuildPhase; 42 | buildActionMask = 2147483647; 43 | dstPath = /usr/share/man/man1/; 44 | dstSubfolderSpec = 0; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 1; 48 | }; 49 | 99F409F32723044C0010500C /* CopyFiles */ = { 50 | isa = PBXCopyFilesBuildPhase; 51 | buildActionMask = 2147483647; 52 | dstPath = Contents/Library/LaunchServices; 53 | dstSubfolderSpec = 1; 54 | files = ( 55 | 99F409FA272308C50010500C /* com.example.SwiftAuthorizationApp.helper in CopyFiles */, 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXCopyFilesBuildPhase section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | 99A67B4727240DB100F7D92D /* HelperToolMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolMonitor.swift; sourceTree = ""; }; 63 | 99A67B492724111200F7D92D /* HelperToolLaunchdPropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolLaunchdPropertyList.swift; sourceTree = ""; }; 64 | 99A67B4C2724125200F7D92D /* HelperToolInfoPropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolInfoPropertyList.swift; sourceTree = ""; }; 65 | 99A67B4F2724134800F7D92D /* SharedConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConstants.swift; sourceTree = ""; }; 66 | 99A67B52272439E400F7D92D /* AllowedCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllowedCommand.swift; sourceTree = ""; }; 67 | 99A67B5527243F6E00F7D92D /* Uninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uninstaller.swift; sourceTree = ""; }; 68 | 99A67B5727243FD800F7D92D /* CodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeInfo.swift; sourceTree = ""; }; 69 | 99A67B592724413300F7D92D /* AllowedCommandRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllowedCommandRunner.swift; sourceTree = ""; }; 70 | 99A67B5B2724416C00F7D92D /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; }; 71 | 99F409CB27216B210010500C /* Swift Authorization Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Swift Authorization Sample.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | 99F409CE27216B210010500C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 73 | 99F409D027216B210010500C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 74 | 99F409D227216B230010500C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 75 | 99F409D527216B230010500C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 76 | 99F409E127216BFF0010500C /* com.example.SwiftAuthorizationApp.helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = com.example.SwiftAuthorizationApp.helper; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | 99F409E327216BFF0010500C /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 78 | 99F409E827216CD10010500C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 79 | 99F409EA2722FBA10010500C /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 80 | 99F409EB2722FC670010500C /* AppConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppConfig.xcconfig; sourceTree = ""; }; 81 | 99F409EC2722FD0C0010500C /* HelperToolConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = HelperToolConfig.xcconfig; sourceTree = ""; }; 82 | 99F409EE272300EB0010500C /* PropertyListModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyListModifier.swift; sourceTree = ""; }; 83 | 99F409F6272304B20010500C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84 | 99F6E8552729484500ED2001 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | 99F409C827216B210010500C /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | 99A67B392723B2FC00F7D92D /* Blessed in Frameworks */, 93 | 998D9C342865D623006224C4 /* Authorized in Frameworks */, 94 | 99A67B332723B2D200F7D92D /* SecureXPC in Frameworks */, 95 | 99662095272557FF00ECE5C7 /* EmbeddedPropertyList in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | 99F409DE27216BFF0010500C /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | 99A67B3C2723B30800F7D92D /* Blessed in Frameworks */, 104 | 99A67B3E2723B30D00F7D92D /* SecureXPC in Frameworks */, 105 | 996620972725580A00ECE5C7 /* EmbeddedPropertyList in Frameworks */, 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | /* End PBXFrameworksBuildPhase section */ 110 | 111 | /* Begin PBXGroup section */ 112 | 99A67B3A2723B30800F7D92D /* Frameworks */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | ); 116 | name = Frameworks; 117 | sourceTree = ""; 118 | }; 119 | 99F409C227216B210010500C = { 120 | isa = PBXGroup; 121 | children = ( 122 | 99F409E827216CD10010500C /* README.md */, 123 | 99F409EA2722FBA10010500C /* Config.xcconfig */, 124 | 99F409F52723049E0010500C /* Shared */, 125 | 99F409CD27216B210010500C /* SwiftAuthorizationApp */, 126 | 99F409E227216BFF0010500C /* SwiftAuthorizationHelperTool */, 127 | 99F409ED272300910010500C /* BuildScripts */, 128 | 99F409CC27216B210010500C /* Products */, 129 | 99A67B3A2723B30800F7D92D /* Frameworks */, 130 | ); 131 | sourceTree = ""; 132 | }; 133 | 99F409CC27216B210010500C /* Products */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 99F409CB27216B210010500C /* Swift Authorization Sample.app */, 137 | 99F409E127216BFF0010500C /* com.example.SwiftAuthorizationApp.helper */, 138 | ); 139 | name = Products; 140 | sourceTree = ""; 141 | }; 142 | 99F409CD27216B210010500C /* SwiftAuthorizationApp */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 99F409EB2722FC670010500C /* AppConfig.xcconfig */, 146 | 99F409CE27216B210010500C /* AppDelegate.swift */, 147 | 99A67B4727240DB100F7D92D /* HelperToolMonitor.swift */, 148 | 99F409D027216B210010500C /* ViewController.swift */, 149 | 99F409D427216B230010500C /* Main.storyboard */, 150 | 99F409D227216B230010500C /* Assets.xcassets */, 151 | 99F6E8552729484500ED2001 /* Info.plist */, 152 | ); 153 | path = SwiftAuthorizationApp; 154 | sourceTree = ""; 155 | }; 156 | 99F409E227216BFF0010500C /* SwiftAuthorizationHelperTool */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 99F409EC2722FD0C0010500C /* HelperToolConfig.xcconfig */, 160 | 99F409F6272304B20010500C /* Info.plist */, 161 | 99F409E327216BFF0010500C /* main.swift */, 162 | 99A67B5527243F6E00F7D92D /* Uninstaller.swift */, 163 | 99A67B5B2724416C00F7D92D /* Updater.swift */, 164 | 99A67B592724413300F7D92D /* AllowedCommandRunner.swift */, 165 | ); 166 | path = SwiftAuthorizationHelperTool; 167 | sourceTree = ""; 168 | }; 169 | 99F409ED272300910010500C /* BuildScripts */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 99F409EE272300EB0010500C /* PropertyListModifier.swift */, 173 | ); 174 | path = BuildScripts; 175 | sourceTree = ""; 176 | }; 177 | 99F409F52723049E0010500C /* Shared */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 99A67B52272439E400F7D92D /* AllowedCommand.swift */, 181 | 99A67B4F2724134800F7D92D /* SharedConstants.swift */, 182 | 99A67B4C2724125200F7D92D /* HelperToolInfoPropertyList.swift */, 183 | 99A67B492724111200F7D92D /* HelperToolLaunchdPropertyList.swift */, 184 | 99A67B5727243FD800F7D92D /* CodeInfo.swift */, 185 | ); 186 | path = Shared; 187 | sourceTree = ""; 188 | }; 189 | /* End PBXGroup section */ 190 | 191 | /* Begin PBXNativeTarget section */ 192 | 99F409CA27216B210010500C /* SwiftAuthorizationApp */ = { 193 | isa = PBXNativeTarget; 194 | buildConfigurationList = 99F409DA27216B230010500C /* Build configuration list for PBXNativeTarget "SwiftAuthorizationApp" */; 195 | buildPhases = ( 196 | 99F409EF2723039B0010500C /* ShellScript */, 197 | 99F409C727216B210010500C /* Sources */, 198 | 99F409C827216B210010500C /* Frameworks */, 199 | 99F409C927216B210010500C /* Resources */, 200 | 99F409F32723044C0010500C /* CopyFiles */, 201 | 99F409F0272303D10010500C /* ShellScript */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | ); 207 | name = SwiftAuthorizationApp; 208 | packageProductDependencies = ( 209 | 99A67B322723B2D200F7D92D /* SecureXPC */, 210 | 99A67B382723B2FC00F7D92D /* Blessed */, 211 | 99662094272557FF00ECE5C7 /* EmbeddedPropertyList */, 212 | 998D9C332865D623006224C4 /* Authorized */, 213 | ); 214 | productName = SwiftAuthorizationSample; 215 | productReference = 99F409CB27216B210010500C /* Swift Authorization Sample.app */; 216 | productType = "com.apple.product-type.application"; 217 | }; 218 | 99F409E027216BFF0010500C /* SwiftAuthorizationHelperTool */ = { 219 | isa = PBXNativeTarget; 220 | buildConfigurationList = 99F409E527216BFF0010500C /* Build configuration list for PBXNativeTarget "SwiftAuthorizationHelperTool" */; 221 | buildPhases = ( 222 | 99F409F1272303E00010500C /* ShellScript */, 223 | 99F409DD27216BFF0010500C /* Sources */, 224 | 99F409DE27216BFF0010500C /* Frameworks */, 225 | 99F409DF27216BFF0010500C /* CopyFiles */, 226 | 99F409F2272303F20010500C /* ShellScript */, 227 | ); 228 | buildRules = ( 229 | ); 230 | dependencies = ( 231 | ); 232 | name = SwiftAuthorizationHelperTool; 233 | packageProductDependencies = ( 234 | 99A67B3B2723B30800F7D92D /* Blessed */, 235 | 99A67B3D2723B30D00F7D92D /* SecureXPC */, 236 | 996620962725580A00ECE5C7 /* EmbeddedPropertyList */, 237 | ); 238 | productName = SwiftAuthorizationHelper; 239 | productReference = 99F409E127216BFF0010500C /* com.example.SwiftAuthorizationApp.helper */; 240 | productType = "com.apple.product-type.tool"; 241 | }; 242 | /* End PBXNativeTarget section */ 243 | 244 | /* Begin PBXProject section */ 245 | 99F409C327216B210010500C /* Project object */ = { 246 | isa = PBXProject; 247 | attributes = { 248 | BuildIndependentTargetsInParallel = 1; 249 | LastSwiftUpdateCheck = 1300; 250 | LastUpgradeCheck = 1300; 251 | TargetAttributes = { 252 | 99F409CA27216B210010500C = { 253 | CreatedOnToolsVersion = 13.0; 254 | }; 255 | 99F409E027216BFF0010500C = { 256 | CreatedOnToolsVersion = 13.0; 257 | }; 258 | }; 259 | }; 260 | buildConfigurationList = 99F409C627216B210010500C /* Build configuration list for PBXProject "SwiftAuthorizationSample" */; 261 | compatibilityVersion = "Xcode 13.0"; 262 | developmentRegion = en; 263 | hasScannedForEncodings = 0; 264 | knownRegions = ( 265 | en, 266 | Base, 267 | ); 268 | mainGroup = 99F409C227216B210010500C; 269 | packageReferences = ( 270 | 99A67B312723B2D200F7D92D /* XCRemoteSwiftPackageReference "SecureXPC" */, 271 | 99A67B372723B2FC00F7D92D /* XCRemoteSwiftPackageReference "Blessed" */, 272 | 99662093272557FF00ECE5C7 /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */, 273 | 998D9C322865D623006224C4 /* XCRemoteSwiftPackageReference "Authorized" */, 274 | ); 275 | productRefGroup = 99F409CC27216B210010500C /* Products */; 276 | projectDirPath = ""; 277 | projectRoot = ""; 278 | targets = ( 279 | 99F409CA27216B210010500C /* SwiftAuthorizationApp */, 280 | 99F409E027216BFF0010500C /* SwiftAuthorizationHelperTool */, 281 | ); 282 | }; 283 | /* End PBXProject section */ 284 | 285 | /* Begin PBXResourcesBuildPhase section */ 286 | 99F409C927216B210010500C /* Resources */ = { 287 | isa = PBXResourcesBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | 99F409D327216B230010500C /* Assets.xcassets in Resources */, 291 | 99F409D627216B230010500C /* Main.storyboard in Resources */, 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | /* End PBXResourcesBuildPhase section */ 296 | 297 | /* Begin PBXShellScriptBuildPhase section */ 298 | 99F409EF2723039B0010500C /* ShellScript */ = { 299 | isa = PBXShellScriptBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | ); 303 | inputFileListPaths = ( 304 | ); 305 | inputPaths = ( 306 | ); 307 | outputFileListPaths = ( 308 | ); 309 | outputPaths = ( 310 | ); 311 | runOnlyForDeploymentPostprocessing = 0; 312 | shellPath = /bin/sh; 313 | shellScript = "\"${SRCROOT}\"/BuildScripts/PropertyListModifier.swift satisfy-job-bless-requirements\n"; 314 | }; 315 | 99F409F0272303D10010500C /* ShellScript */ = { 316 | isa = PBXShellScriptBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | ); 320 | inputFileListPaths = ( 321 | ); 322 | inputPaths = ( 323 | ); 324 | outputFileListPaths = ( 325 | ); 326 | outputPaths = ( 327 | ); 328 | runOnlyForDeploymentPostprocessing = 0; 329 | shellPath = /bin/sh; 330 | shellScript = "\"${SRCROOT}\"/BuildScripts/PropertyListModifier.swift cleanup-job-bless-requirements\n"; 331 | }; 332 | 99F409F1272303E00010500C /* ShellScript */ = { 333 | isa = PBXShellScriptBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | ); 337 | inputFileListPaths = ( 338 | ); 339 | inputPaths = ( 340 | ); 341 | outputFileListPaths = ( 342 | ); 343 | outputPaths = ( 344 | ); 345 | runOnlyForDeploymentPostprocessing = 0; 346 | shellPath = /bin/sh; 347 | shellScript = "\"${SRCROOT}\"/BuildScripts/PropertyListModifier.swift satisfy-job-bless-requirements specify-mach-services auto-increment-version\n\n"; 348 | }; 349 | 99F409F2272303F20010500C /* ShellScript */ = { 350 | isa = PBXShellScriptBuildPhase; 351 | buildActionMask = 2147483647; 352 | files = ( 353 | ); 354 | inputFileListPaths = ( 355 | ); 356 | inputPaths = ( 357 | ); 358 | outputFileListPaths = ( 359 | ); 360 | outputPaths = ( 361 | ); 362 | runOnlyForDeploymentPostprocessing = 0; 363 | shellPath = /bin/sh; 364 | shellScript = "\"${SRCROOT}\"/BuildScripts/PropertyListModifier.swift cleanup-job-bless-requirements cleanup-mach-services\n"; 365 | }; 366 | /* End PBXShellScriptBuildPhase section */ 367 | 368 | /* Begin PBXSourcesBuildPhase section */ 369 | 99F409C727216B210010500C /* Sources */ = { 370 | isa = PBXSourcesBuildPhase; 371 | buildActionMask = 2147483647; 372 | files = ( 373 | 995AF20C274A586600F855D1 /* CodeInfo.swift in Sources */, 374 | 99A67B4A2724111200F7D92D /* HelperToolLaunchdPropertyList.swift in Sources */, 375 | 99F409D127216B210010500C /* ViewController.swift in Sources */, 376 | 99F409CF27216B210010500C /* AppDelegate.swift in Sources */, 377 | 99A67B53272439E400F7D92D /* AllowedCommand.swift in Sources */, 378 | 99A67B4D2724125200F7D92D /* HelperToolInfoPropertyList.swift in Sources */, 379 | 99A67B502724134800F7D92D /* SharedConstants.swift in Sources */, 380 | 99A67B4827240DB100F7D92D /* HelperToolMonitor.swift in Sources */, 381 | ); 382 | runOnlyForDeploymentPostprocessing = 0; 383 | }; 384 | 99F409DD27216BFF0010500C /* Sources */ = { 385 | isa = PBXSourcesBuildPhase; 386 | buildActionMask = 2147483647; 387 | files = ( 388 | 99A67B512724134800F7D92D /* SharedConstants.swift in Sources */, 389 | 99A67B5627243F6E00F7D92D /* Uninstaller.swift in Sources */, 390 | 99A67B4B2724111200F7D92D /* HelperToolLaunchdPropertyList.swift in Sources */, 391 | 99A67B54272439E400F7D92D /* AllowedCommand.swift in Sources */, 392 | 99A67B4E2724125200F7D92D /* HelperToolInfoPropertyList.swift in Sources */, 393 | 99A67B5C2724416C00F7D92D /* Updater.swift in Sources */, 394 | 99A67B5827243FD800F7D92D /* CodeInfo.swift in Sources */, 395 | 99A67B5A2724413300F7D92D /* AllowedCommandRunner.swift in Sources */, 396 | 99F409E427216BFF0010500C /* main.swift in Sources */, 397 | ); 398 | runOnlyForDeploymentPostprocessing = 0; 399 | }; 400 | /* End PBXSourcesBuildPhase section */ 401 | 402 | /* Begin PBXVariantGroup section */ 403 | 99F409D427216B230010500C /* Main.storyboard */ = { 404 | isa = PBXVariantGroup; 405 | children = ( 406 | 99F409D527216B230010500C /* Base */, 407 | ); 408 | name = Main.storyboard; 409 | sourceTree = ""; 410 | }; 411 | /* End PBXVariantGroup section */ 412 | 413 | /* Begin XCBuildConfiguration section */ 414 | 99F409D827216B230010500C /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | baseConfigurationReference = 99F409EA2722FBA10010500C /* Config.xcconfig */; 417 | buildSettings = { 418 | ALWAYS_SEARCH_USER_PATHS = NO; 419 | CLANG_ANALYZER_NONNULL = YES; 420 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 421 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 422 | CLANG_CXX_LIBRARY = "libc++"; 423 | CLANG_ENABLE_MODULES = YES; 424 | CLANG_ENABLE_OBJC_ARC = YES; 425 | CLANG_ENABLE_OBJC_WEAK = YES; 426 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 427 | CLANG_WARN_BOOL_CONVERSION = YES; 428 | CLANG_WARN_COMMA = YES; 429 | CLANG_WARN_CONSTANT_CONVERSION = YES; 430 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 431 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 432 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 433 | CLANG_WARN_EMPTY_BODY = YES; 434 | CLANG_WARN_ENUM_CONVERSION = YES; 435 | CLANG_WARN_INFINITE_RECURSION = YES; 436 | CLANG_WARN_INT_CONVERSION = YES; 437 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 438 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 439 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 440 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 441 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 442 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 443 | CLANG_WARN_STRICT_PROTOTYPES = YES; 444 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 445 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 446 | CLANG_WARN_UNREACHABLE_CODE = YES; 447 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 448 | COPY_PHASE_STRIP = NO; 449 | DEBUG_INFORMATION_FORMAT = dwarf; 450 | ENABLE_STRICT_OBJC_MSGSEND = YES; 451 | ENABLE_TESTABILITY = YES; 452 | GCC_C_LANGUAGE_STANDARD = gnu11; 453 | GCC_DYNAMIC_NO_PIC = NO; 454 | GCC_NO_COMMON_BLOCKS = YES; 455 | GCC_OPTIMIZATION_LEVEL = 0; 456 | GCC_PREPROCESSOR_DEFINITIONS = ( 457 | "DEBUG=1", 458 | "$(inherited)", 459 | ); 460 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 461 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 462 | GCC_WARN_UNDECLARED_SELECTOR = YES; 463 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 464 | GCC_WARN_UNUSED_FUNCTION = YES; 465 | GCC_WARN_UNUSED_VARIABLE = YES; 466 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 467 | MTL_FAST_MATH = YES; 468 | ONLY_ACTIVE_ARCH = YES; 469 | SDKROOT = macosx; 470 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 471 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 472 | SWIFT_VERSION = 5.0; 473 | }; 474 | name = Debug; 475 | }; 476 | 99F409D927216B230010500C /* Release */ = { 477 | isa = XCBuildConfiguration; 478 | baseConfigurationReference = 99F409EA2722FBA10010500C /* Config.xcconfig */; 479 | buildSettings = { 480 | ALWAYS_SEARCH_USER_PATHS = NO; 481 | CLANG_ANALYZER_NONNULL = YES; 482 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 483 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 484 | CLANG_CXX_LIBRARY = "libc++"; 485 | CLANG_ENABLE_MODULES = YES; 486 | CLANG_ENABLE_OBJC_ARC = YES; 487 | CLANG_ENABLE_OBJC_WEAK = YES; 488 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 489 | CLANG_WARN_BOOL_CONVERSION = YES; 490 | CLANG_WARN_COMMA = YES; 491 | CLANG_WARN_CONSTANT_CONVERSION = YES; 492 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 493 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 494 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 495 | CLANG_WARN_EMPTY_BODY = YES; 496 | CLANG_WARN_ENUM_CONVERSION = YES; 497 | CLANG_WARN_INFINITE_RECURSION = YES; 498 | CLANG_WARN_INT_CONVERSION = YES; 499 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 500 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 501 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 502 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 503 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 504 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 505 | CLANG_WARN_STRICT_PROTOTYPES = YES; 506 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 507 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 508 | CLANG_WARN_UNREACHABLE_CODE = YES; 509 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 510 | COPY_PHASE_STRIP = NO; 511 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 512 | ENABLE_NS_ASSERTIONS = NO; 513 | ENABLE_STRICT_OBJC_MSGSEND = YES; 514 | GCC_C_LANGUAGE_STANDARD = gnu11; 515 | GCC_NO_COMMON_BLOCKS = YES; 516 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 517 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 518 | GCC_WARN_UNDECLARED_SELECTOR = YES; 519 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 520 | GCC_WARN_UNUSED_FUNCTION = YES; 521 | GCC_WARN_UNUSED_VARIABLE = YES; 522 | MTL_ENABLE_DEBUG_INFO = NO; 523 | MTL_FAST_MATH = YES; 524 | SDKROOT = macosx; 525 | SWIFT_COMPILATION_MODE = wholemodule; 526 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 527 | SWIFT_VERSION = 5.0; 528 | }; 529 | name = Release; 530 | }; 531 | 99F409DB27216B230010500C /* Debug */ = { 532 | isa = XCBuildConfiguration; 533 | baseConfigurationReference = 99F409EB2722FC670010500C /* AppConfig.xcconfig */; 534 | buildSettings = { 535 | CODE_SIGN_IDENTITY = "-"; 536 | CODE_SIGN_STYLE = Automatic; 537 | DEVELOPMENT_TEAM = ""; 538 | INFOPLIST_KEY_CFBundleDisplayName = "$(DISPLAY_NAME)"; 539 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 540 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 541 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 542 | LD_RUNPATH_SEARCH_PATHS = ( 543 | "$(inherited)", 544 | "@executable_path/../Frameworks", 545 | ); 546 | }; 547 | name = Debug; 548 | }; 549 | 99F409DC27216B230010500C /* Release */ = { 550 | isa = XCBuildConfiguration; 551 | baseConfigurationReference = 99F409EB2722FC670010500C /* AppConfig.xcconfig */; 552 | buildSettings = { 553 | CODE_SIGN_IDENTITY = "-"; 554 | CODE_SIGN_STYLE = Automatic; 555 | DEVELOPMENT_TEAM = ""; 556 | INFOPLIST_KEY_CFBundleDisplayName = "$(DISPLAY_NAME)"; 557 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 558 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 559 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 560 | LD_RUNPATH_SEARCH_PATHS = ( 561 | "$(inherited)", 562 | "@executable_path/../Frameworks", 563 | ); 564 | }; 565 | name = Release; 566 | }; 567 | 99F409E627216BFF0010500C /* Debug */ = { 568 | isa = XCBuildConfiguration; 569 | baseConfigurationReference = 99F409EC2722FD0C0010500C /* HelperToolConfig.xcconfig */; 570 | buildSettings = { 571 | CODE_SIGN_IDENTITY = "-"; 572 | CODE_SIGN_STYLE = Automatic; 573 | DEVELOPMENT_TEAM = ""; 574 | }; 575 | name = Debug; 576 | }; 577 | 99F409E727216BFF0010500C /* Release */ = { 578 | isa = XCBuildConfiguration; 579 | baseConfigurationReference = 99F409EC2722FD0C0010500C /* HelperToolConfig.xcconfig */; 580 | buildSettings = { 581 | CODE_SIGN_IDENTITY = "-"; 582 | CODE_SIGN_STYLE = Automatic; 583 | DEVELOPMENT_TEAM = ""; 584 | }; 585 | name = Release; 586 | }; 587 | /* End XCBuildConfiguration section */ 588 | 589 | /* Begin XCConfigurationList section */ 590 | 99F409C627216B210010500C /* Build configuration list for PBXProject "SwiftAuthorizationSample" */ = { 591 | isa = XCConfigurationList; 592 | buildConfigurations = ( 593 | 99F409D827216B230010500C /* Debug */, 594 | 99F409D927216B230010500C /* Release */, 595 | ); 596 | defaultConfigurationIsVisible = 0; 597 | defaultConfigurationName = Release; 598 | }; 599 | 99F409DA27216B230010500C /* Build configuration list for PBXNativeTarget "SwiftAuthorizationApp" */ = { 600 | isa = XCConfigurationList; 601 | buildConfigurations = ( 602 | 99F409DB27216B230010500C /* Debug */, 603 | 99F409DC27216B230010500C /* Release */, 604 | ); 605 | defaultConfigurationIsVisible = 0; 606 | defaultConfigurationName = Release; 607 | }; 608 | 99F409E527216BFF0010500C /* Build configuration list for PBXNativeTarget "SwiftAuthorizationHelperTool" */ = { 609 | isa = XCConfigurationList; 610 | buildConfigurations = ( 611 | 99F409E627216BFF0010500C /* Debug */, 612 | 99F409E727216BFF0010500C /* Release */, 613 | ); 614 | defaultConfigurationIsVisible = 0; 615 | defaultConfigurationName = Release; 616 | }; 617 | /* End XCConfigurationList section */ 618 | 619 | /* Begin XCRemoteSwiftPackageReference section */ 620 | 99662093272557FF00ECE5C7 /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */ = { 621 | isa = XCRemoteSwiftPackageReference; 622 | repositoryURL = "https://github.com/trilemma-dev/EmbeddedPropertyList"; 623 | requirement = { 624 | kind = upToNextMajorVersion; 625 | minimumVersion = 2.0.2; 626 | }; 627 | }; 628 | 998D9C322865D623006224C4 /* XCRemoteSwiftPackageReference "Authorized" */ = { 629 | isa = XCRemoteSwiftPackageReference; 630 | repositoryURL = "https://github.com/trilemma-dev/Authorized"; 631 | requirement = { 632 | kind = upToNextMajorVersion; 633 | minimumVersion = 1.0.0; 634 | }; 635 | }; 636 | 99A67B312723B2D200F7D92D /* XCRemoteSwiftPackageReference "SecureXPC" */ = { 637 | isa = XCRemoteSwiftPackageReference; 638 | repositoryURL = "https://github.com/trilemma-dev/SecureXPC"; 639 | requirement = { 640 | kind = upToNextMinorVersion; 641 | minimumVersion = 0.8.0; 642 | }; 643 | }; 644 | 99A67B372723B2FC00F7D92D /* XCRemoteSwiftPackageReference "Blessed" */ = { 645 | isa = XCRemoteSwiftPackageReference; 646 | repositoryURL = "https://github.com/trilemma-dev/Blessed"; 647 | requirement = { 648 | kind = upToNextMinorVersion; 649 | minimumVersion = 0.6.0; 650 | }; 651 | }; 652 | /* End XCRemoteSwiftPackageReference section */ 653 | 654 | /* Begin XCSwiftPackageProductDependency section */ 655 | 99662094272557FF00ECE5C7 /* EmbeddedPropertyList */ = { 656 | isa = XCSwiftPackageProductDependency; 657 | package = 99662093272557FF00ECE5C7 /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */; 658 | productName = EmbeddedPropertyList; 659 | }; 660 | 996620962725580A00ECE5C7 /* EmbeddedPropertyList */ = { 661 | isa = XCSwiftPackageProductDependency; 662 | package = 99662093272557FF00ECE5C7 /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */; 663 | productName = EmbeddedPropertyList; 664 | }; 665 | 998D9C332865D623006224C4 /* Authorized */ = { 666 | isa = XCSwiftPackageProductDependency; 667 | package = 998D9C322865D623006224C4 /* XCRemoteSwiftPackageReference "Authorized" */; 668 | productName = Authorized; 669 | }; 670 | 99A67B322723B2D200F7D92D /* SecureXPC */ = { 671 | isa = XCSwiftPackageProductDependency; 672 | package = 99A67B312723B2D200F7D92D /* XCRemoteSwiftPackageReference "SecureXPC" */; 673 | productName = SecureXPC; 674 | }; 675 | 99A67B382723B2FC00F7D92D /* Blessed */ = { 676 | isa = XCSwiftPackageProductDependency; 677 | package = 99A67B372723B2FC00F7D92D /* XCRemoteSwiftPackageReference "Blessed" */; 678 | productName = Blessed; 679 | }; 680 | 99A67B3B2723B30800F7D92D /* Blessed */ = { 681 | isa = XCSwiftPackageProductDependency; 682 | package = 99A67B372723B2FC00F7D92D /* XCRemoteSwiftPackageReference "Blessed" */; 683 | productName = Blessed; 684 | }; 685 | 99A67B3D2723B30D00F7D92D /* SecureXPC */ = { 686 | isa = XCSwiftPackageProductDependency; 687 | package = 99A67B312723B2D200F7D92D /* XCRemoteSwiftPackageReference "SecureXPC" */; 688 | productName = SecureXPC; 689 | }; 690 | /* End XCSwiftPackageProductDependency section */ 691 | }; 692 | rootObject = 99F409C327216B210010500C /* Project object */; 693 | } 694 | -------------------------------------------------------------------------------- /SwiftAuthorizationSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftAuthorizationSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftAuthorizationSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Authorized", 6 | "repositoryURL": "https://github.com/trilemma-dev/Authorized", 7 | "state": { 8 | "branch": null, 9 | "revision": "e490b9d3f4a0e8b17a8b39b5a9750b8e0be7548a", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | { 14 | "package": "Blessed", 15 | "repositoryURL": "https://github.com/trilemma-dev/Blessed", 16 | "state": { 17 | "branch": null, 18 | "revision": "e7c730ea4bcd2df7b61f022dbd38c5cdc2c875de", 19 | "version": "0.6.0" 20 | } 21 | }, 22 | { 23 | "package": "EmbeddedPropertyList", 24 | "repositoryURL": "https://github.com/trilemma-dev/EmbeddedPropertyList", 25 | "state": { 26 | "branch": null, 27 | "revision": "21bd832e28a9a66ecdb7b4c21910bb0487a22fe5", 28 | "version": "2.0.2" 29 | } 30 | }, 31 | { 32 | "package": "Required", 33 | "repositoryURL": "https://github.com/trilemma-dev/Required.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "82a4fbd388346ca40b1bbe815014dc45a75d503c", 37 | "version": "0.1.1" 38 | } 39 | }, 40 | { 41 | "package": "SecureXPC", 42 | "repositoryURL": "https://github.com/trilemma-dev/SecureXPC", 43 | "state": { 44 | "branch": null, 45 | "revision": "d6e439e2b805de8be9b584fff97cf2f6a839a656", 46 | "version": "0.8.0" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /SwiftAuthorizationSample.xcodeproj/xcuserdata/joshkaplan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftAuthorizationApp.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | SwiftAuthorizationHelperTool.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | SwiftAuthorizationSample.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 0 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trilemma-dev/SwiftAuthorizationSample/85f45622f819ca5b5dcf8867801a6b5d3edf63b2/screenshot.png --------------------------------------------------------------------------------