#!/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=1.0 DATE=Feb/2011 TOMBEXEC=$0 TOMBOPENEXEC="tomb-open" STEGHIDE=1 # 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" >&2; fi } act() { if ! [ $QUIET ]; then echo " . $1" >&2; fi } error() { if ! [ $QUIET ]; then echo "[!] $1" >&2; fi } func() { if [ $DEBUG ]; then echo "[D] $1" >&2; fi } check_bin() { # 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) else WIPE=(rm -f) fi # check for filesystem creation progs which mkfs.ext4 > /dev/null if [ $? = 0 ]; then MKFS=(mkfs.ext4 -q -F -j -L) else MKFS=(mkfs.ext3 -q -F -j -L) fi # check for sudo which sudo > /dev/null if [ $? != 0 ]; then error "Cannot find sudo. Please install it" exit 1 fi # check for steghide which steghide > /dev/null if [ $? != 0 ]; then STEGHIDE=0 fi # check for tomb-open script if [ "$0" = "./tomb" ]; then TOMBOPENEXEC="./tomb-open" elif [ "$0" != "tomb" ]; then TOMBOPENEXEC="`dirname $0`/tomb-open" fi } # safe dir creation function safe_dir() { dir="/tmp/$1.$RANDOM.$RANDOM.$$" (umask 077 && mkdir "$dir") || echo "-1" echo "$dir" } # we use pinentry now # comes from gpg project and is much more secure # it also conveniently uses the right toolkit ask_password() { # pinentry has no custom icon setting # so we need to temporary modify the gtk theme if [ -r /usr/local/share/themes/tomb/gtk-2.0-key/gtkrc ]; then GTK2_RC=/usr/local/share/themes/tomb/gtk-2.0-key/gtkrc elif [ -r /usr/share/themes/tomb/gtk-2.0-key/gtkrc ]; then GTK2_RC=/usr/share/themes/tomb/gtk-2.0-key/gtkrc fi cat < /dev/null if [ $? != 0 ]; then # if not then ask a password cat <&1 >/dev/null if [ $? != 0 ]; then error "$arg is not a valid tomb file, operation aborted" return 1 fi tombname=${tombfile%%\.*} act "tomb found: ${tombdir}/${tombfile}" # now check if the key is kept beside or in args # we use the extension .key # the problem with .tomb.gpg is that decoding by hand using gpg it # can override the tomb contents if the key is in the same # directory than the tomb if [ $KEY ]; then tombkey=$KEY # commandline -k flag act "tomb key specified manually: $tombkey" elif [ -r ${tombdir}/${tombname}.tomb.key ]; then tombkey=${tombdir}/${tombname}.tomb.key act "key found for tomb '${tombname}': ${tombkey}" else error "key not found for tomb '${tombname}'" return 1 fi return 0 } usage() { cat < Syntax: tomb [options] command [file] [place] Commands: create create a new tomb FILE and its keys open open an existing tomb FILE on PLACE close closes the tomb open on PLACE bury hide a tomb key FILE inside a jpeg PLACE exhume extract a tomb key FILE from a jpeg PLACE Options: -s size of the tomb file when creating one (in MB) -k path to the key to use for opening a tomb -n don't process the hooks found in tomb -h print this help -v version information for this tool -q run quietly without printing informations -D print debugging information at runtime For more informations on Tomb read the manual: man tomb Please report bugs on . EOF } create_tomb() { if ! [ ${CMD2} ]; then error "no tomb name specified for creation" return 1 fi tombfile=`basename ${CMD2}` tombdir=`dirname ${CMD2}` # make sure the file has a .tomb extension tombname=${tombfile%%\.*} tombfile=${tombname}.tomb if [ -e ${tombdir}/${tombfile} ]; then error "tomb exists already. I'm not digging here:" ls -lh ${tombdir}/${tombfile} return 1 fi notice "Creating a new tomb in ${tombdir}/${tombfile}" if [ -z $SIZE ]; then if [ $CMD3 ]; then tombsize=${CMD3} else act "No size specified, summoning the Tomb Undertaker to guide us in the creation." "$TOMBOPENEXEC" & wait $! return 0 fi else tombsize=${SIZE} fi tombsize_4k=`expr $tombsize \* 1000 / 4` act "Generating ${tombfile} of ${tombsize}Mb (${tombsize_4k} blocks of 4Kb)" $DD if=/dev/urandom bs=4k count=${tombsize_4k} of=${tombdir}/${tombfile} if [ $? = 0 -a -e ${tombdir}/${tombfile} ]; then act "OK: `ls -lh ${tombdir}/${tombfile}`" else error "Error creating the tomb ${tombdir}/${tombfile}, operation aborted." exit 1 fi nstloop=`losetup -f` # get the number for next loopback device losetup -f ${tombdir}/${tombfile} # allocates the next loopback for our file # create the keyfile in tmpfs so that we leave less traces in RAM keytmp=`safe_dir tomb` if [ "$keytmp" = "-1" ]; then error "error creating temp dir" exit 1 fi #rm -f $keytmp # ?????? creo, cancello e ricreo ?????? #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 ${tombname}.tomb.key" # here user is prompted for key password for c in 1 2 3; do # 3 tries to write two times a matching password tombpass=`exec_as_user ${TOMBEXEC} askpass ${tombname}` tombpasstmp=$tombpass tombpass=`exec_as_user ${TOMBEXEC} askpass "${tombname} (again)"` if [ "$tombpasstmp" = "$tombpass" ]; then break; fi unset tombpasstmp unset tombpass done if [ -z $tombpass ]; then error "passwords don't match, aborting operation" umount ${keytmp} losetup -d $nstloop rm -r $keytmp exit 1 fi echo "${tombpass}" | gpg \ --openpgp --batch --no-options --no-tty --passphrase-fd 0 2>/dev/null \ -o "${tombdir}/${tombname}.tomb.key" -c -a ${keytmp}/tomb.tmp # if [ $? != 0 ]; 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 Ext3/Ext4 filesystem" ${MKFS} ${tombname} /dev/mapper/tomb.tmp if [ $? != 0 ]; then error "Tomb format returns error" error "your tomb ${tombfile} maybe corrupt" fi sync cryptsetup luksClose tomb.tmp losetup -d ${nstloop} act "done creating $tombname encrypted storage (using Luks dm-crypt AES/SHA256)" notice "Your tomb is ready in ${tombdir}/${tombfile} and secured with key ${tombname}.tomb.key" } mount_tomb() { get_arg_tomb $CMD2 if [ $? != 0 ]; then error "operation aborted." return 1 fi if ! [ $CMD3 ]; then tombmount=/media/${tombfile} act "mountpoint not specified, using default: $tombmount" elif ! [ -x $CMD3 ]; then error "mountpoint $CMD3 doesn't exist, operation aborted." if [ -n "$usbkey_mount" ]; then umount $usbkey_mount rmdir $usbkey_mount unset usbkey_mount fi return 1 else tombmount=$CMD3 fi notice "mounting $tombfile on mountpoint $tombmount" # we need root from here on mkdir -p $tombmount nstloop=`losetup -f` if [ $? = 255 ]; then error "too many tomb opened. Please close any of them to open another tomb" exit 1 fi losetup -f ${tombdir}/${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" $norm || rmdir $tombmount 2>/dev/null return 1 fi # save date of mount in minutes since 1970 mapdate="`date +%s`" mapdate="`echo ${mapdate}/60 | bc -l | cut -d. -f1`" mapper="tomb.${tombname}.${mapdate}.`basename $nstloop`" keyname=`basename $tombkey | cut -d. -f1` notice "Password is required for key ${keyname}" for c in 1 2 3; do if [ $c = 1 ]; then tombpass=`exec_as_user ${TOMBEXEC} askpass ${keyname}` else tombpass=`exec_as_user ${TOMBEXEC} askpass "$keyname (retry $c)"` fi echo "${tombpass}" \ | gpg --batch --passphrase-fd 0 --no-tty --no-options \ -d "${tombkey}" 2> /dev/null \ | cryptsetup --key-file - luksOpen ${nstloop} ${mapper} unset tombpass if [ -r /dev/mapper/${mapper} ]; then break; # password was correct fi done if ! [ -r /dev/mapper/${mapper} ]; then error "failure mounting the encrypted file" losetup -d ${nstloop} $norm || rmdir ${tombmount} 2>/dev/null return 1 fi act "encrypted storage filesystem check" fsck -p -C0 /dev/mapper/${mapper} act "tomb engraved as $tombname" tune2fs -L ${tombname} /dev/mapper/${mapper} > /dev/null 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" if ! [ $NOBIND ]; then exec_safe_bind_hooks ${tombmount} exec_safe_post_hooks ${tombmount} open fi return 0 } encode_key() { tombkey=$CMD2 imagefile=$CMD3 file $tombkey | grep PGP > /dev/null if [ $? != 0 ]; then error "encode failed: $tombkey is not a tomb key" return 1 fi file $imagefile | grep JPEG > /dev/null if [ $? != 0 ]; then error "encode failed: $imagefile is not a jpeg image" return 1 fi notice "Encoding key $tombkey inside image $imagefile" act "please choose a password for the encoding" # here user is prompted for key password for c in 1 2 3; do # 3 tries to write two times a matching password tombpass=`exec_as_user ${TOMBEXEC} askpass ${tombkey}` tombpasstmp=$tombpass tombpass=`exec_as_user ${TOMBEXEC} askpass "${tombkey} (again)"` if [ "$tombpasstmp" = "$tombpass" ]; then break; fi unset tombpasstmp unset tombpass done if [ -z $tombpass ]; then error "passwords don't match, aborting operation." return 1 fi awk ' /^-----/ {next} /^Version/ {next} {print $0}' ${tombkey} \ | steghide embed --embedfile - --coverfile ${imagefile} \ -p ${tombpass} -z 9 -e serpent cbc if [ $? != 0 ]; then error "encoding error: steghide reports problems" res=1 else notice "tomb key encoded succesfully into image ${imagefile}" res=0 fi unset tombpass return $res } decode_key() { tombname=$CMD2 imagefile=$CMD3 res=1 file $imagefile | grep JPEG > /dev/null if [ $? != 0 ]; then error "encode failed: $imagefile is not a jpeg image" return 1 fi keyfile=${tombname%%\.*}.tomb.key notice "Trying to exhume a key out of image $imagefile" for c in 1 2 3; do if [ $c = 1 ]; then tombpass=`exec_as_user ${TOMBEXEC} askpass ${keyfile}` else tombpass=`exec_as_user ${TOMBEXEC} askpass "$keyfile (retry $c)"` fi steghide extract -sf ${imagefile} -p ${tombpass} -xf - \ | awk ' BEGIN { print "-----BEGIN PGP MESSAGE-----" } { print $0 } END { print "-----END PGP MESSAGE-----" }' > ${keyfile} if [ "`cat ${keyfile} | wc -l`" != "3" ]; then act "${keyfile} succesfully decoded" res=0 break; fi done unset tombpass if [ $res != 0 ]; then error "nothing found." fi return $res } exec_safe_bind_hooks() { local MOUNTPOINT="${1}" local ME=${SUDO_USER:-$(whoami)} local HOME=$(grep $ME /etc/passwd | sed "s/^${ME}:.*:.*:.*:.*:\([\/a-z]*\):.*$/\1/" 2>/dev/null) if [ $? -ne 0 ]; then error "how pitiful! A tomb, and no HOME" return 1 fi if [ -z "$MOUNTPOINT" -o ! -d "$MOUNTPOINT" ]; then error "cannot exec bind hooks without a mounted tomb." return 1 fi if ! [ -r "$MOUNTPOINT/bind-hooks" ]; then func "bind-hooks not found in $MOUNTPOINT" return 1 fi typeset -al mounted typeset -Al maps maps=($(<"$MOUNTPOINT/bind-hooks")) for dir in ${(k)maps}; do if [ "${dir[1]}" = "/" -o "${dir[1,2]}" = ".." ]; then error "bind-hooks map format: local/to/tomb local/to/\$HOME" continue fi if [ "${${maps[$dir]}[1]}" = "/" -o "${${maps[$dir]}[1,2]}" = ".." ]; then error "bind-hooks map format: local/to/tomb local/to/\$HOME. Rolling back" for dir in ${mounted}; do umount $dir; done return 1 fi if [ ! -r "$HOME/${maps[$dir]}" ]; then error "bind-hook target not existent, skipping $HOME/${maps[$dir]}" elif [ ! -r "$MOUNTPOINT/$dir" ]; then error "bind-hook source not found in tomb, skipping ${MOUNTPOINT}/${dir}" else mount -o bind $MOUNTPOINT/$dir $HOME/${maps[$dir]} mounted+=("$HOME/${maps[$dir]}") fi done } exec_safe_post_hooks() { local mnt=$1 # first argument is where the tomb is mounted local ME=${SUDO_USER:-$(whoami)} 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 $2 fi } umount_tomb() { local tombs how_many_tombs local pathmap mapper tombname tombmount loopdev local ans pidk pname if ! [ $1 ]; then tombs=`find /dev/mapper -name 'tomb.*'` how_many_tombs=`wc -w <<< "$tombs"` 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.*'` notice "trying to close $tombs" umount_tomb ${tombs} return 1 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 notice "Tombs are all closed, cemetery is quiet." return 0 fi for t in ${(f)tombs}; do umount_tomb ${t} done return 0 fi if ! [ -e "$1" ]; then error "tomb not found: $1" error "Please specify an existing tomb" return 0 fi pathmap=`dirname "$1"` if [ "${pathmap}" = "/dev/mapper" ]; then mapper="$1" # $1 is /dev/mapper/tomb.* tombname=`basename "$1"` # this is tomb.NAME.XXX.loopX tombmount=`mount | grep "$mapper" | awk -F ' ' '{print $3}'` # tomb mount point else tombmount="$1" # $1 is the tomb mount point (e.g. /mnt) mapper=`mount | grep -w "${tombmount%%/}" | awk -F ' ' '{print $1}'` tombname=`basename "$mapper"` # this is tomb.NAME.XXX.loopX fi # check if there are binded dirs and close them first mount | grep "${tombmount%%/}" 2>/dev/null | 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 # Execute post-hooks for eventual cleanup if ! [ $NOBIND ]; then exec_safe_post_hooks ${tombmount%%/} close fi act "closing tomb $tombname on dm-crypt $tombmount" umount ${tombmount} 2> /dev/null if ! [ $? = 0 ]; then error "Tomb is busy, cannot umount!" notice "Do you want to force umount? y/N: " read ans if [ "$ans" = "S" -o "$ans" = "s" -o "$ans" = "y" -o "$ans" = "Y" ]; then pidk=`lsof -t "$tombmount"` for p in "$pidk"; do pname=`pidof $p` func "killing PID $p of $pname..." kill -9 $p done umount "${tombmount}" else error "Cannot umount $tombname on $tombmount" return 1 fi fi cryptsetup luksClose $tombname if ! [ $? = 0 ]; then error "error occurred in cryptsetup luksClose ${mapper}" return 1 fi loopdev=`cut -d '.' -f4 <<< "$tombname"` losetup -d "/dev/$loopdev" notice "Tomb $tombname closed: 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, sudo 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="${TOMBOPENEXEC}" %U TryExec=tomb-open Icon=monmort.xpm Terminal=true 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="text" \ 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="${TOMBOPENEXEC}" %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.key 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=true mime-types=application/x-tomb-volume,application/x-tomb-key EOF act "Tomb is now installed." } main () { echo $@ | grep '\-D' 2>&1 > /dev/null # ????? if [ $? = 0 ]; then fi ARGS=$@[@] OPTS=`getopt -o hvqDs:k:n -n 'tomb' -- "$@"` while true; do case "$1" in -h) usage exit 0 ;; -v) notice "Tomb - simple commandline tool for encrypted storage" act "version $VERSION ($DATE) by Jaromil @ dyne.org" # 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) echo "[D] Tomb invoked with args \"${(f)@}\" " echo "[D] running on `date`" DEBUG=1; shift 1 ;; -s) SIZE=$2; shift 2 ;; -k) KEY=$2; shift 2 ;; -n) NOBIND=1; shift 1 ;; --) 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" exit 0 fi func "Tomb command: $CMD $CMD2 $CMD3" 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} ;; bury) if [ "$STEGHIDE" = 0 ]; then error "steghide not installed. Cannot bury your key" return 1 fi encode_key ${CMD2} ${CMD3} ;; exhume) if [ "$STEGHIDE" = 0 ]; then error "steghide not installed. Cannot exhume your key" return 1 fi decode_key ${CMD2} ;; install) check_priv ; install_tomb ;; askpass) ask_password $CMD2 ;; status) tomb-status ;; *) error "command \"$CMD\" not recognized" act "try -h for help" return 1 ;; esac return 0 } check_bin main $@