feature(unitproc) Add legacy unit process scripts
[BK-2020-03.git] / unitproc / bkpoe
diff --git a/unitproc/bkpoe b/unitproc/bkpoe
new file mode 100755 (executable)
index 0000000..ff4c738
--- /dev/null
@@ -0,0 +1,678 @@
+#!/bin/bash
+#
+# Date: 2020-02-18T20:06Z
+#
+# Author: Steven Baltakatei Sandoval (baltakatei.com)
+#
+# License: This bash script, 'bkpoe', is licensed under GPLv3 or
+# later by Steven Baltakatei Sandoval:
+#
+#    'bkpoe', a file system proof of existence generator
+#    Copyright (C) 2020  Steven Baltakatei Sandoval (baltakatei.com)
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    A copy of the GNU General Public License may be found at
+#    <https://www.gnu.org/licenses/>.
+#
+# Description: This is a simple script that uses GNU Coreutils
+# OpenTimestamps to generate a proof that permits a user to prove the
+# existence of any individual file within a directory. A proof
+# consists of:
+#
+#   - An OpenTimestamps 'ots' file (if '-t' option provided)
+#
+#   - Public proof: text file(s) containing a list of salted file
+#     hashes.
+#
+#   - Private proof: text file(s) containing:
+#
+#     - A list of unsalted file hashes
+#
+#     - A list of nonces used to generate the list of salted file hashes.
+#
+#     - The list of salted file hashes
+#
+#     - File attributes (size, relative path)
+#
+#   The proof is produced by performing the following steps:
+#
+#   - Use 'find' and GNU Coreutils digest algorithms to create
+#     numbered lists of file digests at specified directories.
+#
+#   - Save these numbered lists of digests into text files within a
+#     proof directory.
+#
+#   - If more than one specified directory is provided also process
+#     the files within the proof directory.
+#
+#   - Use OpenTimestamps to create an 'ots' timestamp file
+#     for the final digest list file. (if '-t' option provided).
+#
+#   - Use OpenTimestamps to upgrade each 'ots' timestamp file after a
+#     delay. (if '-w' option provided).
+#
+# Dependencies: coreutils, ots. See end of file
+#
+# Tested on:
+#
+#   - GNU/Linux Debian 10
+
+
+# === VARIABLE INITIALIZATION AND FUNCTION DEFINITIONS ===
+
+PATH="/usr/local/bin/:$PATH" # Add default OpenTimestamps path to PATH (necessary if cron used to call this script)
+scriptHostname=$(hostname) # Save hostname of system running this script.
+OTS_TIMEOUT="6h" # maximum time to run `ots --wait` (used via `timeout` command)
+
+# Declare array for storing directories whose contents will be hashed.
+declare -a directoriesToProcess
+
+# Declare arrays for storing file paths, file index, file hashes, file
+# hash nonces, the salted hashes, file sizes, file paths, file
+# modification times, and proof file paths.
+declare -a filePathsFull      # array
+declare -a filePathsIndex     # array
+declare -a fileHashes         # array
+declare -a fileHashNonces     # array
+declare -a fileHashesSalted   # array
+declare -a fileSizes          # array
+declare -a fileModTime        # array
+declare -g PROOF_DIRECTORY    # global
+declare -ag prvProofPaths     # array, global
+declare -ag pubProofPaths     # array, global
+
+# Define 'echoerr' function which outputs text to stderr
+    # Note: function copied from https://stackoverflow.com/a/2990533
+echoerr() {
+    echo "$@" 1>&2;
+}
+
+# Define script version.
+SCRIPT_VERSION="bkpoe 0.1.0"
+
+# Save current date.
+runDate=$(date +%Y%m%dT%H%M%S%z)
+sleep 1 # Space out program run times by at least one second.
+###echoerr "DEBUG: Current date is:"$runDate
+
+# Save working directory
+runPath=$(pwd)
+###echoerr "DEBUG: Current working directory is:"$runPath
+
+# Define proof directory basename
+PROOF_DIRECTORY_BASE="$runDate""..""$scriptHostname""_bkpoe_proofs"
+
+# Define default proof directory
+PROOF_DIRECTORY="$runPath"/"$PROOF_DIRECTORY_BASE"
+
+# Print information on how to use this bash script.
+usage() {
+
+    echo "USAGE:"
+    echo "    bkpoe [ options ] DIRECTORIES"
+    echo
+    echo "OPTIONS:"
+    echo "    -h, --help"
+    echo "            Display help information."
+    echo
+    echo "    -r, --recursive"
+    echo "            Perform recursive hashing of directories."
+    echo
+    echo "    -v, --verbose"
+    echo "            Display debugging info."
+    echo
+    echo "    -a, --algorithm [ algo ]"
+    echo "            Specify GNU Coreutils hash command. Options include:      "
+    echo "            b2sum md5sum sha1sum sha224sum sha256sum sha384sum        "
+    echo "            sha512sum"
+    echo
+    echo "    -m, --message [ string ]"
+    echo "            Specify message to be included in filehash files."
+    echo
+    echo "    -d, --delimiter [ string ]"
+    echo "            Specify character to serve as delimiter in proof files.   "
+    echo "            (Default: ,)                                              "
+    echo "    -t, --timestamp"
+    echo "            Create openTimestmaps proof file."
+    echo "    -w, --wait"
+    echo "            Pass '--wait' option to OpenTimestamps to 'wait           "
+    echo "            until a complete timestamp committed in the               "
+    echo "            Bitcoin blockchain is available instead of                "
+    echo "            returning immediately.                                    "
+    echo "    -o, --output"
+    echo "            Specify output directory to save proof. Default is working"
+    echo "            directory where script was run."
+}
+
+
+# Check that a string matches one of GNU Coreutils hash commands.
+isValidDigestAlgo() {
+    # Syntax: isValidDigestAlgo [string]
+    # Description: Returns 1 if [string] is a valid GNU Coreutils hash function command. Returns 0 otherwise.
+
+    # Exit if more than one argument provided.
+    ###echoerr "DEBUG: Arguments provided to isValidDigestAlgo() are:$@"
+    if [[ $# -ne 1 ]]; then
+       echoerr "ERROR: Illegal number of digest arguments provided."
+       exit 1
+    fi
+
+    # Exit if argument contains non-alphanumeric characters
+    if [[ "$@" =~ [^a-zA-Z0-9$] ]]; then
+       echoerr "ERROR: Illegal characters for digest argument provided."
+       exit 1
+    fi
+        
+    local input=$1 #Save first argument provided to function as input for further processing.
+    
+    ###echoerr "DEBUG: Starting function to check validity of string as valid digest command."
+
+    # 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 ).
+    local gnuCoreutilsAlgos=(b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum)
+    ###echoerr "DEBUG:Contents of \$gnuCoreutilsAlgos array is:${gnuCoreutilsAlgos[@]}"
+    
+    # Make input string lowercase
+    #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
+    ###echoerr "DEBUG: User-specified hash algorithm is:$input"
+    
+    # Return true (0) if $gnuCoreutilsAlgos array contains string $input (see https://unix.stackexchange.com/a/177139 )
+    for item in "${gnuCoreutilsAlgos[@]}"; do
+       ###echoerr "DEBUG: \$item is:$item"
+       ###echoerr "DEBUG: \$input is:$input"
+       if [ "$input" == "$item" ]; then
+           ###echoerr "DEBUG: $input present in \$gnuCoreutilsAlgos array."
+           return 0
+       fi
+    done
+
+    # Return false (1) otherwise.
+    echoerr "DEBUG: $input not recognized as valid hash algorithm name."
+    return 1
+}
+
+writeProofs() {
+    # Syntax: writeProofs [ loopIndex ] [ dirPath ]
+
+    # Description: Construct private and public proofs for files
+    # contained within [ dirPath ]. Save file with date, [ loopIndex ],
+    # and base directory name within filename.
+
+    # Required variables:
+    #  - $runDate: used in creating proof directory
+    #  - $PROOF_DIRECTORY: used as directory where proof files are saved
+    #  - $OPTION_ALGORITHM: used to decide which hash algorithm to use
+    #  - $OPTION_DELIMITER: used to determine which char to use as a delimiter in proof files
+    #  - $OPTION_MESSAGE: used to determine message included in proof files
+    #  - $OPTION_RECURSIVE: used to determine if files within subdirectories under [ dir Path ] directory should be included
+
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:This is the writeProofs() function. Provided arguments are:$@"
+    
+    # Determine scope of 'find' file search (recursive search or not)
+    if [[ $OPTION_RECURSIVE == "true" ]]; then
+        [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Performing recursive file search on directory $directoryToHash"
+       findMaxDepthOption=""  # Do not specify -maxdepth option for find (causes 'find' to include all files recursively within [ dirPath ]
+    else
+       findMaxDepthOption="-maxdepth 1" # Specify "-maxdepth 1 " as option for 'find' so only files immediately within [ dirPath ] directory are included (no subdirectories).
+    fi
+    
+    # Save current date of loop
+    proofLoopRunDate=$(date +%Y%m%dT%H%M%S%z)
+    ###echoerr "DEBUG: Date and time of current loop is:"$proofLoopRunDate
+
+    # Reset arrays.
+    filePathsFull=()
+    filePathsIndex=()
+    fileHashes=()
+    fileHashNonces=()
+    fileHashesSalted=()
+    fileSizes=()
+    fileModTime=()
+
+    # Pass first argument provided to function as directory index.
+    local loopIndex=$1 
+
+    # Pass second argument as directory to process
+    if [[ -d $2 ]]; then
+       [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Valid directory argument provided to writeProofs() function."
+       local directoryToHash=$2
+       ## local directoryToHash=$(readlink -f $2) # Use absolutely full path (not used for privacy).
+    else
+       echoerr "ERROR: Invalid directory argument provided to writeProofs() function."
+       exit 1
+    fi
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:Processing directory:"$directoryToHash
+    local directoryToHashShort="$(basename "$directoryToHash")"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:Directory basename:"$directoryToHashShort
+
+    # Populate filePathsFull array with file paths from 'find' results (see https://stackoverflow.com/a/54561526 ).
+    #   Note: For creating array from output of find via readarray, see https://unix.stackexchange.com/a/263885
+    #   Note: For sorting output of find, see https://unix.stackexchange.com/a/34328
+    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'.
+    [[ $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/ )
+
+    # Construct index array from filePathsFull array for the for loops used to write proofs.
+    filePathsIndex=(${!filePathsFull[@]})
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#filePathsIndex[@]} elements of filePathsIndex array are:${filePathsIndex[@]}"
+
+    # Populate fileHashes with hashes of files listed in filePathsFull array
+    # Iterate through all elements of filePathsFull array using an integer index. (see https://stackoverflow.com/a/6723516 )
+    for i in "${!filePathsIndex[@]}"; do 
+       ###[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:The $i th element of the filePathsFull array is:${filePathsFull[i]}"
+       fileHashes[i]=$($OPTION_ALGORITHM "${filePathsFull[i]}" | awk '{ print $1 }') # Get hash value from GNU Coreutils hash command (see https://stackoverflow.com/a/3679803 )
+    done
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileHashes[@]} elements of fileHashes array are:${fileHashes[@]}"
+
+    # 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.
+    for i in "${!filePathsIndex[@]}"; do
+       fileHashNonces[i]=$(dd bs=64 count=1 if=/dev/urandom 2>/dev/null | $OPTION_ALGORITHM | awk '{ print $1 }')
+    done
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileHashNonces[@]} elements of fileHashNonces array are:${fileHashNonces[@]}"
+
+    # Populate fileHashesSalted array with salted digest for each file hash corresponding nonce.
+    for i in "${!filePathsIndex[@]}"; do
+       fileHashesSalted[i]=$(echo -n "${fileHashes[i]}${fileHashNonces[i]}" | $OPTION_ALGORITHM | awk '{ print $1 }')
+    done
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileHashesSalted[@]} elements of fileHashesSalted array are:${fileHashesSalted[@]}"
+
+    # Populate fileSizes array with file sizes (in bytes).
+    for i in "${!filePathsIndex[@]}"; do
+       fileSizes[i]=$(wc -c "${filePathsFull[i]}" | awk '{ print $1 }')
+    done
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileSizes[@]} elements of fileSizes array are:${fileSizes[@]}"
+
+    # Populate fileModTime array with file modification times (in ISO-8601 format)
+    for i in "${!filePathsIndex[@]}"; do
+       fileModTime[i]=$(date --iso-8601=seconds -r "${filePathsFull[i]}" | awk '{ print $1 }')
+    done
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileModTime[@]} elements of fileModTime array are:${fileModTime[@]}"
+
+    # ==== GENERATE PROOF FILES FROM HASH LIST(S) ====
+
+    # Define output file names and paths.
+    local PROOF_PRIVATE_FILENAME=$proofLoopRunDate"_"$loopIndex"_prv.."$scriptHostname"_"$directoryToHashShort"_"$OPTION_ALGORITHM"_proof_private.txt"
+    local PROOF_PUBLIC_FILENAME=$proofLoopRunDate"_"$loopIndex"_pub.."$scriptHostname"_"$directoryToHashShort"_"$OPTION_ALGORITHM"_proof_public.txt"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: \$PROOF_PRIVATE_FILENAME is $PROOF_PRIVATE_FILENAME"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: \$PROOF_PUBLIC_FILENAME is $PROOF_PUBLIC_FILENAME"
+    PROOF_PRIVATE_FILEPATH="$PROOF_DIRECTORY"/"$PROOF_PRIVATE_FILENAME"
+    PROOF_PUBLIC_FILEPATH="$PROOF_DIRECTORY"/"$PROOF_PUBLIC_FILENAME"
+
+    # Create private proof and public proof files.
+    touch "$PROOF_PRIVATE_FILEPATH"
+    touch "$PROOF_PUBLIC_FILEPATH"
+
+    # Abbreviate OPTION_DELIMITER to local variable DELIM
+    local DELIM=$OPTION_DELIMITER
+    
+    # Define private proof first-line headers (date, version, algorithm, message)
+    local PROOF_PRIVATE_HEADER1="$proofLoopRunDate$DELIM$SCRIPT_VERSION$DELIM$OPTION_ALGORITHM$DELIM$OPTION_MESSAGE"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: \$PROOF_PRIVATE_HEADER1 is $PROOF_PRIVATE_HEADER1"
+    # Define public proof first-line headers (date, version, algorithm, message)
+    local PROOF_PUBLIC_HEADER1="$proofLoopRunDate$DELIM$SCRIPT_VERSION$DELIM$OPTION_ALGORITHM$DELIM$OPTION_MESSAGE"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: \$PROOF_PUBLIC_HEADER1 is $PROOF_PUBLIC_HEADER1"
+
+    # Define private proof second-line column labels (index, digest, nonce, salted digest, mdate, size, filepath)
+    local PROOF_PRIVATE_HEADER2="index"$DELIM"digest"$DELIM"nonce"$DELIM"digest_salted"$DELIM"mdate"$DELIM"size_bytes"$DELIM"file_path"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: \$PROOF_PRIVATE_HEADER2 is $PROOF_PRIVATE_HEADER2"
+    # Define public proof second-line column labels (index, salted digest)
+    local PROOF_PUBLIC_HEADER2="index"$DELIM"digest_salted"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: \$PROOF_PUBLIC_HEADER2 is $PROOF_PUBLIC_HEADER2"
+
+    # Write headers to proof files
+    echo $PROOF_PRIVATE_HEADER1 >> "$PROOF_PRIVATE_FILEPATH"
+    echo $PROOF_PRIVATE_HEADER2 >> "$PROOF_PRIVATE_FILEPATH"
+    echo $PROOF_PUBLIC_HEADER1 >> "$PROOF_PUBLIC_FILEPATH"
+    echo $PROOF_PUBLIC_HEADER2 >> "$PROOF_PUBLIC_FILEPATH"
+    
+    # Loop to append array contents.
+    for i in "${!filePathsIndex[@]}"; do
+       # Append to private proof: filePathsIndex[i],fileHashes[i],fileHashNonces[i],fileHashesSalted[i],fileModTime[i],fileSizes[i],filePathsFull[i]
+       ###[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Writing line to $PROOF_PRIVATE_FILEPATH"
+        echo ${filePathsIndex[i]}$DELIM${fileHashes[i]}$DELIM${fileHashNonces[i]}$DELIM${fileHashesSalted[i]}$DELIM${fileModTime[i]}$DELIM${fileSizes[i]}$DELIM${filePathsFull[i]} >> "$PROOF_PRIVATE_FILEPATH"
+       # Append to public proof: filePathsIndex[i],fileHashesSalted[i]
+       [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Writing line to $PROOF_PUBLIC_FILEPATH"
+       echo ${filePathsIndex[i]}$DELIM${fileHashesSalted[i]} >> $PROOF_PUBLIC_FILEPATH
+    done
+
+    # Save file paths to global variables prvProofPaths and pubProofPaths to allow other functions to manipulate files.
+    prvProofPaths+=($PROOF_PRIVATE_FILEPATH)
+    pubProofPaths+=($PROOF_PUBLIC_FILEPATH)
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#prvProofPaths[@]} elements of prvProofPaths array are:${prvProofPaths[@]}"
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#pubProofPaths[@]} elements of pubProofPaths array are:${pubProofPaths[@]}"
+
+    # Reset arrays.
+    filePathsFull=()
+    filePathsIndex=()
+    fileHashes=()
+    fileHashNonces=()
+    fileHashesSalted=()
+    fileSizes=()
+    fileModTime=()
+}
+
+
+# === INPUT PROCESSING ===
+# Check for presence of options.
+###echoerr "DEBUG: === INPUT PROCESSING START ==="
+
+# 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/ )
+while [ ! $# -eq 0 ]   # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
+do
+    case "$1" in    # Check first of remaining arguments to see if it matches one of strings below.
+       --help | -h)
+           # Code to run if $1 matched "--help" or "-h":
+           ###echoerr "DEBUG: This is the help information."
+           usage # Run usage function to display helpful info to user.
+           exit 1
+           ;;
+       --recursive | -r)
+           # Code to run if $1 matched "--recursive" or "-r":
+           ###echoerr "DEBUG: Recursive option activated."
+           OPTION_RECURSIVE="true"
+           ;;
+       --verbose | -v)
+           # Code to run if $1 matched "--verbose" or "-v":
+           ###echoerr "DEBUG: Verbose option activated."
+           OPTION_VERBOSE="true"
+           ;;
+       --algorithm | -a)
+           # Code to run if $1 matched "--algorithm" or "-a":
+
+           ###echoerr "DEBUG: Selecting hash algorithm."
+           # Check that OPTION_ALGORITHM variable hasn't already been set.
+           # Check that OPTION_ALGORITHM contains only alphanumeric characters.
+           # Check that argument following '-a' or '--algorithm' ($2) is valid algorithm.
+           if [[ ! -v OPTION_ALGORITHM ]] && [[ ! "$2" =~ [^a-zA-Z0-9$] ]] && isValidDigestAlgo $2; then
+               ###echoerr "DEBUG: Valid hash algorithm provided."
+               OPTION_ALGORITHM=$2 # Save argument following '-a' or '--algorithm' to $OPTION_ALGORITHM
+               shift # Remove an additional argument so the additional algorithm argument $2 is properly removed at the end of the while loop.
+           else
+               echoerr "ERROR: Invalid hash algorithm argument provided:""$2"
+               exit 1
+           fi
+           ;;
+       --message | -m)
+           # Code to run if $1 matched "--message" or "-m":
+           ###echoerr "DEBUG: Specifying header message."
+           # Check that OPTION_MESSAGE variable hasn't already been set.
+           if [[ ! -v OPTION_MESSAGE ]]; then
+               OPTION_MESSAGE=$2 # Save argument following '-m' or '--message' to $OPTION_MESSAGE
+               shift # Remove an additional argument so the additional algorithm argument $2 is properly removed at the end of the while loop.
+           fi
+           ;;
+       --delimiter | -d)
+           # Code to run if $1 matched "--delimiter" or "-d":
+           ###echoerr "DEBUG: Specifying delimiter."
+           # Check that OPTION_DELIMITER variable hasn't already been set.
+           if [[ ! -v OPTION_DELIMITER ]]; then
+               OPTION_DELIMITER=$2 # Save argument following '-d' or '--delimiter' to $OPTION_DELIMITER
+               shift # Remove an additional argument so the additional delimiter argument $2 is properly removed at the end of the while loop.
+           fi
+           ;;
+       --timestamp | -t)
+           # Code to run if $1 matched "--timestamp" or "-t":
+           echoerr "DEBUG: OpenTimestamps option selected."
+           # Check that ots command exists. (see https://stackoverflow.com/a/677212 )
+           if command -v ots >/dev/null 2>&1 ; then
+               echoerr "DEBUG: OpenTimestampscommand ('ots') detected."
+               OPTION_OPENTIMESTAMPS="true"
+           else
+               echo >&2 "I require the ots (OpenTimestamps) command but it's not installed.  Aborting."
+               exit 1
+           fi
+           ;;
+       --wait | -w)
+           # Code to run if $1 matched "--wait" or "-w":
+           echoerr "DEBUG: OpenTimestamps '--wait' option selected."
+           OPTION_OPENTIMESTAMPS_WAIT="true"
+           ;;
+       --output | -o)
+           # Code to run if $1 matched "--output" or "-o":
+           # Check that argument $2 is a directory.
+           if [[ -d $2 ]]; then
+               echoerr "DEBUG: Specified ""$PROOF_DIRECTORY_BASE"" in ""$2"" as directory for saving proof files."
+               PROOF_DIRECTORY="$2"/"$PROOF_DIRECTORY_BASE"
+               echoerr "DEBUG: Proof Directory is:""$PROOF_DIRECTORY"
+               shift # Remove an additional argument so the additional delimiter argument $2 is properly removed at the end of the while loop.
+           else
+               echoerr "ERROR: Invalid argument provided as output directory for proof files:""$2"
+               exit 1
+           fi
+           ;;
+       *)
+           # Code to run if $1 isn't any of the above cases:
+           # Check that remaining argument is a directory.
+           if [[ -d $1 ]]; then
+               ###echoerr "DEBUG: Remaining argument is directory. Adding to \$directoriesToProcess array."
+               # Add directory argument $1 to directory array
+               directoriesToProcess+=($1)  # (see https://stackoverflow.com/a/1951523 )
+           else
+               echoerr "ERROR: Remaining argument is not a directory or valid option. Exiting."
+               exit 1
+           fi
+           ;;
+    esac
+    shift  # Remove first argument ($1) so remaining arguments can be processed on next loop.
+done
+
+# If OPTION_ALGORITHM is not set then set it to 'sha256sum' by default.
+if [[ ! -v OPTION_ALGORITHM ]]; then
+    echoerr "DEBUG: No hash algorithm set. Defaulting to sha256sum."
+    OPTION_ALGORITHM="sha256sum"
+fi
+
+# If OPTION_DELIMITER is not set then set it to ',' by default.
+if [[ ! -v OPTION_DELIMITER ]]; then
+    ###echoerr "DEBUG: No delimiter set. Defaulting to ','."
+    OPTION_DELIMITER=","
+fi
+
+# Exit program if directoriesToProcess array is empty.
+if [[ ${#directoriesToProcess[@]} -eq 0 ]]; then
+    echoerr "ERROR: No valid directory specified."
+    exit 1
+fi
+
+###echoerr "DEBUG: === INPUT PROCESSING END ==="
+
+# === MAIN PROGRAM ===
+[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: === MAIN PROGRAM START ==="
+[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Verbose option active."
+[[ $OPTION_VERBOSE == "true" ]] && [[ $OPTION_RECURSIVE == "true" ]] && echoerr "DEBUG: Recursive option active."
+[[ $OPTION_VERBOSE == "true" ]] && [[ -v OPTION_ALGORITHM ]] && echoerr "DEBUG: Selected Hash algorithm is:""$OPTION_ALGORITHM"
+[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Directories to process are: ${directoriesToProcess[@]}"
+[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Message to add to proof is: $OPTION_MESSAGE"
+[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Proof directory is:""$PROOF_DIRECTORY"
+[[ $OPTION_VERBOSE == "true" ]] && [[ $OPTION_OPENTIMESTAMPS == "true" ]] && echoerr "DEBUG: OpenTimestamps option active."
+[[ $OPTION_VERBOSE == "true" ]] && [[ $OPTION_OPENTIMESTAMPS_WAIT == "true" ]] && echoerr "DEBUG: OpenTimestamps '--wait' option selected."
+# Create proof directory
+mkdir "$PROOF_DIRECTORY"
+echoerr "Proof directory created at:""$PROOF_DIRECTORY"
+
+## TEMP REF:
+# declare -a filePathsFull      # array
+# declare -a filePathsIndex     # array
+# declare -a fileHashes         # array
+# declare -a fileHashNonces     # array
+# declare -a fileHashesSalted   # array
+# declare -a fileSizes          # array
+# declare -a fileModTime        # array
+# declare -ag prvProofPaths     # array, global
+# declare -ag pubProofPaths     # array, global
+
+# ==== GENERATE HASH LIST(S) ====
+[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: ==== GENERATING HASH LIST(S) ===="
+
+# Assemble and write proofs to disk for all specified directories.
+
+# Generate proofs from all provided directories.
+for j in "${!directoriesToProcess[@]}"; do
+    writeProofs $j "${directoriesToProcess[j]}"    
+done
+
+# Decide if superproof is required.
+if [[ ${#directoriesToProcess[@]} -eq 1 ]]; then
+    # If only one directory provided then no further proofs required.
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: Only 1 directory processed. No superproof required."
+elif [[ ${#directoriesToProcess[@]} -gt 1 ]]; then
+    # If multiple directories provided then create superproof from files in PROOF_DIRECTORY.
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: More than 1 directory processed. Proceeding to generate superproof from PROOF_DIRECTORY."
+    writeProofs "S" $PROOF_DIRECTORY # Create proof using proof files which are stored in PROOF_DIRECTORY
+fi
+
+# ==== GENERATE HASH LIST(S) REPORT =====
+echoerr "Proof files written to $PROOF_DIRECTORY directory:"
+for i in ${!prvProofPaths[@]}; do
+    echo $(basename "${prvProofPaths[i]}")
+    echo $(basename "${pubProofPaths[i]}")
+done
+
+# ==== GENERATE OPENTIMESTAMPS TIMESTAMP FILE(S) ====
+
+# Determine if OpenTimestamp actions to be performed.
+if [[ $OPTION_OPENTIMESTAMPS == "true" ]]; then
+    # Identify target for OpenTimeStamps script.
+    otsTarget=${pubProofPaths[-1]}  # Select final element in pubProofPaths array as target for 'ots' command.
+    [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: otsTarget is:""$otsTarget"
+
+    # Set wait option as determined by OPTION_OPENTIMESTAMPS_WAIT variable. Set timeout length.
+    if [[ $OPTION_OPENTIMESTAMPS_WAIT == "true" ]]; then
+       otsWaitOption="--wait" # Specify '--wait' option for ots command.
+       otsWaitTimeCmd="timeout ""$OTS_TIMEOUT" # Specify timeout length (6 hours) for ots command.
+       [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: OpenTimestamps '--wait' option active."
+    else
+       otsWaitOption=""
+       otsWaitTimeCmd=""
+       [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: OpenTimestamps '--wait' option not active."
+    fi
+    
+    # Generate OpenTimeStamps proof file.
+    pushd "$PROOF_DIRECTORY" 1>/dev/null 2>/dev/null # temporarily change working dir to PROOF_DIRECTORY, suppress stdout and stderr
+    $otsWaitTimeCmd ots $otsWaitOption stamp $otsTarget
+    echoerr "OpenTimestamps operation successful! $otsTarget created."
+    echoerr "Use 'ots info [ filename ]' for more info."
+    echoerr "Use 'ots upgrade' if '--wait' option not used to make .ots file independently-verifiable."
+    echoerr "See https://opentimestamps.org/ for more information."
+    popd 1>/dev/null 2>/dev/null # reverse temporary change to working dir, suppress stdout and stderr
+
+fi
+
+
+[[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: === MAIN PROGRAM END ==="
+# Exit program successfully.
+exit 0
+
+
+# == Dependencies ==
+#
+# - bash, find, sort, echo, mkdir, basename, awk, wc, date, dd,
+#   readlink, touch, ots,
+
+# - GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu)
+#   Copyright (C) 2019 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
+#   This is free software; you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+
+# - find (GNU findutils) 4.6.0.225-235f
+#   Copyright (C) 2019 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by Eric B. Decker, James Youngman, and Kevin Dalley.
+#   Features enabled: D_TYPE O_NOFOLLOW(enabled) LEAF_OPTIMISATION FTS(FTS_CWDFD) CBO(level=2) 
+
+# - sort (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by Mike Haertel and Paul Eggert.
+
+# - echo (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by Brian Fox and Chet Ramey.
+
+# - mkdir (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by David MacKenzie.
+
+# - basename (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by David MacKenzie.
+
+# - GNU Awk 4.2.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.1.2)
+#   Copyright (C) 1989, 1991-2018 Free Software Foundation.
+#   This program is free software; you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation; either version 3 of the License, or
+#   (at your option) any later version.
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#   You should have received a copy of the GNU General Public License
+#   along with this program. If not, see http://www.gnu.org/licenses/.
+
+# - wc (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by Paul Rubin and David MacKenzie.
+
+# - date (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by David MacKenzie.
+
+# - dd (coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by Paul Rubin, David MacKenzie, and Stuart Kemp.
+
+# - readlink (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by Dmitry V. Levin.
+
+# - touch (GNU coreutils) 8.30
+#   Copyright (C) 2018 Free Software Foundation, Inc.
+#   License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+#   This is free software: you are free to change and redistribute it.
+#   There is NO WARRANTY, to the extent permitted by law.
+#   Written by Paul Rubin, Arnold Robbins, Jim Kingdon,
+#   David MacKenzie, and Randy Smith.
+
+# - ots v0.7.0 ( https://github.com/opentimestamps/opentimestamps-client )
+#   The OpenTimestamps Client is free software: you can redistribute it and/or
+#   modify it under the terms of the GNU Lesser General Public License as published
+#   by the Free Software Foundation, either version 3 of the License, or (at your
+#   option) any later version.
+#   The OpenTimestamps Client is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+#   or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
+#   below for more details.