mirror of
				https://github.com/shokinn/.files.git
				synced 2025-11-03 20:18:10 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			315 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			315 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable file
		
	
	
	
	
#!/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
 | 
						||
#
 | 
						||
#---------------------------------------------------
 | 
						||
 | 
						||
# 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 /dev/shm)"
 | 
						||
  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 /dev/shm)"
 | 
						||
 | 
						||
  # 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})" -- "${@}")
 | 
						||
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
 |