milanvarady / Applite

User-friendly GUI macOS application for Homebrew Casks
https://aerolite.dev/applite
MIT License
4.05k stars 100 forks source link

Handle installs that require password #5

Closed tmikaeld closed 1 year ago

tmikaeld commented 1 year ago

For example, when installing microsoft-edge cask

milanvarady commented 1 year ago

This is an issue that I couldn't solve yet. We can't just call the brew commands with sudo because it will give this error for a good reason:

Error: Running Homebrew as root is extremely dangerous and no longer supported.
As Homebrew does not drop privileges on installation you would be giving all
build scripts full access to your system.

So we would have to inject the password safely into the running shell somehow.

stonerl commented 1 year ago

This works flawlessly for me. Un/-installing Edge asks for authorization, and I can either use Touch ID or my password.

CleanShot 2023-08-11 at 13 29 53@2x
milanvarady commented 1 year ago

What do you mean by "this"? Calling brew with sudo?

stonerl commented 1 year ago

No, when I press install in Applite, it automatically ask me to enter my credentials when I install an app, e.g. Edge that requires sudo.

tmikaeld commented 1 year ago

I think this one works because you bound your sudo to fido/keychain/passkeys. normally it asks for password which it can't get.

stonerl commented 1 year ago

You mean like this:

CleanShot 2023-08-11 at 14 24 15

stonerl commented 1 year ago

The problem here is that although the app is installed and working, Applite can't open it.

CleanShot 2023-08-11 at 14 25 15@2x

milanvarady commented 1 year ago

Hmm... I don't really understand this then. Can you explain this further: "bound your sudo to fido/keychain/passkeys"? I don't know that much about this.

stonerl commented 1 year ago

@tmikaeld are you referring to this:

/etc/pam.d/sudo

# sudo: auth account password session
auth       sufficient     pam_tid.so
auth       sufficient     pam_smartcard.so
auth       required       pam_opendirectory.so
account    required       pam_permit.so
password   required       pam_deny.so
session    required       pam_permit.so

I added the pam_tid.so to allow sudo with Touch ID. But if I would not have added this, the it should have symply asked me for the password.

stonerl commented 1 year ago

Just checked it. Indeed, it is this line I added auth sufficient pam_tid.so

milanvarady commented 1 year ago

I just checked it too, I changed it and it works.

stonerl commented 1 year ago

@milanvarady I guess you need to write a Privileged Helper Extension.

https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/AccessControl.html#//apple_ref/doc/uid/TP40002589-SW2

toobuntu commented 1 year ago

So we would have to inject the password safely into the running shell somehow.

PINEntry is a passphrase entry dialog from GPGTools which utilizes the Assuan protocol. So if you add a dependency on pinentry-mac in homebrew/core (depends_on formula: "pinentry-mac"), then you can set SUDO_ASKPASS[1] to have brew use PINEntry to query the user for a password. You just need to call it in the correct way to interact with the Assuan protocol. For example,

SUDO_ASKPASS=Resources/call-pinentry brew upgrade

where Resources/call-pinentry is the path to:

#! /bin/ksh -p

typeset PATH="/opt/homebrew/bin:/usr/local/bin"

# Minimal permissions are that:
# 1. The calling user is the owner or in the owning group of the file and
# 2. Can read and execute the file.
# sudo chown root:staff <file>; sudo chmod 550 <file>

printf "%s\n" "SETOK OK" "SETCANCEL Cancel" "SETDESC Planned Update" "SETPROMPT Enter Password:" "SETTITLE Software Upgrade via Applite" "GETPIN" | "$(brew --prefix)/bin/pinentry-mac" --no-global-grab | /usr/bin/awk '/^D / {print substr($0, index($0, $2))}'

# NOTE: The printf format string is applied to each argument.

Your call-pinentry script will return the entered text (password). This is just an example. For instance, SETOK, SETCANCEL and SETDESC could all be omitted.

Hope this helps. It might be a challenge to provide localized text in the dialog box that pinentry-mac displays[2], but it should be secure.

[1] https://github.com/Homebrew/brew/blob/3c8b4949baefb1f8166749ff1a3f0665afadedda/docs/Manpage.md?plain=1#L2369-L2370 [2] https://github.com/GPGTools/pinentry/blob/master/doc/HACKING#string-translation Screenshot 2023-08-24

toobuntu commented 1 year ago

I added the pam_tid.so to allow sudo with Touch ID.

