3 # Date: 2020-02-18T20:06Z 
   5 # Author: Steven Baltakatei Sandoval (baltakatei.com) 
   7 # License: This bash script, 'bkpoe', is licensed under GPLv3 or 
   8 # later by Steven Baltakatei Sandoval: 
  10 #    'bkpoe', a file system proof of existence generator 
  11 #    Copyright (C) 2020  Steven Baltakatei Sandoval (baltakatei.com) 
  13 #    This program is free software: you can redistribute it and/or modify 
  14 #    it under the terms of the GNU General Public License as published by 
  15 #    the Free Software Foundation, either version 3 of the License, or 
  18 #    This program is distributed in the hope that it will be useful, 
  19 #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
  20 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  21 #    GNU General Public License for more details. 
  23 #    A copy of the GNU General Public License may be found at 
  24 #    <https://www.gnu.org/licenses/>. 
  26 # Description: This is a simple script that uses GNU Coreutils 
  27 # OpenTimestamps to generate a proof that permits a user to prove the 
  28 # existence of any individual file within a directory. A proof 
  31 #   - An OpenTimestamps 'ots' file (if '-t' option provided) 
  33 #   - Public proof: text file(s) containing a list of salted file 
  36 #   - Private proof: text file(s) containing: 
  38 #     - A list of unsalted file hashes 
  40 #     - A list of nonces used to generate the list of salted file hashes. 
  42 #     - The list of salted file hashes 
  44 #     - File attributes (size, relative path) 
  46 #   The proof is produced by performing the following steps: 
  48 #   - Use 'find' and GNU Coreutils digest algorithms to create 
  49 #     numbered lists of file digests at specified directories. 
  51 #   - Save these numbered lists of digests into text files within a 
  54 #   - If more than one specified directory is provided also process 
  55 #     the files within the proof directory. 
  57 #   - Use OpenTimestamps to create an 'ots' timestamp file 
  58 #     for the final digest list file. (if '-t' option provided). 
  60 #   - Use OpenTimestamps to upgrade each 'ots' timestamp file after a 
  61 #     delay. (if '-w' option provided). 
  63 # Dependencies: coreutils, ots. See end of file 
  67 #   - GNU/Linux Debian 10 
  70 # === VARIABLE INITIALIZATION AND FUNCTION DEFINITIONS === 
  72 PATH
="/usr/local/bin/:$PATH" # Add default OpenTimestamps path to PATH (necessary if cron used to call this script) 
  73 scriptHostname
=$
(hostname
) # Save hostname of system running this script. 
  74 OTS_TIMEOUT
="6h" # maximum time to run `ots --wait` (used via `timeout` command) 
  76 # Declare array for storing directories whose contents will be hashed. 
  77 declare -a directoriesToProcess
 
  79 # Declare arrays for storing file paths, file index, file hashes, file 
  80 # hash nonces, the salted hashes, file sizes, file paths, file 
  81 # modification times, and proof file paths. 
  82 declare -a filePathsFull      
# array 
  83 declare -a filePathsIndex     
# array 
  84 declare -a fileHashes         
# array 
  85 declare -a fileHashNonces     
# array 
  86 declare -a fileHashesSalted   
# array 
  87 declare -a fileSizes          
# array 
  88 declare -a fileModTime        
# array 
  89 declare -g PROOF_DIRECTORY    
# global 
  90 declare -ag prvProofPaths     
# array, global 
  91 declare -ag pubProofPaths     
# array, global 
  93 # Define 'echoerr' function which outputs text to stderr 
  94     # Note: function copied from https://stackoverflow.com/a/2990533 
  99 # Define script version. 
 100 SCRIPT_VERSION
="bkpoe 0.1.0" 
 103 runDate
=$
(date +%Y
%m
%dT
%H
%M
%S
%z
) 
 104 sleep 1 # Space out program run times by at least one second. 
 105 ###echoerr "DEBUG: Current date is:"$runDate 
 107 # Save working directory 
 109 ###echoerr "DEBUG: Current working directory is:"$runPath 
 111 # Define proof directory basename 
 112 PROOF_DIRECTORY_BASE
="$runDate""..""$scriptHostname""_bkpoe_proofs" 
 114 # Define default proof directory 
 115 PROOF_DIRECTORY
