#!/bin/bash
#
# incrypt - a tool to perform in-place alterations to device encryption
#
# it will convert to and from unencrypted, dm-crypt(plain) and dm-crypt(LUKS) formats
#
# See also http://johnlane.ie/incrypt-in-place-crypto-conversion.html
# Download https://raw.githubusercontent.com/johnlane/random-toolbox/master/usr/bin/incypt
#
# Part of the Random Toolbox https://github.com/johnlane/random-toolbox
# (c) John Lane 2014-11-10 Published under the MIT License.
#
# JL20141110
#
# This is pre-alpha software that could easily destroy data: use with caution!
#
############################################################################# JL20141110 ###

log() { echo $@; }
abort() { log "$@. Cannot continue."; exit 1; }
usage() {
  echo "Usage: $0 raw-device-or-file [convert-from] convert-to"
  echo "       convert between raw (unencrypted), plain or luks"
  echo "       convert-from will autodetect if not specified"
  exit 0
}

# Run as root
[[ $EUID == 0 ]] || { sudo $0 "$@"; exit; }

# Command-line arguments
[[ "$#" -ge 2 && (-f "$1" || -b "$1") ]] || usage
raw_device=$1; shift
[[ "$#" -ge 2 ]] && from_crypto="$1" && shift
to_crypto="$1"; shift

