#!/usr/bin/perl -w
# version 3.5 Jan 2009

#
# register a machine with the KDC, set up /etc/srvtab and /etc/keytab
# combines old /usr/sue/etc/kas and /usr/sue/dist/feature/srvtab/update.pl
# assumes some kerberos5 is installed

$ENV{'PATH'} = "/bin:/usr/bin:/sbin"; 

use Getopt::Std;
use strict;
use File::Temp qw/ tempfile tempdir /;
use Socket;
use vars qw($opt_w $opt_s $opt_f $opt_v $opt_d $opt_n $opt_h $opt_r $opt_I $opt_k $opt_t $opt_p $opt_e $opt_x $opt_b);

my($arc_cmd,$arc_server,$pagsh);
my(@query,$cmd,$Mrun);
my($tmpfile);
my($keytab_mixed,$keytab_windows,$keytab_linux,$keytab_register,$keytab);
my($k5_id, $krb5_conf_windows, $krb5_conf_linux, $krb5_conf_mixed);
my($cern_config_keytab,$host);
my($sbin,$bin);
my $history_size = 3;
my $logfile = "/var/log/c-c-k.log";
my $ppwdfile = "";

sub HELP_MESSAGE() {
        print <<EOFhelp
$0: create the required kerberos keytabs by running "arc kas .."
   -w windows_hostname: server to contact which will provide your windows keytab
   -s linux_hostname: server to contact which will provide your linux keytab
   -f: overwrite existing keytabs
   -v: verbose mode
   -d: more debugging
   -n: do nothing, just list what it would do
   -I: create machine identity in the protection database
   -h: this help
   -k keytab_name: place the keytab in the following file
   -t service_type: acquire a key of type <service_type>, ie. 'cvs'. Default is 'host'
   -p: preserve all service keys. Do not discard any of the previous service keys from the keytab, even if there are more than 3 sets
   -r: remove all service keys not in use
   -e: do not attempt to extract new keys, just erase the oldest keys (default is to preserve 3 sets)
   -x: extract new keys, even if the current keys are functional
   -b: plain_pswd_file Applies to Dual-Boot machines. Returns plain password with keytab and places in plain_pswd_file. To be used as part of instructions found at: https://twiki.cern.ch/twiki/bin/view/AFSService/MigrationFAQ
 You will need to be root to run this.
EOFhelp
}
getopts('hnvfdpxet:k:b:m:rw:s:I');

$Mrun = ($opt_n) ? "would run" : ($opt_v) ? "running" : "" ;

$opt_v = 1 if $opt_d;

$krb5_conf_windows = "/etc/krb5.conf.windows";
$krb5_conf_linux = "/etc/krb5.conf.linux";
$krb5_conf_mixed = "/etc/krb5.conf";

$keytab_mixed = "/etc/krb5.keytab";
$keytab_windows = "/etc/krb5.keytab.windows";
$keytab_linux = "/etc/krb5.keytab.linux";

my $cp_path = "/bin/cp";
my $chcon_path = "/usr/bin/chcon";
my $selinux_root_context = "root:object_r:etc_t";
my $selinux_system_context = "system_u:object_r:etc_t";

my $selinux_root_context_slc6 = "unconfined_u:object_r:etc_t:s0";
my $selinux_system_context_slc6 = "system_u:object_r:etc_t:s0";

my $sbin_heimdal    = "/usr/heimdal/sbin";
my $sbin_mit        = "/usr/kerberos/sbin";
my $default_config = &Determine_Default_Configuration_Flavour;

my $tmpconf;

my $arc     = find_cmd("arc") ||
    die "Error: cannot find 'arc' in standard paths\n";

$host = `hostname`;
chomp($host);
unless (($host =~ /^[^\.]+\.[^\.]+\.[^\.]+/) && ($host !~ /^localhost|localdomain$/)) {
   my(@a) = gethostbyname($host);
   $host = shift @a;        # long hostname
   unless (($host =~ /^[^\.]+\.[^\.]+\.[^\.]+/) && ($host !~ /^localhost|localdomain$/)) {
          die "Error: host doesnot resolve to a fully qualified name: $host\n";
   }
}

my $report_server = "lxkerb02";
my $command_verify_success = "$arc -h $report_server -t300 -T300";

my $stat_cmd = "/usr/bin/stat -c %a";
my $cat_cmd = "/bin/cat";
my $release_file = "/etc/redhat-release";
my $data_linux = "";
my $data_windows = "";
my $data_mixed = "";

my $kinit_return_success = 256;

if ($opt_k)
{
    $keytab_mixed = $opt_k;
    $keytab_windows = $opt_k . ".windows";
    $keytab_linux = $opt_k . ".linux";
}

if ($opt_h or  $#ARGV>$[ ) {
        HELP_MESSAGE();
        exit 1;
}

if ($opt_b)
{
    $ppwdfile = $opt_b;
}
else
{
    $ppwdfile = "/etc/krb5.keytab.plaintext";
}

if (-e $cat_cmd && -e $release_file)
{
    my $uname_ret = `$cat_cmd $release_file`;
    if ($uname_ret =~ "6")
    {
        $selinux_root_context = $selinux_system_context_slc6;
        $selinux_system_context = $selinux_system_context_slc6;
        $sbin_mit = "/usr/bin";
    }
}

if ( -d $sbin_mit) {
   $sbin   = ( -x "$sbin_mit/ktutil" ) ? $sbin_mit :
             ( -x "/usr/sbin/ktutil" ) ? "/usr/sbin" :
             ( -x "/usr/bin/ktutil" )  ? "/usr/bin"  :
                  "";
   die "Error: Cannot locate a good copy of ktutil\n" unless $sbin;
   ($bin = $sbin) =~ s!/sbin$!/bin!;
   $pagsh = "";
   @query = "";
 
   $kinit_return_success = 0;
} else {
   $sbin    = $sbin_heimdal;
   ($bin = $sbin) =~ s!/sbin$!/bin!;
   $pagsh = "";
}

