#! /bin/bash
#
# -- aespipe-wrapper-0.46, 2006-09-09; Martin A. Brown <martin@linux-ip.net>
#
# -- released under the GPL
#
# -- This is a little wrapper script to make aespipe easier to use
#    for automated scripting in shell.  Fundamentally, there are a few
#    things that are nifty or helpful about this script:
#
#      - the keys generated by reading from /dev/{u,}random are
#        not held in script variables, but only in pipes; i.e.,
#        you have to be root or share the same UID to see them, and
#        even then, it'd be difficult
#
#      - the file descriptor management when calling aespipe requires
#        that we feed the data on a pipe, see the below example; shell
#        trick courtesy of Jon Nelson:
#
#                 aespipe -p 7 7< <( generate_keys )
#
#        which performs process substitution on generate_keys and feeds
#        the output from generate_keys to aespipe on file descriptor 7
#
#      - and in the function create_keyset, by using tee and a free
#        file descriptor, we can send the stream of data to two
#        different consumers
#
# -- aespipe, written by Jari Ruusu, et alia can be found here:
#
#        http://loop-aes.sourceforge.net/aespipe/

# -- ChangeLog
#
#    0.47 2006-09-09; -MAB
#         version bump for packaging
#    0.46 2006-09-09; -MAB
#         minor update to $DEFAULT_GPG_IDENTITY
#    0.45 2006-09-09; -MAB
#         more error checking and more elegant failure model in
#         validate_id
#    0.44 2006-05-18; -MAB
#         internal, version bump; automating distribution of tarballs
#         including version in --long-usage output
#    0.43 2006-05-18; -MAB
#         internal, version bump; distributing as tarball
#    0.42 2006-05-18; -MAB
#         documentation change discussing null-padded blocks
#         check to see if <id> can be used for encryption
#    0.41 2006-05-17; -MAB
#         fix dumb error in a function that calls "gripe"
#    0.40 2006-05-17; -MAB
#         redesign error reporting, to be more complete
#         reporting of required missing utilities is better
#         add --long-usage option with tons of "help", including
#           several tested examples
#         support multiple GPG --recipient public keys
#    0.30 2006-05-15; -MAB
#         provide pass-through for options to aespipe (combination of
#           ideas from Jon Nelson and Marcin Antkiewicz)
#    0.20 2006-05-09; Jon Nelson <jnelson@jamponi.net>
#         added verify_support, fixed some quoting mistakes
#    0.10 2006-05-09; -MAB
#         initial skeleton and functionality tested
#

# -- Contributors
#
#    Jon Nelson <jnelson@jamponi.net>
#    Marcin Antkiewicz <marcin@kajtek.org>

# -- BUGS/TODO
#
#    - if the user does not specify a GPG identity, and there is no
#      default key, then the behaviour of this script is undefined.
#      In short, don't run this script unless you have a default GPG
#      key.
#
#    - I go to great lengths in create_keyset to avoid stepping on
#      somebody else's file descriptor, and then later, I just pick on
#      file descriptor 42.  This is dumb and should be fixed.
#
#


# -- set some sane defaults
#
DEFAULT_INPUT=/dev/stdin          # -- behave like a good little filter
DEFAULT_OUTPUT=/dev/stdout
DEFAULT_RANDOM=/dev/urandom       # -- faster, not necessarily better
VERBOSE=0
STDOUT=1
STDERR=2

# -- variables used mostly for internal use
VERSION=0.47
NAME=aespipe-wrapper
REQUIRED_UTILITIES="sed head uuencode aespipe gpg tee"

# -- my favorite shell functions (and their offspring)
#
abort  () { gripe STDERR "$@"; exit 1;     }

# - - - - - - - - - 
  gripe () { 
# - - - - - - - - - 
#
  local descriptor=$1   && shift
  printf -- "%s\n" "$@" >&${!descriptor}
}


# - - - - - - - - - - -
  version  () {
# - - - - - - - - - - -
#
  gripe STDOUT "${0##*/}-${VERSION}"
  exit 0
}

