/*
 *  chap.c - support for (mutual) CHAP authentication.
 *
 *  Copyright (C) 2004 Xiranet Communications GmbH <arne.redlich@xiranet.com>
 *  Copyright (C) 2002 - 2003 Ardis Technologies <roman@ardistech.com>,
 *  Copyright (C) 2007 - 2018 Vladislav Bolkhovitin
 *  Copyright (C) 2007 - 2018 Western Digital Corporation
 *
 *  and code taken from UNH iSCSI software:
 *  Copyright (C) 2001-2003 InterOperability Lab (IOL)
 *  University of New Hampshire (UNH)
 *  Durham, NH 03824
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation, version 2
 *  of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  Heavily based on code from UNH iSCSI iscsid.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include "sha1.h"
#include "md5.h"

#include "iscsid.h"

#define HEX_FORMAT    0x01
#define BASE64_FORMAT 0x02

#define CHAP_DIGEST_ALG_MD5   5
#define CHAP_DIGEST_ALG_SHA1  6

#define CHAP_MD5_DIGEST_LEN  16
#define CHAP_SHA1_DIGEST_LEN 20

#define CHAP_INITIATOR_ERROR -1
#define CHAP_AUTH_ERROR      -2
#define CHAP_TARGET_ERROR    -3

#define CHAP_AUTH_STATE_START     AUTH_STATE_START
#define CHAP_AUTH_STATE_CHALLENGE 1
#define CHAP_AUTH_STATE_RESPONSE  2

#define CHAP_INITIATOR_AUTH 0
#define CHAP_TARGET_AUTH    1

#define CHAP_CHALLENGE_MAX	50

static inline int decode_hex_digit(char c)
{
	switch (c) {
	case '0' ... '9':
		return c - '0';
	case 'a' ... 'f':
		return c - 'a' + 10;
	case 'A' ... 'F':
		return c - 'A' + 10;
	}
	return 0;
}

static void decode_hex_string(char *hex_string, u8 *intnum, int intlen)
{
	char *ptr;
	int j;

	j = strlen(hex_string);
	ptr = hex_string + j;
	j = --intlen;
	do {
		intnum[j] = decode_hex_digit(*--ptr);
		intnum[j] |= decode_hex_digit(*--ptr) << 4;
		j--;
	} while (ptr > hex_string);

	while (j >= 0)
		intnum[j--] = 0;
}

/* Base64 decoding, taken from UNH-iSCSI "Base64codeToNumber()" */
static u8 decode_base64_digit(char base64)
{
	switch (base64) {
	case '=':
		return 64;
	case '/':
		return 63;
	case '+':
		return 62;
	default:
		if ((base64 >= 'A') && (base64 <= 'Z'))
			return base64 - 'A';
		else if ((base64 >= 'a') && (base64 <= 'z'))
			return 26 + (base64 - 'a');
		else if ((base64 >= '0') && (base64 <= '9'))
			return 52 + (base64 - '0');
		else
			//XXX This return value should be unsigned; and anyway
			//XXX in case of a bad character in the string, our
			//XXX caller (sometimes) checks for 65, not 255 or -1
			return -1;
	}
}

/* Base64 decoding, taken from UNH-iSCSI "Base64StringToInteger()" */
static void decode_base64_string(char *string, u8 *intnum, int int_len)
{
	int len;
	int count;
	int intptr;
	u8 num[4];
	int octets;

	if ((string == NULL) || (intnum == NULL))
		return;
	len = strlen(string);
	if (len == 0)
		return;
	if ((len % 4) != 0)
		return;
	count = 0;
	intptr = 0;
	while (count < len - 4) {
		num[0] = decode_base64_digit(string[count]);
		num[1] = decode_base64_digit(string[count + 1]);
		num[2] = decode_base64_digit(string[count + 2]);
		num[3] = decode_base64_digit(string[count + 3]);
		if ((num[0] == 65) || (num[1] == 65) || (num[2] == 65) || (num[3] == 65))
			return;
		count += 4;
		octets =
		    (num[0] << 18) | (num[1] << 12) | (num[2] << 6) | num[3];
		intnum[intptr] = (octets & 0xFF0000) >> 16;
		intnum[intptr + 1] = (octets & 0x00FF00) >> 8;
		intnum[intptr + 2] = octets & 0x0000FF;
		intptr += 3;
	}
	num[0] = decode_base64_digit(string[count]);
	num[1] = decode_base64_digit(string[count + 1]);
	num[2] = decode_base64_digit(string[count + 2]);
	num[3] = decode_base64_digit(string[count + 3]);
	//XXX Check for the special "bad character in string" value here like above?
	//XXX Also check the string for missing/incorrect padding?
	if ((num[0] == 64) || (num[1] == 64))
		return;
	if (num[2] == 64) {
		if (num[3] != 64)
			return;
		intnum[intptr] = (num[0] << 2) | (num[1] >> 4);
	} else if (num[3] == 64) {
		intnum[intptr] = (num[0] << 2) | (num[1] >> 4);
		intnum[intptr + 1] = (num[1] << 4) | (num[2] >> 2);
	} else {
		octets =
		    (num[0] << 18) | (num[1] << 12) | (num[2] << 6) | num[3];
		intnum[intptr] = (octets & 0xFF0000) >> 16;
		intnum[intptr + 1] = (octets & 0x00FF00) >> 8;
		intnum[intptr + 2] = octets & 0x0000FF;
	}
}

