feat(user/bkdatev): Add macOS BSD date compatibility
[BK-2020-03.git] / unitproc / bkagedecrypt
1 #!/bin/bash
2 # Desc: Decrypts files encrypted with age
3 # Usage: bkagedecrypt -i key.txt file1 file2 ...
4 # Version: 0.0.1
5
6 #==BEGIN Define script parameters==
7 #===BEGIN Define variables===
8 declare -g runFlag # If "false", indicates exit required
9 declare -ag inputFilePaths # Array to store input file paths
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
14 timeScriptStartNs="$(date +%Y%m%dT%H%M%S.%N%z)";
15 dirTemp="/tmp/$timeScriptStartNs"..bkagedecrypt; # will be automatically deleted
16 #===END Define variables===
17
18 #===BEGIN Declare local script functions===
19 yell() { echo "$0: $*" >&2; } # Yell, Die, Try Three-Fingered Claw technique; # Ref/Attrib: https://stackoverflow.com/a/25515370
20 die() { yell "$*"; exit 111; }
21 try() { "$@" || die "cannot $*"; }
22 vbm() {
23 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
24 # Usage: vbm "DEBUG :verbose message here"
25 # Version 0.2.0
26 # Input: arg1: string
27 # vars: opVerbose
28 # Output: stderr
29 # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date)
30
31 if [ "$opVerbose" = "true" ]; then
32 functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds.
33 echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text.
34 fi
35
36 # End function
37 return 0; # Function finished.
38 } # Displays message if opVerbose true
39 checkapp() {
40 # Desc: If arg is a command, save result in assoc array 'appRollCall'
41 # Usage: checkapp arg1 arg2 arg3 ...
42 # Version: 0.1.1
43 # Input: global assoc. array 'appRollCall'
44 # Output: adds/updates key(value) to global assoc array 'appRollCall'
45 # Depends: bash 5.0.3
46 local returnState
47
48 #===Process Args===
49 for arg in "$@"; do
50 if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
51 appRollCall[$arg]="true";
52 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
53 else
54 appRollCall[$arg]="false"; returnState="false";
55 fi;
56 done;
57
58 #===Determine function return code===
59 if [ "$returnState" = "true" ]; then
60 return 0;
61 else
62 return 1;
63 fi;
64 } # Check that app exists
65 checkfile() {
66 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
67 # Usage: checkfile arg1 arg2 arg3 ...
68 # Version: 0.1.1
69 # Input: global assoc. array 'fileRollCall'
70 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
71 # Output: returns 0 if app found, 1 otherwise
72 # Depends: bash 5.0.3
73 local returnState
74
75 #===Process Args===
76 for arg in "$@"; do
77 if [ -f "$arg" ]; then
78 fileRollCall["$arg"]="true";
79 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
80 else
81 fileRollCall["$arg"]="false"; returnState="false";
82 fi;
83 done;
84
85 #===Determine function return code===
86 if [ "$returnState" = "true" ]; then
87 return 0;
88 else
89 return 1;
90 fi;
91 } # Check that file exists
92 checkdir() {
93 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
94 # Usage: checkdir arg1 arg2 arg3 ...
95 # Version 0.1.1
96 # Input: global assoc. array 'dirRollCall'
97 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
98 # Output: returns 0 if app found, 1 otherwise
99 # Depends: Bash 5.0.3
100 local returnState
101
102 #===Process Args===
103 for arg in "$@"; do
104 if [ -d "$arg" ]; then
105 dirRollCall["$arg"]="true";
106 if ! [ "$returnState" = "false" ]; then returnState="true"; fi
107 else
108 dirRollCall["$arg"]="false"; returnState="false";
109 fi
110 done
111
112 #===Determine function return code===
113 if [ "$returnState" = "true" ]; then
114 return 0;
115 else
116 return 1;
117 fi
118 } # Check that dir exists
119 displayMissing() {
120 # Desc: Displays missing apps, files, and dirs
121 # Usage: displayMissing
122 # Version 0.1.1
123 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
124 # Output: stderr: messages indicating missing apps, file, or dirs
125 # Depends: bash 5, checkAppFileDir()
126 local missingApps value appMissing missingFiles fileMissing
127 local missingDirs dirMissing
128
129 #==BEGIN Display errors==
130 #===BEGIN Display Missing Apps===
131 missingApps="Missing apps :";
132 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
133 for key in "${!appRollCall[@]}"; do
134 value="${appRollCall[$key]}";
135 if [ "$value" = "false" ]; then
136 #echo "DEBUG:Missing apps: $key => $value";
137 missingApps="$missingApps""$key ";
138 appMissing="true";
139 fi;
140 done;
141 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
142 echo "$missingApps" 1>&2;
143 fi;
144 unset value;
145 #===END Display Missing Apps===
146
147 #===BEGIN Display Missing Files===
148 missingFiles="Missing files:";
149 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
150 for key in "${!fileRollCall[@]}"; do
151 value="${fileRollCall[$key]}";
152 if [ "$value" = "false" ]; then
153 #echo "DEBUG:Missing files: $key => $value";
154 missingFiles="$missingFiles""$key ";
155 fileMissing="true";
156 fi;
157 done;
158 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
159 echo "$missingFiles" 1>&2;
160 fi;
161 unset value;
162 #===END Display Missing Files===
163
164 #===BEGIN Display Missing Directories===
165 missingDirs="Missing dirs:";
166 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
167 for key in "${!dirRollCall[@]}"; do
168 value="${dirRollCall[$key]}";
169 if [ "$value" = "false" ]; then
170 #echo "DEBUG:Missing dirs: $key => $value";
171 missingDirs="$missingDirs""$key ";
172 dirMissing="true";
173 fi;
174 done;
175 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
176 echo "$missingDirs" 1>&2;
177 fi;
178 unset value;
179 #===END Display Missing Directories===
180
181 #==END Display errors==
182 } # Display missing apps, files, dirs
183 showVersion() {
184 # Desc: Displays script version and license information.
185 # Usage: showVersion
186 # Version: 0.0.1 (modified)
187 # Input: scriptVersion var containing version string
188 # Output: stdout
189 # Depends: vbm(), yell, GNU-coreutils 8.30
190
191 # Initialize function
192 vbm "DEBUG:showVersion function called."
193
194 cat <<'EOF'
195 bkagedecrypt 0.0.1
196 Copyright (C) 2021 Steven Baltakatei Sandoval
197 License GPLv3: GNU GPL version 3
198 This is free software; you are free to change and redistribute it.
199 There is NO WARRANTY, to the extent permitted by law.
200 EOF
201
202 # End function
203 vbm "DEBUG:showVersion function ended."
204 return 0; # Function finished.
205 } # Display script version.
206 processArgs() {
207 # Desc: Processes arguments provided to script.
208 # Usage: processArgs "$@"
209 # Version: 0.0.1 (modified)
210 # Input: "$@" (list of arguments provided to the function)
211 # Output: Sets following variables used by other functions:
212 # opVerbose Indicates verbose mode enable status. (ex: "true", "false")
213 # pathDirOut1 Path to output directory.
214 # inputFilePaths Array containing paths of files to decrypt
215 # Depends:
216 # yell() Displays messages to stderr.
217 # vbm() Displays messsages to stderr if opVerbose set to "true".
218 # showUsage() Displays usage information about parent script
219 # showVersion() Displays version about parent script
220 # checkfile() Checks if file exists
221 # checkdir() Checks if dir exists
222 # dirRollCall Assoc. array used by checkfile(), checkdir(), checkapp()
223 # fileRollCall Assoc. array used by checkfile(), checkdir(), checkapp()
224 # appRollCall Assoc. array used by checkfile(), checkdir(), checkapp()
225 # External dependencies: bash (5.0.3), echo
226 # Ref./Attrib.:
227 # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
228 vbm "STATUS:start processArgs()";
229
230 # Perform work
231 while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
232 #vbm "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1].
233 #vbm "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1].
234 case "$1" in
235 -h | --help) showUsage; exit 1;; # Display usage.
236 --version) showVersion; exit 1;; # Show version
237 -v | --verbose) # Enable verbose mode. See [1].
238 opVerbose="true";
239 vbm "DEBUG:Verbose mode enabled.";;
240 -i | --identity) # Define identity file
241 pathFileIdentity="$2";
242 shift;;
243 -O | --output-dir) # Define output directory path
244 pathDirOut1="$2";
245 vbm "DEBUG:Setting pathDirOut1 to:$pathDirOut1";
246 shift;;
247 *) inputFilePaths+=("$1"); # Add to inputArgs array
248 vbm "DEBUG:Added to inputFilePaths array:$1";
249 esac;
250 shift;
251 done;
252
253 # If pathDirOut1 not set, set as default
254 if [[ -z $pathDirOut1 ]]; then
255 pathDirOut1="$(pwd)"; # Fall back to using current working directory
256 vbm "DEBUG:pathDirOut1 not set. Setting to default:$pathDirOut1";
257 fi;
258
259 # Exit if pathFileIdentity not set
260 if [[ -z $pathFileIdentity ]]; then
261 showUsage;
262 die "ERROR:not set:pathFileIdentity:$pathFileIdentity";
263 fi;
264
265 # Check apps, dirs
266 if ! checkapp age tar gunzip; then runFlag="false"; fi;
267 if ! checkdir "$pathDirOut1"; then runFlag="false"; fi;
268 # Check files
269 ## Check identity file (required)
270 if ! checkfile "$pathFileIdentity"; then runFlag="false"; fi;
271 ## Check inputFilePaths
272 if [[ ${#inputFilePaths[@]} -eq 0 ]]; then
273 vbm "DEBUG:ERROR:inputFilePaths array empty:'${inputFilePaths[*]}'";
274 runFlag="false"; fi;
275 for path in "${inputFilePaths[@]}"; do
276 vbm "DEBUG:Checking if file path $path exists.";
277 if ! checkfile "$path"; then
278 runFlag="false";
279 vbm "DEBUG:ERROR:File does not exist:$path"; fi;
280 done;
281
282 # On error, display missing elements, exit.
283 if [[ $runFlag == "false" ]]; then
284 displayMissing;
285 showUsage;
286 die "ERROR:Input argument requirements unsatisfied. Exiting.";
287 else
288 vbm "STATUS:Input argument requirements satisfied.";
289 fi;
290
291 # End function
292 vbm "STATUS:end processArgs()";
293 return 0; # Function finished.
294 } # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
295 showUsage() {
296 # Desc: Display script usage information
297 # Usage: showUsage
298 # Version 0.0.1 (modified)
299 # Input: none
300 # Output: stdout
301 # Depends: GNU-coreutils 8.30 (cat)
302 cat <<'EOF'
303 NAME:
304 bkagedecrypt - decrypt age-encrypted files
305 USAGE:
306 bkagedecrypt [ options ] [FILE...]
307
308 DESCRIPTION:
309 Decrypt FILE(s) using `age` v1.0.0-rc.3. See:
310 https://github.com/FiloSottile/age
311
312 FILE(s) must have the following file name extensions and properties:
313 .gz.age.tar, File is a tar archive containing one or more
314 subfiles within. Each subfile contains an
315 age-encrypted, gzip-compressed plaintext file.
316 .gz.age, File is an age-encrypted gzip-compressed plaintext
317 file.
318 .age, File is an age-encrypted plaintext file.
319
320 Decryption via password is not supported.
321
322 OPTIONS:
323 -i, --identity KEY
324 Path of private key file passed to `age`.
325 -O, --output-dir
326 Define output directory path. (Default: current working dir)
327 -h, --help
328 Display help information.
329 --version
330 Display script version.
331 -v, --verbose
332 Display debugging info.
333
334 EXAMPLE:
335 $ bkagedecrypt -i key.txt foo.gz.age.tar
336 $ bkagedecrypt -i key.txt foo.gz.age.tar bar.gz.age baz.age
337 $ bkagedecrypt -i ky.txt -O ../ foo.gz.age.tar bar.gz.age baz.age
338
339 EOF
340 } # Display information on how to use this script.
341 extractGzAgeTar() {
342 # Desc: Extracts contents from .gz.age.tar
343 # Usage: extractGzAgeTar arg1
344 # Input: - arg1: path to file
345 # - pathFileIdentity: path to age identity file (for decryption)
346 # - pathDirOut1: path to output dir
347 # Output: file writes $pathDirOut1
348 # Depends: age v1.0.0-rpc3, GNU tar v1.30
349 vbm "STATUS:start extractGzAgeTar()";
350 vbm "args:$*";
351 vbm "pathFileIdentity:$pathFileIdentity";
352 vbm "pathDirOut1:$pathDirOut1";
353 local file
354 local -a fileNameList
355
356 # Get filename from path
357 file="$(basename "$1")";
358
359 # Get list of files from tar
360 while read -r line; do
361 fileNameList+=("$line");
362 vbm "Adding to fileNameList:$line";
363 done < <(try tar --list -f "$1");
364 vbm "STATUS:fileNameList:${fileNameList[*]}";
365
366 # Extract .gz.age files from tar to temporary dir
367 vbm "Extracting files from '$1' to '$dirTemp'";
368 try tar -xf "$1" -C "$dirTemp";
369
370 # Decrypt and decompress each .gz.age file to $pathDirOut1
371 for fileName in "${fileNameList[@]}"; do
372 if [[ $fileName =~ .gz.age$ ]]; then
373 ## Decrypt and decompress files ending in .gz.age
374 vbm "DEBUG:Decrypting file:$dirTemp/$fileName";
375 try age -i "$pathFileIdentity" -d "$dirTemp"/"$fileName" | try gunzip > "$pathDirOut1"/"${fileName%.gz.age}";
376 else
377 ## Copy other files as-is
378 try cp "$dirTemp"/"$fileName" "$pathDirOut1"/"$fileName";
379 fi;
380 done;
381
382 vbm "STATUS:end extractGzAgeTar()";
383 } # Extracts contents from .gz.age.tar
384 extractGzAge() {
385 # Desc: Extracts contents from .gz.age
386 # Usage: extractGzAge arg1
387 # Input: - arg1: path to file
388 # - pathFileIdentity: path to age identity file (for decryption)
389 # - pathDirOut1: path to output dir
390 # Output: file writes $pathDirOut1
391 # Depends: age v1.0.0-rpc3
392 vbm "STATUS:start extractGzAge()";
393 vbm "args:$*";
394 vbm "pathFileIdentity:$pathFileIdentity";
395 vbm "pathDirOut1:$pathDirOut1";
396 local file
397
398 # Get filename from path
399 file="$(basename "$1")";
400
401 # Decrypt and decompress to $pathDirOut1
402 try age -i "$pathFileIdentity" -d "$1" | try gunzip > "$pathDirOut1"/"${file%.gz.age}";
403 :
404 vbm "STATUS:end extractGzAge()";
405 } # Extracts contents from .gz.age
406 extractAge() {
407 # Desc: Extracts contents from .age
408 # Usage: extractAge arg1
409 # Input: - arg1: path to file
410 # - pathFileIdentity: path to age identity file (for decryption)
411 # - pathDirOut1: path to output dir
412 # Output: file writes $pathDirOut1
413 # Depends: age v1.0.0-rpc3
414 vbm "STATUS:start extractAge()";
415 vbm "args:$*";
416 vbm "pathFileIdentity:$pathFileIdentity";
417 vbm "pathDirOut1:$pathDirOut1";
418 local file
419
420 # Get filename from path
421 file="$(basename "$1")";
422
423 # Decrypt to $pathDirOut1
424 try age -i "$pathFileIdentity" -d "$1" > "$pathDirOut1"/"${file%.age}";
425
426 vbm "STATUS:end extractAge()";
427 } # Extracts contents from .age
428
429 main() {
430 vbm "STATUS:start main()";
431 # Process options
432 ## Sets vars: - pathDirOut1 (from -O, --output-dir option)
433 ## - opVerbose (from -v, --verbose option)
434 ## - pathFileIdentity (from -i, --identity option)
435 processArgs "$@";
436
437 # Create temporary working dir
438 try mkdir "$dirTemp";
439
440 # Verify input args
441 for arg in "${inputFilePaths[@]}"; do
442 vbm "DEBUG:input file path is:$arg";
443 ## Ends in .gz.age.tar?
444 if [[ $arg =~ .gz.age.tar$ ]]; then
445 vbm "DEBUG:$arg ends in .gz.age.tar";
446 vbm "DEBUG:$arg is a valid file extension";
447 : # do nothing
448 ## Ends in .gz.age?
449 elif [[ $arg =~ .gz.age$ ]]; then
450 vbm "DEBUG:$arg ends in .gz.age";
451 vbm "DEBUG:$arg is a valid file extension";
452 ## Ends in .age?
453 elif [[ $arg =~ .age$ ]]; then
454 vbm "DEBUG:$arg ends in .age";
455 vbm "DEBUG:$arg is a valid file extension";
456 else
457 showUsage;
458 die "ERROR:Invalid file extension detected.";
459 fi;
460 done;
461
462 # Work on each file
463 for file in "${inputFilePaths[@]}"; do
464 vbm "DEBUG:input file path is:$arg";
465 vbm "DEBUG:file is:$file";
466 ## Ends in .gz.age.tar?
467 if [[ $file =~ .gz.age.tar$ ]]; then
468 vbm "DEBUG:Beginning extraction of file(s) from $file";
469 extractGzAgeTar "$file";
470 ## Ends in .gz.age?
471 elif [[ $file =~ .gz.age$ ]]; then
472 vbm "DEBUG:Beginning extraction of file from $file";
473 extractGzAge "$file";
474 ## Ends in .age?
475 elif [[ $file =~ .age$ ]]; then
476 vbm "DEBUG:Beginning extraction of file from $file";
477 extractAge "$file";
478 else
479 vbm "DEBUG:Invalid file extension detected:$file"
480 showUsage;
481 die "Exiting.";
482 fi;
483 done;
484
485 # Remove temporary directory
486 try rm -rf "$dirTemp";
487 vbm "STATUS:end main()";
488 }
489 #===END Declare local script functions===
490 #==END Define script parameters==
491
492 # Run program
493 main "$@";
494
495 # Author: Steven Baltakatei Sandoval
496 # License: GPLv3+