/*

hostfile.c

Author: Tatu Ylonen <ylo@cs.hut.fi>

Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
                   All rights reserved

Created: Thu Jun 29 07:10:56 1995 ylo

Functions for manipulating the known hosts files.

*/

/*
 * $Id: hostfile.c,v 1.3 1998/07/08 00:43:25 kivinen Exp $
 * $Log: hostfile.c,v $
 * Revision 1.3  1998/07/08 00:43:25  kivinen
 * 	Added ip number to mach_hostname. Changed it to use match_host
 * 	instead of match_pattern.
 *
 * Revision 1.2  1997/03/19  21:13:51  kivinen
 * 	Enlarged line buffer in check_host_in_hostfile to 16384.
 *
 * Revision 1.1.1.1  1996/02/18 21:38:12  ylo
 * 	Imported ssh-1.2.13.
 *
 * Revision 1.2  1995/07/13  01:24:36  ylo
 * 	Removed "Last modified" header.
 * 	Added cvs log.
 *
 * $Endlog$
 */

#include "includes.h"
#include "packet.h"
#include "ssh.h"
#include "userfile.h"
#include "xmalloc.h"

/* Reads a multiple-precision integer in hex from the buffer, and advances the
   pointer.  The integer must already be initialized.  This function is
   permitted to modify the buffer.  This leaves *cpp to point just beyond
   the last processed (and maybe modified) character.  Note that this may
   modify the buffer containing the number. */

int auth_rsa_read_mp_int(char **cpp, MP_INT *value)
{
  char *cp = *cpp;
  int len, old;

  /* Skip any leading whitespace. */
  for (; *cp == ' ' || *cp == '\t'; cp++)
    ;

  /* Check that it begins with a hex digit. */
  if (*cp < '0' || *cp > '9')
    return 0;

  /* Save starting position. */
  *cpp = cp;

  /* Move forward until all hex digits skipped. */
  for (; *cp >= '0' && *cp <= '9'; cp++)
    ;

  /* Compute the length of the hex number. */
  len = cp - *cpp;

  /* Save the old terminating character, and replace it by \0. */
  old = *cp;
  *cp = 0;

  /* Parse the number. */
  if (mpz_set_str(value, *cpp, 10) != 0)
    return 0;

  /* Restore old terminating character. */
  *cp = old;

  /* Move beyond the number and return success. */
  *cpp = cp;
  return 1;
}

/* Parses an RSA key (number of bits, e, n) from a string.  Moves the pointer
   over the key.  Skips any whitespace at the beginning and at end. */

int auth_rsa_read_key(char **cpp, unsigned int *bitsp, MP_INT *e, MP_INT *n)
{
  unsigned int bits;
  char *cp;

  /* Skip leading whitespace. */
  for (cp = *cpp; *cp == ' ' || *cp == '\t'; cp++)
    ;

  /* Get number of bits. */
  if (*cp < '0' || *cp > '9')
    return 0; /* Bad bit count... */
  for (bits = 0; *cp >= '0' && *cp <= '9'; cp++)
    bits = 10 * bits + *cp - '0';

  /* Get public exponent. */
  if (!auth_rsa_read_mp_int(&cp, e))
    return 0;

  /* Get public modulus. */
  if (!auth_rsa_read_mp_int(&cp, n))
    return 0;

  /* Skip trailing whitespace. */
  for (; *cp == ' ' || *cp == '\t'; cp++)
    ;
  
  /* Return results. */
  *cpp = cp;
  *bitsp = bits;
  return 1;
}

/* Tries to match the host name (which must be in all lowercase) against the
   comma-separated sequence of subpatterns (each possibly preceded by ! to 
   indicate negation).  Returns true if there is a positive match; zero
   otherwise. */

int match_hostname(const char *host, const char *ip,
		   const char *pattern, unsigned int len)
{
  char sub[1024];
  int negated;
  int got_positive;
  unsigned int i, subi;

  got_positive = 0;
  for (i = 0; i < len;)
    {
      /* Check if the subpattern is negated. */
      if (pattern[i] == '!')
	{
	  negated = 1;
	  i++;
	}
      else
	negated = 0;
      
      /* Extract the subpattern up to a comma or end.  Convert the subpattern
         to lowercase. */
      for (subi = 0; 
	   i < len && subi < sizeof(sub) - 1 && pattern[i] != ',';
	   subi++, i++)
	sub[subi] = isupper(pattern[i]) ? tolower(pattern[i]) : pattern[i];
      /* If subpattern too long, return failure (no match). */
      if (subi >= sizeof(sub) - 1)
	return 0;

      /* If the subpattern was terminated by a comma, skip the comma. */
      if (i < len && pattern[i] == ',')
	i++;
      
      /* Null-terminate the subpattern. */
      sub[subi] = '\0';

      /* Try to match the subpattern against the host name. */
      if (match_host(host, ip, sub))
	if (negated)
	  return 0;  /* Fail if host matches any negated subpattern. */
        else
	  got_positive = 1;
    }

  /* Return success if got a positive match.  If there was a negative match,
     we have already returned zero and never get here. */
  return got_positive;
}

