Home > database >  Trying to turn user input into an asterisks in real-time
Trying to turn user input into an asterisks in real-time

Time:03-07

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);
  • Related