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