erikberglund / SwiftPrivilegedHelper

Example application using a privileged helper tool with authentication in an unsandboxed application written in Swift
MIT License
180 stars 33 forks source link

The sh file is not excuted #31

Open huizhenhe-dev opened 2 years ago

huizhenhe-dev commented 2 years ago

Hi, I build the project, then input path "/tmp/generatePb_swift.sh", then click "Run Command", there is log here "Command exit code: 0". But the sh file is not executed. I see "com.github.erikberglund.SwiftPrivilegedHelper" installed in /Library/PrivilegedHelperTools.

Screen Shot 2022-02-10 at 1 48 25 PM
jeff-h commented 2 years ago

Perhaps create a really simple bash script and try that first? eg

#!/bin/bash

echo Hello World
# or perhaps
touch ~/Desktop/helloworld

Just to rule out that angle.

huizhenhe-dev commented 2 years ago
#!/bin/bash

echo Hello World
# or perhaps
touch ~/Desktop/helloworld

I tried this just now, the simple bash script also cannot execute.

trinhtran-markany commented 1 year ago

Hi, I got very same issue above. Have you gotten any solution on this @huizhenhe-dev ?

Thanks,

chipjarred commented 1 year ago

The command to be executed has to be a Mach-O executable binary, not a shell script. To run a shell script you'd use the path of the shell (ie. /bin/zsh or /bin/bash) and the shell script path would be passed as an argument.

Also the path you enter in the dialog box is the path being passed to the ls command. /bin/ls is hard coded as the command to run. You'd need to edit the Swift code to use the path of the shell instead.

Remember the code provided in this project is intended to show how to call a privileged helper tool. It's expected that you'd modify the code for the command(s) you want to run.

For security reasons it would be better to implement the functionality you want to execute in Swift code rather than call out to a shell script. That code would go in the helper. Ideally you'd use Cocoa or BSD API rather than system commands, but if you do need system command line tools, call exactly the binaries you need and try to avoid shell scripts.

chipjarred commented 1 year ago

When I wrote my previous reply I was on my phone, and it wasn't convenient to look at the source code. Now that I can, I want to follow up.

Using the code as it exists in this repo, if you enter /path/to/your/shell/script.sh in the dialog box, the effect is the same as if you typed this on the command line:

/bin/ls /path/to/your/shell/script.sh

Which matches the output you show, but because of what I believe to be a misunderstanding about what the app is intended to do, it's not what you're expecting. You're expecting for your shell script to be executed.

To do that you have to replace the /bin/ls command that's hard coded in the app, with a command that does what you want. Executing a shell script is probably a bad idea for security reasons, as I mentioned in my previous reply, but I'll assume that's what you want to do anyway. You can't just use the path to your shell script as the executable... on the command line a script is "executable" only in the sense that a shell knows how to interpret it, but you don't have a shell. What you need is a Mach-O binary, which the shell itself is, so you'd replace /bin/ls with the path to your preferred shell. Given that script listed in a previous comment wants to run in bash, the command you want is /bin/bash, although really you could use any shell, since when any of the general purpose shells sees the #!/bin/bash line, it will launch a bash sub-shell to execute the script.

In Helper.swift, you'll find a method, runCommandLs(withPath:completion:). That's where the ls command is being called, and where you'd put the path to the executable you want to run (for example: /bin/bash). It calls the runTask(command:arguments:completion:) method to actually run ls. arguments is an array of String. Currently, it passes the path to run /bin/ls on as the only element of arguments.

To run a shell script, after setting command to /bin/bash (or whatever shell), arguments would be ["/path/to/your/shell/script.sh"], of course you can pass additional arguments to bash as separate elements inarguments in the same order you'd use on the command line.

You'll also need to update the static authorizationRights property in HelperAuthorization.swift to refer to the shell, though again, I want to emphasize that calling out to shell has security implications, and should be avoided (unless it's just for your own private use on your own Mac, in which case, do what you want). If you must call out to a shell, at least hard code the shell commands rather than allowing the user to enter them. If you really must use a script, then at least make the script a resource in your app's bundle.

