2 # Desc: Create and sign a checksum 
   3 # Usage: bksum file.txt 
   4 # Input: stdin: file list (newline delimited) 
   5 #        arg(s): file paths (IFS delimited) 
   6 # Output: file containing sha256 hashes and file paths 
   7 # Depends: bash v5.1.16, date (GNU Coreutils 8.32), gpg v2.2.27, ots v0.7.0 
  10 declare -Ag appRollCall 
# Associative array for storing app status 
  11 declare -Ag fileRollCall 
# Associative array for storing file status 
  12 declare -Ag dirRollCall 
# Associative array for storing dir status 
  13 declare -ag arrPosArgs
; # positional arguments 
  14 declare -ag arrStdin
; # standard input lines 
  15 declare -ag arrInFiles
; # input files 
  17 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr 
  18 die
() { yell 
"$*"; exit 111; } # same as yell() but non-zero exit status 
  19 try
() { "$@" || die 
"cannot $*"; } # runs args as command, reports args if command fails 
  21     # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true". 
  22     # Usage: vbm "DEBUG :verbose message here" 
  27     # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date) 
  29     if [ "$opVerbose" = "true" ]; then 
  30         functionTime
="$(date --iso-8601=ns)"; # Save current time in nano seconds. 
  31         echo "[$functionTime]:$0:""$*" 1>&2;  # Display argument text. 
  35     return 0; # Function finished. 
  36 } # Displays message if opVerbose true 
  38     # Desc: Display script usage information 
  43     # Depends: GNU-coreutils 8.30 (cat) 
  46         bksum [ options ] [FILE...] 
  49         Creates sha256 checksum of files. 
  53                 Display help information. 
  55                 Display script version. 
  57                 Display debugging info. 
  59                 Define output file path. By default, the file 
  60                 name includes the full ISO-8601 timestamp 
  61                 without separators, e.g.: 
  62                     20220920T2117+0000..SHA256SUM 
  64                 Sign with GnuPG the checksum file. 
  66                 Timestamp with OpenTimestamps the checksum file 
  67                 (and GnuPG signature if -s/--sign specified). 
  69                 Same as '-t/--timestamp' except that 
  70                 OpenTimestamps will run as a background process 
  71                 until a calendar server returns a completed 
  74                 Indicate end of options. 
  78       bksum file1.txt file2.txt 
  79       bksum -v -- file1.txt file2.txt 
  80       bksum -v -t -- file1.txt file2.txt 
  81       find . -type f | bksum 
  82       find . -type f | bksum -v -s -t -- file.txt 
  85         If GNU Coreutils 8.32 `sha256sum` used, checksum file 
  86         can be verified using: 
  87         sha256sum --check 20220920T2117+0000..SHA256SUM 
  89 } # Display information on how to use this script. 
  91     # Desc: Displays script version and license information. 
  94     # Input: scriptVersion   var containing version string 
  96     # Depends: vbm(), yell, GNU-coreutils 8.30 
  99     vbm 
"DEBUG:showVersion function called." 
 103 Copyright (C) 2022 Steven Baltakatei Sandoval 
 104 License GPLv3: GNU GPL version 3 
 105 This is free software; you are free to change and redistribute it. 
 106 There is NO WARRANTY, to the extent permitted by law. 
 111     vbm 
"DEBUG:showVersion function ended." 
 112     return 0; # Function finished. 
 113 } # Display script version. 
 115     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
 116     # Usage: checkapp arg1 arg2 arg3 ... 
 118     # Input: global assoc. array 'appRollCall' 
 119     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
 120     # Depends: bash 5.0.3 
 125         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
 126             appRollCall
[$arg]="true"; 
 127             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
 129             appRollCall
[$arg]="false"; returnState
="false"; 
 133     #===Determine function return code=== 
 134     if [ "$returnState" = "true" ]; then 
 139 } # Check that app exists 
 141     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
 142     # Usage: checkfile arg1 arg2 arg3 ... 
 144     # Input: global assoc. array 'fileRollCall' 
 145     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
 146     # Output: returns 0 if app found, 1 otherwise 
 147     # Depends: bash 5.0.3 
 152         if [ -f "$arg" ]; then 
 153             fileRollCall
["$arg"]="true"; 
 154             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
 155         elif [ -z "$arg" ]; then 
 156             fileRollCall
["(no name)"]="false"; returnState
="false"; 
 158             fileRollCall