if ($opt_t)
{
    $opt_t = "" if ($opt_t eq "host");
    verbose("You have requested a non-host keytab. This may take up to 10 minutes if the account was not already registered in Active Directory",1);
}

if ($opt_r)
{
    $history_size = 1;
}
if ($opt_x)
{
    $history_size = 0;
}

#Record the permissions of the keytabs as they are. We need to change them, but it will be useful for debugging to know what they were.
&Get_Keytab_Permissions;

if (-e $keytab_mixed)
{
   # Make sure the permissions, ownership and selinux context are correct
   chmod 0600, $keytab_mixed or verbose("Couldn't chmod $keytab_mixed: $!");
   chown 0, 0, $keytab_mixed or verbose("Couldn't chown $keytab_linux: $!");
   system("$chcon_path $selinux_root_context $keytab_mixed") if -e $chcon_path;
}
if (-e $keytab_windows)
{
   # Make sure the permissions, ownership and selinux context are correct
   chmod 0600, $keytab_windows or verbose("Couldn't chmod $keytab_windows: $!");
   chown 0, 0, $keytab_windows or verbose("Couldn't chown $keytab_linux: $!");
   system("$chcon_path $selinux_root_context $keytab_windows") if -e $chcon_path;
}

if (-e $keytab_linux)
{
   # Make sure the permissions, ownership and selinux context are correct
   chmod 0600, $keytab_linux or verbose("Couldn't chmod $keytab_linux: $!");
   chown 0, 0, $keytab_linux or verbose("Couldn't chown $keytab_linux: $!");
   system("$chcon_path $selinux_root_context $keytab_linux") if -e $chcon_path;
}



if ($opt_e && !$opt_p )
{
    if ($opt_r)
    {
        $history_size = 1;
    }

    if ((-e $krb5_conf_linux) && (-e $krb5_conf_windows))
    {
        &Remove_Keys($keytab_linux,&Determine_Keys_To_Remove($keytab_linux,$history_size));
        &Remove_Keys($keytab_windows,&Determine_Keys_To_Remove($keytab_windows,$history_size));
        system("$cp_path $keytab_linux $keytab_mixed\n");
        &Merge_Keytabs($keytab_windows, $keytab_mixed);
    }
    else
    {
        &Remove_Keys($keytab_mixed,&Determine_Keys_To_Remove($keytab_mixed,$history_size));
    }

    &Restore_SE_Linux_Context;

    verbose("Success: Historical keys cleaned from keytab(s)",1);
    verbose("Verbose output logged in $logfile.",1);
    exit 0;
}

# If this machine does not have both 'alternative' krb5.confs, one keytab is acuired as before.
unless ((-e $krb5_conf_linux) && (-e $krb5_conf_windows))
{
   # Extract a linux keytab
   verbose("Single Kerberos realm configuration.",1);

   my $default_realm_flag = "";
   my $realm_server = $opt_s;

   # Check what realm we are in
   if ($default_config eq "W")
   {
       # Check what realm we are in
       $default_realm_flag = "-w" if ($default_config eq "W");
       $realm_server = $opt_w;    
   }

   &Remove_Keys($keytab_mixed,&Determine_Keys_To_Remove($keytab_mixed,$history_size)) if (!$opt_p && -e $keytab_mixed);

   my $extraction_result = &Construct_Keytab($default_realm_flag,$keytab_mixed,(($realm_server) ? $realm_server : ""),$krb5_conf_mixed); 

   if ($extraction_result != 2)
   {
      if ($default_config eq "W")
      {
          &System("KRB5_CONFIG=$krb5_conf_mixed $command_verify_success verifyextsinglewindows $data_mixed");
      }
      else
      {
          &System("KRB5_CONFIG=$krb5_conf_mixed $command_verify_success verifyextsinglelinux $data_mixed");
      }
   }

   if ($extraction_result == 1)
   {
       die "Error: Invalid primary keytab returned\n";
   }

   &Restore_SE_Linux_Context;

   # Only one keytab was needed, we can finish now.

   verbose("Success: keytab entries now merged in $keytab_mixed",1);
   verbose("Verbose output logged in $logfile.",1);
   exit 0;
}

verbose("Dual Kerberos realm configuration.",1);

if ($opt_h or  $#ARGV>$[ ) {
        HELP_MESSAGE();
        exit 1;
}

# If there is already a linux keytab, we have already merged the original keys and therefore can run as normal.
#if ((-e $keytab_linux) || (! -e $keytab_mixed))
if((-e $keytab_mixed) && (! -e $keytab_linux))
{
    &System("$cp_path $keytab_mixed $keytab_linux\n");
    
}

my $first_keytab = $keytab_windows;
my $second_keytab = $keytab_linux;
my $first_config = $krb5_conf_windows;
my $second_config = $krb5_conf_linux;

my $data_first = $data_windows;
my $data_second = $data_linux;

my $first_validation_function = "verifyextwindows";
my $second_validation_function = "verifyextlinux";


my $first_realm_flag = "-w";
my $second_realm_flag = "";
my $first_realm_server = $opt_w;
my $second_realm_server = $opt_s;

# If the windows realm is the default, then the order is reversed (the default realm must be done last)
if ($default_config eq "W")
{
    $first_realm_flag = "";
    $first_keytab = $keytab_linux;
    $first_config = $krb5_conf_linux;
    $first_realm_server = $opt_s;

    $data_first = $data_linux;
    $data_second = $data_windows;

    $first_validation_function = "verifyextlinux";
    $second_validation_function = "verifyextwindows";

    $second_realm_flag = "-w";
    $second_keytab = $keytab_windows;
    $second_config = $krb5_conf_windows;
    $second_realm_server = $opt_w;
}

