#!/bin/bash
# $Id: tripl,v0.71 9/20/2009 17:04:35 wdef Exp $

# tripl - single or multiple encryption with loop-aes-ciphers
# (c) 2006-2009 wdef v0.71 <wdef200 at users dot sourceforge dot net>

# Wrapper to manage single or multiple encrypted partitions using loop-aes v3.x (multikey).
# Automates gpg key generation, allocation and setup/pulldown of loops, layered embedding of keys, 
# filesystem checking etc. Does not write to /etc/fstab. Note: multiple encryption can use a lot of
# cpu at times. Single or double encryption with a good passphrase is sufficient (really!).
# Script skips loops that are already in use and only pulls down encrypted loops chained to device 
# on umounting. Blowfish is not supported since it does not work with multiline keychains in loopaes
# and is not recommended for large amounts of data.
# Tries hard to prevent loop collisions if you are also using some loops for other things.

# This is free software. No warranty. Use AYOR.

#===================// FUNCTIONS //============================


help(){
cat <<"EOF"
tripl - single or multiple encryption with loop-aes
(c) 2006-2009 wdef v0.71
Usage: tripl [-f <file> ] [-muknrh ]
Option:
	-m = mount
	-u = unmount
	-k = make key
	-n = set up encrypted partition (destroys data!)
	-r = check, repair encrypted filesystem
	-f = use <file> as triplrc instead of ~/.triplrc
	-h = this
Set partition name, keys, order of ciphers, mountpoint and MODE 
(1,2,3 for single,double,triple encryption) in triplrc
EOF
}


set_example_config(){
cat <<EOF

# Make these settings in $HOME/.triplrc, where $HOME is root's home dir.

DEVICE=/dev/your_device_here
MOUNTPT=/mnt/your_mountpoint_here

#MODE=3		# Triple encryption
MODE=2		# Double encryption
#MODE=1		# Single encryption

# You can rearrange the order of ciphers, but only for a new setup: 
#- you'll have to destroy data by runnning tripl -n again if you change this (so back up data first).

CIPHER[1]=serpent128
CIPHER[2]=twofish128
CIPHER[3]=AES128

# Run tripl -k to make a key, then set it here:

key[1]=/path_to_your_key1
key[2]=/path_to_your_key2
key[3]=/path_to_your_key3

FS=ext2		# Filesystem: ext2, ext3
#FS=ext3


#EMBED=no
EMBED=yes	# If enabled, the user only need specify one external gpg-encrypted key (key[1]) in user settings.
		# Other keys will be automatically generated for each encryption layer by tripl -n (prompts user
		# for passphrases) and successively embedded in the encryption layer previous to that being set up.
		# This also means an attacker has to crack the first encryption layer just to get the 
		# encrypted key for the second, and so on. No effect if MODE=1
		
		# If disabled, a seperate external (detached) key should be created with tripl -k for each 
		# encryption layer and set in this config file prior to running tripl -n
		
#FORCE=OFF	# No attempt to interrupt processes to do umount
FORCE=GENTLE	# Flush buffers and signal politely before resorting to sigkill
#FORCE=BRUTE	# Only to enable an emergency rapid umount.

VERBOSE=yes	# Show what is being done. 
#VERBOSE=no

GPGHOME="${HOME}/.gnupg"

DISABLE_NEW=yes	# Disable new encrypted paritition switch as a double failsafe measure to prevent partition destruction
		# Enable when you're sure you need it.
		
		
MSG=""
# MSG="[tripl] "  # set this to prefix messages coming from tripl, for logging or debugging

# KERNELPATCH=yes       # yes if loopaes patch has been applied to kernel, no loop module.
KERNELPATCH=no
		
# If non-standard /path/to/utils, set here:
# losetup=
# mount=
# umount=
# fsck=
# modprobe=
EOF
}


check_losetup(){
if $(which strings &>/dev/null); then
	strings $losetup | grep -q -s multi-key-v3
	return $?
elif grep --version | grep -q 'GNU grep' 2>/dev/null; then
	grep -q -a -s multi-key-v3 $losetup
	return $?
else
	echo "${MSG}Error: can't check losetup compatibility"
	echo "${MSG}Install strings or GNU grep before proceeding."
	exit 1
fi
}