It's not required for the helper tool to call out to an external program. In fact, it's more secure, and more performant, to implement whatever functionality you want in the helper tool directly in Swift code. Being in the helper tool let's you make Cocoa/BSD calls the main app wouldn't have permissions for. Basically you'd replace the contents of runCommandLs(withPath:completion:) with Swift code that does the actual work you want to do - and of course, rename the method to reflect what it actually does.

trinhtranduc commented 1 year ago

Thank you for your excellence answer. I'm aware of using sh file can be a security issue. However i need to execute several chflags command and still not find any api to support it.

#!/bin/bash

sudo chflags -R schg MyFolder

sudo chflags -R hidden MyFolder

Appreciate your help!

trinhtranduc commented 1 year ago

@chipjarred one more question: How can I show the password popup only once, even if I quit the app and run it again? I'm asking because I'm building a background application and I need to ask for the root password only during the installation process.

chipjarred commented 1 year ago

I'm pretty sure you can set file flags with Foundation's FileManager; however, the API that first came to mind to me is the BSD function. In C it's

#include <sys/stat.h>
#include <unistd.h>

int chflags(const char *path, u_int flags);

Which would translate to Swift as:

import Darwin

func chflags(_ path: UnsafePointer<Int8>, _ flags: UInt32) -> Int32

In Terminal, you can get the man page which describes the constants that can be used for flags, but you have to use the section number, which is 2:

man 2 chflags

I'm guessing when you were looking for the API, you probably left out the section number, which would give you the man page for the command line tool instead.

Anyway, I'd wrap the call to chflags in a more convenient Swift function that takes a String or URL, and maybe define your own OptionSet for flags.

As for your other question, I'm not sure if it will work to save an authorization from one execution to another, so I'd have to experiment with it. In the second version of runCommandLs you can see where it calls verifyAuthorization which takes an authData as one of its parameters. Since authData is an NSData you could save it somewhere (probably Key Chain is the best place for it). Then at the beginning of the next execution you could read it back and re-use it. This project isn't set up to do that, so you'll need to make some changes to do it, and I'm not at all sure if it would work, but it's worth a try.

chipjarred commented 1 year ago

You may find Apple's old docs on Authorization Services helpful: Authorization Services Programming Guide

trinhtranduc commented 1 year ago

Thanks @chipjarred for your quick response. I will give a try with Keychain first, appreciated it!

trinhtranduc commented 1 year ago

Hi @chipjarred , sorry for bothering you so much. I have one more question: how can we cache the helper installation request? I mean every time i restart my Macbook, i need to call try self.helperToolController.install() and popup keeps showing. I just want to show only once even if i restart my mac? Is there any solution?

chipjarred commented 1 year ago

It shouldn't need to re-install the helper for each restart - only when you need to update the helper tool . If I recall correctly, SMJobBless installs the helper tool in /Library/LaunchDaemons (that is the system-wide Library folder in the root directory), so it should be there. You can look there to see if it's installed, and to check its version against the one in your app's bundle. In this repo, that's done in AppDelegate.swift in the helperStatus(completion:) method, which is used to set the value of the status in the dialog box; however, you could use the same or similar code to decide whether or not to install the helper, and thus avoid the authorization dialog when the tool is already installed and up-to-date.

trinhtranduc commented 1 year ago

Thanks @chipjarred .

I tried to check status of helper but its always show me an error:

Helper connection was closed with error: Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service named com.mycompany.helper was invalidated: failed at lookup with error 3 - No such process." UserInfo={NSDebugDescription=The connection to service named com.mycompany.helper was invalidated: failed at lookup with error 3 - No such process.}

The error comes from calling remoteObjectProxyWithErrorHandler

func helper(_ completion: ((Bool) -> Void)?) -> HelperProtocol? {

        // Get the current helper connection and return the remote object (Helper.swift) as a proxy object to call functions on.

        guard let helper = self.helperConnection()?.remoteObjectProxyWithErrorHandler({ error in
            self.textViewOutput.appendText("Helper connection was closed with error: \(error)")
            if let onCompletion = completion { onCompletion(false) }
        }) as? HelperProtocol else { return nil }
        return helper
    }

I also check in /Library/LaunchDaemons and make sure the plist file stays here. Do you have a guess as to the cause of the problem?

trinhtranduc commented 1 year ago

Update: i check com.mycompany.helper.plist's status and find out that is unload. But why they're unload when i restart my Macbook?

chipjarred commented 1 year ago

