#!/usr/bin/env bash # Age encryption using secrets in 1password vault # Date: 2024-06-04 # Version: 0.3 # License: MIT or APACHE-2.0 # Author: @stevelr # Author: @anthonycicc # # Run `age-op -h` for help and examples # # For information about age, see https://github.com/FiloSottile/age # Tested on mac and linux. Tested with age and rage (rust-rage) # check dependencies # Change to 'rage' if you prefer AGE=${AGE:-$(which age)} OP=${OP:-op} # Select private folder for temporary secrets. can be overridden with `-t` flag # The defaults for linux and macos are readable by owner only # Returns: temp folder path with no trailing '/' private_tmp() { if [ -d /run/user/$(id -u) ]; then echo /run/user/$(id -u) # linux. folder owned by user with mode 700 elif [ -d "$TMPDIR" ]; then echo "$(echo $TMPDIR | sed 's./$..')" # macos. owned by user with mode 700. remove trailing slash else echo "$PWD" fi } tmppath=$(private_tmp) # Display help message and quit # param: ERROR_MESSAGE # If ERROR_MESSAGE is not empty, exits 1, otherwise exits 0 _help() { [ -n "$1" ] && echo "Error: $1\n" local ds="\$" local prog=$(basename $0) cat <<_HELP age encryption with secret keys in 1password vault Examples: Encrypt file: $prog -e -k KEY_PATH [ -o OUTPUT ] [ -t TMPDIR ] [ FILE ] encrypt a single input file FILE. If FILE is '-' or not specified, stdin is encrypted If OUTPUT is '-' or not specified, the output is sent to stdout To encrypt one or more files or folders to a tar file, use tar czf - FILE_OR_DIR FILE_OR_DIR ... | $prog -e -k KEY_PATH -o foo.tar.gz.age Decrypt file: $prog -d -k KEY_PATH [ -o OUTPUT ] [-t TMPDIR ] [ FILE ] decrypt a file, or stdin If FILE is '-' or not specified, stdin is decrypted If OUTPUT is '-' or not specified, the output is sent to stdout To decrypt a tar file, $prog -d -k KEY_PATH foo.tar.gz.age | tar xzf - Generate an age ed25519 key and store it in the 1password vault. The type of the new item will be "Password" $prog -n -k KEY_PATH KEY_PATH should have one of the following formats: - 'op://vault/title', 'op://vault/title/field', or 'op://vault/title/section/field' In the first case, the field name defaults to 'password' TMPDIR is the temporary folder where key will be briefly written and quickly removed Default is '$tmppath' 1Password configuration: For the 1Password cli ('op') to authenticate with a vault, you can do one of the following: - For use with a service account, set the environment variable OP_SERVICE_ACCOUNT_TOKEN - For use with a 1Password Connect Server, set OP_CONNECT_HOST and OP_CONNECT_TOKEN - sign into a local app with "eval ${ds}(op signin)" Dependencies: Installation instructions and documentation: age: https://age-encryption.org op (1Password cli): https://developer.1password.com/docs/cli/get-started _HELP [ -n "$1" ] && exit 1 || exit 0 } # Store key in unique temp file, with access limited to current user # params: TMPDIR KEY # returns: path to temp file store_secret() { secret=$(mktemp "$1/age-secret.XXXXXX") chmod 600 "$secret" cat >"$secret" <<_EKEY $2 _EKEY echo "$secret" } # Create a new key # params: KEYPATH new_key() { local keypath="$1" local key field out pw title vault field ## ## Create new key ## vault=$(echo $keypath | sed -E 's|op://([^/]+)\/([^/]+)\/(.*)|\1|') title=$(echo $keypath | sed -E 's|op://([^/]+)\/([^/]+)\/(.*)|\2|') field=$(echo $keypath | sed -E 's|op://([^/]+)\/([^/]+)\/(.*)|\3|') # check if the key path exists so we don't overwrite it. # The successs case (key is unique) generates an error, so temporarily disable '+e' set +e key=$($OP item get "$title" "--vault=$vault" 2>/dev/null) [ $? -eq 0 ] && _help "Key vault:$vault title:$title already exists - will not overwrite" set -e pw="$($AGE-keygen)" out=$($OP item create --category=password --title="$title" --vault="$vault" "$field=$pw") echo "Created vault:$vault title:$title" } cmd="" input="" output="" keypath="" stdin=0 _err="" # putting this in a variable makes it work with zsh help_regex="^\-h|^--help|^help$" [ ! $($AGE --help 2>&1 | grep Usage) ] && _help "Missing 'age' or 'rage' dependency. Please see installation url below." # 1password cli [ ! $($OP --version) ] && _help "Missing 'op' dependency. Please see installation url below." [[ $1 =~ $help_regex ]] && _help while getopts ':hnedo:k:t:' OPTION; do case $OPTION in h) _help ;; n) [ -n "$cmd" ] && _help "Only one of -e, -d, or -n may be used" cmd="new" ;; e) [ -n "$cmd" ] && _help "Only one of -e, -d, or -n may be used" cmd="encrypt" ;; d) [ -n "$cmd" ] && _help "Only one of -e, -d, or -n may be used" cmd="decrypt" ;; o) output=$OPTARG ;; k) keypath=$OPTARG if [[ -v AGE_KEYPATH ]]; then keypath=$AGE_KEYPATH fi if [[ ! $keypath =~ ^op://[^/]+/[^/]+/.+$ ]]; then # if path has only two segments (vault & title), append field "password" # since the 'new' function creates items of type Password if [[ $keypath =~ ^op://[^/]+/[^/]+$ ]]; then keypath="$keypath/password" else _help "Invalid key path '$keypath'" fi fi ;; t) tmppath=$OPTARG [ ! -d "$tmppath" ] && _help "Invalid tmp folder: '$tmppath' does not exist" ;; ?) _help "" ;; esac done shift "$(($OPTIND -1))" [ -z "$cmd" ] && _help "One of -e, -d, or -n must be used" [ -z "$keypath" ] && _help "keypath is required. Should be of the form op://vault/title[/field]" if [ "$cmd" = "new" ]; then new_key $keypath else ## ## Encrypt or Decrypt ## if [ -z "$1" ] || [ "$1" = "-" ]; then stdin=1 else input="$1" [ ! -r "$input" ] && _help "Missing or unreadable input file '$input'" # don't re-encrypt file ending in .age [ "$cmd" = "encrypt" ] && [[ $input =~ \.age$ ]] && _help "Input file may not end in '.age'" fi if [ -z "$output" ] || [ "$output" = "-" ]; then output=/dev/stdout fi key=$($OP read "$keypath") if [ $? -ne 0 ] || [ -z "$key" ]; then _help "Invalid keypath '$keypath'" fi secret=$(store_secret "$tmppath" "$key") ## try {( set +e # don't quit on error here - we want to make sure secret is deleted if [ $stdin -eq 1 ]; then $AGE --${cmd} -i "$secret" >"$output" else $AGE --${cmd} -i "$secret" <"$input" >"$output" fi )} ## catch { rm -f "$secret" } fi unset _err cmd input key keypath output secret stdin tmppath