#!/bin/zsh # # Tomb, the Crypto Undertaker # # A commandline tool to easily operate encryption of secret data # # Homepage on: [tomb.dyne.org](http://tomb.dyne.org) # {{{ License # Copyright (C) 2007-2013 Dyne.org Foundation # # Tomb is designed, written and maintained by Denis Roio # # With contributions by Anathema, Boyska and Hellekin O. Wolf. # # Testing and reviews are contributed by Dreamer, Shining, # Mancausoft, Asbesto Molesto and Nignux. # # Tomb's artwork is contributed by Jordi aka Mon Mort. #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. # }}} - License # {{{ Global variables VERSION=1.3.1 DATE="Jun/2013" TOMBEXEC=$0 typeset -a OLDARGS for arg in ${argv}; do OLDARGS+=($arg); done DD="dd" WIPE="rm -f" MKFS="mkfs.ext3 -q -F -j -L" KDF=1 STEGHIDE=1 MKTEMP=1 RESIZER=1 SWISH=1 QRENCODE=1 MOUNTOPTS="rw,noatime,nodev" typeset -A global_opts typeset -A opts typeset -h username typeset -h tombkeydir # global used if key comes from stdin tombkeydir="" typeset -h _uid typeset -h _gid typeset -h _tty # Set a sensible PATH # PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin # }}} # {{{ Safety functions _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 } # Create temporary directories with caution 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 $1 dir in RAM" if (( $MKTEMP )); then mktemp -d /dev/shm/tomb.$1.$$.XXXXXXX else dir="/dev/shm/tomb.$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/tomb.$1.$$.XXXXXXX || \ print "/dev/shm/tomb.$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 ask_password() { # we use pinentry now # comes from gpg project and is much more secure # it also conveniently uses the right toolkit # 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 output=`cat </dev/null | tail -n +7 OPTION ttyname=$TTY OPTION lc-ctype=$LANG SETTITLE $title SETDESC $1 SETPROMPT Password: GETPIN EOF` if [[ `tail -n1 <<<$output` =~ ERR ]]; then return 1 fi head -n1 <<<$output | awk '/^D / { sub(/^D /, ""); print }' return 0 } # 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() { # save original user username=$USER if [ $UID != 0 ]; then xxx "Using sudo for root execution of '${TOMBEXEC} ${(f)OLDARGS}'" # check if sudo has a timestamp active sudok=false if ! option_is_set --sudo-pwd; then 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 else _verbose "Escalating privileges using sudo-pwd" sudo -S -v <<<`option_value --sudo-pwd` fi sudo "${TOMBEXEC}" -U ${UID} -G ${GID} -T ${TTY} "${(@)OLDARGS}" exit $? fi # are we root already # make sure necessary kernel modules are loaded modprobe dm_mod 2>/dev/null modprobe dm_crypt 2>/dev/null return 0 } # check if a filename is a valid tomb is_valid_tomb() { xxx "is_valid_tomb $1" # argument check { test "$1" = "" } && { _warning "Tomb file is missing from arguments"; return 1 } # file checks { test -r "$1" } || { _warning "Tomb file not found: $1"; return 1 } { test -f "$1" } || { _warning "Tomb file is not a regular file: $1"; return 1 } # check file type (if its a Luks fs) file "$1" | grep -i 'luks encrypted file' >/dev/null { test $? = 0 } || { _warning "File is not a valid tomb: $1"; return 1 } # check if its already open tombfile=`basename $1` tombname=${tombfile%%\.*} mount -l | grep "${tombfile}.*\[$tombname\]$" > /dev/null { test $? = 0 } && { _warning "Tomb is currently in use: $tombname"; return 1 } _message "Valid tomb file found: $1" return 0 } # }}} # {{{ Commandline interaction usage() { cat <. EOF } # Check an option option_is_set() { # First argument, the commandline flag (i.e. "-s"). # Second (optional) argument: if "out", command will print it out 'set'/'unset' # (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 commandline flag (i.e. "-s"). <<< ${opts[$1]} } # Messaging function with pretty coloring function _msg() { local command="print -P" local progname="$fg[magenta]${TOMBEXEC##*/}$reset_color" local message="$fg_bold[normal]$fg_no_bold[normal]${2}$reset_color" local -i returncode case "$1" in inline) command+=" -n"; pchars=" > "; pcolor="yellow" ;; message) pchars=" . "; pcolor="white"; message="$fg_no_bold[$pcolor]${2}$reset_color" ;; verbose) pchars="[D]"; pcolor="blue" ;; success) pchars="(*)"; pcolor="green"; message="$fg_no_bold[$pcolor]${2}$reset_color" ;; warning) pchars="[W]"; pcolor="yellow"; message="$fg_no_bold[$pcolor]${2}$reset_color" ;; failure) pchars="[E]"; pcolor="red"; message="$fg_no_bold[$pcolor]${2}$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$reset_color ${message}$color[reset_color]" >&2 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 } # Print out progress to inform GUI caller applications (--batch mode) progress() { # $1 is "what is progressing" # $2 is "percentage" # $3 is (eventually blank) status # Example: if creating a tomb, it could be sth like # progress create 0 filling with random data # progress create 40 generating key # progress keygen 0 please move the mouse # progress keygen 30 please move the mouse # progress keygen 60 please move the mouse # progress keygen 100 key generated # progress create 80 please enter password # progress create 90 formatting the tomb # progress create 100 tomb created successfully if ! option_is_set --batch; then return fi print "[m][P][$1][$2][$3]" >&2 } # Check what's installed check_bin() { # check for required programs for req in cryptsetup pinentry sudo gpg; do command -v $req >/dev/null || die "Cannot find $req. It's a requirement to use Tomb, please install it." 1 done export PATH=/sbin:/usr/sbin:$PATH # which dd command to use command -v dcfldd > /dev/null { test $? = 0 } && { DD="dcfldd statusinterval=1" } # which wipe command to use command -v wipe > /dev/null && WIPE="wipe -f -s" || WIPE="rm -f" # check for filesystem creation progs command -v mkfs.ext4 > /dev/null && \ MKFS="mkfs.ext4 -q -F -j -L" || \ MKFS="mkfs.ext3 -q -F -j -L" # check for mktemp command -v mktemp > /dev/null || MKTEMP=0 # check for steghide command -v steghide > /dev/null || STEGHIDE=0 # check for resize command -v e2fsck resize2fs > /dev/null || RESIZER=0 # check for KDF auxiliary tools command -v tomb-kdb-pbkdf2 > /dev/null || KDF=0 # check for Swish-E file content indexer command -v swish-e > /dev/null || SWISH=0 # check for QREncode for paper backups of keys command -v qrencode > /dev/null || QRENCODE=0 } # }}} - Commandline interaction # {{{ Key operations # This function retrieves a tomb key specified on commandline or one # laying nearby the tomb if found, or from stdin if the option was # selected. It also runs validity checks on the file. Callers should # always use drop_key() when done with all key operations. # On success returns 0 and prints out the full path to the key load_key() { # take the name of a tomb file as argument # this is used for guessing if the key is nearby { test "$1" = "" } || { tombdir=`dirname $1` tombfile=`basename $1` tombname=${tombfile%%\.*} } if option_is_set -k ; then if [[ "`option_value -k`" == "-" ]]; then xxx "load_key reading from stdin" # take key from stdin tombkeydir=`safe_dir load_key` xxx "tempdir is $tombkeydir" cat > ${tombkeydir}/stdin.tmp tombdir=${tombkeydir} tombfile=stdin.tmp tombname="stdin" elif [[ "`option_value -k`" != "" ]]; then # take key from a file tombkey=`option_value -k` tombdir=`dirname $tombkey` tombfile=`basename $tombkey` fi fi tombkey=${tombdir}/${tombfile} xxx "load_key: `ls -lh ${tombkey}`" if [ -r "${tombkey}" ]; then _message "We'll use this key: ${tombkey}" else return 1 fi # this does a check on the file header if ! is_valid_key ${tombkey}; then _warning "The key seems invalid, the application/pgp header is missing" return 1 fi print "$tombkey" return 0 } # This function asks the user for the password to use the key it tests # it against the return code of gpg on success returns 0 and prints # the password (be careful about where you save it!) ask_key_password() { tombkey="$1" local keyname=`basename $tombkey` _message "a password is required to use key ${keyname}" local passok=0 local tombpass="" if option_is_set --tomb-pwd; then tombpass=`option_value --tomb-pwd` xxx "ask_key_password takes tombpass from CLI argument: $tombpass" get_lukskey "$tombpass" ${tombkey} >/dev/null if [ $? = 0 ]; then passok=1; _message "Password OK."; fi else for c in 1 2 3; do if [ $c = 1 ]; then tombpass=`exec_as_user ${TOMBEXEC} askpass "Insert password to use key: $keyname"` else tombpass=`exec_as_user ${TOMBEXEC} askpass "Insert password to use key: $keyname (retry $c)"` fi if [[ $? != 0 ]]; then _warning "User aborted password dialog" return 1 fi get_lukskey "$tombpass" ${tombkey} >/dev/null if [ $? = 0 ]; then passok=1; _message "Password OK." break; fi done fi { test "$passok" = "1" } || { return 1 } print "$tombpass" unset $tombpass return 0 } # change tomb key password change_passwd() { _message "Commanded to change password for tomb key $1" if ! option_is_set -f && ! option_is_set --ignore-swap; then check_swap; fi local keyfile="$1" # $1 is the tomb key path # check the keyfile if ! [ -r $keyfile ]; then _warning "key not found: $keyfile" return 1 fi if ! is_valid_key $keyfile ; then _warning "file doesn't seems to be a tomb key: $keyfile" _warning "operation aborted." return 1 fi local tmpnewkey lukskey c tombpass tombpasstmp tmpnewkey=`safe_filename passnew` lukskey=`safe_filename passold` _success "Changing password for $keyfile" tombpass=`ask_key_password $keyfile` { test $? = 0 } || { die "No valid password supplied" } get_lukskey "${tombpass}" ${keyfile} > ${lukskey}; drop_key { local algo { option_is_set -o } && { algopt="`option_value -o`" } gen_key $lukskey $algopt > ${tmpnewkey} if ! is_valid_key $tmpnewkey; then die "Error: the newly generated keyfile does not seem valid" else # copy the new key as the original keyfile name cp "${tmpnewkey}" "${keyfile}" _success "Your passphrase was successfully updated." fi } always { _verbose "cleanup: $tmpnewkey $lukskey" # wipe all temp file ${=WIPE} "${tmpnewkey}" ${=WIPE} "${lukskey}" } return $? } # To be called after load_key() drop_key() { { test "$tombkeydir" = "" } && { return 0 } { test -r ${tombkeydir}/stdin.tmp } && { ${=WIPE} ${tombkeydir}/stdin.tmp; rmdir ${tombkeydir} } } #$1 is the keyfile we are checking is_valid_key() { xxx "is_valid_key $1" # argument check { test "$1" = "" } && { _warning "Key file is missing from arguments"; return 1 } # file checks { test -r "$1" } || { _warning "Key file not found: $1"; return 1 } { test -f "$1" } || { _warning "Key file is not a regular file: $1"; return 1 } # this header validity check is a virtuosism by Hellekin [[ `file =(awk '/^-+BEGIN/,0' $1)` =~ PGP ]] && { _message "Valid key file found: $1"; return 0 } # if no BEGIN header found then we try to recover it [[ `file $1 -bi` =~ text/plain ]] && { _warning "Key data found with missing headers, attempting recovery" local tmp_keyfix=`safe_filename keyfix` touch $tmp_keyfix # make sure KDF header comes first local header=`grep '^_KDF_' $1` print "$header" >> $tmp_keyfix cat $1 | awk ' BEGIN { print "-----BEGIN PGP MESSAGE-----" print } /^_KDF_/ { next } { print $0 } END { print "-----END PGP MESSAGE-----" }' >> ${tmp_keyfix} mv $tmp_keyfix $1 chown ${_uid}:${_gid} ${1} chmod 0600 ${1} return 0 } _warning "Invalid key format: $1" return 1 } # Gets a key file and a password, prints out the decoded contents to # be used directly by Luks as a cryptographic key get_lukskey() { # $1 is the password, $2 is the keyfile local tombpass=$1 local keyfile=$2 firstline=`head -n1 $keyfile` xxx "get_lukskey XXX $keyfile" if [[ $firstline =~ '^_KDF_' ]]; then _verbose "KDF: `cut -d_ -f 3 <<<$firstline`" case `cut -d_ -f 3 <<<$firstline` in pbkdf2sha1) pbkdf2_param=`cut -d_ -f 4- <<<$firstline | tr '_' ' '` tombpass=$(tomb-kdb-pbkdf2 ${=pbkdf2_param} 2> /dev/null <<<$tombpass) ;; *) _failure "No suitable program for KDF `cut -f 3 <<<$firstline`" unset tombpass return 1 ;; esac fi # fix for gpg 1.4.11 where the --status-* options don't work ;^/ gpgver=`gpg --version | awk '/^gpg/ {print $3}'` if [ "$gpgver" = "1.4.11" ]; then xxx "GnuPG is version 1.4.11 - adopting status fix" print ${tombpass} | \ gpg --batch --passphrase-fd 0 --no-tty --no-options -d "${keyfile}" ret=$? unset tombpass else # using status-file in gpg != 1.4.11 res=`safe_filename lukskey` { test $? = 0 } || { unset tombpass; die "Fatal error creating temp file." } print ${tombpass} | \ gpg --batch --passphrase-fd 0 --no-tty --no-options --status-fd 2 \ --no-mdc-warning --no-permission-warning --no-secmem-warning \ -d "${keyfile}" 2> $res unset tombpass grep 'DECRYPTION_OKAY' $res > /dev/null ret=$?; rm -f $res fi xxx "get_lukskey returns $ret" return $ret } # takes care to encrypt a key # honored options: --kdf --tomb-pwd gen_key() { # $1 the lukskey to encrypt # $2 is the --cipher-algo to use (string taken by GnuPG) local lukskey="$1" local algo="${2:-AES256}" # here user is prompted for key password local tombpass="" local tombpasstmp="" if ! option_is_set --tomb-pwd; then while true; do # 3 tries to write two times a matching password tombpass=`exec_as_user ${TOMBEXEC} askpass "Secure key for ${tombname}"` if [[ $? != 0 ]]; then die "User aborted" fi if [ -z $tombpass ]; then _warning "you set empty password, which is not possible" continue fi tombpasstmp=$tombpass tombpass=`exec_as_user ${TOMBEXEC} askpass "Secure key for ${tombname} (again)"` if [[ $? != 0 ]]; then die "User aborted" fi if [ "$tombpasstmp" = "$tombpass" ]; then break; fi unset tombpasstmp unset tombpass done else tombpass="`option_value --tomb-pwd`" xxx "gen_key takes tombpass from CLI argument: $tombpass" fi header="" { option_is_set --kdf } && { # KDF is a new key strenghtening technique against brute forcing # see: https://github.com/dyne/Tomb/issues/82 itertime="`option_value --kdf`" _verbose "KDF itertime chosen: $itertime" # --kdf takes one parameter: iter time (on present machine) in seconds local -i microseconds microseconds=$((itertime*1000000)) _verbose "Microseconds: $microseconds" pbkdf2_salt=`tomb-kdb-pbkdf2-gensalt` pbkdf2_iter=`tomb-kdb-pbkdf2-getiter $microseconds` # We use a length of 64bytes = 512bits (more than needed!?) tombpass=`tomb-kdb-pbkdf2 $pbkdf2_salt $pbkdf2_iter 64 <<<"${tombpass}"` header="_KDF_pbkdf2sha1_${pbkdf2_salt}_${pbkdf2_iter}_64\n" } print -n $header print "${tombpass}" \ | gpg --openpgp --force-mdc --cipher-algo ${algo} \ --batch --no-options --no-tty --passphrase-fd 0 --status-fd 2 \ -o - -c -a ${lukskey} unset tombpass } # prints an array of ciphers available in gnupg (to encrypt keys) list_gnupg_ciphers() { # prints an error if GnuPG is not found which gpg > /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 } # Steganographic function to bury a key inside an image. # Requires steghide(1) to be installed bury_key() { tombkey="`option_value -k`" imagefile=$1 { is_valid_key ${tombkey} } || { die "Bury failed: not a tomb key $tombkey" } 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 confirm the key password for the encoding" tombpass=`ask_key_password $tombkey` { test $? = 0 } || { _warning "Wrong password supplied." die "You shall not bury a key whose password is unknown to you." } # we omit armor strings since having them as constants can give # ground to effective attacks on steganography 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 } # Steganographic function to exhume a key buries into an image exhume_key() { tombkey="`option_value -k`" imagefile=$1 res=1 file $imagefile | grep JPEG > /dev/null if [ $? != 0 ]; then _warning "encode failed: $imagefile is not a jpeg image" return 1 fi if [[ -e "$tombkey" ]]; then _warning "File exists: $tombkey" { option_is_set -f } || { _warning "Make explicit use of --force to overwrite" die "Refusing to overwrite file. Operation aborted." } _warning "Use of --force selected: overwriting." rm -f ${tombkey} fi _message "Trying to exhume a key out of image $imagefile" if option_is_set --tomb-pwd; then tombpass=`option_value --tomb-pwd` xxx "ask_key_password takes tombpass from CLI argument: $tombpass" else tombpass=`exec_as_user ${TOMBEXEC} askpass "Steg password for ${tombkey}"` fi # always steghide required steghide extract -sf ${imagefile} -p ${tombpass} -xf ${tombkey} res=$? unset tombpass if [ $res = 0 ]; then _success "${tombkey} succesfully decoded" return 0 fi _warning "nothing found in $imagefile" return 1 } # Produces a printable image of the key contents so that it can be # backuped on paper and hidden in books etc. backup_key() { # load key from options tombkey="`load_key $1`" { test $? = 0 } || { die "No key specified." } keyname=`basename $tombkey` pngname="$keyname.qr.png" yes "Rendering a printable QRCode for key: $tombkey" # we omit armor strings to save space awk ' /^-----/ {next} /^Version/ {next} {print $0}' ${tombkey} | qrencode --size 4 -t PNG --level H \ --casesensitive -o "$pngname" { test $? = 0 } || { die "QREncode reported an error." } yes "Operation successful:" _message "`ls -lh $pngname`" _message "`file $pngname`" } # }}} - Key handling # {{{ Create # This is a new way to create tombs which dissects the whole create_tomb() into 3 clear steps: # # * dig a .tomb (the large file) using /dev/random (takes some minutes at least) # # * forge a .key (the small file) using /dev/urandom (good entropy needed) # # * lock the .tomb file with the key, binding the key to the tomb (requires dm_crypt format) forge_key() { xxx "forge_key()" # can be specified both as simple argument or using -k local destkey="$1" { option_is_set -k } && { destkey="`option_value -k`" } { test "$destkey" = "" } && { _warning "no key name specified for creation" return 1 } { test -r "$destkey" } && { _warning "Forging this key would overwrite an existing file. Operation aborted." die "`ls -lh $destkey`" } # if swap is on, we remind the user about possible data leaks to disk if ! option_is_set -f && ! option_is_set --ignore-swap; then check_swap; fi # create the keyfile in tmpfs so that we leave less traces in RAM local keytmp=`safe_dir forge` (( $? )) && die "error creating temp dir" xxx "safe_dir at $keytmp" mount tmpfs "${keytmp}" -t tmpfs -o size=1m if [ $? != 0 ]; then _warning "cannot mount tmpfs filesystem in volatile memory" rm -r "${keytmp}" die "operation aborted." fi local algo { option_is_set -o } && { algopt="`option_value -o`" } algo=${algopt:-AES256} _message "Commanded to forge key $destkey with cipher algorithm $algo" local tombkey="$destkey" _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." _message "If you are on a server, you can use an Entropy Generation Daemon." touch ${keytmp}/tomb.tmp chmod 0600 ${keytmp}/tomb.tmp local random_source=/dev/random if option_is_set --use-urandom; then random_source=/dev/urandom fi xxx "Data dump using ${DD[1]} from $random_source" ${=DD} bs=1 count=256 if=$random_source of=${keytmp}/tomb.tmp if ! [ -r ${keytmp}/tomb.tmp ]; then _warning "cannot generate encryption key" umount ${keytmp} rm -r $keytmp die "operation aborted." fi _success "Choose the password of your key: ${tombkey}" _message "(you can also change it later using 'tomb passwd')" touch ${tombkey} chown ${_uid}:${_gid} ${tombkey} chmod 0600 ${tombkey} tombname="$tombkey" # the gen_key() function takes care of the new key's encryption gen_key "${keytmp}/tomb.tmp" "$algo" > ${tombkey} # this does a check on the file header if ! is_valid_key ${tombkey}; then _warning "The key does not seem to be valid" _warning "Dumping contents to screen:" cat ${tombkey} _warning "--" umount ${keytmp} rm -r $keytmp die "operation aborted." fi ${=WIPE} ${keytmp}/tomb.tmp # no need really, but anyway umount ${keytmp} rm -r ${keytmp} chown ${_uid}:${_gid} ${tombkey} _message "done forging $tombkey" _success "Your key is ready:" ls -lh ${tombkey} } # Dig a tomb, means that it will create an empty file to be formatted # as a loopback filesystem. Initially the file is filled with random data # taken from /dev/urandom which improves the tomb's overall security dig_tomb() { _message "Commanded to dig tomb $1" # if swap is on, we remind the user about possible data leaks to disk if ! option_is_set -f && ! option_is_set --ignore-swap; then check_swap; fi if ! [ $1 ]; then _warning "no tomb name specified for creation" return 1 fi tombfile=`basename $1` tombdir=`dirname $1` # make sure the file has a .tomb extension tombname=${tombfile%%\.*} tombfile=${tombname}.tomb # require the specification of the size of the tomb (-s) in MB tombsize="`option_value -s`" [ $tombsize ] || die "Size argument missing, use --size" [[ $tombsize != <-> ]] && die "Size argument is not an integer" [[ $tombsize -lt 10 ]] && die "Tombs can't be smaller than 10 megabytes" if [ -e ${tombdir}/${tombfile} ]; then _warning "A tomb exists already. I'm not digging here:" _warning " `ls -lh ${tombdir}/${tombfile}`" return 1 fi _success "Creating a new tomb in ${tombdir}/${tombfile}" _message "Generating ${tombfile} of ${tombsize}MiB" # we will first touch the file and set permissions: this way, even if interrupted, permissions are right touch ${tombdir}/${tombfile} chmod 0600 "${tombdir}/${tombfile}" chown $_uid:$_gid "${tombdir}/${tombfile}" xxx "Data dump using ${DD[1]} from /dev/urandom" ${=DD} if=/dev/urandom bs=1048576 count=${tombsize} of=${tombdir}/${tombfile} if [ $? = 0 -a -e ${tombdir}/${tombfile} ]; then _message " `ls -lh ${tombdir}/${tombfile}`" else die "Error creating the tomb ${tombdir}/${tombfile}, operation aborted." fi _success "Done digging $tombname" _message "your tomb is not yet ready, you need to forge a key and lock it:" _message "tomb forge ${tombname}.tomb.key" _message "tomb lock ${tombname}.tomb ${tombname}.tomb.key" } # this function locks a tomb with a key file # in fact LUKS formatting the loopback volume # it take arguments as the LUKS cipher to be used lock_tomb_with_key() { if ! [ $1 ]; then _warning "no tomb specified for locking" _warning "usage: tomb lock file.tomb file.tomb.key" return 1 fi tombfile="$1" _message "Commanded to lock tomb ${tombfile}" tombdir=`dirname $1` tombfile=`basename $1` tombname=${tombfile%%\.*} { test -f ${tombdir}/${tombfile} } || { die "There is no tomb here. You have to it dig first." return 1 } xxx "tomb found: ${tombdir}/${tombfile}" nstloop=`losetup -f` # get the number for next loopback device losetup -f ${tombdir}/${tombfile} # allocates the next loopback for our file xxx "loop mounted on ${nstloop}" _message "checking if the tomb is empty (we never step on somebody else's bones)" cryptsetup isLuks ${nstloop} if [ $? = 0 ]; then # is it a LUKS encrypted nest? then bail out and avoid reformatting it _warning "The tomb was already locked with another key" losetup -d ${nstloop} die "Operation aborted. I cannot lock an already locked tomb. Go dig a new one." else _message "fine, this tomb seems empty." fi # load key from options or file tombkey=`load_key ${tombdir}/${tombfile}.key` { test $? = 0 } || { losetup -d $nstloop die "Aborting operations: error loading key $tombkey" } # make sure to call drop_key later # the encryption cipher for a tomb can be set when locking using -o if option_is_set -o; then cipher="`option_value -o`" else cipher="aes-cbc-essiv:sha256" fi _message "locking using cipher: $cipher" # get the pass from the user and check it tombpass=`ask_key_password "$tombkey"` { test $? = 0 } || { losetup -d ${nstloop} die "No valid password supplied" } _success "Locking ${tombfile} with ${tombkey}" _message "formatting Luks mapped device" get_lukskey "${tombpass}" ${tombkey} | \ cryptsetup --key-file - --batch-mode \ --cipher ${cipher} --key-size 256 --key-slot 0 \ luksFormat ${nstloop} if ! [ $? = 0 ]; then _warning "cryptsetup luksFormat returned an error" unset tombpass losetup -d $nstloop die "Operation aborted." fi get_lukskey "${tombpass}" ${tombkey} | \ cryptsetup --key-file - \ --cipher ${cipher} luksOpen ${nstloop} tomb.tmp if ! [ $? = 0 ]; then _warning "cryptsetup luksOpen returned an error" unset tombpass losetup -d $nstloop die "Operation aborted." fi # cleanup tombs unset tombpass drop_key # make sure all temp files are out _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} _message "done locking $tombname using Luks dm-crypt ${create_cipher}" _success "Your tomb is ready in ${tombdir}/${tombfile} and secured with key ${tombkey}" } # This function changes the key that locks a tomb change_tomb_key() { if ! option_is_set -f && ! option_is_set --ignore-swap; then check_swap; fi { option_is_set -k } || { die "Specify the new key with -k" } newkey="`option_value -k`" { is_valid_key "$newkey" } || { die "New key invalid. Check your usage of the --key option." } oldkey="$1" { is_valid_key "$oldkey" } || { die "Old key invalid. Check your usage of the first argument." } { is_valid_tomb "$2" } || { die "Specify the name of a tomb as second argument" } nstloop=`losetup -f` { test $? = 255 } && { die "Too many tombs are open. Please close any of them to proceed." } losetup -f "$2" cryptsetup isLuks ${nstloop} # is it a LUKS encrypted nest? we check one more timesee cryptsetup(1) { test $? = 0 } || { losetup -d "$nstloop" die "Not a valid LUKS encrypted volume: $2" } # we have everything, prepare to mount yes "Changing lock on tomb $tombname" _message "old key: $oldkey" _message "new key: $newkey" # render the mapper mapdate=`date +%s` # save date of mount in minutes since 1970 mapper="tomb.${tombname}.${mapdate}.`basename $nstloop`" # load the new key from the -k option tombkey=`load_key` { test $? = 0 } || { die "Aborting operations: error loading new key $tombkey" } newkeypass=`ask_key_password $tombkey` { test $? = 0 } || { die "No valid password supplied for the new key" } newkeyfile="`safe_filename newkey`" get_lukskey "$newkeypass" "$newkey" > $newkeyfile # load the old key oldkeypass="`ask_key_password $oldkey`" { test $? = 0 } || { die "No valid password supplied for the old key" } # luksOpen the tomb (not really mounting, just on the loopback) get_lukskey "$oldkeypass" "$oldkey" | \ cryptsetup --key-file - luksOpen ${nstloop} ${mapper} { test $? = 0 } || { losetup -d "$nstloop" die "Unexpected error in luksOpen." } get_lukskey "$oldkeypass" "$oldkey" | \ cryptsetup --key-file - luksChangeKey "$nstloop" "$newkeyfile" { test $? = 0 } || { losetup -d "$nstloop" die "Unexpected error in luksChangeKey." } cryptsetup luksClose "${mapper}" { test $? = 0 } || { losetup -d "$nstloop" die "Unexpected error in luksClose." } drop_key unset tombpass ${=WIPE} "$newkeyfile" losetup -d ${nstloop} yes "Succesfully changed key for tomb: $2" _message "The new key is: $newkey" return 0 } # backward compatibility create_tomb() { xxx "create_tomb(): ${=@} ${=OLDARGS}" if ! [ $1 ]; then _warning "no tomb name specified for creation" return 1 fi { test -r "$1" } && { _warning "Creating this tomb would overwrite an existing file. Operation aborted." die "`ls -lh $1`" } { test $? = 0 } || { die "Failed to dig tomb, operation aborted." } tombfile=`basename $1` tombdir=`dirname $1` # make sure the file has a .tomb extension tombname=${tombfile%%\.*} tombfile=${tombname}.tomb ${TOMBEXEC} dig ${=PARAM} ${TOMBEXEC} forge ${tombdir}/${tombfile}.key { test $? = 0 } || { die "Failed to forge key, operation aborted." } ${TOMBEXEC} lock ${tombdir}/${tombfile} -k ${tombdir}/${tombfile}.key { test $? = 0 } || { die "Failed to lock tomb with key, operation aborted." } yes "Tomb $tombname succesfully created" ls -l ${tombfile}* } # }}} - Creation # {{{ Open # $1 = tombfile $2(optional) = mountpoint mount_tomb() { _message "Commanded to open tomb $1" if ! option_is_set -f && ! option_is_set --ignore-swap; then check_swap; fi if ! [ ${1} ]; 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 ${1}` tombdir=`dirname ${1}` # check file type (if its a Luks fs) file ${tombdir}/${tombfile} | grep -i 'luks encrypted file' 2>&1 >/dev/null if [ $? != 0 ]; then _warning "$1 is not a valid tomb file, operation aborted" return 1 fi tombname=${tombfile%%\.*} xxx "tomb found: ${tombdir}/${tombfile}" # load_key called here tombkey=`load_key ${tombdir}/${tombfile}.key` { test $? = 0 } || { die "Aborting operations: error loading key $tombkey" } if [ "$2" = "" ]; then tombmount=/media/${tombfile} _message "mountpoint not specified, using default: $tombmount" else tombmount=$2 fi # check if its already open mount -l | grep "${tombfile}.*\[$tombname\]$" 2>&1 > /dev/null if [ $? = 0 ]; then _warning "$tombname is already open." _message "here below its status is reported:" list_tombs ${tombname} return 0 fi _success "Opening $tombfile on $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} 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" losetup -d ${nstloop} return 1 fi say "this tomb is a valid LUKS encrypted device" luksdump="`cryptsetup luksDump ${nstloop}`" tombdump=(`print $luksdump | awk ' /^Cipher name/ {print $3} /^Cipher mode/ {print $3} /^Hash spec/ {print $3}'`) say "cipher is \"$tombdump[1]\" mode \"$tombdump[2]\" hash \"$tombdump[3]\"" slotwarn=`print $luksdump | awk ' BEGIN { zero=0 } /^Key slot 0/ { zero=1 } /^Key slot.*ENABLED/ { if(zero==1) print "WARN" }'` { test "$slotwarn" = "WARN" } && { _warning "Multiple key slots are enabled on this tomb. Beware: there can be a backdoor." } # save date of mount in minutes since 1970 mapdate=`date +%s` mapper="tomb.${tombname}.${mapdate}.`basename $nstloop`" keyname=`basename $tombkey | cut -d. -f1` tombpass=`ask_key_password $tombkey` { test $? = 0 } || { losetup -d ${nstloop} die "No valid password supplied" } get_lukskey "${tombpass}" ${tombkey} | \ cryptsetup --key-file - luksOpen ${nstloop} ${mapper} # key dropped here drop_key unset tombpass if ! [ -r /dev/mapper/${mapper} ]; then losetup -d ${nstloop} 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 # we need root from here on mkdir -p $tombmount mount -o $MOUNTOPTS /dev/mapper/${mapper} ${tombmount} chown ${_uid}:${_gid} ${tombmount} chmod 0750 ${tombmount} _success "Success opening $tombfile on $fg_bold[white]$tombmount$fg_no_bold[white]" # print out when was opened the last time, by whom and where { test -r ${tombmount}/.last } && { tombtty="`cat ${tombmount}/.tty`" tombhost="`cat ${tombmount}/.host`" tombuid="`cat ${tombmount}/.uid`" tomblast="`cat ${tombmount}/.last`" tombuser=`awk -F: '/:'"$tombuid"':/ {print $1}' /etc/passwd` say "last visit by $fg_bold[white]$tombuser($tombuid)$fg_no_bold[white] from $fg_bold[white]$tombtty$fg_no_bold[white] on $fg_bold[white]$tombhost$fg_no_bold[white]" say "on date $fg_bold[white]`date --date @${tomblast} +%c`$fg_no_bold[white]" } # write down the UID and TTY that opened the tomb rm -f ${tombmount}/.uid echo ${_uid} > ${tombmount}/.uid rm -f ${tombmount}/.tty echo ${_tty} > ${tombmount}/.tty # also the hostname rm -f ${tombmount}/.host echo `hostname` > ${tombmount}/.host # and the "last time opened" information # in minutes since 1970, this is printed at next open rm -f ${tombmount}/.last echo "`date +%s`" > ${tombmount}/.last # human readable: date --date=@"`cat .last`" +%c # process bind-hooks (mount -o bind of directories) # and post-hooks (execute on open) if ! option_is_set -n ; then exec_safe_bind_hooks ${tombmount} exec_safe_post_hooks ${tombmount} open fi return 0 } # ## Hooks execution 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 open # {{{ List # list all tombs mounted in a readable format # $1 is optional, to specify a tomb list_tombs() { # list all open tombs mounted_tombs=(`list_tomb_mounts $1`) { test ${#mounted_tombs} = 0 } && { die "I can't see any ${1:-open} tomb, may they all rest in peace." } for t in ${mounted_tombs}; do mapper=`basename ${t[(ws:;:)1]}` tombname=${t[(ws:;:)5]} tombmount=${t[(ws:;:)2]} tombfs=${t[(ws:;:)3]} tombfsopts=${t[(ws:;:)4]} tombloop=${mapper[(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` # find out who opens it from where { test -r ${tombmount}/.tty } && { tombtty="`cat ${tombmount}/.tty`" tombhost="`cat ${tombmount}/.host`" tombuid="`cat ${tombmount}/.uid`" tombuser=`awk -F: '/:'"$tombuid"':/ {print $1}' /etc/passwd` } if option_is_set --get-mountpoint; then echo $tombmount continue fi # 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]" { test "$tombtty" = "" } || { print -n "$fg_no_bold[green]$tombname" print -n "$fg_no_bold[white] open by " print -n "$fg_bold[white]$tombuser" print -n "$fg_no_bold[white] from " print -n "$fg_bold[white]$tombtty" print -n "$fg_no_bold[white] on " print "$fg_bold[white]$tombhost" } 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 mounted_hooks=(`list_tomb_binds $tombname`) for h in ${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 } # print out an array of mounted tombs (internal use) # format is semi-colon separated list of attributes # if 1st arg is supplied, then list only that tomb # # String positions in the semicolon separated array: # # 1. full mapper path # # 2. mountpoint # # 3. filesystem type # # 4. mount options # # 5. tomb name list_tomb_mounts() { if [ "$1" = "" ]; then # list all open tombs mount -l \ | awk ' BEGIN { main="" } /^\/dev\/mapper\/tomb/ { if(main==$1) next; print $1 ";" $3 ";" $5 ";" $6 ";" $7 main=$1 } ' else # list a specific tomb mount -l \ | awk -vtomb="[$1]" ' BEGIN { main="" } /^\/dev\/mapper\/tomb/ { if($7!=tomb) next; if(main==$1) next; print $1 ";" $3 ";" $5 ";" $6 ";" $7 main=$1 } ' fi } # list_tomb_binds # print out an array of mounted bind hooks (internal use) # format is semi-colon separated list of attributes # needs an argument: name of tomb whose hooks belong list_tomb_binds() { if [ "$1" = "" ]; then _failure "internal error: list_tomb_binds called without argument."; fi # list bind hooks on util-linux 2.20 (Debian 7) mount -l \ | awk -vtomb="$1" ' BEGIN { main="" } /^\/dev\/mapper\/tomb/ { if($7!=tomb) next; if(main=="") { main=$1; next; } if(main==$1) print $1 ";" $3 ";" $5 ";" $6 ";" $7 } ' # list bind hooks on util-linux 2.17 (Debian 6) tombmount=`mount -l \ | awk -vtomb="$1" ' /^\/dev\/mapper\/tomb/ { if($7!=tomb) next; print $3; exit; }'` mount -l | grep "^$tombmount" \ | awk -vtomb="$1" ' /bind/ { print $1 ";" $3 ";" $5 ";" $6 ";" $7 }' } # }}} - Tomb list # {{{ Index and search # index files in all tombs for search # $1 is optional, to specify a tomb index_tombs() { { command -v updatedb > /dev/null } || { die "Cannot index tombs on this system: updatedb (mlocate) not installed" } updatedbver=`updatedb --version | grep '^updatedb'` [[ "$updatedbver" =~ "GNU findutils" ]] && { _warning "Cannot use GNU findutils for index/search commands" } [[ "$updatedbver" =~ "mlocate" ]] || { die "Index command needs 'mlocate' to be installed." } xxx "$updatedbver" mounted_tombs=(`list_tomb_mounts $1`) { test ${#mounted_tombs} = 0 } && { if [ $1 ]; then die "There seems to be no open tomb engraved as [$1]" else die "I can't see any open tomb, may they all rest in peace." fi } yes "Creating and updating search indexes" # start the LibreOffice document converter if installed { command -v unoconv >/dev/null } && { unoconv -l 2>/dev/null & xxx "unoconv listener launched" sleep 1 } for t in ${mounted_tombs}; do mapper=`basename ${t[(ws:;:)1]}` tombname=${t[(ws:;:)5]} tombmount=${t[(ws:;:)2]} { test -r ${tombmount}/.noindex } && { say "skipping $tombname (.noindex found)" continue } say "indexing $tombname filenames..." updatedb -l 0 -o ${tombmount}/.updatedb -U ${tombmount} # here we use swish to index file contents { test $SWISH = 1 } && { say "indexing $tombname contents..." swishrc=`safe_filename swish` cat < $swishrc DefaultContents TXT* FileFilter .pdf pdftotext "'%p' -" FileFilterMatch unoconv "-d spreadsheet -f csv --stdout %P" /\.xls.*/ FileFilterMatch unoconv "-d spreadsheet -f csv --stdout %P" /\.xlt.*/ FileFilter .ods unoconv "-d spreadsheet -f csv --stdout %P" FileFilter .ots unoconv "-d spreadsheet -f csv --stdout %P" FileFilter .dbf unoconv "-d spreadsheet -f csv --stdout %P" FileFilter .dif unoconv "-d spreadsheet -f csv --stdout %P" FileFilter .uos unoconv "-d spreadsheet -f csv --stdout %P" FileFilter .sxc unoconv "-d spreadsheet -f csv --stdout %P" FileFilterMatch unoconv "-d document -f txt --stdout %P" /\.doc.*/ FileFilterMatch unoconv "-d document -f txt --stdout %P" /\.odt.*/ FileFilterMatch unoconv "-d document -f txt --stdout %P" /\.rtf.*/ IndexContents HTML* .htm .html .shtml IndexContents XML* .xml IndexDir $tombmount IndexFile $tombmount/.swish EOF xxx "Using swish-e to create index" swish-e -c $swishrc -S fs -v3 rm -f $swishrc } say "search index updated" done } search_tombs() { { command -v locate > /dev/null } || { die "Cannot index tombs on this system: updatedb (mlocate) not installed" } updatedbver=`updatedb --version | grep '^updatedb'` [[ "$updatedbver" =~ "GNU findutils" ]] && { _warning "Cannot use GNU findutils for index/search commands" } [[ "$updatedbver" =~ "mlocate" ]] || { die "Index command needs 'mlocate' to be installed." } xxx "$updatedbver" # list all open tombs mounted_tombs=(`list_tomb_mounts`) if [ ${#mounted_tombs} = 0 ]; then die "I can't see any open tomb, may they all rest in peace."; fi yes "Searching for: $fg_bold[white]${(f)@}$fg_no_bold[white]" for t in ${mounted_tombs}; do xxx "checking for index: ${t}" mapper=`basename ${t[(ws:;:)1]}` tombname=${t[(ws:;:)5]} tombmount=${t[(ws:;:)2]} if [ -r ${tombmount}/.updatedb ]; then # use mlocate to search hits on filenames say "Searching filenames in tomb $tombname" locate -d ${tombmount}/.updatedb -e -i "${(f)@}" say "Matches found: `locate -d ${tombmount}/.updatedb -e -i -c ${(f)@}`" # use swish-e to search over contents { test $SWISH = 1 } && { test -r $tombmount/.swish } && { say "Searching contents in tomb $tombname" swish-search -w ${=@} -f $tombmount/.swish -H0 } else no "skipping tomb $tombname: not indexed" no "run 'tomb index' to create indexes" fi done say "Search completed." } # }}} - Index and search # {{{ Resize # resize tomb file size resize_tomb() { _message "Commanded to resize tomb $1 to $opts[-s] megabytes" if ! [ $1 ]; then _failure "No tomb name specified for resizing" elif ! [ -r "$1" ]; then _failure "Cannot find $1" fi # $1 is the tomb file path local c tombpass tombkey tombdir=`dirname $1` tombfile=`basename $1` tombname=${tombfile%%\.*} # load key from options or file tombkey=`load_key ${tombdir}/${tombfile}` { test $? = 0 } || { die "Aborting operations: error loading key $tombkey" } # make sure to call drop_key later local tmp_resize=`safe_filename resize` local newtombsize=$opts[-s] local oldtombsize=$(( `stat -c %s "$1" 2>/dev/null` / 1048576 )) 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 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 delta="$(( $newtombsize - $oldtombsize ))" act "Generating ${tombfile} of ${newtombsize}MiB" xxx "Data dump using ${DD[1]} from /dev/urandom" ${=DD} if=/dev/urandom bs=1048576 count=${delta} 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}" >> ${tombdir}/${tombfile} rm "${tmp_resize}" tombpass=`ask_key_password $tombkey` { test $? = 0 } || { die "No valid password supplied" } local nstloop=`losetup -f` if [ $? = 255 ]; then _failure "too many tomb opened. Please close any of them to open another tomb" fi losetup -f ${tombdir}/${tombfile} local mapdate=`date +%s` local mapper="tomb.${tombname}.${mapdate}.`basename $nstloop`" get_lukskey "${tombpass}" ${tombkey} | \ cryptsetup --key-file - luksOpen ${nstloop} ${mapper} unset tombpass drop_key # cleanup after load_key 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 } # }}} # {{{ Close umount_tomb() { local tombs how_many_tombs local pathmap mapper tombname tombmount loopdev local ans pidk pname if [ "$1" = "all" ]; then mounted_tombs=(`list_tomb_mounts`) else mounted_tombs=(`list_tomb_mounts $1`) fi { test ${#mounted_tombs} = 0 } && { _warning "There is no open tomb to be closed" return 1 } { test ${#mounted_tombs} -gt 1 } && { test "$1" = "" } && { _warning "Too many tombs mounted, please specify one (see tomb list)" _warning "or issue the command 'tomb close all' to close them all." return 1 } say "Tomb close $1" for t in ${mounted_tombs}; do mapper=`basename ${t[(ws:;:)1]}` tombname=${t[(ws:;:)5]} tombmount=${t[(ws:;:)2]} tombfs=${t[(ws:;:)3]} tombfsopts=${t[(ws:;:)4]} tombloop=${mapper[(ws:.:)4]} xxx "name: $tombname" xxx "mount: $tombmount" xxx "mapper: $mapper" { test -e "$mapper" } && { _warning "Tomb not found: $1" _warning "Please specify an existing tomb." return 0 } if [ $SLAM ]; then _success "Slamming tomb $tombname mounted on $tombmount" _message "Kill all processes busy inside the tomb" if ! slam_tomb "$tombmount"; then _warning "Cannot slam the tomb $tombname" return 1 fi else say "Closing tomb $tombname mounted on $tombmount" fi # check if there are binded dirs and close them bind_tombs=(`list_tomb_binds $tombname`) for b in ${bind_tombs}; do bind_mapper="${b[(ws:;:)1]}" bind_mount="${b[(ws:;:)2]}" _message "closing tomb bind hook: $bind_mount" umount $bind_mount if [[ $? != 0 ]]; then if [ $SLAM ]; then _success "Slamming tomb: killing all processes using this hook" slam_tomb "$bind_mount" if [[ $? == 1 ]]; then _warning "Cannot slam the bind hook $bind_mount" return 1 fi umount $bind_mount else _warning "Tomb bind hook $bind_mount is busy, cannot close tomb." fi fi done # Execute post-hooks for eventual cleanup if ! option_is_set -n ; then exec_safe_post_hooks ${tombmount%%/} close fi 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 { test "${tombmount}" = "/media/${tombname}.tomb" } && { rmdir ${tombmount} } fi cryptsetup luksClose $mapper { test $? = 0 } || { _warning "error occurred in cryptsetup luksClose ${mapper}" return 1 } losetup -d "/dev/$tombloop" _success "Tomb $tombname closed: your bones will rest in peace." done # loop across mounted tombs return 0 } # Kill all processes using the tomb slam_tomb() { # $1 = tomb mount point if [[ -z `fuser -m "$1" 2> /dev/null` ]]; then return 0 fi #Note: shells are NOT killed by INT or TERM, but they are killed by HUP for s in TERM HUP KILL; do xxx "Sending $s to processes inside the tomb:" if option_is_set -D; then ps -fp `fuser -m /media/a.tomb 2> /dev/null`| while read line; do xxx $line done fi fuser -s -m "$1" -k -M -$s if [[ -z `fuser -m "$1" 2> /dev/null` ]]; then return 0 fi if ! option_is_set -f; then sleep 3 fi done return 1 } # }}} - Tomb close # {{{ Main routine 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 U: -uid=U G: -gid=G T: -tty=T -no-color -unsecure-dev-mode) subcommands_opts[__default]="" subcommands_opts[open]="f -force n -nohook=n k: -key=k o: -ignore-swap -sudo-pwd: -tomb-pwd: " subcommands_opts[mount]=${subcommands_opts[open]} subcommands_opts[create]="" # deprecated, will issue warning subcommands_opts[forge]="f -force -ignore-swap k: -key=k -kdf: o: -tomb-pwd: -use-urandom " subcommands_opts[dig]="f -force -ignore-swap s: -size=s " subcommands_opts[lock]="f -force -ignore-swap k: -key=k o: -sudo-pwd: -tomb-pwd: " subcommands_opts[change]="f -force -ignore-swap k: -key=k -sudo-pwd: -tomb-pwd: " subcommands_opts[backup]="k: -key=k " subcommands_opts[passwd]="f -ignore-swap -kdf: -tomb-old-pwd: -tomb-pwd: " subcommands_opts[close]="-sudo-pwd: " subcommands_opts[help]="" subcommands_opts[slam]="" subcommands_opts[list]="-get-mountpoint " subcommands_opts[index]="" subcommands_opts[search]="" subcommands_opts[help]="" subcommands_opts[bury]="f -force k: -key=k -tomb-pwd: " subcommands_opts[exhume]="f -force k: -key=k -tomb-pwd: " # subcommands_opts[decompose]="" # subcommands_opts[recompose]="" # subcommands_opts[install]="" subcommands_opts[askpass]="" subcommands_opts[mktemp]="" subcommands_opts[source]="" subcommands_opts[resize]="f -force -ignore-swap s: -size=s k: -key=k -tomb-pwd: " subcommands_opts[check]="-ignore-swap " # 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) #### detect early: useful for --optiion-parsing zparseopts -M -D -Adiscardme ${every_opts} if [[ -n ${(k)discardme[--option-parsing]} ]]; then echo $1 if [[ -n "$1" ]]; then return 1 fi return 0 fi unset discardme if ! zparseopts -M -E -D -Adiscardme ${every_opts}; then die "error parsing" return 127 fi 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 for subcommand $subcommand" 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 if ! option_is_set --unsecure-dev-mode; then for opt in --sudo-pwd --tomb-pwd --use-urandom --tomb-old-pwd; do if option_is_set $opt; then die "You specified option $opt, which is DANGEROUS and should only be used for testing\nIf you really want so, add --unsecure-dev-mode" 127 fi done fi # when we run as root, we remember the original uid:gid # to set permissions for the calling user and drop privileges if option_is_set -U; then _uid="`option_value -U`"; fi if option_is_set -G; then _gid="`option_value -G`"; fi if option_is_set -T; then _tty="`option_value -T`"; fi xxx "Tomb command: $subcommand ${PARAM}" xxx "caller uid[$_uid] gid[$_gid] tty[$_tty]" case "$subcommand" in # new creation in three steps forge) check_priv forge_key ${=PARAM} ;; dig) dig_tomb ${=PARAM} ;; lock) check_priv lock_tomb_with_key ${=PARAM} ;; change) check_priv change_tomb_key ${=PARAM} ;; backup) { test "$QRENCODE" = 0 } && { die "QREncode not installed: cannot backup keys on paper." } backup_key ${=PARAM} ;; # backward compat create) _warning "The create command is deprecated, please use dig, forge and lock instead." _warning "For more informations see Tomb's manual page (man tomb)." ;; mount|open) check_priv mount_tomb $PARAM[1] $PARAM[2] ;; umount|close|slam) check_priv [ "$subcommand" = "slam" ] && SLAM=1 umount_tomb $PARAM[1] ;; passwd) change_passwd $PARAM[1] ;; list) list_tombs $PARAM[1] ;; index) index_tombs $PARAM[1] ;; search) search_tombs ${=PARAM} ;; help) usage ;; bury) { test "$STEGHIDE" = 0 } && { die "Steghide not installed: cannot bury keys into images." } bury_key $PARAM[1] ;; exhume) { test "$STEGHIDE" = 0 } && { die "Steghide not installed: cannot exhume keys from images." } exhume_key $PARAM[1] ;; resize) { test "$RESIZER" = 0 } && { die "Resize2fs not installed: cannot resize tombs." } check_priv resize_tomb $PARAM[1] ;; # internal commands useful to developers 'source') return 0 ;; install) check_priv ; install_tomb ;; askpass) ask_password $PARAM[1] $PARAM[2] ;; mktemp) safe_dir $PARAM[1] ;; translate) generate_translatable_strings ;; __default) cat < EOF option_is_set -v && { cat <