From d0b75ddf9a8012909dfabd1180e800782afb34cd Mon Sep 17 00:00:00 2001 From: Ryan Mulligan Date: Sun, 29 Jan 2023 11:58:38 -0800 Subject: [PATCH] contrib: use mkDerivation for agenix cli * use mkDerivation * separate shell code in own file * use shellcheck to lint shell code * remove rage version check since rage is greater than 0.5.0 on all maintained nixpkgs --- pkgs/agenix.nix | 204 +++++++----------------------------------------- pkgs/agenix.sh | 162 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 177 deletions(-) create mode 100644 pkgs/agenix.sh diff --git a/pkgs/agenix.nix b/pkgs/agenix.nix index 56e3da3..648ccb9 100644 --- a/pkgs/agenix.nix +++ b/pkgs/agenix.nix @@ -1,187 +1,37 @@ { lib, - writeShellScriptBin, - runtimeShell, - callPackage, + stdenv, rage, gnused, nix, mktemp, diffutils, - ageBin ? "${ - # we need at least rage 0.5.0 to support ssh keys - if rage.version < "0.5.0" - then callPackage ./rage.nix {} - else rage - }/bin/rage", -}: let - sedBin = "${gnused}/bin/sed"; - nixInstantiate = "${nix}/bin/nix-instantiate"; - mktempBin = "${mktemp}/bin/mktemp"; - diffBin = "${diffutils}/bin/diff"; -in - lib.recursiveUpdate (writeShellScriptBin "agenix" '' - set -Eeuo pipefail + substituteAll, + ageBin ? "${rage}/bin/rage", + shellcheck, +}: +stdenv.mkDerivation rec { + pname = "agenix"; + version = "0.13.0"; + src = substituteAll { + inherit ageBin version; + sedBin = "${gnused}/bin/sed"; + nixInstantiate = "${nix}/bin/nix-instantiate"; + mktempBin = "${mktemp}/bin/mktemp"; + diffBin = "${diffutils}/bin/diff"; + src = ./agenix.sh; + }; + dontUnpack = true; - PACKAGE="agenix" + doCheck = true; + checkInputs = [shellcheck]; + postCheck = '' + shellcheck $src + ''; - function show_help () { - echo "$PACKAGE - edit and rekey age secret files" - echo " " - echo "$PACKAGE -e FILE [-i PRIVATE_KEY]" - echo "$PACKAGE -r [-i PRIVATE_KEY]" - echo ' ' - echo 'options:' - echo '-h, --help show help' - echo '-e, --edit FILE edits FILE using $EDITOR' - echo '-r, --rekey re-encrypts all secrets with specified recipients' - echo '-i, --identity identity to use when decrypting' - echo '-v, --verbose verbose output' - echo ' ' - echo 'FILE an age-encrypted file' - echo ' ' - echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file' - echo ' ' - echo 'EDITOR environment variable of editor to use when editing FILE' - echo ' ' - echo 'RULES environment variable with path to Nix file specifying recipient public keys.' - echo "Defaults to './secrets.nix'" - echo ' ' - echo "agenix version: 0.13.0" - echo "age binary path: ${ageBin}" - echo "age version: $(${ageBin} --version)" - } + installPhase = '' + install -D $src ${placeholder "out"}/bin/agenix + ''; - test $# -eq 0 && (show_help && exit 1) - - REKEY=0 - DEFAULT_DECRYPT=(--decrypt) - - while test $# -gt 0; do - case "$1" in - -h|--help) - show_help - exit 0 - ;; - -e|--edit) - shift - if test $# -gt 0; then - export FILE=$1 - else - echo "no FILE specified" - exit 1 - fi - shift - ;; - -i|--identity) - shift - if test $# -gt 0; then - DEFAULT_DECRYPT+=(--identity "$1") - else - echo "no PRIVATE_KEY specified" - exit 1 - fi - shift - ;; - -r|--rekey) - shift - REKEY=1 - ;; - -v|--verbose) - shift - set -x - ;; - *) - show_help - exit 1 - ;; - esac - done - - RULES=''${RULES:-./secrets.nix} - - function cleanup { - if [ ! -z ''${CLEARTEXT_DIR+x} ] - then - rm -rf "$CLEARTEXT_DIR" - fi - if [ ! -z ''${REENCRYPTED_DIR+x} ] - then - rm -rf "$REENCRYPTED_DIR" - fi - } - trap "cleanup" 0 2 3 15 - - function edit { - FILE=$1 - KEYS=$((${nixInstantiate} --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" rules.\"$FILE\".publicKeys)" | ${sedBin} 's/"//g' | ${sedBin} 's/\\n/\n/g') | ${sedBin} '/^$/d' || exit 1) - - if [ -z "$KEYS" ] - then - >&2 echo "There is no rule for $FILE in $RULES." - exit 1 - fi - - CLEARTEXT_DIR=$(${mktempBin} -d) - CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")" - - if [ -f "$FILE" ] - then - DECRYPT=("''${DEFAULT_DECRYPT[@]}") - if [ -f "$HOME/.ssh/id_rsa" ]; then - DECRYPT+=(--identity "$HOME/.ssh/id_rsa") - fi - if [ -f "$HOME/.ssh/id_ed25519" ]; then - DECRYPT+=(--identity "$HOME/.ssh/id_ed25519") - fi - if [[ "''${DECRYPT[*]}" != *"--identity"* ]]; then - echo "No identity found to decrypt $FILE. Try adding an SSH key at $HOME/.ssh/id_rsa or $HOME/.ssh/id_ed25519 or using the --identity flag to specify a file." - exit 1 - fi - DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE") - ${ageBin} "''${DECRYPT[@]}" || exit 1 - cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before" - fi - - $EDITOR "$CLEARTEXT_FILE" - - if [ ! -f "$CLEARTEXT_FILE" ] - then - echo "$FILE wasn't created." - return - fi - [ -f "$FILE" ] && [ "$EDITOR" != ":" ] && ${diffBin} "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" 1>/dev/null && echo "$FILE wasn't changed, skipping re-encryption." && return - - ENCRYPT=() - while IFS= read -r key - do - ENCRYPT+=(--recipient "$key") - done <<< "$KEYS" - - REENCRYPTED_DIR=$(${mktempBin} -d) - REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")" - - ENCRYPT+=(-o "$REENCRYPTED_FILE") - - ${ageBin} "''${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1 - - mv -f "$REENCRYPTED_FILE" "$1" - } - - function rekey { - FILES=$((${nixInstantiate} --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" (builtins.attrNames rules))" | ${sedBin} 's/"//g' | ${sedBin} 's/\\n/\n/g') || exit 1) - - for FILE in $FILES - do - echo "rekeying $FILE..." - EDITOR=: edit "$FILE" - cleanup - done - } - - [ $REKEY -eq 1 ] && rekey && exit 0 - edit "$FILE" && cleanup && exit 0 - '') - { - meta.description = "age-encrypted secrets for NixOS"; - } + meta.description = "age-encrypted secrets for NixOS"; +} diff --git a/pkgs/agenix.sh b/pkgs/agenix.sh new file mode 100644 index 0000000..afbfa4d --- /dev/null +++ b/pkgs/agenix.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +PACKAGE="agenix" + +function show_help () { + echo "$PACKAGE - edit and rekey age secret files" + echo " " + echo "$PACKAGE -e FILE [-i PRIVATE_KEY]" + echo "$PACKAGE -r [-i PRIVATE_KEY]" + echo ' ' + echo 'options:' + echo '-h, --help show help' + # shellcheck disable=SC2016 + echo '-e, --edit FILE edits FILE using $EDITOR' + echo '-r, --rekey re-encrypts all secrets with specified recipients' + echo '-i, --identity identity to use when decrypting' + echo '-v, --verbose verbose output' + echo ' ' + echo 'FILE an age-encrypted file' + echo ' ' + echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file' + echo ' ' + echo 'EDITOR environment variable of editor to use when editing FILE' + echo ' ' + echo 'RULES environment variable with path to Nix file specifying recipient public keys.' + echo "Defaults to './secrets.nix'" + echo ' ' + echo "agenix version: @version@" + echo "age binary path: @ageBin@" + echo "age version: $(@ageBin@ --version)" +} + +test $# -eq 0 && (show_help && exit 1) + +REKEY=0 +DEFAULT_DECRYPT=(--decrypt) + +while test $# -gt 0; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + -e|--edit) + shift + if test $# -gt 0; then + export FILE=$1 + else + echo "no FILE specified" + exit 1 + fi + shift + ;; + -i|--identity) + shift + if test $# -gt 0; then + DEFAULT_DECRYPT+=(--identity "$1") + else + echo "no PRIVATE_KEY specified" + exit 1 + fi + shift + ;; + -r|--rekey) + shift + REKEY=1 + ;; + -v|--verbose) + shift + set -x + ;; + *) + show_help + exit 1 + ;; + esac +done + +RULES=${RULES:-./secrets.nix} + +function cleanup { + if [ -n "${CLEARTEXT_DIR+x}" ] + then + rm -rf "$CLEARTEXT_DIR" + fi + if [ -n "${REENCRYPTED_DIR+x}" ] + then + rm -rf "$REENCRYPTED_DIR" + fi +} +trap "cleanup" 0 2 3 15 + +function edit { + FILE=$1 + KEYS=$( (@nixInstantiate@ --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" rules.\"$FILE\".publicKeys)" | @sedBin@ 's/"//g' | @sedBin@ 's/\\n/\n/g') | @sedBin@ '/^$/d' || exit 1) + + if [ -z "$KEYS" ] + then + >&2 echo "There is no rule for $FILE in $RULES." + exit 1 + fi + + CLEARTEXT_DIR=$(@mktempBin@ -d) + CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")" + + if [ -f "$FILE" ] + then + DECRYPT=("${DEFAULT_DECRYPT[@]}") + if [ -f "$HOME/.ssh/id_rsa" ]; then + DECRYPT+=(--identity "$HOME/.ssh/id_rsa") + fi + if [ -f "$HOME/.ssh/id_ed25519" ]; then + DECRYPT+=(--identity "$HOME/.ssh/id_ed25519") + fi + if [[ "${DECRYPT[*]}" != *"--identity"* ]]; then + echo "No identity found to decrypt $FILE. Try adding an SSH key at $HOME/.ssh/id_rsa or $HOME/.ssh/id_ed25519 or using the --identity flag to specify a file." + exit 1 + fi + DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE") + @ageBin@ "${DECRYPT[@]}" || exit 1 + cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before" + fi + + $EDITOR "$CLEARTEXT_FILE" + + if [ ! -f "$CLEARTEXT_FILE" ] + then + echo "$FILE wasn't created." + return + fi + [ -f "$FILE" ] && [ "$EDITOR" != ":" ] && @diffBin@ "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" 1>/dev/null && echo "$FILE wasn't changed, skipping re-encryption." && return + + ENCRYPT=() + while IFS= read -r key + do + ENCRYPT+=(--recipient "$key") + done <<< "$KEYS" + + REENCRYPTED_DIR=$(@mktempBin@ -d) + REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")" + + ENCRYPT+=(-o "$REENCRYPTED_FILE") + + @ageBin@ "${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1 + + mv -f "$REENCRYPTED_FILE" "$1" +} + +function rekey { + FILES=$( (@nixInstantiate@ --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" (builtins.attrNames rules))" | @sedBin@ 's/"//g' | @sedBin@ 's/\\n/\n/g') || exit 1) + + for FILE in $FILES + do + echo "rekeying $FILE..." + EDITOR=: edit "$FILE" + cleanup + done +} + +[ $REKEY -eq 1 ] && rekey && exit 0 +edit "$FILE" && cleanup && exit 0