#!/usr/bin/sh
#
# set_tcb - tool to switch between tcb and shadow passwords
#
# $Id: set_tcb 837 2008-12-16 18:59:48Z vdanen $
#
# Copyright (c) 2008 Vincent Danen <vdanen@annvix.org>
#
#  Licensed under the GPLv2.
#

# This script changes a few key files to ease configuration when switching
# from tcb to shadow passwords.  Because tcb transparently handles both schemes,
# life is made pretty easy.  We only need to change /etc/pam.d/system-auth,
# /etc/nsswitch.conf, and /etc/login.defs, as well as run tcb_(un)convert

VERSION="0.7"
header="set_tcb - switch between tcb and shadow password schemes\nversion: ${VERSION} - \$Id: set_tcb 837 2008-12-16 18:59:48Z vdanen $\n\n"
syslogfac="auth.notice"
star=$' \e[36;01m*\e[0m'
ystar=$' \e[33;01m*\e[0m'


#
# display usage/help information
#
function usage()
{
    printf "${header}"
    printf "Usage: set_tcb [--tcb|--shadow|--nis|--migrate|--hash] [md5|blowfish|sha256c]\n\n"
    printf "\t--tcb|-t\tconvert to tcb passwords\n"
    printf "\t--shadow|-s\tconvert to shadow passwords\n"
    printf "\t--nis|n\t\tinstruct pam_tcb to be able to use NIS authentication\n"
    printf "\t--migrate|-m\tconvert system-auth to use pam_tcb instead of pam_unix\n"
    printf "\t--revert|-r\trevert system-auth to use pam_unix instead of pam_tcb\n"
    printf "\t--hash|-a\tchange the password hash to use (md5, blowfish, sha256c)\n"
    printf "\t--help|-h\tthis help message\n\n"
    # should note sha512c above, but a bug in glibc is preventing it from being used
    printf "See set_tcb(8) for more information.\n\n"
}


#
# function to ask for confirmation
#
function confirm()
{
    if [ "${automode}" != "1" ]; then
        msg="${1}"
        printf "\n${msg}\n\n"
        read -e -p "${ystar} Are you sure you wish to proceed? (y/N) " doit
        printf "\n"
        if [ "${doit}" == "y" -o "${doit}" == "Y" ]; then
            return
        else
            printf "Execution aborted.\n\n"
            clean_fdir
            exit 2
        fi
    fi
}

#
# function to clean the temporary directory
#
function clean_fdir()
{
    if [ "${debug}" == "1" ]; then
        printf "${star} Not removing temporary directory ${fdir}...\n\n"
    else
        rm -rf ${fdir}
    fi
}


#
# function to make sure we're using a pam_tcb-enabled system-auth
#
function check_if_migrated()
{
    if [ "$(grep -q "pam_tcb" /etc/pam.d/system-auth; echo $?)" -gt "0" ]; then
        printf "You need to convert system-auth to use pam_tcb before this change can be made.\n"
        printf "Re-execute ${0} with the --migrate option.\n\n"
        exit 1
    fi
}

#
# function to change /etc/nsswitch.conf
#
function conv_nsswitch()
{
    local type="${1}"
    local new="${fdir}/nsswitch.conf"

    if [ "${type}" == "tcb" ]; then
	narg="tcb"
	oarg="files"
    elif [ "${type}" == "shadow" ]; then
	narg="files"
	oarg="tcb"
    else
        printf "${ystar} ERROR: Invalid type passed to conv_nsswitch(): ${type}\n\n"
        clean_fdir
        exit 1
    fi

    sed -e "s|^\(shadow:\(.*\)\)\(${oarg}\)\(.*\)|\1${narg}\4|g" /etc/nsswitch.conf >${new}
    if [ "`grep '^shadow:' ${new} | grep -q "${narg}"; echo $?`" -gt "0" ]; then
        printf "${ystar} ERROR: Changing /etc/nsswitch.conf failed!  Aborting!\n\n"
        clean_fdir
        exit 1
    fi
}


