#!/usr/bin/env bash # # Common functions. # # Setup main configuration and load preferences function keyringer_config_load { if [ -f "$HOME/.$NAME" ]; then echo "Converting legacy configuration scheme..." mv "$HOME/.$NAME" "$HOME/.$NAME.tmp" mkdir "$HOME/.$NAME" mv "$HOME/.$NAME.tmp" "$CONFIG" fi if [ ! -e "$CONFIG" ]; then echo "Creating $CONFIG..." mkdir -p `dirname $CONFIG` touch "$CONFIG" chmod 600 "$CONFIG" echo "# Keyringer config file." > "$CONFIG" echo "" >> "$CONFIG" fi keyringer_config_load_preferences } # Load config preferences function keyringer_config_load_preferences { # Load custom keyring preferences if [ ! -z "$PREFERENCES" ] && [ -f "$PREFERENCES" ]; then source "$PREFERENCES" elif [ ! -z "$PREFERENCES" ]; then touch $PREFERENCES fi } # Load a parameter from config function keyringer_config { if [ -z "$CONFIG" ]; then echo "Your have to set CONFIG variable in the code" exit 1 elif [ -e "$CONFIG" ]; then grep -e "^$1=" "$CONFIG" | tail -n 1 | cut -d = -f 2 | sed -e 's/"//g' -e "s/'//g" | sed -e 's/ *#.*$//' else echo "Config file not found: $CONFIG" exit 1 fi } # Return the list of recipients function keyringer_recipients { grep -v '^#' "$1" | grep -v '^$' | awk '{ print "-r " $2 }' | xargs } # Check if keyringer has a given action function keyringer_has_action { if [ -z "$ACTIONS" ]; then echo "Your have to set ACTIONS variable in the code" exit 1 fi if [ -e "$ACTIONS/$1" ] && [ ! -d "$ACTIONS/$1" ]; then true else false fi } # Execute an action function keyringer_exec { # Setup action="$1" basedir="$2" shift 2 # Dispatch if keyringer_has_action "$action"; then "$ACTIONS/$action" "$basedir" $* err="$?" if [ "$err" != "0" ]; then exit "$err" fi fi } # Return a filename with correct extension function keyringer_filename { if [ -z "$1" ]; then return else printf "%s/%s.asc\n" "$(dirname "$1")" "$(basename "$1" .asc)" fi } # Check if a folder is inside a git repository function keyringer_is_git { if [ -z "$1" ]; then false elif [ ! -d "$1" ]; then false elif [ -d "$1/.git" ]; then true else ( cd "$1" && git status &> /dev/null ) if [ "$?" != "128" ]; then true else false fi fi } # Check the security of a temporary folder function keyringer_check_tmp { local path="$1" local mount # Mode check if [ -z "$path" ] || [ ! -d "$path" ] || [ ! -w "$path" ] || [ ! -x "$path" ]; then return 1 fi # Ramdisk check mount="`df "$path" | sed -n '$p' | awk '{ print $NF }'`" mount -l -t tmpfs | awk '{ print $3 }' | grep -q -e "^$mount$" } # Setup a temporary file function keyringer_set_tmpfile { local tmp local candidate local candidates="$TMPDIR $TMP /tmp /run/shm" if [ -z "$BASEDIR" ]; then echo "Please set BASEDIR before creating a tmp file" exit 1 fi # Ramdisk check for candidate in $candidates; do if keyringer_check_tmp $candidate; then tmp="$candidate/keyringer.`whoami`" break fi done # Set base temp folder if [ -z "$tmp" ]; then echo "WARNING: neither one of $candidates is mounted in a tmpfs/ramdisk, using $BASEDIR/tmp as fallback." echo "Make sure that $BASEDIR is atop of an encrypted volume." echo "Press any key to continue, Ctrl-C to abort" read key tmp="$BASEDIR/tmp" # Just to be sure keyringer_git_ignore 'tmp/*' fi # Determine template if [ -z "$1" ]; then template="$tmp/keyringer.XXXXXXXXXX" else template="$tmp/keyringer.$1.XXXXXXXXXX" fi mkdir -p "$tmp" if [ "$2" == "-d" ]; then TMPWORK="$(mktemp -d "$template")" else TMPWORK="$(mktemp "$template")" fi if [ "$?" != "0" ]; then printf "Error: can't set TMPWORK %s\n" "$TMPWORK" exit 1 fi trap "keyringer_unset_tmpfile $TMPWORK; exit" INT TERM EXIT } # Shred files function keyringer_shred { local path="$1" local tool local message="Removing" if [ -z "$path" ]; then return elif [ ! -e "$path" ]; then return fi # Create our test target local rmtest="$(mktemp)" # Get shred implementation if which wipe &> /dev/null; then tool="wipe" elif which shred &> /dev/null; then tool="shred" elif rm -P "${rmtest}" &> /dev/null; then tool="rm -P" else # Worst implementation message="WARNING $message" tool="rm" fi # Cleanup in case "rm -P" is never called or -P flag is unsupported rm -f "${rmtest}" echo "$message $path using $tool..." if [ -d "$path" ]; then if [ "$tool" == "wipe" ] || [ "$tool" == "rm" ] || [ "$tool" == "rm -P" ]; then $tool -rf $path else find $path -type f -exec $tool -uf {} \; find $path -depth -type d -exec rmdir {} \; fi else if [ "$tool" == "wipe" ] || [ "$tool" == "rm" ] || [ "$tool" == "rm -P" ]; then $tool -f "$path" else $tool -uf "$path" fi fi } # Remove a temporary file function keyringer_unset_tmpfile { if [ -z "$1" ]; then echo "No tmp file set" fi keyringer_shred "$1" if [ "$?" != "0" ]; then echo "Warning: could not delete file $1. Please delete it manually as it might have sensitive information." exit 1 fi } # Add a pattern into gitignore function keyringer_git_ignore { if [ ! -z "$BASEDIR/.gitignore" ]; then echo "$1" > "$BASEDIR/.gitignore" keyringer_exec git "$BASEDIR" add .gitignore else if ! grep -q -e "^$1$" "$BASEDIR/.gitignore"; then echo "$1" >> "$BASEDIR/.gitignore" fi fi } # Set needed environment variables and do basic checks. function keyringer_set_env { if [ -z "$1" ]; then echo "Error: missing arguments for keyringer_set_env" exit 1 fi # The first argument tells what the action plans to do with the # keyring database: either read, write, readwrite or maintenance. if [ "$1" != "read" ] && [ "$1" != "write" ] && [ "$1" != "readwrite" ] && [ "$1" != "maintenance" ]; then echo "Error: first keyringer_set_env argument must be either read, write or readwrite" exit 1 fi ACTIONS="`dirname $0`" BASENAME="`basename $0`" KEYRINGER_MODE="$1" BASEDIR="$2" SUBCOMMAND="$3" KEYDIR="$BASEDIR/keys" RECIPIENTS_BASE="config/recipients" RECIPIENTS="$BASEDIR/$RECIPIENTS_BASE" OPTIONS="$BASEDIR/config/options" VERSION_INFO="$BASEDIR/config/version" if [ -z "$BASEDIR" ]; then keyringer_action_usage exit 1 fi if [ ! -e "$RECIPIENTS" ]; then echo "No recipient config was found" exit 1 fi if [ -z "$EDITOR" ]; then if type sensible-editor > /dev/null 2>&1 ; then EDITOR=sensible-editor elif type editor > /dev/null 2>&1 ; then EDITOR=editor else echo "You have to set EDITOR env variable" exit 1 fi fi # Avoid viminfo, see https://keyringer.pw/trac/ticket/50 if $EDITOR --help 2>&1 | grep -q -e "^VIM"; then if ! echo $EDITOR | grep -q -- "-i NONE"; then EDITOR="$EDITOR -S $SHARE/editors/vim" fi fi if [ ! -f "$OPTIONS" ]; then echo "No option config was found" exit 1 fi if [ ! -z "$KEYID" ]; then GPG="gpg --quiet --no-encrypt-to -u $KEYID" else GPG="gpg --quiet --no-encrypt-to" fi # Check keyring config version keyringer_check_version # Upgrade configuration keyringer_upgrade # Check repository integrity if [ "$BASENAME" == "check" ]; then keyringer_check_repository fi # Check recipients file keyringer_check_recipients $SUBCOMMAND # Ensure that keydir exists mkdir -p "$KEYDIR" && chmod 700 "$KEYDIR" } # Configuration version tracking to help keyring upgrades function keyringer_check_version { if [ "$KEYRINGER_CHECK_VERSION" == "false" ]; then if [ -f "$VERSION_INFO" ]; then VERSION="`cat $VERSION_INFO`" else VERSION="" fi return fi if [ ! -f "$VERSION_INFO" ]; then echo "Configuration version file not found, trying to pull from remotes..." # Do not use keyringer_exec as it would trigger keyringer_check_version again ( cd "$BASEDIR" && git pull ) if [ ! -f "$VERSION_INFO" ]; then echo "Creating configuration version file..." echo 0 > "$VERSION_INFO" if keyringer_is_git "$BASEDIR"; then keyringer_exec git "$BASEDIR" add config/version echo "Pushing configuration version file to remotes..." for remote in "$BASEDIR/.git/refs/remotes/*"; do keyringer_exec git "$BASEDIR" push $remote master done fi fi fi VERSION="`cat $VERSION_INFO`" # Check if config version is supported by keyringer if [ "$VERSION" != "$CONFIG_VERSION" ]; then echo "Configuration version differs from keyringer version, trying to pull from remotes" # Do not use keyringer_exec as it would trigger keyringer_check_version again ( cd "$BASEDIR" && git pull ) if [ "$VERSION" != "$CONFIG_VERSION" ]; then NEWEST="`echo -e "$VERSION\n$CONFIG_VERSION" | sort -V | tail -n 1`" if [ "$NEWEST" == "$VERSION" ]; then echo "Fatal: config version: $CONFIG_VERSION / config version: $VERSION" echo "Please upgrade your keyringer application" exit 1 fi fi fi } # Configuration upgrades function keyringer_upgrade { # Variable used to hold applied version upgrades local version="$VERSION" if [ "$KEYRINGER_CHECK_VERSION" == "false" ]; then return fi # Upgrade 0.1 if [ "$version" == "0" ]; then if [ ! -d "$RECIPIENTS" ]; then echo "Converting recipients to the new scheme..." mv $RECIPIENTS $RECIPIENTS.tmp mkdir $RECIPIENTS mv $RECIPIENTS.tmp $RECIPIENTS/default keyringer_exec git "$BASEDIR" add $RECIPIENTS_BASE/default keyringer_exec git "$BASEDIR" commit -m "Config-upgrade-0.1" fi # Done 0.1 upgrade echo "0.1" > $VERSION_INFO version="0.1" fi # Upgrade 0.X #if [ "$version" == "0.1" ]; then # echo "Upgrading to 0.X config format..." # # # Done 0.X upgrade # echo "0.X" > $VERSION_INFO # version="0.X" #fi # Update version information if [ "$CONFIG_VERSION" != "$VERSION" ]; then echo $CONFIG_VERSION > $VERSION_INFO keyringer_exec git "$BASEDIR" add config/version keyringer_exec git "$BASEDIR" commit -m "Config-update-$CONFIG_VERSION" echo "Upgrade to version $CONFIG_VERSION completed, pushing to remotes..." for remote in "$BASEDIR/.git/refs/remotes/*"; do keyringer_exec git "$BASEDIR" push $remote master done fi } # Get an option # # Given that options are shared among users through the # repository, we can't just "source $OPTIONS" as we would # be opening a simple arbitrary code execution hole. # # TODO function keyringer_get_option { false } # Get a file argument function keyringer_get_file { FILE="$(keyringer_filename "$RELATIVE_PATH/$1")" if [ ! -f "$KEYDIR/$FILE" ]; then # Try to find a similar file FILE="" count=0 candidates=(`keyringer_exec find "$BASEDIR" | grep -i "$1" | grep -e '.asc$'`) if [ ! -z "$candidates" ]; then if [ ! -z "$1" ]; then echo "Could not find exact match for \"$1\"" fi echo "Choose one of the following or type a pattern:" echo "" for candidate in ${candidates[@]}; do echo -e "\t[$count] $candidate" let count++ done echo "" read -p "Enter option (Ctrl-C to abort): " option if [[ "$option" =~ ^[0-9]+$ ]] && [ ! -z "${candidates[$option]}" ]; then FILE="$(keyringer_filename "$RELATIVE_PATH/${candidates[$option]}")" elif [ ! -z "$option" ]; then keyringer_get_file $option fi else echo "Nothing matches $option, try again." keyringer_get_file fi fi # Probably Ctrl-D was hit if [ -z "$FILE" ]; then echo "" exit fi } # Get a new file argument function keyringer_get_new_file { # File must not contain spaces if [ ! -z "$2" ] ; then FILE="`echo "$*" | sed -e 's/ /_/g'`" else FILE="$1" fi # Sanitize and complete file name FILE="`echo $FILE | sed -e 's/[^A-Za-z0-9@.\/\-]/_/g'`" # Warn user about file name change if [ "`basename "$*"`" != "`basename $FILE`" ]; then echo "Sanitizing destination filename to `basename $FILE`" fi # Complete file name FILE="$RELATIVE_PATH/$(keyringer_filename "$FILE")" if [ -z "$*" ]; then keyringer_action_usage exit 1 fi } # Get a command argument function keyringer_get_command { # Aditional parameters COMMAND="$1" if [ -z "$COMMAND" ]; then keyringer_action_usage command exit 1 fi } # Run the action usage function keyringer_action_usage { if [ "`type -t "keyringer_usage_$BASENAME"`" == "function" ]; then # Use custom action usage "keyringer_usage_$BASENAME" else # Default usage if [ "$1" == "command" ]; then echo "Usage: keyringer $BASENAME [arguments]" else echo "Usage: keyringer $BASENAME " fi fi } # Return available actions function keyringer_show_actions { ls -C $ACTIONS } # Usage function keyringer_usage { # are we're using an `ls` that supports `--color`? if ls --version > /dev/null 2>&1; then local keyrings="$(ls --color=never `dirname $CONFIG` | sed -e 's/config//' | xargs)" else local keyrings="$(ls `dirname $CONFIG` | sed -e 's/config//' | xargs)" fi printf "Keyringer $KEYRINGER_VERSION\n" printf "Usage: keyringer [arguments]\n\n" # Display only when not in a keyring context if [ ! -z "$keyrings" ] && [ -z "$1" ]; then printf "Available keyrings: %s \n" "$keyrings" fi # Show available actions printf "Available actions: \n\n" if [ ! -z "$keyrings" ] && [ -z "$1" ]; then printf "\tinit [remote]\n" $BASENAME fi # \t is a GNU extension # https://stackoverflow.com/questions/8400602/sed-replace-literal-tab keyringer_show_actions | sed -e "`printf 's/^/\t/'`" printf "\n" } # Check repository integrity function keyringer_check_repository { # Check if it's a git repository if [ ! -d "$BASEDIR/.git" ]; then echo "Fatal: not a git repository: $BASEDIR" exit 1 fi # Git maintenance operations echo "Running git maintenance operations..." keyringer_exec git "$BASEDIR" fsck keyringer_exec git "$BASEDIR" gc echo "" # Sync the repository if [ "`keyringer_exec git "$BASEDIR" remote | wc -l`" != "0" ]; then echo "Syncing git repository..." keyringer_exec git "$BASEDIR" pull echo "" fi } # Receive keys from keyservers # TODO: gpg-maintenance trickery # TODO: should be controlled by user preference function keyringer_recv_keys { local recipient="$1" echo "Trying to receive missing key $recipient..." gpg --batch --recv-keys "$recipient" } # Refresh keys from keyserver # TODO: gpg-maintenance trickery # TODO: should be controlled by user preference function keyringer_refresh_keys { local recipient="$1" echo "Trying to refresh key $recipient..." gpg --batch --recv-keys "$recipient" } # Check recipient size function keyringer_check_recipient_size { local recipient="$1" local size=$(echo "$recipient" | wc -c) if (( $size < 41 )); then echo "Fatal: please set the full OpenPGP fingerprint for key ID $recipient:" cat <<-EOF Please provide a full OpenPGP fingerprint, for example: john@doe.com ABCD1234ABCD12345678ABCD1234ABCD12345678 Short key ids (for example, DEADBEEF or DECAF123) are not allowed in recipient files because they are easy to spoof. Researchers have proven that it is possible to build fake keys to match any possible short key id by using a few gigabytes of disk space, and a day of computation on common hardware. Otherwise, the encryption can be broken, if someone spoofs a short key id, and causes a participant in a keyringer repository to encrypt secrets to a fake key. EOF exit 1 fi } # Check recipients function keyringer_check_recipients { # Shall we check recipients? if [ "$KEYRINGER_CHECK_RECIPIENTS" == "false" ]; then return fi # Local variables local processed=":" # Check if recipients file is empty. if [ "`grep -vE "^#|^$" "$RECIPIENTS"/* | wc -l`" == 0 ] && [ "$SUBCOMMAND" != "edit" ]; then echo "Fatal: no recipients configured for this keyring." echo "Please edit your recipients file first." exit 1 fi # Check recipients header for updates. if grep -qe ' XXXXXXXX$' "$RECIPIENTS"/*; then echo "Updating recipients file..." sed -i -e 's/ XXXXXXXX$/ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/' "$RECIPIENTS"/* fi #if [ "$1" == "edit" ]; then # # Don't do the other checks at edit mode. # return #fi for recipient in $(cat "$RECIPIENTS"/* | grep -v '^#' | awk '{ print $2 }'); do # Process a recipient just once if echo $processed | grep -q "$recipient:"; then continue else processed="$processed$recipient:" fi # Check recipient size keyringer_check_recipient_size "$recipient" # Check if key is present keyringer_check_recipient_key "$recipient" # Refresh keys if [ "$BASENAME" == "check" ] && [ "$refresh" != "no" ]; then keyringer_refresh_keys "$recipient" echo "" fi # Check key expiration keyringer_check_expiration "$recipient" done } # Check if a key is present function keyringer_check_recipient_key { local recipient="$1" gpg --list-key "$recipient" &> /dev/null if [ "$?" != "0" ]; then if [ "$BASENAME" == "check" ]; then refresh="no" keyringer_recvs_keys "$recipient" if [ "$?" != 0 ]; then echo "Error fetching $recipient from keyservers." continue fi echo "" else echo "Fatal: no such key $recipient on your GnuPG keyring." echo "Please retrieve this key yourself or fix the recipient file." exit 1 fi fi } # Check key expiration function keyringer_check_expiration { # Variables local recipient="$1" local not_expired="0" # Current date seconds="`date +%s`" # Check the main key expiry="`gpg --with-colons --fixed-list-mode --list-keys "$recipient" | grep ^pub | head -n1 | cut -d : -f 7`" # TODO: Time to expire can be configured via repository options. ahead="$((86400 * 30 + $seconds))" # Check if key is expired if [ ! -z "$expiry" ] && [[ "$seconds" -gt "$expiry" ]]; then echo -n "Warning: primary key for $recipient expired on `date --date="@$expiry"`" if [ "$KEYRINGER_MODE" == "write" ] || [ "$KEYRINGER_MODE" == "readwrite" ]; then echo ", aborting." exit 1 fi echo "" return 1 fi # Check if key is about to expire # TODO: Users can be alerted by mail if configured by user preferences. # TODO: Outgoing emails can be encrypted. if [ "$BASENAME" == "check" ] && [ ! -z "$expiry" ] && [[ "$ahead" -gt "$expiry" ]]; then echo "Warning: key $recipient will expire soon, on `date --date="@$expiry"`" fi # Check the subkeys local subkey="" for subkey in $(gpg --with-colons --fixed-list-mode --list-keys "$recipient" | grep ^sub); do local expiry=$(cut -d : -f 7 <<< "$subkey") if [[ -z "$expiry" ]]; then not_expired=1 fi if [[ "$seconds" -lt "$expiry" ]]; then not_expired="1" if [[ "$ahead" -gt "$expiry" ]] && [ "$BASENAME" == "check" ]; then echo "Warning: subkey from $recipient will expire soon, on `date --date="@$expiry"`" fi fi done # All subkeys are expired if [ ! -z "$subkey" ] && [ "$not_expired" != "1" ]; then echo -n "Warning: key $recipient has no keys suitable for encryption: all subkeys expired." if [ "$KEYRINGER_MODE" == "write" ] || [ "$KEYRINGER_MODE" == "readwrite" ]; then echo ", aborting." exit 1 fi echo "" return 1 fi } # Set recipients function keyringer_set_recipients { if [ -z "$1" ]; then keyringer_set_default_recipients else candidate="$1" candidate_no_extension="`echo $1 | sed -e 's/.asc$//'`" # Find the first matching recipient while [ ! -z "$candidate" ] && [ "$candidate" != "." ] && [ "$candidate" != "/" ] && [ "$candidate" != "/." ]; do if [ -e "$RECIPIENTS/$candidate" ]; then RECIPIENTS_FILE="$RECIPIENTS/$candidate" RECIPIENTS_FILE_BASE="$RECIPIENTS_BASE/$candidate" return elif [ -e "$RECIPIENTS/$candidate_no_extension" ]; then RECIPIENTS_FILE="$RECIPIENTS/$candidate_no_extension" RECIPIENTS_FILE_BASE="$RECIPIENTS_BASE/$candidate_no_extension" return fi candidate="`dirname $candidate`" done keyringer_set_default_recipients "$1" fi } # Set default recipients function keyringer_set_default_recipients { if [ -e "$RECIPIENTS/default" ]; then RECIPIENTS_FILE="$RECIPIENTS/default" RECIPIENTS_FILE_BASE="$RECIPIENTS_BASE/default" else echo "Fatal: no suitable recipient file found for path $1" exit 1 fi } # Set a new recipient, avoid file checks function keyringer_set_new_recipients { if [ -z "$1" ]; then keyringer_set_default_recipients else RECIPIENTS_FILE="$RECIPIENTS/$1" RECIPIENTS_FILE_BASE="$RECIPIENTS_BASE/$1" fi } # Create a new recipients file function keyringer_create_new_recipients { local recipients="$1" local recipient local key local uid local fpr if [ ! -e "$recipients" ]; then mkdir -p "`dirname $recipients`" echo "# Use entries in the form of 'john@doe.com XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'" > "$1" echo "" >> "$recipients" # Try to get an initial recipient if [ -e "$HOME/.gnupg/gpg.conf" ]; then recipient="`grep -e "^default-key" ~/.gnupg/gpg.conf | cut -d ' ' -f 2`" if [ ! -z "$recipient" ]; then key="`gpg --fingerprint --with-colons $recipient 2> /dev/null`" if [ "$?" == "0" ]; then fpr="`echo "$key" | grep -e '^fpr:' | head -1 | cut -d : -f 10`" uid="`echo "$key" | grep -e '^uid:' | head -1 | cut -d : -f 10 | sed -e 's|^[^<]*<||' -e 's|>$||'`" if [ ! -z "$uid" ] && [ ! -z "$fpr" ]; then echo "Default key $fpr ($uid) found at ~/.gnupg/gpg.conf, using as initial recipient." echo "$uid $fpr" >> "$recipients" fi fi fi fi fi } # Setup environment if [ "$(basename "$0")" != "keyringer" ]; then keyringer_config_load_preferences keyringer_set_env $* fi