#! /usr/bin/perl

# Copyright Rainer Wichmann (2004)
#
# License Information:
# 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 2 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#

use warnings;
use strict;
use Getopt::Long;
use File::Basename;
use File::Copy;
use File::stat;
use File::Temp qw/ tempfile tempdir unlink0 /;
use IO::Handle;
use Fcntl qw(:DEFAULT :flock);
use Tie::File;

# Do I/O to the data file in binary mode (so it 
# wouldn't complain about invalid UTF-8 characters).
use bytes;

File::Temp->safe_level( File::Temp::HIGH );

my %opts = ();
my $action;
my $file1;
my $file2;
my $passphrase;
my $secretkeyring;
my $return_from_sign = 0;
my $no_print_examine = 0;
my $no_remove_lock   = 0;
my $base = basename($0);

my $cfgfile  = "@myconffile@";
my $datafile = "@mydatafile@";
my $daemon   = "@sbindir@/@install_name@";
my $gpg      = "@mygpg@";

my $TARGETKEYID = "@mykeyid@";
my $KEYTAG      = "@mykeytag@";

$cfgfile  =~ s/^REQ_FROM_SERVER//;
$datafile =~ s/^REQ_FROM_SERVER//;

$gpg = "gpg" if ($gpg eq "");

sub check_gpg_agent() {
    my  $gpgconf = "$ENV{'HOME'}/.gnupg/gpg.conf";

    if (!-f "$gpgconf") {
	$gpgconf = "$ENV{'HOME'}/.gnupg/options";
    }

    if (-f $gpgconf) {

	my @array = ();
	tie @array, 'Tie::File', $gpgconf or die "Cannot tie ${gpgconf}: $!";
	my @grep = grep(/^\s*use-agent/, @array);
	
	# print "matches = $#grep\n";
	
	if ($#grep >= 0)
	{
	    if (exists $ENV{'GPG_AGENT_INFO'})
	    {
		my $socke = $ENV{'GPG_AGENT_INFO'};
		$socke =~ s/:.*//;
		
		# print "socke = $socke\n";
		
		if (! -S $socke)
		{
		    print "--------------------------------------------------\n";
		    print "\n";
		    print " GPG is set to use gpg-agent, but GPG agent is";
		    print " not running, though GPG_AGENT_INFO is defined.\n\n";
		    print " Please restart gpg-agent, or remove the use-agent\n";
		    print " option from ${gpgconf} and unset GPG_AGENT_INFO\n\n";
		    print "--------------------------------------------------\n";
		    print "\n";
		    exit 1;
		}
	    }
	    else
	    {
		print "--------------------------------------------------\n";
		print "\n";
		print " GPG is set to use gpg-agent, but ";
		print " GPG_AGENT_INFO is not defined.\n\n";
		print " Please start gpg-agent, or remove the use-agent\n";
		print " option from ${gpgconf}\n\n";
		print "--------------------------------------------------\n";
		print "\n";
		exit 1;
	    }
	}
	untie @array;
    }
}


sub usage() {
    print "Usage:\n";
    print "  $base { -m F | --create-cfgfile }    [options] [in.cfgfile]\n";
    print "    Sign the configuration file. If in.cfgfile is given, sign it\n";
    print "    and install it as configuration file.\n\n";

    print "  $base { -m f | --print-cfgfile }     [options] \n";
    print "    Print the configuration file to stdout. Signatures are removed.\n\n";

    print "  $base { -m D | --create-datafile }   [options] [in.datafile]\n";
    print "    Sign the database file. If in.datafile is given, sign it\n";
    print "    and install it as database file.\n\n";

    print "  $base { -m d | --print-datafile }    [options] \n";
    print "    Print the database file to stdout. Signatures are removed. Use\n";
    print "    option --list to list files in database rather than printing the raw file.\n\n";

    print "  $base { -m R | --remove-signature }  [options] file1 [file2 ...]\n";
    print "    Remove cleartext signature from input file(s). The file\n";
    print "    is replaced by the non-signed file.\n\n";

    print "  $base { -m E | --sign }              [options] file1 [file2 ...]\n";
    print "    Sign file(s) with a cleartext signature. The file\n";
    print "    is replaced by the signed file.\n\n";

    print "  $base { -m e | --examine }           [options] file1 [file2 ...]\n";
    print "    Report signature status of file(s).\n\n";

    print "  $base { -m G | --generate-keys }     [options] \n";
    print "    Generate a PGP keypair to use for signing.\n\n";

    print "Options:\n";
    print "  -c cfgfile    --cfgfile cfgfile\n";
    print "    Select an alternate configuration file.\n\n";

    print "  -d datafile   --datafile datafile\n";
    print "    Select an alternate database file.\n\n";

    print "  -p passphrase --passphrase passphrase\n";
    print "    Set the passphrase for gpg. By default, gpg will ask.\n\n";

    print "  -s gnupg_homedir --secretkeyring gnupg_homedir\n";
    print "    Select an alternate gpg homedirectory to locate the secret keyring.\n";
    print "    Will use '$ENV{'HOME'}/.gnupg/' by default.\n\n";

    print "  -k keyid      --keyid keyid\n";
    print "   Select the keyid to use for signing.\n\n";

    print "  -l            --list\n";
    print "    List the files in database rather than printing the raw file.\n\n";

    print "  -v            --verbose\n";
    print "    Verbose output.\n\n";
    return;
}

sub check_gpg_uid () {
    if (0 != $>) {
	print "--------------------------------------------------\n";
	print "\n";
	print " You are not root. Please remember that samhain/yule\n";
	print " will use the public keyring of root to verify a signature.\n";
	print "\n";
	print "--------------------------------------------------\n";
    } else {
	if (!("@yulectl_prg@" =~ //)) {
	    print "--------------------------------------------------\n";
	    print "\n";
	    print " Please remember that yule will drop root after startup. Signature\n";
	    print " verification on SIGHUP will fail if you do not import the public key\n";
	    print " into the keyring of the non-root yule user.\n";
	    print "\n";
	    print "--------------------------------------------------\n";
	}
    }
}
    
sub check_gpg_sign () {
    if ( defined($secretkeyring)) {
        if ( (!-d "$secretkeyring")){
            print "--------------------------------------------------\n";
            print "\n";
            print " Secret keyring $secretkeyring not found!\n";
            print "\n";
            print " Please check the path/name of the alternate secret keyring.\n";
            print "\n";
            print "--------------------------------------------------\n";
            print "\n";
            exit;
        }
    } else {
        if ( (!-d "$ENV{'HOME'}/.gnupg") || (!-e "$ENV{'HOME'}/.gnupg/secring.gpg")) {
	    print "--------------------------------------------------\n";
	    print "\n";
	    if (!-d "$ENV{'HOME'}/.gnupg") {
	        print " Directory \$HOME/.gnupg not found!\n";
	    } else {
	        print " Secret keyring \$HOME/.gnupg/secring.gpg not found!\n";
	    }
	    print "\n";
	    print " This indicates that you have never created a \n";
	    print " public/private keypair, and thus cannot sign.\n";
	    print " \n";
	    print " Please use $0 --generate-keys or gpg --gen-key\n";
	    print " to generate a public/private keypair first.\n";
	    print "\n";
	    print "--------------------------------------------------\n";
	    print "\n";
	    exit;
        }
    }
}

sub check_gpg_verify () {
    if ( (!-d "$ENV{'HOME'}/.gnupg") || (!-e "$ENV{'HOME'}/.gnupg/pubring.gpg")) {
	print "--------------------------------------------------\n";
	print "\n";
	if (!-d "$ENV{'HOME'}/.gnupg") {
	    print " Directory \$HOME/.gnupg not found!\n";
	} else {
	    print " Public keyring \$HOME/.gnupg/pubring.gpg not found!\n";
	}
	print "\n";
	print " This indicates that you have never used gpg before \n";
	print " and/or have no public keys to verify signatures.\n";
	print " \n";
	print " Please use 'gpg --export key_id' to export the public\n";
	print " signing key of the user who is signing the\n";
	print " configuration/database files.\n\n";
	print " Then you can use 'gpg --import keyfile' to import the key\n";
	print " into this user's public keyring.\n";
	print "\n";
	print "--------------------------------------------------\n";
	print "\n";
	exit;
    }
}


sub generate () {
    my $command = "$gpg --homedir $ENV{'HOME'}/.gnupg --gen-key";
    check_gpg_uid();
    system ($command) == 0 
	or die "system $command failed: $?";
    exit;
}

sub examine () {
    my $iscfg = 0;
    my $have_fp  = 0;
    my $have_sig = 0;
    my $message = '';
    my $retval  = 9;
    my $fh;
    my $filename;

    if (!($file1 =~ /^\-$/)) {
	die ("Cannot open $file1 for read: $!") unless ((-e $file1) && (-r _));
    }
    open FIN,  "<$file1" or die "Cannot open $file1 for read: $!";

    my $dir = tempdir( CLEANUP => 1 );
    $filename = $dir . "/exa_jhfdbilw." . $$;
    open $fh, ">$filename" or die "Cannot open $filename";
    autoflush $fh 1;

    while (<FIN>) {
	print $fh $_;
	if ($_ =~ /^\s*\[Misc\]/) {
	    $iscfg = 1;
	}
    }
    if ($iscfg == 1) {
	$message .=  "File $file1 is a configuration file\n\n";
    } else {
	$message .=  "File $file1 is a database file\n\n";
    }


    my $command = "$gpg --homedir $ENV{'HOME'}/.gnupg --status-fd 1 ";
    $command .= "--verbose " if (defined($opts{'v'}));
    $command .= "--verify $filename ";
    if (defined($opts{'v'})) {
	$command .= "2>&1";
    } else {
	$command .= "2>/dev/null";
    }

    print STDOUT "Using: $command\n\n" if (defined($opts{'v'}));
    open  GPGIN, "$command |" or die "Cannot fork: $!";

    while (<GPGIN>) {
	if ($_ =~ /^\[GNUPG:\] GOODSIG ([0-9A-F]+) (.*)$/) {
	    $message .= "GOOD signature with key: $1\n";
	    $message .= "Key owner:               $2\n";
	    $have_sig = 1;
	    $retval   = 0;
	}
	if ($_ =~ /^\[GNUPG:\] VALIDSIG ([0-9A-F]+) ([0-9\-]+)\s/) {
	    $message .= "Key fingerprint:         $1\n";
	    $message .= "Signature generated on:  $2\n\n";
	    $have_fp = 1;
	    $message .=  "This file is signed with a valid signature.\n" 
		if ($have_sig == 1);
	    $have_sig = 1;
	    $have_fp = 1;
	}
	if ($_ =~ /^\[GNUPG:\] NODATA 1/) {
	    $message .=  "NO signature found.\n\n";
	    $message .=  "This file is not signed !!!\n";
	    $have_sig = 1;
	    $have_fp = 1;
	    $retval  = 2;
	}
	if ($_ =~ /^\[GNUPG:\] BADSIG ([0-9A-F]+) (.*)$/) {
	    $message .=  "BAD signature with key: $1\n";
	    $message .=  "Key owner:              $2\n\n";
	    $message .=  "This file is signed with an invalid signature !!!\n";
	    $have_sig = 1;
	    $have_fp = 1;
	    $retval = 1;
	}
	if ($_ =~ /^\[GNUPG:\] NO_PUBKEY ([0-9A-F]+)/) {
	    $message .=  "NOT CHECKED signature with key: $1\n\n";
	    $message .=  "The signature of this file cannot be checked: no public key available !!!\n";
	    $have_sig = 1;
	    $have_fp = 1;
	    $retval  = 1;
	}
	print STDOUT $_ if (defined($opts{'v'}));
    }
    close (GPGIN);
    print STDOUT "\n" if (defined($opts{'v'}));
    if ($have_sig == 0) {
	$message .=  "NO valid signature found\n";
    } 
    elsif ($have_fp == 0) {
	$message .=  "NO fingerprint found\n";
    }
    close (FIN);
    if ($no_print_examine == 0) {
	print STDOUT $message;
    }
    unlink0( $fh, $filename ) or die "Cannot unlink $filename safely";
    return $retval;
}

sub remove () {
    my $bodystart = 1;
    my $sigstart  = 0;
    my $sigend    = 0;
    my $filename  = "";
    my $fh;
    my $stats;

    open FH, "<$file1" or die "Cannot open file $file1 for read: $!";
    if (!($file1 =~ /^\-$/)) {
	flock(FH, LOCK_EX) unless ($no_remove_lock == 1);
	my $dir = tempdir( CLEANUP => 1 ) or die "Tempdir failed";
	$filename = $dir . "/rem_iqegBCQb." . $$;
	open $fh, ">$filename" or die "Cannot open $filename";
	$stats = stat($file1);
	# ($fh, $filename) = tempfile(UNLINK => 1);
    } else {
	open $fh, ">$file1" or die "Cannot open file $file1 for write: $!";
    }
    autoflush $fh 1;
    while (<FH>) {
	if ($_ =~ /^-----BEGIN PGP SIGNED MESSAGE-----/) {
	    $sigstart = 1;
	    $bodystart = 0;
	    next;
	} elsif (($sigstart == 1) && ($_ =~ /^\s+$/)) {
	    $sigstart = 0;
	    $bodystart = 1;
	    next;
	} elsif ($_ =~ /^-----BEGIN PGP SIGNATURE-----/) {
	    $bodystart = 0;
	    $sigend = 1;
	    next;
	} elsif (($sigend == 1) && ($_ =~ /^-----END PGP SIGNATURE-----/)) {
	    $sigend = 0;
	    $bodystart = 1;
	    next;
	}
	if ($bodystart == 1) {
	    print $fh $_;
	}
    }
    if (!($file1 =~ /^\-$/)) {
	copy("$filename", "$file1") 
	    or die "Copy $filename to $file1 failed: $!";
	chmod $stats->mode, $file1;
	chown $stats->uid, $stats->gid, $file1;
	flock(FH, LOCK_UN) unless ($no_remove_lock == 1);
	close FH;
    }
    unlink0( $fh, $filename ) or die "Cannot unlink $filename safely";
    return;
}

sub print_cfgfile () {
    my $bodystart = 0;
    my $sigstart  = 0;

    if (!defined($file2)) {
	$file2 = '-';
    }

    open FH, "<$file1" or die "Cannot open file $file1 for read: $!";
    open FO, ">$file2" or die "Cannot open file $file2 for write: $!";
    while (<FH>) {
	if ($_ =~ /^-----BEGIN PGP SIGNED MESSAGE-----/) {
	    $sigstart = 1;
	    next;
	} elsif (($sigstart == 1) && ($_ =~ /^\s+$/)) {
	    $sigstart = 0;
	    $bodystart = 1;
	    next;
	} elsif ($_ =~ /^-----BEGIN PGP SIGNATURE-----/) {
	    $bodystart = 0;
	    exit;
	}
	if ($bodystart == 1) {
	    print FO $_;
	}
    }
    exit;
}
sub print_datafile () {
    die ("Cannot find program $daemon") 
	unless (-e $daemon);
    if (defined($opts{'v'})) {
	open FH, "$daemon --full-detail -d $datafile |" 
	    or die "Cannot open datafile $datafile for read: $!";
    } else {
	open FH, "$daemon -d $datafile |" 
	    or die "Cannot open datafile $datafile for read: $!";
    }
    while (<FH>) {
	print $_;
    }
    exit;
}

sub sign_file () {

    my $fileout = '';
    my $bodystart = 1;
    my $sigstart  = 0;
    my $sigend    = 0;
    my $stats;
    my $fh1;
    my $filename1;
    my $flag1     = 0;

    check_gpg_uid();
    check_gpg_agent();

    if (!defined($file2)) {
	$file2 = $file1;
    }

    if ($file1 =~ /^\-$/) {
	my $dir = tempdir( CLEANUP => 1 ) or die "Tempdir failed";
	$filename1 = $dir . "/sig_vs8827sd." . $$;
	open $fh1, ">$filename1" or die "Cannot open $filename1";
	$flag1 = 1;
	# my ($fh1, $filename1) = tempfile(UNLINK => 1);

	while (<STDIN>) {
	  if ($_ =~ /^-----BEGIN PGP SIGNED MESSAGE-----/) {
	    $sigstart = 1;
	    $bodystart = 0;
	    next;
	  } elsif (($sigstart == 1) && ($_ =~ /^\s+$/)) {
	    $sigstart = 0;
	    $bodystart = 1;
	    next;
	  } elsif ($_ =~ /^-----BEGIN PGP SIGNATURE-----/) {
	    $bodystart = 0;
	    $sigend = 1;
	    next;
	  } elsif (($sigend == 1) && ($_ =~ /^-----END PGP SIGNATURE-----/)) {
	    $sigend = 0;
	    $bodystart = 1;
	    next;
	  }
	  if ($bodystart == 1) {
	    print $fh1 $_;
	  }
	  #
	  # print $fh1 $_;
	  #
	}
	$file1 = $filename1;
	$fileout = '-';
    } else {
	open (LOCKFILE, "<$file1") or die "Cannot open $file1: $!";
	flock(LOCKFILE, LOCK_EX);
	$no_print_examine = 1;
	$no_remove_lock   = 1;
	if (examine() < 2) {
	    remove();
	}
	$fileout = $file1 . ".asc";
	$stats   = stat($file1)
	    or die "No file $file1: $!";
    }

    if (defined($passphrase)) {
	local $SIG{PIPE} = 'IGNORE';
	my $command = "$gpg --homedir $ENV{'HOME'}/.gnupg --passphrase-fd 0 -a ${KEYTAG} ${TARGETKEYID} --clearsign -o $fileout --not-dash-escaped ";
        $command .= "--secret-keyring $secretkeyring " if (defined($opts{'s'}));
	$command .= "$file1";
	open (FH, "|$command")  or die "can't fork: $!";
	print FH "$passphrase"  or die "can't write: $!";
	close FH                or die "can't close: status=$?";
    } else {
	my $command = "$gpg --homedir $ENV{'HOME'}/.gnupg                   -a ${KEYTAG} ${TARGETKEYID} --clearsign -o $fileout --not-dash-escaped ";
        $command .= "--secret-keyring $secretkeyring " if (defined($opts{'s'}));
	$command .= "$file1";
	system("$command") == 0 
	    or die "system $command failed: $?";
    }

    if (!($fileout =~ /^\-$/)) {
	my $st_old = stat($file1) 
	    or die "No file $file1: $!";
	my $st_new = stat($fileout) 
	    or die "No file $fileout: $!";
	die ("Signed file is smaller than unsigned file") 
	    unless ($st_new->size > $st_old->size);
	move("$fileout", "$file2") 
	    or die "Move $fileout to $file2 failed: $!";
	chmod $stats->mode, $file2;
	chown $stats->uid, $stats->gid, $file2;
	flock(LOCKFILE, LOCK_UN);
    }

    if ($flag1 == 1) {
	unlink0( $fh1, $filename1 ) or die "Cannot unlink $filename1 safely";
    }
    if ($return_from_sign == 1) {
	return;
    }
    exit;
}

Getopt::Long::Configure ("posix_default");
Getopt::Long::Configure ("bundling");
# Getopt::Long::Configure ("debug");

GetOptions (\%opts, 'm=s', 'h|help', 'v|verbose', 'l|list',
	    'c|cfgfile=s',
	    'd|datafile=s',
	    'p|passphrase=s',
	    's|secretkeyring=s',
            'k|keyid=s',
	    'create-cfgfile',  # -m F
	    'print-cfgfile',   # -m f
	    'create-datafile', # -m D
	    'print-datafile',  # -m d
	    'remove-signature',# -m R
	    'sign',            # -m E
	    'examine',         # -m e
	    'generate-keys');  # -m G

if (defined ($opts{'h'})) {
    usage();
    exit;
}

if (defined($opts{'k'})) {
    $TARGETKEYID = $opts{'k'};
    $KEYTAG = "--default-key";
}
if (defined($opts{'c'})) {
    $cfgfile = $opts{'c'};
}
if (defined($opts{'d'})) {
    $datafile = $opts{'d'};
}
if (defined($opts{'p'})) {
    $passphrase = $opts{'p'};
}
if (defined($opts{'s'})) {
    $secretkeyring = $opts{'s'};
}

if (defined ($opts{'m'}) && ($opts{'m'} =~ /[FfDdREeG]{1}/) ) {
    $action = $opts{'m'};
}
elsif (defined ($opts{'create-cfgfile'})) {
    $action = 'F';
}
elsif (defined ($opts{'print-cfgfile'})) {
    $action = 'f';
}
elsif (defined ($opts{'create-datafile'})) {
    $action = 'D';
}
elsif (defined ($opts{'print-datafile'})) {
    $action = 'd';
}
elsif (defined ($opts{'remove-signature'})) {
    $action = 'R';
}
elsif (defined ($opts{'sign'})) {
    $action = 'E';
}
elsif (defined ($opts{'examine'})) {
    $action = 'e';
}
elsif (defined ($opts{'generate-keys'})) {
    $action = 'G';
}
else {
    usage();
    die ("No valid action specified !");
}

if (defined($ARGV[0])) {
    $file1 = $ARGV[0];
} 
if (defined($ARGV[1])) {
    $file2 = $ARGV[1];
} 


if (($action =~ /[REe]{1}/) && !defined($file1)) {
    usage();
    die("Option -m $action requires a filename (or '-' for stdio)\n");
}

if ($action =~ /^F$/) {
    if (!defined($file1)) {
	$file1 = $cfgfile;
    }
    $file2 = $cfgfile;
    sign_file ();
}

if ($action =~ /^D$/) {
    if (!defined($file1)) {
	$file1 = $datafile;
    }
    $file2 = $datafile;
    sign_file ();
}

if ($action =~ /^R$/) {
    # $file1 defined
    my $i = 0;
    while (defined($ARGV[$i])) {
	$file1 = $ARGV[$i];
	remove ();
	++$i;
    }
}

if ($action =~ /^E$/) {
    # $file1 defined
    # default: $file2 = $file1
    check_gpg_sign();
    my $i = 0;
    while (defined($ARGV[$i])) {
	$file1 = $ARGV[$i];
	$file2 = $file1;
	$return_from_sign = 1;
	sign_file ();
	++$i;
    }
}

if ($action =~ /^e$/) {
    # $file1 defined
    # default: $file2 = stdout
    check_gpg_verify();
    my $i = 0;
    my $ret = 0;
    while (defined($ARGV[$i])) {
	print "\n";
	$file1 = $ARGV[$i];
	$ret += examine ();
	++$i;
	print "\n--------------------------------\n" if (defined($ARGV[$i]));
    }
    exit($ret);
}

if ($action =~ /^f$/) {
    $file1 = $cfgfile;
    $file2 = "-";
    print_cfgfile ();
}

if ($action =~ /^d$/) {
    # $file1 irrelevant
    if (defined($opts{'l'})) {
	print_datafile ();
    } else {
	$file1 = $datafile;
	$file2 = "-";
	print_cfgfile ();
    }
}