#
# function to change /etc/login.defs
#
function conv_logindefs()
{
    local type="${1}"
    local new="${fdir}/login.defs"

    if [ "${type}" == "tcb" ]; then
	narg="yes"
    elif [ "${type}" == "shadow" ]; then
	narg="no"
    else
        printf "${ystar} ERROR: Invalid type passed to conv_logindefs(): ${type}\n\n"
        clean_fdir
        exit 1
    fi

    sed -e "s|^USE_TCB.*|USE_TCB                 ${narg}|" /etc/login.defs >${new}
    if [ "`grep '^USE_TCB' ${new} | grep -q "${narg}"; echo $?`" -gt "0" ]; then
        printf "${ystar} ERROR: Changing /etc/login.defs failed!  Aborting!\n\n"
        clean_fdir
        exit 1
    fi
}


#
# function to change /etc/pam.d/system-auth
#
function conv_sysauth()
{
    local type="${1}"
    local new="${fdir}/system-auth"

    if [ "${type}" == "tcb" ]; then
	narg="tcb"
	oarg="shadow"
    elif [ "${type}" == "shadow" ]; then
	narg="shadow"
	oarg="tcb"
    else
        printf "${ystar} ERROR: Invalid type passed to conv_sysauth(): ${type}\n\n"
        clean_fdir
        exit 1
    fi

    sed -e "s|^\(password.*write_to=\)\(${oarg}\)\(.*\)|\1${narg}\3|" /etc/pam.d/system-auth >${new}
    if [ "`grep -e '^password.*pam_tcb' ${new} | grep -q "write_to=${narg}"; echo $?`" -gt "0" ]; then
        printf "${ystar} ERROR: Changing /etc/pam.d/system-auth failed!  Aborting!\n\n"
        clean_fdir
        exit 1
    fi
}


#
# function to move the newly converted files; this only happens if the
# files have been successfully converted and tcb_(un)convert has also
# been successfully executed -- if anything fails anywhere, the original
# files don't get replaced
#
function move_files()
{
    [[ -f ${fdir}/nsswitch.conf ]] && mv -f ${fdir}/nsswitch.conf /etc/nsswitch.conf && \
        chmod 0644 /etc/nsswitch.conf
    [[ -f ${fdir}/login.defs ]]    && mv -f ${fdir}/login.defs /etc/login.defs && \
        chmod 0644 /etc/login.defs
    [[ -f ${fdir}/system-auth ]]   && mv -f ${fdir}/system-auth /etc/pam.d/system-auth && \
        chown root:shadow /etc/pam.d/system-auth && chmod 0644 /etc/pam.d/system-auth
    clean_fdir
}

#
# function to switch from /etc/shadow to tcb
#
function go_tcb()
{
    printf "${header}"

    confirm "This will convert the system from using /etc/shadow to using /etc/tcb."

    if [ `grep '^USE_TCB' /etc/login.defs | grep -q 'yes'; echo $?` == "0" ]; then
	printf "${ystar} ERROR: System is already using tcb!\n\n"
	clean_fdir
	exit 1
    fi
    conv_sysauth "tcb"
    conv_logindefs "tcb"
    conv_nsswitch "tcb"
    tcb_convert
    if [ "${?}" == "0" ]; then
        printf "${star} tcb_convert successfully run; removing /etc/shadow...\n"
        logger -p ${syslogfac} -t set_tcb "tcb password scheme enabled and /etc/shadow removed"
        rm -f /etc/shadow /etc/shadow-
    else
        printf "${ystar} An error occurred executing tcb_convert!  Leaving /etc/shadow behind.\n\n"
        logger -p ${syslogfac} -t set_tcb "could not convert to tcb passwords, /etc/shadow not removed"
        clean_fdir
        exit 1
    fi
    move_files
    exit 0
}


#
# function to switch from tcb to /etc/shadow
#
function go_shadow()
{
    printf "${header}"

    confirm "This will convert the system from using /etc/tcb to using /etc/shadow."

    if [ `grep '^USE_TCB' /etc/login.defs | grep -q 'no'; echo $?` == "0" ]; then
        printf "${ystar} ERROR: System is already using shadow passwords!\n\n"
        clean_fdir
        exit 1
    fi
    conv_sysauth "shadow"
    conv_logindefs "shadow"
    conv_nsswitch "shadow"
    tcb_unconvert
    if [ "${?}" == "0" ]; then
        printf "${star} tcb_unconvert successfully run; removing /etc/tcb...\n"
        logger -p ${syslogfac} -t set_tcb "shadow password scheme enabled and /etc/tcb removed"
        rm -rf /etc/tcb
    else
        printf "${ystar} An error occurred executing tcb_unconvert!  Leaving /etc/tcb behind.\n\n"
        logger -p ${syslogfac} -t set_tcb "could not convert to shadow passwords, /etc/tcb not removed"
        clean_fdir
        exit 1
    fi
    move_files
    # make sure /etc/shadow has the right ownership and permissions
    chmod 0440 /etc/shadow
    chown root:shadow /etc/shadow
    exit 0
}

