/*
 * authreadkeys.c - routines to support the reading of the key file
 */
#include <config.h>
#include <stdio.h>
#include <ctype.h>

//#include "ntpd.h"	/* Only for DPRINTF */
//#include "ntp_fp.h"
#include "ntp.h"
#include "ntp_syslog.h"
#include "ntp_stdlib.h"
#include "ntp_keyacc.h"

#ifdef OPENSSL
#include "openssl/objects.h"
#include "openssl/evp.h"
#endif	/* OPENSSL */

/* Forwards */
static char *nexttok (char **);

/*
 * nexttok - basic internal tokenizing routine
 */
static char *
nexttok(
	char	**str
	)
{
	register char *cp;
	char *starttok;

	cp = *str;

	/*
	 * Space past white space
	 */
	while (*cp == ' ' || *cp == '\t')
		cp++;

	/*
	 * Save this and space to end of token
	 */
	starttok = cp;
	while (*cp != '\0' && *cp != '\n' && *cp != ' '
	       && *cp != '\t' && *cp != '#')
		cp++;

	/*
	 * If token length is zero return an error, else set end of
	 * token to zero and return start.
	 */
	if (starttok == cp)
		return NULL;

	if (*cp == ' ' || *cp == '\t')
		*cp++ = '\0';
	else
		*cp = '\0';

	*str = cp;
	return starttok;
}


/* TALOS-CAN-0055: possibly DoS attack by setting the key file to the
 * log file. This is hard to prevent (it would need to check two files
 * to be the same on the inode level, which will not work so easily with
 * Windows or VMS) but we can avoid the self-amplification loop: We only
 * log the first 5 errors, silently ignore the next 10 errors, and give
 * up when when we have found more than 15 errors.
 *
 * This avoids the endless file iteration we will end up with otherwise,
 * and also avoids overflowing the log file.
 *
 * Nevertheless, once this happens, the keys are gone since this would
 * require a save/swap strategy that is not easy to apply due to the
 * data on global/static level.
 */

static const u_int nerr_loglimit = 5u;
static const u_int nerr_maxlimit = 15;

static void log_maybe(u_int*, const char*, ...) NTP_PRINTF(2, 3);

typedef struct keydata KeyDataT;
struct keydata {
	KeyDataT *next;		/* queue/stack link		*/
	KeyAccT  *keyacclist;	/* key access list		*/
	keyid_t   keyid;	/* stored key ID		*/
	u_short   keytype;	/* stored key type		*/
	u_short   seclen;	/* length of secret		*/
	u_char    secbuf[1];	/* begin of secret (formal only)*/
};

static void
log_maybe(
	u_int      *pnerr,
	const char *fmt  ,
	...)
{
	va_list ap;
	if ((NULL == pnerr) || (++(*pnerr) <= nerr_loglimit)) {
		va_start(ap, fmt);
		mvsyslog(LOG_ERR, fmt, ap);
		va_end(ap);
	}
}

static void
free_keydata(
	KeyDataT *node
	)
{
	KeyAccT *kap;

	if (node) {
		while (node->keyacclist) {
			kap = node->keyacclist;
			node->keyacclist = kap->next;
			free(kap);
		}

		/* purge secrets from memory before free()ing it */
		memset(node, 0, sizeof(*node) + node->seclen);
		free(node);
	}
}

/*
 * authreadkeys - (re)read keys from a file.
 */
