#!/usr/bin/perl #
# filter for pine 3.93 and newer for de/encoding pgp mails
# This uses a feature in Pine, the _DATAFILE_, a file which is 
# created by pine for storing information by filters between
# two runs.
# use this with something like the following in your .pinerc:
# display-filters=_LEADING("-----BEGIN PGP")_ /path/to/papp
#                  -key _PREPENDKEY_ -passfile _DATAFILE_
# sending-filters=/path/to/papp -key _PREPENDKEY_ 
#                 -metoo -encode -passfile  _DATAFILE_ _RECIPIENTS_
# And DONT Forget to have a look at the source!
# This script requires Perl5 (and, of course, PGP :-) and will not work 
# on non-UNIX systems. (It is known to work with SunOS4 and Linux).

# History:
#
# $Id: papp,v 1.24 1997/09/18 17:31:17 gator Exp $
#
# 21.03.96 Roland Rosenfeld  
# 23.03.96 $TMPFILE moved to $PGPPATH.
# 17.05.96 total rewrite in perl; many new features
# 27.08.96 corrected code for adding new keys
# 19.11.96 ask whether to run pgp before running it
# 25.11.96 some compatibility fixes
# x-mas '96 Aldo Valente: Added option "-passfile "
#             to make it cooperative with my script, which is able 
#             to "remember" a given passphrase. This option reads 
#             the cleartext passphrase from   (yuck!)
#             You can always get Rolands Original
#             at http://www.rhein.de/~roland/
#             aldo@rhein.de; http://www.rhein.de/~aldo/
# feb '97 Aldo: moving my bash-script to /dev/null and merging
#           functionality into Rolands perlscript, which was
#           a display-filter before.
#           So the new features are sending of pgpified mails
#           and the option to "remember" a passphrase.
#           changed the Name from pgpdecode to papp (Pine And PgP)
# Mar '97 Aldo:BUGFIX: Ronald.Wahl@Informatik.TU-Chemnitz.DE 
#           told me that PAPP would not work if the recipients List
#           contains a '-'. PAPP thinks of such mailaddresses as an 
#           (unrecognized) option ans complains. Stupid me. Fixed.
# May '97 Peter Daum  Another rewrite:
#            Some Bug fixes. Security enhancements 
#            (Passphrase passed via pipe instead of commandline, 
#            encrypt datafile containing passphrase, option "-key" to 
#            signal use of session key, no more temporary files...)
#            Misc new features (lots of options to control behavior,
#            all options can be set in an rcfile instead of having to
#            answer questions at runtime, ...)
# Sep '97 PASZTOR Miklos  in sub encode: 
#            only ask for recipients when actually encrypting

use strict;			# (be pedantic :-)

umask 077; # actually, no files are created anyway, but just in case ...

use vars ( # global variables:
	  '%opt',	 # global/command line options
	  '%hlp',	 # and descriptions of their usage
	  '$encode', '$dump_cfg',
	  '$passfile', '$recipients', '$session_key',

	  '$passphrase', # pgp "passphrase" to unlock RSA key
	  '$have_pass',  # set if valid passphrase is stored in datafile
	  '$tmp_key',    # key for encrypting the passphrase between calls

	  '$yes', '$zaphod', '$pgp', '$stty', '$rcfile'
	  );

# paths to external programs (change these to suit your locations):
$pgp="/usr/bin/pgp";           # guess what ... ;-)
$stty="/bin/stty";	       # where to find a stty, which supports -cbreak
			       # on SunOS 4, you need /usr/5bin/stty
# other "constants":
$yes="y|j";			# confirmatative user input
$zaphod='zaphod beeblebrox for president'; # taken from pgpdoc2.txt
$rcfile=".pgp4pinerc";		# name of configuration file

$hlp{dont_ask}='0: never run pgp; 1: ask; 2: just do it...';
$opt{dont_ask}=1;
$hlp{auto_decode}='1: decode messages without asking';
$opt{auto_decode}=0;
$hlp{auto_add_keys}='1: add keys to keyring without asking';
$opt{auto_add_keys}=0;
$hlp{auto_store_passphrase}='1: store passphrase without asking';
$opt{auto_store_passphrase}=0;
$hlp{always_encrypt}='1: always encrypt without asking';
$opt{always_encrypt}=0;
$hlp{always_sign}='1: always sign without asking';
$opt{always_sign}=0;
$hlp{override_recipients}='1: explicitly ask for recipients';
$opt{override_recipients}=1;
$hlp{encrypt_to_self}='1: implicitly add sender to list of recipients';
$opt{encrypt_to_self}=1;
$opt{pgp_verbosity}=0;
$hlp{pgp_verbosity}='0: only important stuff reported; 2: lots of noise..';

