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 }