From: Steven Baltakatei Sandoval Date: Thu, 29 Jul 2021 12:37:05 +0000 (+0000) Subject: feat(unitproc):bkagedecrypt - age decryption tool X-Git-Tag: 0.5.0~51 X-Git-Url: https://zdv2.bktei.com/gitweb/BK-2020-03.git/commitdiff_plain/e6bb2ae13949291359b48d66847ae1605b160bc2?hp=891bb4bd2a14c4c91f94de6cd17b944a12e87194 feat(unitproc):bkagedecrypt - age decryption tool --- diff --git a/unitproc/bkagedecrypt b/unitproc/bkagedecrypt new file mode 100644 index 0000000..1c2d055 --- /dev/null +++ b/unitproc/bkagedecrypt @@ -0,0 +1,496 @@ +#!/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+