Commit message: util: make sure to close the authfile before returning success

fixes #136
CVE: CVE-2018-9275
CWE: CWE-200
Vulnerability description: In check_user_token in util.c in the Yubico PAM module (aka pam_yubico) 2.18 through 2.25, successful logins can leak file descriptors to the auth mapping file, which can lead to information disclosure (serial number of a device) and/or DoS (reaching the maximum number of file descriptors).

Project: yubico-pam

int challenge_response(YK_KEY *yk, int slot,
		       char *challenge, unsigned int len,
		       bool hmac, bool may_block, bool verbose,
		       char *response, unsigned int res_size, unsigned int *res_len)
{
	int yk_cmd;

  if(hmac == true) {
    *res_len = 20;
  } else {
    *res_len = 16;
  }
	if (res_size < *res_len) {
	  return 0;
  }

	memset(response, 0, res_size);

	if (verbose) {
		fprintf(stderr, "Sending %u bytes %s challenge to slot %i\n", len, (hmac == true)?"HMAC":"Yubico", slot);
	}

	switch(slot) {
	case 1:
		yk_cmd = (hmac == true) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_OTP1;
		break;
	case 2:
		yk_cmd = (hmac == true) ? SLOT_CHAL_HMAC2 : SLOT_CHAL_OTP2;
		break;
	default:
		return 0;
	}

  if(! yk_challenge_response(yk, yk_cmd, may_block, len,
        (unsigned char*)challenge, res_size, (unsigned char*)response)) {
    return 0;
  }


	return 1;
}

Project: yubico-pam
File: util.c

check_firmware_version(YK_KEY *yk, bool verbose, bool quiet, FILE *debug_file)
{
	YK_STATUS *st = ykds_alloc();

	if (!yk_get_status(yk, st)) {
		free(st);
		return 0;
	}

	if (verbose) {
		D(debug_file, "YubiKey Firmware version: %d.%d.%d\n",
		       ykds_version_major(st),
		       ykds_version_minor(st),
		       ykds_version_build(st));
	}

	if (ykds_version_major(st) < 2 ||
	    (ykds_version_major(st) == 2
         && ykds_version_minor(st) < 2)) {
		if (! quiet)
			fprintf(stderr, "Challenge-response not supported before YubiKey 2.2.\n");
		free(st);
		return 0;
	}

	free(st);
	return 1;
}

Project: yubico-pam
File: util.c

check_user_challenge_file(const char *chalresp_path, const struct passwd *user, FILE *debug_file)
{
  /*
   * This function will look for users challenge files.
   *
   * Returns one of AUTH_FOUND, AUTH_NOT_FOUND, AUTH_ERROR
   */
  size_t len;
  int r;
  int ret = AUTH_NOT_FOUND;
  char *userfile = NULL;
  char *userfile_pattern = NULL;
  glob_t userfile_glob;
  const char *filename = NULL;

  if (! chalresp_path) {
    filename = "challenge";
  } else {
    filename = user->pw_name;
  }

  /* check for userfile challenge files */
  r = get_user_cfgfile_path(chalresp_path, filename, user, &userfile);
  if (!r) {
    D (debug_file, "Failed to get user cfgfile path");
    ret = AUTH_ERROR;
    goto out;
  }

  if (!access(userfile, F_OK)) {
    ret = AUTH_FOUND;
    goto out;
  }

  /* check for userfile-* challenge files */
  len = strlen(userfile) + 2 + 1;
  if ((userfile_pattern = malloc(len)) == NULL) {
    D (debug_file, "Failed to allocate memory for userfile pattern: %s", strerror(errno));
    ret = AUTH_ERROR;
    goto out;
  }
  snprintf(userfile_pattern, len, "%s-*", userfile);

  r = glob(userfile_pattern, 0, NULL, &userfile_glob);
  globfree(&userfile_glob);
  switch (r) {
    case GLOB_NOMATCH:
      /* No matches found, so continue */
      break;
    case 0:
      ret = AUTH_FOUND;
      goto out;
    default:
      D (debug_file, "Error while checking for %s challenge files: %s", userfile_pattern, strerror(errno));
      ret = AUTH_ERROR;
      goto out;
  }

out:
  free(userfile_pattern);
  free(userfile);
  return ret;
}

Project: yubico-pam
File: util.c

char *filter_printf(const char *filter, const char *user) {
  char *result = malloc(filter_result_len(filter, user, NULL));
  filter_result_len(filter, user, result);
  return result;
}

