#!/bin/bash # Desc: Decrypts files encrypted with age # Usage: bkagedecrypt -i key.txt file1 file2 ... # Version: 0.0.1 #==BEGIN Define script parameters== #===BEGIN Define variables=== declare -g runFlag # If "false", indicates exit required declare -ag inputFilePaths # Array to store input file paths declare -Ag appRollCall # Associative array for storing app status declare -Ag fileRollCall # Associative array for storing file status declare -Ag dirRollCall # Associative array for storing dir status timeScriptStartNs="$(date +%Y%m%dT%H%M%S.%N%z)"; dirTemp="/tmp/$timeScriptStartNs"..bkagedecrypt; # will be automatically deleted #===END Define variables=== #===BEGIN Declare local script functions=== yell() { echo "$0: $*" >&2; } # Yell, Die, Try Three-Fingered Claw technique; # Ref/Attrib: https://stackoverflow.com/a/25515370 die() { yell "$*"; exit 111; } try() { "$@" || die "cannot $*"; } vbm() { # Description: Prints verbose message ("vbm") to stderr if opVerbose is set to "true". # Usage: vbm "DEBUG :verbose message here" # Version 0.2.0 # Input: arg1: string # vars: opVerbose # Output: stderr # Depends: bash 5.0.3, GNU-coreutils 8.30 (echo, date) if [ "$opVerbose" = "true" ]; then functionTime="$(date --iso-8601=ns)"; # Save current time in nano seconds. echo "[$functionTime]:$0:""$*" 1>&2; # Display argument text. fi # End function return 0; # Function finished. } # Displays message if opVerbose true checkapp() { # Desc: If arg is a command, save result in assoc array 'appRollCall' # Usage: checkapp arg1 arg2 arg3 ... # Version: 0.1.1 # Input: global assoc. array 'appRollCall' # Output: adds/updates key(value) to global assoc array 'appRollCall' # Depends: bash 5.0.3 local returnState #===Process Args=== for arg in "$@"; do if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command appRollCall[$arg]="true"; if ! [ "$returnState" = "false" ]; then returnState="true"; fi; else appRollCall[$arg]="false"; returnState="false"; fi; done; #===Determine function return code=== if [ "$returnState" = "true" ]; then return 0; else return 1; fi; } # Check that app exists checkfile() { # Desc: If arg is a file path, save result in assoc array 'fileRollCall' # Usage: checkfile arg1 arg2 arg3 ... # Version: 0.1.1 # Input: global assoc. array 'fileRollCall' # Output: adds/updates key(value) to global assoc array 'fileRollCall'; # Output: returns 0 if app found, 1 otherwise # Depends: bash 5.0.3 local returnState #===Process Args=== for arg in "$@"; do if [ -f "$arg" ]; then fileRollCall["$arg"]="true"; if ! [ "$returnState" = "false" ]; then returnState="true"; fi; else fileRollCall["$arg"]="false"; returnState="false"; fi; done; #===Determine function return code=== if [ "$returnState" = "true" ]; then return 0; else return 1; fi; } # Check that file exists checkdir() { # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' # Usage: checkdir arg1 arg2 arg3 ... # Version 0.1.1 # Input: global assoc. array 'dirRollCall' # Output: adds/updates key(value) to global assoc array 'dirRollCall'; # Output: returns 0 if app found, 1 otherwise # Depends: Bash 5.0.3 local returnState #===Process Args=== for arg in "$@"; do if [ -d "$arg" ]; then dirRollCall["$arg"]="true"; if ! [ "$returnState" = "false" ]; then returnState="true"; fi else dirRollCall["$arg"]="false"; returnState="false"; fi done #===Determine function return code=== if [ "$returnState" = "true" ]; then return 0; else return 1; fi } # Check that dir exists displayMissing() { # Desc: Displays missing apps, files, and dirs # Usage: displayMissing # Version 0.1.1 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall # Output: stderr: messages indicating missing apps, file, or dirs # Depends: bash 5, checkAppFileDir() local missingApps value appMissing missingFiles fileMissing local missingDirs dirMissing #==BEGIN Display errors== #===BEGIN Display Missing Apps=== missingApps="Missing apps :"; #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done for key in "${!appRollCall[@]}"; do value="${appRollCall[$key]}"; if [ "$value" = "false" ]; then #echo "DEBUG:Missing apps: $key => $value"; missingApps="$missingApps""$key "; appMissing="true"; fi; done; if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing. echo "$missingApps" 1>&2; fi; unset value; #===END Display Missing Apps=== #===BEGIN Display Missing Files=== missingFiles="Missing files:"; #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done for key in "${!fileRollCall[@]}"; do value="${fileRollCall[$key]}"; if [ "$value" = "false" ]; then #echo "DEBUG:Missing files: $key => $value"; missingFiles="$missingFiles""$key "; fileMissing="true"; fi; done; if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing. echo "$missingFiles" 1>&2; fi; unset value; #===END Display Missing Files=== #===BEGIN Display Missing Directories=== missingDirs="Missing dirs:"; #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done for key in "${!dirRollCall[@]}"; do value="${dirRollCall[$key]}"; if [ "$value" = "false" ]; then #echo "DEBUG:Missing dirs: $key => $value"; missingDirs="$missingDirs""$key "; dirMissing="true"; fi; done; if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing. echo "$missingDirs" 1>&2; fi; unset value; #===END Display Missing Directories=== #==END Display errors== } # Display missing apps, files, dirs showVersion() { # Desc: Displays script version and license information. # Usage: showVersion # Version: 0.0.1 (modified) # Input: scriptVersion var containing version string # Output: stdout # Depends: vbm(), yell, GNU-coreutils 8.30 # Initialize function vbm "DEBUG:showVersion function called." cat <<'EOF' bkagedecrypt 0.0.1 Copyright (C) 2021 Steven Baltakatei Sandoval License GPLv3: GNU GPL version 3 This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. EOF # End function vbm "DEBUG:showVersion function ended." return 0; # Function finished. } # Display script version. processArgs() { # Desc: Processes arguments provided to script. # Usage: processArgs "$@" # Version: 0.0.1 (modified) # Input: "$@" (list of arguments provided to the function) # Output: Sets following variables used by other functions: # opVerbose Indicates verbose mode enable status. (ex: "true", "false") # pathDirOut1 Path to output directory. # inputFilePaths Array containing paths of files to decrypt # Depends: # yell() Displays messages to stderr. # vbm() Displays messsages to stderr if opVerbose set to "true". # showUsage() Displays usage information about parent script # showVersion() Displays version about parent script # checkfile() Checks if file exists # checkdir() Checks if dir exists # dirRollCall Assoc. array used by checkfile(), checkdir(), checkapp() # fileRollCall Assoc. array used by checkfile(), checkdir(), checkapp() # appRollCall Assoc. array used by checkfile(), checkdir(), checkapp() # External dependencies: bash (5.0.3), echo # Ref./Attrib.: # [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347 vbm "STATUS:start processArgs()"; # Perform work while [ ! $# -eq 0 ]; do # While number of arguments ($#) is not (!) equal to (-eq) zero (0). #vbm "DEBUG:Starting processArgs while loop." # Debug stderr message. See [1]. #vbm "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1]. case "$1" in -h | --help) showUsage; exit 1;; # Display usage. --version) showVersion; exit 1;; # Show version -v | --verbose) # Enable verbose mode. See [1]. opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; -i | --identity) # Define identity file pathFileIdentity="$2"; shift;; -O | --output-dir) # Define output directory path pathDirOut1="$2"; vbm "DEBUG:Setting pathDirOut1 to:$pathDirOut1"; shift;; *) inputFilePaths+=("$1"); # Add to inputArgs array vbm "DEBUG:Added to inputFilePaths array:$1"; esac; shift; done; # If pathDirOut1 not set, set as default if [[ -z $pathDirOut1 ]]; then pathDirOut1="$(pwd)"; # Fall back to using current working directory vbm "DEBUG:pathDirOut1 not set. Setting to default:$pathDirOut1"; fi; # Exit if pathFileIdentity not set if [[ -z $pathFileIdentity ]]; then showUsage; die "ERROR:not set:pathFileIdentity:$pathFileIdentity"; fi; # Check apps, dirs if ! checkapp age tar gunzip; then runFlag="false"; fi; if ! checkdir "$pathDirOut1"; then runFlag="false"; fi; # Check files ## Check identity file (required) if ! checkfile "$pathFileIdentity"; then runFlag="false"; fi; ## Check inputFilePaths if [[ ${#inputFilePaths[@]} -eq 0 ]]; then vbm "DEBUG:ERROR:inputFilePaths array empty:'${inputFilePaths[*]}'"; runFlag="false"; fi; for path in "${inputFilePaths[@]}"; do vbm "DEBUG:Checking if file path $path exists."; if ! checkfile "$path"; then runFlag="false"; vbm "DEBUG:ERROR:File does not exist:$path"; fi; done; # On error, display missing elements, exit. if [[ $runFlag == "false" ]]; then displayMissing; showUsage; die "ERROR:Input argument requirements unsatisfied. Exiting."; else vbm "STATUS:Input argument requirements satisfied."; fi; # End function vbm "STATUS:end processArgs()"; return 0; # Function finished. } # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.). showUsage() { # Desc: Display script usage information # Usage: showUsage # Version 0.0.1 (modified) # Input: none # Output: stdout # Depends: GNU-coreutils 8.30 (cat) cat <<'EOF' NAME: bkagedecrypt - decrypt age-encrypted files USAGE: bkagedecrypt [ options ] [FILE...] DESCRIPTION: Decrypt FILE(s) using `age` v1.0.0-rc.3. See: https://github.com/FiloSottile/age FILE(s) must have the following file name extensions and properties: .gz.age.tar, File is a tar archive containing one or more subfiles within. Each subfile contains an age-encrypted, gzip-compressed plaintext file. .gz.age, File is an age-encrypted gzip-compressed plaintext file. .age, File is an age-encrypted plaintext file. Decryption via password is not supported. OPTIONS: -i, --identity KEY Path of private key file passed to `age`. -O, --output-dir Define output directory path. (Default: current working dir) -h, --help Display help information. --version Display script version. -v, --verbose Display debugging info. EXAMPLE: $ bkagedecrypt -i key.txt foo.gz.age.tar $ bkagedecrypt -i key.txt foo.gz.age.tar bar.gz.age baz.age $ bkagedecrypt -i ky.txt -O ../ foo.gz.age.tar bar.gz.age baz.age EOF } # Display information on how to use this script. extractGzAgeTar() { # Desc: Extracts contents from .gz.age.tar # Usage: extractGzAgeTar arg1 # Input: - arg1: path to file # - pathFileIdentity: path to age identity file (for decryption) # - pathDirOut1: path to output dir # Output: file writes $pathDirOut1 # Depends: age v1.0.0-rpc3, GNU tar v1.30 vbm "STATUS:start extractGzAgeTar()"; vbm "args:$*"; vbm "pathFileIdentity:$pathFileIdentity"; vbm "pathDirOut1:$pathDirOut1"; local file local -a fileNameList # Get filename from path file="$(basename "$1")"; # Get list of files from tar while read -r line; do fileNameList+=("$line"); vbm "Adding to fileNameList:$line"; done < <(try tar --list -f "$1"); vbm "STATUS:fileNameList:${fileNameList[*]}"; # Extract .gz.age files from tar to temporary dir vbm "Extracting files from '$1' to '$dirTemp'"; try tar -xf "$1" -C "$dirTemp"; # Decrypt and decompress each .gz.age file to $pathDirOut1 for fileName in "${fileNameList[@]}"; do if [[ $fileName =~ .gz.age$ ]]; then ## Decrypt and decompress files ending in .gz.age vbm "DEBUG:Decrypting file:$dirTemp/$fileName"; try age -i "$pathFileIdentity" -d "$dirTemp"/"$fileName" | try gunzip > "$pathDirOut1"/"${fileName%.gz.age}"; else ## Copy other files as-is try cp "$dirTemp"/"$fileName" "$pathDirOut1"/"$fileName"; fi; done; vbm "STATUS:end extractGzAgeTar()"; } # Extracts contents from .gz.age.tar extractGzAge() { # Desc: Extracts contents from .gz.age # Usage: extractGzAge arg1 # Input: - arg1: path to file # - pathFileIdentity: path to age identity file (for decryption) # - pathDirOut1: path to output dir # Output: file writes $pathDirOut1 # Depends: age v1.0.0-rpc3 vbm "STATUS:start extractGzAge()"; vbm "args:$*"; vbm "pathFileIdentity:$pathFileIdentity"; vbm "pathDirOut1:$pathDirOut1"; local file # Get filename from path file="$(basename "$1")"; # Decrypt and decompress to $pathDirOut1 try age -i "$pathFileIdentity" -d "$1" | try gunzip > "$pathDirOut1"/"${file%.gz.age}"; : vbm "STATUS:end extractGzAge()"; } # Extracts contents from .gz.age extractAge() { # Desc: Extracts contents from .age # Usage: extractAge arg1 # Input: - arg1: path to file # - pathFileIdentity: path to age identity file (for decryption) # - pathDirOut1: path to output dir # Output: file writes $pathDirOut1 # Depends: age v1.0.0-rpc3 vbm "STATUS:start extractAge()"; vbm "args:$*"; vbm "pathFileIdentity:$pathFileIdentity"; vbm "pathDirOut1:$pathDirOut1"; local file # Get filename from path file="$(basename "$1")"; # Decrypt to $pathDirOut1 try age -i "$pathFileIdentity" -d "$1" > "$pathDirOut1"/"${file%.age}"; vbm "STATUS:end extractAge()"; } # Extracts contents from .age main() { vbm "STATUS:start main()"; # Process options ## Sets vars: - pathDirOut1 (from -O, --output-dir option) ## - opVerbose (from -v, --verbose option) ## - pathFileIdentity (from -i, --identity option) processArgs "$@"; # Create temporary working dir try mkdir "$dirTemp"; # Verify input args for arg in "${inputFilePaths[@]}"; do vbm "DEBUG:input file path is:$arg"; ## Ends in .gz.age.tar? if [[ $arg =~ .gz.age.tar$ ]]; then vbm "DEBUG:$arg ends in .gz.age.tar"; vbm "DEBUG:$arg is a valid file extension"; : # do nothing ## Ends in .gz.age? elif [[ $arg =~ .gz.age$ ]]; then vbm "DEBUG:$arg ends in .gz.age"; vbm "DEBUG:$arg is a valid file extension"; ## Ends in .age? elif [[ $arg =~ .age$ ]]; then vbm "DEBUG:$arg ends in .age"; vbm "DEBUG:$arg is a valid file extension"; else showUsage; die "ERROR:Invalid file extension detected."; fi; done; # Work on each file for file in "${inputFilePaths[@]}"; do vbm "DEBUG:input file path is:$arg"; vbm "DEBUG:file is:$file"; ## Ends in .gz.age.tar? if [[ $file =~ .gz.age.tar$ ]]; then vbm "DEBUG:Beginning extraction of file(s) from $file"; extractGzAgeTar "$file"; ## Ends in .gz.age? elif [[ $file =~ .gz.age$ ]]; then vbm "DEBUG:Beginning extraction of file from $file"; extractGzAge "$file"; ## Ends in .age? elif [[ $file =~ .age$ ]]; then vbm "DEBUG:Beginning extraction of file from $file"; extractAge "$file"; else vbm "DEBUG:Invalid file extension detected:$file" showUsage; die "Exiting."; fi; done; # Remove temporary directory try rm -rf "$dirTemp"; vbm "STATUS:end main()"; } #===END Declare local script functions=== #==END Define script parameters== # Run program main "$@"; # Author: Steven Baltakatei Sandoval # License: GPLv3+