#
# function to change password hashing scheme
#
function go_hash()
{
    local passtype="${1}"
    local newdefs="${fdir}/login.defs"
    local newauth="${fdir}/system-auth"

    printf "${header}"

    check_if_migrated

    case "${passtype}" in
        md5)
            hash='\$1\$' ;;
        blowfish)
            hash='\$2a\$' ;;
        sha256c)
            hash='\$5\$' ;;
# temporarily disable sha512c support as it doesn't work with passwd in glibc (yet)
#        sha512c)
#            hash='\$6\$' ;;
        *)
            printf "${ystar} Invalid hash specified: ${passtype}.  Must be one of md5, blowfish, or sha256c.\n\n";
            clean_fdir;
            exit 1 ;;
    esac
    
    confirm "This will change the system to use ${passtype}-based passwords."

    if [ "$(grep -q ${hash} /etc/pam.d/system-auth; echo $?)" == "0" ]; then
        printf "${ystar} The system is already using the ${passtype} hash!\n\n"
        clean_fdir
        exit 1
    fi

    sed -e 's|^CRYPT_PREFIX.*|CRYPT_PREFIX             '$hash'|' /etc/login.defs >${newdefs}

    if [ "${passtype}" == "blowfish" ]; then
        # we only want CRYPT_ROUNDS when using blowfish
        sed -i -e 's|^#CRYPT_ROUNDS|CRYPT_ROUNDS|' ${newdefs}
    else
        sed -i -e 's|^CRYPT_ROUNDS|#CRYPT_ROUNDS|' ${newdefs}
    fi
    
    if [ "`grep '^CRYPT_PREFIX' ${newdefs} | grep -q "$hash"; echo $?`" -gt "0" ]; then
        printf "${ystar} ERROR: Changing /etc/login.defs failed!  Aborting!\n\n"
        clean_fdir
        exit 1
    fi

    sed -e 's|^\(.*\)\( prefix=\$[[:alnum:]]*\$\)\(.*\)|\1 prefix='$hash'\3|g' /etc/pam.d/system-auth >${newauth}

    if [ "${passtype}" == "blowfish" ]; then
        # we only want count= when using blowfish
        if [ "`grep -q 'count=' ${newauth}; echo $?`" -gt "0" ]; then
            # count= is missing; get the number of rounds from login.defs and use that value
            rounds=$(grep CRYPT_ROUNDS ${newdefs} | awk '{print $2}')
            sed -i -e 's|^auth\([[:blank:]]*[[:alnum:]]*[[:blank:]]*pam_tcb.*\)|auth\1 count='$rounds'|g' \
                   -e 's|^password\([[:blank:]]*[[:alnum:]]*[[:blank:]]*pam_tcb.*\)|password\1 count='$rounds'|g' ${newauth}
        fi
    else
        # drop count=
        sed -i -e 's|^\(.*\)\( count=[[:digit:]]\)\(.*\)|\1\3|g' ${newauth}
    fi
    
    if [ "`grep -e '^password.*pam_tcb' ${newauth} | grep -q "prefix=$hash"; echo $?`" -gt "0" ]; then
        printf "${ystar} ERROR: Changing /etc/pam.d/system-auth failed!  Aborting!\n\n"
        clean_fdir
        exit 1
    fi
    if [ "`grep -e '^auth.*pam_tcb' ${newauth} | grep -q "prefix=$hash"; echo $?`" -gt "0" ]; then
        printf "${ystar} ERROR: Changing /etc/pam.d/system-auth failed!  Aborting!\n\n"
        clean_fdir
        exit 1
    fi
    printf "${star} Converted system to use ${passtype} password hashing.\n"
    logger -p ${syslogfac} -t set_tcb "Converted system to use ${passtype} password hashing."
    move_files
    exit 0 
}


