#!/bin/bash # password.sh - password manipulation, using openssl passwd # Editor for shadow password file, following format from SHADOW(5) # Version: 0.0.23 -- Last update: Mon Mar 23 16:37:22 2026 # (C) 2025-2026 Felix Hauri - felix@f-hauri.ch # - Localized using "shadow.mo" and "bash.mo" messages file. # - Password read interaction showing stars `*` or real character using `F1` # This script is for educational purpose only! Don't use this on production # environment! # Run without argument, on some dir that doesn't contain some ``passfile.txt'', # this will show a quick demo. # Main variables : "${passfile:=passfile.txt}" # without path, will consider current working dir : "${defAlgo:=5}" # 1 apr1 aixmd5 5 6 : "${maxTries:=3}" # Max retry when asked to autheticate. # This variables could be set at run time by using: # $ defAlgo=6 passfile=/path/to/somefile ./password.sh -a someUser@someDomain # $ passfile=/path/to/somefile ./password.sh -l # Searched for most suitable localized messages TEXTDOMAIN=shadow;: $TEXTDOMAIN declare -A msgs=$'( # Localized messages regarding $LANG [reenter]=$"Re-enter new password: " [invalid]=$"Invalid password.\n" [badName]=$"%s: invalid name: \47%s\47\n" [nochange]=$"%s: no changes\n" [notUser]=$"%s: user \47%s\47 does not exist\n" [oldPas]=$"Old password: " [nomatch]=$"They don\47t match; try again" [pass]=$"Password: " [alreadyExist]=$"%s: user \47%s\47 already exists\n" [authFail]=$"%s: Authentication failure\n" [newPas]=$"New password: " [noPassFile]=$"%s: the shadow password file is not present\n" [editedSuccess]=$"passwd: password updated successfully\n" [username]=$"Username Port Latest" [lastChange]=$"Last Password Change (YYYY-MM-DD)" )' # With some cosmetic for list (-l) header msgs[username]=${msgs[username]%% *} # Drop from two spaces to eol read -r 'msgs[lastChange]' <<<"${msgs[lastChange]%\(*)}" # Drop (YYYY-MM-DD) TEXTDOMAIN=bash declare -A msgs+=$'( [abort]=\\\\n$"Aborting..."\\\\n [argMissing]=$"argument expected"\\\\n )' die() { local msg="$1" shift # shellcheck disable=2059 printf >&2 "$msg" "$0" "$@" exit 1 } weakness() { # This is my arbitrary weakness calculator - 8 levels [[ -z $bcIO ]] || (( bcIO[0] == 0 )) && coproc bcIO { exec bc -l ;} local -i base=0 _val local string="$1" wMsgs=('too weak' weak easy 'good enough' good 'very good' strong 'very strong' ) [[ $string == *[a-z]* ]] && base+=26 [[ $string == *[A-Z]* ]] && base+=26 [[ $string == *[0-9]* ]] && base+=10 if [[ -z ${string//*['!'-'/'':'-'@''['-$'\140'$'\173'-$'\176']*} ]]; then base+=16 local specCnt=${string//[^'!'-'/'':'-'@''['-'`''{'-'~']} # shellcheck disable=2086 # wanted behaviour printf -v specCnt '[%d]+=1 ' ${specCnt//?/\'& } local -ia "specCnt=( $specCnt )" (( ${#specCnt[@]} > 1 )) && base+=16 fi echo >&"${bcIO[1]}" \ "scale=0; r= l( $base ^ ${#string} ) / 16 ;if ( r > 7 ) 7 else r " IFS=. read -ru "${bcIO[0]}" _val printf -v "${2:-weakval}" '%s%*s' "${wMsgs[_val]}" \ $(( (11-${#wMsgs[_val]})/2 )) '' } getPass() { # getPass [-w] [variable name] local -i strpos=0 show=0 replCc=0 replcCw=(1 2 2 1) showWeakness=0 [[ $1 == -w ]] && showWeakness=1 && shift local key subkey string='' replChr=('*' '😜' '🔑' '●') weak while true; do if (( showWeakness )); then weakness "$string" weak if ((show)); then printf '\r%s \e[3;1m%11s\e[0m %s\e[K\r\e[%dC' "$1" "$weak" \ "$string" $((${#1}+13+strpos)) else printf '\r%s \e[3;1m%11s\e[0m %s\e[K\r\e[%dC' "$1" "$weak" \ "${string//?/${replChr[replCc]}}" \ $((${#1}+13+strpos*replcCw[replCc])) fi else if ((show)); then printf '\r%s %s\e[K\r\e[%dC' "$1" "$string" $((${#1}+1+strpos)) else printf '\r%s %s\e[K\r\e[%dC' "$1" \ "${string//?/${replChr[replCc]}}" \ $((${#1}+1+strpos*replcCw[replCc])) fi fi IFS= read -rsn1 -d '' key [[ $key == $'\e' ]] && while IFS= read -d '' -rsn 1 -t .002 subkey; do key+=$subkey done case $key in $'\n' ) # Return break ;; $'\e' ) # Escape (abort) return 3 ;; $'\E[D' ) # Move Cursor Left strpos=' strpos < 1? 0: strpos-1 ' ;; $'\E[C' ) # Move Cursor Right strpos=" strpos >= ${#string} ? ${#string} : strpos +1 " ;; $'\177' ) # Backspace (delete back) ((strpos)) && string=${string:0: strpos -1 }${string: strpos} strpos+=-1 ;; $'\E[3~' ) # Delete (delete forward) ((strpos < ${#string} )) && string=${string:0: strpos }${string: strpos + 1 } ;; $'\E[H' | $'\001' ) # Home or Ctrl+A (top of string) strpos=0 ;; $'\E[F' | $'\005' ) # End or Ctrl+E (end of string) strpos=${#string} ;; $'\t' ) replCc="( replCc + 1 ) % ${#replChr[@]}" # Tab (useless but fun) ;; $'\EOP' | $'\E[19~' | $'\E[24~' ) # F1, F8 or F12 (toggle show) show=1-show ;; [' '-~] ) # One printable character between ascii 0x20 and 0x7E string=${string::$strpos}$key${string:$strpos} strpos+=1 ;; esac done echo printf -v "${2:-passWord}" '%s' "$string" } checkName() { # Check for valid user name (maybe with one at (@), but no more) if [[ $1 == *[!a-zA-Z0-9@.-]* ]] || [[ ${1//[^@]} == @@* ]] ; then die "${msgs[badName]}" "$1" fi } getNewPass() { # Ask two time for new password, compare, then store hash in var. local pass1 pass2= [[ $yRe ]] || { # get localized Yes/No regular expression IFS=^ read -r _ yRe IFS=^ read -r _ nRe }< <(locale yesexpr noexpr) while :; do getPass -w "${msgs[newPas]}" pass1 (($?==3)) && die "${msgs[abort]}" getPass -w "${msgs[reenter]}" pass2 (($?==3)) && die "${msgs[abort]}" if [[ $pass1 == "$pass2" ]]; then # match IFS= read -r "${2:-passWord}" < <( openssl passwd -"$defAlgo" -stdin <<<"$pass1" ) return else printf '%s? ' "${msgs[nomatch]}" local ans= while [[ -z $ans ]]; do IFS= read -rsn 1 ans # shellcheck disable=2254 case ${ans,} in $yRe | $'\n' ) ans=y ;; $nRe | $'\e' ) ans=n ;; * ) ans= ;; esac done [[ $ans == n ]] && printf '\r\e[K' && die "${msgs[nochange]}" fi done } addUser() { # Check name, get new password, then add to passfile local newPass checkName "$1" [[ -f $passfile ]] && grep -q "^$1:" "$passfile" && die "${msgs[alreadyExist]}" "$1" getNewPass "$1" newPass printf >>"$passfile" '%s:%s:%s:0:99999:7:::\n' \ "$1" "$newPass" $(( EPOCHSECONDS / 86400 )) } editUser() { # Verify old pass, get new pass, replace in passfile, used sed. local force=false [[ $1 == -f ]] && force=true && shift checkName "$1" [[ -f $passfile ]] || die "${msgs[noPassFile]}" grep -q "^$1:" "$passfile" || die "${msgs[notUser]}" "$1" $force || testUser "$1" "${msgs[oldPas]}" getNewPass "$1" newPass sed -e "s|^$1:[^:]\+:[^:]\+:|$1:$newPass:$(( EPOCHSECONDS / 86400 )):|" \ -i "$passfile" } deleteUser() { # Remove user's entry from passfile, using sed. checkName "$1" [[ -f $passfile ]] || die "${msgs[noPassFile]}" grep -q "^$1:" "$passfile" || die "${msgs[notUser]}" "$1" sed -e "/^$1:/d" -i "$passfile" } testUser() { # Get algo, salt and hash from passfile, ask user and compare. local askmsg="${2:-${msgs[pass]}}" checkName "$1" [[ -f $passfile ]] || die "${msgs[noPassFile]}" grep -q "^$1:" "$passfile" || die "${msgs[notUser]}" "$1" local algo salt hash pass penalty foo hashed hashed=$(sed -ne "s/^$1:\([^:]\+\)\(:.*\|\)/\1/p" "$passfile") IFS=\$ read -r foo algo salt hash <<<"$hashed" [[ -z $hash ]] && hash=$algo salt=$foo algo=aixmd5 for (( penalty = maxTries ; penalty > 0 ; penalty-- )); do getPass "$askmsg" pass (($?==3)) && die "${msgs[abort]}" [[ $( openssl passwd -"$algo" -salt "$salt" -stdin <<<"$pass" ) == "$hashed" ]] && return 0 printf "%s" "${msgs[invalid]}" done die "${msgs[authFail]/$'\n'}" } listUsers() { # show user list, sorted by last modification time local list list="$( sed <"$passfile" -ne \ 's/^\([^:]\+\):[^:]*:\([^:]\+\).*/\1 \o44((\2*86400))/p' | sort -n -t\( -k 3 )" local -a list="( $list )" printf '\e[4m - %-22s %s\e[0m\n' \ "${msgs[username]}" "${msgs[lastChange]}" printf ' - %-22s %(%a %d %b %Y)T\n' "${list[@]}" } if [[ $1 ]]; then case $1 in -a ) # Add user shift addUser "$@" ;; -u|-c ) # Change user password shift editUser "$@" ;; -C ) # Force new password without asking for old password shift editUser -f "$@" ;; -d ) # Delete user shift deleteUser "$@" ;; -l ) # List users by last modification date listUsers exit 0 ;; * ) # Ask user password and validate testUser "$@" exit 0 ;; esac echo "${msgs[editedSuccess]}" else if [[ -e $passfile ]]; then die "${msgs[argMissing]}" else echo "No shadow password file found, no argument submitted!" echo "This is a bash shadow password file editor for sample!" echo "The main purpose of this script is to play with openssl passwd." echo "Demo:" set -- {a..z} {0..9} {A..Z} + - / _ \# . , \; \! \( \) \{ \} exec {spFd}> >(tac|tac) for i in titi:1 toto:apr1 tutu:aixmd5 tata:5 nelson:6; do printf -v pass %s "${*:RANDOM%$#+1:1}"{,,,,,,}{,,} echo -n "$ defAlgo=${i#*:} ./password.sh -a ${i%:*} # " echo "with password: '$pass'." # printf '\n' "[Tab] [Tab] $pass [Return] [F1] $pass [Return]" printf '⭾\U20E3 ⭾\U20E3%s ⏎\U20E3 [F1]%s ⏎\U20E3\n' \ "${pass//?/ &$'\342\203\243'}"{,} defAlgo=${i#*:} passfile=/dev/fd/$spFd ./password.sh -a ${i%:*} < <( printf '\t\t%s\n\eOP' "$pass" # $pass sleep .1 echo "$pass" # password ) done echo "Will produce:" exec {spFd}>&- wait fi fi