#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

set -e

while :
do
      case $1 in
      setup)
        setup_sandbox="1"
        shift
        break
        ;;
      start)
        start_program="1"
        shift
        break
        ;;
      remove)
        remove="1"
        shift
        break
        ;;
      list)
        echo -e "Currently configured sandboxes:\n"
        getent passwd | grep "sal" | sed -e 's/:.*//g' | str_replace "sal-" ""
        exit
        ;;
      help)
        echo "Usage: sandbox-app-launcher [OPTION] [APPLICATION]

  setup       Setup the sandbox for the application.
  start       Execute the application within the sandbox.
  remove      Remove all application data and configuration files.
  list        List all currently configured sandboxes.

Examples:
  sandbox-app-launcher setup firefox
  sandbox-app-launcher start firefox
  sandbox-app-launcher remove firefox
  sandbox-app-launcher list"
        exit
        ;;
      *)
        echo "ERROR: Invalid argument. See the 'help' command for details."
        exit 1
        ;;
      esac
done

app_name="${1}"
shift 1
: ${app_path:="$(type -P "${app_name}" || true)"}

main_app_dir="/usr/share/sandbox-app-launcher"
auto_dir="/var/cache/sandbox-app-launcher-autogenerated"
appdata_dir="/home/sandbox-app-launcher-appdata"
app_user="sal-${app_name}"
app_homedir="${appdata_dir}/${app_name}"
seccomp_filter_non_wx="${auto_dir}/seccomp-filter.bpf"
seccomp_filter_wx="${auto_dir}/seccomp-filter-wx.bpf"
shared_dir="${appdata_dir}/shared"
wrapper_script_non_wx="${main_app_dir}/wrapper-script"
wrapper_script_wx="${main_app_dir}/wrapper-script-wx"

## proper whitespace handling
## https://www.whonix.org/wiki/Dev/bash
unset bwrap_args
declare -a bwrap_args

if ! [[ "${app_name}" =~ [0-9a-zA-Z/] ]]; then
   echo "ERROR: Invalid character in app_name." >&2
   exit 1
fi

if [ "${app_name}" = "torbrowser" ]; then
  app_path="${app_homedir}/.tb/tor-browser/Browser/start-tor-browser"
fi

if ! [ -x "${app_path}" ] && ! [ "${app_name}" = "torbrowser" ]; then
  echo "ERROR: Could not find '${app_name}' in \$PATH. Is it installed?" >&2
  exit 1
fi

if ! [[ "${app_path}" =~ [0-9a-zA-Z/] ]]; then
   echo "ERROR: Invalid character in app_path." >&2
   exit 1
fi

if [ "${app_name}" = "shared" ]; then
  echo "ERROR: The app name cannot be 'shared'." >&2
  exit 1
fi

if [ -f "/etc/sandbox-app-launcher/${app_name}.conf" ]; then
  . "/etc/sandbox-app-launcher/${app_name}.conf"
fi

## Optionally allow dynamic native code execution.
##
## This allows creating memory mappings that are both
## writable and executable, allows transitioning a
## writable memory mapping to executable and allows
## executing programs from writable directories i.e.
## violating W^X.
##
## This is generally a security issue since it allows an
## attacker to execute new arbitrary code so preventing this
## will force the attacker to use the already existing code
## (ROP/JOP) which is far more limited.
##
## Although, some things require this such as JIT engines in
## browsers so it must be optional.
if [ "${allow_dynamic_native_code_exec}" = "yes" ]; then
  seccomp_filter="${seccomp_filter_wx}"
  wrapper_script="${wrapper_script_wx}"
else
  seccomp_filter="${seccomp_filter_non_wx}"
  wrapper_script="${wrapper_script_non_wx}"
fi

if [ "$(id -u)" = "0" ]; then
  sal_is_run_with_root=true
fi

error_handler() {
  echo "
## sandbox-app-launcher BUG.
## BASH_COMMAND: ${BASH_COMMAND}
## Please report this BUG!
" >&2
  exit 1
}

trap "error_handler" ERR

kill_processes_inside_sandbox() {
  pids="$(pgrep --uid="${app_user}" 2>/dev/null)" || true
  if [ "$pids" = "" ]; then
     return 0
  fi

  ## Don't leave any left-over processes such as the D-Bus daemon.
  killall -9 -u "${app_user}"

  ## Cannot use bash built-in `wait` since that would fail if command is run
  ## from a different shell.
  ## /usr/bin/sandbox-app-launcher: line 129: wait: pid 12973 is not a child of this shell
  #wait $pids

  ## This is not the cleanest way. Would be better if there was a tool
  ## that can block until a list of pids has terminated to avoid a hardcoded
  ## `sleep 0.2`.
  while true ; do
    ## `ps` will exit 0 if at least one pid is still running.
    ## `ps` will exit 1 if no pid not even one is running.
    ## Examples:
    ##
    ## `ps --no-header -p 1` -> exit code: 0
    ##
    ## pid 1 really exists.
    ## pid 555555 does not exist.
    ## `ps --no-header -p 1 555555` -> exit code: 0
    ## ps still exit 0.
    if ! ps --no-header -p $pids &>/dev/null ; then
      break
    fi
    sleep 0.2 &
    ## Non-blocking. Process event loop such as signals.
    wait "$!"
  done
}