="$runPath"/"$PROOF_DIRECTORY_BASE" 
 117 # Print information on how to use this bash script. 
 121     echo "    bkpoe [ options ] DIRECTORIES" 
 125     echo "            Display help information." 
 127     echo "    -r, --recursive" 
 128     echo "            Perform recursive hashing of directories." 
 130     echo "    -v, --verbose" 
 131     echo "            Display debugging info." 
 133     echo "    -a, --algorithm [ algo ]" 
 134     echo "            Specify GNU Coreutils hash command. Options include:      " 
 135     echo "            b2sum md5sum sha1sum sha224sum sha256sum sha384sum        " 
 138     echo "    -m, --message [ string ]" 
 139     echo "            Specify message to be included in filehash files." 
 141     echo "    -d, --delimiter [ string ]" 
 142     echo "            Specify character to serve as delimiter in proof files.   " 
 143     echo "            (Default: ,)                                              " 
 144     echo "    -t, --timestamp" 
 145     echo "            Create openTimestmaps proof file." 
 147     echo "            Pass '--wait' option to OpenTimestamps to 'wait           " 
 148     echo "            until a complete timestamp committed in the               " 
 149     echo "            Bitcoin blockchain is available instead of                " 
 150     echo "            returning immediately.                                    " 
 152     echo "            Specify output directory to save proof. Default is working" 
 153     echo "            directory where script was run." 
 157 # Check that a string matches one of GNU Coreutils hash commands. 
 158 isValidDigestAlgo
