6

We can use the command stty -echo with a bash shell script to prevent characters to be echoed to the console, e.g., for password input.

Is there a way to print a * for each input character instead, and to delete them when pressing backspace?

2
  • Note that not echoing for each character is a deliberate security choice, as it reduces the likelihood that someone looking over the shoulder or in logs can determine the exact password length; knowing that makes brute-forcing and other cracking attempts relatively easier. So it may be perilous to override that decision.
    – Miral
    Commented Jul 24 at 4:52
  • 2
    @Miral I'd argue that the security benefit of that is completely negligible, but the reduced user experience of not getting any feedback is detrimental to security. Users who are frustrated when entering a password are more likely to choose passwords that are easier to enter (i.e. shorter). Not providing any feedback that characters are typed is terrible UX, which is why no other system does that. And honestly, since those terminal APIs date back about 5 decades to original Unix, I suspect not echoing anything is simply a historical artifact of being easy to implement.
    – marcelm
    Commented Jul 24 at 7:52

3 Answers 3

5

Well, here it is in bash:

#!/bin/bash

prompt="Enter password: "
password=""

# Turn off echo
stty -echo

# Print prompt
printf "$prompt"

while IFS= read -r -s -n1 char; do
  if [[ $char == $'\0' ]]; then
    break
  elif [[ $char == $'\177' ]]; then
    if [ ${#password} -gt 0 ]; then
      password="${password%?}"
      printf "\b \b"
    fi
  else
    password+="$char"
    printf "*"
  fi
done

# Turn echo back on
stty echo
printf "\n"

# The password is now stored in the $password variable
echo "Password entered: $password"

This code will prompt the user for a password, display * for each character typed, and handle backspaces correctly. The entered password is stored in the password variable.

Some programming languages have that functionality already built-in.

2
  • 1
    Ok, this is a hack, but it works. I thought it could be something built in the shells to do this. Thanks! Commented Jul 23 at 12:22
  • 1
    I don't think there's any point in using stty -echo if you're also using -s with read, is there?
    – terdon
    Commented Jul 23 at 12:39
3

It maybe a bit overkill, but if you have GPG's pinentry programs installed, you can use those to present a nice interface to the user for password prompts, including asterisks for masking characters (enabled by default), password strength meters, password confirmation, etc. However, it uses a command-response system for communication over a pipe, instead of being simply invokable using options directly. So you send commands over pinentry's input to customise the prompt, actually prompt for the password, etc., and then parse the output (which will include status messages and such) to get the password.

Something like:

get_password() {
  # Get locale information https://unix.stackexchange.com/a/757655/70524
  eval "$(locale)"
  # Use `$TTY` if available (zsh), `$(tty)` otherwise (bash)
  TTY=${TTY:-"$(tty)"}
  {
    echo SETPROMPT "Password:"  # Set the prompt
    # Add more such customization commands here
    echo SETDESC "Enter password for $USER" # Set the description
    # Then finally ask for the password itself.
    echo GETPIN
  } |
    # pinentry assumes the frontend will be invoked in the background,
    # and needs to be explicitly told what environment it should use
    pinentry-curses --ttytype "$TERM" --ttyname "$TTY" --lc-ctype "$LC_CTYPE" --lc-messages "$LC_MESSAGES" |
    while IFS= read -r line; do
      case $line in
        OK*) continue;;
        D*) pass=${line#D }; printf "%s\n" "$pass";; # `D`ata line will have the password
        *) printf "%s\n" "$line" >&2;; # Dump everything else as possible errors 
      esac;
    done
}

And use it like so:

password=$(get_password)

This will open a full-screen dialog in the terminal:

             ┌────────────────────────────────────────────────────┐
             │ Enter password for muru                            │
             │                                                    │
             │ Password: ________________________________________ │
             │                                                    │
             │       <OK>                            <Cancel>     │
             └────────────────────────────────────────────────────┘

(It's a perfect box in my terminal, but might not appear so on the webpage.)

You can also use pinentry-gnome, pinentry-qt, pinentry-mac, etc. for making GUI prompts if the appropriate environment is present.

4
  • Interesting idea. But I tried to run your command and got an error in the line (D *). Replaced that with D*), and got this error, bot in konsole and rxvt: S ERROR curses.open_tty_for_read 83918929 ERR 83918929 No such file or directory <Pinentry> Commented Jul 24 at 1:09
  • I tested this using zsh :facepalm: where $TTY is set, unlike in bash. Try with "$(tty)" instead of "$TTY". (Using zsh is also the reason for the case error, I think).
    – muru
    Commented Jul 24 at 1:51
  • Hmm, not even plain tty is sufficient, because both stdin and stdout are redirected. We'll need to use $(tty <&2) instead, or get the TTY before the pipe. I've updated the post to set $TTY if unset using tty.
    – muru
    Commented Jul 24 at 1:59
  • Neat! It works now. Thanks! Commented Jul 24 at 2:35
2

if you're using bash, you can just use the -s option of the read which suppresses input:

$ help read | grep -- -s
      -s    do not echo input coming from a terminal

You still need to hack showing the asterisks, but you can do this using the approach described by Dennis Williamson in this SO answer:

#!/bin/bash
unset password
prompt="Enter your password: "
while IFS= read -rsp "$prompt"  -n1 char
do
  if [[ $char == $'\0' ]]
  then
    echo ""
    break
  fi
  prompt='*'
  password+="$char"
done
printf 'You entered: %s\n' "$password"

However, this doesn't support deleting ay characters as the backspace is also just echoed as one more * instead of deleting. So Max Haase's answer, which does handle this, is better.

4
  • This does not work since backspace does not delete neither the entered character, nor the echoed wildcard. Commented Jul 23 at 13:55
  • @LuisA.Florit ah, no it won't. But that's the kind of thing that should be mentioned as a requirement. Max's answer does handle this nicely though.
    – terdon
    Commented Jul 23 at 13:57
  • I explicitly mentioned it: "and to delete them when pressing backspace". Sorry if it wasn't clear enough. Commented Jul 23 at 14:07
  • 1
    Sigh. No, @LuisA.Florit, that was absolutely clear enough. It isn't your fault that I didn't read carefully. Very much my bad, sorry!
    – terdon
    Commented Jul 23 at 14:47

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .