#!/bin/sh
# Filename:      grml-crypt
# Purpose:       Program to format, mount and unmount encrypted devices/files
# Authors:       Michael Gebetsroither <gebi@grml.org>
# Bug-Reports:   see http://grml.org/bugs/
# License:       This file is licensed under the GPL v2.
# Latest change: Mon Aug 08 11:37:20 CEST 2005
################################################################################


###
### __INCLUDES
###
. /etc/grml/sh-lib
#. /etc/grml/sysexits-sh



###
### __VARIABLES
###

verbose_=0
DEV_MAPPER_="/dev/mapper"
CRYPTSETUP_="cryptsetup"
IS_IMAGE_='false'
SIZE_="10"
SIZE_SET_='false'
FSTYPE_="vfat"
TARGET_=""
VERIFY_PW_=""
MKFS_=""
DM_NAME_=""
DM_PATH_=""
ACTION_=""
DM_PREFIX_="grml-crypt_"
FORCE_='false'
OVERWRITE_SOURCE_DEV_='/dev/urandom'
OPTIMIZED_MODE_SET_='false'
OPTIMIZING_LEVEL_=0
CIPHER_SIZE_="128"
CIPHER_="aes-cbc-essiv:sha256"
ITERATION_TIME_="1"
ADDITIONAL_CRYPTSETUP_ARGS_=""
READONLY_SET_='false'
ADDITIONAL_MOUNT_ARGS_=""
BATCH_MODE_="--batch-mode"

###
### __FUNCTIONS
###

function printUsage
{
  cat <<EOT
Usage: "$PROG_NAME__" [OPTIONS] action <device/file> [mountpoint]

$PROG_NAME__ is a wrapper around cryptsetup with LUKS support to format a device

OPTIONS:
   -s         size of the loop-filesystem to create, in MB (default=$SIZE_)
   -t         type of filesystem (default=$FSTYPE_)
   -r         read only mode (fully supported only by start)
   -z         insecure mode, using /dev/zero for most of the initialisation (INSECURE!)
   -o         optimized initialisation mode (should be as secure as the default but faster)
   -y         verifies the passphrase by asking for it twice
   -f         force file overwriting in format mode and/or disable confirmation dialog
   -m         additional arguments to mount
   -v         verbose (show what is going on, v++)
   -h         this help text

CRYPTSETUP FORMAT OPTIONS:
   -S         cipher size, could be 128, 192 or 256 (default=$CIPHER_SIZE_)
   -C         cipher, should be aes-plain for pre-2.6.10 (default=$CIPHER_)
   -I         iteration time spend with PBKDF2 password  processing in seconds (default=$ITERATION_TIME_)
   -A         additional arguments for cryptsetup (only supportet by format)

ACTIONS:
   format  <device/file> [mountpoint]
              Format a device or a file (is created with the given size if it
              does not exist) with the given filesystem and mount it, if a
              mountpoint was given.
   start  <device/file> <mountpoint>
              Mount the device/file in the mountpoint.
   stop   <mountpoint>
              Umount the given mountpoint (umount, luksClose, losetup -d)

EOT

  isGrml && cat <<EOT
NOTICE:
   losetup does _NOT_ work on unionfs => grml-crypt with a filesystem image does ONLY
   work if the image is on a tmpfs (eg. in /home/grml or /tmp).

EOT

}


function getDMName
{
  device_="${1##*/}"
  
  # first trying normal devicename
  tmp_="${DM_PREFIX_}${device_}"
  if [ ! -e "$tmp_" ]; then
    echo "$tmp_"
    return 0
  fi

  # second trying uuid of luks
  #uuid_=`execute "$CRYPTSETUP_ luksUUID $1"`
  #if [[ $? == 0 ]]; then
  #  echo "$prefix_$uuid_"
  #  return 0
  #fi
  warn "could not create device-mapper name for $1"
  return 1
}