() { 
 159     # Syntax: isValidDigestAlgo [string] 
 160     # Description: Returns 1 if [string] is a valid GNU Coreutils hash function command. Returns 0 otherwise. 
 162     # Exit if more than one argument provided. 
 163     ###echoerr "DEBUG: Arguments provided to isValidDigestAlgo() are:$@" 
 164     if [[ $# -ne 1 ]]; then 
 165         echoerr 
"ERROR: Illegal number of digest arguments provided." 
 169     # Exit if argument contains non-alphanumeric characters 
 170     if [[ "$@" =~ 
[^a-zA-Z0-9$
] ]]; then 
 171         echoerr 
"ERROR: Illegal characters for digest argument provided." 
 175     local input
=$1 #Save first argument provided to function as input for further processing. 
 177     ###echoerr "DEBUG: Starting function to check validity of string as valid digest command." 
 179     # Define array of valid hash algorithm commands present in by GNU coreutils package. List is current as of Debian Buster. List taken from https://packages.debian.org/buster/amd64/coreutils/filelist ). 
 180     local gnuCoreutilsAlgos
=(b2sum 
md5sum sha1sum sha224sum sha256sum sha384sum sha512sum
) 
 181     ###echoerr "DEBUG:Contents of \$gnuCoreutilsAlgos array is:${gnuCoreutilsAlgos[@]}" 
 183     # Make input string lowercase 
 184     #input=${input,,} # Make all letters in $1 lowercase (see https://stackoverflow.com/questions/2264428/how-to-convert-a-string-to-lower-case-in-bash ) and save as $input 
 185     ###echoerr "DEBUG: User-specified hash algorithm is:$input" 
 187     # Return true (0) if $gnuCoreutilsAlgos array contains string $input (see https://unix.stackexchange.com/a/177139 ) 
 188     for item 
in "${gnuCoreutilsAlgos[@]}"; do 
 189         ###echoerr "DEBUG: \$item is:$item" 
 190         ###echoerr "DEBUG: \$input is:$input" 
 191         if [ "$input" == "$item" ]; then 
 192             ###echoerr "DEBUG: $input present in \$gnuCoreutilsAlgos array." 
 197     # Return false (1) otherwise. 
 198     echoerr 
"DEBUG: $input not recognized as valid hash algorithm name." 
 203     # Syntax: writeProofs [ loopIndex ] [ dirPath ] 
 205     # Description: Construct private and public proofs for files 
 206     # contained within [ dirPath ]. Save file with date, [ loopIndex ], 
 207     # and base directory name within filename. 
 209     # Required variables: 
 210     #  - $runDate: used in creating proof directory 
 211     #  - $PROOF_DIRECTORY: used as directory where proof files are saved 
 212     #  - $OPTION_ALGORITHM: used to decide which hash algorithm to use 
 213     #  - $OPTION_DELIMITER: used to determine which char to use as a delimiter in proof files 
 214     #  - $OPTION_MESSAGE: used to determine message included in proof files 
 215     #  - $OPTION_RECURSIVE: used to determine if files within subdirectories under [ dir Path ] directory should be included 
 217     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG:This is the writeProofs() function. Provided arguments are:$@" 
 219     # Determine scope of 'find' file search (recursive search or not) 
 220     if [[ $OPTION_RECURSIVE == "true" ]]; then 
 221         [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: Performing recursive file search on directory $directoryToHash" 
 222         findMaxDepthOption
=""  # Do not specify -maxdepth option for find (causes 'find' to include all files recursively within [ dirPath ] 
 224         findMaxDepthOption
="-maxdepth 1" # Specify "-maxdepth 1 " as option for 'find' so only files immediately within [ dirPath ] directory are included (no subdirectories). 
 227     # Save current date of loop 
 228     proofLoopRunDate
=$
(date +%Y
%m
%dT
%H
%M
%S
%z
) 
 229     ###echoerr "DEBUG: Date and time of current loop is:"$proofLoopRunDate 
 240     # Pass first argument provided to function as directory index. 
 243     # Pass second argument as directory to process 
 245         [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: Valid directory argument provided to writeProofs() function." 
 246         local directoryToHash
=$2 
 247         ## local directoryToHash=$(readlink -f $2) # Use absolutely full path (not used for privacy). 
 249         echoerr 
"ERROR: Invalid directory argument provided to writeProofs() function." 
 252     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG:Processing directory:"$directoryToHash 
 253     local directoryToHashShort
="$(basename "$directoryToHash")" 
 254     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG:Directory basename:"$directoryToHashShort 
 256     # Populate filePathsFull array with file paths from 'find' results (see https://stackoverflow.com/a/54561526 ). 
 257     #   Note: For creating array from output of find via readarray, see https://unix.stackexchange.com/a/263885 
 258     #   Note: For sorting output of find, see https://unix.stackexchange.com/a/34328 
 259     readarray 
-d '' filePathsFull 
< <(find $directoryToHash $findMaxDepthOption -readable -type f 
-print0 | 
sort -z) # Populate filePathsFull array with sorted list of files present in $directoryToHash (and subdirectories if $OPTION_RECURSIVE set to 'true'. 
 260     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: The ${#filePathsFull[@]} elements of filePathsFull array are:${filePathsFull[@]}" # Show array length (see https://www.cyberciti.biz/faq/finding-bash-shell-array-length-elements/ ) 
 262     # Construct index array from filePathsFull array for the for loops used to write proofs. 
 263     filePathsIndex
=(${!filePathsFull[@]}) 
 264     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: The ${#filePathsIndex[@]} elements of filePathsIndex array are:${filePathsIndex[@]}" 
 266     # Populate fileHashes with hashes of files listed in filePathsFull array 
 267     # Iterate through all elements of filePathsFull array using an integer index. (see https://stackoverflow.com/a/6723516 ) 
 268     for i 
in "${!filePathsIndex[@]}"; do  
 269         ###[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:The $i th element of the filePathsFull array is:${filePathsFull[i]}" 
 270         fileHashes
[i
]=$
($OPTION_ALGORITHM "${filePathsFull[i]}" | 
awk '{ print $1 }') # Get hash value from GNU Coreutils hash command (see https://stackoverflow.com/a/3679803 ) 
 272     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: The ${#fileHashes[@]} elements of fileHashes array are:${fileHashes[@]}" 
 274     # Populate fileHashNonces array with same number of elements as filePathsFull array. Each element is a random hexadecimal nonce of length matching OPTION_ALGORITHM. Hex nonce generated from 64-byte (512-bits) block from /dev/urandom. 
 275     for i 
in "${!filePathsIndex[@]}"; do 
 276         fileHashNonces
[i
]=$
(dd bs
=64 count
=1 if=/dev
/urandom 
2>/dev
/null | 
$OPTION_ALGORITHM | 
awk '{ print $1 }') 
 278     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: The ${#fileHashNonces[@]} elements of fileHashNonces array are:${fileHashNonces[@]}" 
 280     # Populate fileHashesSalted array with salted digest for each file hash corresponding nonce. 
 281     for i 
in "${!filePathsIndex[@]}"; do 
 282         fileHashesSalted
[i
]=$
(echo -n "${fileHashes[i]}${fileHashNonces[i]}" | $OPTION_ALGORITHM | awk '{ print $1 }') 
 284     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: The 
${#fileHashesSalted[@]} elements of fileHashesSalted array are
:${fileHashesSalted[@]}" 
 286     # Populate fileSizes array with file sizes (in bytes). 
 287     for i in "${!filePathsIndex[@]}"; do 
 288         fileSizes[i]=$(wc -c "${filePathsFull[i]}" | awk '{ print $1 }') 
 290     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: The 
${#fileSizes[@]} elements of fileSizes array are
:${fileSizes[@]}" 
 292     # Populate fileModTime array with file modification times (in ISO-8601 format) 
 293     for i in "${!filePathsIndex[@]}"; do 
 294         fileModTime[i]=$(date --iso-8601=seconds -r "${filePathsFull[i]}" | awk '{ print $1 }') 
 296     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: The 
${#fileModTime[@]} elements of fileModTime array are
:${fileModTime[@]}" 
 298     # ==== GENERATE PROOF FILES FROM HASH LIST(S) ==== 
 300     # Define output file names and paths. 
 301     local PROOF_PRIVATE_FILENAME=$proofLoopRunDate"_
"$loopIndex"_prv..
"$scriptHostname"_
"$directoryToHashShort"_
"$OPTION_ALGORITHM"_proof_private.txt
" 
 302     local PROOF_PUBLIC_FILENAME=$proofLoopRunDate"_
"$loopIndex"_pub..
"$scriptHostname"_
"$directoryToHashShort"_
"$OPTION_ALGORITHM"_proof_public.txt
" 
 303     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: \
$PROOF_PRIVATE_FILENAME is 
$PROOF_PRIVATE_FILENAME" 
 304     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: \
$PROOF_PUBLIC_FILENAME is 
$PROOF_PUBLIC_FILENAME" 
 305     PROOF_PRIVATE_FILEPATH="$PROOF_DIRECTORY"/"$PROOF_PRIVATE_FILENAME" 
 306     PROOF_PUBLIC_FILEPATH="$PROOF_DIRECTORY"/"$PROOF_PUBLIC_FILENAME" 
 308     # Create private proof and public proof files. 
 309     touch "$PROOF_PRIVATE_FILEPATH" 
 310     touch "$PROOF_PUBLIC_FILEPATH" 
 312     # Abbreviate OPTION_DELIMITER to local variable DELIM 
 313     local DELIM=$OPTION_DELIMITER 
 315     # Define private proof first-line headers (date, version, algorithm, message) 
 316     local PROOF_PRIVATE_HEADER1="$proofLoopRunDate$DELIM$SCRIPT_VERSION$DELIM$OPTION_ALGORITHM$DELIM$OPTION_MESSAGE" 
 317     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: \
$PROOF_PRIVATE_HEADER1 is 
$PROOF_PRIVATE_HEADER1" 
 318     # Define public proof first-line headers (date, version, algorithm, message) 
 319     local PROOF_PUBLIC_HEADER1="$proofLoopRunDate$DELIM$SCRIPT_VERSION$DELIM$OPTION_ALGORITHM$DELIM$OPTION_MESSAGE" 
 320     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: \
$PROOF_PUBLIC_HEADER1 is 
$PROOF_PUBLIC_HEADER1" 
 322     # Define private proof second-line column labels (index, digest, nonce, salted digest, mdate, size, filepath) 
 323     local PROOF_PRIVATE_HEADER2="index
"$DELIM"digest
"$DELIM"nonce
"$DELIM"digest_salted
"$DELIM"mdate
"$DELIM"size_bytes
"$DELIM"file_path
" 
 324     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: \
$PROOF_PRIVATE_HEADER2 is 
$PROOF_PRIVATE_HEADER2" 
 325     # Define public proof second-line column labels (index, salted digest) 
 326     local PROOF_PUBLIC_HEADER2="index
"$DELIM"digest_salted
" 
 327     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: \
$PROOF_PUBLIC_HEADER2 is 
$PROOF_PUBLIC_HEADER2" 
 329     # Write headers to proof files 
 330     echo $PROOF_PRIVATE_HEADER1 >> "$PROOF_PRIVATE_FILEPATH" 
 331     echo $PROOF_PRIVATE_HEADER2 >> "$PROOF_PRIVATE_FILEPATH" 
 332     echo $PROOF_PUBLIC_HEADER1 >> "$PROOF_PUBLIC_FILEPATH" 
 333     echo $PROOF_PUBLIC_HEADER2 >> "$PROOF_PUBLIC_FILEPATH" 
 335     # Loop to append array contents. 
 336     for i in "${!filePathsIndex[@]}"; do 
 337         # Append to private proof: filePathsIndex[i],fileHashes[i],fileHashNonces[i],fileHashesSalted[i],fileModTime[i],fileSizes[i],filePathsFull[i] 
 338         ###[[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: Writing line to 
$PROOF_PRIVATE_FILEPATH" 
 339         echo ${filePathsIndex[i]}$DELIM${fileHashes[i]}$DELIM${fileHashNonces[i]}$DELIM${fileHashesSalted[i]}$DELIM${fileModTime[i]}$DELIM${fileSizes[i]}$DELIM${filePathsFull[i]} >> "$PROOF_PRIVATE_FILEPATH" 
 340         # Append to public proof: filePathsIndex[i],fileHashesSalted[i] 
 341         [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: Writing line to 
$PROOF_PUBLIC_FILEPATH" 
 342         echo ${filePathsIndex[i]}$DELIM${fileHashesSalted[i]} >> $PROOF_PUBLIC_FILEPATH 
 345     # Save file paths to global variables prvProofPaths and pubProofPaths to allow other functions to manipulate files. 
 346     prvProofPaths+=($PROOF_PRIVATE_FILEPATH) 
 347     pubProofPaths+=($PROOF_PUBLIC_FILEPATH) 
 348     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: The 
${#prvProofPaths[@]} elements of prvProofPaths array are
:${prvProofPaths[@]}" 
 349     [[ $OPTION_VERBOSE == "true
" ]] && echoerr "DEBUG
: The 
${#pubProofPaths[@]} elements of pubProofPaths array are
:${pubProofPaths[@]}" 
 362 # === INPUT PROCESSING === 
 363 # Check for presence of options. 
 364 ###echoerr "DEBUG
: === INPUT PROCESSING START 
===" 
 366 # Process initial option arguments (see https://jonalmeida.com/posts/2013/05/26/different-ways-to-implement-flags-in-bash/ and https://likegeeks.com/linux-bash-scripting-awesome-guide-part3/ ) 
 367 while [ ! $# -eq 0 ]   # While number of arguments ($#) is not (!) equal to (-eq) zero (0). 
 369     case "$1" in    # Check first of remaining arguments to see if it matches one of strings below. 
 371             # Code to run if $1 matched "--help" or "-h": 
 372             ###echoerr "DEBUG
: This is the 
help information.
" 
 373             usage # Run usage function to display helpful info to user. 
 377             # Code to run if $1 matched "--recursive" or "-r": 
 378             ###echoerr "DEBUG
: Recursive option activated.
" 
 379             OPTION_RECURSIVE="true
" 
 382             # Code to run if $1 matched "--verbose" or "-v": 
 383             ###echoerr "DEBUG
: Verbose option activated.
" 
 384             OPTION_VERBOSE="true
" 
 387             # Code to run if $1 matched "--algorithm" or "-a": 
 389             ###echoerr "DEBUG
: Selecting 
hash algorithm.
" 
 390             # Check that OPTION_ALGORITHM variable hasn't already been set. 
 391             # Check that OPTION_ALGORITHM contains only alphanumeric characters. 
 392             # Check that argument following '-a' or '--algorithm' ($2) is valid algorithm. 
 393             if [[ ! -v OPTION_ALGORITHM ]] && [[ ! "$2" =~ [^a-zA-Z0-9$] ]] && isValidDigestAlgo $2; then 
 394                 ###echoerr "DEBUG
: Valid 
hash algorithm provided.
" 
 395                 OPTION_ALGORITHM=$2 # Save argument following '-a' or '--algorithm' to $OPTION_ALGORITHM 
 396                 shift # Remove an additional argument so the additional algorithm argument $2 is properly removed at the end of the while loop. 
 398                 echoerr "ERROR
: Invalid 
hash algorithm argument provided
:""$2" 
 403             # Code to run if $1 matched "--message" or "-m": 
 404             ###echoerr "DEBUG
: Specifying header message.
" 
 405             # Check that OPTION_MESSAGE variable hasn't already been set. 
 406             if [[ ! -v OPTION_MESSAGE ]]; then 
 407                 OPTION_MESSAGE=$2 # Save argument following '-m' or '--message' to $OPTION_MESSAGE 
 408                 shift # Remove an additional argument so the additional algorithm argument $2 is properly removed at the end of the while loop. 
 412             # Code to run if $1 matched "--delimiter" or "-d": 
 413             ###echoerr "DEBUG
: Specifying delimiter.
" 
 414             # Check that OPTION_DELIMITER variable hasn't already been set. 
 415             if [[ ! -v OPTION_DELIMITER ]]; then 
 416                 OPTION_DELIMITER=$2 # Save argument following '-d' or '--delimiter' to $OPTION_DELIMITER 
 417                 shift # Remove an additional argument so the additional delimiter argument $2 is properly removed at the end of the while loop. 
 421             # Code to run if $1 matched "--timestamp" or "-t": 
 422             echoerr "DEBUG
: OpenTimestamps option selected.
" 
 423             # Check that ots command exists. (see https://stackoverflow.com/a/677212 ) 
 424             if command -v ots >/dev/null 2>&1 ; then 
 425                 echoerr "DEBUG
: OpenTimestampscommand 
('ots') detected.
" 
 426                 OPTION_OPENTIMESTAMPS="true
" 
 428                 echo >&2 "I require the ots 
(OpenTimestamps
) command but it
's not installed.  Aborting." 
 433             # Code to run if $1 matched "--wait" or "-w": 
 434             echoerr "DEBUG: OpenTimestamps '--wait' option selected." 
 435             OPTION_OPENTIMESTAMPS_WAIT="true" 
 438             # Code to run if $1 matched "--output" or "-o": 
 439             # Check that argument $2 is a directory. 
 441                 echoerr "DEBUG: Specified ""$PROOF_DIRECTORY_BASE"" in ""$2"" as directory for saving proof files." 
 442                 PROOF_DIRECTORY="$2"/"$PROOF_DIRECTORY_BASE" 
 443                 echoerr "DEBUG: Proof Directory is:""$PROOF_DIRECTORY" 
 444                 shift # Remove an additional argument so the additional delimiter argument $2 is properly removed at the end of the while loop. 
 446                 echoerr "ERROR: Invalid argument provided as output directory for proof files:""$2" 
 451             # Code to run if $1 isn't any of the above cases
: 
 452             # Check that remaining argument is a directory. 
 454                 ###echoerr "DEBUG: Remaining argument is directory. Adding to \$directoriesToProcess array." 
 455                 # Add directory argument $1 to directory array 
 456                 directoriesToProcess
+=($1)  # (see https://stackoverflow.com/a/1951523 ) 
 458                 echoerr 
"ERROR: Remaining argument is not a directory or valid option. Exiting." 
 463     shift  # Remove first argument ($1) so remaining arguments can be processed on next loop. 
 466 # If OPTION_ALGORITHM is not set then set it to 'sha256sum' by default. 
 467 if [[ ! -v OPTION_ALGORITHM 
]]; then 
 468     echoerr 
"DEBUG: No hash algorithm set. Defaulting to sha256sum." 
 469     OPTION_ALGORITHM
="sha256sum" 
 472 # If OPTION_DELIMITER is not set then set it to ',' by default. 
 473 if [[ ! -v OPTION_DELIMITER 
]]; then 
 474     ###echoerr "DEBUG: No delimiter set. Defaulting to ','." 
 478 # Exit program if directoriesToProcess array is empty. 
 479 if [[ ${#directoriesToProcess[@]} -eq 0 ]]; then 
 480     echoerr 
"ERROR: No valid directory specified." 
 484 ###echoerr "DEBUG: === INPUT PROCESSING END ===" 
 486 # === MAIN PROGRAM === 
 487 [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: === MAIN PROGRAM START ===" 
 488 [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: Verbose option active." 
 489 [[ $OPTION_VERBOSE == "true" ]] && [[ $OPTION_RECURSIVE == "true" ]] && echoerr 
"DEBUG: Recursive option active." 
 490 [[ $OPTION_VERBOSE == "true" ]] && [[ -v OPTION_ALGORITHM 
]] && echoerr 
"DEBUG: Selected Hash algorithm is:""$OPTION_ALGORITHM" 
 491 [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: Directories to process are: ${directoriesToProcess[@]}" 
 492 [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: Message to add to proof is: $OPTION_MESSAGE" 
 493 [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: Proof directory is:""$PROOF_DIRECTORY" 
 494 [[ $OPTION_VERBOSE == "true" ]] && [[ $OPTION_OPENTIMESTAMPS == "true" ]] && echoerr 
"DEBUG: OpenTimestamps option active." 
 495 [[ $OPTION_VERBOSE == "true" ]] && [[ $OPTION_OPENTIMESTAMPS_WAIT == "true" ]] && echoerr 
"DEBUG: OpenTimestamps '--wait' option selected." 
 496 # Create proof directory 
 497 mkdir 
"$PROOF_DIRECTORY" 
 498 echoerr 
"Proof directory created at:""$PROOF_DIRECTORY" 
 501 # declare -a filePathsFull      # array 
 502 # declare -a filePathsIndex     # array 
 503 # declare -a fileHashes         # array 
 504 # declare -a fileHashNonces     # array 
 505 # declare -a fileHashesSalted   # array 
 506 # declare -a fileSizes          # array 
 507 # declare -a fileModTime        # array 
 508 # declare -ag prvProofPaths     # array, global 
 509 # declare -ag pubProofPaths     # array, global 
 511 # ==== GENERATE HASH LIST(S) ==== 
 512 [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: ==== GENERATING HASH LIST(S) ====" 
 514 # Assemble and write proofs to disk for all specified directories. 
 516 # Generate proofs from all provided directories. 
 517 for j 
in "${!directoriesToProcess[@]}"; do 
 518     writeProofs 
$j "${directoriesToProcess[j]}"     
 521 # Decide if superproof is required. 
 522 if [[ ${#directoriesToProcess[@]} -eq 1 ]]; then 
 523     # If only one directory provided then no further proofs required. 
 524     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: Only 1 directory processed. No superproof required." 
 525 elif [[ ${#directoriesToProcess[@]} -gt 1 ]]; then 
 526     # If multiple directories provided then create superproof from files in PROOF_DIRECTORY. 
 527     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: More than 1 directory processed. Proceeding to generate superproof from PROOF_DIRECTORY." 
 528     writeProofs 
"S" $PROOF_DIRECTORY # Create proof using proof files which are stored in PROOF_DIRECTORY 
 531 # ==== GENERATE HASH LIST(S) REPORT ===== 
 532 echoerr 
"Proof files written to $PROOF_DIRECTORY directory:" 
 533 for i 
in ${!prvProofPaths[@]}; do 
 534     echo $
(basename "${prvProofPaths[i]}") 
 535     echo $
(basename "${pubProofPaths[i]}") 
 538 # ==== GENERATE OPENTIMESTAMPS TIMESTAMP FILE(S) ==== 
 540 # Determine if OpenTimestamp actions to be performed. 
 541 if [[ $OPTION_OPENTIMESTAMPS == "true" ]]; then 
 542     # Identify target for OpenTimeStamps script. 
 543     otsTarget
=${pubProofPaths[-1]}  # Select final element in pubProofPaths array as target for 'ots' command. 
 544     [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: otsTarget is:""$otsTarget" 
 546     # Set wait option as determined by OPTION_OPENTIMESTAMPS_WAIT variable. Set timeout length. 
 547     if [[ $OPTION_OPENTIMESTAMPS_WAIT == "true" ]]; then 
 548         otsWaitOption
="--wait" # Specify '--wait' option for ots command. 
 549         otsWaitTimeCmd
="timeout ""$OTS_TIMEOUT" # Specify timeout length (6 hours) for ots command. 
 550         [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: OpenTimestamps '--wait' option active." 
 554         [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: OpenTimestamps '--wait' option not active." 
 557     # Generate OpenTimeStamps proof file. 
 558     pushd "$PROOF_DIRECTORY" 1>/dev
/null 
2>/dev
/null 
# temporarily change working dir to PROOF_DIRECTORY, suppress stdout and stderr 
 559     $otsWaitTimeCmd ots 
$otsWaitOption stamp 
$otsTarget 
 560     echoerr 
"OpenTimestamps operation successful! $otsTarget created." 
 561     echoerr 
"Use 'ots info [ filename ]' for more info." 
 562     echoerr 
"Use 'ots upgrade' if '--wait' option not used to make .ots file independently-verifiable." 
 563     echoerr 
"See https://opentimestamps.org/ for more information." 
 564     popd 1>/dev
/null 
2>/dev
/null 
# reverse temporary change to working dir, suppress stdout and stderr 
 569 [[ $OPTION_VERBOSE == "true" ]] && echoerr 
"DEBUG: === MAIN PROGRAM END ===" 
 570 # Exit program successfully. 
 576 # - bash, find, sort, echo, mkdir, basename, awk, wc, date, dd, 
 577 #   readlink, touch, ots, 
 579 # - GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu) 
 580 #   Copyright (C) 2019 Free Software Foundation, Inc. 
 581 #   License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> 
 582 #   This is free software; you are free to change and redistribute it. 
 583 #   There is NO WARRANTY, to the extent permitted by law. 
 585 # - find (GNU findutils) 4.6.0.225-235f 
 586 #   Copyright (C) 2019 Free Software Foundation, Inc. 
 587 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 588 #   This is free software: you are free to change and redistribute it. 
 589 #   There is NO WARRANTY, to the extent permitted by law. 
 590 #   Written by Eric B. Decker, James Youngman, and Kevin Dalley. 
 591 #   Features enabled: D_TYPE O_NOFOLLOW(enabled) LEAF_OPTIMISATION FTS(FTS_CWDFD) CBO(level=2)  
 593 # - sort (GNU coreutils) 8.30 
 594 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 595 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 596 #   This is free software: you are free to change and redistribute it. 
 597 #   There is NO WARRANTY, to the extent permitted by law. 
 598 #   Written by Mike Haertel and Paul Eggert. 
 600 # - echo (GNU coreutils) 8.30 
 601 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 602 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 603 #   This is free software: you are free to change and redistribute it. 
 604 #   There is NO WARRANTY, to the extent permitted by law. 
 605 #   Written by Brian Fox and Chet Ramey. 
 607 # - mkdir (GNU coreutils) 8.30 
 608 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 609 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 610 #   This is free software: you are free to change and redistribute it. 
 611 #   There is NO WARRANTY, to the extent permitted by law. 
 612 #   Written by David MacKenzie. 
 614 # - basename (GNU coreutils) 8.30 
 615 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 616 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 617 #   This is free software: you are free to change and redistribute it. 
 618 #   There is NO WARRANTY, to the extent permitted by law. 
 619 #   Written by David MacKenzie. 
 621 # - GNU Awk 4.2.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.1.2) 
 622 #   Copyright (C) 1989, 1991-2018 Free Software Foundation. 
 623 #   This program is free software; you can redistribute it and/or modify 
 624 #   it under the terms of the GNU General Public License as published by 
 625 #   the Free Software Foundation; either version 3 of the License, or 
 626 #   (at your option) any later version. 
 627 #   This program is distributed in the hope that it will be useful, 
 628 #   but WITHOUT ANY WARRANTY; without even the implied warranty of 
 629 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 630 #   GNU General Public License for more details. 
 631 #   You should have received a copy of the GNU General Public License 
 632 #   along with this program. If not, see http://www.gnu.org/licenses/. 
 634 # - wc (GNU coreutils) 8.30 
 635 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 636 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 637 #   This is free software: you are free to change and redistribute it. 
 638 #   There is NO WARRANTY, to the extent permitted by law. 
 639 #   Written by Paul Rubin and David MacKenzie. 
 641 # - date (GNU coreutils) 8.30 
 642 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 643 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 644 #   This is free software: you are free to change and redistribute it. 
 645 #   There is NO WARRANTY, to the extent permitted by law. 
 646 #   Written by David MacKenzie. 
 648 # - dd (coreutils) 8.30 
 649 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 650 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 651 #   This is free software: you are free to change and redistribute it. 
 652 #   There is NO WARRANTY, to the extent permitted by law. 
 653 #   Written by Paul Rubin, David MacKenzie, and Stuart Kemp. 
 655 # - readlink (GNU coreutils) 8.30 
 656 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 657 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 658 #   This is free software: you are free to change and redistribute it. 
 659 #   There is NO WARRANTY, to the extent permitted by law. 
 660 #   Written by Dmitry V. Levin. 
 662 # - touch (GNU coreutils) 8.30 
 663 #   Copyright (C) 2018 Free Software Foundation, Inc. 
 664 #   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. 
 665 #   This is free software: you are free to change and redistribute it. 
 666 #   There is NO WARRANTY, to the extent permitted by law. 
 667 #   Written by Paul Rubin, Arnold Robbins, Jim Kingdon, 
 668 #   David MacKenzie, and Randy Smith. 
 670 # - ots v0.7.0 ( https://github.com/opentimestamps/opentimestamps-client ) 
 671 #   The OpenTimestamps Client is free software: you can redistribute it and/or 
 672 #   modify it under the terms of the GNU Lesser General Public License as published 
 673 #   by the Free Software Foundation, either version 3 of the License, or (at your 
 674 #   option) any later version. 
 675 #   The OpenTimestamps Client is distributed in the hope that it will be useful, 
 676 #   but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 677 #   or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License 
 678 #   below for more details.