#!/usr/bin/perl
#
# This is a simple example PAM authentication agent, it implements a
# simple shared secret authentication scheme. The PAM module pam_secret.so
# is its counter part. Both the agent and the remote server are able to
# authenticate one another, but the server is given the opportunity to
# ignore a failed authentication.
#

$^W = 1;
use strict;
use IPC::Open2;
$| = 1;

# display extra information to STDERR
my $debug = 0;
if (scalar @ARGV) {
    $debug = 1;
}

# Globals

my %state;
my $default_key;

my $next_key = $$;

# loop over binary prompts
for (;;) {
    my ($control, $data) = ReadBinaryPrompt();
    my ($reply_control, $reply_data);

    if ($control == 0) {
	if ($debug) {
	    print STDERR "agent: no packet to read\n";
	}
	last;
    } elsif ($control == 0x02) {
	($reply_control, $reply_data) = HandleAgentSelection($data);
    } elsif ($control == 0x01) {
	($reply_control, $reply_data) = HandleContinuation($data);
    } else {
	if ($debug) {
	    print STDERR
		"agent: unrecognized packet $control {$data} to read\n";
	}
	($reply_control, $reply_data) = (0x04, "");
    }

    WriteBinaryPrompt($reply_control, $reply_data);
}

# Only willing to exit well if we've completed our authentication exchange

if (scalar keys %state) {
    if ($debug) {
	print STDERR "The following sessions are still active:\n  ";
	print STDERR join ', ', keys %state;
	print STDERR "\n";
    }
    exit 1;
} else {
    exit 0;
}

sub HandleAgentSelection ($) {
    my ($data) = @_;

    unless ( $data =~ /^([a-zA-Z0-9_]+\@?[a-zA-Z0-9_.]*)\/(.*)$/ ) {
	return (0x04, "");
    }

    my ($agent_name, $payload) = ($1, $2);
    if ($debug) {
	print STDERR "agent: ". "agent=$agent_name, payload=$payload\n";
    }

    # this agent has a defined name
    if ($agent_name ne "secret\@here") {
	if ($debug) {
	    print STDERR "bad agent name: [$agent_name]\n";
	}
	return (0x04, "");
    }

    # the selection request is acompanied with a hexadecimal cookie
    my @tokens = split '\|', $payload;

    unless ((scalar @tokens) == 2) {
	if ($debug) {
	    print STDERR "bad payload\n";
	}
	return (0x04, "");
    }

    unless ($tokens[1] =~ /^[a-z0-9]+$/) {
	if ($debug) {
	    print STDERR "bad server cookie\n";
	}
	return (0x04, "");
    }

    my $shared_secret = IdentifyLocalSecret($tokens[0]);

    unless (defined $shared_secret) {
	# make a secret up
	if ($debug) {
	    print STDERR "agent: cannot authenticate user\n";
	}
	$shared_secret = GetRandom();
    }

    my $local_cookie = GetRandom();
    $default_key = $next_key++;

    $state{$default_key} = $local_cookie ."|". $tokens[1] ."|". $shared_secret;

    if ($debug) {
	print STDERR "agent: \$state{$default_key} = $state{$default_key}\n";
    }

    return (0x01, $default_key ."|". $local_cookie);
}

sub HandleContinuation ($) {
    my ($data) = @_;

    my ($key, $server_digest) = split '\|', $data;

    unless (defined $state{$key}) {
	# retries and out of sequence prompts are not permitted
	return (0x04, "");
    }

    my $expected_digest = CreateDigest($state{$key});
    my ($local_cookie, $remote_cookie, $shared_secret)
	= split '\|', $state{$key};
    delete $state{$key};

    unless ($expected_digest eq $server_digest) {
	if ($debug) {
	    print STDERR "agent: don't trust server - faking reply\n";
	    print STDERR "agent: got      ($server_digest)\n";
	    print STDERR "agent: expected ($expected_digest)\n";
	}

	## FIXME: Agent should exchange a prompt with the client warning
	## that the server is faking us out.

	return (0x03, CreateDigest($expected_digest . $data . GetRandom()));
    }

    if ($debug) {
	print STDERR "agent: server appears to know the secret\n";
    }

    my $session_authenticated_ticket =
	CreateDigest($remote_cookie."|".$shared_secret."|".$local_cookie);

    # FIXME: Agent should set a derived session key environment
    # variable (available for the client (and its children) to sign
    # future data exchanges.

    if ($debug) {
	print STDERR "agent: should putenv("
	    ."\"AUTH_SESSION_TICKET=$session_authenticated_ticket\")\n";
    }

    # return agent's authenticating digest
    return (0x03, CreateDigest($shared_secret."|".$remote_cookie
			       ."|".$local_cookie));
}