sub err_exit {
    # it seems like "die" in some cases returns an exit status of 0
    # thus causing errors to stay unnoticed; as a quick fix, all 
    # calls to die have been replaced by calls to this function.
    warn @_; exit (1);
}

sub usage {
    err_exit "USAGE: $0 [-metoo] [-dontask] [encode] [-passfile ] [-key] [recipients]
 
OPTIONS:  -metoo           Enables +EncryptToSelf=on
          -conf            print currently active global options (in a format 
                           suitable for a fresh $rcfile)
          -key             First line of message contains session key
          -dontask         Default is ask every time if You want to use PGP
          -encode          Default is encoding of mail from STDIN
          -passfile   is normally the _DATAFILE_
                           simply, don't use this Option, if you don't want it...
          Everything else is taken as a list of recipients

";
}

sub dump_cfg {
    print "# sample configuration file for $0\n"; 
    print "# redirect to $rcfile in your home directory and edit there.\n\n";
    print "# ('dont_ask' is just for compatibility; leave it set to '1'\n";
    print "# and use 'auto_decode' and 'auto_add_keys' instead.)\n\n";
    for (sort keys(%opt)) {
	printf "%-22s= %s # %s\n", $_, $opt{$_},$hlp{$_}; 
    }
    exit 0;
}

sub stty  {
# Beware! Setting tty modes is system dependent; used modes are:
# cbreak: read char without pressing RETURN
# -echo: turn off echoing.
# You might have to adjust these to work with your OS.

    my ($what, $state)=@_;
    $state= ($state eq "off") ? "-" : "";
    system("$stty ${state}$what < /dev/tty > /dev/tty")
	&& err_exit("$stty failed: $?");
}

sub filter_cmd  {
# usage: filter_cmd "cmd", \@out, \@err, @rest
# execute "cmd" with @rest as input, return exit status
# @out contains cmd's stdout, @err its stderr channel

    my $cmd=shift; my $out=shift; my $err=shift;
    my ($pid, $res);
    local ($?, $^F=10);

    pipe(CHILD_IN,PARENT_OUT); pipe(PARENT_IN,CHILD_OUT);
    pipe(ERR_READI,ERR_WRITE); pipe(ERR_READ,ERR_WRITEI);

    if (($pid = fork) < 0) { err_exit "cant't fork: $!"; }

    elsif (!$pid) { # child
        close(PARENT_IN); close(PARENT_OUT); close(ERR_READ);

	# another child: (poor man's "tee" :-)
	if (($pid = fork) < 0) { err_exit "cant't fork: $!"; }
	elsif (!$pid) {
	    close(CHILD_IN); close(CHILD_OUT); close (ERR_WRITE);
	    open(TTY,">/dev/tty") || err_exit "can't open /dev/tty: $!";
	
	    while () {
		print TTY; print ERR_WRITEI;
	    }
	    exit(0);
	}

	close(ERR_WRITEI); close(ERR_READI);
	open(STDIN,"<&CHILD_IN")  || err_exit "Can't redirect stdin: $!";
	open(STDOUT,">&CHILD_OUT") || err_exit "Can't redirect stdout: $!";
	open(STDERR,">&ERR_WRITE") || err_exit "Can't redirect stderr: $!";
        exec $cmd;
        err_exit "can't exec $cmd: $!";
    }
    
    # parent:
    close(CHILD_IN); close(CHILD_OUT); close (ERR_WRITE);
    close(ERR_READI); close(ERR_WRITEI);

    print PARENT_OUT @_;
    close PARENT_OUT;

    @$out=;
    @$err=;
    close PARENT_IN;

    waitpid($pid,0);
    return $?;
}