run_checks(){
if [ $EUID -ne 0 ]; then echo "${MSG}You're not root."; exit 1; fi
if [ $# -eq 0 ]; then help; exit 1; fi
if ! which gpg &>/dev/null; then echo "${MSG}Can't find GnuPG"; exit 1; fi
if [ $MAXLOOPS -eq 0 ]; then echo "${MSG}Can't find any loop devices"; exit 1;fi
}


set_modules(){
# Set modules to match ciphers
for p in $(seq 1 $MODE); do
	case ${CIPHER[p]} in
		aes128|AES128) MODULE[$p]=loop;;
		serpent128|SERPENT128) MODULE[$p]=loop_serpent;;
		twofish128|TWOFISH128) MODULE[$p]=loop_twofish;;
		"") echo "${MSG}Config error: CIPHER[$p] is unset"; exit 1 ;;
		*) echo "${MSG}Config error: CIPHER[$p] = ${CIPHER[p]} is unsupported"; exit 1 ;;
	esac
done
}

check_device_free(){
if $losetup -a | grep -wq ${DEVICE} || grep -wq ${DEVICE} /proc/mounts; then
	echo "${MSG}Error: ${DEVICE} is in use."
	exit 1
fi
}


conf_checks(){

if [ "$CLI_CONFIG" != yes ]; then
	if [ -f "${HOME}/.triplrc" ]; then
		. ${HOME}/.triplrc
	else
		set_example_config >${HOME}/.triplrc
		echo "${MSG}Edit ${HOME}/.triplrc for your desired setup"
		echo "${MSG}Then run tripl again."
		exit 0
	fi
fi

# Default utils locations if not set or in PATH
losetup=${losetup:=/sbin/losetup}
mount=${mount:=/bin/mount}
umount=${umount:=/bin/umount}
fsck=${fsck:=/sbin/fsck}
modprobe=${modprobe:=/sbin/modprobe}
fdisk=${fdisk:=/sbin/fdisk}

# Config sanity checks
case $MODE in
	1|2|3);; 
	"") echo "${MSG}Config error: MODE is unset"; exit 1;;
	*) echo "${MSG}Config error: MODE=$MODE is invalid."; exit 1;;
esac
case $EMBED in
	yes|no);;
	"") echo "${MSG}Config error: EMBED is unset"; exit 1;;
	*) echo "${MSG}Config error: EMDED=$EMBED is invalid."; exit 1;;
esac
case $FORCE in
	BRUTE|GENTLE|OFF);; 
	"") echo "${MSG}Config error: FORCE is unset"; exit 1;;
	*) echo "${MSG}Config error: FORCE=$FORCE is invalid."; exit 1;;
esac
case $VERBOSE in
	yes|no);;
	"") echo "${MSG}Config error: VERBOSE is unset"; exit 1;;
	*) echo "${MSG}Config error: VERBOSE=$VERBOSE is invalid."; exit 1;;
esac
case $KERNELPATCH in
	yes|no);;
	"") echo "${MSG}Config error: KERNELPATCH is unset"; exit 1;;
	*) echo "${MSG}Config error: KERNELPATCH=$KERNELPATCH is invalid."; exit 1;;
esac
if [ -z "${DEVICE}" ]; then echo "${MSG}Config error: DEVICE is unset"; exit 1; fi
if [ -z "${MOUNTPT}" ]; then echo "${MSG}Config error: MOUNTPT is unset"; exit 1; fi
if [ ! -d "${MOUNTPT}" ]; then echo "${MSG}${MOUNTPT} does not exist."; exit 1 ;fi
if [ ! -b "${DEVICE}" ]; then echo "${MSG}$DEVICE is not a valid block device"; exit 1 ; fi

D=${DEVICE##/dev/}

if grep -wq $D /proc/partitions; then
	if ! dd if=$DEVICE count=1 bs=1024 >/dev/null 2>&1; then
		echo "${MSG}Error: can't open $DEVICE for reading - is it connected?"
		exit 1
	fi
else
	echo "${MSG}Error: can't find device $DEVICE - is it connected?"
	exit 1
fi

case $FS in
	ext2|ext3);;
	*) echo "${MSG}Config error: Invalid filesystem FS=\"$FS\""; exit 1;;
esac

if ! check_losetup; then 
	echo "${MSG}Error: your $losetup is incompatible with loop-aes v3.x"
	echo "${MSG}See the loop-aes README for details."
	exit 1
fi


set_modules
}


loopfree_new(){
# Important that loopfree_new checks all fields in losetup -a
# for busy loops
while read field; do
	for g in $field; do
		BUSYLOOP=""
		case $g in
			*/dev/loop?*) h=${g##*/dev/loop};
			BUSYLOOP=${h%%[^0-9]*}
			BUSYARRAY[$BUSYLOOP]=1;;
		esac
	done

done <<EOT
$(losetup -a)
EOT

m=0
until [ x${BUSYARRAY[m]} = x ]; do
	(( m++))
done
if [ $m -ge $MAXLOOPS ]; then
	echo "${MSG}[loopfree_new] NO FREE LOOPS"
	return 1
else
	# loop $m is free
	NEXTLP=$m
fi
unset BUSYARRAY
return 0
}



