Search code examples
bashinputpasswordsmasking

Bash: Masking user input for a password, with * support for backspace and special characters


I have this snippet below based on the idea to mask the input for a password by @SiegeX and @mklement0 from this question.

It's great and my only desired addition was to delete for length of entered chars only, so we're not wiping out the entire line.

I don't understand this very well, so have run into bugs.

With below, entering "12345" and backspacing, the numbers don't "backspace"; no error.

Entering "123FourFive" and backspacing, produces error: line 9: [[: 123FourFiv: value too great for base (error token is "123FourFiv")

Entering "OneTwo345" and backspacing, seems to work fine.

Entering symbols one might expect in a password and then backspacing produces error: line 9: [[: OneTwo./?: syntax error: invalid arithmetic operator (error token is "./?")

Also pressing arrow keys during input creates wild screen behaviour after backspacing...

How to improve this so we're masking user input, and only deleting what has been entered?

Or are we "reinventing the wheel"? Is there something else out there that will do what I'm trying to do already (which is mask user input to obtain a password in a bash script to put it in a variable)?

User environment is Linux Mint 19.3 with Cinnamon.

#!/bin/bash

printf "\n\tPlease enter password: "
    # mask the input for the password by @SiegeX and @mklement0 (https://stackoverflow.com/questions/4316730)
    while IFS= read -r -s -n1 char; do
        [[ -z "${char}" ]] && { printf '\n'; break; } # ENTER pressed; output \n and break.
        if [[ "${char}" == $'\x7f' ]]; then # backspace was pressed
            # @nooblag, only delete for length of entered chars?
            if [[ "${password}" -lt "${#password}" ]]; then printf '\b \b'; fi # erase one '*' to the left.
            [[ -n $password ]] && password=${password%?} # remove last char from output variable
        else
            # add typed char to output variable
            password+="${char}"
            # print '*' in its stead
            printf '*'
        fi
    done

printf "\tPassword: ${password}\n\n"

Update: askpass as suggested here nearly does what I'm after, but if the user tries to abort/kill it with Ctrl+C it messes up the terminal...


Solution

  • This might be the solution! Taken from here.

    #!/bin/bash
    #
    # Read and echo a password, echoing responsive 'stars' for input characters
    # Also handles: backspaces, deleted and ^U (kill-line) control-chars
    #
    unset PWORD
    PWORD=
    echo -n 'password: ' 1>&2
    while true; do
      IFS= read -r -N1 -s char
      # Note a NULL will return a empty string
      # Convert users key press to hexadecimal character code
      code=$(printf '%02x' "'$char") # EOL (empty char) -> 00
      case "$code" in
      ''|0a|0d) break ;;   # Exit EOF, Linefeed or Return
      08|7f)  # backspace or delete
          if [ -n "$PWORD" ]; then
            PWORD="$( echo "$PWORD" | sed 's/.$//' )"
            echo -n $'\b \b' 1>&2
          fi
          ;;
      15) # ^U or kill line
          echo -n "$PWORD" | sed 's/./\cH \cH/g' >&2
          PWORD=''
          ;;
      [01]?) ;;                        # Ignore ALL other control characters
      *)  PWORD="$PWORD$char"
          echo -n '*' 1>&2
          ;;
      esac
    done
    echo
    echo $PWORD