sub pgp_strerror {
# map pgp exit codes to error messages
# (taken from ver 2.6.3i; hopefully valid in other versions, too)

    my $sig= ($_[0] & 255); my $ret= ($_[0] >> 8);
    my $msg= " ($ret) (Unknown exit code)";

    my %msg = (
        1  => "INVALID_FILE_ERROR",
        2  => "FILE_NOT_FOUND_ERROR",
        3  => "UNKNOWN_FILE_ERROR",
        4  => "NO_BATCH	",
        5  => "BAD_ARG_ERROR",
        6  => "INTERRUPT",
        7  => "OUT_OF_MEM",
        10 => "KEYGEN_ERROR",	       # Keyring errors: Base value = 10
        11 => "NONEXIST_KEY_ERROR",
        12 => "KEYRING_ADD_ERROR",
        13 => "KEYRING_EXTRACT_ERROR",
        14 => "KEYRING_EDIT_ERROR",
        15 => "KEYRING_VIEW_ERROR",
        16 => "KEYRING_REMOVE_ERROR",
        17 => "KEYRING_CHECK_ERROR",
        18 => "KEY_SIGNATURE_ERROR",
	19 => "KEYSIG_REMOVE_ERROR",
	20 => "SIGNATURE_ERROR",       # Encode errors: Base value = 20
        21 => "RSA_ENCR_ERROR",
        22 => "ENCR_ERROR",
        23 => "COMPRESS_ERROR",
        30 => "SIGNATURE_CHECK_ERROR", # Decode errors: Base value = 30
        31 => "RSA_DECR_ERROR",
        32 => "DECR_ERROR",
        33 => "DECOMPRESS_ERROR");

    $msg = qq/ ($ret) "$msg{$ret}"/ if exists($msg{$ret});
    $msg .= " (Killed by signal $sig)" if $sig;
    return $msg;
}

sub run_pgp {
    my ($status);
    if ($status= filter_cmd @_) {
	warn "@{$_[2]}", "\n" if (@{$_[2]});
	warn "syserror: $!\n" if ($!);
	err_exit "$0: pgp failed: ", pgp_strerror($status);
    }
}

sub swallow_key {
    my $oldenv;

    $oldenv=$ENV{'PGPPASSFD'}; # bug in pgp 2.63: if PGPPASSFD is set,
    delete $ENV{'PGPPASSFD'}; # pgp is looking in "/tmp/pgptemp.$00" ?!
    run_pgp "$pgp -kaf +verbose=$opt{pgp_verbosity}", @_;
    $ENV{'PGPPASSFD'}=$oldenv;
}