# - - - - - - - - - - -
  usage  () {
# - - - - - - - - - - -
#
  local descriptor=$1   && shift
  cat >&${!descriptor} <<-EOUSAGE
Usage: ${0##*/} [ options ] -k <file> [ -- [ aespipe_options ] ]

Keyfile option (required):
  -k, --keyfile <file>   Specify filename in which to store AES keys.

Action options (one of these mutually exclusive options is required):
  -D, --decrypt          Decrypt the input file
  -E, --encrypt          Encrypt the input file
  -C, --create           Create and encrypt a keyset into <keyfile>.

Options (defaults marked, or in parentheses):
  -h, --help             Print out this handy help screen.
  -L, --long-usage       Provide help and some more hints.
  -q, --quiet            Reset verbosity to none.
  -v, --verbose          Be verbose.  (interactive default)
  -i, --input <name>     Specify input filename ($DEFAULT_INPUT)
  -o, --output <name>    Specify output filename ($DEFAULT_OUTPUT)
  -U, --urandom          Use /dev/urandom. (default)
  -R, --random           Use /dev/random, might be slow.
  -r, --recipient <id>   Encrypt AES keys with <id>'s GPG pubkey. (self)
                         <id>s must be shell-safe (e.g., no spaces)

EOUSAGE
}

# - - - - - - - - - - -
  short_usage  () {
# - - - - - - - - - - -
#
  local descriptor="${1:-STDERR}"  && shift
  usage $descriptor
  test "$#" -gt 0 && abort "$@"
  exit 0
}

# - - - - - - - - - - -
  long_usage  () {
# - - - - - - - - - - -
#
  local descriptor=$1   && shift
  cat >&${!descriptor} <<-EOUSAGE
${0##*/}-${VERSION} README

Description
===========
This is simply a thin convenience wrapper around aespipe*.  The aespipe
utility acts as a filter (reading from STDIN, writing to STDOUT), but it
also reads a set of keys on a separate file descriptor.  There are two
sets of keys involved in the operation of this script.

  aespipe uses a set of AES keys for encrypting your stream of data
  GPG encrypts the AES keys, so they aren't stored unsafely on disk

NOTE:  During encryption, aespipe null-pads the end of the file to reach
       a block boundary.  During decryption, aespipe does not remove
       these nulls, so the checksum of the initial file and the
       decrypted will differ.  Compression utilities are comfortable
       stripping the nulls, so consider using bzip2/gzip before
       passing the data to aespipe.

Modes
=====
Encryption (--encrypt):  To accommodate automated encryption, ${0##*/}
does the following:

  1. it does not ask for passwords or passphrases from the terminal in
     encryption mode (suitable for fully automated use)
  2. it generates a keyset suitable for aespipe encryption
  3. it stores the keyset in the specified keyfile, encrypted using GPG
     public key cryptography to an arbitrary set of recipients
  4. finally, it calls aespipe, feeding STDIN, STDOUT and keys

Decryption (--decrypt):  Decryption is simpler.  The wrapper simply
calls aespipe with the provided keyfile and the input and output
redirections in place.

Creation (--create):  Key creation is a simple way to test that everything is working, and is
only supplied as a convenience.

The user must always:

  - specify a keyfile with the -k (--keyfile) option
  - specify an action (--decrypt, --encrypt or --create)

If you have a need to pass options through to aespipe itself, simply place
them after \"--\", and they will be passed through directly to aespipe.
See the example section below.


Options
=======
${0##*/} operates on STDIN and STDOUT, as any good filter.  It provides
the compatibility options --input (-i) and --output (-o) for convenience.

Usually, data collected by the pseudo-random device (/dev/urandom) is
suitably random to use in cryptography, although many people prefer to
use a real random source (e.g., /dev/random).  Because of the possibility
for program delay awaiting random data, the default option is the 
pseudo-random device.

If no GPG recipient <id> is specified with the -r (--recipient) option,
${0##*/} will use GPG's --default-recipient-self option to
specify a public key identity use for encrypting the AES keys.  Multiple
recipients can be specified with the --recipient option.  This could
allow any of the recipients to decrypt the AES keyset.

Examples
========
Encrypt the word 'HOWDY.' into a file called howdy.aes.  Retain the AES
keys used for encryption in the howdykeys.asc file.

  echo HOWDY. | aespipe-wrapper -E -k howdykeys.asc -o howdy.aes

Decrypt the file called howdy.aes, using the keys in the file called
howdykeys.asc.  Note that this will require reading the user's GPG
passphrase from the terminal.

  aespipe-wrapper -D -k howdykeys.asc -i howdy.aes

In this more practical example, let's look at how we might use
aespipe to encrypt a backup of our files.  To add further complexity,
let's ask aespipe to use 256-bit keys:

  tar --create                  \\
      --bzip2                   \\
      --to-stdout               \\
      --files-from \$filelist    \\
    | aespipe-wrapper --encrypt \\
      --keyfile \$keys           \\
      --output \$archive         \\
      -- -e aes256

Now, let's decrypt that same data file.  Note that we have to specify the
pass-through option to aespipe ('-e aes256') or else we'll have no luck
at all decrypting the data.

  aespipe-wrapper --decrypt     \\
      --input \$archive          \\
      --keyfile \$keys           \\
      -- -e aes256              \\
    | bzip2 --decompress        \\
      --quiet                   \\
      --stdout                  \\
    | tar --list

Comments, criticism, corrections and suggestions are welcome.

 *How in the world did you get here without aespipe!?  If you don't have
  this great utility, then sprint to the aespipe site and grab yourself
  a copy:  http://loop-aes.sourceforge.net/aespipe/

EOUSAGE

  usage $descriptor

exit 0

}

# - - - - - - - - - - -
  parse_options () {
# - - - - - - - - - - -
#
  command getopt                    \
    --unquoted                      \
    --name "${0##*/}"               \
    --options "${OPTIONS}"          \
    --longoptions "${LONGOPTIONS}"  \
    -- "$@"
}

# - - - - - - - - - - -
  verify_support  () {
# - - - - - - - - - - -
#
# -- all we want to do is make sure that all of our support
# programs exist.
#
  local program
  local programs="$@"
  local N
  local ok=0           # -- return 0, if everything is OK!
  
  for program in $programs; do
    N=$( which "$program" 2>/dev/null )
    if test -z "$N"; then
      ok=1
      gripe STDERR "Could not locate necessary support program:  $program"
    fi
  done
  return $ok
}


# - - - - - - - - - - -
  generate_keys  () {
# - - - - - - - - - - -
#
# -- Generate some random data for use as keys.
#     - if user did not specify number of keys, then use 65
#     - create $BYTES bytes of random data
#     - uuencode the random data
#     - take only the first $lines of the output
#
  local RAND_SOURCE="${1}"    && shift
  local NUMKEYS="${1:-65}"    && shift  # -- Optional, MUST be last argument
  let LINECOUNT="${NUMKEYS}+1"
  let BYTES="${NUMKEYS}*45"
  
  head -c $BYTES $RAND_SOURCE  \
    | uuencode -m -            \
    | sed -ne "2,${LINECOUNT}p"
}

# - - - - - - - - - - -
  call_aespipe  () {
# - - - - - - - - - - -
#
# -- remember that we may be receiving keys on FD 7 from
#    the caller, but otherwise, we are just acting like a filter,
#    reading from STDIN and writing to STDOUT.  It's true that
#    might actually be files, but aespipe doesn't care.
#
  aespipe "$@" < "$INPUT" > "$OUTPUT"
}

# - - - - - - - - - - -
  opt_excl  () {
# - - - - - - - - - - -
#
# -- all of the options are mutually exclusive.  If a user has
#    called two options, we need to halt before we do anything
#    stupid.
# -- analogously, if the user specifies the same option twice,
#    there may be a problem; so also abort here
#
  local mode="$1" && shift
  local user_specified="$1" && shift
  test "$mode" == "$user_specified" \
    && abort "Action option specified multiple times. -E, -D or -C once only."
  test -n "$mode" \
    && abort "Conflicting action options specified. -E, -D or -C only."
  printf -- "%s\n" "$user_specified"
}

# - - - - - - - - - - -
  validate_id  () {
# - - - - - - - - - - -
#
# -- all we want to do is make sure that the GPG ID specified
#    exists on the keyring
#
  local gpgid="$1"   && shift
  verify_support gpg || return   # -- ugly, let the script fail later

  # -- does the specified ID exist on the GPG keyring?
  #
  gpg >/dev/null 2>&1              \
    --no-secmem-warning            \
    --list-keys "$gpgid"

  # -- if we did not find the user-specified GPG recipient on
  #    the keyring, let's gripe about it, but keep going
  #
  if test "$?" -ne 0 ; then

    printf -- "%s\n" "${GPG_IDENTITY:-$DEFAULT_GPG_IDENTITY}"
    gripe STDERR "Could not find public key for ID:  $gpgid"
    return

  fi

  # -- is the public key usable for automated encryption?
  #
  gpg </dev/null >/dev/null 2>&1   \
    --encrypt                      \
    --armor                        \
    --quiet                        \
    --batch                        \
    --no-tty                       \
    --recipient "$gpgid"           \
    --output /dev/null
  
  if test "$?" -ne 0 ; then

    printf -- "%s\n" "${GPG_IDENTITY:-$DEFAULT_GPG_IDENTITY}"
    gripe STDERR "Could not encrypt using public key for ID:  $gpgid"
    return

  fi

  test "$GPG_IDENTITY" = "$DEFAULT_GPG_IDENTITY" && unset GPG_IDENTITY

  printf -- "$GPG_IDENTITY --recipient %s\n" "$gpgid"
}

# - - - - - - - - - - -
  encrypt_to_self  () {
# - - - - - - - - - - -
#
#  - and send the rest to gpg for storing to disk
#  - note that we DO not protect $GPG_IDENTITY with quotations!
#    we might have multiple recipients
#
  local output="${1}"  && shift
  gpg 2>/dev/null                  \
    --encrypt                      \
    --armor                        \
    --no-secmem-warning            \
    --quiet                        \
    --batch                        \
    --no-tty                       \
    --output "$output"             \
      $GPG_IDENTITY ;
}

# - - - - - - - - - - -
  create_keyset () {
# - - - - - - - - - - -
#
# -- make a little shell grouping so we can play FD games
#
  local KEY_SOURCE="$1"     && shift
  unset FD
  local fd
  local FREEFD
  local low=5
  local high=20

  # -- find a free file descriptor, between $low and $high
  #
  for fd in $( seq $low $high ); do
    if ! test -e /dev/fd/$fd ; then
      FREEFD=/dev/fd/$fd
      break
    fi
  done
  test -n "$FREEFD" \
    || abort "Could not get a free file descriptor between $low and $high."
  

  # -- open up another file descriptor and tie it
  #    to the STDOUT of the function, letting our caller
  #    do something else with it
  #
  eval exec "${fd}>&1"

  {
    generate_keys $KEY_SOURCE  \
      | tee $FREEFD                \
      | encrypt_to_self "$@"
  }
}

# -- Set some sane defaults
#
KEY_SOURCE=${KEY_SOURCE:-$DEFAULT_RANDOM}
INPUT=${INPUT:-$DEFAULT_INPUT}
OUTPUT=${OUTPUT:-$DEFAULT_OUTPUT}

DEFAULT_GPG_IDENTITY="--default-recipient-self"
GPG_IDENTITY="$DEFAULT_GPG_IDENTITY"

unset MODE

verify_support which \
  || abort "${0##*/}: Cannot proceed without \"which\" utility, quitting."

verify_support getopt head \
  || abort "${0##*/}: Cannot process command line options, quitting."

# - - - - - - - - - - -
# main () {
# - - - - - - - - - - -
#
# -- normalize the command-line options
#
OPTIONS="VvqhLDECRUi:r:k:o:"
LONGOPTIONS="verbose,version,quiet,help,long-usage,encrypt,decrypt,create"
LONGOPTIONS="$LONGOPTIONS,recip:,recipient:,keyfile:,output:,input:"
LONGOPTIONS="$LONGOPTIONS,random,urandom"

# -- calling parse_options twice seems silly.  It is.  It allows, us
#    however, to control the error reporting to the user a bit more
#    carefully.  All the OCD kids are doing it.
#
parse_options "$@" >/dev/null \
  || abort "Try \"${0##*/} --help\" for more information."

set -- $( parse_options "$@" )

while test "$#" -gt "0" ; do
   case "$1" in

      -h | --h*    ) short_usage STDOUT                       ;;
      -L | --l*    ) long_usage STDOUT                        ;;
      -V | --vers* ) version                                  ;;
      -v | --verb* ) let VERBOSE=VERBOSE+1                    ;;
      -q | --q*    ) VERBOSE=0                                ;;
      -D | --d*    ) MODE=$( opt_excl "$MODE" decrypt )       ;;
      -E | --e*    ) MODE=$( opt_excl "$MODE" encrypt )       ;;
      -C | --c*    ) MODE=$( opt_excl "$MODE" genkeys )       ;;
      -i | --inpu* ) INPUT="$2"         && shift              ;;
      -o | --o*    ) OUTPUT="$2"        && shift              ;;
      -k | --k*    ) KEYFILE="$2"       && shift              ;;
      -U | --uran* ) KEY_SOURCE="/dev/urandom"                ;;
      -R | --rand* ) KEY_SOURCE="/dev/random"                 ;;
      -r | --reci* ) GPG_IDENTITY="$( validate_id $2 )"       ;;
             --    ) shift              && break              ;;
             -*    ) short_usage STDERR "Unknown option: $1"  ;;

   esac
   shift
done

# -- check to make sure we have all of the programs we'll need
#
verify_support $REQUIRED_UTILITIES \
  || abort "${0##*/}: Missing utilities, cannot continue, quitting."

test -z "$KEYFILE" \
  && short_usage STDERR "Keyfile option (-k, --keyfile) is required."

test -z "$GPG_IDENTITY" \
  && short_usage STDERR "Check your GPG keyring.  Are keys signed and trusted?"

# -- we should now have all of the variables set, 

case "$MODE" in

  genkeys) test -e "$KEYFILE" \
             && abort "Keyfile $KEYFILE already exists!  Aborting!"

           generate_keys "$KEY_SOURCE" | encrypt_to_self "$KEYFILE"     ;;

  decrypt) call_aespipe -d -K $KEYFILE "$@"                             ;;

  encrypt) test -e "$KEYFILE" \
             && abort "Keyfile $KEYFILE already exists!  Aborting!"
              
           call_aespipe "$@" \
                      -p 42 42< <( create_keyset $KEY_SOURCE $KEYFILE ) ;;

        *) abort "No action option specified, try ${0##*/} --help"      ;;

esac

# -- end of file
