0
\$\begingroup\$

From original very simplistic code: Shell POSIX OpenSSL file decryption script

I learned a lot both from the first follow-up review, and the second one as well, many thanks!


I just need one final review round, so that I can feel confident, there are no more blind spots.

I promise it is the last from this project, I don't expect you to upvote or anythig else than actually do the review of the script.


Changes:

  • check_for_prerequisites, notice the s at the end, is now looping over given arguments.

  • POSIX compliance, no long options or anything else.

  • Relatively small files (< 1GiB) are on an SSD en-/decrypted within seconds, so pv got out completely on such files.

  • In any case, the script newly works directly without the pv, so no more a prerequisite.

  • Not quoting variable assignments anymore.

  • Simplified printf strings in error handler.

  • If the destination file exists, it no longer exits, and asks for if the user wants to overwrite the file.

  • File size < Free space check is done in KiloBytes now.

  • Various small changes I don't recall otherwise.


Decription script follows:

#!/bin/sh

###############################################################################
##                OpenSSL file decryption POSIX shell script                 ##
##                             revision: 1.0                                 ##
##                        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

#------------------------------------------------------------------------------

colors_supported()
{
    command -v tput > /dev/null 2>&1 && tput setaf 1 > /dev/null 2>&1
}

#------------------------------------------------------------------------------

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 colors_supported
    then
        # color definitions
        readonly bold=$(tput bold)
        readonly red=$(tput setaf 1)
        readonly white=$(tput setaf 7)
        readonly yellow=$(tput setaf 3)
        readonly nocolor=$(tput sgr0)

        # here we have color support, so we highlight everything in different color
        printf '%b\n\n\t%b\n\n%b\n' \
            "${bold}${yellow}${2}${nocolor}" \
            "${bold}${white}${3}${nocolor}" \
            "${bold}${red}Error occurred, see above.${nocolor}"
    else
        # here we do not have color support
        printf '%b\n\n\t%b\n\n%b\n' \
            "${2}" \
            "${3}" \
            "Error occurred, see above."
    fi

    exit "${1}"
}

#------------------------------------------------------------------------------

# 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_prerequisites()
# expected arguments:
# any number of commands / program names
{
    while [ "${#}" -gt 0 ]
    do
        # check if the argument is a program which is installed
        command -v "${1}" > /dev/null 2>&1 || print_error_and_exit 1 "check_for_prerequisites" "This script requires '${1}' but it is not installed or available on this system!\\n\\tPlease install the corresponding package manually."
        # move to the next argument
        shift
    done
}

check_for_prerequisites openssl file grep df du tail awk

#------------------------------------------------------------------------------

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 -i 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!
if [ -e "${output_file}" ]
then
    printf '%s' "Do you want to overwrite the file ${output_file}? [y/.]"
    read -r overwrite
    [ "${overwrite}" = "y" ] || print_error_and_exit 1 "[ ${overwrite} = y ]" "You have decided not to overwrite the file ${output_file}. Aborting."
fi

#------------------------------------------------------------------------------

file_size=$(du -k "${input_file}" | awk '{ print $1 }')
free_space=$(df -k "${output_directory}" | tail -n 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}"

#------------------------------------------------------------------------------

# here comes the core part - decryption of the given file

# we shall use 'pv' for files larger than 1GiB if available
[ "${file_size}" -gt $(( 1024 * 1024 )) ] && use_pv_if_available=0 || use_pv_if_available=1

if [ "${use_pv_if_available}" -eq 0 ] && command -v pv > /dev/null 2>&1
then
    pv -W -p -t -e -r -a -b "${input_file}" | openssl enc -aes-256-cbc -md sha256 -salt -out "${output_file}" -d
else
    openssl enc -aes-256-cbc -md sha256 -salt -in "${input_file}" -out "${output_file}" -d
fi

#------------------------------------------------------------------------------

# in this test, the SC2181 information is safe to ignore
# link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2181
# shellcheck disable=SC2181
if [ "${?}" -eq 0 ]
then
    colors_supported && ( tput bold; tput setaf 2 )
    echo "Decryption successful."
    colors_supported && tput sgr0
else
    [ -f "${output_file}" ] && rm "${output_file}"
    print_error_and_exit 1 "openssl enc -aes-256-cbc -md sha256 -salt -in ${input_file} -out ${output_file} -d" "Decryption failed."
fi

As always I do not attach the encryption script, as it is almost the same piece of code.


Uploaded on GitHub: https://git.io/fxslm

\$\endgroup\$
1
\$\begingroup\$

The comment

# 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

doesn't add anything. Instead, it should summarize what SC2016 is, (instead of requiring us to follow a link that's outside our control), and why the script needs to use the constructs it warns about.

I found only one line where we have text in single quotes that looks like parameter expansion, so we can greatly reduce the scope of the suppression:

    if [ "${#}" -ne 3 ]
    then
        # $1, $2 and $3 are to be shown literally in this message
        # shellcheck disable=SC2016
        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

Similarly, consider this suppression:

# 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

It can be more helpfully commented:

# Don't warn that no caller passes arguments - that's the point of the test
# shellcheck disable=SC2120

Although, TBH, I wouldn't waste effort checking that the caller hasn't passed arguments we won't use - just ignore them, as do standard commands such as true or false.

Finally,

# in this test, the SC2181 information is safe to ignore
# link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2181
# shellcheck disable=SC2181

becomes:

# Testing $? is clearer than embedding preceding pipeline here
# shellcheck disable=SC2181

The test for sufficient space is dubious - even if the space exists now, we can't be sure that it will still be there a few cycles later when we actually write the file (or we might be out of inodes, or we might have reached the user's quota, or any number of issues). It's better to attempt to write the output, and recover gracefully if we fail (e.g. remove partly-written files).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.