sub ask_if {
    # this is a comfortable way to ask random Yes or No questions
    my $key;

    print STDERR $_[0];
    stty "cbreak" => "on";
    open (TTY, " "off";
    print STDERR "\n";
    return ($key =~ /$yes/i);
}

sub index_of {
    # usage: index_of $STRING \@ARRAY
    # returns 1 + index of first element in @ARRAY matching $STRING
    my $search_str = $_[0];
    my @a=@{$_[1]}; my $i;

    for ($i=0; $i <= $#a; $i++) {
	return (1+$i) if ($a[$i] =~ /$search_str/)
    }
    return 0;
}

sub decode {
    # reads message from stdin, 
    # (recursively) runs pgp on "interesting" portions

    my $pgp_msg_start = "^-----BEGIN PGP( SIGNED)? MESSAGE-----\$";
    my $pgp_msg_end   = "^-----END PGP (MESSAGE|SIGNATURE)-----\$";
    my $pgp_key_start = "^-----BEGIN PGP PUBLIC KEY BLOCK-----\$";
    my $pgp_key_end   = "^-----END PGP PUBLIC KEY BLOCK-----\$";
    my ($start_idx, $end_idx, @err, @out);
    
    if (!$opt{dont_ask} 
	or !$opt{auto_decode}
	and !ask_if ('Message signed or encrypted; run PGP? [N/y] '))
    {
	print while (<>);	# nop; just echo message
	return;
    }
    
    my @msg=;		# swallow whole message
    
    while (index_of("^-----BEGIN PGP", \@msg)) {

	if ($start_idx=index_of($pgp_msg_start,\@msg)) {
	    # found pgp message : try to decrypt it
	    unless ($end_idx=index_of($pgp_msg_end,\@msg)) {
		print "| Unterminated PGP message!, please check:";
		print @msg;
		err_exit "$0: can't decode unterminated message";
	    } 
	    
	    @out=@err=();
	    my $passwd= $have_pass ? "$passphrase\n" : "";
	    run_pgp"$pgp +verbose=$opt{pgp_verbosity}"
		,\@out, \@err, $passwd, @msg[($start_idx-1)..($end_idx -1)];
	    substr($msg[$start_idx-1], 0, 0) = "| ";
	    ($msg[$end_idx-1] = $msg[$start_idx-1]) =~ s/BEGIN/END/;
	    splice(@msg,$start_idx, ($end_idx - $start_idx-1), @out);
	    substr($msg[$start_idx-1], 0, 0) = join("| ", "", @err);
	};
	
	if ($start_idx=index_of($pgp_key_start,\@msg)) {
	    # found a public key: run pgp on it
	    unless ($end_idx=index_of($pgp_key_end,\@msg)) {
		print "| Unterminated PGP public key!, please check:";
		print @msg;
		err_exit "$0: can't process unterminated key";
	    } 
	    
	    @out=@err=();
	    swallow_key \@out, \@err, @msg[($start_idx-1) .. ($end_idx -1)] 
		if ($opt{auto_add_keys} 
		    or ask_if("Public key found. Add to keyring? [N/y] "));
	    substr($msg[$start_idx-1], 0, 0) = 
		join("| ", "", @err, @out,"\n| ");
	    substr($msg[$end_idx-1], 0, 0) = "| ";
	}
    }
   print @msg;
}

sub encode {
    # read message from stdin and feed it to pgp for encryption

    my $pgp_cmd='fat';
    my $encrypt_for;
    my $pgp_opts="+clearsig=on +verbose=$opt{pgp_verbosity}";

    ($opt{always_encrypt} || ask_if("Encrypt message? [N/y] "))  and $pgp_cmd .='e';
    ($opt{always_sign} || ask_if("Sign message? [N/y] "))  and $pgp_cmd .='s';
    $opt{encrypt_to_self} and $pgp_opts .= ' +EncryptToSelf=on';
    if ($opt{override_recipients} && $pgp_cmd =~ /e/) {
	print STDERR "recipient(s): [$recipients]\n",
	"Press [RETURN] to confirm or enter the real recipients;\n",
	"Be careful: if you screw up, your mail will be unreadable!\n> ";
	open(TTY,");
	close TTY;
	$recipients= $encrypt_for if ($encrypt_for);
    }

    open(PGP,"| $pgp -$pgp_cmd $pgp_opts $recipients") 
	|| err_exit("Can't execute pgp: $!");
    print PGP "$passphrase\n" if ($have_pass);
    print PGP while (<>);
    close PGP;
    $? && err_exit "Encoding failed!", pgp_strerror($?);
} 

sub check_pass {
    # usage: check_pass $pass
    # check, whether $pass is a valid pgp passphrase for current user

    (!$_[0] || ($_[0] eq "")) && return 0;
    open(PGP,"| $pgp -saft +batchmode  >/dev/null 2>&1")
	|| err_exit("Can't execute pgp: $!");
    print PGP "$_[0]\nxyz\n";
    close PGP;
    return (!$?);
}

sub read_pass {
    # usage: read_pass $passfile
    # try to retrieve pgp passphrase from the given file
    # (which is assumed to be encrypted using $tmp_key)

    my $passfile = shift;
    my @s=stat($passfile);
    my (@err,@out, @encrypted);

    # passphrase given in environment? (extremely insecure!)
    return 1 if  check_pass("$ENV{'PGPPASS'}");

    # Some paranoia checks
    if (! -f _) { 
	warn "file $passfile is not a regular file!\n";
	return 2;
    }
    if (600  != sprintf "%lo", $s[2]  & 07777) {
	warn "file $passfile has invalid permissions\n";
	return 3;
    }
    if (! -o _) {
	warn "file $passfile is not owned by you!\n";
	return 4;
    }
    if($s[3] != 1) {
	warn "there are multiple links to file $passfile!\n";
	return 5;
    }

    # construct temporary password; 
    # use "stat" output, in case no session key was given:
    local $"="";
    $tmp_key="@s[0..5]$tmp_key";
    
    # We may only ask for a phrase, if nothing is in this file
    if (!$s[7]) { return 0; }

    open(ENCRYPTED,"<$passfile") || err_exit("Can't open $passfile");
    @encrypted=;
    close ENCRYPTED;

    run_pgp "$pgp -f", \@out, \@err, "$tmp_key\n", @encrypted;
    $passphrase=@out[0];

    # if this occur, we already asked for the phrase
    return 6 if $passphrase eq $zaphod;
    $have_pass=1;
    return 7 unless $passphrase eq '';
}

sub get_pass {
    # Only once per run, we ask to save a passphrase
    $passphrase=$zaphod;
    
    if ($opt{auto_store_passphrase}
	or ask_if('Store your passphrase for current session? [N/y] ')) {
	print STDERR "Enter passphrase, [RETURN] when done\n",
	"(Cursor will not move) ...> ";

	do {
	    stty "echo" => "off";
	    open(TTY,");
	    stty "echo" => "on";
	    print STDERR "\n";

	    if (check_pass("$passphrase")) { $have_pass=1; } 
	    else {
		print STDERR "Wrong passphrase! Try again >";
		$passphrase=$zaphod; 
	    }
	} until ($have_pass);
    }

    open(PASS,"| $pgp -cf >$passfile") || err_exit("Can't execute pgp: $!");
    print PASS "$tmp_key\n";
    print PASS "$passphrase\n";
    close PASS;
}

sub get_args {
    my $arg;
    while ($arg = shift(@ARGV)) {
	if ($arg =~ /^-metoo$/i)    { $opt{encrypt_to_self}=1;next;}
	if ($arg =~ /^-conf$/i)     { $dump_cfg='true'; next; }
	if ($arg =~ /^-key$/i)      { $session_key='true'; next; }
	if ($arg =~ /^-dontask$/i)  { 
	    $opt{dont_ask}=2; 
	    $opt{auto_add_keys}=$opt{auto_decode}=1; 
	    next; 
	}
	if ($arg =~ /^-passfile$/i) { $passfile=shift(@ARGV); next;}
	if ($arg =~ /^-encode$/)    { $encode='true';next;}
	if (($arg =~ /^--?h(elp)?$/) or ($arg =~ /^-\?$/)) {usage}
	$recipients .= " $arg";
    }
}

sub get_rc_options {
    my $home;
    
    unless ($home = $ENV{'HOME'} || $ENV{'LOGDIR'} || (getpwuid($<))[7]) {
	warn "Who the heck are you?!\n";
	return;
    }
    if (-r "$home/$rcfile") {
	my $line=0;
	open(RC, "$home/$rcfile");
      LINE:
	while () {
	    $line++;
	    if (/^\s*$/) { next LINE } # empty: ignore
	    if (/^\s*(?:;|\#)/) { next LINE };
	    if (/^\s*(\w+)\s*=\s*(\w+)\s*(?:(?:\#|;).*)?$/i) 
	    {
		my ($option, $value) = (lc($1), $2);
		unless (defined $opt{$option}) {
		    warn "unknown option '$option': file "
			,"$home/$rcfile line $line!\n";
		    next LINE;
		}
		if ($option =~ /^(dont_ask|pgp_verbosity)$/) {
		    if ($value =~ /^(0|1|2)$/) 
		    { $opt{$option}= $value; }
		    else {
			warn "illegal value '$value': file "
			    ,"$home/$rcfile line $line!\n";
		    }
		    next LINE;
		}
		if ($value =~ /^(1|on|true)|(0|off|false)$/i)
		{ $opt{$option}= $1 ? 1 : 0; }
		else {
		    warn "illegal value '$value': file "
			,"$home/$rcfile line $line!\n";
		}
		next LINE;
	    }
	    warn "ignoring garbage in $home/$rcfile line $line!\n";
	}
    }
}

# Main ;-)
{
    get_args;
    get_rc_options;
    dump_cfg if $dump_cfg;
    $ENV{'PGPPASSFD'}=0;
    chomp($tmp_key=) if $session_key; # get session key
    read_pass($passfile) or get_pass if $passfile;
    delete $ENV{'PGPPASSFD'} if (!$have_pass);
    
    if ($encode) {encode;}
    else {decode;}  
}