["$arg"]="false"; returnState
="false"; 
 162     #===Determine function return code=== 
 163     if [ "$returnState" = "true" ]; then 
 168 } # Check that file exists 
 170     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
 171     # Usage: checkdir arg1 arg2 arg3 ... 
 173     # Input: global assoc. array 'dirRollCall' 
 174     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
 175     # Output: returns 0 if all args are dirs; 1 otherwise 
 176     # Depends: Bash 5.0.3 
 181         if [ -z "$arg" ]; then 
 182             dirRollCall
["(Unspecified Dirname(s))"]="false"; returnState
="false"; 
 183         elif [ -d "$arg" ]; then 
 184             dirRollCall
["$arg"]="true"; 
 185             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
 187             dirRollCall
["$arg"]="false"; returnState
="false"; 
 191     #===Determine function return code=== 
 192     if [ "$returnState" = "true" ]; then 
 197 } # Check that dir exists 
 199     # Desc: Displays missing apps, files, and dirs 
 200     # Usage: displayMissing 
 202     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 203     # Output: stderr: messages indicating missing apps, file, or dirs 
 204     # Output: returns exit code 0 if nothing missing; 1 otherwise 
 205     # Depends: bash 5, checkAppFileDir() 
 206     local missingApps value appMissing missingFiles fileMissing
 
 207     local missingDirs dirMissing
 
 209     #==BEGIN Display errors== 
 210     #===BEGIN Display Missing Apps=== 
 211     missingApps
="Missing apps  :"; 
 212     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
 213     for key 
in "${!appRollCall[@]}"; do 
 214         value
="${appRollCall[$key]}"; 
 215         if [ "$value" = "false" ]; then 
 216             #echo "DEBUG:Missing apps: $key => $value"; 
 217             missingApps
="$missingApps""$key "; 
 221     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 222         echo "$missingApps" 1>&2; 
 225     #===END Display Missing Apps=== 
 227     #===BEGIN Display Missing Files=== 
 228     missingFiles
="Missing files:"; 
 229     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done 
 230     for key 
in "${!fileRollCall[@]}"; do 
 231         value
="${fileRollCall[$key]}"; 
 232         if [ "$value" = "false" ]; then 
 233             #echo "DEBUG:Missing files: $key => $value"; 
 234             missingFiles
="$missingFiles""$key "; 
 238     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 239         echo "$missingFiles" 1>&2; 
 242     #===END Display Missing Files=== 
 244     #===BEGIN Display Missing Directories=== 
 245     missingDirs
="Missing dirs:"; 
 246     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done 
 247     for key 
in "${!dirRollCall[@]}"; do 
 248         value
="${dirRollCall[$key]}"; 
 249         if [ "$value" = "false" ]; then 
 250             #echo "DEBUG:Missing dirs: $key => $value"; 
 251             missingDirs
="$missingDirs""$key "; 
 255     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 256         echo "$missingDirs" 1>&2; 
 259     #===END Display Missing Directories=== 
 261     #==END Display errors== 
 262     #==BEGIN Determine function return code=== 
 263     if [ "$appMissing" == "true" ] || 
[ "$fileMissing" == "true" ] || 
[ "$dirMissing" == "true" ]; then 
 268     #==END Determine function return code=== 
 269 } # Display missing apps, files, dirs 
 271     # Desc: Processes arguments provided to script 
 272     # Usage: processArgs "$@" 
 273     # Version: 1.0.0 (modified) 
 274     # Input: "$@"          (list of arguments provided to the function) 
 275     # Output: Sets following variables used by other functions: 
 276     #   opVerbose            Indicates verbose mode enable status.  (ex: "true", "false") 
 277     #   pathDirOut1          Path to output directory. 
 278     #   pathFileOut1         Path to output file. 
 279     #   opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false"). 
 280     #   opTs                 Indicates timestamp mode 
 281     #   opTsWait             Indicates timestamp mode with wait option 
 282     #   arrPosArgs         Array of remaining positional argments 
 284     #   yell()           Displays messages to stderr. 
 285     #   vbm()            Displays messsages to stderr if opVerbose set to "true". 
 286     #   showUsage()      Displays usage information about parent script. 
 287     #   showVersion()    Displays version about parent script. 
 288     #   arrPosArgs     Global array for storing non-option positional arguments (i.e. arguments following the `--` option). 
 289     # External dependencies: bash (5.1.16), echo 
 291     #  [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347 
 292     #  [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams 
 294     # Initialize function 
 295     vbm 
"DEBUG:processArgs function called." 
 298     while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0). 
 299         #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1]. 
 300         #yell "DEBUG:Provided arguments are:""$*"      # Debug stderr message. See [1]. 
 302             -h | 
--help) showUsage
; exit 1;; # Display usage. 
 303             --version) showVersion
; exit 1;; # Show version 
 304             -v | 