static inline void encode_hex_string(u8 *intnum, long length, char *string)
{
	int i;
	char *strptr;

	strptr = string;
	for (i = 0; i < length; i++, strptr += 2)
		sprintf(strptr, "%.2hhx", intnum[i]);
}

/* Base64 encoding, taken from UNH iSCSI "IntegerToBase64String()" */
static void encode_base64_string(u8 *intnum, long length, char *string)
{
	int count, octets, strptr, delta;
	static const char base64code[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G',
					   'H', 'I', 'J', 'K', 'L', 'M', 'N',
					   'O', 'P', 'Q', 'R', 'S', 'T', 'U',
					   'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
					   'c', 'd', 'e', 'f', 'g', 'h', 'i',
					   'j', 'k', 'l', 'm', 'n', 'o', 'p',
					   'q', 'r', 's', 't', 'u', 'v', 'w',
					   'x', 'y', 'z', '0', '1', '2', '3',
					   '4', '5', '6', '7', '8', '9', '+',
					   '/', '=' };

	if ((!intnum) || (!string) || (!length))
		return;

	count = 0;
	octets = 0;
	strptr = 0;

	while ((delta = (length - count)) > 2) {
		octets = (intnum[count] << 16) | (intnum[count + 1] << 8) | intnum[count + 2];
		string[strptr] = base64code[(octets & 0xfc0000) >> 18];
		string[strptr + 1] = base64code[(octets & 0x03f000) >> 12];
		string[strptr + 2] = base64code[(octets & 0x000fc0) >> 6];
		string[strptr + 3] = base64code[octets & 0x00003f];
		count += 3;
		strptr += 4;
	}
	if (delta == 1) {
		string[strptr] = base64code[(intnum[count] & 0xfc) >> 2];
		string[strptr + 1] = base64code[(intnum[count] & 0x03) << 4];
		string[strptr + 2] = base64code[64];
		string[strptr + 3] = base64code[64];
		strptr += 4;
	} else if (delta == 2) {
		string[strptr] = base64code[(intnum[count] & 0xfc) >> 2];
		string[strptr + 1] = base64code[((intnum[count] & 0x03) << 4) | ((intnum[count + 1] & 0xf0) >> 4)];
		string[strptr + 2] = base64code[(intnum[count + 1] & 0x0f) << 2];
		string[strptr + 3] = base64code[64];
		strptr += 4;
	}
	string[strptr] = '\0';
}

static inline int chap_check_encoding_format(char *encoded)
{
	int encoding_fmt;

	if (!encoded)
		return -1;
	if ((strlen(encoded) < 3) || (encoded[0] != '0'))
		return -1;

	if (encoded[1] == 'x' || encoded[1] == 'X')
		encoding_fmt = HEX_FORMAT;
	else if (encoded[1] == 'b' || encoded[1] == 'B')
		encoding_fmt = BASE64_FORMAT;
	else
		return -1;

	return encoding_fmt;
}

static int chap_alloc_decode_buffer(char *encoded, u8 **decode_buf, int encoding_fmt)
{
	int i;
	int decode_len = 0;

	i = strlen(encoded);
	i -= 2;

	if (encoding_fmt == HEX_FORMAT)
		decode_len = (i - 1) / 2 + 1;
	else if (encoding_fmt == BASE64_FORMAT) {
		if (i % 4)
			return CHAP_INITIATOR_ERROR;

		decode_len =  i / 4 * 3;
		if (encoded[i + 1] == '=')
			decode_len--;
		if (encoded[i] == '=')
			decode_len--;
	}

	if (!decode_len)
		return CHAP_INITIATOR_ERROR;

	*decode_buf = malloc(decode_len);
	if (!*decode_buf)
		return CHAP_TARGET_ERROR;

	return decode_len;
}