# Convert a hex digit string into a binary byte string
# http://stackoverflow.com/a/25724518
hex2bin() {
  local b=''
  local i=0
  test $((${#1} & 1)) == 0 || abort "Encountered hex stream with an odd number of characters"
  for ((i=0; i<${#1}; i+=2));do b+=\\x${1:$i:2};done
  printf "$b"
}

# Wrapper around 'mktemp' to collect files and provide a cleanup function to securely delete them
tempfiles=()
tempfile() {
  local var=$1; shift
  local t=$(mktemp "$@")
  tempfiles+=("$t")
  eval ${var}="$t"
}
cleanup() { for f in "${tempfiles[@]}"; do shred $f; done; }

# try to detect any crypto on the raw device unless explicitly given
if [[ -z "${from_crypto}" ]]
then
  case $(blkid -s TYPE -o value "${raw_device}") in
    crypto_LUKS)
      cryptsetup isLuks "${raw_device}" || abort "detected ${raw_device} as LUKS but check failed"
      from_crypto=luks
      ;;
    '')
      abort "Unable to autodetect type of ${raw_device}. Please specify type explicitly"
      ;;
    *)
      from_crypto=raw
  esac
fi

# proceed only with known crypto
[[ "${from_crypto}" == "${to_crypto}" ]] && abort "Pointless converting $from_crypto to $to_crypto"
[[ "${from_crypto}" =~ ^(raw|plain|luks)$ ]] || abort "cannot convert from '${from_crypto}'"
[[ "${to_crypto}" =~ ^(raw|plain|luks)$ ]] || abort "cannot convert to '${to_crypto}'"
echo "Converting ${raw_device} from ${from_crypto} to ${to_crypto}" 

# need to whether to re-encrypt when converting luks to plain
if [[ "${from_crypto}" == 'luks' && "${to_crypto}" == 'plain' ]]
then
  [[ "$#" -gt 0 ]] && reencrypt_luks="${1:0:1}" && reencrypt_luks=${reencrypt_luks^} && shift
  [[ "${reencrypt_luks}" =~ ^(Y|N)$ ]] || abort "Need to know whether to reencrypt"
fi

# default arguments for plain-mode dm-crypt or override with any remaining command-line arguments
[[ "$#" -gt 0 ]] && cryptsetup_args="$*" || cryptsetup_args="--cipher aes-xts-plain64 --key-size=512 --hash sha512"
    
tempfile tmp # Create a temporary file

if [[ "$to_crypto" == 'plain' ]]
then

  if [[ "${from_crypto}" == 'luks' && "${reencrypt_luks}" == 'Y' ]]
  then
    # create luks device mapper to copy from
    echo Enter the current luks dm-crypt passphrase when prompted
    cryptsetup open "${raw_device}" incrypt-src
    src="/dev/mapper/incrypt-src"

    # create the destination device mapper
    echo "Choose a new passphrase for plain dm-crypt"
    cryptsetup open "${raw_device}" incrypt --type plain ${cryptsetup_args}
    dst="/dev/mapper/incrypt"

  elif [[ "${from_crypto}" == 'luks' && "${reencrypt_luks}" == 'N' ]]
  then
    # retrieve luks master key and offset
    echo Enter the current luks dm-crypt passphrase when prompted
    cryptsetup open "${raw_device}" incrypt
    dmsetup table --target crypt --showkey /dev/mapper/incrypt > $tmp
    cryptsetup close incrypt
    dm_type=$(awk '{print $3}' $tmp)
    [[ "$dm_type" == "crypt" ]] || abort "Unexpected device mapper '$dm_type' for ${raw_device}"
    master_key=$(awk '{print $5}' $tmp)
    dd_skip="skip=$(awk '{print $8}' $tmp)" # skip the offset to the beginning of data
    src="$raw_device"
    dst="$src"
    keyfile=$(blkid -s UUID -o value "${raw_device}").key.bin
    ( hex2bin $master_key ) > "${keyfile}"
    echo "Key written to $keyfile"
  else # raw to plain conversion
    src="$raw_device"

    # create the destination device mapper
    echo "Choose a passphrase for plain dm-crypt"
    cryptsetup open "${raw_device}" incrypt --type plain ${cryptsetup_args}
    dst="/dev/mapper/incrypt"

  fi

  # convert in-place with visual progress feedback
  dd if="${src}" ${dd_skip} 2>/dev/null | mbuffer | dd conv=notrunc of="${dst}" 2>/dev/null

elif [[ "$to_crypto" == 'luks' ]]
then

  # Check the raw device and get encryption parameters
  if [[ "$from_crypto" == "plain" ]]
  then
    echo Enter the current plain dm-crypt passphrase when prompted
    cryptsetup open "${raw_device}" incrypt --type plain ${cryptsetup_args}
    dmsetup table --target crypt --showkey /dev/mapper/incrypt > $tmp
    cryptsetup close incrypt
    start_sector=$(awk '{print $1}' $tmp)
    num_sectors=$(awk '{print $2}' $tmp)
    dm_type=$(awk '{print $3}' $tmp)
    [[ "$dm_type" == "crypt" ]] || abort "Unexpected device mapper '$dm_type' for ${raw_device}"
    cipher=$(awk '{print $4}' $tmp)
    master_key=$(awk '{print $5}' $tmp)
    master_key_size=$(( ${#master_key} * 4 ))

    tempfile master_key_file
    ( hex2bin $master_key ) > "${master_key_file}"
  fi

  # Create a temporary LUKS device (we only want the header)
  echo "Creating LUKS header: choose a passphrase when prompted"
  args=''
  [[ -n "$master_key_file" ]] && args+=" --master-key-file $master_key_file"
  [[ -n "$master_key_size" ]] && args+=" --key-size $master_key_size"
  [[ -n "$cipher" ]] && args+=" --cipher $cipher"
  head -c 2M /dev/urandom > $tmp # make a file big enough to hold a LUKS header
  cryptsetup luksFormat $tmp $args

  # Extract the LUKS header
  tempfile hdr -u             # header dump file must not exist
  cryptsetup luksHeaderBackup $tmp --header-backup-file $hdr
  offset=$(cryptsetup luksDump $hdr | sed -nr 's/^Payload offset:\s*(.*)/\1/p')

  # Move the payload to make room for the header
  echo "Moving data by $offset sectors ($(($offset * 512)) bytes) to make space for LUKS header"
  dd if="${raw_device}" 2>/dev/null | mbuffer -s 512 -b ${offset} -P 100 | dd of="${raw_device}" seek="${offset}" conv=notrunc 2>/dev/null

  # Write the header
  echo "Writing header"
  dd if="${hdr}" of="${raw_device}" conv=notrunc 2>/dev/null

  if [[ "$from_crypto" == 'raw' ]]
  then
    # create a device mapper
    echo "Enter LUKS passphrase again"
    cryptsetup open "${raw_device}" incrypt

    # convert in-place with visual progress feedback
    echo "Encrypting raw data"
    dd if="${raw_device}" skip="${offset}" 2>/dev/null | mbuffer | dd conv=notrunc of=/dev/mapper/incrypt 2>/dev/null

  fi

elif [[ "$to_crypto" == 'raw' ]]
then

  # Create a device mapper to read from
  cryptsetup isLuks "${raw_device}" && cryptsetup open "${raw_device}" incrypt || cryptsetup open "${raw_device}" incrypt --type plain ${cryptsetup_args}

  # convert in-place with visual progress feedback
  dd if=/dev/mapper/incrypt 2>/dev/null | mbuffer | dd of="${raw_device}" conv=notrunc 2>/dev/null

fi

# destroy any device mappers
sleep 1 # avoid "device-mapper: remove ioctl on incrypt failed: Device or resource busy"
for dm in incrypt incrypt-src
do
  [[ -f "/dev/mapper/$dm" ]] && cryptsetup close "$dm"
done

cleanup