dechain_loops(){
# Outputs encrypted loop devices chained to device $1 
# Sanity checks chain
D=$1
B=$D
while read f1 f2 f3 f4; do
	case $f3 in
		\($B\)) case $f4 in 
			*encryption*) case $f1 in
					/dev/loop?:) B=${f1%:}; echo $B;;
					*) echo "${MSG}Error: $f1 not a loop device on $f3" >/dev/tty; exit 1;;
					esac;;
			*) echo "${MSG}Error: $f1 not an encrypted loop on $f3" >/dev/tty; exit 1;;
			esac;;
	esac
done <<EOT
$(losetup -a)
EOT

if [ x$B = x$D ]; then
	[ "$VERBOSE" = yes ] && echo "${MSG}No loop on $D" >/dev/tty
	exit 1;
fi
}



pull_down(){
X=$1
if grep -wq "${MOUNTPT}" /proc/mounts; then
	case $FORCE in
	
	BRUTE)  # just kill 'em.  Could damage filesystem and/or lose some data
		fuser -m -k ${MOUNTPT};; 
	
	GENTLE) # Be gentle with me
		s[0]=-SIGHUP; s[1]=-SIGTERM; s[2]=-SIGKILL
		sync;
		q=0
		for SIG in ${s[*]}; do
			fuser -m -k $SIG ${MOUNTPT}
			sleep 2
			if fuser -ms ${MOUNTPT}; then
				echo -n "${MSG}$SIG failed, "
				if [ $q -eq 2 ]; then
					echo "${MSG}Error: processes still running on ${MOUNTPT}"
					exit
				fi
				echo "trying ${s[q+1]} .."
			else
				break
			fi
			(( q = q + 1 ))
		done ;;
	OFF);;
	
	esac
	
	$umount ${MOUNTPT}
	grep -wq "${MOUNTPT}" /proc/mounts && echo "${MSG}Error: umount failed!"
else
	[ "$VERBOSE" = yes ] && echo "${MSG}${MOUNTPT} is not in use"
fi

# Pull down our encrypted loops chained on top of device 

 dechain_loops ${DEVICE} | sort -r | while read D; do
			[ "$VERBOSE" = yes ] && echo "${MSG}Pulling down ${D} .."
			$losetup -d $D 
			X=$?
			if [ $X -ne 0 ]; then
				echo "${MSG}Error: free up ${D} and try again."
				exit $X
			fi
			done && exit $X
}



ask_user(){
while true; do
	echo -n "$1? (y/N) "
	read
	case $REPLY in
		y*|Y*) if [ "$1" = Abort ]; then exit 1; fi ;;
		n*|N*) if [ "$1" = Abort ]; then break; fi;;
		*) echo "${MSG}Invalid response" ;;
	esac
done
}



check_gpg_iterations(){
echo
echo x | gpg --no-tty --passphrase-fd 3 3< <(echo whatever) --symmetric >${gpgtest}
TESTSTR=$(gpg --no-tty --passphrase-fd 3 3< <(echo whatever)  --decrypt -v -v <${gpgtest} 2>&1 | grep -E "salt.+count")
ITERFLAG=${TESTSTR##*[ ]}
rm -f ${gpgtest}
case $ITERFLAG in
	# newer gnupg versions put () around iteration flag
	208|\(208\)) echo "${MSG}Good, GnuPG using increased iterations flag = $ITERFLAG" ;;
	96|\(96\)) echo "${MSG}GnuPG using only standard iterations flag = $ITERFLAG"
	echo "${MSG}See loop-aes README for details."
	ask_user Abort ;;
	*) echo "${MSG}Error: WTF?? unknown gpg iteration counts flag $ITERFLAG"; exit 1;;
esac
}


makekey(){
check_gpg_iterations &>/dev/tty
head -c 2925 /dev/random | uuencode -m - | head -n 66 | tail -n 65 \
| gpg --symmetric -a
return
}


checkloops(){
NUMBUSY=$($losetup -a|wc -l)
(( NUMFREELOOPS = MAXLOOPS - NUMBUSY ))
if [ $NUMFREELOOPS -lt $MODE ]; then
	echo "${MSG}Error: insufficient loops free ($NUMFREELOOPS)"
	echo "${MSG}Require $MODE free loop device(s)."
	exit 1
fi
}