run_if_root() {
  if [ "${sal_is_run_with_root}" = "true" ]; then
    "${@}"
  else
    echo "ERROR: The setup for this program is incomplete. To fix, please execute:

sudo sandbox-app-launcher setup ${app_name}

(Debugging information: ${@})" >&2
    exit 1
  fi
}

setup() {
  if ! [ "${sal_is_run_with_root}" = "true" ]; then
    echo "ERROR: The setup must be run as root (sudo)." >&2
    exit 1
  fi
  setup_or_check
  echo "INFO: Done, setup complete, OK."
}

setup_or_check() {
  ## Some folders or files might be non-existing in case SAL is used outside of
  ## or ported to other Linux distributions not using Debian package maintainer
  ## script: debian/sandbox-app-launcher.postinst
  ## Might also happen in other corner cases such as incomplete installation
  ## (due to power loss, freeze during initial installation / upgrade or user
  ## error deleting files). Better to check and potentially error out early
  ## than getting difficult to understand error messages later.

  for dir in "${main_app_dir}" "${auto_dir}" "${appdata_dir}"; do
    ## Check if the required directories exist.
    if ! [ -d "${dir}" ]; then
      echo "ERROR: Directory '${dir}' does not exist. This package was not installed properly." >&2
      exit 1
    fi
    if ! [ "$(stat -c %a "${dir}")" = "755" ]; then
      ## Fix permissions.
      run_if_root chmod 755 "${dir}"
    fi
  done

  ## Check if the shared directory exists.
  if ! [ -d "${shared_dir}" ]; then
    echo "ERROR: Directory '${shared_dir}' does not exist. This package was not installed properly." >&2
    exit 1
  fi
  if ! [ "$(stat -c %a "${shared_dir}")" = "1777" ]; then
    ## Fix permissions.
    run_if_root chmod 1777 "${shared_dir}"
  fi

  ## Check if the second wrapper script exists.
  if ! [ -f "${wrapper_script}" ]; then
    echo "ERROR: File '${wrapper_script}' does not exist. This package was not installed properly." >&2
    exit 1
  fi

  ## Create the user that the sandboxed application will run as.
  if ! getent passwd | sed -e 's/:.*//g' | grep -qw "${app_user}"; then
    ## Use 'adduser' on Debian or Ubuntu systems but 'useradd' on other systems
    ## for portability.
    ## https://github.com/madaidan/sandbox-app-launcher/issues/9
    if grep --quiet --invert-match --extended-regexp 'NAME="Debian|NAME="Ubuntu' /etc/os-release; then
      run_if_root adduser --home "${app_homedir}" --no-create-home --disabled-login --comment "" "${app_user}" >/dev/null
    else
      run_if_root useradd --home-dir "${app_homedir}" --no-create-home "${app_user}" >/dev/null
    fi
  fi

  if ! [ -d "${app_homedir}" ]; then
    run_if_root mkdir --parents --mode=700 "${app_homedir}"
  fi

  if ! [ "$(stat -c "%U" "${app_homedir}")" = "${app_user}" ]; then
    run_if_root chown --recursive "${app_user}" "${app_homedir}"
  fi

  if ! [ "$(stat -c %a "${app_homedir}")" = "700" ]; then
    ## command:
    ## find /home/sandbox-app-launcher-appdata/torbrowser/.tb/tor-browser -executable -type f
    ## output:
    ## /home/sandbox-app-launcher-appdata/torbrowser/.tb/tor-browser/start-tor-browser.desktop
    ## /home/sandbox-app-launcher-appdata/torbrowser/.tb/tor-browser/Browser/libmozavcodec.so
    ## ...
    #run_if_root chmod --recursive 700 "${app_homedir}"
    ## Therefore do not use --recursive.
    run_if_root chmod 700 "${app_homedir}"
  fi

  ## https://forums.whonix.org/t/system-wide-sandboxing-framework-sandbox-app-launcher/9008/311
  if ! test -f /etc/machine-id ; then
    run_if_root touch /etc/machine-id
  fi
}

