0

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.

Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Use `NSOpenPanel` and point `directoryURL` to /usr/local/bin. And ask the user to select it. – El Tomato Oct 31 '21 at 11:06
  • Well, `Kaleidoscope` for instance does it without an `NSOpenPanel` (which is a horrible experience). So there must be a way to do this... or they are using deprecated API. –  Oct 31 '21 at 11:12
  • 1
    Ask the user for the password and then use `Process` to execute the commands using sudo. See [this answer](https://stackoverflow.com/a/68229559/9223839) for an example of handling sudo and password – Joakim Danielson Oct 31 '21 at 11:14
  • You can't use sudo for Mac App Store applications to my knowledge. – El Tomato Oct 31 '21 at 11:15
  • 1
    Big security OOF. I really don't want to ask for sudo passwords. @ElTomato doesn't have to support MAS though. Can't use external helpers in the MAS version anyway –  Oct 31 '21 at 11:15
  • 1
    I don't know what the Kaleidoscope guy is. BBEdit uses `NSOpenPanel` and has the user select a file path if I remember correctly. That's what I do as well. – El Tomato Oct 31 '21 at 11:17
  • Note that sudo password here is the users normal password, so I am not talking about some kind of root access. It's quite common for apps to ask for password to install or do something on a system level but maybe this is then used with some specific API. – Joakim Danielson Oct 31 '21 at 15:59
  • "Also SMJobBlessrequires my helper tool to have an Info.plist of its own, which it doesn't have since its just a plain binary" That's what I thought too, but it turns out that plain old executables on macOS (in the Mach O object format) can embed arbitrary files into "sections", including the `info.plist` and `bundle.plist`. Search for "other linker flags" on this page: https://www.woodys-findings.com/posts/cocoa-implement-privileged-helper – Alexander Oct 31 '21 at 23:53
  • This tutorial covers *exactly* this. Installing a CLI tool into `/usr/local/bin`: https://www.youtube.com/watch?v=hPEDjbb_BD0 – Alexander Oct 31 '21 at 23:56
  • @Alexander I know how to do it, I was just looking for a good **non-deprecated** way to do it. But apparently there is none. –  Nov 01 '21 at 09:19

2 Answers2

3

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)
    }
}
0

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.

Joshua Kaplan
  • 843
  • 5
  • 9
  • Thats exactly what I do not want to do since its even worse than using deprecated API. –  Nov 03 '21 at 10:13
  • Of course it’s your choice. From a complexity point of view it’s definitely way higher. The way in which it’s better is that it’s 100% supported. Apple does in many cases eventually fully remove deprecated APIs. – Joshua Kaplan Nov 03 '21 at 11:23