Files
profile/profile.d/pwd.sh
2026-04-01 17:52:09 +02:00

409 lines
13 KiB
Bash

#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
# Protected by the BSD3 license. Please read bellow for details.
#
# * Redistribution and use in source and binary forms,
# * with or without modification, are permitted provided
# * that the following conditions are met:
# *
# * Redistributions of source code must retain the above
# * copyright notice, this list of conditions and the
# * following disclaimer.
# *
# * Redistributions in binary form must reproduce the above
# * copyright notice, this list of conditions and the following
# * disclaimer in the documentation and/or other materials
# * provided with the distribution.
# *
# * Neither the name of the copyright holder nor the names
# * of any other contributors may be used to endorse or
# * promote products derived from this software without
# * specific prior written permission.
# *
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# genpwd : generate a password with different criteria
# Usage: genpwd [options] [--extracars=<cars>] [--length=<n>] [nb_passwd]
# Options:
# -h, --help Display that help screen
# -s, --nosymbols Exclude symbols
# -n, --nonumbers Exclude numbers
# -u, --noup Exclude uppercase letters
# -l, --nolow Exclude lowercase letters
# -e=<c>, --extracars=<c>
# Add the given caracters to the possible caracter list
# -L=<n>, --length=<n>
# Set length of the password (default is 16)
# -o=<n>, --occurences=<n>
# Set the maximum occurences of a same caracter (default is 2)
# The function is very slow on Windows
genpwd()
{
local length=16
local occurs=2
local symb=1 maj=1 min=1 numb=1
local nbpwd=1
local extcar=""
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 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"
printf "\t-s, --nosymbols\t\tExclude symbols\n"
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 <c>\tAdd characters to the pool\n"
printf "\t-L, --length <n>\tSet password length (default: 16)\n"
printf "\t-o, --occurences <n>\tMax occurrences per character (default: 2)\n"
return 0
;;
-s|--nosymbols)
symb=0
shift
;;
-n|--nonumbers)
numb=0
shift
;;
-u|--noup)
maj=0
shift
;;
-l|--nolow)
min=0
shift
;;
-e|--extracars)
extcar="$2"
shift 2
;;
-L|--length)
length="$2"
if ! [[ $length =~ ^[1-9][0-9]*$ ]]; then
disp E "The --length parameter requires a positive integer."
return 1
fi
shift 2
;;
-o|--occurences|--occurrences)
occurs="$2"
if ! [[ $occurs =~ ^[1-9][0-9]*$ ]]; then
disp E "The --occurences parameter requires a positive integer."
return 1
fi
shift 2
;;
--)
shift
break
;;
*)
disp E "Invalid options, use \"genpwd --help\" to display usage."
return 1
;;
esac
done
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 =~ ^[1-9][0-9]*$ ]]; then
disp E "The number of passwords to generate must be a positive integer."
return 1
fi
fi
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
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 -a password_chars=()
local -A char_count=()
max_attempts=$(( ${#carset} * (occurs + 1) + 32 ))
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
idx=$(( RANDOM % ${#set} ))
char=${set:idx:1}
count=${char_count["$char"]:-0}
if (( count < occurs )); then
char_count["$char"]=$(( count + 1 ))
password_chars+=("$char")
break
fi
((attempts++))
done
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] <password>
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] <password>\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<length; i++ )); do
ch=${password:i:1}
if [[ -z ${seen["$ch"]+x} ]]; then
seen["$ch"]=1
((unique_count++))
fi
done
(( score += (unique_count * 10) / length ))
if [[ $lower =~ (password|admin|root|qwerty|azerty|welcome|letmein|secret|changeme) ]]; then
((score -= 25))
fi
if [[ $lower =~ (1234|abcd|qwer|0000|1111|aaaa) ]]; then
((score -= 15))
fi
[[ $password =~ (.)\1\1 ]] && ((score -= 10))
(( length < 8 )) && ((score -= 10))
(( unique_count * 2 < length )) && ((score -= 10))
for (( idx=0; idx<length-2; idx++ )); do
printf -v c1 '%d' "'${lower:idx:1}"
printf -v c2 '%d' "'${lower:idx+1:1}"
printf -v c3 '%d' "'${lower:idx+2:1}"
if (( (c2 == c1 + 1 && c3 == c2 + 1) || \
(c2 == c1 - 1 && c3 == c2 - 1) )); then
((score -= 10))
break
fi
done
(( score < 1 )) && score=1
(( score > 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