#!/bin/zsh # # Tomb, the Crypto Undertaker # # a tool to easily operate file encryption of private and secret data # # Copyleft (C) 2007-2011 Denis Roio # # This source code is free software; you can redistribute it and/or # modify it under the terms of the GNU Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This source code is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # Please refer to the GNU Public License for more details. # # You should have received a copy of the GNU Public License along with # this source code; if not, write to: # Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. VERSION=0.9.2 DATE=Feb/2011 # PATH=/usr/bin:/usr/sbin:/bin:/sbin # standard output message routines # it's always useful to wrap them, in case we change behaviour later notice() { if ! [ $QUIET ]; then echo "[*] $1"; fi } act() { if ! [ $QUIET ]; then echo " . $1"; fi } error() { if ! [ $QUIET ]; then echo "[!] $1"; fi } func() { if [ $DEBUG ]; then echo "[D] $1"; fi } # which dd command to use which dcfldd > /dev/null if [ $? = 0 ]; then DD="dcfldd" else DD=dd fi # which wipe command to use which wipe > /dev/null if [ $? = 0 ]; then WIPE=(wipe -f -s -q) else WIPE=(rm -f) fi # usb auto detect using dmesg # tested on ubuntu 10.04 - please test and patch on other systems if you can # TODO: use udev rules, see how archlinux folks document it - arch rox 8) # https://wiki.archlinux.org/index.php/System_Encryption_with_LUKS_for_dm-crypt # here we could modularize the choice of methods using function pointers, # so that they are configurable when calling tomb. ask_usbkey() { notice "Waiting 1 minute for a usb key to connect" echo -n " . please insert your usb key " exec_as_user notify-send -i monmort \ -u normal -h string:App:Tomb \ -h double:Version:${VERSION} \ -t 60 \ "Insert your USB KEY" \ "Tomb is waiting 1 minute for you to insert an external key." plugged=false c=0 while [ "$plugged" != "true" ]; do dmesg | tail -n 12 | grep -q 'new.*USB device' if [ $? = 0 ]; then plugged=true; fi echo -n "." sleep .5 c=`expr $c + 1` if [ $c -gt 15 ]; then echo error "timeout." export usbkey_mount=none return 1; fi done echo echo -n " . usb key inserted, attaching " c=0 attached=false while [ "$attached" != "true" ]; do dmesg | tail -n 12| grep -q 'Attached.*removable disk' if [ $? = 0 ]; then attached=true; fi echo -n "." sleep 1 c=`expr $c + 1` if [ $c -gt 15 ]; then echo error "timeout." export usbkey_mount=none return 1; fi done echo echo -n " . usb attached, opening " # get the first partition # usbpart=`dmesg |tail -n 12 | grep ' sd.:' |cut -d: -f2 |tr -d ' '` for i in $(seq 1 10); do usbpart=$(dmesg | tail -n 12 | sed '/ sd.:/!d;s/^.*: \(sd.[0-9]*\)/\1/') if [ -n "$usbpart" ]; then break elif [ $i -eq 10 ]; then error "timeout." return 1 else echo -n . sleep 1 fi done # # wait that is mounted (it automount is on) # c=0 # mounted=false # while [ "$mounted" != "true" ]; do # cat /proc/mounts | tail -n 2 | grep -q $usbpart # if [ $? = 0 ]; then mounted=true; fi # echo -n "." # sleep .5 # c=`expr $c + 1` # if [ $c -gt 30 ]; then # echo # error "timeout." # export usbkey_mount=none # return 1; # fi # done # # check where it is mounted # usbmount=`cat /proc/mounts | awk -v p=$usbpart '{ if( $1 == "/dev/" p) print $2 }'` sleep 1 # mount the first partition on the usb key mtmp=`tempfile -p tomb` rm -f $mtmp mkdir -p $mtmp mount /dev/$usbpart $mtmp if [ $? = 0 ]; then usbmount=$mtmp else error "cannot mount usbkey partition $usbmount" return 1 fi echo act "usb key mounted on $usbmount" export usbkey_mount=$usbmount return 0 } # user interface (just to ask the password) ask_password() { exec_as_user xhost 2>&1 >/dev/null if [ $? = 0 ]; then # we have access to the X display exec_as_user which tomb-askpass > /dev/null if [ $? = 0 ]; then export scolopendro="`exec_as_user tomb-askpass ${1} 2>/dev/null`" return fi exec_as_user which ssh-askpass # 2>&1 > /dev/null if [ $? = 0 ]; then export scolopendro="`exec_as_user ssh-askpass "Tomb: provide the password to unlock"`" return fi else # we'll collect the password from commandline act "Tomb: provide the password to unlock" echo -n " > " read -s scolopendro export scolopendro fi # just in case we'd like to have dialog supported too: # dialog --backtitle "This file is encrypted for privacy protection" \ # --title "Security check" --insecure \ # --passwordbox "Enter password:" 10 30 2> /var/run/.scolopendro } # popup notification tomb-notify() { # look for our icon in common prefixes if [ -r /usr/share/pixmaps/monmort.xpm ]; then icon=/usr/share/pixmaps/monmort.xpm elif [ -r /usr/share/icons/monmort.xpm ]; then icon=/usr/share/icons/monmort.xpm elif [ -r /usr/local/share/pixmaps/monmort.xpm ]; then icon=/usr/local/share/pixmaps/monmort.xpm elif [ -r /usr/local/share/icons/monmort.xpm ]; then icon=/usr/local/share/icons/monmort.xpm elif [ -r /opt/share/pixmaps/monmort.xpm ]; then icon=/opt/share/pixmaps/monmort.xpm elif [ -r /sw/share/pixmaps/monmort.xpm ]; then icon=/sw/share/pixmaps/monmort.xpm fi if [ -z $1 ]; then exec_as_user notify-send -i $icon \ -u low -h string:App:Tomb \ -h double:Version:${VERSION} \ "Tomb version $VERSION" \ "Hi, I'm the Undertaker. Let's start setting your Crypt?" else exec_as_user notify-send -i $icon ${@} fi } # drop privileges exec_as_user() { if ! [ $SUDO_USER ]; then exec $@[@] return $? fi func "exec_as_user '$SUDO_USER': ${(f)@}" which sudo > /dev/null if [ $? = 0 ]; then sudo -u $SUDO_USER "${@[@]}" return $? else error "Tomb requires sudo. please install it." return 1 fi } # escalate privileges check_priv() { id | grep root > /dev/null if [ $? != 0 ]; then which gksu > /dev/null if [ $? = 0 ]; then func "Using gksu for root execution of 'tomb ${(f)ARGS}'" gksudo "tomb ${ARGS[@]}" exit $? fi which sudo > /dev/null if [ $? = 0 ]; then func "Using sudo for root execution of 'tomb ${(f)ARGS}'" sudo "tomb ${ARGS[@]}" exit $? fi return 1 fi return 0 } ############################ ### main() ### echo $@ | grep '\-q' 2>&1 > /dev/null if [ $? != 0 ]; then notice "Tomb - simple commandline tool for encrypted storage" act "version $VERSION ($DATE) by Jaromil @ dyne.org" fi echo $@ | grep '\-D' 2>&1 > /dev/null if [ $? = 0 ]; then echo "[D] invoked with args \"${(f)@}\" " echo "[D] running on `date`" fi ARGS=$@[@] OPTS=`getopt -o hvqDs:k: -n 'tomb' -- "$@"` while true; do case "$1" in -h) act "" notice "Syntax: tomb [options] command [file] [mountpoint]" act "" notice "Commands:" act "create create a new encrypted storage FILE and keys" act "open open an existing tomb FILE on MOUNTPOINT" act "close closes the tomb on MOUNTPOINT" act "" notice "Options:" act "-s size of the storage file when creating one (MB)" act "-k path to the key to use for decryption" act "" act "-h print this help" act "-v version information for this tool" act "-q run quietly without printing informations" act "-D print debugging information at runtime" echo; exit 2 ;; -v) # print out the GPL license in this file act "" cat $0 | awk ' BEGIN { license=0 } /^# This source/ { license=1 } { if(license==1) print " " $0 } /MA 02139, USA.$/ { license=0 } ' act "" exit 0 ;; -q) QUIET=1; shift 1 ;; -D) DEBUG=1; shift 1 ;; -s) SIZE=$2; shift 2 ;; -k) KEY=$2; shift 2 ;; --) shift; break ;; *) CMD=$1; FILE=$2; MOUNT=$3; # compat with old args CMD2=${2}; CMD3=${3}; break ;; esac done if ! [ $CMD ]; then error "first argument missing, use -h for help" tomb-notify exit 0 fi func "Tomb called: $CMD $CMD2 $CMD3" create_tomb() { # make sure the file has a .tomb extension FILE="${FILE%\.*}.tomb" if [ -e "$FILE" ]; then error "$FILE exists already. I'm not digging here." return 1 fi notice "Creating a new tomb" if [ -z $SIZE ]; then if [ $MOUNT ]; then SIZE=$MOUNT else act "No size specified, summoning the Tomb Undertaker to guide us in the creation." tomb-open &! return 0 fi fi SIZE_4k=`expr $SIZE \* 1000 / 4` act "Generating ${FILE} of ${SIZE}Mb (${SIZE_4k} blocks of 4Kb)" $DD if=/dev/urandom bs=4k count=${SIZE_4k} of=${FILE} if [ $? = 0 -a -e ${FILE} ]; then act "OK: `ls -lh ${FILE}`" else error "Error creating the tomb ${FILE}, operation aborted." exit 1 fi modprobe dm-crypt modprobe aes-i586 nstloop=`losetup -f` # get the number for next loopback device losetup -f ${FILE} # allocates the next loopback for our file # create the keyfile in tmpfs so that we leave less traces in RAM keytmp=`tempfile -p tomb` rm -f $keytmp mkdir -p $keytmp mount tmpfs ${keytmp} -t tmpfs -o size=1m if [ $? != 0 ]; then error "cannot mount tmpfs filesystem in volatile memory" error "operation aborted." losetup -d $nstloop rm -r $keytmp exit 1 fi act "Generating secret key..." act "this operation takes time, keep using this computer on other tasks," act "once done you will be asked to choose a password for your tomb." touch ${keytmp}/tomb.tmp chmod 0600 ${keytmp}/tomb.tmp $DD bs=1 count=256 if=/dev/random of=${keytmp}/tomb.tmp if ! [ -r ${keytmp}/tomb.tmp ]; then error "cannot generate encryption key, operation aborted." umount ${keytmp} losetup -d $nstloop rm -r $keytmp exit 1 fi notice "Setup your secret key file ${FILE}.gpg" tomb-notify "The Tomb key is being forged:" "please set your password." # here user is prompted for key password for c in 1 2 3; do # 3 tries to write two times a matching password ask_password ${FILE} scolotemp=$scolopendro ask_password "${FILE} (again)" if [ "$scolotemp" = "$scolopendro" ]; then break; fi unset $scolotemp unset $scolopendro done if [ -z $scolopendro ]; then error "passwords don't match, aborting operation" umount ${keytmp} losetup -d $nstloop rm -r $keytmp exit 1 fi echo "${scolopendro}" | gpg --batch --no-options --no-tty --passphrase-fd 0 \ -o "${FILE}.gpg" -c -a ${keytmp}/tomb.tmp if [ $? = 2 ]; then error "setting password failed: gnupg returns 2" umount ${keytmp} losetup -d $nstloop rm -r $keytmp exit 1 fi act "formatting Luks mapped device" # we use aes-cbc-essiv with sha256 # for security, performance and compatibility cryptsetup --batch-mode \ --cipher aes-cbc-essiv:sha256 --key-size 256 \ luksFormat ${nstloop} ${keytmp}/tomb.tmp if ! [ $? = 0 ]; then act "operation aborted." exit 0 fi cryptsetup --key-file ${keytmp}/tomb.tmp --cipher aes luksOpen ${nstloop} tomb.tmp ${WIPE[@]} ${keytmp}/tomb.tmp umount ${keytmp} rm -r ${keytmp} # cryptsetup luksDump ${nstloop} act "formatting your Tomb with Ext4 filesystem" mkfs.ext4 -q -F -j -L "${FILE%%.*}" /dev/mapper/tomb.tmp if [ $? = 0 ]; then act "OK, encrypted storage succesfully formatted" else act "error formatting Tomb" fi sync cryptsetup luksClose tomb.tmp losetup -d ${nstloop} notice "done creating $FILE encrypted storage (using Luks dm-crypt AES/SHA256)" tomb-notify "The Tomb is ready!" "We will now open your new Tomb for the first time." notice "Your tomb is ready on ${FILE} and secured with key ${FILE}.gpg" act "Would you like to save the key on an external usb device?" act "This is recommended for safety:" act "Always keep the key in a different place than the door!" act "If you answer yes, you'll need a USB KEY now: (y/n)" tomb-notify "Tomb has forged a key." "Would you like to save it on USB?" echo -n " > " read -q if [ $? = 0 ]; then ask_usbkey if ! [ -e ${usbkey_mount} ]; then error "cannot save the key in a separate place, move it yourself later." else mkdir -m 0700 -p ${usbkey_mount}/.tomb cp -v ${FILE}.gpg ${usbkey_mount}/.tomb/ chmod -R go-rwx ${usbkey_mount}/.tomb umount ${usbkey_mount} unset ${usbkey_mount} notice "Key ${FILE}.gpg succesfully saved on your USB" act "now we proceed opening your new tomb" KEY=${FILE}.gpg CMD2=${FILE} CMD3=/media/${FILE} mount_tomb ${FILE} ${WIPE[@]} ${FILE}.gpg fi else # kept besides (deprecated behaviour) act "now we proceed opening your new tomb" KEY=${FILE}.gpg mount_tomb ${FILE} fi } mount_tomb() { if ! [ $CMD2 ]; then error "need an argument, operation aborted." return 1 elif [ -r $CMD2 ]; then tombfile=$CMD2 else # try also adding a .tomb extension tombfile="${CMD2%\.*}.tomb" if ! [ -r $tombfile ]; then error "cannot find a tomb named $CMD" return 1 fi fi file $tombfile | grep -i 'luks encrypted.*cbc-essiv' 2>&1 >/dev/null if [ $? != 0 ]; then error "$CMD2 is not a valid tomb file, operation aborted" tomb-notify "Not a tomb." "$CMD2 doesn't seems a real tomb." return 1 fi tombdir=`dirname $tombfile` tombname=`echo $tombfile | cut -d. -f1` if [ $KEY ]; then # key manually chosen from commandline with -k tombkey="`basename $KEY`" tombkeypath="$KEY" else tombkey="`basename ${tombfile}.gpg`" if [ -r $tombkey ]; then tombkeypath=$tombkey elif [ -r "$tombdir/$tombkey" ]; then tombkeypath="$tombdir/$tombkey" else notice "please insert your USB KEY" error "encryption key ${enc_key} not found on disk" error "use -k option to specify which key to use" error "or provide a usb key, or press ctrl-c to abort" ask_usbkey # returns usbkey_mount, now check if the key is there if [ -r ${usbkey_mount}/.tomb/${tombkey} ]; then tombkeypath=${usbkey_mount}/.tomb/${tombkey} notice "key found on ${tombkeypath}" else error "key is missing, try to locate $tombkey in your files." error "operation aborted" return 1 fi fi fi if ! [ $CMD3 ]; then tombmount=/media/`basename ${tombfile}` act "mountpoint not specified, using default: $tombmount" elif ! [ -x $CMD3 ]; then error "mountpoint $CMD2 doesn't exist, operation aborted." return 1 else tombmount=$CMD3 fi notice "mounting $tombfile on mountpoint $tombmount" # we need root from here on mkdir -p $tombmount nstloop=`losetup -f` losetup -f ${tombfile} act "check for a valid LUKS encrypted device" cryptsetup isLuks ${nstloop} if [ $? != 0 ]; then # is it a LUKS encrypted nest? see cryptsetup(1) error "$tombfile is not a valid Luks encrypted storage file" return 1 fi modprobe dm-crypt modprobe aes-i586 # save date of mount in minutes since 1970 mapdate="`date +%s`" mapdate="`echo ${mapdate}/60 | bc -l | cut -d. -f1`" mapper="tomb.`basename $tombfile | cut -d. -f1`.$mapdate.`basename $nstloop`" notice "Password is required for key ${tombkey}" keyname=`basename $tombkey | cut -d. -f1` for c in 1 2 3; do if [ $c = 1 ]; then ask_password ${keyname} else ask_password "$keyname (retry $c)" fi echo "${scolopendro}" \ | gpg --batch --passphrase-fd 0 --no-tty --no-options \ -d "${tombkeypath}" 2>/dev/null \ | cryptsetup --key-file - luksOpen ${nstloop} ${mapper} unset scolopendro if [ -r /dev/mapper/${mapper} ]; then break; # password was correct fi done if [ -r ${usbkey_mount}/.tomb/${tombkey} ]; then umount ${usbkey_mount} unset ${usbkey_mount} fi if ! [ -r /dev/mapper/${mapper} ]; then error "failure mounting the encrypted file" losetup -d ${nstloop} return 1 fi act "encrypted storage filesystem check" fsck -p -C0 /dev/mapper/${mapper} mount -o rw,noatime,nodev /dev/mapper/${mapper} ${tombmount} # Ensure the user can write the disk - 10x Hellekin :) ME=${SUDO_USER:-$(whoami)} chmod 0750 ${tombmount} chown $(id -u $ME):$(id -g $ME) ${tombmount} notice "encrypted storage $tombfile succesfully mounted on $tombmount" exec_bind_hooks ${tombmount} exec_post_hooks ${tombmount} return 0 } exec_bind_hooks() { mnt=$1 # first argument is where the tomb is mounted if ! [ -r ${mnt}/bind-hooks ]; then return; fi # if 'bind-hooks' is found inside the tomb, parse it # every line contains two strings: # the first is a directory existing inside the tomb # the second is the place where it should be mounted (-o bind) hook=`cat ${mnt}/bind-hooks | awk ' /^#/ { next } { if($1 && $2) print "mount -o bind \${mnt}/" $1 " " $2 "; " } '` # restore $HOME for the calling user HOME=/home/${SUDO_USER} act "bind hooks found, mounting direcories as requested" # execute the mount commands eval $hook } exec_post_hooks() { mnt=$1 # first argument is where the tomb is mounted if ! [ -x ${mnt}/post-hooks ]; then return; fi # if 'post-hooks' is found inside the tomb, check it: if it is an # executable, launch it as a user this might need a dialog for # security on what is being run, however we expect you know well # what is inside your tomb. this feature opens the possibility to # make encrypted executables. cat ${mnt}/post-hooks | head -n1 | grep '^#!/' if [ $? = 0 ]; then act "post hooks found, executing as user $SUDO_USER" exec_as_user ${mnt}/post-hooks fi } umount_tomb() { if ! [ $1 ]; then how_many_tombs="`find /dev/mapper -name 'tomb.*' | wc -w`" if [ "$how_many_tombs" = "0" ]; then error "there is no open tomb to be closed" return 1 elif [ "$how_many_tombs" = "1" ]; then mapper=`find /dev/mapper -name 'tomb.*'` else error "too many tombs mounted, please specify which to unmount:" ls /dev/mapper/tomb.* error "or issue the command 'tomb close all' to clos'em all." return 1 fi fi if [ "$1" = "all" ]; then tombs=`find /dev/mapper -name 'tomb.*'` if ! [ $tombs ]; then error "Tombs are all closed, cemetery is quiet." return 1 fi for t in ${(f)tombs}; do umount_tomb ${t} done return 0 fi if [ -r "$1" ]; then # accepts relative and absolute path mapper="$1" elif [ -r /dev/mapper/${1} ]; then mapper=/dev/mapper/${1} else error "tomb not found: $1" error "please specify an existing /dev/mapper/tomb.*" tomb-notify "Tomb was already closed." "Undertaker will rest in peace." return 0 fi basemap=`basename $mapper` tombname=`echo ${basemap} | cut -d. -f2` tombmount=`mount | grep $mapper | awk '{print $3}'` # check if there are binded dirs and close them first mount | grep "${tombmount}" | grep -v loop 2>&1 > /dev/null if [ $? = 0 ]; then act "closing bind hooks for tomb $tombname " unbind=`mount | grep ${tombmount} | grep -v loop | awk ' { print "umount " $3 "; " } '` eval $unbind func "umount binded dirs:" func "$unbind" fi act "closing tomb $tombname on dm-crypt $basemap" mount | grep $mapper 2>&1 >/dev/null if [ $? = 0 ]; then # still mounted umount ${mapper} if ! [ $? = 0 ]; then tomb-notify "Tomb '$tombname' is too busy." \ "Close all applications and file managers, then try again." return 1 fi fi cryptsetup luksClose $basemap if ! [ $? = 0 ]; then error "error occurred in cryptsetup luksClose ${basemap}" return 1 fi losetup -d "/dev/`echo $basemap | cut -d. -f4`" notice "crypt storage ${mapper} unmounted" tomb-notify "Tomb closed: $tombname" "Your bones will Rest In Peace." return 0 } # install mime-types, bells and whistles for the desktop # see http://developers.sun.com/solaris/articles/integrating_gnome.html # and freedesktop specs install_tomb() { # TODO: distro package deps (for binary) # debian: zsh, cryptsetup, libgtk2.0-0, libnotify-bin act "updating mimetypes..." cat < /tmp/dyne-tomb.xml Tomb encrypted volume Tomb crypto key EOF xdg-mime install /tmp/dyne-tomb.xml xdg-icon-resource install --context mimetypes --size 32 monmort.xpm monmort xdg-icon-resource install --size 32 monmort.xpm dyne-monmort rm /tmp/dyne-tomb.xml act "updating desktop..." cat < /usr/share/applications/tomb.desktop [Desktop Entry] Version=1.0 Type=Application Name=Tomb crypto undertaker GenericName=Crypto undertaker Comment=Keep your bones safe Exec=tomb-open %U TryExec=tomb-open Icon=monmort.xpm Terminal=false Categories=Utility;Security;Archiving;Filesystem; MimeType=application/x-tomb-volume; X-AppInstall-Package=tomb EOF update-desktop-database act "updating menus..." cat < /etc/menu/tomb ?package(tomb):command="tomb" icon="/usr/share/pixmaps/monmort.xpm" needs="cryptsetup" \ section="Applications/Accessories" title="Tomb" hints="Crypto" \ hotkey="Tomb" EOF update-menus act "updating mime info..." cat < /usr/share/mime-info/tomb.keys # actions for encrypted tomb storage application/x-tomb-volume: open=tomb-open %f view=tomb-open %f icon-filename=monmort.xpm short_list_application_ids_for_novice_user_level=tomb EOF cat < /usr/share/mime-info/tomb.mime # mime type for encrypted tomb storage application/x-tomb-volume ext: tomb application/x-tomb-key ext: tomb.gpg EOF cat < /usr/lib/mime/packages/tomb application/x-tomb-volume; tomb-open '%s'; priority=8 EOF update-mime act "updating application entry..." cat < /usr/share/application-registry/tomb.applications tomb command=tomb-open name=Tomb - Crypto Undertaker can_open_multiple_files=false expects_uris=false requires_terminal=false mime-types=application/x-tomb-volume,application/x-tomb-key EOF act "Tomb is now installed." } kill_tomb() { # TODO: fixME - should close all tombs umount /tmp/tomb* 2>&1 > /dev/null # todo check which are tomb loops losetup -d /dev/loop* 2>&1 > /dev/null statuses=`ps ax| grep -v awk | awk "/tomb-status.$basemap/"' { print $1 }'` for ts in ${(f)statuses}; do kill $ts done } case "$CMD" in create) check_priv ; create_tomb ;; mount) check_priv ; mount_tomb ;; open) check_priv ; mount_tomb ;; umount) check_priv ; umount_tomb ${CMD2} ;; unmount) check_priv ; umount_tomb ${CMD2} ;; close) check_priv ; umount_tomb ${CMD2} ;; install) check_priv ; install_tomb ;; kill) check_priv ; kill_tomb ;; status) tomb-status ;; notify) tomb-notify $CMD2 $CMD3 ;; *) error "command \"$CMD\" not recognized" act "try -h for help" return 1 ;; esac # return codes from called functions return $?