[ Team LiB ] Previous Section Next Section

8.7 Prompting for a Password

8.7.1 Problem

You need to prompt an interactive user for a password.

8.7.2 Solution

On Unix systems, you can use the standard C runtime function getpass( ) if you can accept limiting passwords to _PASSWORD_LEN, which is typically defined to be 128 characters. If you want to read longer passwords, you can use the function described in the following Section 8.7.3.

On Windows, you can use the standard EDIT control with ES_PASSWORD specified as a style flag to mask the characters typed by a user.

8.7.3 Discussion

In the following subsections we'll look at several different approaches to prompting for passwords.

8.7.3.1 Prompting for a password on Unix using getpass( ) or readpassphrase( )

The standard C runtime function getpass( ) is the most portable way to obtain a password from a user interactively. Unfortunately, it does have several limitations that you may find unacceptable. The first is that only up to _PASSWORD_LEN (typically 128) characters may be entered; any characters after that are simply discarded. The second is that the password is stored in a statically defined buffer, so it is not thread-safe, but ordinarily this is not much of a problem because there is fundamentally no way to read from the terminal in a thread-safe manner anyway.

The getpass( ) function has the following signature:

#include <sys/types.h>
#include <unistd.h>
   
char *getpass(const char *prompt);

The text passed as the function's only argument is displayed on the terminal, terminal echo is disabled, and input is gathered in a buffer internal to the function until the user presses Enter. The return value from the function is a pointer to the internal buffer, which will be at most _PASSWORD_LEN + 1 bytes in size, with the additional byte left to hold the NULL terminator.

FreeBSD and OpenBSD both support an alternative function, readpassphrase( ), that provides the underlying implementation for getpass( ). It is more flexible than getpass( ), allowing the caller to preallocate a buffer to hold a password or passphrase of any size. In addition, it also supports a variety of control flags that control its behavior.

The readpassphrase( ) function has the following signature:

#include <sys/types.h>
#include <readpassphrase.h>
   
char *readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags);

This function has the following arguments:

prompt

String that will be displayed to the user before accepting input.

buf

Buffer into which the input read from the interactive user will be placed.

bufsiz

Size of the buffer (in bytes) into which input read from the interactive user is placed. Up to one less byte than the size specified may be read. Any additional input is silently discarded.

flags

Set of flags that may be logically OR'd together to control the behavior of the function.

A number of flags are defined as macros in the readpassphrase.h header file. While some of the flags are mutually exclusive, some of them may be logically combined together:

RPP_ECHO_OFF

Disables echoing of the user's input on the terminal. If neither this flag nor RPP_ECHO_ON is specified, this is the default. The two flags are mutually exclusive, but if both are specified, echoing will be enabled.

RPP_ECHO_ON

Enables echoing of the user's input on the terminal.

RPP_REQUIRE_TTY

If there is no controlling tty, and this flag is specified, readpassphrase( ) will return an error; otherwise, the prompt will be written to stderr, and input will be read from stdin. When input is read from stdin, it's often not possible to disable echoing.

RPP_FORCELOWER

Causes all input from the user to be automatically converted to lowercase. This flag is mutually exclusive with RPP_FORCEUPPER; however, if both flags are specified, RPP_FORCEUPPER will take precedence.

RPP_FORCEUPPER

Causes all input from the user to be automatically converted to uppercase.

RPP_SEVENBIT

Indicates that the high bit will be stripped from all user input.

For both getpass( ) and readpassphrase( ), a pointer to the input buffer will be returned if the function completes successfully; otherwise, a NULL pointer will be returned, and the error that occurred will be stored in the global errno variable.

Both getpass( ) and readpassphrase( ) can return an error with errno set to EINTR, which means that the input from the user was interrupted by a signal. If such a condition occurs, all input from the user up to the point when the signal was delivered will be stored in the buffer, but in the case of getpass( ), there will be no way to retrieve that data.

Once getpass( ) or readpassphrase( ) return successfully, you should perform as quickly as possible whatever operation you need to perform with the password that was obtained. Then clear the contents of the returned buffer so that the cleartext password or passphrase will not be left visible in memory to a potential attacker.

8.7.3.2 Prompting for a password on Unix without getpass( ) or readpassphrase( )

The function presented in this subsection, spc_read_password( ), requires two arguments. The first is a prompt to be displayed to the user, and the second is the FILE object that points to the input source. If the input source is specified as NULL, spc_read_password( ) will use _PATH_TTY, which is usually defined to be /dev/tty.

The function reads as much data from the input source as memory is available to hold. It allocates an internal buffer, which grows incrementally as it is filled. If the function is successful, the return value will be a pointer to this buffer; otherwise, it will be a NULL pointer.

