From a4fcba60e2a21fb05f9a965cafabf1e43f24abbc Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Tue, 20 Sep 2022 21:24:49 +0000 Subject: [PATCH] feat(user/bksumsign.sh):Personal checksum script - Note: Permits signing and timestamping checksum with GnuPG and OpenTimestamps respectively. --- user/bksumsign.sh | 477 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100755 user/bksumsign.sh diff --git a/user/bksumsign.sh b/user/bksumsign.sh new file mode 100755 index 0000000..de17450 --- /dev/null +++ b/user/bksumsign.sh @@ -0,0 +1,477 @@ +#!/usr/bin/env bash +# Desc: Create and sign a checksum +# Input: stdin: file list (newline delimited) +# arg(s): file paths (IFS delimited) +# Output: file containing sha256 hashes and file paths +# Depends: bash v5.1.16, date (GNU Coreutils 8.32), gpg v2.2.27, ots v0.7.0 +# Version: 0.0.1 + +declare -Ag appRollCall # Associative array for storing app status +declare -Ag fileRollCall # Associative array for storing file status +declare -Ag dirRollCall # Associative array for storing dir status +declare -ag arrPosArgs; # positional arguments +declare -ag arrStdin; # standard input lines +declare -ag arrInFiles; # input files + +yell() { echo "$0: $*" >&2; } # print script path and all args to stderr +die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status +try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails +vbm() { + # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true". + # Usage: vbm "DEBUG :verbose message here" + # Version 0.2.0 + # Input: arg1: string + # vars: opVerbose + # Output: stderr + # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date) + + if [ "$opVerbose" = "true" ]; then + functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds. + echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text. + fi + + # End function + return 0; # Function finished. +} # Displays message if opVerbose true +showUsage() { + # Desc: Display script usage information + # Usage: showUsage + # Version 0.0.2 + # Input: none + # Output: stdout + # Depends: GNU-coreutils 8.30 (cat) + cat <<'EOF' + USAGE: + bksumsign.sh [ options ] [FILE...] + + DESCRIPTION: + Creates sha256 checksum of files. + + OPTIONS: + -h, --help + Display help information. + --version + Display script version. + -v, --verbose + Display debugging info. + -o, --output-file + Define output file path. By default, the file + name includes the full ISO-8601 timestamp + without separators, e.g.: + 20220920T2117+0000..SHA256SUM + -s, --sign + Sign with GnuPG the checksum file. + -t, --timestamp + Timestamp with OpenTimestamps the checksum file + (and GnuPG signature if -s/--sign specified). + -- + Indicate end of options. + + EXAMPLE: + bksumsign.sh file.txt + bksumsign.sh file1.txt file2.txt + bksumsign.sh -v -- file1.txt file2.txt + bksumsign.sh -v -t -- file1.txt file2.txt + find . -type f | bksumsign.sh + find . -type f | bksumsign.sh -v -s -t -- file.txt + + NOTE: + If GNU Coreutils 8.32 `sha256sum` used, checksum file + can be verified using: + sha256sum --check 20220920T2117+0000..SHA256SUM +EOF +} # Display information on how to use this script. +showVersion() { + # Desc: Displays script version and license information. + # Usage: showVersion + # Version: 0.0.2 + # Input: scriptVersion var containing version string + # Output: stdout + # Depends: vbm(), yell, GNU-coreutils 8.30 + + # Initialize function + vbm "DEBUG:showVersion function called." + + cat <<'EOF' +bksumsign 0.0.1 +Copyright (C) 2022 Steven Baltakatei Sandoval +License GPLv3: GNU GPL version 3 +This is free software; you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +EOF + + # End function + vbm "DEBUG:showVersion function ended." + return 0; # Function finished. +} # Display script version. +checkapp() { + # Desc: If arg is a command, save result in assoc array 'appRollCall' + # Usage: checkapp arg1 arg2 arg3 ... + # Version: 0.1.1 + # Input: global assoc. array 'appRollCall' + # Output: adds/updates key(value) to global assoc array 'appRollCall' + # Depends: bash 5.0.3 + local returnState + + #===Process Args=== + for arg in "$@"; do + if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command + appRollCall[$arg]="true"; + if ! [ "$returnState" = "false" ]; then returnState="true"; fi; + else + appRollCall[$arg]="false"; returnState="false"; + fi; + done; + + #===Determine function return code=== + if [ "$returnState" = "true" ]; then + return 0; + else + return 1; + fi; +} # Check that app exists +checkfile() { + # Desc: If arg is a file path, save result in assoc array 'fileRollCall' + # Usage: checkfile arg1 arg2 arg3 ... + # Version: 0.1.2 + # Input: global assoc. array 'fileRollCall' + # Output: adds/updates key(value) to global assoc array 'fileRollCall'; + # Output: returns 0 if app found, 1 otherwise + # Depends: bash 5.0.3 + local returnState + + #===Process Args=== + for arg in "$@"; do + if [ -f "$arg" ]; then + fileRollCall["$arg"]="true"; + if ! [ "$returnState" = "false" ]; then returnState="true"; fi; + elif [ -z "$arg" ]; then + fileRollCall["(no name)"]="false"; returnState="false"; + else + fileRollCall["$arg"]="false"; returnState="false"; + fi; + done; + + #===Determine function return code=== + if [ "$returnState" = "true" ]; then + return 0; + else + return 1; + fi; +} # Check that file exists +checkdir() { + # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' + # Usage: checkdir arg1 arg2 arg3 ... + # Version 0.1.2 + # Input: global assoc. array 'dirRollCall' + # Output: adds/updates key(value) to global assoc array 'dirRollCall'; + # Output: returns 0 if all args are dirs; 1 otherwise + # Depends: Bash 5.0.3 + local returnState + + #===Process Args=== + for arg in "$@"; do + if [ -z "$arg" ]; then + dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false"; + elif [ -d "$arg" ]; then + dirRollCall["$arg"]="true"; + if ! [ "$returnState" = "false" ]; then returnState="true"; fi + else + dirRollCall["$arg"]="false"; returnState="false"; + fi + done + + #===Determine function return code=== + if [ "$returnState" = "true" ]; then + return 0; + else + return 1; + fi +} # Check that dir exists +displayMissing() { + # Desc: Displays missing apps, files, and dirs + # Usage: displayMissing + # Version 1.0.0 + # Input: associative arrays: appRollCall, fileRollCall, dirRollCall + # Output: stderr: messages indicating missing apps, file, or dirs + # Output: returns exit code 0 if nothing missing; 1 otherwise + # Depends: bash 5, checkAppFileDir() + local missingApps value appMissing missingFiles fileMissing + local missingDirs dirMissing + + #==BEGIN Display errors== + #===BEGIN Display Missing Apps=== + missingApps="Missing apps :"; + #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done + for key in "${!appRollCall[@]}"; do + value="${appRollCall[$key]}"; + if [ "$value" = "false" ]; then + #echo "DEBUG:Missing apps: $key => $value"; + missingApps="$missingApps""$key "; + appMissing="true"; + fi; + done; + if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing. + echo "$missingApps" 1>&2; + fi; + unset value; + #===END Display Missing Apps=== + + #===BEGIN Display Missing Files=== + missingFiles="Missing files:"; + #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done + for key in "${!fileRollCall[@]}"; do + value="${fileRollCall[$key]}"; + if [ "$value" = "false" ]; then + #echo "DEBUG:Missing files: $key => $value"; + missingFiles="$missingFiles""$key "; + fileMissing="true"; + fi; + done; + if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing. + echo "$missingFiles" 1>&2; + fi; + unset value; + #===END Display Missing Files=== + + #===BEGIN Display Missing Directories=== + missingDirs="Missing dirs:"; + #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done + for key in "${!dirRollCall[@]}"; do + value="${dirRollCall[$key]}"; + if [ "$value" = "false" ]; then + #echo "DEBUG:Missing dirs: $key => $value"; + missingDirs="$missingDirs""$key "; + dirMissing="true"; + fi; + done; + if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing. + echo "$missingDirs" 1>&2; + fi; + unset value; + #===END Display Missing Directories=== + + #==END Display errors== + #==BEGIN Determine function return code=== + if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then + return 1; + else + return 0; + fi + #==END Determine function return code=== +} # Display missing apps, files, dirs +processArgs() { + # Desc: Processes arguments provided to script + # Usage: processArgs "$@" + # Version: 1.0.0 (modified) + # Input: "$@" (list of arguments provided to the function) + # Output: Sets following variables used by other functions: + # opVerbose Indicates verbose mode enable status. (ex: "true", "false") + # pathDirOut1 Path to output directory. + # pathFileOut1 Path to output file. + # opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false"). + # arrPosArgs Array of remaining positional argments + # Depends: + # yell() Displays messages to stderr. + # vbm() Displays messsages to stderr if opVerbose set to "true". + # showUsage() Displays usage information about parent script. + # showVersion() Displays version about parent script. + # arrPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option). + # External dependencies: bash (5.1.16), echo + # Ref./Attrib.: + # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347 + # [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams + + # Initialize function + vbm "DEBUG:processArgs function called." + + # Perform work + while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0). + #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1]. + #yell "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1]. + case "$1" in + -h | --help) showUsage; exit 1;; # Display usage. + --version) showVersion; exit 1;; # Show version + -v | --verbose) opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1]. + -o | --output-file) # Define output file path + if [ -f "$2" ]; then # If $2 is file that exists, prompt user to continue to overwrite, set pathFileOut1 to $2, pop $2. + yell "Specified output file $2 already exists. Overwrite? (y/n):" + read -r m; case $m in + y | Y | yes) opFileOut1_overwrite="true";; + n | N | no) opFileOut1_overwrite="false";; + *) yell "Invalid selection. Exiting."; exit 1;; + esac + if [ "$opFileOut1_overwrite" == "true" ]; then + pathFileOut1="$2"; + shift; + vbm "DEBUG:Output file pathFileOut1 set to:""$2"; + else + yell "ERORR:Exiting in order to not overwrite output file:""$pathFileOut1"; + exit 1; + fi + else + pathFileOut1="$2"; + shift; + vbm "DEBUG:Output file pathFileOut1 set to:""$2"; + fi ;; + -s | --sign) # sign with gpg + opSign="true"; vbm "DEBUG:Signing mode enabled.";; + -t | --timestamp) # timestamp with ots + opTimestamp="true"; vbm "DEBUG:Timestamp mode enabled.";; + --) # End of all options. See [2]. + shift; + for arg in "$@"; do + vbm "DEBUG:adding to arrPosArgs:$arg"; + arrPosArgs+=("$arg"); + done; + break;; + -*) showUsage; yell "ERROR: Unrecognized option."; exit 1;; # Display usage + *) + for arg in "$@"; do + vbm "DEBUG:adding to arrPosArgs:$arg"; + arrPosArgs+=("$arg"); + done; + break;; + esac; + shift; + done; + + # End function + vbm "DEBUG:processArgs function ended." + return 0; # Function finished. +} # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.). +processStdin() { + # Desc: Save stdin lines to array + # Input: stdin standard input + # arrStdin array for storing stdin lines + # Output: arrStdin array for storing stdin lines + # Ref/Attrib: [1] https://unix.stackexchange.com/a/484643 Check if no command line arguments and STDIN is empty + + if [[ -t 0 ]]; then + return 1; # error if file descriptor 0 (stdin) open + else + while read -r line; do + arrStdin+=("$line"); done; + return 0; + fi; +}; # Save stdin to array +checkInput() { + # Desc: Check that files (specified by positional arguments and + # standard input lines) exist and are in the same directory. + # Input: arrPosArgs[@] positional arguments + # arrStdin[@] standard input lines + # Output: return code 0 success + # return code 1 failure + # arrInFiles list of verified files + # Depends: displayMissing(), checkfile(); + local flagMissing flagDirErr workDir; + + # Check that positional arguments are files + for elem in "${arrPosArgs[@]}"; do + if checkfile "$elem"; then + arrInFiles+=("$elem"); + else + flagMissing="true"; + fi; + done; + + # Check that stdin lines are files + for elem in "${arrStdin[@]}"; do + if checkfile "$elem"; then + arrInFiles+=("$elem"); + else + flagMissing="true"; + fi; + done; + + # Check that all files are in the same directory + if [[ "$flagMissing" != "true" ]]; then + workDir="$( dirname "$( readlink -f "${arrInFiles[0]}" )" )"; + for elem in "${arrInFiles[@]}"; do + elem="$(readlink -f "$elem")"; # full path + if [[ "$(dirname "$elem")" != "$workDir" ]]; then + flagDirErr="true"; + fi; + done; + fi; + + # Return non-zero if displayMissing() reports files missing. + if [[ "$flagMissing" == "true" ]]; then + displayMissing; return 1; fi; + if [[ "$flagDirErr" == "true" ]]; then + yell "ERROR:All files not in same directory."; + return 1; fi; + return 0; +}; # Check positional arguments +checkDepends() { + # Desc: Check if expected commands available + + checkapp date sha256sum; + if [[ $opSign == "true" ]]; then checkapp gpg; fi; + if [[ $opTimestamp == "true" ]]; then checkapp ots; fi; + + # Return failure is displayMissing() reports apps missing. + if ! displayMissing; then return 1; else return 0; fi; +}; # Check dependencies +main() { + # Input: pathFileOut1 option-specified output file path + # appRollCall assoc-array for checkapp(), displayMissing() + # fileRollCall assoc-array for checkfile(), displayMissing() + # dirRollCall assoc-array for checkdir(), displayMissing() + # arrPosArgs array for processArgs() + # arrStdin array for processStdin() + # arrInFiles array for checkInput() + # (args) for processArgs() + # (stdin) for processStdin() + # Output: file written to $pathSumOut + # file written to $pathSigOut + # + local fileSumOut dirOut pathSumOut pathSigOut; + + # Check dependencies and input + if ! checkDepends; then die "FATAL:Missing apps."; fi; + processArgs "$@"; + processStdin; + vbm "DEBUG:$(declare -p arrPosArgs)"; + vbm "DEBUG:$(declare -p arrStdin)"; + if ! [[ "${#arrPosArgs[@]}" -ge 1 || "${#arrStdin[@]}" -ge 1 ]]; then + yell "ERROR:No positional arguments or stdin lines."; + showUsage; exit 1; fi; + if ! checkInput; then die "FATAL:Invalid file list."; fi; + vbm "DEBUG:$(declare -p arrInFiles)"; + + # Do work + if [[ -n "$pathFileOut1" ]]; then + pathSumOut="$pathFileOut1"; + else + dirOut="$( dirname "${arrInFiles[0]}" )"; + fileSumOut="$(date +%Y%m%dT%H%M%S%z)..SHA256SUMS"; + pathSumOut="$dirOut"/"$fileSumOut"; + fi; + pathSigOut="$pathSumOut".asc; + for file in "${arrInFiles[@]}"; do + sha256sum "$file" >> "$pathSumOut"; + done; + + # Do optional work + ## Sign checksum file. + if [[ $opSign == "true" ]]; then + try gpg --detach-sign --armor --output "$pathSigOut" "$pathSumOut"; + fi; + ## Timestamp checksum file. + if [[ $opTimestamp == "true" ]]; then + try ots s "$pathSumOut"; fi; + + ## Timestamp checksum signature file. + if [[ $opTimestamp == "true" && $opSign == "true" ]]; then + try ots s "$pathSigOut"; + fi; + + return 0; +}; # main program + +main "$@"; + +# Author: Steven Baltakatei Sandoval +# License: GPLv3+ -- 2.30.2