.files/dotfiles/bin/secretfiles

319 lines
10 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# More safety, by turning some bugs into errors.
# Without `errexit` you dont 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\`"
}
set -x
# -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 'Im 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})" -- "${@}")
echo "${PIPESTATUS[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 getopts 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