Project: yubico-pam
File: util.c

size_t filter_result_len(const char *filter, const char *user, char *output) {
  const char *part = NULL;
  size_t result = 0;
  do
    {
      size_t len;
      part = strstr(filter, "%u");
      if(part)
        len = part - filter;
      else
        len = strlen(filter);
      if (output)
        {
          strncpy(output, filter, len);
          output += len;
        }
      result += len;
      filter += len + 2;
      if(part)
        {
          if(output)
            {
              strncpy(output, user, strlen(user));
              output += strlen(user);
            }
          result += strlen(user);
        }
    }
  while(part);

  if(output)
    *output = '\0';
  return(result + 1);
}

Project: yubico-pam
File: util.c

int generate_random(void *buf, int len)
{
	FILE *u;
	int res;

	u = fopen("/dev/urandom", "r");
	if (!u) {
		return -1;
	}

	res = fread(buf, 1, (size_t) len, u);
	fclose(u);

	return (res != len);
}

Project: yubico-pam
File: util.c

get_user_cfgfile_path(const char *common_path, const char *filename, const struct passwd *user, char **fn)
{
  /* Getting file from user home directory, e.g. ~/.yubico/challenge, or
   * from a system wide directory.
   *
   * Format is hex(challenge):hex(response):slot num
   */
  char *userfile;
  size_t len;

  if (common_path != NULL) {
    len = strlen(common_path) + 1 + strlen(filename) + 1;
    if ((userfile = malloc(len)) == NULL) {
      return 0;
    }
    snprintf(userfile, len, "%s/%s", common_path, filename);
    *fn = userfile;
    return 1;
  }

  /* No common path provided. Construct path to user's ~/.yubico/filename */

  len = strlen(user->pw_dir) + 9 + strlen(filename) + 1;
  if ((userfile = malloc(len)) == NULL) {
    return 0;
  }
  snprintf(userfile, len, "%s/.yubico/%s", user->pw_dir, filename);
  *fn = userfile;
  return 1;
}

Project: yubico-pam
File: util.c

get_user_challenge_file(YK_KEY *yk, const char *chalresp_path, const struct passwd *user, char **fn, FILE *debug_file)
{
  /* Getting file from user home directory, i.e. ~/.yubico/challenge, or
   * from a system wide directory.
   */

  /* The challenge to use is located in a file in the user's home directory,
   * which therefor can't be encrypted. If an encrypted home directory is used,
   * the option chalresp_path can be used to point to a system-wide directory.
   */

  const char *filename = NULL; /* not including directory */
  char *ptr = NULL;
  unsigned int serial = 0;
  int ret;

  if (! yk_get_serial(yk, 0, 0, &serial)) {
    D (debug_file, "Failed to read serial number (serial-api-visible disabled?).");
    if (! chalresp_path)
      filename = "challenge";
    else
      filename = user->pw_name;
  } else {
    /* We have serial number */
    /* 0xffffffff == 4294967295 == 10 digits */
    size_t len = strlen(chalresp_path == NULL ? "challenge" : user->pw_name) + 1 + 10 + 1;
    if ((ptr = malloc(len)) != NULL) {
      int res = snprintf(ptr, len, "%s-%u", chalresp_path == NULL ? "challenge" : user->pw_name, serial);
      filename = ptr;
      if (res < 0 || (unsigned long)res > len) {
	/* Not enough space, strangely enough. */
	free(ptr);
	filename = NULL;
      }
    }
  }

  if (filename == NULL)
    return 0;

  ret = get_user_cfgfile_path (chalresp_path, filename, user, fn);
  if(ptr) {
    free(ptr);
  }
  return ret;
}

Project: yubico-pam
File: util.c

init_yubikey(YK_KEY **yk)
{
	if (!yk_init())
		return 0;

	if (!(*yk = yk_open_first_key()))
		return 0;

	return 1;
}

Project: yubico-pam
File: util.c