&Remove_Keys($first_keytab,&Determine_Keys_To_Remove($first_keytab,$history_size)) if (!$opt_p && -e $first_keytab);

my $first_extraction_result = &Construct_Keytab($first_realm_flag,$first_keytab,(($first_realm_server) ? $first_realm_server : ""),$first_config);

# If some keys were extracted, check the keytab
if ($first_extraction_result != 2)
{
    &System("KRB5_CONFIG=$first_config $command_verify_success $first_validation_function $data_first");   
}

# Extract first keytab and copy
if ($first_extraction_result != 1)
{
    system("$cp_path $first_keytab $keytab_mixed\n") if (-e $first_keytab);
}
else
{
    verbose("Warning: Failure to acquire secondary keytab",1);
    if ($opt_r)
    { 
        unlink ($keytab_mixed);
    }
}

system "$bin/kdestroy 2>/dev/null";

&Remove_Keys($second_keytab,&Determine_Keys_To_Remove($second_keytab,$history_size)) if (!$opt_p && -e $second_keytab);

# Extract second keytab and merge

my $second_extraction_result = &Construct_Keytab($second_realm_flag,$second_keytab,(($second_realm_server) ? $second_realm_server : ""),$second_config);

# If some keys were extracted, check the keytab
if ($second_extraction_result != 2)
{
    &System("KRB5_CONFIG=$second_config $command_verify_success $second_validation_function $data_second");
}


if ($second_extraction_result == 1)
{
    verbose("Warning: Invalid primary keytab returned\n",1);
}
else
{
    &Merge_Keytabs($second_keytab, $keytab_mixed) if (-e $second_keytab);


    # If some keys were extracted, check the keytab
    if ($first_extraction_result != 2 || $second_extraction_result != 2)
    {
        &System("KRB5_CONFIG=$tmpconf $command_verify_success -l verifyextcentral $data_mixed");
    }

    &Restore_SE_Linux_Context;
  
    verbose("Success: keytab entries now merged in $keytab_mixed",1);
    verbose("Verbose output logged in $logfile.",1);
}

sub Restore_SE_Linux_Context
{
    # if we changed or created /etc/krb5.keytab AND have SELinux:
    if ( ( -x "/sbin/restorecon" ) &&
         ( -x "/usr/sbin/selinuxenabled" ) &&
         ( `/usr/sbin/selinuxenabled` ))
    {
        system "/sbin/restorecon -v $keytab_mixed" if -e $keytab_mixed;
        system "/sbin/restorecon -v $keytab_linux" if -e $keytab_linux;
        system "/sbin/restorecon -v $keytab_windows" if -e $keytab_windows;
    }

}

sub Get_Keytab_Permissions
{
    $data_linux = $host;
    $data_windows = $host;
    $data_mixed = $host;

    if ( -x "/usr/bin/stat")
    {
        $data_linux = $data_linux . " , " . `$stat_cmd $keytab_linux` if -e $keytab_linux;
        $data_windows = $data_windows . " , " . `$stat_cmd $keytab_windows` if -e $keytab_windows;
        $data_mixed = $data_mixed . " , " . `$stat_cmd $keytab_mixed` if -e $keytab_mixed;
    }
}

