Commit | Line | Data |
---|---|---|
e6bb2ae1 SBS |
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+ |