github.com/StackExchange/blackbox/v2@v2.0.1-0.20220331193400-d84e904973ab/bin/_blackbox_common.sh (about)

     1  #!/usr/bin/env bash
     2  
     3  #
     4  # Common constants and functions used by the blackbox_* utilities.
     5  #
     6  
     7  # Usage:
     8  #
     9  #   set -e
    10  #   source "${0%/*}/_blackbox_common.sh"
    11  
    12  # Load additional useful functions
    13  source "${0%/*}"/_stack_lib.sh
    14  
    15  # Where are we?
    16  : "${BLACKBOX_HOME:="$(cd "${0%/*}" ; pwd)"}" ;
    17  
    18  # What are the candidates for the blackbox data directory?
    19  #
    20  # The order of candidates matter. The first entry of the array
    21  # sets the default Blackbox directory for all new repositories.
    22  declare -a BLACKBOXDATA_CANDIDATES
    23  BLACKBOXDATA_CANDIDATES=(
    24    '.blackbox'
    25    'keyrings/live'
    26  )
    27  
    28  # If $EDITOR is not set, set it to "vi":
    29  : "${EDITOR:=vi}" ;
    30  
    31  # Allow overriding gpg command
    32  : "${GPG:=gpg}" ;
    33  
    34  function physical_directory_of() {
    35    local d=$(dirname "$1")
    36    local f=$(basename "$1")
    37    (cd "$d" && echo "$(pwd -P | sed 's/\/$//')/$f" )
    38  }
    39  
    40  # Set REPOBASE to the top of the repository
    41  # Set VCS_TYPE to 'git', 'hg', 'svn' or 'unknown'
    42  if which >/dev/null 2>/dev/null git && git rev-parse --show-toplevel >/dev/null 2>&1 ; then
    43    VCS_TYPE=git
    44    REPOBASE=$(git rev-parse --show-toplevel)
    45  elif [ -d ".svn" ] ; then
    46    # Find topmost dir with .svn sub-dir
    47    parent=""
    48    grandparent="."
    49    while [ -d "$grandparent/.svn" ]; do
    50      parent=$grandparent
    51      grandparent="$parent/.."
    52    done
    53  
    54    REPOBASE=$(cd "$parent" ; pwd)
    55    VCS_TYPE=svn
    56  elif which >/dev/null 2>/dev/null hg && hg root >/dev/null 2>&1 ; then
    57    # NOTE: hg has to be tested last because it always "succeeds".
    58    VCS_TYPE=hg
    59    REPOBASE=$(hg root 2>/dev/null)
    60  else
    61    # We aren't in a repo at all.  Assume the cwd is the root
    62    # of the tree.
    63    VCS_TYPE=unknown
    64    REPOBASE="$(pwd)"
    65  fi
    66  export VCS_TYPE
    67  export REPOBASE=$(physical_directory_of "$REPOBASE")
    68  # FIXME: Verify this function by checking for .hg or .git
    69  # after determining what we believe to be the answer.
    70  
    71  if [[ -n "$BLACKBOX_REPOBASE" ]]; then
    72  	echo "Using custom repobase: $BLACKBOX_REPOBASE" >&2
    73  	export REPOBASE="$BLACKBOX_REPOBASE"
    74  fi
    75  
    76  if [ -z "$BLACKBOXDATA" ] ; then
    77    BLACKBOXDATA="${BLACKBOXDATA_CANDIDATES[0]}"
    78    for candidate in ${BLACKBOXDATA_CANDIDATES[@]} ; do
    79      if [ -d "$REPOBASE/$candidate" ] ; then
    80        BLACKBOXDATA="$candidate"
    81        break
    82      fi
    83    done
    84  fi
    85  
    86  KEYRINGDIR="$REPOBASE/$BLACKBOXDATA"
    87  BB_ADMINS_FILE="blackbox-admins.txt"
    88  BB_ADMINS="${KEYRINGDIR}/${BB_ADMINS_FILE}"
    89  BB_FILES_FILE="blackbox-files.txt"
    90  BB_FILES="${KEYRINGDIR}/${BB_FILES_FILE}"
    91  SECRING="${KEYRINGDIR}/secring.gpg"
    92  : "${DECRYPT_UMASK:=0022}" ;
    93  # : ${DECRYPT_UMASK:=o=} ;
    94  
    95  # Checks if $1 is 0 bytes, and if $1/keyrings
    96  # is a directory
    97  function is_blackbox_repo() {
    98    if [[ -n "$1" ]] && [[ -d "$1/keyrings" ]]; then
    99      return 0 # Yep, its a repo
   100    else
   101      return 1
   102    fi
   103  }
   104  
   105  # Return error if not on cryptlist.
   106  function is_on_cryptlist() {
   107    # Assumes $1 does NOT have the .gpg extension
   108    file_contains_line "$BB_FILES" "$(vcs_relative_path "$1")"
   109  }
   110  
   111  # Exit with error if a file exists.
   112  function fail_if_exists() {
   113    if [[ -f "$1" ]]; then
   114      echo ERROR: "$1" exists.  "$2" >&2
   115      echo Exiting... >&2
   116      exit 1
   117    fi
   118  }
   119  
   120  # Exit with error if a file is missing.
   121  function fail_if_not_exists() {
   122    if [[ ! -f "$1" ]]; then
   123      echo ERROR: "$1" not found.  "$2" >&2
   124      echo Exiting... >&2
   125      exit 1
   126    fi
   127  }
   128  
   129  # Exit we we aren't in a VCS repo.
   130  function fail_if_not_in_repo() {
   131    if [[ $VCS_TYPE = "unknown" ]]; then
   132      echo "ERROR: This must be run in a VCS repo: git, hg, or svn." >&2
   133      echo Exiting... >&2
   134      exit 1
   135    fi
   136  }
   137  
   138  # Exit with error if filename is not registered on blackbox list.
   139  function fail_if_not_on_cryptlist() {
   140    # Assumes $1 does NOT have the .gpg extension
   141  
   142    local name="$1"
   143  
   144    if ! is_on_cryptlist "$name" ; then
   145      echo "ERROR: $name not found in $BB_FILES" >&2
   146      echo "PWD=$(/usr/bin/env pwd)" >&2
   147      echo 'Exiting...' >&2
   148      exit 1
   149    fi
   150  }
   151  
   152  # Exit with error if keychain contains secret keys.
   153  function fail_if_keychain_has_secrets() {
   154    if [[ -s ${SECRING} ]]; then
   155      echo 'ERROR: The file' "$SECRING" 'should be empty.' >&2
   156      echo 'Did someone accidentally add this private key to the ring?' >&2
   157      echo 'Exiting...' >&2
   158      exit 1
   159    fi
   160  }
   161  
   162  function get_pubring_path() {
   163    if [[ -f "${KEYRINGDIR}/pubring.gpg" ]]; then
   164      echo "${KEYRINGDIR}/pubring.gpg"
   165    else
   166      echo "${KEYRINGDIR}/pubring.kbx"
   167    fi
   168  }
   169  
   170  # Output the unencrypted filename.
   171  function get_unencrypted_filename() {
   172    echo "$(dirname "$1")/$(basename "$1" .gpg)" | sed -e 's#^\./##'
   173  }
   174  
   175  # Output the encrypted filename.
   176  function get_encrypted_filename() {
   177    echo "$(dirname "$1")/$(basename "$1" .gpg).gpg" | sed -e 's#^\./##'
   178  }
   179  
   180  # Prepare keychain for use.
   181  function prepare_keychain() {
   182    local keyringasc
   183    echo '========== Importing keychain: START' >&2
   184    # Works with gpg 2.0
   185    #$GPG --import "$(get_pubring_path)" 2>&1 | egrep -v 'not changed$' >&2
   186    # Works with gpg 2.0 and 2.1
   187    # NB: We must export the keys to a format that can be imported.
   188    make_self_deleting_tempfile keyringasc
   189    export LANG="C.UTF-8"
   190  
   191    #if gpg2 is installed next to gpg like on ubuntu 16
   192    if [[ "$GPG" != "gpg2" ]]; then
   193      $GPG --export --no-default-keyring --keyring "$(get_pubring_path)" >"$keyringasc"
   194      $GPG --import "$keyringasc" 2>&1 | egrep -v 'not changed$' >&2
   195    else
   196      $GPG --keyring "$(get_pubring_path)" --export | $GPG --import
   197    fi
   198  
   199    echo '========== Importing keychain: DONE' >&2
   200  }
   201  
   202  # Add file to list of encrypted files.
   203  function add_filename_to_cryptlist() {
   204    # If the name is already on the list, this is a no-op.
   205    # However no matter what the datestamp is updated.
   206    
   207    # https://github.com/koalaman/shellcheck/wiki/SC2155
   208    local name
   209    name=$(vcs_relative_path "$1")
   210  
   211    if file_contains_line "$BB_FILES" "$name" ; then
   212      echo "========== File is registered. No need to add to list."
   213    else
   214      echo "========== Adding file to list."
   215      touch "$BB_FILES"
   216      echo "$name" >> "$BB_FILES"
   217      sort -u -o "$BB_FILES" "$BB_FILES"
   218    fi
   219  }
   220  
   221  # Removes a file from the list of encrypted files
   222  function remove_filename_from_cryptlist() {
   223    # If the name is not already on the list, this is a no-op.
   224  
   225    # https://github.com/koalaman/shellcheck/wiki/SC2155
   226    local name
   227    name=$(vcs_relative_path "$1")
   228  
   229    if ! file_contains_line "$BB_FILES" "$name" ; then
   230      echo "========== File is not registered. No need to remove from list."
   231    else
   232      echo "========== Removing file from list."
   233      remove_line "$BB_FILES" "$name"
   234    fi
   235  }
   236  
   237  # Print out who the current BB ADMINS are:
   238  function disclose_admins() {
   239    echo "========== blackbox administrators are:"
   240    cat "$BB_ADMINS"
   241  }
   242  
   243  # Encrypt file, overwriting .gpg if it exists.
   244  function encrypt_file() {
   245    local unencrypted
   246    local encrypted
   247    unencrypted="$1"
   248    encrypted="$2"
   249  
   250    echo "========== Encrypting: $unencrypted" >&2
   251    $GPG --use-agent --yes --trust-model=always --encrypt -o "$encrypted"  $(awk '{ print "-r" $1 }' < "$BB_ADMINS") "$unencrypted" >&2
   252    echo '========== Encrypting: DONE' >&2
   253  }
   254  
   255  # Decrypt .gpg file, asking "yes/no" before overwriting unencrypted file.
   256  function decrypt_file() {
   257    local encrypted
   258    local unencrypted
   259    local old_umask
   260    encrypted="$1"
   261    unencrypted="$2"
   262  
   263    echo "========== EXTRACTING $unencrypted" >&2
   264  
   265    old_umask=$(umask)
   266    umask "$DECRYPT_UMASK"
   267    $GPG --use-agent -q --decrypt -o "$unencrypted" "$encrypted" >&2
   268    umask "$old_umask"
   269  }
   270  
   271  # Decrypt .gpg file, overwriting unencrypted file if it exists.
   272  function decrypt_file_overwrite() {
   273    local encrypted
   274    local unencrypted
   275    local old_hash
   276    local new_hash
   277    local old_umask
   278    encrypted="$1"
   279    unencrypted="$2"
   280  
   281    if [[ -f "$unencrypted" ]]; then
   282      old_hash=$(md5sum_file "$unencrypted")
   283    else
   284      old_hash=unmatchable
   285    fi
   286  
   287    old_umask=$(umask)
   288    umask "$DECRYPT_UMASK"
   289    $GPG --use-agent --yes -q --decrypt -o "$unencrypted" "$encrypted" >&2
   290    umask "$old_umask"
   291  
   292    new_hash=$(md5sum_file "$unencrypted")
   293    if [[ "$old_hash" != "$new_hash" ]]; then
   294      echo "========== EXTRACTED $unencrypted" >&2
   295    fi
   296  }
   297  
   298  # Shred a file.  If shred binary does not exist, delete it.
   299  function shred_file() {
   300    local name
   301    local CMD
   302    local OPT
   303    name="$1"
   304  
   305    if which shred >/dev/null 2>/dev/null ; then
   306      CMD=shred
   307      OPT=-u
   308    elif which srm >/dev/null 2>/dev/null ; then
   309      #NOTE: srm by default uses 35-pass Gutmann algorithm
   310      CMD=srm
   311      OPT=-f
   312    elif _F=$(mktemp); rm -P "${_F}" >/dev/null 2>/dev/null ; then
   313      CMD=rm
   314      OPT=-Pf
   315    else
   316      echo "shred_file: WARNING: No secure deletion utility (shred or srm) present; using insecure rm" >&2
   317      CMD=rm
   318      OPT=-f
   319    fi
   320  
   321    $CMD $OPT -- "$name"
   322  }
   323  
   324  # $1 is the name of a file that contains a list of files.
   325  # For each filename, output the individual subdirectories
   326  # leading up to that file. i.e. one one/two one/two/three
   327  function enumerate_subdirs() {
   328    local listfile
   329    local dir
   330    local filename
   331    listfile="$1"
   332  
   333    while read filename; do
   334      dir=$(dirname "$filename")
   335      while [[ $dir != '.' && $dir != '/' ]]; do
   336        echo "$dir"
   337        dir=$(dirname "$dir")
   338      done
   339    done <"$listfile" | sort -u
   340  }
   341   
   342  
   343  # chdir to the base of the repo.
   344  function change_to_vcs_root() {
   345    # if vcs_root not explicitly defined, use $REPOBASE
   346  
   347    local rbase=${1:-$REPOBASE} # use $1 but if unset use $REPOBASE
   348  
   349    cd "$rbase"
   350  
   351  }
   352  
   353  # $1 is a string pointing to a directory.  Outputs a
   354  # list of  valid blackbox repos,relative to $1
   355  function enumerate_blackbox_repos() {
   356    if [[ -z "$1" ]]; then
   357      echo "enumerate_blackbox_repos: ERROR: No Repo provided to Enumerate"
   358      exit 1
   359    fi
   360  
   361    # https://github.com/koalaman/shellcheck/wiki/Sc2045
   362    for dir in $1*/; do
   363      if is_blackbox_repo "$dir"; then
   364        echo "$dir"
   365      fi
   366    done
   367  }
   368  
   369  # Output the path of a file relative to the repo base
   370  function vcs_relative_path() {
   371    # Usage: vcs_relative_path file
   372    local name="$1"
   373    #python -c 'import os ; print(os.path.relpath("'"$(pwd -P)"'/'"$name"'", "'"$REPOBASE"'"))'
   374    local p=$( printf "%s" "$( pwd -P )/${1}" | sed 's#//*#/#g' )
   375    local name="${p#$REPOBASE}"
   376    name=$( printf "%s" "$name" | sed 's#^/##g' | sed 's#/$##g' )
   377    printf "%s" "$name"
   378  }
   379  
   380  # Removes a line from a text file
   381  function remove_line() {
   382    local tempfile
   383  
   384    make_self_deleting_tempfile tempfile
   385  
   386    # Ensure source file exists
   387    touch "$1"
   388    grep -Fsxv "$2" "$1" > "$tempfile" || true
   389  
   390    # Using cat+rm instead of cp will preserve permissions/ownership
   391    cat "$tempfile" > "$1"
   392  }
   393  
   394  # Determine if a file contains a given line
   395  function file_contains_line() {
   396    # $1: the file
   397    # $2: the line
   398    grep -xsqF "$2" "$1"
   399  }
   400  
   401  #
   402  # Portability Section:
   403  #
   404  
   405  #
   406  # Abstract the difference between Linux and Mac OS X:
   407  #
   408  
   409  function md5sum_file() {
   410    # Portably generate the MD5 hash of file $1.
   411    case $(uname -s) in
   412      Darwin | FreeBSD )
   413        md5 -r "$1" | awk '{ print $1 }'
   414        ;;
   415      NetBSD )
   416        md5 -q "$1"
   417        ;;
   418      SunOS )
   419        digest -a md5 "$1"
   420        ;;
   421      Linux | CYGWIN* | MINGW* )
   422        md5sum "$1" | awk '{ print $1 }'
   423        ;;
   424      * )
   425        echo 'ERROR: Unknown OS. Exiting. (md5sum_file)'
   426        exit 1
   427        ;;
   428    esac
   429  }
   430  
   431  function cp_permissions() {
   432    # Copy the perms of $1 onto $2 .. end.
   433    case $(uname -s) in
   434      Darwin )
   435        chmod $( stat -f '%Lp' "$1" ) "${@:2}"
   436        ;;
   437      FreeBSD | NetBSD )
   438        chmod $( stat -f '%p' "$1" | sed -e "s/^100//" ) "${@:2}"
   439        ;;
   440      SunOS )
   441        chmod $( stat -c '%a' "$1" ) "${@:2}"
   442        ;;
   443      Linux | CYGWIN* | MINGW* | SunOS )
   444        if [[ -e /etc/alpine-release ]]; then
   445          chmod $( stat -c '%a' "$1" ) "${@:2}"
   446        else
   447          chmod --reference "$1" "${@:2}"
   448        fi
   449        ;; 
   450      * )
   451        echo 'ERROR: Unknown OS. Exiting. (cp_permissions)'
   452        exit 1
   453        ;;
   454    esac
   455  }
   456  
   457  
   458  #
   459  # Abstract the difference between git and hg:
   460  #
   461  
   462  # Is this file in the current repo?
   463  function is_in_vcs() {
   464    is_in_$VCS_TYPE "$@"
   465  }
   466  # Mercurial
   467  function is_in_hg() {
   468    local filename
   469    filename="$1"
   470  
   471    if hg locate "$filename" ; then
   472      echo true
   473    else
   474      echo false
   475    fi
   476  }
   477  # Git:
   478  function is_in_git() {
   479    local filename
   480    filename="$1"
   481  
   482    if git ls-files --error-unmatch >/dev/null 2>&1 -- "$filename" ; then
   483      echo true
   484    else
   485      echo false
   486    fi
   487  }
   488  # Subversion
   489  function is_in_svn() {
   490    local filename
   491    filename="$1"
   492  
   493    if svn list "$filename" ; then
   494      echo true
   495    else
   496      echo false
   497    fi
   498  }
   499  # Perforce
   500  function is_in_p4() {
   501    local filename
   502    filename="$1"
   503  
   504    if p4 list "$filename" ; then
   505      echo true
   506    else
   507      echo false
   508    fi
   509  }
   510  # No repo
   511  function is_in_unknown() {
   512    echo true
   513  }
   514  
   515  
   516  # Add a file to the repo (but don't commit it).
   517  function vcs_add() {
   518    vcs_add_$VCS_TYPE "$@"
   519  }
   520  # Mercurial
   521  function vcs_add_hg() {
   522    hg add "$@"
   523  }
   524  # Git
   525  function vcs_add_git() {
   526    git add "$@"
   527  }
   528  # Subversion
   529  function vcs_add_svn() {
   530    svn add --parents "$@"
   531  }
   532  # Perfoce
   533  function vcs_add_p4() {
   534    p4 add "$@"
   535  }
   536  # No repo
   537  function vcs_add_unknown() {
   538    :
   539  }
   540  
   541  
   542  # Commit a file to the repo
   543  function vcs_commit() {
   544    vcs_commit_$VCS_TYPE "$@"
   545  }
   546  # Mercurial
   547  function vcs_commit_hg() {
   548    hg commit -m "$@"
   549  }
   550  # Git
   551  function vcs_commit_git() {
   552    git commit -m "$@"
   553  }
   554  # Subversion
   555  function vcs_commit_svn() {
   556    svn commit -m "$@"
   557  }
   558  # Perforce
   559  function vcs_commit_p4() {
   560    p4 submit -d "$@"
   561  }
   562  # No repo
   563  function vcs_commit_unknown() {
   564    :
   565  }
   566  
   567  
   568  # Remove file from repo, even if it was deleted locally already.
   569  # If it doesn't exist yet in the repo, it should be a no-op.
   570  function vcs_remove() {
   571    vcs_remove_$VCS_TYPE "$@"
   572  }
   573  # Mercurial
   574  function vcs_remove_hg() {
   575    hg rm -A -- "$@"
   576  }
   577  # Git
   578  function vcs_remove_git() {
   579    git rm --ignore-unmatch -f -- "$@"
   580  }
   581  # Subversion
   582  function vcs_remove_svn() {
   583    svn delete "$@"
   584  }
   585  # Perforce
   586  function vcs_remove_p4() {
   587    p4 delete "$@"
   588  }
   589  # No repo
   590  function vcs_remove_unknown() {
   591    :
   592  }
   593  
   594  # Get a path for the ignore file if possible in current vcs
   595  function vcs_ignore_file_path() {
   596    vcs_ignore_file_path_$VCS_TYPE
   597  }
   598  # Mercurial
   599  function vcs_ignore_file_path_hg() {
   600    echo "$REPOBASE/.hgignore"
   601  }
   602  # Git
   603  function vcs_ignore_file_path_git() {
   604    echo "$REPOBASE/.gitignore"
   605  }
   606  
   607  
   608  # Ignore a file in a repo.  If it was already ignored, this is a no-op.
   609  function vcs_ignore() {
   610    local file
   611    for file in "$@"; do
   612      vcs_ignore_$VCS_TYPE "$file"
   613    done
   614  }
   615  # Mercurial
   616  function vcs_ignore_hg() {
   617    vcs_ignore_generic_file "$(vcs_ignore_file_path)" "$file"
   618  }
   619  # Git
   620  function vcs_ignore_git() {
   621    vcs_ignore_generic_file "$(vcs_ignore_file_path)" "$file"
   622    git add "$REPOBASE/.gitignore"
   623  }
   624  # Subversion
   625  function vcs_ignore_svn() {
   626    svn propset svn:ignore "$file" "$(vcs_relative_path)"
   627  }
   628  # Perforce
   629  function vcs_ignore_p4() {
   630    :
   631  }
   632  # No repo
   633  function vcs_ignore_unknown() {
   634    :
   635  }
   636  # Generic - add line to file
   637  function vcs_ignore_generic_file() {
   638    local file
   639    file="$(vcs_relative_path "$2")"
   640    file="${file/\$\//}"
   641    file="$(echo "/$file" | sed 's/\([\*\?]\)/\\\1/g')"
   642    if ! file_contains_line "$1" "$file" ; then
   643      echo "$file" >> "$1"
   644      vcs_add "$1"
   645    fi
   646  }
   647  
   648  
   649  # Notice (un-ignore) a file in a repo.  If it was not ignored, this is
   650  # a no-op
   651  function vcs_notice() {
   652    local file
   653    for file in "$@"; do
   654      vcs_notice_$VCS_TYPE "$file"
   655    done
   656  }
   657  # Mercurial
   658  function vcs_notice_hg() {
   659    vcs_notice_generic_file "$REPOBASE/.hgignore" "$file"
   660  }
   661  # Git
   662  function vcs_notice_git() {
   663    vcs_notice_generic_file "$REPOBASE/.gitignore" "$file"
   664    git add "$REPOBASE/.gitignore"
   665  }
   666  # Subversion
   667  function vcs_notice_svn() {
   668    svn propdel svn:ignore "$(vcs_relative_path "$file")"
   669  }
   670  # Perforce
   671  function vcs_notice_p4() {
   672    :
   673  }
   674  # No repo
   675  function vcs_notice_unknown() {
   676    :
   677  }
   678  # Generic - remove line to file
   679  function vcs_notice_generic_file() {
   680    local file
   681    file="$(vcs_relative_path "$2")"
   682    file="${file/\$\//}"
   683    file="$(echo "/$file" | sed 's/\([\*\?]\)/\\\1/g')"
   684    if file_contains_line "$1" "$file" ; then
   685      remove_line "$1" "$file"
   686      vcs_add "$1"
   687    fi
   688    if file_contains_line "$1" "${file:1}" ; then
   689      echo "WARNING:  Found a non-absolute ignore match in $1"
   690      echo "WARNING:  Confirm the pattern is intended to only exclude $file"
   691      echo "WARNING:  If so, manually update the ignore file"
   692    fi
   693  }
   694  
   695  function gpg_agent_version_check() {
   696    if ! hash 'gpg-agent' &> /dev/null; then
   697      return 1
   698    fi
   699    local gpg_agent_version=$(gpg-agent --version | head -1 | awk '{ print $3 }' | tr -d '\n')
   700    semverLT $gpg_agent_version "2.1.0"
   701  }
   702  
   703  function gpg_agent_notice() {
   704    if [[ $(gpg_agent_version_check) == '0' && -z $GPG_AGENT_INFO ]];then
   705      echo 'WARNING: You probably want to run gpg-agent as'
   706      echo 'you will be asked for your passphrase many times.'
   707      echo 'Example: $ eval $(gpg-agent --daemon)'
   708      read -r -p 'Press CTRL-C now to stop. ENTER to continue: '
   709    fi
   710  }