#!/usr/bin/env bash # # Common constants and functions used by the blackbox_* utilities. # # Usage: # # set -e # source "${0%/*}/_blackbox_common.sh" # Load additional useful functions source "${0%/*}"/_stack_lib.sh # Where are we? : "${BLACKBOX_HOME:="$(cd "${0%/*}" ; pwd)"}" ; # What are the candidates for the blackbox data directory? declare -a BLACKBOXDATA_CANDIDATES BLACKBOXDATA_CANDIDATES=( 'keyrings/live' '.blackbox' ) # If $EDITOR is not set, set it to "vi": : "${EDITOR:=vi}" ; # Allow overriding gpg command : "${GPG:=gpg}" ; function physical_directory_of() { local d=$(dirname "$1") local f=$(basename "$1") (cd "$d" && echo "$(pwd -P | sed 's/\/$//')/$f" ) } # Set REPOBASE to the top of the repository # Set VCS_TYPE to 'git', 'hg', 'svn' or 'unknown' if which >/dev/null 2>/dev/null git && git rev-parse --show-toplevel >/dev/null 2>&1 ; then VCS_TYPE=git REPOBASE=$(git rev-parse --show-toplevel) elif [ -d ".svn" ] ; then # Find topmost dir with .svn sub-dir parent="" grandparent="." while [ -d "$grandparent/.svn" ]; do parent=$grandparent grandparent="$parent/.." done REPOBASE=$(cd "$parent" ; pwd) VCS_TYPE=svn elif which >/dev/null 2>/dev/null hg && hg root >/dev/null 2>&1 ; then # NOTE: hg has to be tested last because it always "succeeds". VCS_TYPE=hg REPOBASE=$(hg root 2>/dev/null) else # We aren't in a repo at all. Assume the cwd is the root # of the tree. VCS_TYPE=unknown REPOBASE="$(pwd)" fi export VCS_TYPE export REPOBASE=$(physical_directory_of "$REPOBASE") # FIXME: Verify this function by checking for .hg or .git # after determining what we believe to be the answer. if [[ -n "$BLACKBOX_REPOBASE" ]]; then echo "Using custom repobase: $BLACKBOX_REPOBASE" >&2 export REPOBASE="$BLACKBOX_REPOBASE" fi if [ -z "$BLACKBOXDATA" ] ; then BLACKBOXDATA="${BLACKBOXDATA_CANDIDATES[0]}" for candidate in ${BLACKBOXDATA_CANDIDATES[@]} ; do if [ -d "$REPOBASE/$candidate" ] ; then BLACKBOXDATA="$candidate" break fi done fi KEYRINGDIR="$REPOBASE/$BLACKBOXDATA" BB_ADMINS_FILE="blackbox-admins.txt" BB_ADMINS="${KEYRINGDIR}/${BB_ADMINS_FILE}" BB_FILES_FILE="blackbox-files.txt" BB_FILES="${KEYRINGDIR}/${BB_FILES_FILE}" SECRING="${KEYRINGDIR}/secring.gpg" : "${DECRYPT_UMASK:=0022}" ; # : ${DECRYPT_UMASK:=o=} ; # Checks if $1 is 0 bytes, and if $1/keyrings # is a directory function is_blackbox_repo() { if [[ -n "$1" ]] && [[ -d "$1/keyrings" ]]; then return 0 # Yep, its a repo else return 1 fi } # Return error if not on cryptlist. function is_on_cryptlist() { # Assumes $1 does NOT have the .gpg extension file_contains_line "$BB_FILES" "$(vcs_relative_path "$1")" } # Exit with error if a file exists. function fail_if_exists() { if [[ -f "$1" ]]; then echo ERROR: "$1" exists. "$2" >&2 echo Exiting... >&2 exit 1 fi } # Exit with error if a file is missing. function fail_if_not_exists() { if [[ ! -f "$1" ]]; then echo ERROR: "$1" not found. "$2" >&2 echo Exiting... >&2 exit 1 fi } # Exit we we aren't in a VCS repo. function fail_if_not_in_repo() { if [[ $VCS_TYPE = "unknown" ]]; then echo "ERROR: This must be run in a VCS repo: git, hg, or svn." >&2 echo Exiting... >&2 exit 1 fi } # Exit with error if filename is not registered on blackbox list. function fail_if_not_on_cryptlist() { # Assumes $1 does NOT have the .gpg extension local name="$1" if ! is_on_cryptlist "$name" ; then echo "ERROR: $name not found in $BB_FILES" >&2 echo "PWD=$(/bin/pwd)" >&2 echo 'Exiting...' >&2 exit 1 fi } # Exit with error if keychain contains secret keys. function fail_if_keychain_has_secrets() { if [[ -s ${SECRING} ]]; then echo 'ERROR: The file' "$SECRING" 'should be empty.' >&2 echo 'Did someone accidentally add this private key to the ring?' >&2 echo 'Exiting...' >&2 exit 1 fi } function get_pubring_path() { if [[ -f "${KEYRINGDIR}/pubring.gpg" ]]; then echo "${KEYRINGDIR}/pubring.gpg" else echo "${KEYRINGDIR}/pubring.kbx" fi } # Output the unencrypted filename. function get_unencrypted_filename() { echo "$(dirname "$1")/$(basename "$1" .gpg)" | sed -e 's#^\./##' } # Output the encrypted filename. function get_encrypted_filename() { echo "$(dirname "$1")/$(basename "$1" .gpg).gpg" | sed -e 's#^\./##' } # Prepare keychain for use. function prepare_keychain() { local keyringasc echo '========== Importing keychain: START' >&2 # Works with gpg 2.0 #$GPG --import "$(get_pubring_path)" 2>&1 | egrep -v 'not changed$' >&2 # Works with gpg 2.0 and 2.1 # NB: We must export the keys to a format that can be imported. make_self_deleting_tempfile keyringasc export LANG="C.UTF-8" $GPG --export --no-default-keyring --keyring "$(get_pubring_path)" >"$keyringasc" $GPG --import "$keyringasc" 2>&1 | egrep -v 'not changed$' >&2 echo '========== Importing keychain: DONE' >&2 } # Add file to list of encrypted files. function add_filename_to_cryptlist() { # If the name is already on the list, this is a no-op. # However no matter what the datestamp is updated. # https://github.com/koalaman/shellcheck/wiki/SC2155 local name name=$(vcs_relative_path "$1") if file_contains_line "$BB_FILES" "$name" ; then echo "========== File is registered. No need to add to list." else echo "========== Adding file to list." touch "$BB_FILES" sort -u -o "$BB_FILES" <(echo "$name") "$BB_FILES" fi } # Removes a file from the list of encrypted files function remove_filename_from_cryptlist() { # If the name is not already on the list, this is a no-op. # https://github.com/koalaman/shellcheck/wiki/SC2155 local name name=$(vcs_relative_path "$1") if ! file_contains_line "$BB_FILES" "$name" ; then echo "========== File is not registered. No need to remove from list." else echo "========== Removing file from list." remove_line "$BB_FILES" "$name" fi } # Print out who the current BB ADMINS are: function disclose_admins() { echo "========== blackbox administrators are:" cat "$BB_ADMINS" } # Encrypt file, overwriting .gpg if it exists. function encrypt_file() { local unencrypted local encrypted unencrypted="$1" encrypted="$2" echo "========== Encrypting: $unencrypted" >&2 $GPG --use-agent --yes --trust-model=always --encrypt -o "$encrypted" $(awk '{ print "-r" $1 }' < "$BB_ADMINS") "$unencrypted" >&2 echo '========== Encrypting: DONE' >&2 } # Decrypt .gpg file, asking "yes/no" before overwriting unencrypted file. function decrypt_file() { local encrypted local unencrypted local old_umask encrypted="$1" unencrypted="$2" echo "========== EXTRACTING $unencrypted" >&2 old_umask=$(umask) umask "$DECRYPT_UMASK" $GPG --use-agent -q --decrypt -o "$unencrypted" "$encrypted" >&2 umask "$old_umask" } # Decrypt .gpg file, overwriting unencrypted file if it exists. function decrypt_file_overwrite() { local encrypted local unencrypted local old_hash local new_hash local old_umask encrypted="$1" unencrypted="$2" if [[ -f "$unencrypted" ]]; then old_hash=$(md5sum_file "$unencrypted") else old_hash=unmatchable fi old_umask=$(umask) umask "$DECRYPT_UMASK" $GPG --use-agent --yes -q --decrypt -o "$unencrypted" "$encrypted" >&2 umask "$old_umask" new_hash=$(md5sum_file "$unencrypted") if [[ "$old_hash" != "$new_hash" ]]; then echo "========== EXTRACTED $unencrypted" >&2 fi } # Shred a file. If shred binary does not exist, delete it. function shred_file() { local name local CMD local OPT name="$1" if which shred >/dev/null 2>/dev/null ; then CMD=shred OPT=-u elif which srm >/dev/null 2>/dev/null ; then #NOTE: srm by default uses 35-pass Gutmann algorithm CMD=srm OPT=-f elif _F=$(mktemp); rm -P "${_F}" >/dev/null 2>/dev/null ; then CMD=rm OPT=-Pf else echo "shred_file: WARNING: No secure deletion utility (shred or srm) present; using insecure rm" >&2 CMD=rm OPT=-f fi $CMD $OPT -- "$name" } # $1 is the name of a file that contains a list of files. # For each filename, output the individual subdirectories # leading up to that file. i.e. one one/two one/two/three function enumerate_subdirs() { local listfile local dir local filename listfile="$1" while read filename; do dir=$(dirname "$filename") while [[ $dir != '.' && $dir != '/' ]]; do echo "$dir" dir=$(dirname "$dir") done done <"$listfile" | sort -u } # chdir to the base of the repo. function change_to_vcs_root() { # if vcs_root not explicitly defined, use $REPOBASE local rbase=${1:-$REPOBASE} # use $1 but if unset use $REPOBASE cd "$rbase" } # $1 is a string pointing to a directory. Outputs a # list of valid blackbox repos,relative to $1 function enumerate_blackbox_repos() { if [[ -z "$1" ]]; then echo "enumerate_blackbox_repos: ERROR: No Repo provided to Enumerate" exit 1 fi # https://github.com/koalaman/shellcheck/wiki/Sc2045 for dir in $1*/; do if is_blackbox_repo "$dir"; then echo "$dir" fi done } # Output the path of a file relative to the repo base function vcs_relative_path() { # Usage: vcs_relative_path file local name="$1" #python -c 'import os ; print(os.path.relpath("'"$(pwd -P)"'/'"$name"'", "'"$REPOBASE"'"))' local p=$( printf "%s" "$( pwd -P )/${1}" | sed 's#//*#/#g' ) local name="${p#$REPOBASE}" name=$( printf "%s" "$name" | sed 's#^/##g' | sed 's#/$##g' ) printf "%s" "$name" } # Removes a line from a text file function remove_line() { local tempfile make_self_deleting_tempfile tempfile # Ensure source file exists touch "$1" grep -Fsxv "$2" "$1" > "$tempfile" || true # Using cat+rm instead of cp will preserve permissions/ownership cat "$tempfile" > "$1" } # Determine if a file contains a given line function file_contains_line() { # $1: the file # $2: the line grep -xsqF "$2" "$1" } # # Portability Section: # # # Abstract the difference between Linux and Mac OS X: # function md5sum_file() { # Portably generate the MD5 hash of file $1. case $(uname -s) in Darwin | FreeBSD ) md5 -r "$1" | awk '{ print $1 }' ;; Linux | CYGWIN* | MINGW* ) md5sum "$1" | awk '{ print $1 }' ;; * ) echo 'ERROR: Unknown OS. Exiting. (md5sum_file)' exit 1 ;; esac } function cp_permissions() { # Copy the perms of $1 onto $2 .. end. case $(uname -s) in Darwin ) chmod $( stat -f '%p' "$1" ) "${@:2}" ;; FreeBSD ) chmod $( stat -f '%p' "$1" | sed -e "s/^100//" ) "${@:2}" ;; Linux | CYGWIN* | MINGW* ) if [[ -e /etc/alpine-release ]]; then chmod $( stat -c '%a' "$1" ) "${@:2}" else chmod --reference "$1" "${@:2}" fi ;; * ) echo 'ERROR: Unknown OS. Exiting. (cp_permissions)' exit 1 ;; esac } # # Abstract the difference between git and hg: # # Is this file in the current repo? function is_in_vcs() { is_in_$VCS_TYPE "$@" } # Mercurial function is_in_hg() { local filename filename="$1" if hg locate "$filename" ; then echo true else echo false fi } # Git: function is_in_git() { local filename filename="$1" if git ls-files --error-unmatch >/dev/null 2>&1 -- "$filename" ; then echo true else echo false fi } # Subversion function is_in_svn() { local filename filename="$1" if svn list "$filename" ; then echo true else echo false fi } # Perforce function is_in_p4() { local filename filename="$1" if p4 list "$filename" ; then echo true else echo false fi } # No repo function is_in_unknown() { echo true } # Add a file to the repo (but don't commit it). function vcs_add() { vcs_add_$VCS_TYPE "$@" } # Mercurial function vcs_add_hg() { hg add "$@" } # Git function vcs_add_git() { git add "$@" } # Subversion function vcs_add_svn() { svn add --parents "$@" } # Perfoce function vcs_add_p4() { p4 add "$@" } # No repo function vcs_add_unknown() { : } # Commit a file to the repo function vcs_commit() { vcs_commit_$VCS_TYPE "$@" } # Mercurial function vcs_commit_hg() { hg commit -m "$@" } # Git function vcs_commit_git() { git commit -m "$@" } # Subversion function vcs_commit_svn() { svn commit -m "$@" } # Perforce function vcs_commit_p4() { p4 submit -d "$@" } # No repo function vcs_commit_unknown() { : } # Remove file from repo, even if it was deleted locally already. # If it doesn't exist yet in the repo, it should be a no-op. function vcs_remove() { vcs_remove_$VCS_TYPE "$@" } # Mercurial function vcs_remove_hg() { hg rm -A -- "$@" } # Git function vcs_remove_git() { git rm --ignore-unmatch -f -- "$@" } # Subversion function vcs_remove_svn() { svn delete "$@" } # Perforce function vcs_remove_p4() { p4 delete "$@" } # No repo function vcs_remove_unknown() { : } # Get a path for the ignore file if possible in current vcs function vcs_ignore_file_path() { vcs_ignore_file_path_$VCS_TYPE } # Mercurial function vcs_ignore_file_path_hg() { echo "$REPOBASE/.hgignore" } # Git function vcs_ignore_file_path_git() { echo "$REPOBASE/.gitignore" } # Ignore a file in a repo. If it was already ignored, this is a no-op. function vcs_ignore() { local file for file in "$@"; do vcs_ignore_$VCS_TYPE "$file" done } # Mercurial function vcs_ignore_hg() { vcs_ignore_generic_file "$(vcs_ignore_file_path)" "$file" } # Git function vcs_ignore_git() { vcs_ignore_generic_file "$(vcs_ignore_file_path)" "$file" git add "$REPOBASE/.gitignore" } # Subversion function vcs_ignore_svn() { svn propset svn:ignore "$file" "$(vcs_relative_path)" } # Perforce function vcs_ignore_p4() { : } # No repo function vcs_ignore_unknown() { : } # Generic - add line to file function vcs_ignore_generic_file() { local file file="$(vcs_relative_path "$2")" file="${file/\$\//}" file="$(echo "/$file" | sed 's/\([\*\?]\)/\\\1/g')" if ! file_contains_line "$1" "$file" ; then echo "$file" >> "$1" vcs_add "$1" fi } # Notice (un-ignore) a file in a repo. If it was not ignored, this is # a no-op function vcs_notice() { local file for file in "$@"; do vcs_notice_$VCS_TYPE "$file" done } # Mercurial function vcs_notice_hg() { vcs_notice_generic_file "$REPOBASE/.hgignore" "$file" } # Git function vcs_notice_git() { vcs_notice_generic_file "$REPOBASE/.gitignore" "$file" git add "$REPOBASE/.gitignore" } # Subversion function vcs_notice_svn() { svn propdel svn:ignore "$(vcs_relative_path "$file")" } # Perforce function vcs_notice_p4() { : } # No repo function vcs_notice_unknown() { : } # Generic - remove line to file function vcs_notice_generic_file() { local file file="$(vcs_relative_path "$2")" file="${file/\$\//}" file="$(echo "/$file" | sed 's/\([\*\?]\)/\\\1/g')" if file_contains_line "$1" "$file" ; then remove_line "$1" "$file" vcs_add "$1" fi if file_contains_line "$1" "${file:1}" ; then echo "WARNING: Found a non-absolute ignore match in $1" echo "WARNING: Confirm the pattern is intended to only exclude $file" echo "WARNING: If so, manually update the ignore file" fi } function gpg_agent_version_check() { if ! hash 'gpg-agent' &> /dev/null; then return 1 fi local gpg_agent_version=$(gpg-agent --version | head -1 | awk '{ print $3 }' | tr -d '\n') semverLT $gpg_agent_version "2.1.0" } function gpg_agent_notice() { if [[ $(gpg_agent_version_check) == '0' && -z $GPG_AGENT_INFO ]];then echo 'WARNING: You probably want to run gpg-agent as' echo 'you will be asked for your passphrase many times.' echo 'Example: $ eval $(gpg-agent --daemon)' read -r -p 'Press CTRL-C now to stop. ENTER to continue: ' fi }