Masking Password with *'s

So I've been working on this for some time now and can't seem to find the solution that works for me. I'm working in C/Unix. Basically, I want to take a user input and output something different. For example, I want to take a password and output *'s. In another instance, I want to take inputed numbers and output colored ones. I figured the colored numbers is the same as the password one, just using if statements.

Here's a code that I found online that seems to almost do the job, except I need it to output *'s as the user is typing something. Instead, it just stays blank.

#include <stdio.h>
#include <termios.h>
#include <unistd.h>

struct termios save;

void echoOff()
{
  struct termios term;

  tcgetattr( 0, &save );
  term = save;
  term.c_lflag &= (~ECHO);
  tcsetattr( 0, TCSANOW, &term );
}

void echoOn()
{
  tcsetattr( 0, TCSANOW, &save );
}

int main()
{
  char password[30];

  echoOff();
  fgets( password, 30, stdin );
  echoOn();

  printf("Hello dude, your Password was: %s\n", password );

  return 0;
}

Can anyone help me with this? Thanks!

// this one line
fgets( password, 30, stdin );
// could be like this for example:
int ch=0;
while( (ch=fgetc(stdin)) !=EOF)
{
    printf("%c", (ch=='\n') ? ch : '*');
    if(ch=='\n') break;
}
// .... rest of code

Okay I tried that, but when I type the password it still stays blank, only showing the stars after I press enter instead of doing it after each keystroke. Also, it's not even passing the password into the password array. For example if I type in 12345, it stays blank, then I press enter and it shows the five *'s, then it says "Hello dude, your Password was: " without the 12345 after the colon. Thanks for the help by the way!

If you want to read raw keystrokes you'll have to mess with your terminal and use read() instead of stdio.

1 Like

my bad - this works, my code had several logic errors - thanks corona.

#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <string.h>

struct termios save;

void echoOff()
{
  struct termios term;

  tcgetattr( 0, &save );
  term = save;
  term.c_lflag &= ~(ICANON | ECHO);;
  term.c_cc[VMIN]=0;
  term.c_cc[VTIME]=0;
  
  tcsetattr( 0, TCSANOW, &term );
}

void echoOn()
{
  tcsetattr( 0, TCSANOW, &save );
}

int kbd_rd(const int fd)  /* wait 2 seconds to read keyboard stroke */
{
    fd_set set_read;
    int maxfd = fd + 1;
    int ch=0;
    char buf[4]={0x0};
    struct timeval tv_tmout={0,0};
    tv_tmout.tv_sec=99;
    FD_ZERO(&set_read);
    FD_SET(fd, &set_read);
    select(maxfd, &set_read, NULL, NULL, &tv_tmout);
    if (FD_ISSET(fd, &set_read))
        read(fd, buf, 1);
    ch=buf[0];

    return ch;
}


int main()
{
  char password[30]={0x0};
  char *p=password;
  int ch=0;
  int fd=fileno(stdin);
  
  echoOff();
  while( (ch=kbd_rd(fd)) )
  {
      printf("%c", (ch=='\n') ? ch : '*');
      fflush(stdout);
      if(ch=='\n') break;
      *p=ch;
      p++;
  }
  
  echoOn();

  printf("Hello dude, your Password was: %s\n", password );

  return 0;
}

There is no error checking, plus tcsetattr() will return success even if it does not set all the attributes correctly. See 'Advanced Programming in the UNIX Environment' - Stevens & Rago - section on terminal I/O/

I'm actually getting the same results as the first code you posted. I was wondering, would getc() and putc() work? I thought getc() didn't echo and I could basically do a loop using getc(stdin) and putc('*', stdout). I could never get it to work though...

I tested the code I wrote on Solaris 10 and ubuntu. What did you do exactly? Please post your code.

And no, fgetc is not any better. Corona wqs right to start with - you need low-level I/O. I was asleep at the switch.