Note that /private/etc/pam.d/sudo will be overwritten when macOS receives an update, and it will be necessary to re-edit /private/etc/pam.d/sudo after each such macOS update. Something like this might work.

cat <<\EOF > com.toobuntu.launchd.update_pam.d_sudo.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.toobuntu.launchd.update_pam.d_sudo</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Library/com.toobuntu.launchd/update_pam.d_sudo.ksh</string>
    </array>
    <key>StandardOutPath</key>
    <string>/Library/com.toobuntu.launchd/update_pam.d_sudo_out.log</string>
    <key>StandardErrorPath</key>
    <string>/Library/com.toobuntu.launchd/update_pam.d_sudo_err.log</string>
    <key>WatchPaths</key>
    <array>
        <string>/private/etc/pam.d/sudo</string>
    </array>
</dict>
</plist>
EOF

cat <<\EOF > update_pam.d_sudo.ksh
#! /bin/ksh -p

if /usr/bin/grep --quiet --fixed-strings pam_smartcard.so /private/etc/pam.d/sudo &&
  ! /usr/bin/grep --quiet --fixed-strings pam_tid.so /private/etc/pam.d/sudo; then
  {
    /usr/bin/sed -i '.orig' -e '/pam_smartcard.so/ i\
auth       sufficient     pam_tid.so # Touch ID for sudo
' /private/etc/pam.d/sudo
  }
else
  printf 1>&2 "%s\n" "Warning: The PAM Smartcard module was not detected or the PAM Touch ID module" "was already configured."
fi
EOF

sudo install -m 0644 -o root com.toobuntu.launchd.update_pam.d_sudo.plist /Library/LaunchDaemons/
sudo install -d /Library/com.toobuntu.launchd/
sudo install -m 0755 -o root -g wheel update_pam.d_sudo.ksh /Library/com.toobuntu.launchd/
sudo launchctl bootstrap 'system' /Library/LaunchDaemons/com.toobuntu.launchd.update_pam.d_sudo.plist
open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'
# Add /bin/ksh to the list of applications granted Full Disk Access for all users on this Mac.

And if you are on the CLI and use tmux, then you also need pam_reattach.so[1].

[1] https://github.com/fabianishere/pam_reattach

milanvarady commented 1 year ago

@toobuntu thanks for your detailed answers, now I understand things better. I have a question. Does using PINEntry have any advantage, or would a simple script like this work too?

#!/bin/sh
osascript <<EOT
    tell application "System Events"
        activate
        set mypassword to text returned of (display dialog "Please enter your password:" default answer "" with hidden answer)
    end tell
    mypassword
EOT

Alternatively, I've been looking for a way to prompt the user with the default password prompt that also allows Touch ID, but couldn't find a solution. Do you know of a method that would allow this?

toobuntu commented 1 year ago
  1. Using SUDO_ASKPASS

    Does using PINEntry have any advantage, or would a simple script like this work too?

Yes, a simple AppleScript dialog will work. But, even Apple admits it is not an ideal solution for obtaining passwords[1]:

Protect potentially sensitive information from prying eyes by using the display dialog command’s default answer parameter in conjunction with the hidden answer parameter to show bullets instead of plain text in the dialog’s text field. * Always be cautious when requesting sensitive data, such as passwords. Hidden text is returned by the display dialog command as plain, unencrypted text, so this command offers limited security.

By the way, the AppleScript can be simplified to[2]:

osascript -e 'text returned of (display dialog "Enter password:" default answer "" with hidden answer)' 2> /dev/null

Using pam_tid.so is a nicety when invoking sudo in the terminal and I know which commands sudo wants to run. But it does have limitations. The prompt indicates only that sudo is trying to execute a command as administrator, and nothing about the command which sudo is trying to run[3]. There are also people who refuse to enable Touch ID, though pam_tid.so at least falls back to asking the user to input a password in this case.

Another consideration is NIST recommendations to allow unmasking (bottom of page 14). AppleScript does not. PINEntry does.

Enter pinentry[4]:

pinentry is a small collection of dialog programs that allow GnuPG to read passphrases and PIN numbers in a secure manner.

In browsing the source[5], it seems to try to minimize the chance that the passphrase is saved in memory and that memory is exposed to an attacker by initializing the secure memory subsystem and dropping privileges. Debian has manpages for the various pinentries, which sum it up (I have replaced the toolkit with an asterisk because the remainder of the paragraph is identical regardless of whether it is pinentry-tty or pinentry-gnome3, etc.):

   pinentry-*  is  a  program  that  allows for secure entry of PINs or pass phrases.  That
   means it tries to take care that the  entered  information  is  not  swapped  to  disk  or
   temporarily  stored anywhere.  This functionality is particularly useful for entering pass
   phrases when using encryption software such as GnuPG or e-mail clients using the same.  It
   uses an open protocol and is therefore not tied to particular software.

