#!/usr/bin/perl

## no critic (CodeLayout::ProhibitParensWithBuiltins);

# key-system -- Change root password and download system keytab.
#
# Run after the initial system build, this script prompts the user to change
# the root password and then to get Kerberos root instance tickets to download
# the system keytab.  Normally, it's run from an init script configured after
# boot, but it may be run interactively via a normal login.
#
# Written by Huaqing Zheng and Russ Allbery
# Copyright 2005, 2006, 2007, 2008, 2012, 2013
#     The Board of Trustees of the Leland Stanford Junior University

use 5.006;
use strict;
use warnings;
#The below line was commented out and IEDO-900 was created to fix this issue
#use autodie;

use Crypt::PasswdMD5 qw(unix_md5_crypt);
use Getopt::Long qw(GetOptions);
use POSIX qw(SIGTERM);
use Sys::Hostname::Long qw(hostname_long);

# Find common programs.
$ENV{PATH} = '/sbin:/usr/sbin:/bin:/usr/bin';

# Location of the profile.d fragment that runs key-system, which we remove
# once key-system succeeds.
our $PROFILE = '/etc/profile.d/key-system.sh';

# Location of the lock file.  Unlike many other programs, this one uses the
# PID in the lock file to kill the other copy of itself if run again.  This
# allows the script to be run from an init script, and then run separately
# interactively.
our $LOCK = '/var/lock/key-system';

# Whether we have the lock.  If true, we'll remove the lock on exit.
our $LOCKED = 0;

# Whether we've finished keying the system.  If true, we'll remove the startup
# links on exit.
our $DONE = 0;

##############################################################################
# Lock handling
##############################################################################

# Remove the lock on exit from the script if we have it, remove authconfig
# init script links if there are any, and ensure that echo is re-enabled.
END {
    system ('stty', 'echo');
    my $save = $!;

    if ($LOCKED) {
        unlink $LOCK;
    }

    if ($DONE) {
        unlink glob('/etc/rc?.d/*authconfig');
        unlink $PROFILE;
    }

    $! = $save;
}

# Remove the lock and re-enable echo on abort.
$SIG{INT} = sub {
    system ('stty', 'echo');
    if ($LOCKED) {
        unlink $LOCK;
    }
    exit;
};

# Create a lock file to indicate to others that we're running.
sub create_lock {
    open (my $LOCK_FH, '>', $LOCK) or die "$0: cannot create $LOCK: $!\n";
    print {$LOCK_FH} "$$\n"        or die "$0: cannot write to $LOCK: $!\n";
    close $LOCK_FH                 or die "$0: cannot close $LOCK: $!\n";
    $LOCKED = 1;
}

# Reads the lock file and returns the PID of our other running copy, or undef
# if no other copy is running.  We don't make any attempt to handle races
# here; the program will either be run once from init or will be run
# interactively, and either way races are not likely or interesting.
sub check_lock {
    if (!(-e $LOCK)) {
        return ;
    }
    open (my $LOCK_FH, '<', $LOCK) or return;
    my $pid = <$LOCK_FH>;
    close $LOCK_FH or die "$0: cannot close $LOCK: $!\n";
    chomp $pid;
    if (!(kill (0, $pid))) {
        unlink $LOCK;
        return;
    }
    return $pid;
}

# Kill our other copy and take over the lock, if necessary.  Otherwise, just
# write out our lock file.
sub take_lock {
    my $pid = check_lock;
    if ($pid) {
        kill (SIGTERM, $pid) or warn "$0: unable to kill PID $pid: $!\n";
    }
    create_lock;
}

##############################################################################
# Root password handling
##############################################################################

# This is a manual reimplementation of passwd for root, used when we're
# running from an init script and can't rely on passwd doing the right thing.
# Note that we currently can only cope with MD5 hashes.
sub change_root_passwd {
    open (my $SHADOW_FH, '<', '/etc/shadow')
        or die "$0: cannot open /etc/shadow: $!\n";
    my $old;
    local $_;
    while (<$SHADOW_FH>) {
        if (/^root:([^:]*)/xsm) {
            $old = $1;
            last;
        }
    }
    close $SHADOW_FH;

    # Prompt for the old password and confirm that it matches.
    my $host = hostname_long;
    {
        no warnings;
        print "\n";
        print "$host: Please pick a new password for root.\n";

        # Turn off the screen echo (hides password):
        system 'stty', '-echo';

        if (defined $old) {
            print 'Old password: ';
            my $passwd = <STDIN>;
            chomp $passwd;

            # Short-circuit the call to unix_md5_crypt if $passwd is not
            # set yet.
            if ($passwd && ($old eq unix_md5_crypt($passwd, $old))) {
                print "\n";
            } else {
                print "\n\nPassword incorrect.\n\n";
                redo;
            }
        }
    }

    # Prompt for the new password and ensure that it matches.
    my $passwd;
    {
        system 'stty', '-echo';
        print 'New password: ';
        $passwd = <STDIN>;
        print "\nNew password (again): ";
        my $check = <STDIN>;
        if ($check ne $passwd) {
            print "\n\nPasswords did not match.\n\n";
            redo;
        }
    }
    chomp $passwd;
    system 'stty', 'echo';
    print "\n";

    # Replace the password hash inside /etc/shadow.
    my $key = unix_md5_crypt($passwd);
    if (! defined($key)) {
        die "$0: unable to generate new encrypted password\n";
    }
    open (my $SHADOW_FH2, '<', '/etc/shadow')
        or die "$0: cannot open /etc/shadow: $!\n";
    open (NEW, '>', '/etc/shadow.new')
        or die "$0: cannot create /etc/shadow.new: $!\n";
    chown (-1, scalar (getgrnam 'shadow'), \*NEW)
        or die "$0: cannot chgrp /etc/shadow.new: $!\n";
    chmod (0640, \*NEW) or die "$0: cannot chmod /etc/shadow.new: $!\n";
    while (<$SHADOW_FH2>) {
        s/^root:[^:]*/root:$key/xsm;
        print NEW $_ or die "$0: cannot write to /etc/shadow.new: $!\n";
    }
    close $SHADOW_FH2;
    close NEW or die "$0: cannot write to /etc/shadow.new: $!\n";
    rename ('/etc/shadow.new', '/etc/shadow')
        or die "$0: cannot rename /etc/shadow.new to /etc/shadow: $!\n";

    return;
}