It's nothing to do with what function you use because your terminal device is configured to echo and line-buffer. The echoing and buffering isn't happening anywhere in your code -- it happens in the operating system itself. You have to tell it not to do that.

But stdio isn't prepared to read from a raw terminal, since they act rather strange. So, once you change the terminal settings, you need to read from it with a low-level read().

@jim: I used your exact code to test it before I implemented it and I couldn't get it to work.

@corona: So if it has to do with my terminal settings what setting do I need to change?

I haven't tried your code, but this line bothers me:

term.c_cc[VMIN]=0;

Isn't that supposed to be 1 for what you're looking to do? Maybe your OS handles the 0 case differently from those systems that jim_mcnamara tested it with.

There is also no need for select().

Well, echo, of course, that must be turned off. And the minimum read size has to be set to zero. And to put it out of canon mode, which line buffers and such, into the raw mode which does what you want.

Or, in a nutshell: All the settings Jim changes.

Strictly speaking, there's not. But since the terminal doesn't block in raw mode like this, it's way better than a CPU-sucking infinite read() loop. Other alternatives, like poll(), aren't portable.

I can't perfectly explain why, but 0 seems to work better than 1. with 1 it seems to block mysteriously.

So you're saying 1 is the correct setting, right? :slight_smile: We do want to block, reading one character at the time, and that's just normal behavior for noncanonical terminals unless you're using O_NONBLOCK.

The select() kludge is unneeded if you set c_cc[VMIN] properly.

Nowhere in the original question does the OP mention any wishes for timeouts of any sort.

The terminal I'm using is pretty simple. I took a course in C/Unix last quarter and I'm using the terminal they had us use, which I'm sure is probably very basic since the class is aimed at engineers rather than computer science/computer programming majors. It's called SSH Secure Shell. Do you have any recommendations on maybe a more advanced terminal? Free:)?

Having to change the terminal settings to get the behavior you want has nothing to do with your terminal client. Any terminal login you make, be it ssh, telnet, a hardwired local console, or even a serial port plugged into the machine, will behave much the same way as far as your program is concerned. That's the entire point of having the kernel manage them: It handles the raw I/O itself, and emulates any other low-level behavior necessary to make them communicate the same way as far as a program can see.

By default you get blocking mode with echo for the convenience of your program, since it's way more predictable, not to mention efficient when doing bulk data transfers. If you want raw mode, you change the kernel's terminal settings to get it.

---------- Post updated at 09:50 AM ---------- Previous update was at 09:35 AM ----------

No. By block mysteriously, I mean, block on more than one character even when vmin=1. I wasn't able to get "proper" behavior as you describe it, and don't want to give someone "compliant" yet broken code.

You're only assuming that. The ability to detect when keystrokes are not happening is useful, why deny it to them?

Even assuming you're right, we want it to block at one character, no more. I couldn't get a terminal to honor that. It may not be portable either.

select() on the other hand can be trusted. It's also flexible enough to give us both options -- blocking, and timeouts, without changing the terminal settings further.

select() is not a kludge. It's a very efficient way to use a file descriptor. It also means getting the behavior you want without having to fight the the hugely complex and baroque terminal option structure.

But it's handy to give them that option.

The proper behavior is to wait for one character. I have used this construct (~ICANON|ECHO, VMIN=1, VTIME=0) on tons of systems (Linux, *BSD, OSX, AIX, Tru64 and Digital UNIX, IRIX, HP-UX, UnixWare) as well as Cygwin and it always worked as expected. It is standard, documented and it works.

Did you try the exact code Jim posted with the only change being VMIN=1?

Blocking "mysteriously" in noncanonical mode after tcsetattr() usually happens if you DON'T set VMIN, in which case some systems such as IRIX and (I think) Solaris default to VMIN=4, whereas Linux and BSD default to 1. But then they only block until you entered 4 chars.

I labeled the select() solution a kludge because it seemed an unnecessarily contrived solution if you're setting VMIN to 1, since the read operation will block until 1 char has been read and no timeout was indended.

