23

The problem: Programmers want users to input passwords. The getpass() function is nice for this purpose, but its use has a drawback: While entering a password nothing is printed to stdout.

The question: How can a getpass() be implemented while asterisks are printed for every character typed by a user? (Of course backspace - and ideally pos1 and end - should be taken care of accordingly.)

The motivation: There have been people in the community not understanding why this question has been asked. And then referred to getpass() with a) this way ignoring the task at hand and b) without thinking about that there reference would not answer the question. The reason why s.o. might want to have asterisks printed is for convenience of users: They get a direct visual response during password input. Therefor they do not get confused by pressing keys and - to there eyes - nothing seems to be happening.

A step towards a solution:

Let me present a first step towards a solution here. Please help in order to evolve it into a real solution.

There is a module named getch which seems to allow reading character by character from stdin. Backspace is - quite strangely - mapped to an integer value of 127, but such a solution could then look like this:

def readLineWithAsterisks():
    sBuffer = ''
    while True:
        c = getch.getch()
        if c == '\n':
            return sBuffer
        elif ord(c) == 127:
            if len(sBuffer) > 0:
                sys.stdout.write('\x08 \x08')
                sys.stdout.flush()
                sBuffer = sBuffer[0:-1]
            continue
        else:
            sys.stdout.write('*')
            sys.stdout.flush()
            sBuffer += c

But this code has some drawbacks. First I'm very much confused about c not being '\b' if s.o. entered a backspace. Maybe s.o. has an explanation for this? Second only ASCII characters are processed, at least on Linux. I don't know about Windows here, but if a character other than A-Z0-9 is pressed, the line c = getch.getch() will throw an exception. getch() does not seem to be able to process umlauts and other kinds of characters, at least to some extent.

To come to solution input the following issues should be addressed:

  • How can a read from stdin be done character by character in regard to non-ASCII characters?
  • How can this be done in a platform independent way?
  • How can this been done safely (without security issues)? (getpass somehow seems to address this but I don't fully understand how.)
Cœur
  • 37,241
  • 25
  • 195
  • 267
Regis May
  • 3,070
  • 2
  • 30
  • 51

8 Answers8

5

See the first answer there :

What's the simplest way of detecting keyboard input in python from the terminal?

Just print stars '*' or anything when a key is pressed.

All credit obviously goes to Phylliida for the research.

Community
  • 1
  • 1
Loïc
  • 11,804
  • 1
  • 31
  • 49
1

NOTE: my other answer contains working python2 code to do this in platform independent manner.

The secure platform independent way will setup everything identical to getpass.getpass() so look at the source (/usr/lib/python2.7/getpass.py for me); it is pretty straight-forward.

As for echoing the stars ...

win_getpass() is already reading char by char, just echo some * in that loop. You may need to use msvcrt.getwch() instead of msvcrt.getch() but that would mean the python getpass module has a bug.

unix_getpass() is trickier. You need to setup cbreak for the terminal similar to how ECHO is being disabled already (see https://utcc.utoronto.ca/~cks/space/blog/unix/CBreakAndRaw). You will then have to use read(1) in a loop (similar to win_getpass()) rather than readline() that _raw_input() is using.

Once you are reading byte by byte, you can go through the pains of determining what constitutes a "letter." This depends on the encoding and may even be variable in length (in the case of UTF-8).

nabin-info
  • 289
  • 1
  • 7
  • "it is pretty straight-forward" - no, not really :-) Sorry, but not everybody knows every detail about TTYs and all their peculiarities :-) -- And read(1) is not that useful: As far as I learned STDIN in python is handled line by line is by design. That's one of the fundamental problems. Nevertheless I'll have a look into the other information you gave here. Some of it seems to be really useful. Thank you very much! – Regis May Nov 21 '16 at 23:25
  • The linewise handling of STDIN is not a fundamental python problem; TTYs are traditionally line-buffered to allow for line-editing to be performed client-side. Also, read(1) is extremely useful when you need to read a single byte. – nabin-info Nov 22 '16 at 16:21
1

You may want to look at how jupyter/ipython implemented this. I'm getting a dot displayed immediately for every character typed using getpass().

enter image description here

denfromufa
  • 5,610
  • 13
  • 81
  • 138
  • @nabin-info something is wrong with your setup - I'm using jupyter 4.2.3. I updated my answer with gif animation. – denfromufa Nov 22 '16 at 16:42
  • @denfromufa oh, we are talking about input from a windows console or unix tty. Your input is actually in an HTML form through the "jupyter" web app. – nabin-info Nov 22 '16 at 18:30
1

I wrote a module to illustrate roughly how you do it platform independently.

#!/usr/bin/python2

def _masked_input_unix(prompt="Password: ", mask="*"):
    pw = ""
    # save terminal settings
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    new = termios.tcgetattr(fd)
    # setup 'cbreak' mode
    new[3] = new[3] & ~termios.ECHO
    new[3] = new[3] & ~termios.ICANON
    new[6][termios.VMIN] = '\x01'
    new[6][termios.VTIME] = '\x00'
    try:
        termios.tcsetattr(fd, termios.TCSADRAIN, new)
        print prompt,
        # Read the password
        while True:
            c = sys.stdin.read(1)
            # submit chars
            if c == '\r' or c == '\n':
                sys.stdout.write("%s" % (c))
                break
            # delete chars
            elif c == '\b' or c == '\x7f':
                if len(pw) > 0:
                    pw = pw[:-1]
                    sys.stdout.write("%s" % ('\b \b'))
            # password chars
            else:
                pw += c
                sys.stdout.write("%s" % (mask))
    finally:
        # ensure we reset the terminal
        termios.tcsetattr(fd, termios.TCSADRAIN, old)
    return pw

