#!/bin/bash
# a7crypt v1.0.0 last mod 2012/01/09
# Latest version at
# Copyright 2011, 2012 Ryan Sawhill
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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. See the GNU
# General Public License for more details.
#-------------------------------------------------------------------------------
# System requirements: zenity and one of either gpg, gpg2, or openssl.
#
# The original goal of this project was to make symmetric text {en,de}cryption
# more accessible and easy to use. While GPG rocks (for both symmetric &
# public-key) if you're comfortable on the commandline, and there are GUI
# encryption options for key-based, there's not much out there for people who
# need to do the simplest kind of encryption -- with a shared passphrase.
#
# First I developed a super-simple wrapper for the commandline. (To see an
# evolution of that, check out a3crypt at , which
# doesn't have any of the bells and whistles of a7crypt.) Once that was
# complete, I decided it was time to the fill the hole of a GUI for symmetric
# encryption, and began fleshing it out and adding features, quickly adding the
# ability to pick files (and have the script automatically choose ASCII or
# binary output type based on the chosen file).
#
# It almost goes without saying, but from the beginning, a requirement of all
# this was that security not be in any way sacrificed for the convenience.
#
# To that end: No processes are run with arguments of a passphrase, nor are
# passphrases or message data stored on disk; the script creates a working dir
# in ram that is only readable by the current user and whenever message data or
# passphrases need to be stored, they are stored there and deleted as soon as
# they have served their purpose. (Note: We WILL fall-back to creating a
# working dir on disk in users' HOME if /dev/shm is unavailable for some crazy
# reason.)
#
# We of course want to ensure that all tmpfiles are cleaned up on exit, even
# though they're hidden away in directories only available to the user. A simple
# trap will cover exiting due to receiving a sigterm or sigint, but for sigkill,
# we need something more. So each time the script runs it creates lockfiles with
# its PID and the location of the temp working directory. That way, if an old
# instance of the script ever dies without cleaning up its potentially-sensitive
# data, we can take out the trash on the next run.
#
# And a final note: When lockfiles & temp directories are created by this
# script, they're named based on the script's current filename; not some static
# project name. This means that you can feel free to rename the script.
# Everything else will go along with the new name--you don't need to change
# anything in the content. Caveat: Don't be stupid & put spaces in the name. :)
#
# Feel free to hit me/the tracker up if you have any questions or suggestions!
#
# PS: If you like this you should check out a8crypt -- a gui frontend with more
# features implemented in Python & GTK.
#-------------------------------------------------------------------------------
## If the filename of our script is /usr/local/bin/a7crypt, then this variable would end up being "a7crypt", which we will use throughout
zero=${0##*/} ; [[ -z ${zero##* *} ]] && zero=a7crypt
#===============================================================================
#-------------------------------------------------- 1) BEGIN SANITY CHECKS ---->
## Print help message if no X or if run with any arguments
if [[ -z $DISPLAY || $# -ne 0 ]]; then
echo -e "$zero provides symmetric encryption/decryption using GPG, GPG2, or OpenSSL\nIt is meant to be run with no arguments from a GUI desktop"
exit
## Ensure we have zenity
elif ! command -v zenity >/dev/null; then
tty --silent || notify-send --urgency=critical "$zero Critical Error" "You tried to execute $zero but it relies on Zenity (a small helper-application you don't have installed) to produce GUI menus and dialog boxes. You can install 'zenity' using your add/remove software management application."
echo -e "This program relies on Zenity (a small helper-application you don't have\ninstalled) to produce GUI menus and dialog boxes. You can install 'zenity' using\nyour add/remove software management application.\nCtrl-c to quit."
sleep 20
exit 255
## Need writable home
elif [[ ! -w $HOME ]]; then
zenity --error --title="$zero: CRITICAL ERROR" --text="You are running $zero as $USER and your home directory ($HOME) is not writable. Cannot continue."
exit 250
## Do we have gpg2?
elif command -v gpg2 >/dev/null; then
mode=GPG2
## Next up, check for gpg
elif command -v gpg >/dev/null; then
mode=GPG
## If no gpg funness, use openssl
elif command -v openssl >/dev/null; then
mode=OpenSSL-AES
## On the off-chance that none of those three are on the system...
else
zenity --error --title="You pulling my leg?" --text='Neither GPG, GPG2, nor OpenSSL were found in your $PATH..? Right. If you say so.'
exit 250
fi
#===============================================================================
#------------------------------------------------ 2) SETUP OUR ENVIRONMENT ---->
## Uber-restrictive umask, because we can
umask 077
## Create settings dotdir in ~ if not already there
dotdir=$HOME/.$zero ; [[ ! -d $dotdir ]] && mkdir $dotdir
## Setup a private working directory in ram or in ~
if [[ -w /dev/shm/ ]]; then
wd=$(mktemp -d /dev/shm/.$zero.$UID.XXX)
else
wd=$(mktemp -d $dotdir/wd.XXX)
fi
## If there are existing lockfiles not associated with a running process, rm them and their stale working dirs
for lockfile in $dotdir/lock-*; do
[[ $(/dev/null
## Create new lockfile, e.g. ~/.a7crypt/lock-PID, which is a link to the working dir
lockfile=$dotdir/lock-$$ ; ln -s $wd $lockfile
## Set up trap to clean-up working dir & kill any child processes (zenity) on exit
trap "{ pkill -P $$; rm -r $wd $lockfile 2>/dev/null; }" EXIT
## Set filemanager for optionally looking at files
for filemgr in nautilus dolphin konqueror thunar pcmanfm firefox; do
command -v $filemgr >/dev/null && break
done
## Set viewer for optionally looking at encrypted/decrypted text
for viewer in gedit gvim kedit; do
command -v $viewer >/dev/null && break
done
#===============================================================================
#-------------------------------------------------- 3) ENUMERATE FUNCTIONS ---->
## Functions to initialize the variables for each mode (see beginning of MAIN function for $gpg enumeration)
init_env_GPG() {
switch_to=OpenSSL
encrypt="$gpg --batch --no-tty --yes --passphrase-file $wd/pass -o $wd/output -c --force-mdc --cipher-algo aes256"
decrypt="$gpg --batch --no-tty --yes --passphrase-file $wd/pass -o $wd/output -d"
}
init_env_OPENSSL() {
switch_to=GPG
encrypt="openssl aes-256-cbc -pass file:$wd/pass -out $wd/output -salt"
decrypt="openssl aes-256-cbc -pass file:$wd/pass -out $wd/output -d"
}
#---------------------------------------------- 3.1) FUNCTION: MAIN THREAD ---->
# MAIN manages menu & dialogs, encryption, decryption, & loading of UPDATE func.
MAIN() {
## Ensure buffers are clear
>$wd/pass; rm $wd/input $wd/output 2>/dev/null
## (re-)Initialize our variables, depending on which mode we're in
if [[ $mode = GPG2 ]]; then
gpg="gpg2"
init_env_GPG
elif [[ $mode = GPG ]]; then
gpg="gpg --no-use-agent"
init_env_GPG
elif [[ $mode = OpenSSL-AES ]]; then
init_env_OPENSSL
fi
## Set main menu content for 'zenity --list'
menu_head_enc="[----- ENCRYPTION -----]"
menu_enc_text=" • Type or paste text"
menu_enc_file=" • Select file"
menu_head_dec="[----- DECRYPTION -----]"
menu_dec_text=" • Paste text"
menu_dec_file=" • Select file " ## Trailing space to make different than $menu_enc_file
menu_blanksep=" --------------------------"
menu_switchto="Switch to $switch_to-mode"
menu_ckupdate="Check online for update"
## Display main menu and grab the user's choice
menu_choice=$(zenity --list --hide-header --title="$zero [$mode]" --text="What would you like to do?" --column=1 "$menu_head_enc" "$menu_enc_text" "$menu_enc_file" "$menu_head_dec" "$menu_dec_text" "$menu_dec_file" "$menu_blanksep" "$menu_switchto" "$menu_ckupdate" --cancel-label="Quit" --height=340) || exit
#------------------------------------------------
## BEGIN PARSING MENU CHOICES
## Reload menu if user picks one of the header lines
if [[ $menu_choice = $menu_head_enc || $menu_choice = $menu_head_dec || $menu_choice = $menu_blanksep ]]; then
MAIN
## Run our UPDATE function if they choose updates
elif [[ $menu_choice = $menu_ckupdate ]]; then
UPDATE
#------------------------------------------------
## 3.1.1) ENCRYPT: TYPE/PASTE TEXT
elif [[ $menu_choice = $menu_enc_text ]]; then
## Warn user about openssl obscurity
if [[ $mode = OpenSSL-AES && ! -f $dotdir/nowarn.openssl-txt ]]; then
zenity --info --title="$zero [$mode]: NOTICE" --text="OpenSSL text-input mode uses 'openssl aes-256-cbc -salt -a' to encrypt text to ASCII-armored (plain-text) output. OpenSSL doesn't add any pretty envelope info to its encrypted data like GPG does, meaning there are no GUI applications (AFAIK) that will know what to do with files containing said data.\n\nIn short, to decrypt data encrypted with this mode, you can either:\n1) use this app\n2) use OpenSSL directly on a terminal, i.e., drop the data into a file of SOMENAME and run\n\topenssl aes-256-cbc -a -d -in SOMENAME\n\n[Press Enter or Esc if you can't see OK button.]"
touch $dotdir/nowarn.openssl-txt
fi
## Encrypt command for openssl needs -a (ASCII) and this doesn't hurt gpg
encrypt="$encrypt -a"
## Ask for text message to be encrypted
zenity --text-info --editable --title="$zero [$mode]: Type or paste text to be encrypted" --width=800 --height=600 --no-wrap >$wd/input || MAIN
## Keep prompting until user actually enters some data
until [[ -s $wd/input ]]; do
zenity --text-info --editable --title="$zero [$mode]: You must enter text to be encrypted" --width=800 --height=600 --no-wrap >$wd/input || MAIN
done
## Ask for encryption passphrase
## TODO: I designed this to be fast and simple; I don't like the idea of having to enter a passphrase twice, but I also don't like the idea of accidentally mistyping a long passphrase. That's why I decided not to use --hide-text to hide the passphrase when doing encryption. Need to get feedback from users on this choice though.
zenity --entry --title="$zero [$mode]" --text="Enter passphrase very carefully" --ok-label="Encrypt" >$wd/pass || MAIN
## Keep prompting until user enters a passphrase (zenity --entry inserts a newline even if no input so we can't use "test -s" like we can with --text-info)
until [[ $(wc -c $wd/pass || MAIN
done
## Start a progress bar dialog for those really big jobs or slow machines (this gives the user feedback and a chance to cancel)
coproc zenity --progress --pulsate --title="$mode: Please wait" --text="Encrypting text..." --auto-kill --auto-close; echo 1 >&${COPROC[1]}
## Run our encrytion; if failed, save & report back errors, cleanup, and go to main menu
if ! $encrypt $wd/errlog; then
>$wd/pass; >$wd/input ## Clear passphrase & input buffers ASAP
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
zenity --error --title="$zero [$mode]: ENCRYPTION ERROR!" --text="$($wd/pass; >$wd/input ## Clear passphrase & input buffers ASAP
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
## Display encrypted data back to user, loop back to main menu if they don't hit quit
zenity --text-info --title="$zero [$mode]: Your encrypted text" --width=650 --height=400 --no-wrap --cancel-label="Show in txt editor" --ok-label="Menu" 'openssl aes-256-cbc -salt' to encrypt non-text files to binary output and the same plus an option of '-a' to encrypt text files to ASCII-armored (plain-text) output. (Output files get the same name with an extension of 'aes256' appended.) OpenSSL doesn't add any pretty envelope info to its encrypted data like GPG does, meaning there are no GUI applications (AFAIK) that will know what to do with files containing said data.\n\nIn short, to decrypt files encrypted with this mode, you can either:\n1) use this app\n2) use OpenSSL directly on a terminal, e.g.,\n openssl aes-256-cbc -a -d -in FILE.txt.aes256\n\t(to decrypt the ASCII-files)\n openssl aes-256-cbc -d -in FILE.zip.aes256\n\t(to decrypt the binary files)\n\n[Press Enter or Esc if you can't see OK button.]"
touch $dotdir/nowarn.openssl-file
fi
## Get file to encrypt via file-selection dialog
file_to_encrypt=$(zenity --file-selection --title="$zero [$mode]: Select file to be encrypted") || MAIN
## Keep prompting until user chooses a proper file
while [[ ! -s $file_to_encrypt || ! -r $file_to_encrypt ]]; do
file_to_encrypt=$(zenity --file-selection --title="$zero [$mode]: Could not read file; select another") || MAIN
done
## Detect whether input is binary or text and set output mode and output filename accordingly
file_type=$(file -b -e soft "$file_to_encrypt") ## Get filetype
if [[ ${file_type%% *} = ASCII ]]; then
encrypt="$encrypt -a"
ascii_yesno="ASCII-armored output" ; encrypted_file=$file_to_encrypt.asc
else
ascii_yesno="binary output" ; encrypted_file=$file_to_encrypt.gpg
fi
[[ $mode = OpenSSL-AES ]] && encrypted_file=$file_to_encrypt.aes256
## Now that we know what our input and output files are going to be, setup symlinks
ln -sf "$file_to_encrypt" $wd/input
ln -sf "$encrypted_file" $wd/output
## Ask for encryption passphrase
zenity --entry --title="$zero [$mode]" --text="Enter passphrase very carefully" --ok-label="Encrypt" >$wd/pass || MAIN
## Keep prompting until user enters a passphrase (zenity --entry inserts a newline even if no input so we can't use "test -s" like we can with --text-info)
until [[ $(wc -c $wd/pass || MAIN
done
## Start a progress bar dialog for those really big jobs or slow machines (this gives the user feedback and a chance to cancel)
coproc zenity --progress --pulsate --title="$mode: Please wait" --text="Encrypting $file_to_encrypt..." --auto-kill --auto-close; echo 1 >&${COPROC[1]}
## Run our encrytion; if failed, save & report back errors, cleanup, and go to main menu
if ! $encrypt /$wd/errlog; then
>$wd/pass ## Clear the passphrase buffer
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
zenity --error --title="$zero [$mode]: ENCRYPTION ERROR!" --text="$($wd/pass ## Clear the passphrase buffer
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
## Report final encrypted filename back to user; give them a chance to see the file in their filemanager (hopefully)
echo -en "Successfully encrypted [ $file_to_encrypt ] with $ascii_yesno. Saved encrypted copy to:\n$encrypted_file" | zenity --text-info --title="$zero [$mode]: Your encrypted file is ready" --width=500 --height=160 --cancel-label="Show file" --ok-label="Menu" || $filemgr "${encrypted_file%/*}"
MAIN
#------------------------------------------------
## 3.1.3) DECRYPT: PASTE ENCRYPTED ASCII
elif [[ $menu_choice = $menu_dec_text ]]; then
## Decrypt command for openssl needs -a (ASCII) and this doesn't hurt gpg
decrypt="$decrypt -a"
## Ask for text message to be decrypted
zenity --text-info --editable --title="$zero [$mode]: Paste $mode-encrypted message" --width=650 --height=400 --no-wrap >$wd/input || MAIN
## Keep prompting until user actually enters some data
until [[ -s $wd/input ]]; do
zenity --text-info --editable --title="$zero [$mode]: You must enter an encrypted message" --width=650 --height=400 --no-wrap >$wd/input || MAIN
done
## Ask for decryption passphrase
zenity --entry --title="$zero [$mode]" --text="Enter passphrase" --hide-text --ok-label="Decrypt" >$wd/pass || MAIN
## Keep prompting until user enters a passphrase (zenity --entry inserts a newline even if no input so we can't use "test -s" like we can with --text-info)
until [[ $(wc -c $wd/pass || MAIN
done
## Start a progress bar dialog for those really big jobs or slow machines (this gives the user feedback and a chance to cancel)
coproc zenity --progress --pulsate --title="$mode: Please wait" --text="Decrypting text..." --auto-kill --auto-close; echo 1 >&${COPROC[1]}
## Run our decrytion; if failed, save & report back errors, cleanup, and go to main menu
if ! $decrypt $wd/errlog; then
>$wd/pass; >$wd/input ## Clear passphrase & input buffers ASAP
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
zenity --error --title="$zero [$mode]: DECRYPTION ERROR!" --text="$($wd/pass; >$wd/input ## Clear passphrase & input buffers ASAP
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
## Display decrypted data back to user, loop back to main menu if they don't hit quit
zenity --text-info --title="$zero [$mode]: Your decrypted text" --width=650 --height=600 --no-wrap --cancel-label="Show in txt editor" --ok-label="Menu" $wd/pass || MAIN
## Keep prompting until user enters a passphrase (zenity --entry inserts a newline even if no input so we can't use "test -s" like we can with --text-info)
until [[ $(wc -c $wd/pass || MAIN
done
## Start a progress bar dialog for those really big jobs or slow machines (this gives the user feedback and a chance to cancel)
coproc zenity --progress --pulsate --title="$mode: Please wait" --text="Decrypting $file_to_decrypt..." --auto-kill --auto-close; echo 1 >&${COPROC[1]}
## Run our decryption; if failed, save & report back errors, cleanup, and go to main menu
if ! $decrypt /$wd/errlog; then
>$wd/pass ## Clear the passphrase buffer
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
zenity --error --title="$zero [$mode]: DECRYPTION ERROR!" --text="$($wd/pass ## Clear the passphrase buffer
echo 100 >&${COPROC[1]} ## Set our progress dialog to 100% so it closes
## Report final decrypted filename back to user; give them a chance to see the file in their filemanager (hopefully)
echo -en "Successfully decrypted [ $file_to_decrypt ] to new file:\n$decrypted_file" | zenity --text-info --title="$zero [$mode]: Your decrypted file is ready" --width=500 --height=160 --cancel-label="Show file" --ok-label="Menu" || $filemgr "${decrypted_file%/*}"
MAIN
#------------------------------------------------
## 3.1.5) SWITCH MODES
else
## If switching to openssl mode, ensure we have openssl (haha) before setting our mode
if [[ $switch_to = OpenSSL ]]; then
if command -v openssl >/dev/null; then
mode=OpenSSL-AES
else
zenity --error --title="Missing OpenSSL" --text="OpenSSL not found in your \$PATH. That's so shocking I almost don't believe it. In any case, if you really want to try it out, you can install it using your software-management application." --width=400
fi
## If switching to gpg mode, check for gpg2 first; otherwise gpg; in the unlikely event of neither, display an error
elif [[ $switch_to = GPG ]]; then
if command -v gpg2 >/dev/null; then
mode=GPG2
elif command -v gpg >/dev/null; then
mode=GPG
else
zenity --error --title="Missing GPG" --text="Neither GPG nor GPG2 were found in your \$PATH. You can install either of them using your software-management application." --width=400
fi
fi
## Reload the main menu
MAIN
fi
}
#-------------------------------------------------- 3.2) FUNCTION: UPDATES ---->
# UPDATE is called by MAIN in order to check for & optionally download updates
UPDATE() {
## Set some variables
red=""; blue=""
orange=""; end_color=""
file_dL=$dotdir/latest_update
diff_file=$dotdir/latest_changes.patch
## Make sure we CAN download something
if command -v wget >/dev/null; then
downloader=wget; out='-O'
elif command -v curl >/dev/null; then
downloader=curl; out='-o'
else
zenity --error --title="$zero [UPDATE]" --text="${red}Need either wget or curl installed to perform the download\!${end_color}"
MAIN
fi
## Start a progress bar
coproc zenity --progress --pulsate --title="$zero [UPDATE]" --text="Downloading latest version of a7crypt from github..." --auto-kill --auto-close
echo 1 >&${COPROC[1]}
## Download latest version of a7crypt with wget or curl
if ! $downloader https://raw.github.com/ryran/a7crypt/master/a7crypt $out $file_dL; then
echo 100 >&${COPROC[1]}
zenity --error --title="$zero [UPDATE]" --text="${red}There was a problem downloading the file\! Try running $zero from a terminal for more info.${end_color}"
MAIN
fi
## Close progress bar
echo 100 >&${COPROC[1]}
## Grab version strings from files
version=$(head -n2 $0 | grep -o "a7crypt v.*")
file_dL_version=$(head -n2 $file_dL | grep -o "a7crypt v.*")
## Compare currently running a7crypt with downloaded file -- if they're the same ...
if diff -u $0 $file_dL >$diff_file; then
zenity --info --title="$zero [UPDATE]" --text="$0 is the same version as what is on github, i.e.,\n\t${blue}$version${end_color}"
MAIN
## Else, if they're different, give user a chance to look at differences
else
if ! zenity --question --title="$zero [UPDATE]" --text="The version you are running reports as:\n\t${blue}$version${end_color}\nThe version on github appears to be different and reports as:\n\t${orange}$file_dL_version${end_color}" --cancel-label="Show differences" --ok-label="Next"; then
$viewer $diff_file
sleep 4
fi
## Confirm update
zenity --question --title="$zero [UPDATE]" --text="Replace $0 with the latest version?" --cancel-label="Back to Menu" --ok-label="UPDATE" || MAIN
## If script location is writable, then let's do the do and save output to errlog
if [[ -w $0 ]]; then
{ cp -v $0 $dotdir/$zero.bak; cp -v $file_dL $0; chmod -v +x $0; } >$wd/errlog 2>&1
## Otherwise we have to use a GUI su/sudo program, so first we search & pick one
else
for Gsu in beesu kdesu gksu ktsuss NO_tenemos_Gsu; do
command -v $Gsu >/dev/null && break
done
## If we don't have one, display error + return to menu
[[ $Gsu = NO_tenemos_Gsu ]] && { zenity --error --title="$zero [UPDATE]" --text="${orange}Unable to authenticate as root in order to modify $0. You can re-run $zero with root privileges from a terminal (sudo $zero or su -lc a7crypt)${end_color}"; MAIN; }
## Finally, run our commands with whichever GUI su program we found, saving output to errlog
$Gsu "cp -v $0 $dotdir/$zero.bak; cp -v $file_dL $0; chmod -v +x $0" >$wd/errlog 2>&1 ## TODO: Need to test this with different programs; perhaps output should be inside quotes
fi
rm -v $dotdir/nowarn* 2>/dev/null ## TODO: Decide if really want this. (There might be important updates to warning messages?)
## Pipe some messages + the errlog to a zenity window for the user
{ echo "Backing up current version and replacing it with downloaded version..."; cat $wd/errlog; echo -n 'ALL DONE!'; } |
zenity --text-info --title="$zero [UPDATE]" --no-wrap --cancel-label="Quit" --ok-label="Re-load $zero new version" --width=510 --height=300 && exec $0 || exit
fi
## Script should never get here, but...
MAIN
}
#===============================================================================
#---------------------------------------------- 4) BEGIN USER-INTERACTION! ---->
## First-run information
if [[ ! -f $dotdir/nowarn.1strun ]]; then
zenity --info --title="$zero: First-run message" --text="This app is designed to be a frontend for utilities that provide symmetric (i.e., one passphrase used for both) encryption and decryption of files or blocks of text. It doesn't set any limits on the size of the input, so be warned: if e.g., you paste in a 150 MB text file for encryption, $zero will do it, but you'll lose upwards of 150 megs of RAM temporarily as it stores input in /dev/shm to speed operations. So you'd be better off using the file-selection mode for giant text files, as the file will be fed directly into the encryption program.\n\nAlso, a random tip: When in the main menu, you can double-click to make your choice; you don't have to use the OK button. Enjoy\!\n\n[Press Enter or Esc if you can't see OK button.]"
touch $dotdir/nowarn.1strun
fi
## All setup is complete & we're ready to start the main menu (function above)
MAIN