Okay so now I'm almost thinking that the reason your code didn't work when I tried it was because I think I might have compiled the wrong file. Because as i try to compile it now I'm getting errors and I didn't even change anything. It's giving me this error message:

test2.c: In function �kbd_rd�:
test2.c:28: error: �fd_set� undeclared (first use in this function)
test2.c:28: error: (Each undeclared identifier is reported only once
test2.c:28: error: for each function it appears in.)
test2.c:28: error: expected �;� before �set_read�
test2.c:29: warning: ISO C90 forbids mixed declarations and code
test2.c:32: error: variable �tv_tmout� has initializer but incomplete type
test2.c:32: warning: excess elements in struct initializer
test2.c:32: warning: (near initialization for �tv_tmout�)
test2.c:32: warning: excess elements in struct initializer
test2.c:32: warning: (near initialization for �tv_tmout�)
test2.c:32: error: storage size of �tv_tmout� isn�t known
test2.c:34: warning: implicit declaration of function �FD_ZERO�
test2.c:34: error: �set_read� undeclared (first use in this function)
test2.c:35: warning: implicit declaration of function �FD_SET�
test2.c:36: warning: implicit declaration of function �select�
test2.c:37: warning: implicit declaration of function �FD_ISSET�
test2.c:32: warning: unused variable �tv_tmout�
test2.c: In function �main�:
test2.c:50: warning: implicit declaration of function �fileno�

Some of these confuse me because it seems that it is saying that certain things are undeclared right where they are being declared, if that makes sense. Thanks for the help by the way guys, I really appreciate it. It seems like a lot of this is over my head as of right now. I'll need more than one quarter of class to fully understand what's going on.

The code is missing one or more includes.

If you're on a newish system, try adding "#include <sys/select.h>". Otherwise, try "#include <sys/types.h>" and "#include <unistd.h>".

Jim's code looks generally acceptable, except that it does not do any error checking, and I'd rather set c_cc[VMIN] = 1 and not use select().

If Jim's code doesn't work for you, try the following much simplified version. Yes, you heard right, I'd even go as far as using stdio on a noncanonical terminal device. I have no indications that it is prohibited by UNIX to do this, nor have I encountered any real world systems which do not allow it. (What did break stdio for me was nonblocking file descriptors on HP-UX, although I do not remember details)

#include <termios.h>
#include <stdio.h>

static struct termios termold;

void
echo_off(void) {
        struct termios t;
        (void) tcgetattr(0, &termold);
        t = termold;
        t.c_lflag &= ~(ICANON | ECHO);
        t.c_cc[VMIN] = 1;
        t.c_cc[VTIME] = 0;
        (void) tcsetattr(0, TCSANOW, &t);
}

void
echo_on(void) {
        (void) tcsetattr(0, TCSANOW, &termold);
}

void
read_unbuffered(void) {
        int ch;
        setbuf(stdin, NULL);
        echo_off();
        while ((ch = getchar()) != '\n') {
             putchar('*');
        }
        echo_on();
}

int
main() {
        read_unbuffered();
}

Okay so I tried including the headers you said and the first one worked, except it had issues with the reference to "fileno". However, I tried your simplified code and it seems to work... kinda. I could be wrong but is this code reading the password into an array at all? Or is it just covering it up with *'s?

Right, it just shows how to read the data. You'd have to store and evaluate it yourself. You will probably also want to handle backspace (\b) and print "\b \b" to erase that last character from the display.

Oh okay so print("\b \b") will print a backspace? Awesome. Also, what is delete as opposed to backspace? Like backspace is \b. What is delete?

I think that basically is what I needed then. One last thing, so if I wanted to print a colored number then would I just put that where * is in the code? Because I tried that and it said something like it was too big or something. Should I use printf() instead of putchar() in that case? I can look up the code and error message exactly if that doesn't make sense. Sorry, this is off of my iphone right now.