overwrite_partition(){
checkloops
loopfree_new || pull_down 1
LP=/dev/loop$NEXTLP
head -c 15 /dev/urandom | uuencode -m - | head -n 2 | tail -n 1 | $losetup -p 0 -e AES128 $LP ${DEVICE}
dd if=/dev/zero of=$LP bs=4k conv=notrunc 2>/dev/null
$losetup -d $LP
}


setup_loops(){
[ "${1}" != new ] && check_device_free # for new setup, this has already been checked
checkloops
echo
if [ $EMBED = yes ]; then
	if [ ! -e "${key[1]}" ]; then
		echo "${MSG}Error: Can't find key ${key[1]} - need one external key for embedded mode."
		exit 1
	fi
	if [ "${1}" = new ]; then
		[ "$VERBOSE" = yes ] && echo ">>>>>> ${MSG}Using key ${key[1]} for loop1 <<<<<<"
	else
		[ "$VERBOSE" = yes ] && echo "${MSG}Enter $MODE passphrase(s):"
	fi
else
	[ "$VERBOSE" = yes ] && echo "${MSG}Enter $MODE passphrase(s):"
fi


for k in $(seq 1 $MODE); do

      if ! [[ $KERNELPATCH = yes &&  ${MODULE[k]} == loop ]]; then
            $modprobe ${MODULE[k]} || pull_down 1
      fi
	loopfree_new || pull_down 1
	TOPLOOP="/dev/loop$NEXTLP"

	if [ $k -gt 1 ]; then
		# Sanity check:
		PL=$(dechain_loops ${DEVICE}| tail -n 1)
		if [ ${PREVIOUSLOOP} != ${PL} ]; then
			echo "${MSG}[$k] Error! dechain_loops says previous loop is ${PL}"
			echo "${MSG}But here previousloop = ${PREVIOUSLOOP}"
			exit 1
		fi
	fi
	
	if [ $EMBED = yes ]; then
		if [ $k -eq 1 ]; then
			# No offset key on first loop
			[ "$VERBOSE" = yes ] && echo "${MSG}[$k] Setting up ${TOPLOOP} on ${DEVICE} .."
			p=0
			while ! $losetup -e ${CIPHER[1]} -K ${key[1]} -G ${GPGHOME} ${TOPLOOP} ${DEVICE}; do
				(( p++ ))
				if [ $p -eq 3 ]; then echo "${MSG}3rd failed attempt, exiting .."; pull_down 1; fi
				echo "${MSG}Try again ($p) .."
			done
		else
			if [ "${1}" = new ]; then
				echo
				echo "${MSG}>>>>>> [$k] Making embedded key on ${PREVIOUSLOOP}  <<<<<<"
				echo "${MSG}>>>>>> Enter new passphrase at THREE (3) prompts .. <<<<<<"
				yes "" | dd of=${PREVIOUSLOOP} bs=512 count=16
				makekey | dd of=${PREVIOUSLOOP} conv=notrunc
			fi
			[ "$VERBOSE" = yes ] && echo "${MSG}[$k] Setting up ${TOPLOOP} on ${PREVIOUSLOOP} .."
			p=0
			while ! $losetup -e ${CIPHER[k]} -K ${PREVIOUSLOOP} -o 8192 -G ${GPGHOME} ${TOPLOOP} ${PREVIOUSLOOP}; do
				(( p++ ))
				if [ $p -eq 3 ]; then echo "${MSG}3rd failed attempt, exiting .."; pull_down 1; fi
				echo "${MSG}Try again ($p) .."
			done

		fi
	else
		if [ ! -e "${key[k]}" ]; then
			echo "${MSG}[$k] Error: Can't find key ${key[k]}"
			pull_down 1
		fi
		[ $k -eq 1 ] && PREVIOUSLOOP=${DEVICE}
		[ "$VERBOSE" = yes ] && echo "${MSG}[$k] Setting up ${TOPLOOP} on ${PREVIOUSLOOP} .."
		p=0
		while ! $losetup -e ${CIPHER[k]} -K ${key[k]} -G ${GPGHOME} ${TOPLOOP} ${PREVIOUSLOOP}; do
			(( p++ ))
			if [ $p -eq 3 ]; then echo "${MSG}3rd failed attempt, exiting .."; pull_down 1; fi
			echo "${MSG}Try again ($p) .."
		done
	fi
	PREVIOUSLOOP=${TOPLOOP}
done

}

#=====================// MAIN //==================================


gpgtest=/tmp/gpgtest.$RANDOM.$$

MAXLOOPS=$(ls /dev/loop* | wc -l)

run_checks $*

