#!/bin/bash # # firma v0.3: encrypted mailing list manager # feedback: rhatto@riseup.net luis@riseup.net | GPL # # list configuration is passed thru the config file, # where you put PARAMETER=value (whithout spaces) # # MAIL= path for mail program # GPG= path for gnupg binary # LISTNAME= list email # LISTADMIN= list administrator email addresses (space separated) # GPGDIR= gpg dir for the lists' keyring # PASSWD= passwd for the lists' keyring # FIRMA_LIST_PATH=/usr/local/etc/lists VERSION=0.3 # todo: # errase all vars before quit the game # unset MESSAGE # unset GPG_MESSAGE # umask .... function usage { echo usage: $0 firma \ \ echo -c: create a new list using config-file echo -p: process a message echo -r: admin and user requests (mail only) echo -a: admin commands } function check_config { # check configuration file parameters if [ ! -f $GPG -o ! -x $GPG ]; then echo -e "\n$1: GPG binary ($GPG) could not be found.\n" exit 1 elif [ ! -f $MAIL -o ! -x $MAIL ]; then echo -e "\n$1: Mail program ($MAIL) could not be found.\n" exit 1 elif [ ! -d $GPGDIR -o ! -f $GPGDIR/pubring.gpg -o ! -f $GPGDIR/secring.gpg ]; then echo -e "\n$1: GPG home directory ($GPGDIR) or the GPG keyrings could not be found.\n" exit 1 elif [ -z "$(cat $CONFIG | grep -o ^PASSWD=\'[^\']*\'$)" -o \ -z "$(echo -n $PASSWD)" -o \ "$(echo -n $PASSWD | wc -m)" -lt "25" -o \ -z "$(echo -n $PASSWD | grep -o [[:lower:][:upper:]])" -o \ -z "$(echo -n $PASSWD | grep -o [[:digit:]])" -o \ "$(echo -n $PASSWD | grep -o [[:punct:]] | wc -l)" -lt "5" ]; then echo -e "\n$CONFIG: PASSWD is empty or does not meet the minimum complexity requirements." echo "$1: Please set a new passphrase for the list's private key. Make it at least" echo "$1: 25 characters long (using a combination of letters, numbers and at least" echo "$1: 5 special characters) and enclose it in 'single quotes'. The passphrase" echo -e "$CONFIG: itself, though, cannot contain any single quote.\n" exit 1 elif [ -z "$($GPGLIST | grep ^pub | cut -d : -f 10 | grep -i \<$LISTNAME\>$)" ]; then echo -e "\n$CONFIG: GPG key for list \"$LISTNAME\" could not be found." echo -e "$CONFIG: Note that this parameter expects an email address.\n" exit 1 else for ADMIN in $LISTADMIN; do { if [ -z "$($GPGLIST | grep ^pub | cut -d : -f 10 | grep -i \<$ADMIN\>$)" ]; then echo -e "\n$CONFIG: GPG key for list administrator \"$ADMIN\" could not be found." echo -e "$CONFIG: Note that this parameter expects one or more space separated email addresses.\n" exit 1 fi; } done fi } function GPGSTDERR { # discard $GPGDECRYPT STDOUT and get its STDERR instead, for signature checking echo -e "$PASSWD\n${GPG_MESSAGE[@]}" | sed -e 's/^ //' | ($GPGDECRYPT --status-fd 2 1> /dev/null) 2>&1 ; } function SUBSCRIBERS { # get list susbscriber's addresses $GPGLIST | sed -ne "/$LISTNAME/Id" -e '/pub/p' | cut -d : -f 10 | grep -o '<[^<>]*>$' | sed -e 's/[<>]//g' ; } function get_message { n=0; while read STDIN; do MESSAGE[$n]="$STDIN\n" ((++n)) done } function get_gpg_message { signal=0; x=0; for ((count=0;count<=n;count++)); do if [[ $signal == "0" ]] && [[ "$(echo "${MESSAGE[$count]}" | grep -v -e "-----BEGIN PGP MESSAGE-----")" == "" ]]; then GPG_MESSAGE[$x]=${MESSAGE[$count]}; ((++x)) signal=1 elif [[ $signal == "1" ]]; then GPG_MESSAGE[$x]=${MESSAGE[$count]} ((++x)) if [[ "$(echo "${MESSAGE[$count]}" | grep -v -e "-----END PGP MESSAGE-----")" == "" ]]; then signal=0 fi fi done } function get_headers { # get the message headers and the sender's email address FROM=$(echo -e "${MESSAGE[@]}" | grep -m 1 "From:" | cut -d : -f 2- | sed -e 's/^ //') FROMADD=$(if [ -z "$(echo $FROM | grep '>$')" ] ; then echo $FROM ; else echo $FROM | grep -o '<[^<>]*>$' | sed -e 's/[<>]// g' ; fi) DATE=$(echo -e "${MESSAGE[@]}" | grep -m 1 "Date:") SUBJECT=$(echo -e "${MESSAGE[@]}" | grep -m 1 "Subject:" | cut -d : -f 2- | sed -e 's/^ //') } function message_list { # compose and send a message to the list # $1: subscriber email # sorry no identation :P echo "$PASSWD Message from: $FROM Subject: $SUBJECT $DATE $(GPGSTDERR | grep -F 'gpg: Signature made') $(GPGSTDERR | grep -F 'gpg: Good signature from') $(echo -e "$PASSWD\n${GPG_MESSAGE[@]}" | $GPGDECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPGENCRYPT $1 | $MAIL -r $LISTNAME $1 } function message_list_error { # compose and send an error message # sorry no identation :P echo "$PASSWD Message from: $FROM Subject: [BAD SIGNATURE] $SUBJECT $DATE $(GPGSTDERR | grep -F 'gpg: Signature made') $(GPGSTDERR | grep -F 'gpg: BAD signature from') $(echo -e "$PASSWD\n${GPG_MESSAGE[@]}" | $GPGDECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPGENCRYPT $1 | $MAIL -r $LISTNAME $1 } function message_list_return { # send a bouce message # $1: sender email (usually $FROMADD) # sorry no identation :P echo " Message from: $FROM Subject: [RETURNED MAIL] $SUBJECT $DATE [ It was not possible to process this message. Either or both the message was not encrypted and/or signed, or you are not subscribed to this list. Contact the list administrator if you have any questions. ] -- firma v$VERSION" | $MAIL -r $LISTNAME $1 } function process_message { # process a message sent to the list get_message get_headers get_gpg_message # if signature is Good, encrypt and send it for each list subscriber # todo: declare a function to decrypt, re-encrypt and send the list messages if (GPGSTDERR | grep -Fq GOODSIG); then for EMAIL in $(SUBSCRIBERS); do message_list $EMAIL done # else, if signature is BAD, email it back to the list admins and to sender elif (GPGSTDERR | grep -Fq BADSIG) ; then for EMAIL in $(echo $LISTADMIN $FROMADD); do message_list_error $EMAIL done # else, probably either the message was not signed or the sender is not subscribed to the list # email the message back to sender including a note about this # todo: parse STDERR to find out why the signature couldn't be checked and send more specific errors back to sender else message_list_return $FROMADD fi } function newlist { # create a list if it doesnt already exist if [ ! -d "$CONFIG_PATH" ]; then echo creating folder $CONFIG_PATH... mkdir "$CONFIG_PATH" # || (echo "error creating $CONFIG_PATH: installation aborted"; exit 1) echo "creating list config file and will ask some questions." GPGDIR="$CONFIG_PATH" read -p "path to nail command (eg, /usr/bin/nail): " MAIL read -p "path to gpg binary (eg, /usr/bin/gpg): " GPG # if [ ! -x $GPG ]; then read -p "list keyring folder (defaults to $GPGDIR): " GPGDIR # todo: please no utf-8 (see DETAILS) read -p "list email (eg, firma@domain.tld): " LISTNAME read -p "list admins emails (space delimited)" LISTADMIN read -p "password for list keyring (use a huge one): " PASSWD # todo: key specs (size, expiry date...) echo "creating your config..." touch $CONFIG chown root.root $CONFIG chmod 600 $CONFIG if [ -f $CONFIG ]; then echo -e "MAIL=$MAIL\nGPG=$GPG\nGPGDIR=$GPGDIR\nLISTNAME=$LISTNAME\nLISTADMIN=$LISTADMIN\nPASSWD=$PASSWD" > $CONFIG echo "now generating your keyring..." # re-eval GPGCOMMAND # todo: GPGFLAGS depende de GPGDIR GPGCOMMAND="$GPG $GPGFLAGS" $GPGCOMMAND --gen-key fi else echo error creating $CONFIG_FILE: list already exists exit 1 fi } # main - # command line checking if [ -z $2 ]; then usage; exit 1 else CONFIG_FILE="$2" CONFIG_PATH="$FIRMA_LIST_PATH/$2" CONFIG="$CONFIG_PATH/$2.conf" fi # if the configuration file exists, disable "sourcepath" and evaluate the parameters if [ -f $CONFIG ] && [[ $1 != "-c" ]]; then shopt -u sourcepath && source $CONFIG else echo -e "\nConfiguration file \"$CONFIG\" could not be found.\n" exit 1 fi declare -a MESSAGE declare -a GPG_MESSAGE declare n export LANG=en_US # declare GPG variables GPGFLAGS="--quiet --homedir $GPGDIR --batch --no-tty --no-use-agent --no-permission-warning" GPGCOMMAND="$GPG $GPGFLAGS" GPGLIST="$GPGCOMMAND --list-keys --with-colons" GPGDECRYPT="$GPGCOMMAND --passphrase-fd 0 --decrypt" GPGENCRYPT="$GPGCOMMAND --passphrase-fd 0 --always-trust --encrypt --sign --armor --recipient" # then check the config check_config # command line parsing if [[ $1 == "-c" ]]; then newlist elif [[ $1 == "-p" ]]; then process_message elif [[ $1 == "-a" ]]; then list_admin elif [[ $1 == "-r" ]]; then list_request else usage; exit 1 fi