#!/bin/zsh # # Tomb, the Crypto Undertaker # # a tool to easily operate file encryption of private and secret data # # {{{ Copyleft (C) 2007-2012 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. # }}} # {{{ GLOBAL VARIABLES VERSION=1.3 DATE=Nov/2012 TOMBEXEC=$0 TOMBOPENEXEC="${TOMBEXEC}-open" typeset -a OLDARGS for arg in ${argv}; do OLDARGS+=($arg); done STEGHIDE=1 MKTEMP=1 MOUNTOPTS="rw,noatime,nodev" typeset -A global_opts typeset -A opts # Set a sensible PATH PATH=/sbin:/bin:/usr/sbin:/usr/bin [[ "$TOMBEXEC" =~ "^/usr/local" ]] && PATH="/usr/local/bin:/usr/local/sbin:$PATH" # PATH=/usr/bin:/usr/sbin:/bin:/sbin # }}} # {{{ HELPER FUNCTIONS # {{{ - Standard output message routines function _msg() { local command="print -P" local progname="$fg[magenta]${TOMBEXEC##*/}$fg[reset_color]" local message="$fg_bold[normal]$fg_no_bold[normal]${2}$fg[reset_color]" local -i returncode case "$1" in inline) command+=" -n"; pchars=" > "; pcolor="yellow" ;; message) pchars=" . "; pcolor="white"; message="$fg_no_bold[$pcolor]${2}$fg[reset_color]" ;; verbose) pchars="[D]"; pcolor="blue" ;; success) pchars="(*)"; pcolor="green"; message="$fg_no_bold[$pcolor]${2}$fg[reset_color]" ;; warning) pchars="[W]"; pcolor="yellow"; message="$fg_no_bold[$pcolor]${2}$fg[reset_color]" ;; failure) pchars="[E]"; pcolor="red"; message="$fg_no_bold[$pcolor]${2}$fg[reset_color]" returncode=1 ;; *) pchars="[F]"; pcolor="red" message="Developer oops! Usage: _msg MESSAGE_TYPE \"MESSAGE_CONTENT\"" returncode=127 ;; esac ${=command} "${progname} $fg_bold[$pcolor]$pchars$fg[reset_color] ${message}$color[reset_color]" return $returncode } function _message say() { local notice="message" [[ "$1" = "-n" ]] && shift && notice="inline" option_is_set -q || _msg "$notice" "$1" return 0 } alias act="_message -n" function _verbose xxx() { option_is_set -D && _msg verbose "$1" return 0 } function _success yes() { option_is_set -q || _msg success "$1" return 0 } function _warning no() { option_is_set -q || _msg warning "$1" return 1 } function _failure die() { typeset -i exitcode=${2:-1} option_is_set -q || _msg failure "$1" exit $exitcode } # }}} # {{{ - CHECK BINARY DEPENDENCIES check_bin() { # check for required programs for req in cryptsetup pinentry sudo gpg; do which $req >/dev/null || die "Cannot find $req. Please install it." 1 done # which dd command to use which dcfldd > /dev/null && DD=dcfldd || DD=dd # which wipe command to use which wipe > /dev/null && WIPE="wipe -f -s" || WIPE="rm -f" # check for filesystem creation progs which mkfs.ext4 > /dev/null && \ MKFS="mkfs.ext4 -q -F -j -L" || \ MKFS="mkfs.ext3 -q -F -j -L" # check for mktemp which mktemp > /dev/null || MKTEMP=0 # check for steghide which steghide > /dev/null || STEGHIDE=0 # resize suite check bin! which e2fsck > /dev/null || die "Cannot find e2fsck. Please install it." 1 which resize2fs > /dev/null || die "Cannot find resize2fs. Please install it." 1 } # }}} # {{{ - "SAFE" FUNCTIONS # {{{ - Create a directory with caution _have_shm() { # Check availability of 1MB of SHM xxx "_have_shm 0 We need only 1 MB of RAM" [[ -k /dev/shm ]] || return 1 local -i SHM RAM SHM=$(df -k -B 4K -a -t tmpfs /dev/shm | awk '/\/dev\/shm/ { print $4; }') (( $? )) && return 1 xxx "_have_shm 1 SHM $SHM KB are available" RAM=$(awk '/MemFree/ { print $2 }' /proc/meminfo) xxx "_have_shm 2 RAM $RAM KB are free" (( $RAM >= 1024 )) && return 0 xxx "_have_shm 3 RAM $RAM KB left only :(" # Now we have more RAM than affected to SHM, so we can expect some for our little needs. # Does that work when SHM is disabled from kernel config? return 1 } safe_dir() { # Try and create our temporary directory in RAM # Note that there's no warranty the underlying FS won't swap # every 5 seconds (e.g., ext3) local -i tries while (( $tries < 3 )) ; do tries+=1 if _have_shm; then xxx "safe_dir creating directory in RAM" if (( $MKTEMP )); then mktemp -d /dev/shm/$1.$$.XXXXXXX else dir="/dev/shm/$1.$$.$RANDOM$RANDOM" mkdir -m 0700 -p "$dir" print "$dir" fi return 0 else _warning "WARNING: we cannot ensure we're running in RAM." xxx "Wait a bit before retrying... (attempt $tries)" sync && sleep 0.5 fi done _warning "WARNING: no RAM available for me to run safely." return 1 } # }}} # {{{ - Provide a random filename in shared memory safe_filename() { _have_shm || die "No access to shared memory on this system, sorry." (( $MKTEMP )) && \ mktemp -u /dev/shm/$1.$$.XXXXXXX || \ print "/dev/shm/$1.$$.$RANDOM$RANDOM" } # }}} # {{{ - Check if swap is activated check_swap() { # Return 0 if NO swap is used, 1 if swap is used # Return 2 if swap(s) is(are) used, but ALL encrypted local swaps=$(awk '/partition/ { print $1 }' /proc/swaps 2>/dev/null) [[ -z "$swaps" ]] && return 0 # No swap partition is active no "An active swap partition is detected, this poses security risks." no "You can deactivate all swap partitions using the command:" no " swapoff -a" no "But if you want to proceed like this, use the -f (force) flag." die "Operation aborted." } # }}} # {{{ - Ask user for a password # 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 title="Insert tomb password" if [ $2 ]; then title="$2"; fi cat </dev/null | awk '/^D / { sub(/^D /, ""); print }' OPTION ttyname=$TTY OPTION lc-ctype=$LANG SETTITLE $title SETDESC $1 SETPROMPT Password: GETPIN EOF } # }}} # {{{ - Drop privileges exec_as_user() { if ! [ $SUDO_USER ]; then exec $@[@] return $? fi xxx "exec_as_user '$SUDO_USER': ${(f)@}" sudo -u $SUDO_USER "${@[@]}" return $? } # }}} # {{{ - Escalate privileges check_priv() { if [ $UID != 0 ]; then xxx "Using sudo for root execution of 'tomb ${(f)OLDARGS}'" # check if sudo has a timestamp active sudok=false sudo -n ${TOMBEXEC} &> /dev/null if [ $? != 0 ]; then # if not then ask a password cat </dev/null | awk '/^D / { sub(/^D /, ""); print }' | sudo -S -v OPTION ttyname=$TTY OPTION lc-ctype=$LANG SETTITLE Super user privileges required SETDESC Sudo execution of Tomb ${OLDARGS[@]} SETPROMPT Insert your USER password: GETPIN EOF fi sudo "${TOMBEXEC}" "${(@)OLDARGS}" exit $? fi # are we root already return 0 } # }}} # }}} # {{{ - TOMB USAGE usage() { cat <. EOF } # }}} # {{{ - I18N FUNCTIONS generate_translatable_strings() { cat <, 2012. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Tomb $VERSION\n" "PO-Revision-Date: `date`\n" "Last-Translator: Denis Roio \n" "Language-Team: Tomb developers \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" # #: commandline help # msgid "" EOF usage | awk ' { print "\"" $0 "\"" }' cat < /dev/null if [ $? != 0 ]; then _warning "encode failed: $tombkey is not a tomb key" return 1 fi file $imagefile | grep JPEG > /dev/null if [ $? != 0 ]; then _warning "encode failed: $imagefile is not a jpeg image" return 1 fi _success "Encoding key $tombkey inside image $imagefile" _message "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 "Steg password for ${tombkey}"` tombpasstmp=$tombpass tombpass=`exec_as_user ${TOMBEXEC} askpass "Steg password for ${tombkey} (again)"` if [ "$tombpasstmp" = "$tombpass" ]; then break; fi unset tombpasstmp unset tombpass done if [ -z $tombpass ]; then _warning "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 _warning "encoding error: steghide reports problems" res=1 else _success "tomb key encoded succesfully into image ${imagefile}" res=0 fi unset tombpass return $res } # }}} # {{{ - Decode Key decode_key() { tombname=$CMD2 imagefile=$CMD3 res=1 file $imagefile | grep JPEG > /dev/null if [ $? != 0 ]; then _warning "encode failed: $imagefile is not a jpeg image" return 1 fi keyfile=${tombname%%\.*}.tomb.key if [[ -e "$keyfile" ]]; then _warning "Key file $keyfile already exist." return 1 fi _message "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 "Steg password for ${keyfile}"` else tombpass=`exec_as_user ${TOMBEXEC} askpass "Steg password for $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 _success "${keyfile} succesfully decoded" res=0 break; fi done unset tombpass if [ $res != 0 ]; then _warning "nothing found." fi return $res } list_gnupg_ciphers() { # prints an array of ciphers available in gnupg (to encrypt keys) # prints an error if GnuPG is not found which gnupg > /dev/null || die "gpg (GnuPG) is not found, Tomb cannot function without it." ciphers=(`gpg --version | awk ' BEGIN { ciphers=0 } /^Cipher:/ { gsub(/,/,""); sub(/^Cipher:/,""); print; ciphers=1; next } /^Hash:/ { ciphers=0 } { if(ciphers==0) { next } else { gsub(/,/,""); print; } } '`) echo " ${ciphers}" return 1 } # }}} # }}} # {{{ - HOOK HELPERS # {{{ - Execute Bind Hooks exec_safe_bind_hooks() { if [[ -n ${(k)opts[-o]} ]]; then MOUNTOPTS=${opts[-o]} fi local MOUNTPOINT="${1}" local ME=${SUDO_USER:-$(whoami)} local HOME=$(awk -v a="$ME" -F ':' '{if ($1 == a) print $6}' /etc/passwd 2>/dev/null) if [ $? -ne 0 ]; then _warning "how pitiful! A tomb, and no HOME" return 1 fi if [ -z "$MOUNTPOINT" -o ! -d "$MOUNTPOINT" ]; then _warning "cannot exec bind hooks without a mounted tomb." return 1 fi if ! [ -r "$MOUNTPOINT/bind-hooks" ]; then xxx "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 _warning "bind-hooks map format: local/to/tomb local/to/\$HOME" continue fi if [ "${${maps[$dir]}[1]}" = "/" -o "${${maps[$dir]}[1,2]}" = ".." ]; then _warning "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 _warning "bind-hook target not existent, skipping $HOME/${maps[$dir]}" elif [ ! -r "$MOUNTPOINT/$dir" ]; then _warning "bind-hook source not found in tomb, skipping ${MOUNTPOINT}/${dir}" else mount -o bind,$MOUNTOPTS $MOUNTPOINT/$dir $HOME/${maps[$dir]} mounted+=("$HOME/${maps[$dir]}") fi done } # }}} # {{{ - POST-MOUNT HOOKS 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 _success "post hooks found, executing as user $SUDO_USER" exec_as_user ${mnt}/post-hooks $2 fi } # }}} # }}} # }}} # {{{ TOMB SUB-COMMANDS # {{{ - Create create_tomb() { _message "Commanded to create tomb $CMD2" if ! option_is_set -f; then check_swap; fi if ! [ ${CMD2} ]; then _warning "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 tombsize=$opts[-s] [ $tombsize ] || die "Size argument missing, use --size" [[ $tombsize != <-> ]] && die "Size argument is not an integer" if [ -e ${tombdir}/${tombfile} ]; then _warning "tomb exists already. I'm not digging here:" ls -lh ${tombdir}/${tombfile} return 1 fi if option_is_set -k; then tombkey="`option_value -k`.tomb.key" else tombkey="${tombdir}/${tombfile}.key" fi if [ -e "${tombkey}" ]; then _warning "tomb key already exists. Quitting." ls -lh ${tombkey} return 1 fi _success "Creating a new tomb in ${tombdir}/${tombfile}" if [ -z $tombsize ]; then _message "No size specified, summoning the Tomb Undertaker to guide us in the creation." "$TOMBOPENEXEC" & wait $! return 0 fi tombsize_4k=`expr $tombsize \* 1024 / 4` _message "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 _success "OK: `ls -lh ${tombdir}/${tombfile}`" else die "Error creating the tomb ${tombdir}/${tombfile}, operation aborted." 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` (( $? )) && die "error creating temp dir" xxx "safe_dir at $keytmp" #rm -f $keytmp # ?????? creo, cancello e ricreo ?????? #mkdir -p $keytmp mount tmpfs "${keytmp}" -t tmpfs -o size=1m if [ $? != 0 ]; then _warning "cannot mount tmpfs filesystem in volatile memory" losetup -d $nstloop rm -r "${keytmp}" die "operation aborted." fi _message "Generating secret key..." _message "this operation takes time, keep using this computer on other tasks," _message "once done you will be asked to choose a password for your tomb." _message "To make it faster you can move the mouse around" touch ${keytmp}/tomb.tmp chmod 0600 ${keytmp}/tomb.tmp if [[ $DD = "dcfldd" ]]; then $DD bs=1 count=256 if=/dev/random of=${keytmp}/tomb.tmp statusinterval=1 else $DD bs=1 count=256 if=/dev/random of=${keytmp}/tomb.tmp fi if ! [ -r ${keytmp}/tomb.tmp ]; then _warning "cannot generate encryption key" umount ${keytmp} losetup -d $nstloop rm -r $keytmp die "operation aborted." fi _success "Setup your secret key file ${tombkey}" # 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 "Secure key for ${tombname}"` tombpasstmp=$tombpass tombpass=`exec_as_user ${TOMBEXEC} askpass "Secure key for ${tombname} (again)"` if [ "$tombpasstmp" = "$tombpass" ]; then break; fi unset tombpasstmp unset tombpass done if [ -z $tombpass ]; then umount ${keytmp} losetup -d $nstloop rm -r $keytmp die "passwords don't match, aborting operation" fi gpg \ --openpgp --batch --no-options --no-tty --passphrase-fd 0 2>/dev/null \ -o "${tombkey}" -c -a ${keytmp}/tomb.tmp <<< ${tombpass} # if [ $? != 0 ]; then # _warning "setting password failed: gnupg returns 2" # umount ${keytmp} # losetup -d $nstloop # rm -r $keytmp # exit 1 # fi _message "formatting Luks mapped device" # we use aes-cbc-essiv with sha256 # for security, performance and compatibility # XXX: More for compatibility then, because xts-plain is better nowadays. cryptsetup --batch-mode \ --cipher aes-cbc-essiv:sha256 --key-size 256 \ luksFormat ${nstloop} ${keytmp}/tomb.tmp if ! [ $? = 0 ]; then die "operation aborted." 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} _message "formatting your Tomb with Ext3/Ext4 filesystem" ${=MKFS} ${tombname} /dev/mapper/tomb.tmp if [ $? != 0 ]; then _warning "Tomb format returned an error:" _warning "your tomb ${tombfile} may be corrupted." fi sync cryptsetup luksClose tomb.tmp losetup -d ${nstloop} # set permissions on the tomb ME=${SUDO_USER:-$(whoami)} chmod 0600 "${tombdir}/${tombfile}" chown $(id -u $ME):$(id -g $ME) "${tombdir}/${tombfile}" _message "done creating $tombname encrypted storage (using Luks dm-crypt AES/SHA256)" _success "Your tomb is ready in ${tombdir}/${tombfile} and secured with key ${tombkey}" } # }}} # {{{ - Open mount_tomb() { _message "Commanded to open tomb $CMD2" if ! option_is_set -f; then check_swap; fi if ! [ ${CMD2} ]; then _warning "no tomb name specified for creation" return 1 fi # set up variables to be used # the full path is made with $tombdir/$tombfile local tombkey local tombfile local tombdir local tombname tombfile=`basename ${CMD2}` tombdir=`dirname ${CMD2}` # check file type (if its a Luks fs) file ${tombdir}/${tombfile} | grep -i 'luks encrypted file' 2>&1 >/dev/null if [ $? != 0 ]; then _warning "$CMD2 is not a valid tomb file, operation aborted" return 1 fi tombname=${tombfile%%\.*} xxx "tomb found: ${tombdir}/${tombfile}" if option_is_set -k ; then if [[ "`option_value -k`" == "-" ]]; then # take key from stdin local tombkeydir tombkeydir=`safe_dir` cat > ${tombkeydir}/stdin.tmp tombkey=${tombkeydir}/stdin.tmp else # take key from a file tombkey=`option_value -k` fi else # guess key as lying besides the tomb tombkey=${tombdir}/${tombfile}.key fi if ! [ -r ${tombkey} ]; then _warning "key file not found: ${tombkey}" _warning "operation aborted." return 1 fi if ! [ $CMD3 ]; then tombmount=/media/${tombfile} _message "mountpoint not specified, using default: $tombmount" elif ! [ -x $CMD3 ]; then _warning "mountpoint $CMD3 doesn't exist, operation aborted." return 1 else tombmount=${CMD3} fi # check if its already open mount -l | grep "${tombfile}.*\[$tombname\]$" 2>&1 > /dev/null if [ $? = 0 ]; then _warning "$tombname is already open on $tombmount" _message "here below its status is reported:" list_tombs ${tombname} return 1 fi _success "Opening $tombfile on $tombmount" # we need root from here on mkdir -p $tombmount nstloop=`losetup -f` if [ $? = 255 ]; then die "too many tomb opened. Please close any of them to open another tomb" fi losetup -f ${tombdir}/${tombfile} _message "check for a valid LUKS encrypted device" cryptsetup isLuks ${nstloop} if [ $? != 0 ]; then # is it a LUKS encrypted nest? see cryptsetup(1) _warning "$tombfile is not a valid Luks encrypted storage file" $norm || rmdir $tombmount 2>/dev/null return 1 fi tombdump=(`cryptsetup luksDump ${nstloop} | awk ' /^Cipher name/ {print $3} /^Cipher mode/ {print $3} /^Hash spec/ {print $3}'`) say "cipher is \"$tombdump[1]\" mode \"$tombdump[2]\" hash \"$tombdump[3]\"" # save date of mount in minutes since 1970 mapdate=`date +%s` mapper="tomb.${tombname}.${mapdate}.`basename $nstloop`" keyname=`basename $tombkey | cut -d. -f1` _warning "Password is required for key ${keyname}" for c in 1 2 3; do if [ $c = 1 ]; then tombpass=`exec_as_user ${TOMBEXEC} askpass "Open tomb ${keyname}"` else tombpass=`exec_as_user ${TOMBEXEC} askpass "Open tomb $keyname (retry $c)"` fi (gpg --batch --passphrase-fd 0 --no-tty --no-options \ -d "${tombkey}" 2> /dev/null <<< ${tombpass} ) \ | cryptsetup --key-file - luksOpen ${nstloop} ${mapper} unset tombpass # if key was from stdin delete temp file and dir if [ $tombkeydir ]; then ${=WIPE} ${tombkey} rmdir $tombkeydir fi # if key was from stdin delete temp file and dir if [ $tombkeydir ]; then ${WIPE[@]} ${tombkey} rmdir $tombkeydir fi if [ -r /dev/mapper/${mapper} ]; then break; # password was correct fi done if ! [ -r /dev/mapper/${mapper} ]; then losetup -d ${nstloop} $norm || rmdir ${tombmount} 2>/dev/null die "failure mounting the encrypted file" fi # array: [ cipher, keysize, loopdevice ] tombstat=(`cryptsetup status ${mapper} | awk ' /cipher:/ {print $2} /keysize:/ {print $2} /device:/ {print $2}'`) yes "Success unlocking tomb $tombname" xxx "key size is $tombstat[2] for cipher $tombstat[1]" _message "checking filesystem via $tombstat[3]" fsck -p -C0 /dev/mapper/${mapper} xxx "tomb engraved as $tombname" tune2fs -L ${tombname} /dev/mapper/${mapper} > /dev/null mount -o $MOUNTOPTS /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} _success "Success opening $tombfile on $tombmount" if ! option_is_set -n ; then exec_safe_bind_hooks ${tombmount} exec_safe_post_hooks ${tombmount} open fi return 0 } # }}} # {{{ - Close # {{{ - Slam the door # Kill all processes using the tomb slam_tomb() { # $1 = tomb mount point for s in INT TERM HUP KILL; do fuser -s -m "$1" || return 0 fuser -s -m "$1" -k -M -$s && { option_is_set -f || sleep 3 } done return 1 } # }}} # {{{ - Unmount Tomb 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 _warning "There is no open tomb to be closed" return 1 elif [[ "$how_many_tombs" == "1" ]]; then #mapper=`find /dev/mapper -name 'tomb.*'` xxx "closing mapper $tombs" umount_tomb ${tombs} return 1 else _warning "Too many tombs mounted, please specify which to unmount:" ls /dev/mapper/tomb.* _warning "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 _success "Tombs are all closed, cemetery is quiet." return 0 fi for t in ${(f)tombs}; do umount_tomb ${t} done return 0 fi # tomb close argument deduction pathmap=`dirname "$1"` if [ "${pathmap}" = "/dev/mapper" ]; then mapper="$1" # argument is the mapper (or none which autofills mapper) tombname="`print $mapper | cut -d. -f2`" tombmount=`mount -l | \ awk -vtomb="[$tombname]" '/^\/dev\/mapper\/tomb/ { if($7==tomb) print $3 } '` elif [ "$pathmap" = "." ]; then tombname="$1" # argument is the name mapper=`mount -l | \ awk -vtomb="[$tombname]" '/^\/dev\/mapper\/tomb/ { if($7==tomb) print $1 } '` tombmount=`mount -l | \ awk -vtomb="[$tombname]" '/^\/dev\/mapper\/tomb/ { if($7==tomb) print $3 } '` else tombmount="$1" # argument should be the mount mapper=`mount | awk -vmnt="$tombmount" '/^\/dev\/mapper\/tomb/ { if($3==mnt) print $1 }'` tombname="`print $mapper | cut -d. -f2`" fi # avoid block when the same tomb is mounted, take only the first for tm in ${(f)tombmount}; do tombmount=${tm}; break; done xxx "tomb close argument: $1" xxx "name:\t$tombname" xxx "mount:\t$tombmount" xxx "mapper:\t$mapper" if ! [ -e "$mapper" ]; then _warning "Tomb not found: $1" _warning "Please specify an existing tomb." return 0 fi if [ $SLAM ]; then _success "Slamming tomb $tombname mounted on $tombmount" _message "Kill all processes busy inside the tomb" slam_tomb "$tombmount" if [[ $? == 1 ]]; then _warning "Cannot slam the tomb $tombname" return 1 fi else _success "Closing tomb $tombname mounted on $tombmount" fi # check if there are binded dirs and close them tombmount_esc=`sed 's:\/:\\\/:g' <<< $tombmount ` unbind=`mount | awk "/^$tombmount_esc.*bind/"' { print $3 }'` for b in ${(f)unbind}; do hook="`basename $b`" _message "closing tomb hook: $hook" umount $b if [[ $? != 0 ]]; then if [ $SLAM ]; then _success "Slamming tomb: killing all processes using this hook" slam_tomb "$b" if [[ $? == 1 ]]; then _warning "Cannot slam the tomb $b" return 1 fi umount $b else _warning "Tomb hook is busy, cannot close tomb." return 1 fi fi done # Execute post-hooks for eventual cleanup if ! option_is_set -n ; then exec_safe_post_hooks ${tombmount%%/} close fi if [ $tombmount ]; then # tomb is actively mounted xxx "performing umount of $tombmount" umount ${tombmount} if ! [ $? = 0 ]; then _warning "Tomb is busy, cannot umount!" else # this means we used a "default" mount point if [ "${tombmount}" = "/media/${tombname}.tomb" ]; then rmdir ${tombmount} fi fi fi cryptsetup luksClose $mapper if ! [ $? = 0 ]; then _warning "error occurred in cryptsetup luksClose ${mapper}" return 1 fi loopdev=`cut -d '.' -f4 <<< "$mapper"` losetup -d "/dev/$loopdev" # kill the status tray widget if still present # this makes the widget disappear when closing tomb from cli awkmapper=`sed 's:\/:\\\/:g' <<< $mapper` statustray_pid=`ps ax | awk "/tomb-status $awkmapper/"' {print $1} '` if [ ${statustray_pid} ]; then kill ${statustray_pid} fi _success "Tomb $tombname closed: your bones will rest in peace." return 0 } # }}} # }}} # {{{ - Change Password # change tomb key password change_passwd() { _message "Commanded to change password for tomb key $CMD2" if ! option_is_set -f; then check_swap; fi local keyfile="$CMD2" # check the keyfile if ! [ -r $keyfile ]; then _warning "key not found: $keyfile" return 1 fi file $keyfile | grep PGP > /dev/null if [ $? != 0 ]; then _warning "file doesn't seems to be a tomb key: $keyfile" _warning "operation aborted." return 1 fi local tmpnewkey tmpoldkey c tombpass tombpasstmp tmpnewkey=`safe_filename tomb` tmpoldkey=`safe_filename tomb` _success "Changing password for $keyfile" keyname=`basename $keyfile` for c in 1 2 3; do if [ $c = 1 ]; then tombpass=`exec_as_user ${TOMBEXEC} askpass "Type old password for ${keyname}" "Change tomb key password"` else tombpass=`exec_as_user ${TOMBEXEC} askpass "Type old password for ${keyname} (retry $c)" "Change tomb key password"` fi gpg --batch --no-options --no-tty --passphrase-fd 0 -o "${tmpoldkey}" -d $keyfile <<< "$tombpass" &> /dev/null if [ $? = 0 ]; then tombpass="ok" break fi done if [ "$tombpass" != "ok" ]; then _warning "You typed an Invalid old password. Operation aborted." # /dev/null because the file may not exist ${=WIPE} "${tmpnewkey}" 2> /dev/null ${=WIPE} "${tmpoldkey}" 2> /dev/null return 1 fi for c in 1 2 3; do # 3 tries to write two times a matching password if [ $c = 1 ]; then tombpass=`exec_as_user ${TOMBEXEC} askpass "Type the new password for ${keyname}" "Change tomb key password"` else tombpass=`exec_as_user ${TOMBEXEC} askpass "Type the new password for ${keyname} (retry $c)" "Change tomb key password"` fi tombpasstmp=$tombpass tombpass=`exec_as_user ${TOMBEXEC} askpass "Type the new password again" "Change tomb key password"` if [ "$tombpasstmp" = "$tombpass" ]; then break; fi unset tombpasstmp unset tombpass done if [ -z $tombpass ]; then _warning "You mistyped the new password. Operation aborted." # /dev/null because the file cannot exists ${=WIPE} "${tmpnewkey}" 2> /dev/null ${=WIPE} "${tmpoldkey}" 2> /dev/null return 1 fi gpg \ --openpgp --batch --no-options --no-tty --passphrase-fd 0 \ -o "${tmpnewkey}" -c -a ${tmpoldkey} <<< ${tombpass} if [ $? != 0 ]; then _warning "Cannot change your key passphrase" # /dev/null because the file cannot exists ${=WIPE} "${tmpnewkey}" 2> /dev/null ${=WIPE} "${tmpoldkey}" 2> /dev/null return 1 fi # wipe the previous, original, key ${=WIPE} "${keyfile}" # copy the new key as the original keyfile name cp "${tmpnewkey}" "${keyfile}" _message "Cleaning environment" # wipe all temp file ${=WIPE} "${tmpnewkey}" ${=WIPE} "${tmpoldkey}" _success "Your passphrase was successfully updated." return 0 } # }}} # {{{ - Resize # resize tomb file size resize_tomb() { _message "Commanded to resize tomb $CMD2 to $opts[-s] megabytes" if ! [ ${CMD2} ]; then _failure "No tomb name specified for resizing" elif ! [ -r "${CMD2}" ]; then _failure "Cannot find ${CMD2}" fi local c tombpass tombkey local tombfile=`basename ${CMD2}` local tombdir=`dirname ${CMD2}` # make sure the file has a .tomb extension local tombname=${tombfile%%\.*} tombfile=${tombname}.tomb if option_is_set -k ; then if [[ "`option_value -k`" == "-" ]]; then # take key from stdin local tombkeydir tombkeydir=`safe_dir` cat > ${tombkeydir}/stdin.tmp tombkey=${tombkeydir}/stdin.tmp else # take key from a file tombkey=`option_value -k` fi else # guess key as lying besides the tomb tombkey=${tombdir}/${tombfile}.key fi if ! [ -r ${tombkey} ]; then _failure "key file not found: ${tombkey}" fi local tmp_resize=`safe_filename tmbrsz` local newtombsize=$opts[-s] local oldtombsize=`stat -c %s "${CMD2}" 2>/dev/null` local mounted_tomb=`mount -l | awk -vtomb="[$tombname]" '/^\/dev\/mapper\/tomb/ { if($7==tomb) print $1 }'` if [ "$mounted_tomb" ]; then _failure "the tomb $tombname is open, to resize it it needs to be close." fi # MB to bytes conversion newtombsize=`expr \( $newtombsize \* 1024 \) \* 1024 2> /dev/null` if ! [ "$newtombsize" ] ; then _failure "You must specify the new size of $tombname" elif [[ $newtombsize != <-> ]]; then _failure "Size is not an integer" elif [ "$newtombsize" -le "$oldtombsize" ]; then _failure "the new size must be greater then old tomb size." fi local delta=`expr $newtombsize \- $oldtombsize` local tombsize_4k=`expr $delta \/ 1024` tombsize_4k=`expr $tombsize_4k \/ 4 ` act "Generating ${tombfile} of ${newtombsize}Mb (${tombsize_4k} blocks of 4Kb)" "$DD" if=/dev/urandom bs=4k count=${tombsize_4k} of="${tmp_resize}" if [ $? = 0 -a -e "${tmp_resize}" ]; then xxx "OK: `ls -lh ${tmp_resize}`" else _failure "Error creating the extra resize $tmp_resize, operation aborted." fi cat "${tmp_resize}" >> "${CMD2}" ${=WIPE} "${tmp_resize}" local nstloop=`losetup -f` if [ $? = 255 ]; then _failure "too many tomb opened. Please close any of them to open another tomb" fi losetup -f "${CMD2}" local mapdate=`date +%s` local mapper="tomb.${tombname}.${mapdate}.`basename $nstloop`" _message "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 (gpg --batch --passphrase-fd 0 --no-tty --no-options \ -d "${tombkey}" 2> /dev/null <<< ${tombpass} ) \ | 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 losetup -d ${nstloop} _failure "failure mounting the encrypted file" fi cryptsetup resize "${mapper}" if [ $? != 0 ]; then losetup -d ${nstloop} _failure "cryptsetup failed to resize $mapper" fi e2fsck -f /dev/mapper/${mapper} if [ $? != 0 ]; then losetup -d ${nstloop} _failure "e2fsck failed to check $mapper" fi resize2fs /dev/mapper/${mapper} if [ $? != 0 ]; then losetup -d ${nstloop} _failure "resize2fs failed to resize $mapper" fi sleep 1 # needs to settle a bit # close and free the loop device cryptsetup luksClose "${mapper}" losetup -d ${nstloop} return 0 } # }}} # {{{ - List # list all tombs mounted in a readable format list_tombs() { if [ $CMD2 ]; then # list a specific tomb mounted_tombs=`mount -l | awk -vtomb="[$1]" '/^\/dev\/mapper\/tomb/ { if($7==tomb) print $1 ";" $3 ";" $5 ";" $6 ";" $7 }'` else # list all open tombs mounted_tombs=`mount -l | awk '/^\/dev\/mapper\/tomb/ { print $1 ";" $3 ";" $5 ";" $6 ";" $7 }'` fi # 1 = full mapper path # 2 = mountpont # 3 = filesystem # 4 = mount options # 5 = name if ! [ $mounted_tombs ]; then if [ $1 ]; then die "There seems to be no open tomb engraved as [$CMD2]" else die "I can't see any open tomb, may they all rest in peace." fi fi for t in ${(f)mounted_tombs}; do mapper=`basename ${t[(ws:;:)1]}` tombname=${t[(ws:;:)5]} tombmount=${t[(ws:;:)2]} tombfs=${t[(ws:;:)3]} tombfsopts=${t[(ws:;:)4]} # calculate tomb size ts=`df -hP /dev/mapper/$mapper | awk "/mapper/"' { print $2 ";" $3 ";" $4 ";" $5 }'` tombtot=${ts[(ws:;:)1]} tombused=${ts[(ws:;:)2]} tombavail=${ts[(ws:;:)3]} tombpercent=${ts[(ws:;:)4]} tombp=${tombpercent%%%} tombsince=`date --date=@${mapper[(ws:.:)3]} +%c` # breaking up such strings is good for translation print -n "$fg[green]$tombname" print -n "$fg[white] open on " print -n "$fg_bold[white]$tombmount" print -n "$fg_no_bold[white] using " print "$fg_bold[white]$tombfs $tombfsopts" print -n "$fg_no_bold[green]$tombname" print -n "$fg_no_bold[white] open since " print "$fg_bold[white]$tombsince$fg_no_bold[white]" print -n "$fg_no_bold[green]$tombname" print -n "$fg[white] size " print -n "$fg_bold[white]$tombtot" print -n "$fg_no_bold[white] of which " print -n "$fg_bold[white]$tombused" print -n "$fg_no_bold[white] used: " print -n "$fg_bold[white]$tombavail" print -n "$fg_no_bold[white] free (" print -n "$fg_bold[white]$tombpercent" print "$fg_no_bold[white] full)" if [[ ${tombp} -ge 90 ]]; then print -n "$fg_no_bold[green]$tombname" print "$fg_bold[red] Your tomb is almost full!" fi # now check hooks mtomb=`sed 's:\/:\\\/:g' <<< $tombmount` mounted_hooks=`mount | awk "/^$mtomb/"' {print $1 ";" $3}'` for h in ${(f)mounted_hooks}; do print -n "$fg_no_bold[green]$tombname" print -n "$fg_no_bold[white] hooks " print -n "$fg_bold[white]`basename ${h[(ws:;:)1]}`" print -n "$fg_no_bold[white] on " print "$fg_bold[white]${h[(ws:;:)2]}$fg_no_bold[white]" done done } # }}} # {{{ - Status launch_status() { # calculates the correct arguments to launch tomb-status tray # applet; it takes the tomb name as an argument and can be # launched after a successful tomb mount. which tomb-status > /dev/null if [ $? != 0 ]; then die "Cannot find tomb-status binary, operation aborted." return 1 fi if ! [ $DISPLAY ]; then _warning "No active X display found, operation aborted." _warning "Status launches a graphical tray applet, you need X running." return 1 fi # check argument. use single tomb mounted if no argument # abort if more than one tomb is mounted. if ! [ $1 ]; then tombs=`find /dev/mapper -name 'tomb.*'` how_many_tombs=`wc -w <<< "$tombs"` if [[ "$how_many_tombs" == "0" ]]; then _warning "There is no open tomb, status cannot be launched" return 1 elif [[ "$how_many_tombs" == "1" ]]; then #mapper=`find /dev/mapper -name 'tomb.*'` tombname=`find /dev/mapper -name "tomb.*"` tombname=`basename $tombname | cut -d. -f2` _success "launching status for tomb $tombname" else _warning "Too many tombs mounted, please specify which one" list_tombs return 0 fi else # name was specified on command line tombname=$1 ls /dev/mapper | grep "^tomb.${tombname}.*" > /dev/null if [ $? != 0 ]; then _warning "Cannot find any tomb named $tombname being open, operation aborted." return 1 fi fi # finally we launch tombmap=`mount -l | awk "/\[${tombname}\]$/"' { print $1 } '` tombmount=`mount -l | awk "/\[${tombname}\]$/"' { print $3 } '` tomb-status $tombmap $tombname $tombmount &! return 0 } # }}} # {{{ - Install GUI # 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 _message "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 _message "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 _message "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 _message "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 _message "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 _message "Tomb is now installed." } # }}} # }}} # {{{ OPTION PARSING # {{{ - Check an option option_is_set() { #First argument, the option (something like "-s") #Second (optional) argument: if it's "out", command will print it out 'set'/'unset' # This is useful for if conditions #Return 0 if is set, 1 otherwise [[ -n ${(k)opts[$1]} ]]; r=$? if [[ $2 == out ]]; then if [[ $r == 0 ]]; then echo 'set' else echo 'unset' fi fi return $r; } # }}} # {{{ - Get an option value option_value() { #First argument, the option (something like "-s") <<< ${opts[$1]} } # }}} # }}} # {{{ MAIN COMMAND main() { local -A subcommands_opts ### Options configuration # Hi, dear developer! Are you trying to add a new subcommand, or # to add some options? Well, keep in mind that an option CAN'T # have differente meanings/behaviour in different subcommands. # For example, "-s" means "size" and accept an argument. If you are tempted to add # an option "-s" (that means, for example "silent", and doesn't accept an argument) # DON'T DO IT! # There are two reasons for that: # I. usability; user expect that "-s" is "size" # II. Option parsing WILL EXPLODE if you do this kind of bad things # (it will say "option defined more than once, and he's right") # # If you want to use the same option in multiple commands then # you can only use the non-abbreviated long-option version like: # -force and NOT -f main_opts=(q -quiet=q D -debug=D h -help=h v -version=v -no-color) subcommands_opts[__default]="" subcommands_opts[open]="f n -nohook=n k: -key=k o: -mount-options=o" subcommands_opts[mount]=${subcommands_opts[open]} subcommands_opts[create]="f s: -size=s -force k: -key=k" subcommands_opts[passwd]="f" subcommands_opts[close]="" subcommands_opts[help]="" subcommands_opts[slam]="" subcommands_opts[list]="" subcommands_opts[help]="" subcommands_opts[bury]="" subcommands_opts[exhume]="" subcommands_opts[decompose]="" subcommands_opts[recompose]="" subcommands_opts[install]="" subcommands_opts[askpass]="" subcommands_opts[mktemp]="" subcommands_opts[source]="" subcommands_opts[status]="" subcommands_opts[resize]="s: -size=s k: -key=k" # subcommands_opts[translate]="" ### Detect subcommand local -aU every_opts #every_opts behave like a set; that is, an array with unique elements for optspec in $subcommands_opts$main_opts; do for opt in ${=optspec}; do every_opts+=${opt} done done local -a oldstar oldstar=($argv) zparseopts -M -E -D -Adiscardme ${every_opts} unset discardme subcommand=$1 if [[ -z $subcommand ]]; then subcommand="__default" fi if [[ -z ${(k)subcommands_opts[$subcommand]} ]]; then _warning "There's no such command \"$subcommand\"." _failure "Please try -h for help" 127 # die "Subcommand '$subcommand' doesn't exist" 127 fi argv=(${oldstar}) unset oldstar ### Parsing global + command-specific options # zsh magic: ${=string} will split to multiple arguments when spaces occur set -A cmd_opts ${main_opts} ${=subcommands_opts[$subcommand]} # if there is no option, we don't need parsing if [[ -n $cmd_opts ]]; then zparseopts -M -E -D -Aopts ${cmd_opts} if [[ $? != 0 ]]; then _warning "Some error occurred during option processing." die "See \"tomb help\" for more info" 127 fi fi #build PARAM (array of arguments) and check if there are unrecognized options ok=0 PARAM=() for arg in $*; do if [[ $arg == '--' || $arg == '-' ]]; then ok=1 continue #it shouldnt be appended to PARAM elif [[ $arg[1] == '-' ]]; then if [[ $ok == 0 ]]; then die "unrecognized option $arg" 127 fi fi PARAM+=$arg done #first parameter actually is the subcommand: delete it and shift if [[ $subcommand != '__default' ]]; then PARAM[1]=() shift fi ### End parsing command-specific options if ! option_is_set --no-color; then autoload colors; colors fi CMD=$subcommand CMD2=$PARAM[1] CMD3=$PARAM[2] xxx "Tomb command: $CMD $CMD2 $CMD3" case "$subcommand" in create) check_priv create_tomb ;; mount|open) check_priv mount_tomb ;; umount|close|slam) check_priv [ "$subcommand" = "slam" ] && SLAM=1 umount_tomb ${CMD2} ;; passwd) check_priv change_passwd ${CMD2} ;; list) list_tombs ${CMD2} ;; status) launch_status ${CMD2} ;; help) usage ;; bury) if [ "$STEGHIDE" = 0 ]; then _warning "steghide not installed. Cannot bury your key" return 1 fi encode_key ${CMD2} ${CMD3} ;; exhume) if [ "$STEGHIDE" = 0 ]; then _warning "steghide not installed. Cannot exhume your key" return 1 fi decode_key ${CMD2} ;; resize) check_priv resize_tomb ;; # internal commands useful to developers 'source') return 0 ;; install) check_priv ; install_tomb ;; askpass) ask_password ${CMD2} ${CMD3} ;; mktemp) safe_dir ${CMD2} ;; translate) generate_translatable_strings ;; __default) cat < GnuPG available ciphers: `list_gnupg_ciphers` EOF option_is_set -v && return 0 usage ;; *) _warning "command \"$CMD\" not recognized" _message "try -h for help" return 1 ;; esac return $? } # }}} # {{{ RUNTIME check_bin main $@ if [[ $? != 0 ]]; then #this "if" seems useless, but avoid source tomb source from exiting exit $? fi # }}}