#
# function to migrate old /etc/pam.d/system-auth files from using
# pam_unix to use pam_tcb
#
function migrate_systemauth()
{
    local newdefs="${fdir}/login.defs"
    local newauth="${fdir}/system-auth"

    printf "${header}"

    confirm "This will convert the system from using pam_unix to using pam_tcb by adjusting\n/etc/pam.d/systen-auth and /etc/login.defs."

    if [ `grep -q tcb /etc/pam.d/system-auth; echo $?` == "1" ]; then
        printf "${star} Upgrading /etc/pam.d/system-auth to use pam_tcb... "
        cp -f /etc/pam.d/system-auth ${newauth}
        cp -f /etc/pam.d/system-auth /etc/pam.d/system-auth.original
        sed -i -e 's|^auth\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_unix.so.*|auth\1pam_tcb.so shadow nullok prefix=\$2y\$ count=8|' \
               -e 's|^account\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_unix.so.*|account\1pam_tcb.so shadow|' \
               -e 's|^password\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_unix.so.*|password\1pam_tcb.so use_authtok shadow write_to=shadow nullok prefix=\$2y\$ count=8|' \
               -e 's|^session\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_unix.so.*|session\1pam_tcb.so|' ${newauth}
        if [ "$?" != "0" ]; then
	    printf "${ystar} ERROR: Changing /etc/pam.d/system-auth failed!  Aborting!\n\n"
	    clean_fdir
	    exit 1
	fi
        printf "done\n\n!!! PLEASE NOTE THE FOLLOWING WARNING !!!\n\n"
        printf "Your original /etc/pam.d/system-auth has been saved as system-auth.original;\nplease double-check the changes prior to rebooting.\n\n"
        printf "Please note: the system will continue to use shadow passwords, but the default\npassword hash is now blowfish.\n\n"
    else
        printf "${ystar} /etc/pam.d/system-auth already uses pam_tcb!  Nothing to migrate!\n\n"
    fi
    # if /etc/login.defs doesn't have support for TCB, we need to add it
    # at the same time, comment out PASS_MIN_LEN as it's not valid with tcb
    # PASS_MIN_LEN will always be uncommented when using pam_unix
    if [ `grep -q -e '^PASS_MIN_LEN' /etc/login.defs; echo $?` == "0" ]; then
        printf "${star} Upgrading /etc/login.defs to use TCB... "
        sed -e 's|^PASS_MIN_LEN|#PASS_MIN_LEN|g' /etc/login.defs >${newdefs}
        if [ `grep -q USE_TCB /etc/login.defs; echo $?` == "1" ]; then
            # the system does not have TCB support in /etc/login.defs so let's add it
            # even on subsequent --revert calls, the USE_TCB keyword will always remain
            cat <<EOT >>${newdefs}

#
# The password hashing method and iteration count to use for group
# passwords that may be set with gpasswd(1).
#
CRYPT_PREFIX            \$2a\$
CRYPT_ROUNDS            8

#
# Whether to use tcb password shadowing scheme.  Use 'yes' if using
# tcb and 'no' if using /etc/shadow
#
USE_TCB                 no

#
# Whether newly created tcb-style shadow files should be readable by
# group "auth".
#
TCB_AUTH_GROUP          yes

#
# Whether useradd should create symlinks rather than directories under
# /etc/tcb for newly created accounts with UIDs over 1000.  See tcb(5)
# for information on why this may be needed.
#
TCB_SYMLINKS            no
EOT
        fi
        # USE_TCB may be in login.defs, but if it's commented out, we need to uncomment it
        if [ `grep -q -e '^#USE_TCB' /etc/login.defs; echo $?` == "0" ]; then
        
            sed -i -e 's|^#USE_TCB|USE_TCB|g; s|^\(#CRYPT\)_\(.*\)|CRYPT_\2|g; s|^\(#TCB\)_\(.*\)|TCB_\2|g' ${newdefs}
        fi
        printf "done\n\n"
    fi
    move_files
    # we should also make sure /etc/shadow has the right permissions
    chmod 0440 /etc/shadow
    chown root:shadow /etc/shadow
    exit 0
}


