This question is the second follow-up of: Shell POSIX OpenSSL file decryption script
The first follow-up was: Shell POSIX OpenSSL file decryption script follow-up #1
Both of which have proven very useful and sped up my learning curve of shell scripting.
You should probably look at them, but as I made so many changes, I feel it is not necessary.
All reviews are welcome, feel free to also comment in case of minor points.
#!/bin/sh
###############################################################################
## OpenSSL file decryption POSIX shell script ##
## revision: 0.9 ##
## GitHub: https://git.io/fxslm ##
###############################################################################
# shellcheck disable=SC2016
# disable shellcheck information SC2016 globally for the script
# link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2016
# reason: the script's main parts use constructs like that
# treat unset variables as an error when substituting
set -o nounset
# pipe will be considered successful only if all the commands involved are executed without errors
# ERROR: Illegal option -o pipefail. This likely works in Bash and alike only.
#set -o pipefail
#------------------------------------------------------------------------------
print_error_and_exit()
# expected arguments:
# $1 = exit code
# $2 = error origin (usually function name)
# $3 = error message
{
# redirect all output of this function to standard error stream
exec 1>&2
# check if exactly 3 arguments have been passed
# if not, print out an internal error without colors
if [ "${#}" -ne 3 ]
then
printf 'print_error_and_exit internal error\n\n\tWrong number of arguments has been passed: %b!\n\tExpected the following 3:\n\t\t$1 - exit code\n\t\t$2 - error origin\n\t\t$3 - error message\n\nexit code = 1\n' "${#}"
exit 1
fi
# check if the first argument is a number
# if not, print out an internal error without colors
if ! [ "${1}" -eq "${1}" ] 2> /dev/null
then
printf 'print_error_and_exit internal error\n\n\tThe first argument is not a number: %b!\n\tExpected an exit code from the script.\n\nexit code = 1\n' "${1}"
exit 1
fi
# check if we have color support
if command -v tput > /dev/null 2>&1 && tput setaf 1 > /dev/null 2>&1
then
# color definitions
readonly bold=$(tput bold)
readonly red=$(tput setaf 1)
readonly yellow=$(tput setaf 3)
readonly nocolor=$(tput sgr0)
# here we do have color support, so we highlight the error origin and the exit code
printf '%b%b\n\n\t%b%b%b\n\nexit code = %b%b\n' \
"${bold}${yellow}" "${2}" "${nocolor}" \
"${3}" \
"${bold}${red}" "${1}" "${nocolor}"
exit "${1}"
else
# here we do not have color support
printf '%b\n\n\t%b\n\nexit code = %b\n' \
"${2}" "${3}" "${1}"
exit "${1}"
fi
}
#------------------------------------------------------------------------------
# in this function, the SC2120 warning is irrelevant and safe to ignore
# link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2120
# shellcheck disable=SC2120
am_i_root()
# expected arguments: none
{
# check if no argument has been passed
[ "${#}" -eq 0 ] || print_error_and_exit 1 "am_i_root" "Some arguments have been passed to the function!\\n\\tNo arguments expected.\\n\\tPassed: ${*}"
# check if the user is root
# this will return an exit code of the command itself directly
[ "$(id -u)" -eq 0 ]
}
# check if the user had by any chance run the script with root privileges
# if you need to run it as root, feel free to comment out the line below
# in this function call, the SC2119 information is irrelevant and safe to ignore
# link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2119
# shellcheck disable=SC2119
am_i_root && print_error_and_exit 1 "am_i_root" "Running this script with root privileges is discouraged!\\n\\tQuiting to shell."
#------------------------------------------------------------------------------
check_for_prerequisite()
# expected arguments:
# $1 = command / program name
{
# check if exactly one argument has been passed
[ "${#}" -eq 1 ] || print_error_and_exit 1 "check_for_prerequisite" "Exactly one argument has not been passed to the function!\\n\\tOne command to test expected.\\n\\tPassed: ${*}"
# check if the argument is a program which is installed
command -v "${1}" > /dev/null 2>&1 || print_error_and_exit 1 "check_for_prerequisite" "This script requires '${1}' but it is not installed or available on this system!\\n\\tPlease install the corresponding package manually."
}
check_for_prerequisite 'openssl'
check_for_prerequisite 'pv'
check_for_prerequisite 'file'
check_for_prerequisite 'grep'
#------------------------------------------------------------------------------
is_number()
# expected arguments:
# $1 = variable or literal
{
# check if exactly one argument has been passed
[ "${#}" -eq 1 ] || print_error_and_exit 1 "is_number" "Exactly one argument has not been passed to the function!\\n\\tOne variable or literal to test expected.\\n\\tPassed: ${*}"
# check if the argument is an integer number
# this will return an exit code of the command itself directly
[ "${1}" -eq "${1}" ] 2> /dev/null
}
#------------------------------------------------------------------------------
print_usage_and_exit()
{
# check if exactly one argument has been passed
[ "${#}" -eq 1 ] || print_error_and_exit 1 "print_usage_and_exit" "Exactly one argument has not been passed to the function!\\n\\tPassed: ${*}"
# check if the argument is a number
is_number "${1}" || print_error_and_exit 1 "print_usage_and_exit" "The argument is not a number!\\n\\Expected an exit code from the script.\\n\\tPassed: ${1}"
# in case of non-zero exit code given, redirect all output to stderr
[ "${1}" -ne 0 ] && exec 1>&2
echo "Usage: ${0} [-o directory] file"
echo
echo " -o directory: Write the output file into the given directory;"
echo " Optional and must be given before the file."
echo
echo " file: Regular file to decrypt."
exit "${1}"
}
#------------------------------------------------------------------------------
given_output_directory=
while getopts ":ho:" option
do
case "${option}" in
o)
given_output_directory="${OPTARG}"
;;
h)
print_usage_and_exit 0
;;
*)
print_usage_and_exit 1
;;
esac
done
shift $(( OPTIND - 1 ))
#------------------------------------------------------------------------------
[ "${#}" -eq 0 ] && print_usage_and_exit 1
#------------------------------------------------------------------------------
[ "${#}" -gt 1 ] && print_error_and_exit 1 '[ "${#}" -gt 1 ]' "You have passed ${#} arguments to the script!\\n\\tOnly one file expected.\\n\\tPassed: ${*}"
#------------------------------------------------------------------------------
[ -f "${1}" ] || print_error_and_exit 1 '[ -f "${1}" ]' "The given argument is not an existing regular file!\\n\\tPassed: ${1}"
#------------------------------------------------------------------------------
input_file="${1}"
[ -r "${input_file}" ] || print_error_and_exit 1 '[ -r "${input_file}" ]' "Input file is not readable by you!\\n\\tPassed: ${input_file}"
#------------------------------------------------------------------------------
is_file_encrypted_using_openssl()
{
# check if exactly one argument has been passed
[ "${#}" -eq 1 ] || print_error_and_exit 1 "is_file_encrypted_using_openssl" "Exactly one argument has not been passed to the function!\\n\\tPassed: ${*}"
# check if the argument is a file
[ -f "${1}" ] || print_error_and_exit 1 "is_file_encrypted_using_openssl" "The provided argument is not a regular file!\\n\\tPassed: ${1}"
# check if the provided file has been encrypted using openssl
# this will return an exit code of the command itself directly
file "${1}" | grep --ignore-case 'openssl' > /dev/null 2>&1
}
is_file_encrypted_using_openssl "${input_file}" || print_error_and_exit 1 "is_file_encrypted_using_openssl" "Input file does not seem to have been encrypted using OpenSSL!\\n\\tPassed: ${input_file}"
#------------------------------------------------------------------------------
# parameter substitution with - modifier will cause the output_directory
# variable to to get dirname ... in case given_output_directory is empty
output_directory="${given_output_directory:-$(dirname "${input_file}")}"
[ -d "${output_directory}" ] || print_error_and_exit 1 '[ -d "${output_directory}" ]' "Destination:\\n\\t\\t${output_directory}\\n\\tis not a directory!"
[ -w "${output_directory}" ] || print_error_and_exit 1 '[ -w "${output_directory}" ]' "Destination directory:\\n\\t\\t${output_directory}\\n\\tis not writable by you!"
#------------------------------------------------------------------------------
filename_extracted_from_path=$(basename "${input_file}")
filename_without_enc_extension="${filename_extracted_from_path%.enc}"
if [ "${filename_extracted_from_path}" = "${filename_without_enc_extension}" ]
then
# the file has a different than .enc extension or no extension at all
# what we do now, is that we append .dec extention to the file name
output_file="${output_directory}/${filename_extracted_from_path}.dec"
else
# the file has the .enc extension
# what we do now, is that we use the file name without .enc extension
output_file="${output_directory}/${filename_without_enc_extension}"
fi
#------------------------------------------------------------------------------
# -e FILE: True if file exists. Any type of file!
[ -e "${output_file}" ] && print_error_and_exit 1 '[ -e "${output_file}" ]' "Destination file:\\n\\t\\t${output_file}\\n\\talready exists!"
#------------------------------------------------------------------------------
# here comes the core part - decryption of the given file
if ! pv --wait "${input_file}" | openssl enc -aes-256-cbc -md sha256 -salt -out "${output_file}" -d 2> /dev/null
then
[ -f "${output_file}" ] && rm "${output_file}"
echo
print_error_and_exit 1 'pv --wait "${input_file}" | openssl enc -aes-256-cbc -md sha256 -salt -out "${output_file}" -d' "Decryption failed!"
else
echo
echo "Decryption successful."
# scripts exit with 0 exit code by default, in case of success,
# so this just more explicit for readers
exit 0
fi
The project is uploaded on GitHub: https://git.io/fxslm
But not this revision. I will learn from answers and make the first release afterwards.
EDIT1
I now realized I should check for free space, so I added this:
file_size=$(du --bytes "${input_file}" | awk '{ print $1 }')
free_space=$(df "${output_directory}" | tail --lines=1 | awk '{ print $4 }')
[ "${free_space}" -gt "${file_size}" ] || print_error_and_exit 1 '[ "${free_space}" -gt "${file_size}" ]' "There is not enough free space in the destination directory!\\n\\t\\tFile size: ${file_size}\\n\\t\\tFree space: ${free_space}"
EDIT2
I have fixed an issue with getting free space in bytes by adding -B1
to df
:
free_space=$(df -B1 "${output_directory}" | tail --lines=1 | awk '{ print $4 }')