function formatDevice
{
  type_="$1"  # could be donothing or init
  ret_=0

  args_="$VERIFY_PW_ $BATCH_MODE_ --key-size $CIPHER_SIZE_ --cipher $CIPHER_ --iter-time $ITERATION_TIME_ $ADDITIONAL_CRYPTSETUP_ARGS_"
  #args_=`echo "$args_" |tr -s ' '`
  execute "$CRYPTSETUP_ $args_ luksFormat $TARGET_" warn || return 1

  execute "$CRYPTSETUP_ luksOpen $TARGET_ $DM_NAME_" warn \
    "could not open $DM_PATH_ to create a filesystem on it!" || return 1
  if [[ $type_ == 'init' && $OPTIMIZED_MODE_SET_ == 'true' ]]; then
    echo "finishing optimized initialisation (this could take some time)"
    # FIXME
    execute "dd if=/dev/zero of=$DM_PATH_ bs=1M &>/dev/null" # || \
    #  warn "could not finish optimized initialisation properly"
    ret_=$?
    # cutted out because of no space left on device error :(
    #if [[ $ret_ != 0 ]]; then
    #  execute "$CRYPTSETUP_ luksClose $DM_NAME_" warn
    #  return 1
    #fi
  fi

  execute "$MKFS_ $DM_PATH_ >/dev/null" warn
  if [[ $? != 0 ]]; then
    execute "$CRYPTSETUP_ luksClose $DM_NAME_"
    warn "could not create filesystem on $DM_PATH_" 1
    return 1
  else
    echo "Successully created $FSTYPE_ on encrypted $TARGET_"
    return 0
  fi
} 


function actionStart
{
  ret_=0
  
  # no mountpoint, by-by
  if [[ "$MOUNT_POINT_" == "" ]]; then
    printUsage
    die 'no mountpoint given'
  fi
  if [ ! -d "$MOUNT_POINT_" ]; then
    die "mountpoint $MOUNT_POINT_ does not exist"
  fi
  # removed due to unionfs problem isLuks does not work with filesystem images
  # without losetup
  #$CRYPTSETUP_ isLuks $TARGET_ || die "$TARGET_ is not a luks partition"

  # TARGET (is/should be) a filesystem image
  if [ ! -b "$TARGET_" ]; then
    notice "Operating on a file"
    isExistent "$TARGET_" die "image does not exist"
    TARGET_=`findNextFreeLoop` || die "could not find a free loop device"

    # TARGET_ is now /dev/loop<x>
    execute "losetup $TARGET_ $ORIG_TARGET_" die
  fi
  cargs_=""
  $READONLY_SET_ && cargs_='--readonly'
  execute "$CRYPTSETUP_ $cargs_ luksOpen $TARGET_ $DM_NAME_" warn || execute "losetup -d $TARGET_" || \
    die "could not luksOpen $TARGET_"
  margs_=""
  $READONLY_SET_ && margs_='-r'
  execute "mount $margs_ $ADDITIONAL_MOUNT_ARGS_ $DM_PATH_ $MOUNT_POINT_" die
}


function actionStop
{
  mp_="$1"
  ret_=0

  isExistent "$mp_" die
  tmp_=`realpath $mp_` || die "could not get realpath of $mp_"
  dprint "realpath_=\"$tmp_\""
  
  dm_path_=`mount |grep "$tmp_ "` || die "$tmp_ is not mounted"
  dprint "dm_path_=\"$dm_path_\""
  dm_path_=`echo $dm_path_ |awk '{print $1}'` || die "could not get devicemapper name for $tmp_"
  dprint "dm_path_=\"$dm_path_\""
  
  dm_name_="${dm_path_##*/}"
  dprint "dm_name_=\"$dm_name_\""

  dmsetup info $dm_name_ >/dev/null ||die "$dm_name_ is not aktive"
  device_=`$CRYPTSETUP_ status $dm_name_ |awk '/device:/{print $2}'` || \
    die "could not get underlying device of $dm_path_"
  dprint "device_=\"$device_\""
  
  execute "umount $dm_path_" die "could not unmount $device_"
  execute "$CRYPTSETUP_ luksClose $dm_name_" die "could not close $dm_path_"
  echo "$device_" |grep loop &>/dev/null && execute "losetup -d $device_" \
    die "could not delete loop device $device_" || \
    execute "losetup -d $device_ &>/dev/null" eprint "could not delete loop device $device_, \
this device could possible not be a loop device => maybe bogus error"
  notice "$mp_ successfully unmountet/closed/deleted"
}

