Listing 4: chkaging Script

#!/bin/sh
#
#       chkaging
#
#       Report current password aging settings from the
#       /etc/passwd file.  If -a is used, translates an
#       aging value into a set of weeks numbers.  If -d
#       is used, translates a set of weeks number into an
#       aging value.
#
#       Each aging value is formed from a set of base-64
#       numbers representing:
#
#       1.      the number of weeks maximum before a password
#               change is forced by the system,
#       2.      the number of weeks minimum before a new
#               password may be changed by the user, and
#       3.      the number of weeks elapsed since week 0, 1970,
#               when the password was last changed.
#
#       The base-64 numbers are formed from the set of
#       alphanumeric characters and two punctuation marks.
#       The aging value follows the password, separated by a
#       comma.  If the comma isn't there, aging is
#       deactiviated for that account.  The weeks elapsed
#       string may be missing, which corresponds to 0 weeks
#       elapsed.  That should force the password to change
#       with the next login.  According to passwd(4), the ..
#       (00) should always force a new password on the next
#       login.
#
#       Copyright 1994, Lawrence S Reznick

#       HISTORY
#
# 94Apr19       LSR
#       The base-64 digits are output in reverse place-value
#       order.  That is, if the digits should have been Hm
#       (H in the 64s place and m in the 1s place), the
#       password aging system will output it as mH.
#       Corrected the weeknum() & code_val() functions to
#       handle it that way.
#
#       Also, the NOW_WEEKS value is off because a 365-day
#       year is really 52 weeks plus 1 day.  expr doesn't
#       work with floating point, so the simplest solution
#       is to add 1 day for every 7 years since the epoch.

PW_FILE=/etc/passwd                     # Point to passwd file

#
# NOW_WEEKS holds the number of weeks elapsed since
# 1970: aging epoch
#

#NOW_WEEKS=`expr \( \`date '+%Y'\` - 1970 \) \* 52 + \`date '+%U'\``
NOW_WEEKS=`expr \`date '+%Y'\` - 1970`
NOW_WEEKS=`expr $NOW_WEEKS \* 52 + \`date '+%U'\` + \( $NOW_WEEKS / 7 \)`

#
# Given an encoded aging weeks value, return the
# numeric equivalent
#

DIGITS="./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

weeknum ()
{
        echo $1 |
        awk '{
#               for (i = 1; i <= length; i++) {
                i = length;
                do {
                        sum *= 64;
                        sum += index(digits, substr($0, i, 1)) - 1;
                } while (--i);
        }
        END {
                print sum;
        }' digits=$DIGITS
}

#
# Given a comma-separated number list (max,min,elapsed),
# output aging code
#

code_val ()
{
        echo $1 |
        awk -F, '
                $1 < 0 || $1 >= length(digits) {
                        print "Max out of range! 0 <= Max < ", length(digits);
                        exit;
                }

                $2 < 0 || $2 >= length(digits) {
                        print "Min out of range! 0 <= Min < ", length(digits);
                        exit;
                }

                {
                        code = substr(digits, $1 + 1, 1);
                        code = code substr(digits, $2 + 1, 1);

                        elapsed = $3;
                        do {
                                quot = int(elapsed / 64);
                                remn = elapsed % 64;
#                               base64 = substr(digits, remn + 1, 1) base64;
                                base64 = base64 substr(digits, remn + 1, 1);
                                elapsed = quot;
                        } while (elapsed >= 64);
#                       base64 = substr(digits, quot + 1, 1) base64;
                        base64 = base64 substr(digits, quot + 1, 1);

                        code = code base64;
                        print code;
                }
        ' digits=$DIGITS
}

#
# Given a full aging value (no comma), display its
# numeric values
#

aging_val ()
{
        code = $1
        if [ `expr length $code` -eq 2 ]
        then
                code=${code}..
        else if [ `expr length $code` -lt 3 ]
        then
                echo Invalid aging code \"$code\"
                return
        fi
        fi

        MAX=`expr $code : '\(.\)'`              # Grab max weeks
        MIN=`expr $code : '.\(.\)'`             # Grab min weeks
        ELAPSED=`expr $code : '..\(.*\)'`       # Grab weeks since epoch

        echo "aging set to `weeknum $MAX` weeks max, \c"
        echo "`weeknum $MIN` weeks min, \c"
        echo "last changed `expr $NOW_WEEKS - \`weeknum $ELAPSED\`` weeks ago."
}

#
# Collect the 1st 2 fields of the password file & parse
# the aging info
#

show_aging ()
{
        INFO=`
        cut -f1-2 -d: $PW_FILE |        # Extract name & passwd fields
        egrep -v '\*'`                  # Ignore disabled accounts

        for n in $INFO
        do
#               echo "`echo $n | cut -d: -f1`\t\c"

                oIFS="$IFS"
                IFS=","                 # Use field sep to parse aging
                set $n                  # $2 has the aging field value
                IFS="$oIFS"

                if [ -z "$2" ]
                then
#                       echo aging inactive!
                        NOAGING="$NOAGING `echo $n | cut -d: -f1`"
                else
                        echo "`echo $n | cut -d: -f1`\t\c"
                        aging_val $2
                fi
        done

        if [ -n "$NOAGING" ]
        then
                echo "\rAging inactive ($PWFILE order):"
                echo $NOAGING | tr ' ' '\012' | pr -t -6
        fi
}

#
# Main processing
#

if [ $# -eq 0 ]                         # Default execution
then
        show_aging                      # Show passwd file's aging settings
        exit 0
fi

case $1 in
        -a)                             # Turn aging value into weeks number
                shift                   # Skip to the aging value
                for v
                do
                        echo $v = `aging_val $v`
                done
                ;;

        -d)                             # Turn weeks numbers into aging value
                shift                   # Skip to the week number sets
                for v
                do
                        echo $v = `code_val $v`
                done
                ;;

        *)                              # Tell how program works
                echo "Usage:\t`basename $0` [-a aging_codes] [-d week_nums]\n"
                echo "No arg:\tTells aging settings for "$PW_FILE" file."
                echo "-a arg:\tTurns aging codes into week numbers."
                echo "-d arg:\tTurns max,min,elapsed num sets into aging codes."
                ;;
esac
exit 0

