From 0737d0c64777fec9bb9dd69ab68cf9c0b7d861e2 Mon Sep 17 00:00:00 2001 From: fatalerrors Date: Wed, 1 Apr 2026 17:52:09 +0200 Subject: [PATCH] reworked genpwd / introduce pwdscore --- profile.d/help.sh | 1 + profile.d/pwd.sh | 349 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 278 insertions(+), 72 deletions(-) diff --git a/profile.d/help.sh b/profile.d/help.sh index 8eb13d2..918fb1c 100644 --- a/profile.d/help.sh +++ b/profile.d/help.sh @@ -63,6 +63,7 @@ help() printf "ppn\t\tDisplay process matching the exact process name given in parameter\n" printf "ppu\t\tDisplay processes owned by the given user\n" printf "profile_upgrade\tUpgrade profile to the latest version\n" + printf "pwdscore\tCalculate password strength score\n" printf "rain\t\tLet the rain fall\n" printf "rmhost\t\tRemove host (IP and/or DNS name) from current known_hosts\n" printf "rmspc\t\tRemove spaces from file and directory names\n" diff --git a/profile.d/pwd.sh b/profile.d/pwd.sh index 383dc7b..fba6b2a 100644 --- a/profile.d/pwd.sh +++ b/profile.d/pwd.sh @@ -53,21 +53,22 @@ genpwd() { local length=16 - local occurs=2 # Bug, if set to 1, seems to be ignored + local occurs=2 local symb=1 maj=1 min=1 numb=1 local nbpwd=1 - local extcar + local extcar="" -local PARSED - PARSED=$(getopt -o hsnu l e:L:o: --long \ - help,nosymbols,nonumbers,noup,nolow,extracars:,length:,occurences: -n 'genpwd' -- "$@") + local PARSED + PARSED=$(getopt -o hsnule:L:o: --long \ + help,nosymbols,nonumbers,noup,nolow,extracars:,length:,occurences:,occurrences: \ + -n 'genpwd' -- "$@") if [[ $? -ne 0 ]]; then return 1; fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) - printf "genpwd: Generate secure random password(s).\n\n" + printf "genpwd: Generate random password(s).\n\n" printf "Usage: genpwd [options] [nb_passwd]\n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" @@ -75,9 +76,9 @@ local PARSED printf "\t-n, --nonumbers\t\tExclude numbers\n" printf "\t-u, --noup\t\tExclude uppercase letters\n" printf "\t-l, --nolow\t\tExclude lowercase letters\n" - printf "\t-e, --extracars \tAdd characters to list\n" + printf "\t-e, --extracars \tAdd characters to the pool\n" printf "\t-L, --length \tSet password length (default: 16)\n" - printf "\t-o, --occurences \tMax occurences per character (default: 2)\n" + printf "\t-o, --occurences \tMax occurrences per character (default: 2)\n" return 0 ;; -s|--nosymbols) @@ -102,102 +103,306 @@ local PARSED ;; -L|--length) length="$2" - if ! [[ $length =~ ^[0-9]+$ ]]; then - disp E "The --length parameter requires a number." + if ! [[ $length =~ ^[1-9][0-9]*$ ]]; then + disp E "The --length parameter requires a positive integer." return 1 fi shift 2 ;; - -o|--occurences) + -o|--occurences|--occurrences) occurs="$2" - if ! [[ $occurs =~ ^[1-9]+$ ]]; then - disp E "The --occurs parameter requires a number from 1 to 9." + if ! [[ $occurs =~ ^[1-9][0-9]*$ ]]; then + disp E "The --occurences parameter requires a positive integer." return 1 fi shift 2 ;; --) - shift; break + shift + break ;; *) - break + disp E "Invalid options, use \"genpwd --help\" to display usage." + return 1 ;; esac done - if [[ -n "$1" ]]; then + if [[ $# -gt 1 ]]; then + disp E "Too many positional arguments. Use only [nb_passwd]." + return 1 + fi + + if [[ $# -eq 1 ]]; then nbpwd="$1" - if ! [[ $nbpwd =~ ^[0-9]+$ ]]; then - disp E "The number of password to generate must be a number." + if ! [[ $nbpwd =~ ^[1-9][0-9]*$ ]]; then + disp E "The number of passwords to generate must be a positive integer." return 1 fi fi - # Function selecting a random caracter from the list in parameter - pickcar() { - # When a character is picked we check if it's not appearing already twice - # elsewhere, we choose an other char, to compensate weak bash randomizer - while [[ -z $char ]]; do - local char="${1:RANDOM%${#1}:1} $RANDOM" - if [[ $(awk -F"$char" '{print NF-1}' <<<"$picked") -gt $occurs ]]; then - unset char - fi - done - picked+="$char" - echo "$char" + local carset="" + local unique_carset="" + local ch="" + local i=0 + local n=0 + local idx=0 + local attempts=0 + local count=0 + local max_attempts=0 + local set="" + local char="" + local -a required_sets=() + declare -A seen_chars=() + + (( symb )) && { + required_sets+=('!.@#&%/^-_') + carset+='!.@#&%/^-_' } + (( numb )) && { + required_sets+=('0123456789') + carset+='0123456789' + } + (( maj )) && { + required_sets+=('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + carset+='ABCDEFGHIJKLMNOPQRSTUVWXYZ' + } + (( min )) && { + required_sets+=('abcdefghijklmnopqrstuvwxyz') + carset+='abcdefghijklmnopqrstuvwxyz' + } + if [[ -n $extcar ]]; then + required_sets+=("$extcar") + carset+="$extcar" + fi - disp I "Generating $nbpwd passwords, please wait..." + if [[ -z $carset ]]; then + disp E "No characters are available. Re-enable at least one character class." + return 1 + fi + + for (( i=0; i<${#carset}; i++ )); do + ch=${carset:i:1} + if [[ -z ${seen_chars["$ch"]+x} ]]; then + seen_chars["$ch"]=1 + unique_carset+="$ch" + fi + done + unset seen_chars + carset="$unique_carset" + + if (( ${#required_sets[@]} > length )); then + disp E "The selected character classes require a longer password." + return 1 + fi + + if (( length > ${#carset} * occurs )); then + disp E "The occurrence limit is too strict for the selected length." + disp E "Please allow more characters or increase --occurences." + return 1 + fi + + disp I "Generating $nbpwd password(s), please wait..." for (( n=1; n<=nbpwd; n++ )); do - { - local carset='' # store final caracter set to use - local picked='' # store already used caracter - local rlength=0 # store already assigned length of caracters + local -a password_chars=() + local -A char_count=() + max_attempts=$(( ${#carset} * (occurs + 1) + 32 )) - # ?, *, $ and \ impossible to use to my knowledge as it would be interpreted - if [[ $symb == 1 ]]; then - pickcar '!.@#&%/^-_' - carset+='!.@#&%/^-_' - ((rlength++)) - fi - if [[ $numb == 1 ]]; then - pickcar '0123456789' - carset+='0123456789' - ((rlength++)) - fi - if [[ $maj == 1 ]]; then - pickcar 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - carset+='ABCDEFGHIJKLMNOPQRSTUVWXYZ' - ((rlength++)) - fi - if [[ $min == 1 ]]; then - pickcar 'abcdefghijklmnopqrstuvwxyz' - carset+='abcdefghijklmnopqrstuvwxyz' - ((rlength++)) - fi - if [[ -n $extcar ]]; then - pickcar "$extcar" - carset+=$extcar - ((rlength++)) - fi + for set in "${required_sets[@]}"; do + attempts=0 + while :; do + if (( attempts >= max_attempts )); then + disp E "Unable to satisfy the occurrence limit with the current settings." + return 1 + fi - # Check if we have enough car to have something viable - if [[ ${#carset} -lt $length ]]; then - disp E 'Not enought caracters are authorised for the password length.' - disp E 'Please allow more caracter (preferably) or reduce password lentgh.' - return 1 - fi + idx=$(( RANDOM % ${#set} )) + char=${set:idx:1} + count=${char_count["$char"]:-0} - for i in $(seq 1 $(($length - $rlength))); do - pickcar "$carset" + if (( count < occurs )); then + char_count["$char"]=$(( count + 1 )) + password_chars+=("$char") + break + fi + + ((attempts++)) done - } | sort -R | awk '{printf "%s", $1}' - unset picked carset rlength - echo + done + + while (( ${#password_chars[@]} < length )); do + attempts=0 + while :; do + if (( attempts >= max_attempts )); then + disp E "Unable to satisfy the occurrence limit with the current settings." + return 1 + fi + + idx=$(( RANDOM % ${#carset} )) + char=${carset:idx:1} + count=${char_count["$char"]:-0} + + if (( count < occurs )); then + char_count["$char"]=$(( count + 1 )) + password_chars+=("$char") + break + fi + + ((attempts++)) + done + done + + for (( i=${#password_chars[@]} - 1; i>0; i-- )); do + idx=$(( RANDOM % (i + 1) )) + char=${password_chars[i]} + password_chars[i]=${password_chars[idx]} + password_chars[idx]=$char + done + + printf '%s' "${password_chars[@]}" + printf '\n' done } export -f genpwd # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +# pwdscore : score a password quality from 1 to 100 +# Usage: pwdscore [options] +pwdscore() +{ + local verbose=0 + local PARSED + + PARSED=$(getopt -o hv --long help,verbose -n 'pwdscore' -- "$@") + if [[ $? -ne 0 ]]; then return 1; fi + eval set -- "$PARSED" + + while true; do + case "$1" in + -h|--help) + printf "pwdscore: Score a password from 1 to 100.\n\n" + printf "Usage: pwdscore [options] \n\n" + printf "Options:\n" + printf "\t-h, --help\t\tDisplay this help screen\n" + printf "\t-v, --verbose\t\tShow details about the computed score\n" + return 0 + ;; + -v|--verbose) + verbose=1 + shift + ;; + --) + shift + break + ;; + *) + disp E "Invalid options, use \"pwdscore --help\" to display usage." + return 1 + ;; + esac + done + + [[ $# -ne 1 ]] && { + disp E "Please provide exactly one password to score." + return 1 + } + + local password="$1" + local lower=${password,,} + local length=${#password} + local score=0 + local rating="very weak" + local unique_count=0 + local i=0 idx=0 + local c1=0 c2=0 c3=0 + local ch="" + local has_lower=0 has_upper=0 has_digit=0 has_symbol=0 + local -A seen=() + + if [[ -z $password ]]; then + printf '1\n' + return 0 + fi + + if (( length >= 20 )); then + score=40 + elif (( length >= 16 )); then + score=34 + elif (( length >= 12 )); then + score=28 + elif (( length >= 8 )); then + score=18 + else + score=$(( length * 2 )) + fi + + [[ $password =~ [a-z] ]] && { has_lower=1; ((score += 12)); } + [[ $password =~ [A-Z] ]] && { has_upper=1; ((score += 12)); } + [[ $password =~ [0-9] ]] && { has_digit=1; ((score += 12)); } + [[ $password =~ [^[:alnum:]] ]] && { has_symbol=1; ((score += 14)); } + + for (( i=0; i 100 )) && score=100 + + if (( score >= 90 )); then + rating='excellent' + elif (( score >= 75 )); then + rating='strong' + elif (( score >= 60 )); then + rating='good' + elif (( score >= 40 )); then + rating='fair' + elif (( score >= 20 )); then + rating='weak' + fi + + if (( verbose )); then + printf 'Score: %d/100\n' "$score" + printf 'Rating: %s\n' "$rating" + printf 'Length: %d\n' "$length" + printf 'Lowercase: %s\n' "$([[ $has_lower -eq 1 ]] && echo yes || echo no)" + printf 'Uppercase: %s\n' "$([[ $has_upper -eq 1 ]] && echo yes || echo no)" + printf 'Digits: %s\n' "$([[ $has_digit -eq 1 ]] && echo yes || echo no)" + printf 'Symbols: %s\n' "$([[ $has_symbol -eq 1 ]] && echo yes || echo no)" + printf 'Unique chars: %d\n' "$unique_count" + else + printf '%d\n' "$score" + fi +} +export -f pwdscore +# ------------------------------------------------------------------------------ + + # EOF