static int chap_decode_string(char *encoded, u8 *decode_buf, int buf_len, int encoding_fmt)
{
	if (encoding_fmt == HEX_FORMAT) {
		if ((strlen(encoded) - 2) > (2 * buf_len)) {
			log_error("%s(%d) BUG? "
				  " buf[%d] !sufficient to decode string[%d]",
				  __func__, __LINE__, buf_len, (int) strlen(encoded));
			return CHAP_TARGET_ERROR;
		}
		decode_hex_string(encoded + 2, decode_buf, buf_len);

	} else if (encoding_fmt == BASE64_FORMAT) {
		if ((strlen(encoded) - 2) > ((buf_len - 1) / 3 + 1) * 4) {
			log_error("%s(%d) BUG? "
				  " buf[%d] !sufficient to decode string[%d]",
				  __func__, __LINE__, buf_len, (int) strlen(encoded));
			return CHAP_TARGET_ERROR;
		}
		decode_base64_string(encoded + 2, decode_buf, buf_len);

	} else
		return CHAP_INITIATOR_ERROR;

	return 0;
}

static inline void chap_encode_string(u8 *intnum, int buf_len, char *encode_buf, int encoding_fmt)
{
	encode_buf[0] = '0';
	if (encoding_fmt == HEX_FORMAT) {
		encode_buf[1] = 'x';
		encode_hex_string(intnum, buf_len, encode_buf + 2);
	} else if (encoding_fmt == BASE64_FORMAT) {
		encode_buf[1] = 'b';
		encode_base64_string(intnum, buf_len, encode_buf + 2);
	}
}

static inline void chap_calc_digest_md5(char chap_id, const char *secret, int secret_len,
	const u8 *challenge, int challenge_len, u8 *digest)
{
	struct md5_ctx ctx;

	md5_init(&ctx);
	md5_update(&ctx, &chap_id, 1);
	md5_update(&ctx, secret, secret_len);
	md5_update(&ctx, challenge, challenge_len);
	md5_final(&ctx, digest);
}

static inline void chap_calc_digest_sha1(char chap_id, const char *secret, int secret_len,
	const u8 *challenge, int challenge_len, u8 *digest)
{
	struct sha1_ctx ctx;

	sha1_init(&ctx);
	sha1_update(&ctx, &chap_id, 1);
	sha1_update(&ctx, secret, secret_len);
	sha1_update(&ctx, challenge, challenge_len);
	sha1_final(&ctx, digest);
}

/*
 * To generate challenge for CHAP, use stronger random number generator as
 * opposed to simple rand().
 */
static int chap_rand(void)
{
	int fd;
	int r;

	fd = open("/dev/urandom", O_RDONLY);
	assert(fd != -1);
	if (read(fd, &r, sizeof(r)) < sizeof(r)) {
	}
	close(fd);
	return r;
}

static int chap_initiator_auth_create_challenge(struct connection *conn)
{
	char *value, *p;
	char text[CHAP_CHALLENGE_MAX * 2 + 8];
	static int chap_id;
	int i;

	value = text_key_find(conn, "CHAP_A");
	if (!value)
		return CHAP_INITIATOR_ERROR;
	while ((p = strsep(&value, ","))) {
		if (!strcmp(p, "5")) {
			conn->auth.chap.digest_alg = CHAP_DIGEST_ALG_MD5;
			conn->auth_state = CHAP_AUTH_STATE_CHALLENGE;
			break;
		} else if (!strcmp(p, "6")) {
			conn->auth.chap.digest_alg = CHAP_DIGEST_ALG_SHA1;
			conn->auth_state = CHAP_AUTH_STATE_CHALLENGE;
			break;
		}
	}
	if (!p)
		return CHAP_INITIATOR_ERROR;

	text_key_add(conn, "CHAP_A", p);
	conn->auth.chap.id = ++chap_id;
	sprintf(text, "%u", (unsigned char)conn->auth.chap.id);
	text_key_add(conn, "CHAP_I", text);

	/*
	 * ToDo: does a random challenge length provide any benefits security-
	 * wise, or should we rather always use the max. allowed length of
	 * 1024 for the (unencoded) challenge?
	 */
	conn->auth.chap.challenge_size = (chap_rand() % (CHAP_CHALLENGE_MAX / 2)) + CHAP_CHALLENGE_MAX / 2;

	conn->auth.chap.challenge = malloc(conn->auth.chap.challenge_size);
	if (!conn->auth.chap.challenge)
		return CHAP_TARGET_ERROR;

	p = text;
	strcpy(p, "0x");
	p += 2;
	for (i = 0; i < conn->auth.chap.challenge_size; i++) {
		conn->auth.chap.challenge[i] = chap_rand();
		sprintf(p, "%.2hhx", conn->auth.chap.challenge[i]);
		p += 2;
	}
	text_key_add(conn, "CHAP_C",  text);

	return 0;
}