int
authreadkeys(
	const char *file
	)
{
	FILE	*fp;
	char	*line;
	char	*token;
	keyid_t	keyno;
	int	keytype;
	char	buf[512];		/* lots of room for line */
	u_char	keystr[AUTHPWD_MAXSECLEN];
	size_t	len;
	u_int   nerr;
	KeyDataT *list = NULL;
	KeyDataT *next = NULL;

	/*
	 * Open file.  Complain and return if it can't be opened.
	 */
	fp = fopen(file, "r");
	if (fp == NULL) {
		msyslog(LOG_ERR, "authreadkeys: file '%s': %m",
		    file);
		goto onerror;
	}
	INIT_SSL();

	/*
	 * Now read lines from the file, looking for key entries. Put
	 * the data into temporary store for later propagation to avoid
	 * two-pass processing.
	 */
	nerr = 0;
	while ((line = fgets(buf, sizeof buf, fp)) != NULL) {
		if (nerr > nerr_maxlimit)
			break;
		token = nexttok(&line);
		if (token == NULL)
			continue;

		/*
		 * First is key number.  See if it is okay.
		 */
		keyno = atoi(token);
		if (keyno < 1) {
			log_maybe(&nerr,
				  "authreadkeys: cannot change key %s",
				  token);
			continue;
		}

		if (keyno > NTP_MAXKEY) {
			log_maybe(&nerr,
				  "authreadkeys: key %s > %d reserved for Autokey",
				  token, NTP_MAXKEY);
			continue;
		}

		/*
		 * Next is keytype. See if that is all right.
		 */
		token = nexttok(&line);
		if (token == NULL) {
			log_maybe(&nerr,
				  "authreadkeys: no key type for key %d",
				  keyno);
			continue;
		}

		/* We want to silently ignore keys where we do not
		 * support the requested digest type. OTOH, we want to
		 * make sure the file is well-formed.  That means we
		 * have to process the line completely and have to
		 * finally throw away the result... This is a bit more
		 * work, but it also results in better error detection.
		 */
#ifdef OPENSSL
		/*
		 * The key type is the NID used by the message digest
		 * algorithm. There are a number of inconsistencies in
		 * the OpenSSL database. We attempt to discover them
		 * here and prevent use of inconsistent data later.
		 */
		keytype = keytype_from_text(token, NULL);
		if (keytype == 0) {
			log_maybe(NULL,
				  "authreadkeys: invalid type for key %d",
				  keyno);
#  ifdef ENABLE_CMAC
		} else if (NID_cmac != keytype &&
				EVP_get_digestbynid(keytype) == NULL) {
			log_maybe(NULL,
				  "authreadkeys: no algorithm for key %d",
				  keyno);
			keytype = 0;
#  endif /* ENABLE_CMAC */
		}
#else	/* !OPENSSL follows */
		/*
		 * The key type is unused, but is required to be 'M' or
		 * 'm' for compatibility.
		 */
		if (! (toupper(*token) == 'M')) {
			log_maybe(NULL,
				  "authreadkeys: invalid type for key %d",
				  keyno);
			keytype = 0;
		} else {
			keytype = KEY_TYPE_MD5;
		}
#endif	/* !OPENSSL */

		/*
		 * Finally, get key and insert it. If it is longer than 20
		 * characters, it is a binary string encoded in hex;
		 * otherwise, it is a text string of printable ASCII
		 * characters.
		 */
		token = nexttok(&line);
		if (token == NULL) {
			log_maybe(&nerr,
				  "authreadkeys: no key for key %d", keyno);
			continue;
		}
		next = NULL;
		len = authdecodepw(keystr, sizeof(keystr), token, AUTHPWD_UNSPEC);
		if (len > sizeof(keystr)) {
			switch (errno) {
			case ENOMEM:
				log_maybe(&nerr,
					  "authreadkeys: passwd too long for key %d",
					  keyno);
				break;
			case EINVAL:
				log_maybe(&nerr,
					  "authreadkeys: passwd has bad char for key %d",
					  keyno);
				break;
			default:
				log_maybe(&nerr,
					  "authreadkeys: unknown errno %d for key %d",
					  errno, keyno);
				break;
			}
			continue;
		}
		next = emalloc(sizeof(KeyDataT) + len);
		next->keyacclist = NULL;
		next->keyid   = keyno;
		next->keytype = keytype;
		next->seclen  = len;
		memcpy(next->secbuf, keystr, len);

		token = nexttok(&line);
		if (token != NULL) {	/* A comma-separated IP access list */
			char *tp = token;

			while (tp) {
				char *i;
				char *snp;	/* subnet text pointer */
				unsigned int snbits;
				sockaddr_u addr;

				i = strchr(tp, (int)',');
				if (i) {
					*i = '\0';
				}
				snp = strchr(tp, (int)'/');
				if (snp) {
					char *sp;

					*snp++ = '\0';
					snbits = 0;
					sp = snp;

					while (*sp != '\0') {
						if (!isdigit((unsigned char)*sp))
						    break;
						if (snbits > 1000)
						    break;	/* overflow */
						snbits = 10 * snbits + (*sp++ - '0');       /* ascii dependent */
					}
					if (*sp != '\0') {
						log_maybe(&nerr,
							  "authreadkeys: Invalid character in subnet specification for <%s/%s> in key %d",
							  sp, snp, keyno);
						goto nextip;
					}
				} else {
					snbits = UINT_MAX;
				}

				if (is_ip_address(tp, AF_UNSPEC, &addr)) {
					/* Make sure that snbits is valid for addr */
				    if ((snbits < UINT_MAX) &&
					( (IS_IPV4(&addr) && snbits > 32) ||
					  (IS_IPV6(&addr) && snbits > 128))) {
						log_maybe(NULL,
							  "authreadkeys: excessive subnet mask <%s/%s> for key %d",
							  tp, snp, keyno);
				    }
				    next->keyacclist = keyacc_new_push(
					next->keyacclist, &addr, snbits);
				} else {
					log_maybe(&nerr,
						  "authreadkeys: invalid IP address <%s> for key %d",
						  tp, keyno);
				}

			nextip:
				if (i) {
					tp = i + 1;
				} else {
					tp = 0;
				}
			}
		}

		/* check if this has to be weeded out... */
		if (0 == keytype) {
			free_keydata(next);
			next = NULL;
			continue;
		}

		DEBUG_INSIST(NULL != next);
#if defined(OPENSSL) && defined(ENABLE_CMAC)
		if (NID_cmac == keytype && len < 16) {
			msyslog(LOG_WARNING, CMAC " keys are 128 bits, "
				"zero-extending key %u by %u bits",
				(u_int)keyno, 8 * (16 - (u_int)len));
		}
#endif	/* OPENSSL && ENABLE_CMAC */
		next->next = list;
		list = next;
	}
	fclose(fp);
	if (nerr > 0) {
		const char * why = "";

		if (nerr > nerr_maxlimit)
			why = " (emergency break)";
		msyslog(LOG_ERR,
			"authreadkeys: rejecting file '%s' after %u error(s)%s",
			file, nerr, why);
		goto onerror;
	}

	/* first remove old file-based keys */
	auth_delkeys();
	/* insert the new key material */
	while (NULL != (next = list)) {
		list = next->next;
		MD5auth_setkey(next->keyid, next->keytype,
			       next->secbuf, next->seclen, next->keyacclist);
		next->keyacclist = NULL; /* consumed by MD5auth_setkey */
		free_keydata(next);
	}
	return (1);

  onerror:
	/* Mop up temporary storage before bailing out. */
	while (NULL != (next = list)) {
		list = next->next;
		free_keydata(next);
	}
	return (0);
}