# Parse cli options

ARGS="$@"


# Catch malformed switches:

# number

if [ $# -gt 3 ]; then
	echo "${MSG}Error: too many args."
	exit 1
fi

# length

for P in $ARGS; do
	case $P in 
	-*) if [ ${#P} -ne 2 ]; then
			echo "${MSG}Error: unknown option \"$P\". Try tripl -h"; exit 1
		fi;;
	esac
done

# check file option form

case $ARGS in
	-f*)
	if [ $# -ne 3 ]; then
		echo "${MSG}Error: too few args."
		exit 1
	fi
	
	B=${ARGS##-f}
	C=${B%%[ ]-*}  
	E=${C#[ ]}     # should be OPTARG
	;;
	     
	*-f*) echo "${MSG}Error: -f option must be first"; exit 1;;
	
	*) # if no -f option, can only have one argument.
	if [ $# -gt 1 ]; then
		echo "${MSG}Error: unknown option or too many args."
		exit 1
	fi
	case $ARGS in -*);; *) echo "${MSG}Error: invalid arg \"$ARGS\""; exit 1;; esac
	;;
esac


CLI_CONFIG=""
	
	
while getopts ":f:nmukrh" Option; do

case $Option in

f)

if [[ ${E} != $OPTARG ]]; then
	echo "${MSG}Error: 1 (only) config file must be passed with -f"
	exit 1
fi

. ${OPTARG} || exit 1

[ "$VERBOSE" = yes ] && echo "${MSG}Using config file ${OPTARG} .." 
CLI_CONFIG=yes;;

n) 
conf_checks
if [ $DISABLE_NEW = yes ]; then
	echo "${MSG}New partition switch disabled in user settings"
	exit 0
fi
check_device_free
echo
echo "${MSG}WARNING: this will destroy all data on $DEVICE !"
echo "${MSG}~~~~~~~"

while true; do
	echo -n "${MSG}Last chance to exit. Are you sure you want to proceed? (YeS/n) "
	read
	case $REPLY in
		YeS) break ;;
		n*|N*) exit 0;;
		y*|Y*) echo "${MSG}You must type YeS to proceed.";;
		*) echo "${MSG}Invalid response.";;
	esac
done


echo "${MSG}Preparing device ${DEVICE} .. pls wait (could be a *long* time) .."
overwrite_partition

setup_loops new
echo
echo ">>>>>> ${MSG}Making $FS filesystem on ${TOPLOOP} <<<<<<"
mkfs -t $FS ${TOPLOOP}
pull_down 0 ;;


m)
conf_checks
if grep -wq "${MOUNTPT}" /proc/mounts; then echo "${MSG}${MOUNTPT} in use."; exit 1; fi
setup_loops
$mount -t $FS ${TOPLOOP} ${MOUNTPT}
if grep -wq "${TOPLOOP}" /proc/mounts; then
	[ "$VERBOSE" = yes ] && echo "${MSG}OK"
else
	pull_down 1
fi ;;

u) 
conf_checks
pull_down 0;;

k)
conf_checks
while true; do
	echo -n "${MSG}Enter path and filename of new key (CNTRL-C = quit): "
	read
	DIR=$(dirname ${REPLY})
	if [ -e "${REPLY}" ]; then
		echo "${MSG}Error: file ${REPLY} already exists."; continue
	fi
	if [ ! -d "${DIR}" ]; then
		echo "${MSG}Error: invalid path ${DIR}" ; continue
	fi
	if [ ! -w "${DIR}" ]; then
		echo "${MSG}Error: ${DIR} not writeable" ; continue
	fi
	break
done
echo "${MSG}Making key ${REPLY} .."
makekey >${REPLY}
if [ $? -eq 0 ]; then
	echo "${MSG}Done."
	echo "${MSG}Set your new key location in tripl's config file (default ~/.triplrc)."
else
	rm -f ${REPLY} # gpg will provide error messages if it fails.
	exit 1
fi ;;


r)
conf_checks
if grep -wq "${MOUNTPT}" /proc/mounts; then echo "${MSG}${MOUNTPT} in use."; exit 1; fi
echo "${MSG}Repairing encrypted filesystem on ${DEVICE} .."
setup_loops
$fsck -t $FS -C -f -y ${TOPLOOP}
echo "${MSG}Done."
pull_down 0;;


h) help; exit 0 ;;

*) 
A=( echo "$@" )
BADOPT=${A[OPTIND-1]}
echo "${MSG}Error: unknown or repeated option \"$BADOPT\""; exit 1 ;;

esac
done

shift $(($OPTIND - 1))