Similar to pinentry from GnuPG, ssh-askpass is an X11-based passphrase dialog for use with OpenSSH. But it is not relevant in macOS since Apple deprecated the askpass UI and provided an option to integrate ssh passphrases into the Keychain. However, the brew cask for an unofficial (that is, not provided by OpenSSH and available only in a private tap instead of in homebrew/cask) macOS variant of ssh-askpass is just an AppleScript dialog. There is also ssh-askpass-mac written in Swift, which stores the passphrase in the macOS Keychain. And so it is really better suited for ssh which has many passphrases instead of sudo which requires the login passphrase. It also has this security caveat:

The passhprase is temporarily stored in the memory area of the ssh-askpass-mac app and with the Swift programming language it is not possible to ensure that the memory area is overwritten.

  1. Using Touch ID for SUDO_ASKPASS

Alternatively, I've been looking for a way to prompt the user with the default password prompt that also allows Touch ID, but couldn't find a solution. Do you know of a method that would allow this?

I do not. The problem here is that brew is a command line program. It passes SUDO_ASKPASS when sudo is needed by a cask's install script. How would one use Touch ID with GUI password entry fallback, like pam_tid.so or DeviceOwnerAuthentication, and instead of responding "authenticated" actually return a password to sudo on a command line, and as invoked by brew? I don't know whether that's possible in Swift (or any other way of accessing Touch ID).

I would be remiss if I failed to mention sudo-touchid for the concept. It hasn't been updated in seven years. It is a fork of sudo which has Touch ID built in. You could theoretically do the same, with a current sudo, and tell brew to use that instead of the system sudo. But brew hardcodes the path to sudo so that wouldn't work anyway. It seems heavy-handed to forcibly edit /private/etc/pam.d/sudo to add Touch ID, and maintain that configuration, especially if the user doesn't even want Touch ID enabled or wasn't asked.

It occurred to me that, on every brew upgrade, you could grep /private/etc/pam.d/sudo to see whether it contains pam_tid.so in an uncommented line. If so, don't set SUDO_ASKPASS. If not, set SUDO_ASKPASS. That way, your power users who already configured sudo with Touch ID can keep it and the others will get a GUI dialog.

[1] https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/PromptforText.html#//apple_ref/doc/uid/TP40016239-CH80-SW1 [2] https://scriptingosx.com/2020/09/avoiding-applescript-security-and-privacy-requests/, Section: Don’t send to other Processes [3] https://github.com/Homebrew/homebrew-autoupdate/issues/40#issuecomment-949786673 [4] https://gnupg.org/related_software/pinentry/index.html [5] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=pinentry.git;a=tree;h=refs/heads/master;hb=refs/heads/master

toobuntu commented 1 year ago

It came to my attention that Homebrew has multiple packages that provide pinentry-mac: The most popular seems to be the formula pinentry-mac. There are also the GPGTools casks, specifically gpg-suite-pinentry. The paths to pinentry-mac are different for each. The formula installs to "$(brew --prefix)/bin/pinentry-mac", while the cask installs to /usr/local/MacGPG2/libexec/pinentry-mac.app/Contents/MacOS/pinentry-mac (because GPGTools / MacGPG2 always installs into /usr/local). The actual executable, at this time, is basically the same no matter if installed via the formula or cask; they are both currently at version 1.1.1.1. The formula installs a binary for the CPU architecture (Intel or ARM) and the cask installs a universal binary (for both), but otherwise they are the same.

milanvarady commented 1 year ago

Thanks to the help of @toobuntu, I finally implemented this feature. The solution was to use the pinentry-mac package and pass it to brew with the SUDO_ASKPASS parameter.

The app installs pinentry during the setup, and also checks every time the app is opened or a .pkg app is downloaded, and fixes the installation if needed. Pinentry is called from a script called pinentry.ksh, and to add an extra layer of security, the file is checked against an md5 hash to make sure it hasn't been modified.

See changes in this commit: ba987547c7ea7637ca60f1942614404fc0f32dcc. This feature will be released in the next update soon.

milanvarady commented 1 year ago

The feature is now live in the v1.2 update.