# Construct_Keytab
# Create a keytab for the realm indicated.
# Parameters:
#    windows_key_run: whether the keytab is for Active Directory or Heimdal realm
#    keytab: the keytab which will be used
#    opt_s: the server from which the key will be extracted
#    kdconf: the configuration which will be used for the related kerberos commands
# Return Value: 0 on success
sub Construct_Keytab
{
    my($host,$tkt5);
    my($domain,$realm);
    my $ext = "ext";
    my $cp_ext = "cpext";
    # the master kdc server
    my $Master = "afskrb5m";
    my $lxkerb_master = "";
    my $kvno = "";
    my $merge = "";
    my $options = "-n -P -p";
    my $keytab_export = "";
    my $fh;
    $arc_server = "afskrb5m";
    
    my ($windows_key_run,$keytab_local,$opt_s,$kdconf) = @_;
 
    $keytab = $keytab_local;
    
    ($fh, $tmpfile) = tempfile("c-c-k-extXXXXXX", DIR => "/etc/");
    close $fh;

    ($fh, $tmpconf) = tempfile("c-c-k-cnfXXXXXX", DIR => "/etc/");
    close $fh;
    # If this is a windows run...
    if ($windows_key_run)
    { 
         # the master kdc server
         $Master    = "cerndc10"; # temp windows credential provider
         
         # only use that machine if it is not possible to get a domain controller from the alias
         my $ip_address;
         my $packed_ip;
         $packed_ip = gethostbyname("cerndc.cern.ch");
         if (defined $packed_ip) {
             $Master = inet_ntoa($packed_ip);
             $lxkerb_master = $Master;  
         }

         $ext       = "ext_windows";
         $cp_ext    = "cpext_windows";
 
         if ($opt_b)
         {
             $ext    = "ext_windowsplaintext";
             $cp_ext = "cpext_windowsplaintext";
         }

         # This may get overwritten from the command line
         $arc_server = "lxkerb";   # We don't get the key from the DC, it's extracted from a front end.
    }

    if ( -d $sbin_mit) {
       @query  = ("read_kt $keytab\n",
                  "list\n",
                  "quit\n"
                  );

    }

    $host = `hostname`;
    chomp($host);
    unless (($host =~ /^[^\.]+\.[^\.]+\.[^\.]+/) && ($host !~ /^localhost|localdomain$/)) {
        my(@a) = gethostbyname($host);
        $host = shift @a;        # long hostname
        unless (($host =~ /^[^\.]+\.[^\.]+\.[^\.]+/) && ($host !~ /^localhost|localdomain$/)) {
               die "Error: host doesnot resolve to a fully qualified name: $host\n";
       }
    }



    ($fh, $keytab_register) = tempfile("c-c-k-regXXXXXX", DIR => "/etc/");
    close $fh;
    # If a service type was specified

    $arc_cmd   = "$arc -h $arc_server -t300 -T300";

    if ($opt_t)
    {
        $k5_id = "$opt_t/$host";
        $options = "-l -p";

        #$keytab_register = "/etc/krb5.keytab$$.$opt_t";
        # We will need to use the backup host keytab to authenticate, as the default keytab will not contain host keys.
        $keytab_export = "KRB5_KTNAME=$keytab_register";
        $arc_cmd   = "$arc -h $arc_server";
    }
    else
    {
        $k5_id = "host/$host";
        #$keytab_register = "/etc/krb5.keytab$$.host";
    }

    ($host) =~ s/\.([^\.]+\.[^\.]+)$//;
    $domain = $1; $realm = uc $domain;     # this is wrong of course, but works at cern

    if ($opt_s) {
       $arc_server = $opt_s;
    }

    verbose("k5 principal: $k5_id");

    die "Error: You need to be root in order to write $keytab\n" if ($< != 0);


    &Create_Single_KDC_Kerberos_Configuration($kdconf, $tmpconf, $Master, $realm);

    $ENV{KRB5_CONFIG} = $tmpconf;

    # create temporary ticket files in /tmp. Else we would create (and maybe clobber /tmp/tkt0 & /tmk/krb5cc_0)
    # try to create unique names. If someone is trying to over-race us, kinit will fail or "unlink"
    # the old ones before re-creating them.  It will not follow any link left by a hacker
    for (my $i = 5; $i > 0; $i--) {
	#my $rand = rand(999999);
	#$tkt5 = "/tmp/krb5cc_$rand";
        ($fh, $tkt5) = tempfile("c-c-k-krb5cc_XXXXXX", DIR => "/tmp/");
        close $fh;
	next if -s "$tkt5";
	last;
    }

    $ENV{'KRB5CCNAME'} = "FILE:$tkt5";

    # If opt_x was specified, do not check if the current keys are valid, just extract new keys.
    unless ($opt_x)
    {
        return 2 if (&Validate_Existing_Keytab($keytab, $windows_key_run) != 1);
    }

    # Backup the keytab. We are going to insert new keys into the register which will include historical keys, and the other keytab will just contain new keys

    if (-e $keytab)
    {
        system("$cp_path $keytab $keytab_register\n");
    }
    else
    {
        unlink ($keytab_register);
    }

    # If something other than 'host' was requested, we don't need to do a non-authenticated connection
    # as user will be comming in with credentials that enable them to perform this operation
    my $G = ($opt_I) ? "-I" : "";
	
    # entry not defined in keytab yet, or invalid. Do a first pass
    system "$bin/kdestroy 2>/dev/null";
    verbose("creating initial entry in keytab");
    $opt_f = 1;      # allow subsequent re-creation

    unless ($opt_k && $opt_k ne "/etc/krb5.keytab")
    {
        $cmd = "$keytab_export $arc_cmd $options -- kas $G $ext $k5_id $lxkerb_master";    # ext creates entry if necessary, else uses old one
    }
    else
    {
        $cmd = "$arc_cmd $options -- kas $G $ext $k5_id $lxkerb_master"; 
    }

    
    return 1 if (&Ktutil($cmd, $merge, $windows_key_run) == 1);
  
    my $keytab_compare = $keytab_mixed;
    $keytab_compare = $keytab_register if ($keytab eq $keytab_mixed);

    # If there is an existing keytab, we must check to ensure the keys we are getting won't conflict and will be higher if needed
    if (-e $keytab_compare)
    {
         # If this keytab is for the default realm, make sure the keys are the highest
         if (($default_config eq "W") && $windows_key_run)
         {
            while (&Detect_Keytab_Conflict_Higher($keytab_compare,$keytab) != -1)
            {
		 verbose("Keys in $keytab are lower than existing keys, kvno incrementation required");
                 unless ($opt_k && $opt_k ne "/etc/krb5.keytab")
                 {
                     $cmd = "$keytab_export $arc_cmd $options -- kas $G $cp_ext $k5_id $lxkerb_master";    # ext creates entry if necessary, else uses old one
                 }
                 else
                 {
                     $cmd = "$arc_cmd $options -- kas $G $cp_ext $k5_id $lxkerb_master";
                 }

		 return 1 if (&Ktutil($cmd, $merge, $windows_key_run) == 1);
            }
         }
         # With Heimdal, we can set the kvno, so no repetition required
         elsif (($default_config eq "L") && (! $windows_key_run))
         {
            $kvno = &Detect_Keytab_Conflict_Higher($keytab_compare,$keytab);
            if ($kvno != -1)
            {
                verbose("Keys in $keytab are lower than existing existing keys, changing kvnos to $kvno");
                $cp_ext = "cpext_kvno";
                $kvno = 1 if ($kvno > 254 || $kvno < 0);
            }
         }
         # If the keytab is for the non-default realm, just make sure it doesn't conflict with existing keys
         else
         {
            while (&Detect_Keytab_Conflict_Equals($keytab_compare,$keytab) != 0)
            {
                verbose("Keys in $keytab conflict with existing keys, kvno incrementation required");
                unless ($opt_k && $opt_k ne "/etc/krb5.keytab")
                {
                    $cmd = "$keytab_export $arc_cmd $options -- kas $G $cp_ext $k5_id $lxkerb_master";    # ext creates entry if necessary, else uses old one
                }
                else
                {
                    $cmd = "$arc_cmd $options -- kas $G $cp_ext $k5_id $lxkerb_master";
                }
            
                return 1 if (&Ktutil($cmd, $merge, $windows_key_run) == 1);
            }
         }
    }

    # as we did not have an initial keytab, keytab was transferred in clear
    # so we recreate it now through encrypted session.
    verbose("Repeating keytab request, using previously acquired keytab to secure the transport mechanism");

    verbose("recreating entry in keytab");
    $G = ($opt_I) ? "-I" : "";

    unless ($opt_k && $opt_k ne "/etc/krb5.keytab")
    {
        $cmd = "$keytab_export $arc_cmd -l -p -- kas $G $cp_ext $k5_id $kvno $lxkerb_master";    # ext creates entry if necessary, else uses old one
    }
    else
    {
        $cmd = "$arc_cmd -l -p -- kas $G $cp_ext $k5_id $kvno $lxkerb_master";
    }

    $merge = 1;
    return 1 if (&Ktutil($cmd, $merge, $windows_key_run) == 1);

    return 0;
}