function yesDialog
{
  msg_="$1"
  
  echo "WARNING!" >&2
  echo "========" >&2
  echo -n "$msg_" >&2
  echo -n " (type uppercase yes): " >&2
  read input
  if [[ $input == 'YES' ]]; then
    return 0
  fi

  return 1
}

function actionFormat
{
  IS_IMAGE_='false'
  ret_=0
  init_='init'

  if (( $SIZE_ < 3 )); then
    die "the minimum size of an encrypted luks partition should be 2"
  fi

  # TARGET (is/should be) a filesystem image
  if [ ! -b "$TARGET_" ]; then
    notice "Operating on a file"
    IS_IMAGE_='true'
    if [ -e "$TARGET_" ]; then
      $FORCE_ || die "$TARGET_ does already exist"
      warn "overwriting file $TARGET_"
      init_='donothing'
    else
      echo -n "Initialising file with "
      if [[ $OPTIMIZED_MODE_SET_ == 'true' ]]; then
        echo "optimized SECURE mode"
        execute "dd if=/dev/zero of=$TARGET_ bs=1M count=${SIZE_} &>/dev/null" \
          die "could not initialise $TARGET_ with /dev/zero"
      else
        if [[ $OVERWRITE_SOURCE_DEV_ == '/dev/zero' ]]; then
          echo "INSERCURE mode"
        else
          echo "SECURE mode (taking /dev/urandom as source, this could take some time)"
        fi
        execute "dd if=$OVERWRITE_SOURCE_DEV_ of=$TARGET_ bs=1M count=${SIZE_} &>/dev/null" ||\
          die "could not initialise $TARGET_ with $OVERWRITE_SOURCE_DEV_"
      fi
    fi

    TARGET_=`findNextFreeLoop` || die "could not find a free loop device"

    # TARGET_ is now /dev/loop<x>
    execute "losetup $TARGET_ $ORIG_TARGET_" die
    if [[ $OPTIMIZED_MODE_SET_ == 'true' || $OVERWRITE_SOURCE_DEV_ == '/dev/zero' ]]; then
      execute "dd if=/dev/urandom of=$TARGET_ bs=1M count=2 &>/dev/null" \
      die "could not initialise the fist 2MB of $TARGET_ with /dev/urandom"
    fi
    formatDevice "$init_"
    ret_=$?
  else
    $FORCE_ || (yesDialog "Are you shure you want to overwrite $TARGET_ ?" || die 'You are not sure')
    notice 'Operating on a device'
    echo -n 'Initialising device with '
    if [[ $OPTIMIZED_MODE_SET_ == 'true' ]]; then
      echo "optimised SECURE mode"
      execute "dd if=/dev/urandom of=$TARGET_ bs=1M count=2 &>/dev/null" ||\
        die "could not initialise the first 2MB of $TARGET_ with /dev/urandom"
    elif [[ $OVERWRITE_SOURCE_DEV_ != '/dev/zero' ]]; then
      # default mode
      echo "SECURE mode (taking $OVERWRITE_SOURCE_DEV_ as source, this could take some time)"
      execute "dd if=$OVERWRITE_SOURCE_DEV_ of=$TARGET_ bs=1M &>/dev/null" #||\
        # skipped because "no space left on device" from dd
        # die "could not initialise $TARGET_ with $OVERWRITE_SOURCE_DEV_"
    else
      echo 'INSECURE mode (only initialising the fist 2MB with /dev/urandom)'
      execute "dd if=/dev/urandom of=$TARGET_ bs=1M count=2 &>/dev/null" \
        die "could not initialise the first 2MB of $TARGET_ with /dev/urandom"
    fi

    formatDevice "$init_"
    ret_=$?
  fi

  # formatDevice was successfully
  if (( $ret_ == 0 )); then
    # a mountpoint was given (don't luksClose the device)
    local mount_point_exists_='true'
    test -d "$MOUNT_POINT_" || mount_point_exists_='false'

    if [[ $MOUNT_POINT_ != "" && "$mount_point_exists_" == 'true' ]]; then
      $READONLY_SET_ && margs_='-r'
      execute "mount $margs_ $ADDITIONAL_MOUNT_ARGS_ $DM_PATH_ $MOUNT_POINT_" die
    else
      if [[ $MOUNT_POINT_ != "" ]]; then
        $mount_point_exists_ || warn "mountpoint $MOUNT_POINT_ does not exist, not mounting. please use \"grml-crypt start $ORIG_TARGET_ <mountpoint>\" to start the device"
      fi
      execute "$CRYPTSETUP_ luksClose $DM_NAME_" warn
      $IS_IMAGE_ && execute "losetup -d $TARGET_" warn
    fi
  else
    execute "$CRYPTSETUP_ luksClose $DM_NAME_" warn
    $IS_IMAGE_ && execute "losetup -d $TARGET_" warn
  fi
}