--verbose) opVerbose
="true"; vbm 
"DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1]. 
 305             -o | 
--output-file) # Define output file path 
 306                 if [ -f "$2" ]; then # If $2 is file that exists, prompt user to continue to overwrite, set pathFileOut1 to $2, pop $2. 
 307                     yell 
"Specified output file $2 already exists. Overwrite? (y/n):" 
 308                     read -r m
; case $m in 
 309                                 y | Y | 
yes) opFileOut1_overwrite
="true";; 
 310                                 n | N | no
) opFileOut1_overwrite
="false";; 
 311                                 *) yell 
"Invalid selection. Exiting."; exit 1;; 
 313                     if [ "$opFileOut1_overwrite" == "true" ]; then 
 316                         vbm 
"DEBUG:Output file pathFileOut1 set to:""$2"; 
 318                         yell 
"ERORR:Exiting in order to not overwrite output file:""$pathFileOut1"; 
 324                     vbm 
"DEBUG:Output file pathFileOut1 set to:""$2"; 
 326             -s | 
--sign) # sign with gpg 
 327                 opSign
="true"; vbm 
"DEBUG:Signing mode enabled.";; 
 328             -t | 
--timestamp) # timestamp with ots 
 329                 opTs
="true"; vbm 
"DEBUG:Timestamp mode enabled.";; 
 330             -T | 
--timestamp-wait) # timestamp with ots with wait option 
 331                 opTs
="true"; vbm 
"DEBUG:Timestamp mode enabled."; 
 332                 opTsWait
="true"; vbm 
"DEBUG:Timestamp wait mode enabled.";; 
 333             --) # End of all options. See [2]. 
 336                     vbm 
"DEBUG:adding to arrPosArgs:$arg"; 
 337                     arrPosArgs
+=("$arg"); 
 340             -*) showUsage
; yell 
"ERROR: Unrecognized option."; exit 1;; # Display usage 
 343                     vbm 
"DEBUG:adding to arrPosArgs:$arg"; 
 344                     arrPosArgs
+=("$arg"); 
 352     vbm 
"DEBUG:processArgs function ended." 
 353     return  0; # Function finished. 
 354 } # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.). 
 356     # Desc: Save stdin lines to array 
 357     # Input:  stdin         standard input 
 358     #         arrStdin    array for storing stdin lines 
 359     # Output: arrStdin    array for storing stdin lines 
 360     # Ref/Attrib: [1] https://unix.stackexchange.com/a/484643 Check if no command line arguments and STDIN is empty 
 363         return 1; # error if file descriptor 0 (stdin) open 
 365         while read -r line
; do 
 366             arrStdin
+=("$line"); done; 
 369 }; # Save stdin to array 
 371     # Desc: Check that files (specified by positional arguments and 
 372     #   standard input lines) exist and are in the same directory. 
 373     # Input: arrPosArgs[@]    positional arguments 
 374     #        arrStdin[@]      standard input lines 
 375     # Output: return code 0     success 
 376     #         return code 1     failure 
 377     #         arrInFiles        list of verified files 
 378     # Depends: displayMissing(), checkfile(); 
 379     local flagMissing flagDirErr workDir
; 
 381     # Check that positional arguments are files 
 382     for elem 
in "${arrPosArgs[@]}"; do 
 383         if checkfile 
"$elem"; then 
 384             arrInFiles
+=("$elem"); 
 390     # Check that stdin lines are files 
 391     for elem 
in "${arrStdin[@]}"; do 
 392         if checkfile 
"$elem"; then 
 393             arrInFiles
+=("$elem"); 
 399     # Check that all files are in the same directory 
 400     if [[ "$flagMissing" != "true" ]]; then 
 401         workDir
="$( dirname "$
( readlink 
-f "${arrInFiles[0]}" )" )"; 
 402         for elem 
in "${arrInFiles[@]}"; do 
 403             elem
="$(readlink -f "$elem")"; # full path             
 404             if [[ "$(dirname "$elem")" != "$workDir" ]]; then 
 410     # Return non-zero if displayMissing() reports files missing. 
 411     if [[ "$flagMissing" == "true" ]]; then 
 412         displayMissing
; return 1; fi; 
 413     if [[ "$flagDirErr" == "true" ]]; then 
 414         yell 
"ERROR:All files not in same directory."; 
 417 }; # Check positional arguments 
 419     # Desc: Check if expected commands available 
 421     checkapp 
date sha256sum
; 
 422     if [[ $opSign == "true" ]]; then checkapp gpg
; fi; 
 423     if [[ $opTs == "true" ]]; then checkapp ots