load_chalresp_state(FILE *f, CR_STATE *state, bool verbose, FILE *debug_file)
{
  /*
   * Load the current challenge and expected response information from a file handle.
   *
   * Format is hex(challenge):hex(response):slot num
   */
  char challenge_hex[CR_CHALLENGE_SIZE * 2 + 1], response_hex[CR_RESPONSE_SIZE * 2 + 1];
  char salt_hex[CR_SALT_SIZE * 2 + 1];
  unsigned int iterations;
  int slot;
  int r;

  if (! f)
    goto out;

  /* XXX not ideal with hard coded lengths in this scan string.
   * 126 corresponds to twice the size of CR_CHALLENGE_SIZE,
   * 40 is twice the size of CR_RESPONSE_SIZE
   * (twice because we hex encode the challenge and response)
   */
  r = fscanf(f, "v2:%126[0-9a-z]:%40[0-9a-z]:%64[0-9a-z]:%d:%d", challenge_hex, response_hex, salt_hex, &iterations, &slot);
  if(r == 5) {
    if (! yubikey_hex_p(salt_hex)) {
      D(debug_file, "Invalid salt hex input : %s", salt_hex);
      goto out;
    }

    if(verbose) {
      D(debug_file, "Challenge: %s, hashed response: %s, salt: %s, iterations: %d, slot: %d",
            challenge_hex, response_hex, salt_hex, iterations, slot);
    }

    yubikey_hex_decode(state->salt, salt_hex, sizeof(state->salt));
    state->salt_len = strlen(salt_hex) / 2;
  } else {
    rewind(f);
    r = fscanf(f, "v1:%126[0-9a-z]:%40[0-9a-z]:%d", challenge_hex, response_hex, &slot);
    if (r != 3) {
      D(debug_file, "Could not parse contents of chalresp_state file (%i)", r);
      goto out;
    }

    if (verbose) {
      D(debug_file, "Challenge: %s, expected response: %s, slot: %d", challenge_hex, response_hex, slot);
    }

    iterations = CR_DEFAULT_ITERATIONS;
  }

  state->iterations = iterations;


  if (! yubikey_hex_p(challenge_hex)) {
    D(debug_file, "Invalid challenge hex input : %s", challenge_hex);
    goto out;
  }

  if (! yubikey_hex_p(response_hex)) {
    D(debug_file, "Invalid expected response hex input : %s", response_hex);
    goto out;
  }

  if (slot != 1 && slot != 2) {
    D(debug_file, "Invalid slot input : %i", slot);
    goto out;
  }

  yubikey_hex_decode(state->challenge, challenge_hex, sizeof(state->challenge));
  state->challenge_len = strlen(challenge_hex) / 2;

  yubikey_hex_decode(state->response, response_hex, sizeof(state->response));
  state->response_len = strlen(response_hex) / 2;

  state->slot = slot;

  return 1;

 out:
  return 0;
}

Project: yubico-pam
File: util.c

write_chalresp_state(FILE *f, CR_STATE *state)
{
  char challenge_hex[CR_CHALLENGE_SIZE * 2 + 1], response_hex[CR_RESPONSE_SIZE * 2 + 1];
  char salt_hex[CR_SALT_SIZE * 2 + 1], hashed_hex[CR_RESPONSE_SIZE * 2 + 1];
  unsigned char salt[CR_SALT_SIZE], hash[CR_RESPONSE_SIZE];
  YK_PRF_METHOD prf_method = {20, yk_hmac_sha1};
  unsigned int iterations = CR_DEFAULT_ITERATIONS;
  int fd;

  memset(challenge_hex, 0, sizeof(challenge_hex));
  memset(response_hex, 0, sizeof(response_hex));
  memset(salt_hex, 0, sizeof(salt_hex));
  memset(hashed_hex, 0, sizeof(hashed_hex));

  yubikey_hex_encode(challenge_hex, (char *)state->challenge, state->challenge_len);
  yubikey_hex_encode(response_hex, (char *)state->response, state->response_len);

  if(state->iterations > 0) {
    iterations = state->iterations;
  }

  generate_random(salt, CR_SALT_SIZE);
  yk_pbkdf2(response_hex, salt, CR_SALT_SIZE, iterations,
      hash, CR_RESPONSE_SIZE, &prf_method);

  yubikey_hex_encode(hashed_hex, (char *)hash, CR_RESPONSE_SIZE);
  yubikey_hex_encode(salt_hex, (char *)salt, CR_SALT_SIZE);

  rewind(f);

  fd = fileno(f);
  if (fd == -1)
    goto out;

  if (ftruncate(fd, 0))
    goto out;

  fprintf(f, "v2:%s:%s:%s:%u:%d\n", challenge_hex, hashed_hex, salt_hex, iterations, state->slot);

  if (fflush(f) < 0)
    goto out;

  if (fsync(fd) < 0)
    goto out;

  return 1;
 out:
  return 0;
}