Note that we use the unbuffered I/O API for reading data from the input source. The unbuffered read is necessary to avoid potential odd side effects in the I/O. We cannot use the stream API because there is no way to save and restore the size of the stream buffer. That is, we cannot know whether the stream was previously buffered.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <paths.h>
   
#define BUF_STEP 1024 /* Allocate this much space for the password, and if it gets
                       * this long, reallocate twice the space.
                       * Rinse, lather, repeat.
                       */
   
static unsigned char *read_password(int termfd) {
  unsigned char ch, *ret, *tmp;
  unsigned long ctr = 0;
   
  if (!(ret = (unsigned char *)malloc(BUF_STEP + 1))) return 0;
  for (;;) {
    switch (read(termfd, &ch, 1)) {
      case 1:
        if (ch != '\n') break;
        /* FALL THROUGH */
      case 0:
        ret[ctr] = 0;
        return ret;
      default:
        free(ret);
        return 0;
    }
    ret[ctr] = ch;
    if (ctr && !(ctr & BUF_STEP)) {
      if (!(tmp = (unsigned char *)realloc(ret, ctr + BUF_STEP + 1))) {
        free(ret);
        return 0;
      }
      ret = tmp;
    }
    ctr++;
  }
}
   
unsigned char *spc_read_password(unsigned char *prompt, FILE *term) {
  int            close = 0, termfd;
  sigset_t       saved_signals, set_signals;
  unsigned char  *retval;
  struct termios saved_term, set_term;
   
  if (!term) {
    if (!(term = fopen(_PATH_TTY, "r+"))) return 0;
    close = 1;
  }
   
  termfd = fileno(term);
  fprintf(term, "%s", prompt);
  fflush(term);
   
  /* Defer interruption when echo is turned off */
  sigemptyset(&set_signals);
  sigaddset(&set_signals, SIGINT);
  sigaddset(&set_signals, SIGTSTP);
  sigprocmask(SIG_BLOCK, &set_signals, &saved_signals);
   
  /*Save the current state and set the terminal to not echo */
  tcgetattr(termfd, &saved_term);
  set_term = saved_term;
  set_term.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL);
  tcsetattr(termfd, TCSAFLUSH, &set_term);
   
  retval = read_password(termfd);
  fprintf(term, "\n");
   
  tcsetattr(termfd, TCSAFLUSH, &saved_term);
  sigprocmask(SIG_SETMASK, &saved_signals, 0);
  if (close) fclose(term);
   
  return retval;
}
8.7.3.3 Prompting for a password on Windows

On Windows, prompting for a password is as simple as setting the ES_PASSWORD style flag for an EDIT control. When this flag is set, Windows will not display the characters typed by the user. Instead, the password character will be displayed for each character that is typed. By default, the password character is an asterisk (*), but you can change it by sending the control an EM_SETPASSWORDCHAR message with wParam set to the character to display.

Unfortunately, there is no way to prevent Windows from displaying something as the user types. The closest that can be achieved is to set the password character to a space, which will make it difficult for an onlooker to determine how many characters have been typed.

To safely retrieve the password stored in the EDIT control's internal buffer, the control should first be queried to determine how many characters it holds. Allocate a buffer to hold the data and query the data from the control. The control will make a copy of the data but leave the original internal buffer unchanged.

To be safe, it's a good idea to set the contents of the buffer to clear the password from internal memory used by the EDIT control. Simply setting the control's internal buffer to an empty string is not sufficient. Instead, set a string that is the length of the string retrieved, then set an empty string if you wish. For example:

#include <windows.h>

BOOL IsPasswordValid(HWND hwndPassword) {
  BOOL   bValid = FALSE;
  DWORD  dwTextLength;
  LPTSTR lpText;
   
  if (!(dwTextLength = (DWORD)SendMessage(hwndPassword, WM_GETTEXTLENGTH, 0, 0)))
    return FALSE;
  lpText = (LPTSTR)LocalAlloc(LMEM_FIXED, (dwTextLength + 1) * sizeof(TCHAR));
  if (!lpText) return FALSE;
  SendMessage(hwndPassword, WM_GETTEXT, dwTextLength + 1, (LPARAM)lpText);
   
  /* Do something to validate the password */
   
  while (dwTextLength--) *(lpText + dwTextLength) = ' ';
  SendMessage(hwndPassword, WM_SETTEXT,  0, (LPARAM)lpText);
  LocalFree(lpText);
   
  return bValid;
}

Other processes running on the same machine can access the contents of your edit control. Unfortunately, the best mitigation strategy, at this time, is to get rid of the edit control as soon as possible.

    [ Team LiB ] Previous Section Next Section