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