#!/usr/bin/env bash
# More safety, by turning some bugs into errors.
# Without `errexit` you don’t need ! and can replace
# ${PIPESTATUS[0]} with a simple $?, but I prefer safety.
set -euf -o pipefail

#---------------------------------------------------
#
# {{@@ header() @@}}
#
# age encryption / decryption helpers
# based on https://git.sr.ht/~digital/secretFiles
#
# For macOS coreutils and gnu-getopt are required to
# run this script.
# brew install coreutils gnu-getopt
#
#---------------------------------------------------

#TMPPATH="/dev/shm"
TMPPATH="/tmp"

[[ -d "/opt/homebrew/opt/coreutils/libexec/gnubin" ]] && export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:${PATH}"
[[ -d "/opt/homebrew/opt/gnu-getopt/bin" ]] && export PATH="/opt/homebrew/opt/gnu-getopt/bin:${PATH}"

# get recipients for age file to encrypt with
get-recipients-list() {
  local target="${1}"
  local recipients=${2:-"-R" "$(pwd)/secrets/hostkeys/masterkey.pubkey"}
  local search="${target}"
  while true; do
    if test -d "${search}.recipients"; then
      for recip in $(ls ${search}.recipients) ; do
        if test -n "${recip}"; then
          recipients+=("-R" "${search}.recipients/${recip}")
        fi
      done
    elif test -f "${search}.recipients"; then
      recipients+=( "-R" "${search}.recipients")
    fi
    if test "$(realpath ${search})" == "$(realpath $(pwd))"; then
      break
    fi
    search=$(dirname "${search}")
  done
  echo "${recipients[@]}"
}

gen-key() {
  local keyname="${1}"
  local working_directory="${2:-$(pwd)}"

  mkdir -p "${working_directory}/secrets/hostkeys/"
  echo "generating new keys for host ${keyname}";
  age-keygen \
    2> "${working_directory}/secrets/hostkeys/${keyname}.pubkey" \
    | age -p --armor -e -o "${working_directory}/secrets/hostkeys/${keyname}.privkey"
  sed -i 's/Public key: //' "${working_directory}/secrets/hostkeys/${keyname}.pubkey"
}

import-secret() {
  # local stdin=$(</dev/stdin)

  local working_directory="${3:-$(pwd)}"
  local secret_path="${working_directory}/${2}"
  if [[ "${1}" == "EMPTY" ]]; then
    local data=$(</dev/stdin)
  fi
  local recipients_list=$(get-recipients-list "${secret_path}")
  local dirname="$(dirname ${secret_path})"
  local identity="${MASTERKEY_FILE:-secrets/hostkeys/masterkey.privkey}"

  mkdir -p "${dirname}"

  if [[ "${1}" == "EMPTY" ]]; then
    echo -n ${data} | age $(sed -e "s/^\'//" -e "s/\'$//" <<<"${recipients_list[@]}") --encrypt --armor --output "${secret_path}"
  else
    age $(sed -e "s/^\'//" -e "s/\'$//" <<<"${recipients_list[@]}") --encrypt --armor --output "${secret_path}" "${working_directory}/${1}"
  fi
}

edit-file() {
  local current_umask=$(umask)
  umask 177

  local working_directory="${2:-$(pwd)}"
  local secret_path="${working_directory}/${1}"
  local tmp_path="$(mktemp -p ${TMPPATH})"
  local recipients_list=$(get-recipients-list "${secret_path}")
  local identity="${MASTERKEY_FILE:-$([[ -f "$(realpath "${working_directory}/secrets/hostkeys/masterkey.privkey")" ]] && echo -n "$(realpath "${working_directory}/secrets/hostkeys/masterkey.privkey")" || echo -n "/dev/stdin")}"

  if test -e "${secret_path}"; then
    set +e +o pipefail

    age \
      --decrypt \
      --identity "${identity}" \
      --output "${tmp_path}" \
      "${secret_path}" || local decrypt_failed=true
    
    set -e -o pipefail
  else
    # if file descriptor 0 is not a terminal, ie if /dev/stdin is a pipe
    if [ ! -t 0 ]; then
      cat "${identity}" > /dev/null
    fi
  fi

  if [[ ! ${decrypt_failed:-} ]]; then
    local mod_time_before=$(stat --format "%Y" "${tmp_path}")
    ${EDITOR} "${tmp_path}"
    local mod_time_after=$(stat --format "%Y" "${tmp_path}")

    if test "${mod_time_before}" != "${mod_time_after}"; then
      echo "change detected, reencrypting file" > /dev/stderr
      age $(sed -e "s/^\'//" -e "s/\'$//" <<<"${recipients_list[@]}") --encrypt --armor --output "${secret_path}" "${tmp_path}"
    else
      echo "no change detected, not reencrypting file" > /dev/stderr
    fi
  fi

  rm "${tmp_path}"

  umask ${current_umask}
}