def _masked_input_win(prompt="Password: ", mask='*'):
    pw = ""
    while True:
        c = msvcrt.getch()
        # submit chars
        if c == '\r' or c == '\n':
            while msvcrt.kbhit():
                msvcrt.getch()
            print
            break
        elif c == '\x03':
            raise KeyboardInterrupt
        # delete chars
        elif c == '\b' or c == '\x7f':
            if len(pw) > 0:
                pw = pw[:-1]
                msvcrt.putch('\b')
                msvcrt.putch(' ')
                msvcrt.putch('\b')
        # password chars
        else:
            pw += c
            msvcrt.putch(mask)
    return pw

## initialize windows or posix function pointer
masked_input = None
try:
    import msvcrt
    masked_input = _masked_input_win
except ImportError:
    import sys, termios
    masked_input = _masked_input_unix

if __name__ == "__main__":
    p = masked_input()
    print "Password is:", p

And this works for single-byte encodings. Adding unicode support is non-trivial. I suspect unicode doesn't work well with the getpass module on Windows. (NOTE: it is not as simple as changing everything to unicode strings and using getwch())

nabin-info
  • 289
  • 1
  • 7
1

This is Linux only version, works in Python 2 and Python 3 with Unicode support.

To type Unicode characters, hold Ctrl+Shift simultaneously and type u and release Ctrl+Shift, now type the codepoint and <Enter>.

I exclusively use os.read and os.write funcitons to bypass (libc and python IO) buffering issues and read bytes from the kernel.

Terminal KILL (^U), ERASE (ascii DEL aka Backspace key), EOF (^D) and ascii BS (\b) are supported.

I ignore SIGTSTP while reading password, because on resumption from background typed characters are echoed.

import tty
import os
import sys
import signal
from array import array

# disable (^Z) SIGTSTP
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
stdin = sys.__stdin__.fileno()
stream = sys.__stderr__.fileno()
old = tty.tcgetattr(stdin)
os.write(stream, b"Passwd: ")
try:
    tty.setcbreak(stdin)
    passwd = array("u")
    while True:
        # UTF-8 is 4 octets (bytes) at max
        c = os.read(stdin, 4)
        # ERASE ascii DEL (0x7f) <Backspace> and ascii BS (0x08) <^H>
        if c in (old[tty.CC][tty.VERASE], b"\b"):
            if passwd:
                os.write(stream, b"\b \b")
                passwd.pop()
        # KILL ascii NAK (0x15) <^U>
        elif c == old[tty.CC][tty.VKILL]:
            if passwd:
                os.write(stream, b"\b \b" * len(passwd))
                passwd = array("u")
        # ascii LF (0x0a) <^J>, CR (0x0d) <^M> and <Enter> and EOT (0x04) <^D>
        elif c in (b"\n", old[tty.CC][tty.VEOF]):
            break
        else:
            #c = c.decode('utf-8')
            c = c.decode(sys.__stdin__.encoding)
            passwd.append(c)
            os.write(stream, b"*")
finally:
    # restore terminal settings
    tty.tcsetattr(stdin, tty.TCSAFLUSH, old)
    # enable (^Z) SIGTSTP 
    signal.signal(signal.SIGTSTP, signal.SIG_DFL)
    os.write(stream, b"\n")

print(passwd.tounicode())

Test;

$ # To input "Þàsswõrd"
$ # U+00de, U+00e0, s,s, w, U+00f5, r, d
$ python getpass.py
$ Passwd: ********
Þàsswõrd
Nizam Mohamed
  • 8,751
  • 24
  • 32
0

you may find this recipe as useful as a starting point for your own platform independent solution
http://code.activestate.com/recipes/134892/

In case of windows it uses msvcrt lib. You can replace a call to getch() to getwch() to be able to process unicode.

ActivePython 2.7.10.12 (ActiveState Software Inc.) based on
Python 2.7.10 (default, Aug 21 2015, 12:07:58) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import msvcrt
>>> msvcrt.getch()
'j'
>>> msvcrt.getch()
'r'
>>> msvcrt.getch()
'?'
>>> msvcrt.getwch()
u'\u0432'
Alexey Smirnov
  • 2,573
  • 14
  • 20
0

My suggestion is -- DO NOT DO IT!

Don't reinvent the wheel, use a password helper if you want 'stars'

See https://stackoverflow.com/a/67327327/701532

anthony
  • 7,696
  • 1
  • 17
  • 11
-1

It is probably easier to use tkinter, though probably not as secure, this is the closest I could get to what you are asking for.

from tkinter import *
from tkinter import ttk

root = Tk()
parent = ttk.Frame(root)
parent.grid()

password = ttk.Entry(parent, show="*").grid()#shows each character as an asterix

root.mainloop()

Sorry I couldn't help more.