Home > database >  Privileged file copy in macOS (Installing a helper binary to /usr/local/bin)
Privileged file copy in macOS (Installing a helper binary to /usr/local/bin)

Time:11-03

I have a helper binary mytool inside my main app bundle that I need to copy to /usr/local/bin.

Now bin might not always exist or have write access, so the standard NSWorkspace calls will fail on it. I looked into different ways to do this, but none are satisfactory (or I am doing it wrong)

  1. Getting an authorization for replaceFile for NSWorkspace.requestAuthorization

    This does not seem to work, as I still get a privileges error after trying to "replace" the file in /usr/local/bin/mytool with the one from my bundle.

  2. Manually getting Authorization via AuthorizationCreate.

    The problem here is that AuthorizationExecuteWithPrivileges is deprecated (or in my case not even available in Swift), and SMJobBless seems to be only for longer running helper processes. Also SMJobBlessrequires my helper tool to have an Info.plist of its own, which it doesn't have since its just a plain binary

So how do I manage to perform a privileged file copy in Swift?

PS: The app is not sandboxed, so NSOpenPanel does not help.

CodePudding user response:

Well I dug out the deprecated API using dlsym, because there is simply no other way besides asking the user manually for his password, which I don't want to do unless the deprecated API disappears entirely.

So what I do now is authenticate a call to mytool --install using AuthorizationExecuteWithPrivileges like this:

import Foundation
import Security

public struct Sudo {

    private typealias AuthorizationExecuteWithPrivilegesImpl = @convention(c) (
        AuthorizationRef,
        UnsafePointer<CChar>, // path
        AuthorizationFlags,
        UnsafePointer<UnsafeMutablePointer<CChar>?>, // args
        UnsafeMutablePointer<UnsafeMutablePointer<FILE>>?
    ) -> OSStatus

    /// This wraps the deprecated AuthorizationExecuteWithPrivileges
    /// and makes it accessible by Swift
    ///
    /// - Parameters:
    ///   - path: The executable path
    ///   - arguments: The executable arguments
    /// - Returns: `errAuthorizationSuccess` or an error code
    public static func run(path: String, arguments: [String]) -> Bool {
        var authRef: AuthorizationRef!
        var status = AuthorizationCreate(nil, nil, [], &authRef)

        guard status == errAuthorizationSuccess else { return false }
        defer { AuthorizationFree(authRef, [.destroyRights]) }

        var item = kAuthorizationRightExecute.withCString { name in
            AuthorizationItem(name: name, valueLength: 0, value: nil, flags: 0)
        }
        var rights = withUnsafeMutablePointer(to: &item) { ptr in
            AuthorizationRights(count: 1, items: ptr)
        }

        status = AuthorizationCopyRights(authRef, &rights, nil, [.interactionAllowed, .preAuthorize, .extendRights], nil)

        guard status == errAuthorizationSuccess else { return false }

        status = executeWithPrivileges(authorization: authRef, path: path, arguments: arguments)

        return status == errAuthorizationSuccess
    }

    private static func executeWithPrivileges(authorization: AuthorizationRef,
                                              path: String,
                                              arguments: [String]) -> OSStatus {
        let RTLD_DEFAULT = dlopen(nil, RTLD_NOW)
        guard let funcPtr = dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges") else { return -1 }
        let args = arguments.map { strdup($0) }
        defer { args.forEach { free($0) }}
        let impl = unsafeBitCast(funcPtr, to: AuthorizationExecuteWithPrivilegesImpl.self)
        return impl(authorization, path, [], args, nil)
    }
}

CodePudding user response:

If you want to do this using public APIs (meaning not using deprecated APIs, invoking Apple Script, shelling out via Process, etc.) then the only way to achieve this is using SMJobBless. For better or worse, that's the only option still officially supported by Apple.

If you want to install your binary in /usr/local/bin then that binary itself doesn't need to have an Info.plist. You'd want to create a different helper tool which would be installed via SMJobBless that could copy your binary to /usr/bin/local. It will be able to do that because a helper tool installed by SMJobBless always runs as root. Once you're done with all of this you could have the helper tool you installed with SMJobBless uninstall itself. No denying it's rather involved.

If you do want to go down this route, take a look at SwiftAuthorizationSample.

  • Related