###
### __MAIN
###

while getopts "s:t:rzoyfm:hvS:C:I:A:" opt; do
  case "$opt" in
    s) SIZE_="$OPTARG"; SIZE_SET_='true' ;;
    t) FSTYPE_="$OPTARG" ;;
    r) READONLY_SET_='true' ;;
    z) let OPTIMIZING_LEVEL_=$OPTIMIZING_LEVEL_+1
        OVERWRITE_SOURCE_DEV_='/dev/zero'
        warn 'initialising from INSECURE source /dev/zero' ;;
    o) let OPTIMIZING_LEVEL_=$OPTIMIZING_LEVEL_+1
        OPTIMIZED_MODE_SET_='true' ;;
    y) VERIFY_PW_="--verify-passphrase" ;;
    f) FORCE_='true' ;;
    m) ADDITIONAL_MOUNT_ARGS_="$OPTARG" ;;
    h) printUsage; exit ;;
    v) let verbose_=$verbose_+1 ;;
    S) CIPHER_SIZE_="$OPTARG" ;;
    C) CIPHER_="$OPTARG" ;;
    I) ITERATION_TIME_="$OPTARG" ;;
    A) ADDITIONAL_CRYPTSETUP_ARGS_="$OPTARG" ;;
    ?) printUsage; exit 64 ;;
  esac
done
shift $(($OPTIND - 1))  # set ARGV to the first not parsed commandline parameter
setVerbose $verbose_

checkRoot die "You have to be root to use this program"
disableSyslog

if [[ $1 == 'help' ]]; then
  printUsage
  exit 0
fi
if (( $# < 2 )); then
  printUsage
  die "wrong number of arguments ($#)" 1
fi
if (( $OPTIMIZING_LEVEL_ > 1 )); then
  printUsage
  die "please choose ONE initialisation methode"
fi
TARGET_="$2"

MKFS_="/sbin/mkfs.$FSTYPE_"
if [ ! -x "$MKFS_" ]; then
  die "invalid filesystem type \"$FSTYPE_\"" 1
fi

# use batch-mode if available
$CRYPTSETUP_ $BATCH_MODE_ --help &>/dev/null;
ret_=$?
case "$ret_" in
  0)  dprint "your cryptsetup understands --batch-mode" ;;
  1)  BATCH_MODE_=""; notice "your cryptsetup does NOT understand --batch-mode, trying without" ;;
  127)  die "could not execute cryptsetup" 127 ;;
  *)  warn "problems executing $CRYPTSETUP_" $ret_
esac

DM_NAME_="`getDMName $TARGET_`"
DM_PATH_="$DEV_MAPPER_/$DM_NAME_"
ORIG_TARGET_="$TARGET_"
MOUNT_POINT_="$3"

case "$1" in
  format) ACTION_='format'; actionFormat ;;
  start)  ACTION_='start'; actionStart ;;
  stop)  ACTION_='stop'; actionStop "$TARGET_" ;;
  *)  printUsage ;;
esac

# END OF FILE
################################################################################
# vim:foldmethod=marker tabstop=2 expandtab shiftwidth=2