#
# function to migrate new /etc/pam.d/system-auth files from using
# pam_tcb to use pam_unix
#
#
function revert_systemauth()
{
    local newdefs="${fdir}/login.defs"
    local newauth="${fdir}/system-auth"

    printf "${header}"

    confirm "This will convert the system from using pam_tcb to using pam_unix."

    if [ `grep -q unix /etc/pam.d/system-auth; echo $?` == "1" ]; then
        printf "${star} Reverting /etc/pam.d/system-auth to use pam_unix... "
        cp -f /etc/pam.d/system-auth ${newauth}
        cp -f /etc/pam.d/system-auth /etc/pam.d/system-auth.tcb-enabled
        sed -i -e 's|^auth\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_tcb.so.*|auth\1pam_unix.so try_first_pass nullok|' \
               -e 's|^account\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_tcb.so.*|account\1pam_unix.so|' \
               -e 's|^password\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_tcb.so.*|password\1pam_unix.so try_first_pass use_authtok shadow nullok md5|' \
               -e 's|^session\([[:blank:]]*[[:alpha:]]*[[:blank:]]*\)pam_tcb.so.*|session\1pam_unix.so|' ${newauth}
        if [ "$?" != "0" ]; then
	    printf "${ystar} ERROR: Changing /etc/pam.d/system-auth failed!  Aborting!\n\n"
	    clean_fdir
	    exit 1
	fi
        printf "done\n\n!!! PLEASE NOTE THE FOLLOWING WARNING !!!\n\n"
        printf "Your previous /etc/pam.d/system-auth has been saved as system-auth.tcb-enabled;\nplease double-check the changes prior to rebooting.\n\n"
    else
        printf "${ystar} /etc/pam.d/system-auth already uses pam_unix!  Nothing to revert!\n\n"
    fi

    lib="`rpm --eval %_lib`"
    if [ "${lib}" != "lib" -o "${lib}" != "lib64" ]; then
        lib="lib"
    fi

    if [ -f /${lib}/security/pam_unix.so ]; then
        if [ `grep -q -e '^#PASS_MIN_LEN' /etc/login.defs; echo $?` == "0" ]; then
            # this is the "real" pam_unix, so set back PASS_MIN_LEN in /etc/login.defs
            # also comment out all the TCB-related stuff; it doesn't hurt, but this is more proper
            printf "${star} Changing /etc/login.defs to enable PASS_MIN_LEN for pam_unix... "
            sed -e 's|^#PASS_MIN_LEN|PASS_MIN_LEN|g; s|^USE_TCB|#USE_TCB|g; s|^\(CRYPT\)_\(.*\)|#CRYPT_\2|g; s|^\(TCB\)_\(.*\)|#TCB_\2|g' \
                /etc/login.defs >${newdefs}
            printf "done\n\n"
        fi
    fi
    move_files
    exit 0

}


#
# function to add nis support
#
function nis()
{
    local newauth="${fdir}/system-auth"

    printf "${header}"

    check_if_migrated

    if [ "$(grep -q "passwd" /etc/pam.d/system-auth; echo $?)" -eq "0" ]; then
	printf "${ystar} Nothing to upgrade, NIS is already configured!\n\n"
	exit 1
    fi

    confirm "This will upgrade the system-auth to support NIS password authentication."

    printf "${star} Updating /etc/pam.d/system-auth to support NIS... "
    if [ "$(grep -q "shadow" /etc/pam.d/system-auth; echo $?)" -eq "0" ]; then
        sed -e 's| shadow| shadow passwd|g' /etc/pam.d/system-auth >${newauth}
    else
        sed -e 's| tcb| tcb passwd|g' /etc/pam.d/system-auth >${newauth}
    fi
    if [ "`grep -q "passwd" ${newauth}; echo $?`" -gt "0" ]; then
        printf "\n${ystar} ERROR: Changing /etc/pam.d/system-auth failed!  Aborting!\n\n"
        clean_fdir
        exit 1
    fi
    move_files
    printf "done\n\n"
    exit 0
}


#
# main execution
#

# create a temporary directory to store all changed files so we can replace
# them in one shot to ensure that if one fails, nothing gets replaced
fdir=`mktemp -d /tmp/set_tcb.XXXXXX`

if [ "${1}" == "-d" -o "${1}" == "--debug" ]; then
    debug=1
    shift
fi

# silent option only to be used really by rpm
if [ "${1}" == "--auto" ]; then
    automode=1
    shift
fi

case "${1}" in
    -h|--help)
        usage; clean_fdir; exit 0 ;;
    -t|--tcb)
        go_tcb ;;
    -s|--shadow)
        go_shadow ;;
    -a|--hash)
        go_hash ${2} ;;
    -m|--migrate)
        migrate_systemauth ;;
    -r|--revert)
        revert_systemauth ;;
    -n|--nis)
        nis ;;
    *)
        usage; clean_fdir; exit 1 ;;
esac

clean_fdir
exit 0