##############################################################################
# Implementation
##############################################################################

# Clean up $0 for error reporting and always flush stdout.
my $fullpath = $0;
$0 =~ s%.*/%%xsm;
$| = 1;

# Parse command-line options.
my ($help, $init);
Getopt::Long::Configure ('bundling', 'no_ignore_case');
GetOptions ('h|help' => \$help,
            'i|init' => \$init) or exit 1;
if ($help) {
    print "Feeding myself to perldoc, please wait....\n";
    exec ('perldoc', '-t', $fullpath);
}

# We have two modes: init mode and regular mode.  When run in init mode, we do
# not take over a lock and we exit successfully if the system keytab already
# exists.  In regular mode, we will take over the lock from any existing
# running instance and we always run, even if the keytab exists.
if ($init) {
    exit if check_lock;
    exit if (-f '/etc/krb5.keytab');
    create_lock;
} else {
    take_lock;
}

# First, change the root password.  If we're not running as an init script, we
# can do this the easy way.  Otherwise, we can't rely on passwd working and we
# need to edit /etc/shadow in place.
if ($init) {
    change_root_passwd;
} else {
    my $host = hostname_long;
    print "Changing password for root on $host\n";
    system ('passwd', 'root') == 0
        or die "$0: changing root password failed\n";
}
print "\n";

## Synchronize the clock using Chrony.
print "Synchronizing the clock with Chrony\n";

# Ensure the chrony service is running
system('/usr/sbin/service', 'chrony', 'start') == 0
  or warn "$0: cannot start chrony service\n";

# Step the clock once
system('chronyc', '-a', 'makestep') == 0
  or warn "$0: cannot run 'chronyc -a makestep'\n";

print "\n";

# Now, key the system using wallet.
{
    print 'Enter the principal to use to obtain keytabs (or abort) ---> ';
    my $username = <STDIN>;
    chomp $username;
    if ($username eq 'abort') {
        print "Aborting, please key the system manually.\n";
        $DONE = 1;
        exit;
    }
    my @wallet = qw(wallet -f /etc/krb5.keytab);
    push (@wallet, '-u', $username);
    push (@wallet, 'get', 'keytab', 'host/' . hostname_long);
    my $status = system (@wallet);
    if ($status == 0) {
        $DONE = 1;
    } else {
        warn "Keytab download failed\n\n";
        redo;
    }
}
exit;


##############################################################################
# Documentation
##############################################################################

=for stopwords
CPAN Hua Init KDCs Zheng --init init keyring keytab keytabs Allbery

=head1 NAME

key-system - Change root password and download system keytab

=head1 SYNOPSIS

B<key-system> [B<-hi>]

=head1 REQUIREMENTS

Requires the Crypt::PasswdMD5 and Sys::Hostname::Long Perl modules, both
available as Debian packages or from CPAN.

=head1 DESCRIPTION

B<key-system> handles initial system keyring.  It prompts for the old root
password and then sets a new root password, prompts for the principal to
use to obtain keytabs, and then downloads the system C<host/*> keytab with
B<wallet>.  The keying process can be aborted by the user after changing
the root password and before downloading keytabs for systems that can't be
keyed with B<wallet>, such as Kerberos KDCs.

B<key-system> can either be run interactively or run via an init script
during the first system boot.  When it runs successfully, it deletes any
file matching C</etc/rc?.d/*authconfig> to disable running the program
again on the next reboot.

If run with the B<-i> flag, indicating that it is running from an init
script, it will avoid using B<passwd> in case tty setup isn't fully
available.  It will also quietly exit if F</etc/krb5.keytab> already
exists or if a lock file indicates that it is already running.

If run normally, without the B<-i> flag, it will kill any copy of itself
that is already running.  This allows logging in remotely while the init
script is still running and running B<key-system> interactively, killing
the init version automatically and letting the system finish booting.

=head1 OPTIONS

=over 4

=item B<-h>, B<--help>

Print out this documentation (which is done simply by feeding the script
to C<perldoc -t>).

=item B<-i>, B<--init>

Indicate that B<key-system> is running from inside init.  This changes
behavior as described above, including avoiding using B<passwd> to change
the root password.

=back

=head1 FILES

=over 4

=item F</etc/krb5.keytab>

Where the system C<host/*> keytab is stored.  If this file exists,
B<key-system> exits quietly without doing anything if run in init (B<-i>)
mode.

=item F</etc/shadow>

Modified in place to set the root password to the MD5 hash of the provided
password if run in init (B<-i>) mode.  Otherwise, B<passwd> is used.

=back

=head1 CAVEATS

Init (B<-i>) mode only supports MD5 password hashes, both for verification
of the current password and for setting the new password.

=head1 AUTHORS

Hua Zheng <morpheus@stanford.edu> and Russ Allbery <rra@stanford.edu>.

=head1 SEE ALSO

passwd(1), wallet(1)

=cut