sub ReadBinaryPrompt {
    my $buffer = "     ";
    my $count = read(STDIN, $buffer, 5);
    if ($count == 0) {
	# no more packets to read
	return (0, "");
    }

    if ($count != 5) {
	# broken packet header
	return (-1, "");
    }

    my ($length, $control) = unpack("N C", $buffer);
    if ($length < 5) {
	# broken packet length
	return (-1, "");
    }

    my $data = "";
    $length -= 5;
    while ($count = read(STDIN, $buffer, $length)) {
	$data .= $buffer;
	if ($count != $length) {
	    $length -= $count;
	    next;
	}

	if ($debug) {
	    print STDERR "agent: ". "data is [$data]\n";
	}

	return ($control, $data);
    }

    # broken packet data
    return (-1, "");
}

sub WriteBinaryPrompt ($$) {
    my ($control, $data) = @_;

    my $length = 5 + length($data);
    if ($debug) {
	printf STDERR "agent: ". "{%d|0x%.2x|%s}\n", $length, $control, $data;
    }
    my $bp = pack("N C a*", $length, $control, $data);
    print STDOUT $bp;
    if ($debug) {
	printf STDERR "agent: ". "agent has replied\n";
    }
}

##
## Here is where we parse the simple secret file
## The format of this file is a list of lines of the following form:
##
## user@client0.host.name  secret_string1
## user@client1.host.name  secret_string2
## user@client2.host.name  secret_string3
##

sub IdentifyLocalSecret ($) {
    my ($identifier) = @_;
    my $secret;

    if (open SECRETS, "< ". (getpwuid($<))[7] ."/.secret\@here") {
	my $line;
	while (defined ($line = <SECRETS>)) {
	    my ($id, $sec) = split /[\s]+/, $line;
	    if ((defined $id) && ($id eq $identifier)) {
		$secret = $sec;
		last;
	    }
	}
	close SECRETS;
    }

    return $secret;
}

## Here is where we generate a message digest

sub CreateDigest ($) {
    my ($data) = @_;

    my $pid = open2(\*MD5out, \*MD5in, "/usr/bin/md5sum -")
	or die "you'll need /usr/bin/md5sum installed";

    my $oldfd = select MD5in; $|=1; select $oldfd;
    if ($debug) {
	print STDERR "agent: ". "telling md5: <$data>\n";
    }
    print MD5in "$data";
    close MD5in;
    my $reply = <MD5out>;
    ($reply) = split /\s/, $reply;
    if ($debug) {
	print STDERR "agent: ". "md5 said: <$reply>\n";
    }
    close MD5out;

    return $reply;
}

## get a random number

sub GetRandom {

    if ( -r "/dev/urandom" ) {
	open RANDOM, "< /dev/urandom" or die "crazy";

	my $i;
	my $reply = "";

	for ($i=0; $i<4; ++$i) {
	    my $buffer = "    ";
	    while (read(RANDOM, $buffer, 4) != 4) {
		;
	    }
	    $reply .= sprintf "%.8x", unpack("N", $buffer);
	    if ($debug) {
		print STDERR "growing reply: [$reply]\n";
	    }
	}
	close RANDOM;

	return $reply;
    } else {
	print STDERR "agent: ". "[got linux?]\n";
	return "%.8x%.8x%.8x%.8x", time, time, time, time;
    }

}