run_program() {
  setup_or_check

  ## TODO: X11 sandbox - not needed if we switch to wayland
  ## TODO: IPC namespace
  ## TODO: Network namespace - probably via ip netns
  ## TODO: Don't preserve the environment - env -i

  ## Optionally remove network access by creating an empty net namespace.
  if [ "${allow_net}" = "no" ]; then
    bwrap_args+=("--unshare-net")
  fi

  ## Optionally allow webcam access.
  if [ "${allow_webcam}" = "yes" ]; then
    for device in /dev/video*
    do
      if [ -f "${device}" ]; then
        bwrap_args+=("--dev-bind-try")
        bwrap_args+=("${device}")
        bwrap_args+=("${device}")
      fi
    done
  fi

  ## Optionally allow microphone access.
  if [ "${allow_mic}" = "yes" ]; then
    bwrap_args+=("--dev-bind-try")
    bwrap_args+=("/dev/snd")
    bwrap_args+=("/dev/snd")
  fi

  ## Shared storage.
  if [ "${shared_storage}" = "read-write" ]; then
    bwrap_args+=("--bind")
    bwrap_args+=("${shared_dir}")
    bwrap_args+=("${shared_dir}")
    bwrap_args+=("--bind")
    bwrap_args+=("/shared")
    bwrap_args+=("/shared")
  elif [ "${shared_storage}" = "read-only" ]; then
    bwrap_args+=("--ro-bind")
    bwrap_args+=("${shared_dir}")
    bwrap_args+=("${shared_dir}")
    bwrap_args+=("--ro-bind")
    bwrap_args+=("/shared")
    bwrap_args+=("/shared")
  fi

  ## Install Tor Browser.
  if [ "${app_name}" = "torbrowser" ]; then
    if ! [ -d "${app_homedir}/.tb" ]; then
      ## Use tb-starter (by Whonix developers) to install Tor Browser.
      sudo --set-home --user="${app_user}" HOME="${app_homedir}" tb_no_start=true tb_installer_started_by_sandbox_app_launcher=true torbrowser
    fi
    bwrap_args+=("--ro-bind-try")
    bwrap_args+=("/run/anon-ws-disable-stacked-tor/127.0.0.1_9150.sock")
    bwrap_args+=("/run/anon-ws-disable-stacked-tor/127.0.0.1_9150.sock")
    bwrap_args+=("--ro-bind-try")
    bwrap_args+=("/run/anon-ws-disable-stacked-tor/127.0.0.1_9151.sock")
    bwrap_args+=("/run/anon-ws-disable-stacked-tor/127.0.0.1_9151.sock")
  fi

  true "{bwrap_args[@]}:"
  true "${bwrap_args[@]}"
  true "..."
  ## XXX: bwrap_args does not have proper whitespace support
  ## A bash associative array cannot be passed to another script.
  bwrap_args_string="${bwrap_args[@]}"
  true "..."
  true "bwrap_args_string:"
  true "$bwrap_args_string"

  ## Debugging.
  if [ -o xtrace ]; then
    ## read by wrapper_script
    sandbox_app_launcher_debug=true
  else
    sandbox_app_launcher_debug=false
  fi

  ## variables app_path and sandbox_app_launcher_debug are read by wrapper_script.

  ## Passing file descriptors to brwap is difficult because sudo closes file
  ## descriptors with a number greater than 3. This can be overwritten in
  ## theory using sudo with parameter -C but that would require permitting this
  ## in global sudo configuration policy with closefrom_override which has
  ## unknown security impact. Hence, file descriptors are passed in file:
  ## /usr/share/sandbox-app-launcher/bwrap-wrapper

  sudo \
    --set-home \
    --user="${app_user}" \
    sandbox_app_launcher_debug="${sandbox_app_launcher_debug}" \
    app_path="${app_path}" \
    app_user="${app_user}" \
    main_app_dir="${main_app_dir}" \
    app_homedir="${app_homedir}" \
    wrapper_script="${wrapper_script}" \
    seccomp_filter="${seccomp_filter}" \
    bwrap_args="$bwrap_args_string" \
    /usr/share/sandbox-app-launcher/bwrap-wrapper "$@"

  kill_processes_inside_sandbox

  true "OK"
}

remove_app() {
  if ! [ "${sal_is_run_with_root}" = "true" ]; then
    echo "ERROR: The removal process must be run as root." >&2
    exit 1
  fi

  kill_processes_inside_sandbox

  if getent passwd | grep -q "${app_user}"; then
    userdel --remove --force "${app_user}"
  else
    echo "INFO: User '${app_user}' does not exist, probably already removed earlier, OK."
  fi

  echo "INFO: Done, removal complete, OK."
}

if [ "${setup_sandbox}" = "1" ]; then
  setup
elif [ "${start_program}" = "1" ]; then
  if sudo --set-home --user="${app_user}" test -d "${app_homedir}" ; then
    run_program "${@}"
  else
    echo "ERROR: The sandbox for this program has not been set up yet. Please execute:

sudo sandbox-app-launcher setup ${app_name}" >&2
  fi
elif [ "${remove}" = "1" ]; then
  remove_app
fi
