| 1 | #!/bin/bash |
| 2 | # |
| 3 | # Date: 2020-02-18T20:06Z |
| 4 | # |
| 5 | # Author: Steven Baltakatei Sandoval (baltakatei.com) |
| 6 | # |
| 7 | # License: This bash script, 'bkpoe', is licensed under GPLv3 or |
| 8 | # later by Steven Baltakatei Sandoval: |
| 9 | # |
| 10 | # 'bkpoe', a file system proof of existence generator |
| 11 | # Copyright (C) 2020 Steven Baltakatei Sandoval (baltakatei.com) |
| 12 | # |
| 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 |
| 16 | # any later version. |
| 17 | # |
| 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. |
| 22 | # |
| 23 | # A copy of the GNU General Public License may be found at |
| 24 | # <https://www.gnu.org/licenses/>. |
| 25 | # |
| 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 |
| 29 | # consists of: |
| 30 | # |
| 31 | # - An OpenTimestamps 'ots' file (if '-t' option provided) |
| 32 | # |
| 33 | # - Public proof: text file(s) containing a list of salted file |
| 34 | # hashes. |
| 35 | # |
| 36 | # - Private proof: text file(s) containing: |
| 37 | # |
| 38 | # - A list of unsalted file hashes |
| 39 | # |
| 40 | # - A list of nonces used to generate the list of salted file hashes. |
| 41 | # |
| 42 | # - The list of salted file hashes |
| 43 | # |
| 44 | # - File attributes (size, relative path) |
| 45 | # |
| 46 | # The proof is produced by performing the following steps: |
| 47 | # |
| 48 | # - Use 'find' and GNU Coreutils digest algorithms to create |
| 49 | # numbered lists of file digests at specified directories. |
| 50 | # |
| 51 | # - Save these numbered lists of digests into text files within a |
| 52 | # proof directory. |
| 53 | # |
| 54 | # - If more than one specified directory is provided also process |
| 55 | # the files within the proof directory. |
| 56 | # |
| 57 | # - Use OpenTimestamps to create an 'ots' timestamp file |
| 58 | # for the final digest list file. (if '-t' option provided). |
| 59 | # |
| 60 | # - Use OpenTimestamps to upgrade each 'ots' timestamp file after a |
| 61 | # delay. (if '-w' option provided). |
| 62 | # |
| 63 | # Dependencies: coreutils, ots. See end of file |
| 64 | # |
| 65 | # Tested on: |
| 66 | # |
| 67 | # - GNU/Linux Debian 10 |
| 68 | |
| 69 | |
| 70 | # === VARIABLE INITIALIZATION AND FUNCTION DEFINITIONS === |
| 71 | |
| 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) |
| 75 | |
| 76 | # Declare array for storing directories whose contents will be hashed. |
| 77 | declare -a directoriesToProcess |
| 78 | |
| 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 |
| 92 | |
| 93 | # Define 'echoerr' function which outputs text to stderr |
| 94 | # Note: function copied from https://stackoverflow.com/a/2990533 |
| 95 | echoerr() { |
| 96 | echo "$@" 1>&2; |
| 97 | } |
| 98 | |
| 99 | # Define script version. |
| 100 | SCRIPT_VERSION="bkpoe 0.1.0" |
| 101 | |
| 102 | # Save current date. |
| 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 |
| 106 | |
| 107 | # Save working directory |
| 108 | runPath=$(pwd) |
| 109 | ###echoerr "DEBUG: Current working directory is:"$runPath |
| 110 | |
| 111 | # Define proof directory basename |
| 112 | PROOF_DIRECTORY_BASE="$runDate""..""$scriptHostname""_bkpoe_proofs" |
| 113 | |
| 114 | # Define default proof directory |
| 115 | PROOF_DIRECTORY="$runPath"/"$PROOF_DIRECTORY_BASE" |
| 116 | |
| 117 | # Print information on how to use this bash script. |
| 118 | usage() { |
| 119 | |
| 120 | echo "USAGE:" |
| 121 | echo " bkpoe [ options ] DIRECTORIES" |
| 122 | echo |
| 123 | echo "OPTIONS:" |
| 124 | echo " -h, --help" |
| 125 | echo " Display help information." |
| 126 | echo |
| 127 | echo " -r, --recursive" |
| 128 | echo " Perform recursive hashing of directories." |
| 129 | echo |
| 130 | echo " -v, --verbose" |
| 131 | echo " Display debugging info." |
| 132 | echo |
| 133 | echo " -a, --algorithm [ algo ]" |
| 134 | echo " Specify GNU Coreutils hash command. Options include: " |
| 135 | echo " b2sum md5sum sha1sum sha224sum sha256sum sha384sum " |
| 136 | echo " sha512sum" |
| 137 | echo |
| 138 | echo " -m, --message [ string ]" |
| 139 | echo " Specify message to be included in filehash files." |
| 140 | echo |
| 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." |
| 146 | echo " -w, --wait" |
| 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. " |
| 151 | echo " -o, --output" |
| 152 | echo " Specify output directory to save proof. Default is working" |
| 153 | echo " directory where script was run." |
| 154 | } |
| 155 | |
| 156 | |
| 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. |
| 161 | |
| 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." |
| 166 | exit 1 |
| 167 | fi |
| 168 | |
| 169 | # Exit if argument contains non-alphanumeric characters |
| 170 | if [[ "$@" =~ [^a-zA-Z0-9$] ]]; then |
| 171 | echoerr "ERROR: Illegal characters for digest argument provided." |
| 172 | exit 1 |
| 173 | fi |
| 174 | |
| 175 | local input=$1 #Save first argument provided to function as input for further processing. |
| 176 | |
| 177 | ###echoerr "DEBUG: Starting function to check validity of string as valid digest command." |
| 178 | |
| 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[@]}" |
| 182 | |
| 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" |
| 186 | |
| 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." |
| 193 | return 0 |
| 194 | fi |
| 195 | done |
| 196 | |
| 197 | # Return false (1) otherwise. |
| 198 | echoerr "DEBUG: $input not recognized as valid hash algorithm name." |
| 199 | return 1 |
| 200 | } |
| 201 | |
| 202 | writeProofs() { |
| 203 | # Syntax: writeProofs [ loopIndex ] [ dirPath ] |
| 204 | |
| 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. |
| 208 | |
| 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 |
| 216 | |
| 217 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:This is the writeProofs() function. Provided arguments are:$@" |
| 218 | |
| 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 ] |
| 223 | else |
| 224 | findMaxDepthOption="-maxdepth 1" # Specify "-maxdepth 1 " as option for 'find' so only files immediately within [ dirPath ] directory are included (no subdirectories). |
| 225 | fi |
| 226 | |
| 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 |
| 230 | |
| 231 | # Reset arrays. |
| 232 | filePathsFull=() |
| 233 | filePathsIndex=() |
| 234 | fileHashes=() |
| 235 | fileHashNonces=() |
| 236 | fileHashesSalted=() |
| 237 | fileSizes=() |
| 238 | fileModTime=() |
| 239 | |
| 240 | # Pass first argument provided to function as directory index. |
| 241 | local loopIndex=$1 |
| 242 | |
| 243 | # Pass second argument as directory to process |
| 244 | if [[ -d $2 ]]; then |
| 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). |
| 248 | else |
| 249 | echoerr "ERROR: Invalid directory argument provided to writeProofs() function." |
| 250 | exit 1 |
| 251 | fi |
| 252 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:Processing directory:"$directoryToHash |
| 253 | local directoryToHashShort="$(basename "$directoryToHash")" |
| 254 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG:Directory basename:"$directoryToHashShort |
| 255 | |
| 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/ ) |
| 261 | |
| 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[@]}" |
| 265 | |
| 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 ) |
| 271 | done |
| 272 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileHashes[@]} elements of fileHashes array are:${fileHashes[@]}" |
| 273 | |
| 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 }') |
| 277 | done |
| 278 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileHashNonces[@]} elements of fileHashNonces array are:${fileHashNonces[@]}" |
| 279 | |
| 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 }') |
| 283 | done |
| 284 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileHashesSalted[@]} elements of fileHashesSalted array are:${fileHashesSalted[@]}" |
| 285 | |
| 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 }') |
| 289 | done |
| 290 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileSizes[@]} elements of fileSizes array are:${fileSizes[@]}" |
| 291 | |
| 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 }') |
| 295 | done |
| 296 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: The ${#fileModTime[@]} elements of fileModTime array are:${fileModTime[@]}" |
| 297 | |
| 298 | # ==== GENERATE PROOF FILES FROM HASH LIST(S) ==== |
| 299 | |
| 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" |
| 307 | |
| 308 | # Create private proof and public proof files. |
| 309 | touch "$PROOF_PRIVATE_FILEPATH" |
| 310 | touch "$PROOF_PUBLIC_FILEPATH" |
| 311 | |
| 312 | # Abbreviate OPTION_DELIMITER to local variable DELIM |
| 313 | local DELIM=$OPTION_DELIMITER |
| 314 | |
| 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" |
| 321 | |
| 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" |
| 328 | |
| 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" |
| 334 | |
| 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 |
| 343 | done |
| 344 | |
| 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[@]}" |
| 350 | |
| 351 | # Reset arrays. |
| 352 | filePathsFull=() |
| 353 | filePathsIndex=() |
| 354 | fileHashes=() |
| 355 | fileHashNonces=() |
| 356 | fileHashesSalted=() |
| 357 | fileSizes=() |
| 358 | fileModTime=() |
| 359 | } |
| 360 | |
| 361 | |
| 362 | # === INPUT PROCESSING === |
| 363 | # Check for presence of options. |
| 364 | ###echoerr "DEBUG: === INPUT PROCESSING START ===" |
| 365 | |
| 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). |
| 368 | do |
| 369 | case "$1" in # Check first of remaining arguments to see if it matches one of strings below. |
| 370 | --help | -h) |
| 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. |
| 374 | exit 1 |
| 375 | ;; |
| 376 | --recursive | -r) |
| 377 | # Code to run if $1 matched "--recursive" or "-r": |
| 378 | ###echoerr "DEBUG: Recursive option activated." |
| 379 | OPTION_RECURSIVE="true" |
| 380 | ;; |
| 381 | --verbose | -v) |
| 382 | # Code to run if $1 matched "--verbose" or "-v": |
| 383 | ###echoerr "DEBUG: Verbose option activated." |
| 384 | OPTION_VERBOSE="true" |
| 385 | ;; |
| 386 | --algorithm | -a) |
| 387 | # Code to run if $1 matched "--algorithm" or "-a": |
| 388 | |
| 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. |
| 397 | else |
| 398 | echoerr "ERROR: Invalid hash algorithm argument provided:""$2" |
| 399 | exit 1 |
| 400 | fi |
| 401 | ;; |
| 402 | --message | -m) |
| 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. |
| 409 | fi |
| 410 | ;; |
| 411 | --delimiter | -d) |
| 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. |
| 418 | fi |
| 419 | ;; |
| 420 | --timestamp | -t) |
| 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" |
| 427 | else |
| 428 | echo >&2 "I require the ots (OpenTimestamps) command but it's not installed. Aborting." |
| 429 | exit 1 |
| 430 | fi |
| 431 | ;; |
| 432 | --wait | -w) |
| 433 | # Code to run if $1 matched "--wait" or "-w": |
| 434 | echoerr "DEBUG: OpenTimestamps '--wait' option selected." |
| 435 | OPTION_OPENTIMESTAMPS_WAIT="true" |
| 436 | ;; |
| 437 | --output | -o) |
| 438 | # Code to run if $1 matched "--output" or "-o": |
| 439 | # Check that argument $2 is a directory. |
| 440 | if [[ -d $2 ]]; then |
| 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. |
| 445 | else |
| 446 | echoerr "ERROR: Invalid argument provided as output directory for proof files:""$2" |
| 447 | exit 1 |
| 448 | fi |
| 449 | ;; |
| 450 | *) |
| 451 | # Code to run if $1 isn't any of the above cases: |
| 452 | # Check that remaining argument is a directory. |
| 453 | if [[ -d $1 ]]; then |
| 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 ) |
| 457 | else |
| 458 | echoerr "ERROR: Remaining argument is not a directory or valid option. Exiting." |
| 459 | exit 1 |
| 460 | fi |
| 461 | ;; |
| 462 | esac |
| 463 | shift # Remove first argument ($1) so remaining arguments can be processed on next loop. |
| 464 | done |
| 465 | |
| 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" |
| 470 | fi |
| 471 | |
| 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 ','." |
| 475 | OPTION_DELIMITER="," |
| 476 | fi |
| 477 | |
| 478 | # Exit program if directoriesToProcess array is empty. |
| 479 | if [[ ${#directoriesToProcess[@]} -eq 0 ]]; then |
| 480 | echoerr "ERROR: No valid directory specified." |
| 481 | exit 1 |
| 482 | fi |
| 483 | |
| 484 | ###echoerr "DEBUG: === INPUT PROCESSING END ===" |
| 485 | |
| 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" |
| 499 | |
| 500 | ## TEMP REF: |
| 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 |
| 510 | |
| 511 | # ==== GENERATE HASH LIST(S) ==== |
| 512 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: ==== GENERATING HASH LIST(S) ====" |
| 513 | |
| 514 | # Assemble and write proofs to disk for all specified directories. |
| 515 | |
| 516 | # Generate proofs from all provided directories. |
| 517 | for j in "${!directoriesToProcess[@]}"; do |
| 518 | writeProofs $j "${directoriesToProcess[j]}" |
| 519 | done |
| 520 | |
| 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 |
| 529 | fi |
| 530 | |
| 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]}") |
| 536 | done |
| 537 | |
| 538 | # ==== GENERATE OPENTIMESTAMPS TIMESTAMP FILE(S) ==== |
| 539 | |
| 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" |
| 545 | |
| 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." |
| 551 | else |
| 552 | otsWaitOption="" |
| 553 | otsWaitTimeCmd="" |
| 554 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: OpenTimestamps '--wait' option not active." |
| 555 | fi |
| 556 | |
| 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 |
| 565 | |
| 566 | fi |
| 567 | |
| 568 | |
| 569 | [[ $OPTION_VERBOSE == "true" ]] && echoerr "DEBUG: === MAIN PROGRAM END ===" |
| 570 | # Exit program successfully. |
| 571 | exit 0 |
| 572 | |
| 573 | |
| 574 | # == Dependencies == |
| 575 | # |
| 576 | # - bash, find, sort, echo, mkdir, basename, awk, wc, date, dd, |
| 577 | # readlink, touch, ots, |
| 578 | |
| 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. |
| 584 | |
| 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) |
| 592 | |
| 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. |
| 599 | |
| 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. |
| 606 | |
| 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. |
| 613 | |
| 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. |
| 620 | |
| 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/. |
| 633 | |
| 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. |
| 640 | |
| 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. |
| 647 | |
| 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. |
| 654 | |
| 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. |
| 661 | |
| 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. |
| 669 | |
| 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. |