de174508c0d38732812ccc02018d5244a307a1f8
[BK-2020-03.git] / user / bksumsign.sh
1 #!/usr/bin/env bash
2 # Desc: Create and sign a checksum
3 # Input: stdin: file list (newline delimited)
4 # arg(s): file paths (IFS delimited)
5 # Output: file containing sha256 hashes and file paths
6 # Depends: bash v5.1.16, date (GNU Coreutils 8.32), gpg v2.2.27, ots v0.7.0
7 # Version: 0.0.1
8
9 declare -Ag appRollCall # Associative array for storing app status
10 declare -Ag fileRollCall # Associative array for storing file status
11 declare -Ag dirRollCall # Associative array for storing dir status
12 declare -ag arrPosArgs; # positional arguments
13 declare -ag arrStdin; # standard input lines
14 declare -ag arrInFiles; # input files
15
16 yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
17 die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
18 try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
19 vbm() {
20 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
21 # Usage: vbm "DEBUG :verbose message here"
22 # Version 0.2.0
23 # Input: arg1: string
24 # vars: opVerbose
25 # Output: stderr
26 # Depends: bash 5.1.16, GNU-coreutils 8.30 (echo, date)
27
28 if [ "$opVerbose" = "true" ]; then
29 functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds.
30 echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text.
31 fi
32
33 # End function
34 return 0; # Function finished.
35 } # Displays message if opVerbose true
36 showUsage() {
37 # Desc: Display script usage information
38 # Usage: showUsage
39 # Version 0.0.2
40 # Input: none
41 # Output: stdout
42 # Depends: GNU-coreutils 8.30 (cat)
43 cat <<'EOF'
44 USAGE:
45 bksumsign.sh [ options ] [FILE...]
46
47 DESCRIPTION:
48 Creates sha256 checksum of files.
49
50 OPTIONS:
51 -h, --help
52 Display help information.
53 --version
54 Display script version.
55 -v, --verbose
56 Display debugging info.
57 -o, --output-file
58 Define output file path. By default, the file
59 name includes the full ISO-8601 timestamp
60 without separators, e.g.:
61 20220920T2117+0000..SHA256SUM
62 -s, --sign
63 Sign with GnuPG the checksum file.
64 -t, --timestamp
65 Timestamp with OpenTimestamps the checksum file
66 (and GnuPG signature if -s/--sign specified).
67 --
68 Indicate end of options.
69
70 EXAMPLE:
71 bksumsign.sh file.txt
72 bksumsign.sh file1.txt file2.txt
73 bksumsign.sh -v -- file1.txt file2.txt
74 bksumsign.sh -v -t -- file1.txt file2.txt
75 find . -type f | bksumsign.sh
76 find . -type f | bksumsign.sh -v -s -t -- file.txt
77
78 NOTE:
79 If GNU Coreutils 8.32 `sha256sum` used, checksum file
80 can be verified using:
81 sha256sum --check 20220920T2117+0000..SHA256SUM
82 EOF
83 } # Display information on how to use this script.
84 showVersion() {
85 # Desc: Displays script version and license information.
86 # Usage: showVersion
87 # Version: 0.0.2
88 # Input: scriptVersion var containing version string
89 # Output: stdout
90 # Depends: vbm(), yell, GNU-coreutils 8.30
91
92 # Initialize function
93 vbm "DEBUG:showVersion function called."
94
95 cat <<'EOF'
96 bksumsign 0.0.1
97 Copyright (C) 2022 Steven Baltakatei Sandoval
98 License GPLv3: GNU GPL version 3
99 This is free software; you are free to change and redistribute it.
100 There is NO WARRANTY, to the extent permitted by law.
101
102 EOF
103
104 # End function
105 vbm "DEBUG:showVersion function ended."
106 return 0; # Function finished.
107 } # Display script version.
108 checkapp() {
109 # Desc: If arg is a command, save result in assoc array 'appRollCall'
110 # Usage: checkapp arg1 arg2 arg3 ...
111 # Version: 0.1.1
112 # Input: global assoc. array 'appRollCall'
113 # Output: adds/updates key(value) to global assoc array 'appRollCall'
114 # Depends: bash 5.0.3
115 local returnState
116
117 #===Process Args===
118 for arg in "$@"; do
119 if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
120 appRollCall[$arg]="true";
121 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
122 else
123 appRollCall[$arg]="false"; returnState="false";
124 fi;
125 done;
126
127 #===Determine function return code===
128 if [ "$returnState" = "true" ]; then
129 return 0;
130 else
131 return 1;
132 fi;
133 } # Check that app exists
134 checkfile() {
135 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
136 # Usage: checkfile arg1 arg2 arg3 ...
137 # Version: 0.1.2
138 # Input: global assoc. array 'fileRollCall'
139 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
140 # Output: returns 0 if app found, 1 otherwise
141 # Depends: bash 5.0.3
142 local returnState
143
144 #===Process Args===
145 for arg in "$@"; do
146 if [ -f "$arg" ]; then
147 fileRollCall["$arg"]="true";
148 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
149 elif [ -z "$arg" ]; then
150 fileRollCall["(no name)"]="false"; returnState="false";
151 else
152 fileRollCall["$arg"]="false"; returnState="false";
153 fi;
154 done;
155
156 #===Determine function return code===
157 if [ "$returnState" = "true" ]; then
158 return 0;
159 else
160 return 1;
161 fi;
162 } # Check that file exists
163 checkdir() {
164 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
165 # Usage: checkdir arg1 arg2 arg3 ...
166 # Version 0.1.2
167 # Input: global assoc. array 'dirRollCall'
168 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
169 # Output: returns 0 if all args are dirs; 1 otherwise
170 # Depends: Bash 5.0.3
171 local returnState
172
173 #===Process Args===
174 for arg in "$@"; do
175 if [ -z "$arg" ]; then
176 dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false";
177 elif [ -d "$arg" ]; then
178 dirRollCall["$arg"]="true";
179 if ! [ "$returnState" = "false" ]; then returnState="true"; fi
180 else
181 dirRollCall["$arg"]="false"; returnState="false";
182 fi
183 done
184
185 #===Determine function return code===
186 if [ "$returnState" = "true" ]; then
187 return 0;
188 else
189 return 1;
190 fi
191 } # Check that dir exists
192 displayMissing() {
193 # Desc: Displays missing apps, files, and dirs
194 # Usage: displayMissing
195 # Version 1.0.0
196 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
197 # Output: stderr: messages indicating missing apps, file, or dirs
198 # Output: returns exit code 0 if nothing missing; 1 otherwise
199 # Depends: bash 5, checkAppFileDir()
200 local missingApps value appMissing missingFiles fileMissing
201 local missingDirs dirMissing
202
203 #==BEGIN Display errors==
204 #===BEGIN Display Missing Apps===
205 missingApps="Missing apps :";
206 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
207 for key in "${!appRollCall[@]}"; do
208 value="${appRollCall[$key]}";
209 if [ "$value" = "false" ]; then
210 #echo "DEBUG:Missing apps: $key => $value";
211 missingApps="$missingApps""$key ";
212 appMissing="true";
213 fi;
214 done;
215 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
216 echo "$missingApps" 1>&2;
217 fi;
218 unset value;
219 #===END Display Missing Apps===
220
221 #===BEGIN Display Missing Files===
222 missingFiles="Missing files:";
223 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
224 for key in "${!fileRollCall[@]}"; do
225 value="${fileRollCall[$key]}";
226 if [ "$value" = "false" ]; then
227 #echo "DEBUG:Missing files: $key => $value";
228 missingFiles="$missingFiles""$key ";
229 fileMissing="true";
230 fi;
231 done;
232 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
233 echo "$missingFiles" 1>&2;
234 fi;
235 unset value;
236 #===END Display Missing Files===
237
238 #===BEGIN Display Missing Directories===
239 missingDirs="Missing dirs:";
240 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
241 for key in "${!dirRollCall[@]}"; do
242 value="${dirRollCall[$key]}";
243 if [ "$value" = "false" ]; then
244 #echo "DEBUG:Missing dirs: $key => $value";
245 missingDirs="$missingDirs""$key ";
246 dirMissing="true";
247 fi;
248 done;
249 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
250 echo "$missingDirs" 1>&2;
251 fi;
252 unset value;
253 #===END Display Missing Directories===
254
255 #==END Display errors==
256 #==BEGIN Determine function return code===
257 if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then
258 return 1;
259 else
260 return 0;
261 fi
262 #==END Determine function return code===
263 } # Display missing apps, files, dirs
264 processArgs() {
265 # Desc: Processes arguments provided to script
266 # Usage: processArgs "$@"
267 # Version: 1.0.0 (modified)
268 # Input: "$@" (list of arguments provided to the function)
269 # Output: Sets following variables used by other functions:
270 # opVerbose Indicates verbose mode enable status. (ex: "true", "false")
271 # pathDirOut1 Path to output directory.
272 # pathFileOut1 Path to output file.
273 # opFileOut1_overwrite Indicates whether file pathFileOut1 should be overwritten (ex: "true", "false").
274 # arrPosArgs Array of remaining positional argments
275 # Depends:
276 # yell() Displays messages to stderr.
277 # vbm() Displays messsages to stderr if opVerbose set to "true".
278 # showUsage() Displays usage information about parent script.
279 # showVersion() Displays version about parent script.
280 # arrPosArgs Global array for storing non-option positional arguments (i.e. arguments following the `--` option).
281 # External dependencies: bash (5.1.16), echo
282 # Ref./Attrib.:
283 # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
284 # [2]: "Handling positional parameters" (2018-05-12). https://wiki.bash-hackers.org/scripting/posparams
285
286 # Initialize function
287 vbm "DEBUG:processArgs function called."
288
289 # Perform work
290 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
291 #yell "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1].
292 #yell "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1].
293 case "$1" in
294 -h | --help) showUsage; exit 1;; # Display usage.
295 --version) showVersion; exit 1;; # Show version
296 -v | --verbose) opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
297 -o | --output-file) # Define output file path
298 if [ -f "$2" ]; then # If $2 is file that exists, prompt user to continue to overwrite, set pathFileOut1 to $2, pop $2.
299 yell "Specified output file $2 already exists. Overwrite? (y/n):"
300 read -r m; case $m in
301 y | Y | yes) opFileOut1_overwrite="true";;
302 n | N | no) opFileOut1_overwrite="false";;
303 *) yell "Invalid selection. Exiting."; exit 1;;
304 esac
305 if [ "$opFileOut1_overwrite" == "true" ]; then
306 pathFileOut1="$2";
307 shift;
308 vbm "DEBUG:Output file pathFileOut1 set to:""$2";
309 else
310 yell "ERORR:Exiting in order to not overwrite output file:""$pathFileOut1";
311 exit 1;
312 fi
313 else
314 pathFileOut1="$2";
315 shift;
316 vbm "DEBUG:Output file pathFileOut1 set to:""$2";
317 fi ;;
318 -s | --sign) # sign with gpg
319 opSign="true"; vbm "DEBUG:Signing mode enabled.";;
320 -t | --timestamp) # timestamp with ots
321 opTimestamp="true"; vbm "DEBUG:Timestamp mode enabled.";;
322 --) # End of all options. See [2].
323 shift;
324 for arg in "$@"; do
325 vbm "DEBUG:adding to arrPosArgs:$arg";
326 arrPosArgs+=("$arg");
327 done;
328 break;;
329 -*) showUsage; yell "ERROR: Unrecognized option."; exit 1;; # Display usage
330 *)
331 for arg in "$@"; do
332 vbm "DEBUG:adding to arrPosArgs:$arg";
333 arrPosArgs+=("$arg");
334 done;
335 break;;
336 esac;
337 shift;
338 done;
339
340 # End function
341 vbm "DEBUG:processArgs function ended."
342 return 0; # Function finished.
343 } # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
344 processStdin() {
345 # Desc: Save stdin lines to array
346 # Input: stdin standard input
347 # arrStdin array for storing stdin lines
348 # Output: arrStdin array for storing stdin lines
349 # Ref/Attrib: [1] https://unix.stackexchange.com/a/484643 Check if no command line arguments and STDIN is empty
350
351 if [[ -t 0 ]]; then
352 return 1; # error if file descriptor 0 (stdin) open
353 else
354 while read -r line; do
355 arrStdin+=("$line"); done;
356 return 0;
357 fi;
358 }; # Save stdin to array
359 checkInput() {
360 # Desc: Check that files (specified by positional arguments and
361 # standard input lines) exist and are in the same directory.
362 # Input: arrPosArgs[@] positional arguments
363 # arrStdin[@] standard input lines
364 # Output: return code 0 success
365 # return code 1 failure
366 # arrInFiles list of verified files
367 # Depends: displayMissing(), checkfile();
368 local flagMissing flagDirErr workDir;
369
370 # Check that positional arguments are files
371 for elem in "${arrPosArgs[@]}"; do
372 if checkfile "$elem"; then
373 arrInFiles+=("$elem");
374 else
375 flagMissing="true";
376 fi;
377 done;
378
379 # Check that stdin lines are files
380 for elem in "${arrStdin[@]}"; do
381 if checkfile "$elem"; then
382 arrInFiles+=("$elem");
383 else
384 flagMissing="true";
385 fi;
386 done;
387
388 # Check that all files are in the same directory
389 if [[ "$flagMissing" != "true" ]]; then
390 workDir="$( dirname "$( readlink -f "${arrInFiles[0]}" )" )";
391 for elem in "${arrInFiles[@]}"; do
392 elem="$(readlink -f "$elem")"; # full path
393 if [[ "$(dirname "$elem")" != "$workDir" ]]; then
394 flagDirErr="true";
395 fi;
396 done;
397 fi;
398
399 # Return non-zero if displayMissing() reports files missing.
400 if [[ "$flagMissing" == "true" ]]; then
401 displayMissing; return 1; fi;
402 if [[ "$flagDirErr" == "true" ]]; then
403 yell "ERROR:All files not in same directory.";
404 return 1; fi;
405 return 0;
406 }; # Check positional arguments
407 checkDepends() {
408 # Desc: Check if expected commands available
409
410 checkapp date sha256sum;
411 if [[ $opSign == "true" ]]; then checkapp gpg; fi;
412 if [[ $opTimestamp == "true" ]]; then checkapp ots; fi;
413
414 # Return failure is displayMissing() reports apps missing.
415 if ! displayMissing; then return 1; else return 0; fi;
416 }; # Check dependencies
417 main() {
418 # Input: pathFileOut1 option-specified output file path
419 # appRollCall assoc-array for checkapp(), displayMissing()
420 # fileRollCall assoc-array for checkfile(), displayMissing()
421 # dirRollCall assoc-array for checkdir(), displayMissing()
422 # arrPosArgs array for processArgs()
423 # arrStdin array for processStdin()
424 # arrInFiles array for checkInput()
425 # (args) for processArgs()
426 # (stdin) for processStdin()
427 # Output: file written to $pathSumOut
428 # file written to $pathSigOut
429 #
430 local fileSumOut dirOut pathSumOut pathSigOut;
431
432 # Check dependencies and input
433 if ! checkDepends; then die "FATAL:Missing apps."; fi;
434 processArgs "$@";
435 processStdin;
436 vbm "DEBUG:$(declare -p arrPosArgs)";
437 vbm "DEBUG:$(declare -p arrStdin)";
438 if ! [[ "${#arrPosArgs[@]}" -ge 1 || "${#arrStdin[@]}" -ge 1 ]]; then
439 yell "ERROR:No positional arguments or stdin lines.";
440 showUsage; exit 1; fi;
441 if ! checkInput; then die "FATAL:Invalid file list."; fi;
442 vbm "DEBUG:$(declare -p arrInFiles)";
443
444 # Do work
445 if [[ -n "$pathFileOut1" ]]; then
446 pathSumOut="$pathFileOut1";
447 else
448 dirOut="$( dirname "${arrInFiles[0]}" )";
449 fileSumOut="$(date +%Y%m%dT%H%M%S%z)..SHA256SUMS";
450 pathSumOut="$dirOut"/"$fileSumOut";
451 fi;
452 pathSigOut="$pathSumOut".asc;
453 for file in "${arrInFiles[@]}"; do
454 sha256sum "$file" >> "$pathSumOut";
455 done;
456
457 # Do optional work
458 ## Sign checksum file.
459 if [[ $opSign == "true" ]]; then
460 try gpg --detach-sign --armor --output "$pathSigOut" "$pathSumOut";
461 fi;
462 ## Timestamp checksum file.
463 if [[ $opTimestamp == "true" ]]; then
464 try ots s "$pathSumOut"; fi;
465
466 ## Timestamp checksum signature file.
467 if [[ $opTimestamp == "true" && $opSign == "true" ]]; then
468 try ots s "$pathSigOut";
469 fi;
470
471 return 0;
472 }; # main program
473
474 main "$@";
475
476 # Author: Steven Baltakatei Sandoval
477 # License: GPLv3+