#!/bin/bash # # firma: 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 # MAIL_ARGS= command-line arguments passed to the smtp wrapper # 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" function usage { echo "usage: $(basename $0) OPTION [LIST-NAME]" echo " -a: admin commands" echo " -c: create a new list" echo " -h: display this help and exit" echo " -p: process a message" echo " -r: admin and user requests (mail only)" echo " -v: output version information and exit" } function version { echo "firma $VERSION" echo "Copyright (C) 2005 A Firma, Inc." echo "This program comes with ABSOLUTELY NO WARRANTY." echo "This is free software, and you are welcome to redistribute it" echo "under certain conditions. See the GNU General Public License" echo "for more details." } function check_config { # check configuration file parameters if [ ! -f $GPG -o ! -x $GPG ]; then echo "$CONFIG_FILE: GPG binary ($GPG) could not be found." exit 1 elif [ ! -f $MAIL -o ! -x $MAIL ]; then echo "$CONFIG_FILE: Mail program ($MAIL) could not be found." exit 1 elif [ ! -d $GPGDIR -o ! -f $GPGDIR/pubring.gpg -o ! -f $GPGDIR/secring.gpg ]; then echo "$CONFIG_FILE: GPG home directory ($GPGDIR) or the GPG keyrings could not be found." 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 "$CONFIG_FILE: PASSWD is empty or does not meet the minimum complexity requirements." echo "$CONFIG_FILE: Please set a new passphrase for the list's private key. Make it at least" echo "$CONFIG_FILE: 25 characters long (using a combination of letters, numbers and at least" echo "$CONFIG_FILE: 5 special characters) and enclose it in 'single quotes'. The passphrase" echo "$CONFIG_FILE: itself, though, cannot contain any single quote." exit 1 elif [ -z "$($GPGLIST | grep ^pub | cut -d : -f 10 | grep -i \<$LISTNAME\>$)" ]; then echo "$CONFIG_FILE: GPG key for list \"$LISTNAME\" could not be found." echo "$CONFIG_FILE: Note that this parameter expects an email address." exit 1 else for ADMIN in $LISTADMIN; do { if [ -z "$($GPGLIST | grep ^pub | cut -d : -f 10 | grep -i \<$ADMIN\>$)" ]; then echo "$CONFIG_FILE: GPG key for list administrator \"$ADMIN\" could not be found." echo "$CONFIG_FILE: Note that this parameter expects one or more space separated email addresses." exit 1 fi; } done fi } function get_gpg_stderr { # 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 get_subscribers_list { # 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 { LINES=0 while read STDIN; do MESSAGE[$LINES]="$STDIN\n" ((++LINES)) done } function get_gpg_message { signal=0 if [ ! -z "$LINES" ]; then x=0; n=$LINES 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 fi } function get_message_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:" | sed -e 's/^ //') 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 $(get_gpg_stderr | grep -F 'gpg: Signature made') $(get_gpg_stderr | 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 $(get_gpg_stderr | grep -F 'gpg: Signature made') $(get_gpg_stderr | 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 bounce message # $1: sender email (usually $FROMADD) # sorry no identation :P echo "From: $LISTNAME To: $1 Subject: none 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 $MAIL_ARGS } function process_message { # process a message sent to the list get_message get_message_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 (get_gpg_stderr | grep -Fq GOODSIG); then for EMAIL in $(get_subscribers_list); do message_list $EMAIL done # else, if signature is BAD, email it back to the list admins and to sender elif (get_gpg_stderr | 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." read -p "path to smtp command (eg, /usr/sbin/sendmail): " MAIL read -p "command-line arguments passed to the smtp wrapper (eg, -oem -oi -t): " MAIL_ARGS 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 "list description (fake?): " DESCRIPTION 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 gpg_args echo -e "MAIL=$MAIL\nGPG=$GPG\nGPGDIR=$GPGDIR\nLISTNAME=$LISTNAME\nLISTADMIN=$LISTADMIN\nPASSWD=$PASSWD" > $CONFIG echo "now generating your keyring..." $GPGCOMMAND --gen-key < /dev/null | grep -i \<$1\>:$ | cut -d : -f 10)" ]; then echo "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" is not associated with any public key on this keyring." return 1 elif [ "$($GPGLIST --fixed-list-mode $1 2> /dev/null | grep ^uid | wc -l)" -eq "1" ]; then echo "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" is part of the only UID on public key \"$KEYID\"." return 1 elif [ "$($GPGLIST --fixed-list-mode $1 2> /dev/null | grep -iF $1 | cut -d : -f 10 | wc -l)" -gt "1" ]; then echo "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" is listed in more than one UID on this keyring. Narrow down your selection" echo "$CONFIG_FILE: or delete all but one of the public keys associated with this email address." return 1 fi expect -nN -- << EOF log_user 0 set timeout 5 set keyid [eval exec $GPG --fingerprint --with-colons --fixed-list-mode $1 | grep ^fpr | cut -d : -f 10] set uid_count [eval exec $GPG --list-keys --with-colons --fixed-list-mode \$keyid | grep ^uid | wc -l] set chosen_uid [eval exec $GPG --list-keys --with-colons --fixed-list-mode \$keyid | grep ^uid | grep -ni $1 | cut -d : -f 1] eval spawn -noecho $GPG --with-colons --command-fd 0 --status-fd 1 --edit-key \$keyid expect "GET_LINE keyedit.prompt" { set uid 1 while { \$uid <= \$uid_count } { if { \$uid != \$chosen_uid } { send "uid \$uid\n" expect "GET_LINE keyedit.prompt" } set uid [incr uid] } send "deluid\n" expect "GET_BOOL keyedit.remove.uid.okay" {send "yes\n"} expect "GET_LINE keyedit.prompt" {send "save\n"} expect "GOT_IT" } wait send_user "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" chosen successfully. [ expr \$uid_count - 1 ] UIDs deleted from public key \"$KEYID\".\n" interact EOF } # main - umask 0777 export LANG=en_US USED_ARRAYS="MESSAGE GPG_MESSAGE LIST_MESSAGE" # declare all vars declare n for array in $USED_ARRAYS; do declare -a $array done export LANG=en_US # command line checking if [ -z "$2" -a "$1" != "-c" -a "$1" != "-h" -a "$1" != "-v" ]; 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 [ "$1" != "-c" -a "$1" != "-h" -a "$1" != "-v" ]; then if [ -f $CONFIG ]; then shopt -u sourcepath && source $CONFIG else echo "$(basename $0): Configuration file \"$CONFIG\" could not be found." exit 1 fi fi # get gpg parameters and check the config if [ "$1" = "-a" -o "$1" = "-p" -o "$1" = "-r" ]; then gpg_args check_config fi # command line parsing if [ "$1" = "-a" ]; then declare -a ADMINCOMMANDS n=0 while read STDIN; do ADMINCOMMANDS[$n]="$STDIN\n" ((n++)) done for i in $(seq 0 $((${#ADMINCOMMANDS[@]} - 1))); do if [ ! -z "$(echo -ne ${ADMINCOMMANDS[$i]})" -a "$(echo -ne ${ADMINCOMMANDS[$i]} | cut -c1)" != "#" ]; then list_admin $(echo -ne "${ADMINCOMMANDS[$i]}") fi done elif [ "$1" = "-c" ]; then newlist elif [ "$1" = "-h" ]; then usage elif [ "$1" = "-p" ]; then process_message elif [ "$1" = "-r" ]; then list_request elif [ "$1" = "-v" ]; then version else usage exit 1 fi # un-declare all vars declare n for array in $USED_ARRAYS; do unset $array done