diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7b8413b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +# Force GitHub to display tabs +# mixed with [4] spaces properly. +[kiss] +indent_style = tab +indent_size = 4 diff --git a/kiss b/kiss index c353568..28b0161 100755 --- a/kiss +++ b/kiss @@ -45,6 +45,106 @@ prompt() { read -r _ } +root_cache() { + # This function simply mimics a 'su' prompt to then store + # the root password for the lifetime of the package manager. + # + # This function is called once when needed to cache the + # password. The password is not accessible to any subprocesses + # and should never leave the package manager's process. + # + # This behavior is needed as there is no POSIX shell method + # of running a shell function as a different user. We can't + # selectively lower or raise permissions in a seamless way + # through "normal" means. + # + # Root is only needed when installing/removing packages whereas + # non-root permissions are needed in countless places throughout. + # + # This is the only *workable* solution to 1) not run the entire + # package manager as root and 2) avoid prompting for password + # before, during and after builds numerous times. + # + # NOTE: Careful consideration has been taken in regards to this + # change and I would have loved an inconspicuous solution + # to this problem... but it doesn't exist. + # + # This change was needed as the existing behavior was not ideal + # in any way and needed to be fixed. + printf 'Password: ' + + # Disable echoing to the terminal while the password is inputted + # by the user. The below commands read from '/dev/tty' to ensure + # they work when run from a subshell. + # + # The variable '$cached' is used to check if we've been here + # before. We cannot check whether or not '$pass' is empty as the + # '[' command may be external which would result in /proc leakage. + stty -F /dev/tty -echo + IFS= read -r pass < /dev/tty && cached=1 + stty -F /dev/tty echo + + printf '\n' + + # Validate the password now with a simple 'true' command as we + # don't yet need to elevate permissions. + dosu /bin/true +} + +dosu() { + [ "$cached" ] || root_cache + + # Declare this as a function to avoid repeating it twice + # below. Great naming of functions all around. + # + # Run a command as root using the cached password. The 'su' + # command allows you to input a password via stdin. To hide + # the prompt, the command's output is sent to '/dev/tty' + # and the output of 'su' is sent to '/dev/null'. + dosudo() { su "${drop_to:-root}" -c "$* >/dev/tty" >/dev/null; } + + # The code below uses the most secure method of sending + # data over stdin based on what is available in the system. + # + # The great debate: Use a heredoc or echo+pipe for password + # input over stdin? Time to explain. + # + # 1) 'printf | cmd' is the most secure IF 'printf' is built + # into the shell and NOT an external command. When 'printf' + # is external, the password WILL be leaked over '/proc'. + # + # Safe shells here are anything with a builtin 'printf', + # 'ash', 'dash', 'bash' and most other shells. + # + # 2) Using a heredoc is as secure as the above method (when + # builtin) IF and only IF the user's shell implements + # heredocs WITHOUT the use of temporary files (See bash!). + # + # When using heredocs and a temporary file the risk is a + # tiny window in which the input is available inside of + # a temporary file. + # + # 'ash' and 'dash' are safe here, 'bash' is not ('bash' + # falls under (1) however). + # + # Which is best? (order is best to worst) + # + # 1) builtin 'printf'. + # 2) heredocs with no temporary file. + # 3) heredocs with a temporary file. + # + # This code below follows the above ordering when deciding + # which method to use. The '$heredocs' variable is declared + # in 'main()' after a check to see if 'printf' is builtin. + if [ "$heredocs" ]; then + dosudo "$@" <<-EOF + $pass + EOF + else + printf '%s\n' "$pass" | dosudo "$@" + fi +} + pkg_lint() { # Check that each mandatory file in the package entry exists. log "$1" "Checking repository files" @@ -448,7 +548,15 @@ pkg_build() { log "Building: $*" # Only ask for confirmation if more than one package needs to be built. - [ $# -gt 1 ] || [ "$pkg_update" ] && prompt + [ $# -gt 1 ] || [ "$pkg_update" ] && { + prompt + + # Prompt for password prior to the build if more than one package + # will be built and installed. No use in forcing the user to wait + # for the first password prompt (before caching) if it may take a + # long long while. + [ "$cached" ] || root_cache + } log "Checking to see if any dependencies have already been built" log "Installing any pre-built dependencies" @@ -467,7 +575,9 @@ pkg_build() { # to 'su' to elevate permissions. [ -f "$bin_dir/$pkg#$version-$release.tar.gz" ] && { log "$pkg" "Found pre-built binary, installing" - (KISS_FORCE=1 args i "$bin_dir/$pkg#$version-$release.tar.gz") + + (KISS_FORCE=1 \ + pkg_install "$bin_dir/$pkg#$version-$release.tar.gz") # Remove the now installed package from the build # list. No better way than using 'sed' in POSIX 'sh'. @@ -554,7 +664,9 @@ pkg_build() { contains "$explicit" "$pkg" && [ -z "$pkg_update" ] && continue log "$pkg" "Needed as a dependency or has an update, installing" - (KISS_FORCE=1 args i "$pkg") + + (KISS_FORCE=1 \ + pkg_install "$bin_dir/$pkg#$version-$release.tar.gz") done # End here as this was a system update and all packages have been installed. @@ -692,9 +804,9 @@ pkg_remove() { [ "${file##/etc/*}" ] || continue if [ -d "$KISS_ROOT/$file" ]; then - rmdir "$KISS_ROOT/$file" 2>/dev/null || continue + dosu rmdir "'$KISS_ROOT/$file'" 2>/dev/null || continue else - rm -f "$KISS_ROOT/$file" + dosu rm -f "'$KISS_ROOT/$file'" fi done < "$sys_db/$1/manifest" @@ -770,9 +882,9 @@ pkg_install() { # This is repeated multiple times. Better to make it a function. pkg_rsync() { - rsync --chown=root:root --chmod=Du-s,Dg-s,Do-s \ + dosu rsync --chown=root:root --chmod=Du-s,Dg-s,Do-s \ -WhHKa --no-compress "$1" --exclude /etc \ - "$tar_dir/$pkg_name/" "$KISS_ROOT/" + "'$tar_dir/$pkg_name/'" "'$KISS_ROOT/'" } # Install the package by using 'rsync' and overwrite any existing files @@ -781,8 +893,8 @@ pkg_install() { # If '/etc/' exists in the package, install it but don't overwrite. [ -d "$tar_dir/$pkg_name/etc" ] && - rsync --chown=root:root -WhHKa --no-compress --ignore-existing \ - "$tar_dir/$pkg_name/etc" "$KISS_ROOT/" + dosu rsync --chown=root:root -WhHKa --no-compress --ignore-existing \ + "'$tar_dir/$pkg_name/etc'" "'$KISS_ROOT/'" # Remove any leftover files if this is an upgrade. [ "$old_manifest" ] && { @@ -799,18 +911,18 @@ pkg_install() { # Remove files. if [ -f "$file" ] && [ ! -L "$file" ]; then - rm -f "$file" + dosu rm -f "'$file'" # Remove file symlinks. elif [ -L "$file" ] && [ ! -d "$file" ]; then - unlink "$file" ||: + dosu unlink "'$file'" ||: # Skip directory symlinks. elif [ -L "$file" ] && [ -d "$file" ]; then : # Remove directories if empty. elif [ -d "$file" ]; then - rmdir "$file" 2>/dev/null ||: + dosu rmdir "'$file'" 2>/dev/null ||: fi done ||: } @@ -826,7 +938,7 @@ pkg_install() { if [ -x "$sys_db/$pkg_name/post-install" ]; then log "$pkg_name" "Running post-install script" - "$sys_db/$pkg_name/post-install" ||: + dosu "'$sys_db/$pkg_name/post-install'" ||: fi log "$pkg_name" "Installed successfully" @@ -879,18 +991,20 @@ pkg_updates() { if [ -w "$PWD" ]; then git fetch git merge + else log "$PWD" "Need root to update" - if command -v sudo >/dev/null; then - sudo git fetch - sudo git merge - elif command -v doas >/dev/null; then - doas git fetch - doas git merge - else - su -c 'git fetch && git merge' - fi + # Find out the owner of the repository and spawn + # git as this user below. + # + # This prevents 'git' from changing the original + # ownership of files and directories in the rare + # case that the repository is owned by a 3rd user. + (drop_to=$(stat -c %U "$PWD") + + dosu git fetch + dosu git merge) fi } done @@ -922,8 +1036,8 @@ pkg_updates() { prompt - pkg_build kiss - args i kiss + pkg_build kiss + pkg_install kiss log "Updated the package manager" log "Re-run 'kiss update' to update your system" @@ -956,6 +1070,8 @@ pkg_updates() { pkg_clean() { # Clean up on exit or error. This removes everything related # to the build. + stty -F /dev/tty echo 2>/dev/null + [ "$KISS_DEBUG" != 1 ] || return # Block 'Ctrl+C' while cache is being cleaned. @@ -991,27 +1107,9 @@ args() { # Parse some arguments earlier to remove the need to duplicate code. case $action in - c|checksum|s|search) + c|checksum|s|search|i|install|r|remove) [ "$1" ] || die "'kiss $action' requires an argument" ;; - - i|install|r|remove) - [ "$1" ] || die "'kiss $action' requires an argument" - - # Rerun the script with 'su' if the user isn't root. - # Cheeky but 'su' can't be used on shell functions themselves. - [ "$(id -u)" = 0 ] || { - if command -v sudo >/dev/null; then - sudo -E KISS_FORCE="$KISS_FORCE" kiss "$action" "$@" - elif command -v doas >/dev/null; then - KISS_FORCE="$KISS_FORCE" doas kiss "$action" "$@" - else - su -pc "KISS_FORCE=$KISS_FORCE kiss $action $*" - fi - - return - } - ;; esac # Actions can be abbreviated to their first letter. This saves @@ -1113,6 +1211,18 @@ args() { } main() { + # Ensure that debug mode is never enabled to prevent internal + # package manager information from leaking to stdout. + set +x + + # Prevent the package manager from running as root. The package + # manager will elevate permissions where needed. + [ "$(id -u)" != 0 ] || die "kiss must be run as a normal user" + + # Use the most secure method of sending data over stdin based on + # whether or not the 'printf' command is built into the shell. + [ "$(command -v printf)" = printf ] || heredocs=1 + # Set the location to the repository and package database. pkg_db=var/db/kiss/installed