reencrypt-all() {
  local current_umask=$(umask)
  umask 177

  local working_directory="${2:-$(pwd)}"
  local identity="${1:-/dev/stdin}"
  local identity_file="$(mktemp -u -p ${TMPPATH})"

  # make the identity file reuseable, in case it actually is /dev/stdin
  umask 177
  cat "${identity}" > "${identity_file}"

  cd ${working_directory}
  find "secrets" -type f -not -name "*\.recipients" \
      | grep -v "^secrets/hostkeys/"| while read line; do
      if ! grep -q "^-----BEGIN AGE ENCRYPTED FILE-----$" "${line}"; then
          echo "skipping unecrypted file '${line}'"
          continue
      fi
      local recipients=$(get-recipients-list "${line}")
      echo "reencrypting '${line}' for recipients ${recipients[@]}"
      local content="$(age --decrypt \
          --identity "${identity_file}" \
          "${line}" \
      )" || {
          echo "ERROR: failed decryption of '${line}'" > /dev/stderr
          echo "aborting and leaving secrets store in an inconsistent state" > /dev/stderr
          exit 2
      }
      if test $? -eq 0 ; then
          echo -n "${content}" \
              | age $(sed -e "s/^\'//" -e "s/\'$//" <<<"${recipients[@]}") \
              --encrypt \
              --armor \
              --output "${line}"
      fi
  done

  rm "${identity_file}"

  umask ${current_umask}

  echo "SUCCESS" > /dev/stderr
}

pass-import-key() {
  local keyname="${1}"
  local passbase="${2:-nixfiles/hostkeys}/${keyname}"
  local working_directory="${3:-$(pwd)}"
  local secretbase="${working_directory}/secrets/hostkeys/${keyname}"

  if test ! -f "${secretbase}.privkey"; then
    echo "missing private key file for key ${keyname}"
    exit 1
  elif test ! -f "${secretbase}.pubkey"; then
    echo "missing public key file for key ${keyname}"
    exit 1
  fi

  echo "importing the keyfiles for host ${keyname}"
  echo "enter the password for the private key file"
  pass insert "${passbase}.pw"
  pass -c "${passbase}.pw"
  echo "enter the password for the private key file again"
  age -d "${secretbase}.privkey" | pass insert -m "${passbase}.privkey" > /dev/null
  cat "${secretbase}.pubkey" | pass insert -m "${passbase}.pubkey" > /dev/null
  echo "success"
}

help() {
  echo "Usage: $(basename ${0}) <edit | gen-key | import | pass-import-key | reencrypt-all>"
  echo ""
  echo "Options:"
  echo "    edit"
  echo "                      -f, --file      relative path to the nixOS root directory to the file"
  echo "                      -p, --path      path to the root directory for the nixOS configuration files, defaults to \`pwd\`"
  echo "    gen-key"
  echo "                      -k, --key       keyname, usually the hostname (e.g. host-<hostname>)"
  echo "                      -p, --path      path to the root directory for the nixOS configuration files, defaults to \`pwd\`"
  echo "    import"
  echo "                      -f, --file      relative path to the nixOS root directory to the file which should be imported"
  echo "                                      Instead of using this option to reference a file, you can also pass the input via \`stdin\`"
  echo "                      -o, --output    relative path to the nixOS root directory where the encrypted secret will be stored"
  echo "                      -p, --path      path to the root directory for the nixOS configuration files, defaults to \`pwd\`"
  echo "    pass-import-key"
  echo "                      -k, --key       keyname, usually the hostname (e.g. host-<hostname>)"
  echo "                      -b, --passbase  base path in pass for stored secret, defaults to \`nixfiles/hostkeys\`"
  echo "                      -p, --path      path to the root directory for the nixOS configuration files, defaults to \`pwd\`"
  echo "    reencrypt-all"
  echo "                      -i, --identity  identity / age private key to DECRYPT the secret for reencryption"
  echo "                      -p, --path      path to the root directory for the nixOS configuration files, defaults to \`pwd\`"
}

# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo 'I’m sorry, `getopt --test` failed in this environment.'
    exit 1
fi

# option --output/-o requires 1 argument
OPTIONS=b:f:hi:k:o:p:
LONGOPTS=passbase:,file:,help,identity:,key:,output:,path:

# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=${OPTIONS} --longoptions=${LONGOPTS} --name "$(basename ${0})" -- "${@:--h}")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "${PARSED}"

# now enjoy the options in order and nicely split until we see --
while true; do
  case "${1}" in
    -b|--passbase)
      passbase="${2}"
      shift 2
      ;;
    -f|--file)
      file="${2}"
      shift 2
      ;;
    -h|--help)
      shift
      help
      exit
      ;;
    -i|--identity)
      identity="${2}"
      shift 2
      ;;
    -k|--key)
      key="${2}"
      shift 2
      ;;
    -o|--output)
      output="${2}"
      shift 2
      ;;
    -p|--path)
      path="${2}"
      shift 2
      ;;
    --)
      shift
      break
      ;;
    *)
      echo "This option (${1}) does not exist. Exiting."
      exit 3
      ;;
  esac
done

# handle non-option arguments
if [[ ${#} -eq 1 ]]; then
  while true; do
    case "${1}" in
      edit)
        edit-file "${file:?Error, missing option \"-f\"}" "${path:-}"
        shift
        exit
        ;;
      gen-key)
        gen-key "${key:?Error, missing option \"-k\"}" "${path:-}"
        shift
        exit
        ;;
      import) 
        import-secret "${file:-"EMPTY"}" "${output:?Error, missing option \"-o\"}" "${path:-}"
        shift
        exit
        ;;
      pass-import-key)
        pass-import-key "${key:?Error, missing option \"-k\"}" "${passbase:?Error, missing option \"-b\"}" "${path:-}"
        shift
        exit
        ;;
      reencrypt-all)
        reencrypt-all "${identity:?Error, missing option \"-i\"}" "${path:-}"
        shift
        exit
        ;;
      *)
        echo "Wrong sub command, use -h to print the help."
        exit 4
        ;;
    esac
  done
else
  echo "No sub command provided, use -h to print the help."
fi