/* Checks whether the given host (which must be in all lowercase) is 
   already in the list of our known hosts.
   Returns HOST_OK if the host is known and has the specified key,
   HOST_NEW if the host is not known, and HOST_CHANGED if the host is known
   but used to have a different host key. */

HostStatus check_host_in_hostfile(uid_t uid,
				  const char *filename, 
				  const char *host, unsigned int bits,
				  MP_INT *e, MP_INT *n)
{
  UserFile uf;
  char line[16384];
  MP_INT ke, kn;
  unsigned int kbits, hostlen;
  char *cp, *cp2;
  HostStatus end_return;
  struct stat st;

  /* Open the file containing the list of known hosts. */
  uf = userfile_open(uid, filename, O_RDONLY, 0);
  if (uf == NULL)
    {
      if (userfile_stat(uid, filename, &st) >= 0)
	{
	  packet_send_debug("Could not open %.900s for reading.", filename);
	  packet_send_debug("If your home directory is on an NFS volume, it may need to be world-readable.");
	}
      return HOST_NEW;
    }

  /* Initialize mp-int variables. */
  mpz_init(&ke);
  mpz_init(&kn);
  
  /* Cache the length of the host name. */
  hostlen = strlen(host);
  
  /* Return value when the loop terminates.  This is set to HOST_CHANGED if
     we have seen a different key for the host and have not found the proper
     one. */
  end_return = HOST_NEW;

  /* Go trough the file. */
  while (userfile_gets(line, sizeof(line), uf))
    {
      cp = line;

      /* Skip any leading whitespace. */
      for (; *cp == ' ' || *cp == '\t'; cp++)
	;

      /* Ignore comment lines and empty lines. */
      if (!*cp || *cp == '#' || *cp == '\n')
	continue;
      
      /* Find the end of the host name portion. */
      for (cp2 = cp; *cp2 && *cp2 != ' ' && *cp2 != '\t'; cp2++)
	;

      /* Check if the host name matches. */
      if (!match_hostname(host, NULL, cp, (unsigned int)(cp2 - cp)))
	continue;
      
      /* Got a match.  Skip host name. */
      cp = cp2;
      
      /* Extract the key from the line.  This will skip any leading 
	 whitespace.  Ignore badly formatted lines. */
      if (!auth_rsa_read_key(&cp, &kbits, &ke, &kn))
	continue;

      /* Check if the current key is the same as the previous one. */
      if (kbits == bits && mpz_cmp(&ke, e) == 0 && mpz_cmp(&kn, n) == 0)
	{
	  /* Ok, they match. */
	  mpz_clear(&ke);
	  mpz_clear(&kn);
	  userfile_close(uf);
	  return HOST_OK;
	}
      
      /* They do not match.  We will continue to go through the file; however,
	 we note that we will not return that it is new. */
      end_return = HOST_CHANGED;
    }
  /* Clear variables and close the file. */
  mpz_clear(&ke);
  mpz_clear(&kn);
  userfile_close(uf);

  /* Return either HOST_NEW or HOST_CHANGED, depending on whether we saw a
     different key for the host. */
  return end_return;
}

/* Appends an entry to the host file.  Returns false if the entry
   could not be appended.  All I/O will be done with the give uid using
   userfile. */

int add_host_to_hostfile(uid_t uid, const char *filename, const char *host,
			 unsigned int bits, MP_INT *e, MP_INT *n)
{
  UserFile uf;
  char buf[1000], *cp;
 
  /* Open the file for appending. */
  uf = userfile_open(uid, filename, O_WRONLY|O_APPEND|O_CREAT, 0600);
  if (uf == NULL)
    return 0;

  /* Print the host name and key to the file. */
  sprintf(buf, "%.500s %u ", host, bits);
  userfile_write(uf, buf, strlen(buf));
  
  cp = xmalloc(mpz_sizeinbase(e, 10) + 2);
  mpz_get_str(cp, 10, e);
  userfile_write(uf, cp, strlen(cp));
  xfree(cp);

  userfile_write(uf, " ", 1);

  cp = xmalloc(mpz_sizeinbase(n, 10) + 2);
  mpz_get_str(cp, 10, n);
  userfile_write(uf, cp, strlen(cp));
  xfree(cp);

  userfile_write(uf, "\n", 1);

  /* Close the file. */
  userfile_close(uf);
  return 1;
}
