From d28b602827cda124f3458a386b6ad3e950d407a2 Mon Sep 17 00:00:00 2001 From: fatalerrors Date: Tue, 13 Oct 2026 14:46:18 +0200 Subject: [PATCH] pwd bug fix --- README.md | 4 +-- doc/CHANGELOG.md | 7 +++++ profile.d/pwd.sh | 82 +++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 79 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 581364b..d594fdc 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ A bar-style prompt showing current time, execution time of the last command | `finddead` | filefct | Find dead symbolic links in the given or current directory | | `findzero` | filefct | Find empty files in the given or current directory; `--delete` does not follow symlinks | | `gacp` | git | Add, commit and push changes; auto-pulls with rebase first if needed | -| `genpwd` | pwd | Generate one or more random secure passwords with configurable constraints | +| `genpwd` | pwd | Generate one or more random passwords with configurable constraints, from the kernel's random generator (`$SRANDOM`, or `/dev/urandom`); only the passwords go to stdout | | `ggraph` | git | Display a decorated git history graph | | `gpid` | processes | Give the list of PIDs matching the given process name(s) | | `gprune` | git | Delete local branches already merged into the main branch; `-D`/`--deep` also deletes (force) branches whose upstream branch was deleted on the remote, e.g. after a merge request | @@ -268,7 +268,7 @@ change the default without having to pass flags every time. | Key | Default | Description | | --- | --- | --- | | `GENPWD_DEFAULT_LENGTH` | `16` | Generated password length | -| `GENPWD_DEFAULT_OCCURS` | `1` | Number of character class occurrences | +| `GENPWD_DEFAULT_OCCURS` | `2` | Maximum number of occurrences of the same character in a password | | `GENPWD_DEFAULT_COUNT` | `1` | Number of passwords to generate | | `GENPWD_DEFAULT_SYMBOLS` | `1` | Include symbols (0/1) | | `GENPWD_DEFAULT_UPPERCASE` | `1` | Include uppercase letters (0/1) | diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index c91695c..1b06e06 100755 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -133,6 +133,13 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`). theme's colours; the prompt no longer forks `date` twice per command (`$EPOCHREALTIME`) and runs one `git status` instead of three git commands, without taking the index lock. +- `pwd.sh` (security): `genpwd` drew its characters from `$RANDOM` (15 bits, + predictable seed); it now uses `$SRANDOM` or `/dev/urandom` with unbiased + selection and shuffle. Its info message goes to stderr, so `pw=$(genpwd)` + captures only the password; the `GENPWD_DEFAULT_*` values are validated + (they were evaluated as arithmetic expressions); `pwdscore` classifies + characters with locale-proof classes; README default of + `GENPWD_DEFAULT_OCCURS` corrected. - `compress.sh`: option flags and loop variables no longer leak into the interactive shell. diff --git a/profile.d/pwd.sh b/profile.d/pwd.sh index 483caad..9209bef 100644 --- a/profile.d/pwd.sh +++ b/profile.d/pwd.sh @@ -36,6 +36,42 @@ load_conf "pwd" +# ------------------------------------------------------------------------------ +# Internal helper: put in _PWD_RAND an integer uniformly distributed in +# [0, $1), for 1 <= $1 <= 2^32. $RANDOM is NOT usable for passwords: it only +# has 15 bits, a state seeded from the time and the PID, and is predictable. +# The source is $SRANDOM (bash >= 5.1, from the kernel's random generator), or +# else /dev/urandom read through od, 256 words at a time to limit the number +# of processes. Values above the largest multiple of $1 are rejected, so that +# "% $1" introduces no bias. +# Usage: _pwd_rand +_pwd_rand() +{ + local max="$1" limit r + limit=$(( 4294967296 - 4294967296 % max )) + while :; do + if [[ -n "${SRANDOM+x}" ]]; then + r="$SRANDOM" + else + if (( ${#_PWD_POOL[@]} == 0 )); then + # shellcheck disable=SC2207 # whitespace-separated numbers + _PWD_POOL=( $(od -An -v -N1024 -tu4 /dev/urandom 2>/dev/null) ) + if (( ${#_PWD_POOL[@]} == 0 )); then + disp E "No source of random numbers available (/dev/urandom)." + return 1 + fi + fi + r="${_PWD_POOL[-1]}" + unset '_PWD_POOL[-1]' + fi + if (( r < limit )); then + _PWD_RAND=$(( r % max )) + return 0 + fi + done +} +_PWD_POOL=() + # ------------------------------------------------------------------------------ # genpwd : generate a password with different criteria # Usage: genpwd [options] [--extracars=] [--length=] [nb_passwd] @@ -68,10 +104,12 @@ genpwd() help,nosymbols,nonumbers,noup,nolow,extracars:,length:,occurences:,occurrences: \ -n 'genpwd' -- "$@") # shellcheck disable=SC2181 # getopt return code is checked immediately after - if [[ $? -ne 0 ]]; then return 1; fi - local -a _pargs=() - readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0') - set -- "${_pargs[@]}" + if [[ $? -ne 0 ]]; then + disp E "Invalid options, use \"genpwd --help\" to display usage." + return 1 + fi + # Restore the getopt-normalised (shell-quoted) argument list + eval set -- "$PARSED" while true; do case "$1" in @@ -150,6 +188,21 @@ genpwd() fi local carset="" + # The defaults may come from the configuration file: bash evaluates a + # non-numeric string inside (( )) as an expression, so check them first. + local _v + for _v in "$symb" "$numb" "$maj" "$min"; do + if [[ $_v != 0 && $_v != 1 ]]; then + disp E "Invalid GENPWD_DEFAULT_* value '$_v': the character class switches must be 0 or 1." + return 1 + fi + done + for _v in "$length" "$occurs" "$nbpwd"; do + if ! [[ $_v =~ ^[1-9][0-9]*$ ]]; then + disp E "Invalid GENPWD_DEFAULT_* value '$_v': length, occurrences and count must be positive integers." + return 1 + fi + done local unique_carset="" local ch="" local i=0 @@ -210,7 +263,9 @@ genpwd() return 1 fi - disp I "Generating $nbpwd password(s), please wait..." + # On stderr: stdout carries the passwords only, so that "pw=$(genpwd)" or + # "genpwd | xclip" do not capture this message. + disp I "Generating $nbpwd password(s), please wait..." >&2 for (( n=1; n<=nbpwd; n++ )); do local -a password_chars=() local -A char_count=() @@ -224,7 +279,8 @@ genpwd() return 1 fi - idx=$(( RANDOM % ${#set} )) + _pwd_rand "${#set}" || return 1 + idx=$_PWD_RAND char=${set:idx:1} count=${char_count["$char"]:-0} @@ -246,7 +302,8 @@ genpwd() return 1 fi - idx=$(( RANDOM % ${#carset} )) + _pwd_rand "${#carset}" || return 1 + idx=$_PWD_RAND char=${carset:idx:1} count=${char_count["$char"]:-0} @@ -261,7 +318,8 @@ genpwd() done for (( i=${#password_chars[@]} - 1; i>0; i-- )); do - idx=$(( RANDOM % (i + 1) )) + _pwd_rand "$(( i + 1 ))" || return 1 + idx=$_PWD_RAND char=${password_chars[i]} password_chars[i]=${password_chars[idx]} password_chars[idx]=$char @@ -312,7 +370,7 @@ pwdscore() break ;; -*) - disp E "Invalid option '$1'. Use \"pwdscore --help\" to display usage." + disp E "Invalid option '$1'. Use \"pwdscore --help\" to display usage (a password starting with '-' must follow '--')." return 1 ;; *) @@ -373,17 +431,17 @@ pwdscore() score=$(( length * 2 )) fi - if [[ $password =~ [a-z] ]]; then + if [[ $password =~ [[:lower:]] ]]; then has_lower=1 pool_size=$(( pool_size + 26 )) score=$(( score + 12 )) fi - if [[ $password =~ [A-Z] ]]; then + if [[ $password =~ [[:upper:]] ]]; then has_upper=1 pool_size=$(( pool_size + 26 )) score=$(( score + 12 )) fi - if [[ $password =~ [0-9] ]]; then + if [[ $password =~ [[:digit:]] ]]; then has_digit=1 pool_size=$(( pool_size + 10 )) score=$(( score + 12 ))