From f226ffe608a8e659d4f215fe946230486a8c7400 Mon Sep 17 00:00:00 2001 From: shokinn Date: Fri, 15 Dec 2023 15:35:29 +0100 Subject: [PATCH] move age secretfiles helper command to own script --- config.yaml | 7 +- dotfiles/bin/secretfiles | 318 +++++++++++++++++++++++++++++++++++++++ dotfiles/commonfunc | 150 ------------------ 3 files changed, 324 insertions(+), 151 deletions(-) create mode 100755 dotfiles/bin/secretfiles diff --git a/config.yaml b/config.yaml index bd786c3..d5e553a 100644 --- a/config.yaml +++ b/config.yaml @@ -134,6 +134,9 @@ dotfiles: f_gpgagent: dst: ~/.gnupg/gpg-agent.conf src: gnupg/gpg-agent.conf + f_secretfiles: + dst: ~/.bin/secretfiles + src: bin/secretfiles profiles: AINCRAD: dotfiles: @@ -256,6 +259,7 @@ profiles: - f_wslshowhypervforwarding - f_commonfunc - f_gpgagent + - f_secretfiles workspace: dotfiles: - d_fonts @@ -300,4 +304,5 @@ profiles: - f_pu - f_rpki - f_rmquarantine - - f_commonfunc \ No newline at end of file + - f_commonfunc + - f_secretfiles \ No newline at end of file diff --git a/dotfiles/bin/secretfiles b/dotfiles/bin/secretfiles new file mode 100755 index 0000000..77fe91d --- /dev/null +++ b/dotfiles/bin/secretfiles @@ -0,0 +1,318 @@ +#!/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/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}) " + 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-)" + 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-)" + 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 '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})" -- "${@}") +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 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 diff --git a/dotfiles/commonfunc b/dotfiles/commonfunc index 3446c3e..fc7b938 100644 --- a/dotfiles/commonfunc +++ b/dotfiles/commonfunc @@ -8,156 +8,6 @@ # {{@@ header() @@}} # -# age encryption / decryption helpers -# based on https://git.sr.ht/~digital/secretFiles -if [[ $(command -v age) ]]; then - # get recipients for age file to encrypt with - ageGetRecipientsList() { - local target="${1}" - local search="${target}" - local recipients=( "-R" "secrets/hostkeys/masterkey.pubkey" ) - local recip - 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[@]}" - } - - age-gen-key() { - set -efu -o pipefail - - local keyname="${1}" - - mkdir -p "secrets/hostkeys/" - echo "generating new keys for host ${keyname}"; - age-keygen \ - 2> "secrets/hostkeys/${keyname}.pubkey" \ - | age -p --armor -e -o "secrets/hostkeys/${keyname}.privkey" - sed -i 's/Public key: //' "secrets/hostkeys/${keyname}.pubkey" - - set +efu +o pipefail - } - - age-import-secret() { - local data=$( /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} - set +efu +o pipefail - } - - age-reencrypt-all() { - set -euf -o pipefail - local current_umask=$(umask) - umask 177 - - 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}" - - 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=$(ageGetRecipientsList "${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} - set +efu +o pipefail - - echo "SUCCESS" > /dev/stderr - } -fi - # eza - set aliasses for eza to use it as ls replacement if [[ $(command -v eza) ]]; then ezafunc() {