If you get that error, it should mean the helper isn't installed. If it is installing it, I don't know why it would be unloading it.

You probably already figured this out, but the actual helper tool binary is installed in /Library/PrivilegedHelperTools. The plist in /Library/LaunchDaemons is config for launchd. I don't think that's related to your issue, but I did neglect to say that before.

Since you reference helperToolController, I'm thinking that you're using my fork. Is that right? It's something I've added in an effort to decouple code that works directly with the helper tool from the AppDelegate in hopes of making it easier to use in other contexts. Assuming you are using my fork as your starting point, your local version of HelperToolController might be out of date, because I have some additional code in the helper(_:) method, mainly to support uninstalling, but that additional code would be useful for you too. Basically the most recent version of that method looks like this:

    // -------------------------------------
    func helper(_ completion: ((Bool) -> Void)?) -> HelperProtocol?
    {
        /*
         Get the current helper connection and return the remote object
         (Helper.swift) as a proxy object to call functions on.
         */
        func localErrorHandler(_ error: Error)
        {
            self.log(
                stdErr: "Helper connection was closed with error: \(error)"
            )
            if let onCompletion = completion {
                onCompletion(false)
            }
        }

        return self.helperConnection()?.remoteObjectProxyWithErrorHandler(
            localErrorHandler
        ) as? HelperProtocol
    }

Note that it calls the completion handler with false if it gets an error from remoteObjectProxyWithErrorHandler. When you call helperStatus(completion:), it passes the completion handler to helper(_:) which means you should be able to do something like this:

    helperStatus { installed in 
        if !installed {
            self.install()
        }
    }

Does this help?

trinhtranduc commented 1 year ago

Thanks @chipjarred , you're right, I'm using your folk and having exactly your above code.

I'm always getting completion is false when restart my macbook.

Here's steps to reproduce:

  1. Build the project, then install the helper and check the daemon is loaded in /Library/LaunchDaemons
  2. Stop building the project, then build it again. Check if the daemon's status is still loaded.
  3. Restart your MacBook and build the project again. Check if the daemon's status is unloaded.

I'm not quite sure why their status is unloaded. My goal is to install the helper just once.

chipjarred commented 1 year ago

Using my fork, it works. These are the steps I took:

  1. Launch the main app
  2. Click the button to install the helper tool
  3. Quit the app
  4. Restart my Mac
  5. Launch the app
  6. It shows the helper tool is installed by showing the "Uninstall Helper" in the button instead of "Install Helper".

Just to verify, after restart, in Xcode I put a breakpoint in the helper(_:) method, and also in the localErrorHandler, as well as in helperStatus(completion:), so I can step through... it never hits the breakpoint in localErrorHandler so it gets no error from remoteObjectProxyWithErrorHandler, and the completion is called with true.

To be fair, for the time being I'm stuck on Big Sur, and there is some possibility that some Launch Services behavior may have changed since, but I think it's unlikely.

At what point in the app's life cycle are you calling helperStatus? In my case it's being called from updateHelperStatus() in AppDelegate which is being called from applicationDidFinishLaunching(_:). Is that the case for you, or have you modified it to call it from somewhere else?

trinhtranduc commented 1 year ago

To make sure, i've just downloaded your repo again, then try above steps and hmm it's not working. I'm curious that almost daemons on /Library/LaunchDaemons folder is unload, not sure its about macOS version or not. Just note that my macOS version is Ventura 13.2.1.

Screenshot 2023-03-09 at 14 32 22 Screenshot 2023-03-09 at 14 31 43
trinhtranduc commented 1 year ago

@chipjarred I checked the application in macOS Big Sur 11.5.2, it's working now! so i think problem is probably macOS version.

chipjarred commented 1 year ago

That's good to know. I wonder if the change was in Ventura or in Monterey. I would hope that Apple provides a way to make it work, but it will require some investigation to find out how. If I come across something I will let you know. If you find a solution, please share it here.

chipjarred commented 1 year ago

It looks like Ventura added a new Service Management API, SMAppService, and deprecated SMJobBless. From a quick look at the docs, it seems it will require some modification of the helper tool. Unfortunately, I can't do anything with it for now, but hopefully it helps you.

trinhtranduc commented 1 year ago

Thanks @chipjarred. Appreciated it!