I am writing a small C program that asks a user for a password, as the user enters characters via keyboard, these characters will be displayed as asterisks and when they hit enter the actual password is displayed. I have tried fiddling with getchar()
and assigning variables but I am not getting the desired solution. Any guidance is appreciated, thanks.
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <string.h>
char *get_password();
int main() {
struct termios info;
tcgetattr(0, &info);
// info.c_lflag &= ~ECHO;
tcsetattr(0, TCSANOW, &info);
printf("Create a password: ");
int c = getchar();
c = '*';
char *password = get_password();
printf("You entered: %s\n", password);
tcgetattr(0, &info);
info.c_lflag |= ECHO;
tcsetattr(0, TCSANOW, &info);
}
#define BUFSIZE 100
char buf[BUFSIZE];
char *get_password() {
int c, len = 0;
while ((c = getchar()) != EOF && c != '\n') {
buf[len ] = c;
if (len == BUFSIZE - 1)
break;
}
buf[len] = 0;
putchar('\n');
return buf;
}
CodePudding user response:
Reading a password without echo is a bit more tricky than fiddling with getchar()
:
- you must disable the buffering in
stdin
or read bytes directly from the low level system handle 0*. - you must disable the terminal echo and buffering features
- to avoid leaving the terminal in a bad state, you should also disable signal processing and handle the signal characters explicitly
- upon reading the bytes from handle 0, you must output stars and flush the output to the terminal
- you should handle backspace as users will expect to be able to correct typing errors
- you must restore the terminal state after input completes
*) Reading from system handle 2 (stderr
) is a useful alternative to get the password from the user even if stdin
is redirected from a file.
Here is a commented implementation:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
char *get_password(void) {
struct termios info, save;
static char buf[100];
size_t len = 0;
char c;
int res;
tcgetattr(0, &info);
save = info;
/* input modes: no break, no CR to NL, no parity check, no strip char,
* no start/stop output control. */
info.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
INLCR | IGNCR | ICRNL | IXON);
/* output modes - disable post processing */
info.c_oflag &= ~(OPOST);
/* local modes - echoing off, canonical off, no extended functions,
* no signal chars (^Z,^C) */
info.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
/* control modes - set 8 bit chars, disable parity handling */
info.c_cflag &= ~(CSIZE | PARENB);
info.c_cflag |= CS8;
/* control chars - set return condition: min number of bytes and timer.
* We want read to return every single byte, without timeout. */
info.c_cc[VMIN] = 1; /* 1 byte */
info.c_cc[VTIME] = 0; /* no timer */
tcsetattr(0, TCSANOW, &info);
for (;;) {
fflush(stdout);
/* read a single byte from stdin, bypassing stream handling */
res = read(0, &c, 1);
if (res != 1) {
/* special case EINTR and restart */
if (res == -1 && errno == EINTR)
continue;
/* other cases: read failure or end of file */
break;
}
if (c == '\n' || c == '\r') {
/* user hit enter */
break;
}
if (c == '\b' || c == 127) {
/* user hit the backspace or delete key */
if (len > 0) {
printf("\b \b");
len--;
}
continue;
}
if (c == 3) {
/* user hit ^C: should abort the program */
tcsetattr(0, TCSANOW, &save);
printf("^C\n");
fflush(stdout);
return NULL;
}
if (c < ' ') {
/* ignore other control characters */
continue;
}
if (len >= sizeof(buf) - 1) {
putchar(7); /* beep */
} else {
putchar('*');
buf[len ] = c;
}
}
tcsetattr(0, TCSANOW, &save);
putchar('\n');
fflush(stdout);
buf[len] = '\0';
return buf;
}
int main() {
char *password;
printf("Create a password: ");
password = get_password();
if (password) {
printf("You entered: %s\n", password);
} else {
printf("You hit ^C, program aborted\n");
return 1;
}
return 0;
}
CodePudding user response:
As @chqrlie points out, proper password processing requires that echoing be disabled and buffering as well. Here are the quick steps to do that:
#include <termios.h>
#include <sys/ioctl.h>
struct termios termstat;
tcgetattr(STDIN_FILENO,&termstat);
termstat.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO,TCSANOW,&termstat);
then after you're done with the password handling, restore the normal behavior with:
termstat.c_lflag |= (ICANON | ECHO);
tcsetattr(STDIN_FILENO,TCSANOW,&termstat);