static int chap_initiator_auth_check_response(struct connection *conn)
{
	char *value;
	u8 *his_digest = NULL, *our_digest = NULL;
	int digest_len = 0, retval = 0, encoding_format;
	char pass[ISCSI_NAME_LEN];

	if (accounts_empty(conn->tid, ISCSI_USER_DIR_INCOMING)) {
		log_warning("CHAP initiator auth.: "
			    "No CHAP credentials configured");
		retval = CHAP_TARGET_ERROR;
		goto out;
	}

	if (!(value = text_key_find(conn, "CHAP_N"))) {
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	conn->user = strdup(value);
	if (conn->user == NULL) {
		log_error("Unable to duplicate initiator's USER %s", value);
	}

	memset(pass, 0, sizeof(pass));
	if (config_account_query(conn->tid, ISCSI_USER_DIR_INCOMING, value, pass) < 0) {
		log_warning("CHAP initiator auth.: "
			    "No valid user/pass combination for initiator %s "
			    "found", conn->initiator);
		retval = CHAP_AUTH_ERROR;
		goto out;
	}

	if (!(value = text_key_find(conn, "CHAP_R"))) {
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	if ((encoding_format = chap_check_encoding_format(value)) < 0) {
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	switch (conn->auth.chap.digest_alg) {
	case CHAP_DIGEST_ALG_MD5:
		digest_len = CHAP_MD5_DIGEST_LEN;
		break;
	case CHAP_DIGEST_ALG_SHA1:
		digest_len = CHAP_SHA1_DIGEST_LEN;
		break;
	default:
		retval = CHAP_TARGET_ERROR;
		goto out;
	}

	if (!(his_digest = malloc(digest_len))) {
		retval = CHAP_TARGET_ERROR;
		goto out;
	}
	if (!(our_digest = malloc(digest_len))) {
		retval = CHAP_TARGET_ERROR;
		goto out;
	}

	if (chap_decode_string(value, his_digest, digest_len, encoding_format) < 0) {
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	switch (conn->auth.chap.digest_alg) {
	case CHAP_DIGEST_ALG_MD5:
		chap_calc_digest_md5(conn->auth.chap.id, pass, strlen(pass),
				     conn->auth.chap.challenge,
				     conn->auth.chap.challenge_size,
				     our_digest);
		break;
	case CHAP_DIGEST_ALG_SHA1:
		chap_calc_digest_sha1(conn->auth.chap.id, pass, strlen(pass),
				      conn->auth.chap.challenge,
				      conn->auth.chap.challenge_size,
				      our_digest);
		break;
	default:
		retval = CHAP_TARGET_ERROR;
		goto out;
	}

	if (memcmp(our_digest, his_digest, digest_len)) {
		log_warning("CHAP initiator auth.: "
			    "authentication of %s failed (wrong secret!?)",
			    conn->initiator);
		retval = CHAP_AUTH_ERROR;
		goto out;
	}

	conn->state = CHAP_AUTH_STATE_RESPONSE;
 out:
	if (his_digest)
		free(his_digest);
	if (our_digest)
		free(our_digest);
	return retval;
}

static int chap_target_auth_create_response(struct connection *conn)
{
	char chap_id, *value, *response = NULL;
	u8 *challenge = NULL, *digest = NULL;
	int encoding_format, response_len;
	int challenge_len = 0, digest_len = 0, retval = 0;
	struct iscsi_attr *user;

	if (!(value = text_key_find(conn, "CHAP_I"))) {
		/* Initiator doesn't want target auth!? */
		conn->state = STATE_SECURITY_DONE;
		retval = 0;
		goto out;
	}
	chap_id = strtol(value, &value, 10);

	user = account_get_first(conn->tid, ISCSI_USER_DIR_OUTGOING);
	if (user == NULL) {
		log_warning("CHAP target auth.: "
			    "no outgoing credentials configured%s",
			    conn->tid ? "." : " for discovery.");
		retval = CHAP_AUTH_ERROR;
		goto out;
	}

	if (!(value = text_key_find(conn, "CHAP_C"))) {
		log_warning("CHAP target auth.: "
			    "got no challenge from initiator %s",
			    conn->initiator);
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	if ((encoding_format = chap_check_encoding_format(value)) < 0) {
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	retval = chap_alloc_decode_buffer(value, &challenge, encoding_format);
	if (retval <= 0)
		goto out;
	else if (retval > 1024) {
		log_warning("CHAP target auth.: "
			    "initiator %s sent challenge of invalid length %d",
			    conn->initiator, challenge_len);
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	challenge_len = retval;
	retval = 0;

	switch (conn->auth.chap.digest_alg) {
	case CHAP_DIGEST_ALG_MD5:
		digest_len = CHAP_MD5_DIGEST_LEN;
		break;
	case CHAP_DIGEST_ALG_SHA1:
		digest_len = CHAP_SHA1_DIGEST_LEN;
		break;
	default:
		retval = CHAP_TARGET_ERROR;
		goto out;
	}

	if (encoding_format == HEX_FORMAT)
		response_len = 2 * digest_len;
	else
		response_len = ((digest_len - 1) / 3 + 1) * 4;
	/* "0x" / "0b" and "\0": */
	response_len += 3;

	if (!(digest = malloc(digest_len))) {
		retval = CHAP_TARGET_ERROR;
		goto out;
	}
	if (!(response = malloc(response_len))) {
		retval = CHAP_TARGET_ERROR;
		goto out;
	}

	if (chap_decode_string(value, challenge, challenge_len, encoding_format) < 0) {
		retval = CHAP_INITIATOR_ERROR;
		goto out;
	}

	/* RFC 3720, 8.2.1: CHAP challenges MUST NOT be reused */
	if (challenge_len == conn->auth.chap.challenge_size) {
		if (!memcmp(challenge, conn->auth.chap.challenge,
			    challenge_len)) {
			/* ToDo: RFC 3720 demands to close TCP conn */
			log_warning("CHAP target auth.: "
				    "initiator %s reflected our challenge",
				    conn->initiator);
			retval = CHAP_INITIATOR_ERROR;
			goto out;
		}
	}

	switch (conn->auth.chap.digest_alg) {
	case CHAP_DIGEST_ALG_MD5:
		chap_calc_digest_md5(chap_id, ISCSI_USER_PASS(user),
			strlen(ISCSI_USER_PASS(user)), challenge, challenge_len, digest);
		break;
	case CHAP_DIGEST_ALG_SHA1:
		chap_calc_digest_sha1(chap_id, ISCSI_USER_PASS(user),
			strlen(ISCSI_USER_PASS(user)), challenge, challenge_len, digest);
		break;
	default:
		retval = CHAP_TARGET_ERROR;
		goto out;
	}

	memset(response, 0x0, response_len);
	chap_encode_string(digest, digest_len, response, encoding_format);
	text_key_add(conn, "CHAP_N", ISCSI_USER_NAME(user));
	text_key_add(conn, "CHAP_R", response);

	conn->state = STATE_SECURITY_DONE;
 out:
	if (challenge)
		free(challenge);
	if (digest)
		free(digest);
	if (response)
		free(response);
	return retval;
}

int cmnd_exec_auth_chap(struct connection *conn)
{
	int res;

	switch (conn->auth_state) {
	case CHAP_AUTH_STATE_START:
		res = chap_initiator_auth_create_challenge(conn);
		break;
	case CHAP_AUTH_STATE_CHALLENGE:
		res = chap_initiator_auth_check_response(conn);
		if (res < 0)
			break;
		/* fall through */
	case CHAP_AUTH_STATE_RESPONSE:
		res = chap_target_auth_create_response(conn);
		break;
	default:
		log_error("%s(%d): BUG. unknown conn->auth_state %d",
			  __func__, __LINE__, conn->auth_state);
		res = CHAP_TARGET_ERROR;
	}

	return res;
}