; fi; 
 425     # Return failure is displayMissing() reports apps missing. 
 426     if ! displayMissing
; then return 1; else return 0; fi; 
 427 }; # Check dependencies 
 429     # Desc: Count and return total number of jobs 
 432     # Output: stdout   integer number of jobs 
 433     # Depends: Bash 5.1.16 
 434     # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done; 
 438     job_count
="$(jobs -r | wc -l | tr -d ' ' )"; 
 439     #yell "DEBUG:job_count:$job_count"; 
 440     if [[ -z $job_count ]]; then job_count
="0"; fi; 
 442 }; # Return number of background jobs 
 444     # Input: pathFileOut1   option-specified output file path 
 445     #        appRollCall    assoc-array for checkapp(), displayMissing() 
 446     #        fileRollCall   assoc-array for checkfile(), displayMissing() 
 447     #        dirRollCall    assoc-array for checkdir(), displayMissing() 
 448     #        arrPosArgs     array for processArgs() 
 449     #        arrStdin       array for processStdin() 
 450     #        arrInFiles     array for checkInput() 
 451     #        (args)         for processArgs() 
 452     #        (stdin)        for processStdin() 
 453     # Output: file    written to $pathSumOut 
 454     #         file    written to $pathSigOut 
 456     local fileSumOut dirOut pathSumOut pathSigOut
; 
 458     # Check dependencies and input 
 459     if ! checkDepends
; then die 
"FATAL:Missing apps."; fi; 
 462     vbm 
"DEBUG:$(declare -p arrPosArgs)"; 
 463     vbm 
"DEBUG:$(declare -p arrStdin)"; 
 464     if ! [[ "${#arrPosArgs[@]}" -ge 1 || 
"${#arrStdin[@]}" -ge 1 ]]; then 
 465         yell 
"ERROR:No positional arguments or stdin lines."; 
 466         showUsage
; exit 1; fi; 
 467     if ! checkInput
; then die 
"FATAL:Invalid file list."; fi; 
 468     vbm 
"DEBUG:$(declare -p arrInFiles)"; 
 471     if [[ -n "$pathFileOut1" ]]; then 
 472         pathSumOut
="$pathFileOut1"; 
 474         dirOut
="$( dirname "${arrInFiles[0]}" )"; 
 475         fileSumOut
="$(date +%Y%m%dT%H%M%S%z)..SHA256SUMS"; 
 476         pathSumOut
="$dirOut"/"$fileSumOut"; 
 478     pathSigOut
="$pathSumOut".asc
; 
 479     for file in "${arrInFiles[@]}"; do 
 480         sha256sum 
"$file" >> "$pathSumOut"; 
 484     ## Sign checksum file. 
 485     if [[ $opSign == "true" ]]; then 
 486         yell 
"STATUS:Signing checksum file..."; 
 487         try gpg 
--detach-sign --armor --output "$pathSigOut" "$pathSumOut" && yell 
"STATUS:Checksum created."; 
 489     ## Timestamp checksum file. 
 490     if [[ $opTs == "true" ]]; then 
 491         yell 
"STATUS:Timestamping checksum file..."; 
 492         if [[ $opTsWait != "true" ]]; then 
 493             try ots s 
"$pathSumOut" & 
 494         elif [[ $opTsWait == "true" ]]; then 
 495             yell 
"NOTICE:Waiting for calendar server response in background. (This may take 8 to 24 hours)..."; 
 496             yell 
"ADVICE:Do not close or suspend this terminal."; 
 497             try ots 
--wait s 
"$pathSumOut" 1>/dev
/random 
2>&1 & 
 500     ## Timestamp checksum signature file. 
 501     if [[ $opTs == "true" && $opSign == "true" ]]; then 
 502         yell 
"STATUS:Timestamping signature file..."; 
 503         if [[ $opTsWait != "true" ]]; then 
 504             try ots s 
"$pathSigOut" && yell 
"STATUS:Timestamp of checksum signature file created."; 
 505         elif [[ $opTsWait == "true" ]]; then 
 506             yell 
"NOTICE:Waiting for calendar server response in background. (This may take 8 to 24 hours)..."; 
 507             yell 
"ADVICE:Do not close or suspend this terminal."; 
 508             try ots 
--wait s 
"$pathSigOut" 1>/dev
/random 
2>&1 & 
 512     ## Wait until background jobs (if any) completed 
 513     for (( n 
= 1; "$(count_jobs)" > 0; n
++ )); do 
 514         if ! [[ $
((n 
% 60)) -eq 0 ]]; then 
 517             printf ".%05ds passed.\n" "$SECONDS"; 
 528 # Author: Steven Baltakatei Sandoval