# verbose
# Parameters:
#    output: data to print
#    override: print the output, regardless of whether debug is enabled or not
sub verbose {
   my ($output, $override) = @_;
   
   print "$output\n" if $opt_v or $opt_n or $override;

   &Log($output);
}

sub Log {
    open(LOGF, ">>$logfile") || return "$!";
    print LOGF scalar(localtime), "  ", @_, "\n"; 
    close(LOGF);
}

sub System {
   my ($cmd,$suffix) = (@_, "");
   verbose("$Mrun \"$cmd\" $suffix\n");
   system $cmd unless $opt_n;
}

sub create_Ident {
   # force creation of rcmd.host & IPaddress in PTS database
   return unless $opt_I;
   $cmd = "$arc_cmd    -l -p -- kas -I nop $k5_id";
   &System("$cmd >/dev/null 2>/dev/null", "to force creation of PTS identity");
}

sub no_f_flag {
   unless ($opt_f)
   {
       verbose("$keytab needs to be updated\n \t but -f option has not been specified\n");
       return 0;
   }
   return 1;
}

#   read composite answer from argument FileHandler
#   returns the obtained string
#
#   answer format is:  <len> keytab <len> password
#   <len> fields are 6 bytes long and contain length in character.
#
#   returns keytab, ticket & data
sub Seperate_Keytab_Password
{
    my($reply) = @_;
    # read answer, then splice it
    my ($l,$k);

#   extract the keytab
    my $v_keytab = "";
    die("Invalid answer from server: null reply") if 0 == length($reply);
    $l = substr($reply,$[,6);
    die("Invalid answer from server: $reply") unless $l =~ /\d+/;
    $k = $l+6;
    die("Invalid answer from server: k=$k ") if $k > length($reply);
    $v_keytab = substr($reply,$[+6,$l) if $l;
    $reply = substr($reply,$[+$k);      # now points to ticket data

#   extracts K5 ticket if provided.
    my $p_pswd = "";
    die("Invalid answer from server: null reply") if 0 == length($reply);
    $l = substr($reply,$[,6);         # could be 0 !
    die("Invalid answer from server: $reply") unless $l =~ /\d+/;
    $k = $l+6;
    die("Invalid answer from server: k=$k ") if $k > length($reply);
    $p_pswd = substr($reply,$[+6,$l) if $l;
    $reply = substr($reply,$[+$k);      # now points to data part

    open (KEYTAB, ">", $tmpfile) or die "Can't open file $tmpfile\n";
    print KEYTAB "$v_keytab";
    close KEYTAB;
    open (PSWD, ">", $ppwdfile) or die "Can't open file $ppwdfile\n";
    print PSWD "$p_pswd";
    close PSWD;

    verbose("Plaintext machine password location: $ppwdfile",1);
    verbose("Warning: Protect $ppwdfile and use only in conjunction with TrustRelationshipRestorer. See usage instructions here: https://twiki.cern.ch/twiki/bin/view/AFSService/MigrationFAQ",1);
}

# Acquires a keytab with the arc command provided. Tests the keytab and copies/merges it
# as necessary.
# Parameters:
#    cmd: the command which will be ran
#    merge: whether to preserve the keytab in a tmp file or merge it with the existing keytab
sub Ktutil {
   my ($cmd,$merge,$windows_key_run) = @_;
   
   unless ($opt_n) {
       my $retry = 3;
 
       while ($retry > 0)
       {
           unlink($tmpfile);

           #verbose($cmd);
           #my $tmpkeyfile_contents = `$cmd > $tmpfile`;

           &System("$cmd > $tmpfile");

           unless (-s $tmpfile or $opt_n)
           {
               verbose("Error: did not receive keys for $k5_id\n");
               return 1;
           }

           open(TMPKEYFILE, $tmpfile) or die "Can't read file $tmpfile\n";
           my $tmpkeyfile_contents = <TMPKEYFILE>;
           close (TMPKEYFILE);

           #$tmpkeyfile_contents = lc($tmpkeyfile_contents);

           # If an error was returned instead of the keytab, print it and return.
           if ($tmpkeyfile_contents =~ "Error")
           {
               verbose($tmpkeyfile_contents,1);
               return 1;
           }
 
           # If the parent program segfaulted, sleep and try again         
           unless ($tmpkeyfile_contents =~ "Segmentation")
           {
               if ($opt_b && $windows_key_run && $cmd =~ /cpext_windowsplaintext/)
               {
                   &Seperate_Keytab_Password($tmpkeyfile_contents)
               }

               last;  
           }

           sleep 5;
           $retry--;
       }
   }
       
   # Make sure the permissions, ownership and selinux context are correct
   chmod 0600, $tmpfile or verbose("Couldn't chmod $tmpfile: $!");
   chown 0, 0, $tmpfile;
   system("$chcon_path $selinux_root_context $tmpfile");

   # verify this keytab is valid
   unless ($opt_n) {
      verbose("we now verify the received data");
      verbose("$pagsh $bin/kinit -k -t $tmpfile $k5_id 2>&1");
      my $out = `$pagsh $bin/kinit -k -t $tmpfile $k5_id 2>&1`;

      if ($out) {                                       # but it is invalid
             my @keytab_entries = &Get_Keytab_Entries(0,$tmpfile);
           
             my ($fh, $error_keytab) = tempfile("error_keytab_XXXXXX", DIR => "/etc/");
             close $fh;
             unlink($error_keytab);

             verbose("Keytab test failed with $out Please check that your system clock is in sync",1);

             verbose("received data invalid (rc = $?). Data is:\n");
             verbose("\nReturned keys if any: \n @keytab_entries \n");
             system("/bin/mv  $tmpfile $error_keytab");
             verbose("Data placed in tmp file: $error_keytab");
                        
             #die "Error: Invalid keytab returned\n"; 
             return 1;
      }

      #system "$bin/kdestroy 2>/dev/null";
   }
   verbose("received valid keytab entries from $arc_server");

   # the MIT kerberos5 case: ktutil reads commands from stdin
   verbose("now ready to merge new keytab entries with old ones");

   if (-e $tmpfile)
   {
       # If this is a service keytab, copy the new keytab into the register, so we can continue to use the host keytab for authentication
       system("/bin/mv  $tmpfile $keytab");

       # If this was a secure keytab, merge it with the existing keys and then remove any unwanted historical keys
       if ($merge && -e $keytab_register)
       {
               &Merge_Keytabs($keytab_register, $keytab);
       }
   }

   return 0;
}

# Merge_Keytabs
# Merge two keytabs together
# Parameters:
#    new_keytab: keys will be taken from this file
#    existing_keytab: keys will be placed in this file
sub Merge_Keytabs
{
   my($new_keytab, $existing_keytab) = @_;

   my @cmds_merge;

   my $tmp_keytab = "";
    my $fh;
   ($fh, $tmp_keytab) = tempfile("c-c-k-tmpXXXXXX", DIR => "/etc/");
   close $fh;
   unlink($tmp_keytab);

   if ( -d $sbin_mit) {
       @cmds_merge = ("read_kt $new_keytab\n",
                      "read_kt $existing_keytab\n",
                      "write_kt $tmp_keytab\n",
                      "quit\n"
                      );
   }


   if (@cmds_merge) {
    verbose("merge commands: @cmds_merge");
       if ($opt_d) {
          my(@xxx) = @cmds_merge;
          @cmds_merge = ();
          foreach (@xxx) {
             push @cmds_merge, $_;
             push @cmds_merge, "list\n";
          }
       }

       verbose("$Mrun $sbin/ktutil with commands\n@cmds_merge\n");
       unless ($opt_n) {
          if ($opt_d) {
              open(P, "|$sbin/ktutil");
          } else {
              open(P, "|$sbin/ktutil >/dev/null");
          }
          print P @cmds_merge;
          close P;
       }
       system("mv $tmp_keytab $existing_keytab") if -s $tmp_keytab;
   }
   # the Heimdal case:  ktutil takes one command at a time, but each command does much more
   else {
       system("$sbin/ktutil copy $new_keytab $existing_keytab");     # merge new entry
       #System("$sbin/ktutil purge --age=0");             # purge older entry if any
   }
}

# find_cmd
# searches for a given command in a list of paths
# returns the location of the command or a null string
sub find_cmd {
   my $cmd = shift @_ || die "Error: no command to search for\n";
   foreach (qw ( /usr/bin /usr/sue/bin /usr/afs/bin /usr/afsws/bin )) {
      return "$_/$cmd" if -e "$_/$cmd";
   }
   return "";
}

# Determine_Keys_To_Remove
# Based on a number of keys which should be retained in the keytab, cycle through and
# check if there are more key sets than desired. 
# Parameters:
#    keytab: the keytab which should be checked
#    number_of_keys_to_retain: this is the maximum number of keys desired within the keytab
# Return value: The kvnos of the keys requiring deletion
sub Determine_Keys_To_Remove
{
    my ($keytab, $number_of_keys_to_retain) = @_;

    my @keys_to_delete;
    my $keys_to_delete_index = "0";
    my @keys;
    my $current_key_number = "-1";
    my $number_of_keys_in_keytab = "0";

    # Need to start from the highest kvnos in the list (always the last in the array)
    @keys = reverse(&Get_Keytab_Entries(0,$keytab));   

    foreach (@keys)
    {
        my $existing_key_set_string = $_; # KVNO:spn
        my @existing_key_set_array = split /:/, $existing_key_set_string;

        if ($existing_key_set_array[0] != $current_key_number)
        {
            $current_key_number = $existing_key_set_array[0];
            $number_of_keys_in_keytab++;
            if ($number_of_keys_in_keytab > $number_of_keys_to_retain)
            {
                $keys_to_delete[$keys_to_delete_index] = $current_key_number;
                $keys_to_delete_index++;
            }
        }
    }

    verbose("keys requiring removal: @keys_to_delete") if (@keys_to_delete);

    return @keys_to_delete;
}
# Remove_Keys
# Remove keys from a keytab based on their kvno
# Parameters:
#    keytab: they keytab from which keys will be removed
#    keys_to_delete: an array of integers indicating the keys of which kvno should be removed
sub Remove_Keys
{
    my($keytab, @keys_to_delete) = @_;
    my @keys;
    my $fh;
    my $comparason_spn_type = "host";
    $comparason_spn_type = $opt_t if ($opt_t);
    
    # MIT case (more complicated because we can't delete keys of a certian kvno)
    if (@query)
    {
 
        #my $tmp_keytab = "/etc/tmp_keytab_$$";
        my $tmp_keytab = "";
        ($fh, $tmp_keytab) = tempfile("c-c-k-tmpXXXXXX", DIR => "/etc/");
        close $fh;
        unlink($tmp_keytab);

        my @delete_cmds  = ("read_kt $keytab\n");

        foreach(@keys_to_delete)
        {
            
            my $current_key = $_;
            my $key_index = 0;

            @keys = reverse(&Get_Keytab_Entries(1,$keytab));
  
            $key_index = @keys;
  
            foreach(@keys)
            {
                my $tmp_key_set_string = $_; # KVNO:spn
                my @tmp_key_set_array = split /:/, $tmp_key_set_string;

                # Check if the spn type matches
                if ($tmp_key_set_string =~ m/$comparason_spn_type/)
                {
                    push(@delete_cmds, "delent $key_index\n") if ($tmp_key_set_array[0] == $current_key);
                }

                $key_index--;
            }
        }
	# If there are any keys to delete...
	if (@delete_cmds != 1)
	{
	    push(@delete_cmds, "write_kt $tmp_keytab\n");
	    push(@delete_cmds, "quit\n");

            verbose("commands: @delete_cmds");

            if ($opt_d) {
                open(P, "|$sbin/ktutil");
            } else {
                open(P, "|$sbin/ktutil >/dev/null");
            }
            print P @delete_cmds;
            close P;
            # if there are keys to move, move them
            if (-e $tmp_keytab)
            {
                system("/bin/mv $tmp_keytab $keytab\n");
            }
            # if there are no keys left over, just delete original keytab
            else
            {
                unlink($keytab);
            }
        }
    }
    # Heimdal case - can delete keys of a given kvno
    else
    {
       foreach(@keys_to_delete)
       {
           &System("$sbin/ktutil --keytab=$keytab remove --kvno=$?");     # merge new entry        
       }
    }
}

# Validate_Existing_Keytab
# Check if an existing keytab contains valid keys and if those keys are not in conflict with 
# existing keys of the opposite realm.
# Parameters: 
#    keytab: the keytab to be checked
#    windows_key_run: whether this is for a windows keytab
# Return Value: 1 if keys are invalid
sub Validate_Existing_Keytab
{
    my ($keytab, $windows_key_run) = @_;
    my $keytab_option = "";
    # first see if we can use existing keytab.
    # If -r (remove keytabs anyway) is specified, don't bother
    if ( -s $keytab ) {                                      #  we already have a keytab
        # to see if keytab valid, try kinit -k $k5_id

        # Only continue with this check if there are acctually keys of the type requested in the keytab.
        return 1 if (&Find_Highest_KVNO(&Get_Keytab_Entries(0,$keytab)) == 0);

        if ($opt_k)
        {
            $keytab_option = "-t $keytab";        
        }
 
        verbose("we already have a keytab\n" .
                "running \"$pagsh $bin/kinit -k $keytab_option $k5_id\" to check keytab");

        &System("$pagsh $bin/kinit -k $keytab_option $k5_id 2>/dev/null");

        if ($? == $kinit_return_success) {                                        # keytab seems valid
            # Keytab is valid but is the order right? This only matters if there are keys for two realms
            if (($keytab ne $keytab_mixed) && ((-e $keytab_linux) && (-e $keytab_windows)))
            {
                my $order_problem = 0;
                if (($default_config eq "W") && $windows_key_run)
                {
                    $order_problem = 1 if (&Detect_Keytab_Conflict_Higher($keytab_linux,$keytab) != -1);
                }
                elsif (($default_config eq "L") && (!$windows_key_run ))
                {
                    $order_problem = 1 if (&Detect_Keytab_Conflict_Higher($keytab_windows,$keytab) != -1);
                }
                if ($order_problem == 0)
                {
                    verbose("keytab seems valid, we keep it as-is");
                    &create_Ident if $opt_I;
                    return 0;
                }
                else
                {
                    verbose("keytab seems valid, but the order is not correct");
                    return &no_f_flag;
                }
            }
            else
            {
                verbose("keytab seems valid, we keep it as-is");
                &create_Ident if $opt_I;
                return 0;
            }
        } else {                                                # keytab considered invalid
            verbose("keytab invalid (rc = $?), will need to recreate everything");
            return &no_f_flag;                                               
        }
    }

    return 1;
}

# Create_Single_KDC_Kerberos_Configuration
# Take a given kerberos config and remove all kdcs but one. This ensures that all
# kerberos calls made using this configuration will be against a single KDC, thus
# avoiding any problems caused by delays in information being propagated between kdcs.
# Parameters:
#    kdconf: the original, multi kdc configuration
#    tmpconf: the new, single kdc configuration
#    Master: the single kdc which will be placed in the new configuration
#    realm: the default realm
sub Create_Single_KDC_Kerberos_Configuration
{
    my ($kdconf, $tmpconf, $Master, $realm) = @_;

    # create a kerberos environment which only uses the master kdc
    die "Error: cannot locate kerberos5 configuration ($kdconf)" unless ( -r $kdconf );
    open (F, "<$kdconf") || die "Error: cannot read kerberos5 configuration ($kdconf)";
    my @S = <F>;
    my $s = 0;
    foreach (@S) {
       my $line = $_;
       if ($line =~ /^\s*\[realms\]\s+(.*)/) {
          $s = 1;
          $line = $1;
       }
       next unless $s >= 1;
       next unless $line;
       if ($line =~ /^\s*$realm\s+=\s+\{\s+(.*)/) {
          $s = 2;
          $line = $1;
       }
       next unless $s >= 2;
       next unless $line;
       if ($line =~ /kdc\s+=\s+/ && $s >= 3) { $_ = ""; }
       elsif ($line =~ /kdc\s+=\s+/) {
          $_ = "  kdc = $Master\n";
          $s = 3;
       }
       elsif ($s >= 3) { last; }
    }
    open G, ">$tmpconf" || die "Error: cannot open temporary kerberos config file $tmpconf";
    print G @S;
    close G;
}

# Get_Keytab_Entries
# Uses either MIT's klist or Heimdal's ktutil to retrieve keys from a given keytab.
# Parameters:
#    all_keys: An integer which indicates if the caller requires all keys of just those matching
#              the spn which is being processed by cern-config-keytab.
#    keytab_tmp: The keytab from which the keytabs will be extracted
# Return Value: An array containing the spns and kvnos of the keys in the keytab
sub Get_Keytab_Entries
{
    my ($all_keys,$keytab_tmp) = @_;
    my @key_line;
    my @key_set;
    my $count = "0";
    my $count_key_set = "0";
    my @key_array;

    my $comparason_spn_type = "host";
    $comparason_spn_type = $opt_t if ($opt_t);

    
    # MIT case (we can use klist)
    if (@query)
    {
        @key_array = `$bin/klist -kt $keytab_tmp`;
    }
    # Heimdal case - use ktutil
    else
    {
        @key_array = `$sbin/ktutil --keytab=$keytab_tmp list`;
    }

    foreach (@key_array)
    {
        # Check that this isn't one of the header lines
        if ($key_array[$count] =~ m/\@/)
        {
            # If we do not want to return all keys and the current key's spn type is incorrect, then skip over.
            if (($all_keys == 1) || ($key_array[$count] =~ m/$comparason_spn_type/))
            {
                @key_line = split(/ /, $key_array[$count]);

                foreach (@key_line)
                {
                    $key_set[$count_key_set] = $_ if($_ =~ /^\d+$/); # This is the kvno
                    $key_set[$count_key_set] = $key_set[$count_key_set] . ":" . $_ if ($_ =~ m/\@/); # This is the principal name
                }
                #verbose("key: $key_set[$count_key_set]");
            
                $count_key_set++;
            }
        }
        $count++;
    }

    return @key_set;
}

# Determine_Default_Configuration_Flavour
# Determine the default configuration flavour of the machine. 
# Return Value: A string containing 'W' if the machine has Active Directory for the default realm
# or 'L' if the machine has Heimdal for the default realm.
sub Determine_Default_Configuration_Flavour
{
    my $flavour_flag = "L";
    my $conf = "/etc/krb5.conf";
    open(F, "<$conf") || die "Error: cannot read krb5 configuration file $conf\n" ;
    my $header = <F>;
    close F;
    if ($header =~ / AD /) {
       $flavour_flag = "W";
    }
    return $flavour_flag;
}

# Detect_Keytab_Conflict_Higher
# Ensures that the kvnos of one keytab are always higher than the other.
# Parameters:
#     keytab_existing: String pointing to a keytab which should contain lower keys
#     keytab_new: String pointing to a keytab file which should contain higher keys
# Return Value: -1, or an integer value which is higher than all kvnos in keytab_existing
sub Detect_Keytab_Conflict_Higher
{
    my ($keytab_existing, $keytab_new) = @_;
    my @existing_kvnos = &Get_Keytab_Entries(0,$keytab_existing);
    my @new_kvnos = &Get_Keytab_Entries(0,$keytab_new);

    my $highest_existing_kvno = &Find_Highest_KVNO(@existing_kvnos);
    my $highest_new_kvno = &Find_Highest_KVNO(@new_kvnos);

    $highest_new_kvno++;

    if ($highest_existing_kvno >= $highest_new_kvno)
    {
        $highest_existing_kvno = $highest_existing_kvno + 2;
        return $highest_existing_kvno;
    }
    else
    {
        return -1;
    }
}

# Find_Highest_KVNO
# Cycles through an array of keytab entries to find the one with the highest kvno.
# Parameters:
#     @tmp_kvnos: an array of keys to be checked
# Return Value: The highest kvno in the array
sub Find_Highest_KVNO
{
    my (@tmp_kvnos) = @_;
    my $highest_kvno = 0;

    foreach(@tmp_kvnos)
    {
        my $tmp_key_set_string = $_; # KVNO:spn
        my @tmp_key_set_array = split /:/, $tmp_key_set_string;

        if ($tmp_key_set_array[0] > $highest_kvno)
        {
                $highest_kvno = $tmp_key_set_array[0];
        }
    }

    return $highest_kvno;
}

# Detect_Keytab_Conflict_Equals
# Determine if there are any kvnos in the two keytabs provided are equal.
# Parameters:
#     keytab_existing: String pointing to keytab file
#     keytab_new: String pointing to keytab file
# Return Value: integer set to 1 if conflict detected
sub Detect_Keytab_Conflict_Equals
{
    my ($keytab_existing, $keytab_new) = @_;
    my @existing_kvnos = &Get_Keytab_Entries(0,$keytab_existing);
    my @new_kvnos = &Get_Keytab_Entries(0,$keytab_new);

    # If the keyset is not the dominant (the one corrosponding to the default realm), just make sure it is not equal to any key in the keytab
    foreach(@existing_kvnos)
    {
        my $existing_key_set_string = $_; # KVNO:spn
        my @existing_key_set_array = split /:/, $existing_key_set_string;

        foreach(@new_kvnos)
        {
            my $new_key_set_string = $_; # KVNO:spn
            my @new_key_set_array = split /:/, $new_key_set_string;

            # If windows key is higher, return it so we can extract new linux keys with higher kvnos
            if ($existing_key_set_array[0] == ($new_key_set_array[0]+1))
            {
                return 1;
            }
        }
    }
    return 0;
}


# make sure we always clean out temporary stuff
END {
   unlink glob('/etc/c-c-k*');
   unlink glob('/tmp/c-c-k*');
   system "$bin/kdestroy 2>/dev/null" unless $opt_h;
}
