2 # Desc: Decrypts files encrypted with age
3 # Usage: bkagedecrypt -i key.txt file1 file2 ...
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
14 timeScriptStartNs
="$(date +%Y%m%dT%H%M%S.%N%z)";
15 dirTemp
="/tmp/$timeScriptStartNs"..bkagedecrypt
; # will be automatically deleted
16 #===END Define variables===
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 $*"; }
23 # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true".
24 # Usage: vbm "DEBUG :verbose message here"
29 # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date)
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.
37 return 0; # Function finished.
38 } # Displays message if opVerbose true
40 # Desc: If arg is a command, save result in assoc array 'appRollCall'
41 # Usage: checkapp arg1 arg2 arg3 ...
43 # Input: global assoc. array 'appRollCall'
44 # Output: adds/updates key(value) to global assoc array 'appRollCall'
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;
54 appRollCall
[$arg]="false"; returnState
="false";
58 #===Determine function return code===
59 if [ "$returnState" = "true" ]; then
64 } # Check that app exists
66 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
67 # Usage: checkfile arg1 arg2 arg3 ...
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
77 if [ -f "$arg" ]; then
78 fileRollCall
["$arg"]="true";
79 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi;
81 fileRollCall
["$arg"]="false"; returnState
="false";
85 #===Determine function return code===
86 if [ "$returnState" = "true" ]; then
91 } # Check that file exists
93 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
94 # Usage: checkdir arg1 arg2 arg3 ...
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
104 if [ -d "$arg" ]; then
105 dirRollCall
["$arg"]="true";
106 if ! [ "$returnState" = "false" ]; then returnState
="true"; fi
108 dirRollCall
["$arg"]="false"; returnState
="false";
112 #===Determine function return code===
113 if [ "$returnState" = "true" ]; then
118 } # Check that dir exists
120 # Desc: Displays missing apps, files, and dirs
121 # Usage: displayMissing
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
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 ";
141 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
142 echo "$missingApps" 1>&2;
145 #===END Display Missing Apps===
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 ";
158 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
159 echo "$missingFiles" 1>&2;
162 #===END Display Missing Files===
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 ";
175 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
176 echo "$missingDirs" 1>&2;
179 #===END Display Missing Directories===
181 #==END Display errors==
182 } # Display missing apps, files, dirs
184 # Desc: Displays script version and license information.
186 # Version: 0.0.1 (modified)
187 # Input: scriptVersion var containing version string
189 # Depends: vbm(), yell, GNU-coreutils 8.30
191 # Initialize function
192 vbm
"DEBUG:showVersion function called."
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.
203 vbm
"DEBUG:showVersion function ended."
204 return 0; # Function finished.
205 } # Display script version.
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
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
227 # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347
228 vbm
"STATUS:start processArgs()";
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].
235 -h |
--help) showUsage
; exit 1;; # Display usage.
236 --version) showVersion
; exit 1;; # Show version
237 -v |
--verbose) # Enable verbose mode. See [1].
239 vbm
"DEBUG:Verbose mode enabled.";;
240 -i |
--identity) # Define identity file
241 pathFileIdentity
="$2";
243 -O |
--output-dir) # Define output directory path
245 vbm
"DEBUG:Setting pathDirOut1 to:$pathDirOut1";
247 *) inputFilePaths
+=("$1"); # Add to inputArgs array
248 vbm
"DEBUG:Added to inputFilePaths array:$1";
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";
259 # Exit if pathFileIdentity not set
260 if [[ -z $pathFileIdentity ]]; then
262 die
"ERROR:not set:pathFileIdentity:$pathFileIdentity";
266 if ! checkapp age
tar gunzip
; then runFlag
="false"; fi;
267 if ! checkdir
"$pathDirOut1"; then runFlag
="false"; fi;
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[*]}'";
275 for path
in "${inputFilePaths[@]}"; do
276 vbm
"DEBUG:Checking if file path $path exists.";
277 if ! checkfile
"$path"; then
279 vbm
"DEBUG:ERROR:File does not exist:$path"; fi;
282 # On error, display missing elements, exit.
283 if [[ $runFlag == "false" ]]; then
286 die
"ERROR:Input argument requirements unsatisfied. Exiting.";
288 vbm
"STATUS:Input argument requirements satisfied.";
292 vbm
"STATUS:end processArgs()";
293 return 0; # Function finished.
294 } # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
296 # Desc: Display script usage information
298 # Version 0.0.1 (modified)
301 # Depends: GNU-coreutils 8.30 (cat)
304 bkagedecrypt - decrypt age-encrypted files
306 bkagedecrypt [ options ] [FILE...]
309 Decrypt FILE(s) using `age` v1.0.0-rc.3. See:
310 https://github.com/FiloSottile/age
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
318 .age, File is an age-encrypted plaintext file.
320 Decryption via password is not supported.
324 Path of private key file passed to `age`.
326 Define output directory path. (Default: current working dir)
328 Display help information.
330 Display script version.
332 Display debugging info.
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
340 } # Display information on how to use this script.
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()";
351 vbm
"pathFileIdentity:$pathFileIdentity";
352 vbm
"pathDirOut1:$pathDirOut1";
354 local -a fileNameList
356 # Get filename from path
357 file="$(basename "$1")";
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[*]}";
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";
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}";
377 ## Copy other files as-is
378 try
cp "$dirTemp"/"$fileName" "$pathDirOut1"/"$fileName";
382 vbm
"STATUS:end extractGzAgeTar()";
383 } # Extracts contents from .gz.age.tar
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()";
394 vbm
"pathFileIdentity:$pathFileIdentity";
395 vbm
"pathDirOut1:$pathDirOut1";
398 # Get filename from path
399 file="$(basename "$1")";
401 # Decrypt and decompress to $pathDirOut1
402 try age
-i "$pathFileIdentity" -d "$1" | try gunzip
> "$pathDirOut1"/"${file%.gz.age}";
404 vbm
"STATUS:end extractGzAge()";
405 } # Extracts contents from .gz.age
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()";
416 vbm
"pathFileIdentity:$pathFileIdentity";
417 vbm
"pathDirOut1:$pathDirOut1";
420 # Get filename from path
421 file="$(basename "$1")";
423 # Decrypt to $pathDirOut1
424 try age
-i "$pathFileIdentity" -d "$1" > "$pathDirOut1"/"${file%.age}";
426 vbm
"STATUS:end extractAge()";
427 } # Extracts contents from .age
430 vbm
"STATUS:start main()";
432 ## Sets vars: - pathDirOut1 (from -O, --output-dir option)
433 ## - opVerbose (from -v, --verbose option)
434 ## - pathFileIdentity (from -i, --identity option)
437 # Create temporary working dir
438 try mkdir
"$dirTemp";
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";
449 elif [[ $arg =~ .gz.age$
]]; then
450 vbm
"DEBUG:$arg ends in .gz.age";
451 vbm
"DEBUG:$arg is a valid file extension";
453 elif [[ $arg =~ .age$
]]; then
454 vbm
"DEBUG:$arg ends in .age";
455 vbm
"DEBUG:$arg is a valid file extension";
458 die
"ERROR:Invalid file extension detected.";
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";
471 elif [[ $file =~ .gz.age$
]]; then
472 vbm
"DEBUG:Beginning extraction of file from $file";
473 extractGzAge
"$file";
475 elif [[ $file =~ .age$
]]; then
476 vbm
"DEBUG:Beginning extraction of file from $file";
479 vbm
"DEBUG:Invalid file extension detected:$file"
485 # Remove temporary directory
486 try
rm -rf "$dirTemp";
487 vbm
"STATUS:end main()";
489 #===END Declare local script functions===
490 #==END Define script parameters==
495 # Author: Steven Baltakatei Sandoval