/*
 * Compuserve RPA authentication mechanism.
 *
 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
 *
 * This software is released under the MIT license.
 */

#include "auth-common.h"
#include "mech.h"
#include "passdb.h"
#include "str.h"
#include "strfuncs.h"
#include "safe-memset.h"
#include "randgen.h"
#include "buffer.h"
#include "hostpid.h"
#include "hex-binary.h"
#include "md5.h"

struct rpa_auth_request {
	struct auth_request auth_request;

	pool_t pool;

	int phase;

	/* cached: */
	unsigned char pwd_md5[MD5_RESULTLEN];
	size_t service_len;
	const unsigned char *service_ucs2be;
	size_t username_len;
	const unsigned char *username_ucs2be;
	size_t realm_len;
	const unsigned char *realm_ucs2be;

	/* requested: */
	unsigned char *service_challenge;
	unsigned char *service_timestamp;

	/* received: */
	unsigned int user_challenge_len;
	unsigned char *user_challenge;
	unsigned char *user_response;
	unsigned char session_key[16];
};

#define RPA_SCHALLENGE_LEN	32
#define RPA_UCHALLENGE_LEN	16
#define RPA_TIMESTAMP_LEN	14

#define ASN1_APPLICATION	0x60

/* Object id encoded using ASN.1 DER */
static const unsigned char rpa_oid[] = {
	0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x73, 0x01, 0x01
};

void *ucs2be_str(pool_t pool, const char *str, size_t *size);

/*
 * Compute client -> server authentication response.
 */
static void rpa_user_response(struct rpa_auth_request *request,
			      unsigned char digest[MD5_RESULTLEN])
{
	struct md5_context ctx;
	unsigned char z[48];

	memset(z, 0, sizeof(z));

	md5_init(&ctx);
	md5_update(&ctx, request->pwd_md5, sizeof(request->pwd_md5));
	md5_update(&ctx, z, sizeof(z));
	md5_update(&ctx, request->username_ucs2be, request->username_len);
	md5_update(&ctx, request->service_ucs2be, request->service_len);
	md5_update(&ctx, request->realm_ucs2be, request->realm_len);
	md5_update(&ctx, request->user_challenge, request->user_challenge_len);
	md5_update(&ctx, request->service_challenge, RPA_SCHALLENGE_LEN);
	md5_update(&ctx, request->service_timestamp, RPA_TIMESTAMP_LEN);
	md5_update(&ctx, request->pwd_md5, sizeof(request->pwd_md5));
	md5_final(&ctx, digest);
}

/*
 * Compute server -> client authentication response.
 */
static void rpa_server_response(struct rpa_auth_request *request,
				unsigned char digest[MD5_RESULTLEN])
{
	struct md5_context ctx;
	unsigned char tmp[MD5_RESULTLEN];
	unsigned char z[48];
	unsigned int i;

	memset(z, 0, sizeof(z));

	md5_init(&ctx);
	md5_update(&ctx, request->pwd_md5, sizeof(request->pwd_md5));
	md5_update(&ctx, z, sizeof(z));
	md5_update(&ctx, request->service_ucs2be, request->service_len);
	md5_update(&ctx, request->username_ucs2be, request->username_len);
	md5_update(&ctx, request->realm_ucs2be, request->realm_len);
	md5_update(&ctx, request->service_challenge, RPA_SCHALLENGE_LEN);
	md5_update(&ctx, request->user_challenge, request->user_challenge_len);
	md5_update(&ctx, request->service_timestamp, RPA_TIMESTAMP_LEN);
	md5_update(&ctx, request->pwd_md5, sizeof(request->pwd_md5));
	md5_final(&ctx, tmp);

	for (i = 0; i < sizeof(tmp); i++)
		tmp[i] = request->session_key[i] ^ tmp[i];

	md5_init(&ctx);
	md5_update(&ctx, request->pwd_md5, sizeof(request->pwd_md5));
	md5_update(&ctx, z, sizeof(z));
	md5_update(&ctx, request->service_ucs2be, request->service_len);
	md5_update(&ctx, request->username_ucs2be, request->username_len);
	md5_update(&ctx, request->realm_ucs2be, request->realm_len);
	md5_update(&ctx, request->session_key, sizeof(request->session_key));
	md5_update(&ctx, request->service_challenge, RPA_SCHALLENGE_LEN);
	md5_update(&ctx, request->user_challenge, request->user_challenge_len);
	md5_update(&ctx, request->service_timestamp, RPA_TIMESTAMP_LEN);
	md5_update(&ctx, tmp, sizeof(tmp));
	md5_update(&ctx, request->pwd_md5, sizeof(request->pwd_md5));
	md5_final(&ctx, digest);
}

static const unsigned char *
rpa_check_message(const unsigned char *data, const unsigned char *end,
		  const char **error)
{
	const unsigned char *p = data;
	unsigned int len = 0;

	if (p + 2 > end) {
		*error = "message too short";
		return NULL;
	}

	if (*p++ != ASN1_APPLICATION) {
		*error = "invalid data type";
		return NULL;
	}

	if ((*p & 0x80) != 0) {
		unsigned int nbytes = *p++ & 0x7f;

		while (nbytes-- > 0) {
			if (p >= end) {
				*error = "invalid structure length";
				return NULL;
			}

			len = (len << 8) | *p++;
		}
	} else
		len = *p++;

	if ((size_t)(end - p) != len) {
		*error = "structure length disagrees with data size";
		return NULL;
	}

	if (p + sizeof(rpa_oid) > end) {
		*error = "not enough space for object id";
		return NULL;
	}

	if (memcmp(p, rpa_oid, sizeof(rpa_oid)) != 0) {
		*error = "invalid object id";
		return NULL;
	}

	return p + sizeof(rpa_oid);
}

static bool
rpa_parse_token1(const void *data, size_t data_size, const char **error)
{
	const unsigned char *end = ((const unsigned char *) data) + data_size;
	const unsigned char *p;
	unsigned int version_lo, version_hi;

	p = rpa_check_message(data, end, error);
	if (p == NULL)
		return FALSE;

	if (p + 6 > end) {
		*error = "message too short";
		return FALSE;
	}

	version_lo = p[0] + (p[1] << 8);
	version_hi = p[2] + (p[3] << 8);

	if ((version_lo > 3) || (version_hi < 3)) {
		*error = "protocol version mismatch";
		return FALSE;
	}
	p += 4;

	if ((p[0] != 0) || (p[1] != 1)) {
		*error = "invalid message flags";
		return FALSE;
	}
	p += 2;

	if (p != end) {
		*error = "unneeded data found";
		return FALSE;
	}

	return TRUE;
}

static unsigned int
rpa_read_buffer(pool_t pool, const unsigned char **data,
		const unsigned char *end, unsigned char **buffer)
{
	const unsigned char *p = *data;
	unsigned int len;

	if (p > end)
		return 0;

	len = *p++;
	if (p + len > end)
		return 0;

	*buffer = p_malloc(pool, len);
	memcpy(*buffer, p, len);

	*data += 1 + len;

	return len;
}

static bool
rpa_parse_token3(struct rpa_auth_request *request, const void *data,
		 size_t data_size, const char **error)
{
	struct auth_request *auth_request = &request->auth_request;
	const unsigned char *end = ((const unsigned char *)data) + data_size;
	const unsigned char *p;
	unsigned int len;
	const char *user, *realm;

	p = rpa_check_message(data, end, error);
	if (p == NULL)
		return FALSE;

	/* Read username@realm */
	if (p + 2 > end) {
		*error = "message too short";
		return FALSE;
	}

	len = (p[0] << 8) + p[1];
	if (p + 2 + len > end) {
		*error = "message too short";
		return FALSE;
	}
	p += 2;

	user = t_strndup(p, len);
	realm = strrchr(user, '@');
	if (realm == NULL) {
		*error = "missing realm";
		return FALSE;
	}
	user = t_strdup_until(user, realm++);
	p += len;

	if (!auth_request_set_username(auth_request, user, error))
		return FALSE;

	request->username_ucs2be = ucs2be_str(request->pool, auth_request->user,
					      &request->username_len);
	request->realm_ucs2be = ucs2be_str(request->pool, realm,
					   &request->realm_len);

	/* Read user challenge */
	request->user_challenge_len = rpa_read_buffer(request->pool, &p, end,
						      &request->user_challenge);
	if (request->user_challenge_len == 0) {
		*error = "invalid user challenge";
		return FALSE;
	}

	/* Read user response */
	len = rpa_read_buffer(request->pool, &p, end, &request->user_response);
	if (len != RPA_UCHALLENGE_LEN) {
		*error = "invalid user response";
		return FALSE;
	}

	if (p != end) {
		*error = "unneeded data found";
		return FALSE;
	}

	return TRUE;
}

static void
buffer_append_asn1_length(buffer_t *buf, unsigned int length)
{
	if (length < 0x80) {
		buffer_append_c(buf, length);
	} else if (length < 0x100) {
		buffer_append_c(buf, 0x81);
		buffer_append_c(buf, length);
	} else {
		buffer_append_c(buf, 0x82);
		buffer_append_c(buf, length >> 8);
		buffer_append_c(buf, length & 0xff);
	}
}

static void
rpa_add_realm(string_t *realms, const char *realm, const char *service)
{
	str_append(realms, service);	
	str_append_c(realms, '@');
	str_append(realms, realm);
	str_append_c(realms, ' ');
}

static const unsigned char *
mech_rpa_build_token2(struct rpa_auth_request *request, size_t *size)
{
	const struct auth_settings *set = request->auth_request.set;
	unsigned int realms_len, length;
	string_t *realms;
	buffer_t *buf;
	unsigned char timestamp[RPA_TIMESTAMP_LEN / 2];
	const char *const *tmp;

	realms = t_str_new(64);
	for (tmp = set->realms_arr; *tmp != NULL; tmp++) {
		rpa_add_realm(realms, *tmp, request->auth_request.service);
	}

	if (str_len(realms) == 0) {
		rpa_add_realm(realms, *set->default_realm != '\0' ?
			      set->default_realm : my_hostname,
			      request->auth_request.service);
	}

	realms_len = str_len(realms) - 1;
        length = sizeof(rpa_oid) + 3 + RPA_SCHALLENGE_LEN +
		RPA_TIMESTAMP_LEN + 2 + realms_len;

	buf = buffer_create_dynamic(request->pool, length + 4);

	buffer_append_c(buf, ASN1_APPLICATION);
	buffer_append_asn1_length(buf, length);
	buffer_append(buf, rpa_oid, sizeof(rpa_oid));

	/* Protocol version */
	buffer_append_c(buf, 3);
	buffer_append_c(buf, 0);

	/* Service challenge */
	request->service_challenge =
		p_malloc(request->pool, RPA_SCHALLENGE_LEN);
	random_fill(request->service_challenge, RPA_SCHALLENGE_LEN);
	buffer_append_c(buf, RPA_SCHALLENGE_LEN);
	buffer_append(buf, request->service_challenge, RPA_SCHALLENGE_LEN);

	/* Timestamp, looks like clients accept anything we send */
	random_fill(timestamp, sizeof(timestamp));
	request->service_timestamp = p_malloc(request->pool, RPA_TIMESTAMP_LEN);
	memcpy(request->service_timestamp,
	       binary_to_hex(timestamp, sizeof(timestamp)),
	       RPA_TIMESTAMP_LEN);
	buffer_append(buf, request->service_timestamp, RPA_TIMESTAMP_LEN);

	/* Realm list */
	buffer_append_c(buf, realms_len >> 8);
	buffer_append_c(buf, realms_len & 0xff);
	buffer_append(buf, str_c(realms), realms_len);

	*size = buffer_get_used_size(buf);
	return buffer_free_without_data(&buf);
}

static const unsigned char *
mech_rpa_build_token4(struct rpa_auth_request *request, size_t *size)
{
	buffer_t *buf;
	unsigned char server_response[MD5_RESULTLEN];
	unsigned int length = sizeof(rpa_oid) +
		sizeof(server_response) + 1 +
		sizeof(request->session_key) + 1 + 1;

	buf = buffer_create_dynamic(request->pool, length + 4);

	buffer_append_c(buf, ASN1_APPLICATION);
	buffer_append_asn1_length(buf, length);
	buffer_append(buf, rpa_oid, sizeof(rpa_oid));

	/* Generate random session key */
	random_fill(request->session_key, sizeof(request->session_key));

	/* Server authentication response */
	rpa_server_response(request, server_response);
	buffer_append_c(buf, sizeof(server_response));
	buffer_append(buf, server_response, sizeof(server_response));

	buffer_append_c(buf, sizeof(request->session_key));
	buffer_append(buf, request->session_key, sizeof(request->session_key));

	/* Status, 0 - success */
	buffer_append_c(buf, 0);

	*size = buffer_get_used_size(buf);
	return buffer_free_without_data(&buf);
}

static bool verify_credentials(struct rpa_auth_request *request,
			       const unsigned char *credentials, size_t size)
{
	unsigned char response[MD5_RESULTLEN];

	if (size != sizeof(request->pwd_md5)) {
                auth_request_log_error(&request->auth_request, AUTH_SUBSYS_MECH,
				       "invalid credentials length");
		return FALSE;
	}

	memcpy(request->pwd_md5, credentials, sizeof(request->pwd_md5));
	rpa_user_response(request, response);
	return memcmp(response, request->user_response, sizeof(response)) == 0;
}

static void
rpa_credentials_callback(enum passdb_result result,
			 const unsigned char *credentials, size_t size,
			 struct auth_request *auth_request)
{
	struct rpa_auth_request *request =
		(struct rpa_auth_request *)auth_request;
	const unsigned char *token4;
	size_t token4_size;

	switch (result) {
	case PASSDB_RESULT_OK:
		if (!verify_credentials(request, credentials, size))
			auth_request_fail(auth_request);
		else {
			token4 = mech_rpa_build_token4(request, &token4_size);
			auth_request_handler_reply_continue(auth_request,
							    token4,
							    token4_size);
			request->phase = 2;
		}
		break;
	case PASSDB_RESULT_INTERNAL_FAILURE:
		auth_request_internal_failure(auth_request);
		break;
	default:
		auth_request_fail(auth_request);
		break;
	}
}

static void
mech_rpa_auth_phase1(struct auth_request *auth_request,
		     const unsigned char *data, size_t data_size)
{
	struct rpa_auth_request *request =
		(struct rpa_auth_request *)auth_request;
	const unsigned char *token2;
	size_t token2_size;
	const char *service, *error;

	if (!rpa_parse_token1(data, data_size, &error)) {
		auth_request_log_info(auth_request, AUTH_SUBSYS_MECH,
			"invalid token 1: %s", error);
		auth_request_fail(auth_request);
		return;
	}

	service = t_str_lcase(auth_request->service);

	token2 = mech_rpa_build_token2(request, &token2_size);

	request->service_ucs2be = ucs2be_str(request->pool, service,
					     &request->service_len);

	auth_request_handler_reply_continue(auth_request, token2, token2_size);
	request->phase = 1;
}

static void
mech_rpa_auth_phase2(struct auth_request *auth_request,
		     const unsigned char *data, size_t data_size)
{
	struct rpa_auth_request *request =
		(struct rpa_auth_request *)auth_request;
	const char *error;

	if (!rpa_parse_token3(request, data, data_size, &error)) {
		auth_request_log_info(auth_request, AUTH_SUBSYS_MECH,
			"invalid token 3: %s", error);
		auth_request_fail(auth_request);
		return;
	}

	auth_request_lookup_credentials(auth_request, "RPA",
					rpa_credentials_callback);
}

static void
mech_rpa_auth_phase3(struct auth_request *auth_request,
		     const unsigned char *data, size_t data_size)
{
	static const unsigned char client_ack[3] = { 0x60, 0x01, 0x00 };

	if ((data_size != sizeof(client_ack)) ||
	    (memcmp(data, client_ack, sizeof(client_ack)) != 0)) {
		auth_request_log_info(auth_request, AUTH_SUBSYS_MECH,
			"invalid token 5 or client rejects us");
		auth_request_fail(auth_request);
	} else {
		auth_request_success(auth_request, "", 0);
	}
}

static void
mech_rpa_auth_continue(struct auth_request *auth_request,
		       const unsigned char *data, size_t data_size)
{
	struct rpa_auth_request *request =
		(struct rpa_auth_request *)auth_request;

	switch (request->phase) {
	case 0:
		mech_rpa_auth_phase1(auth_request, data, data_size);
		break;
	case 1:
		mech_rpa_auth_phase2(auth_request, data, data_size);
		break;
	case 2:
		mech_rpa_auth_phase3(auth_request, data, data_size);
		break;
	default:
		auth_request_fail(auth_request);
		break;
	}
}

static void
mech_rpa_auth_free(struct auth_request *auth_request)
{
	struct rpa_auth_request *request =
		(struct rpa_auth_request *)auth_request;

	safe_memset(request->pwd_md5, 0, sizeof(request->pwd_md5));

	pool_unref(&auth_request->pool);
}

static struct auth_request *mech_rpa_auth_new(void)
{
	struct rpa_auth_request *request;
	pool_t pool;

	pool = pool_alloconly_create(MEMPOOL_GROWING"rpa_auth_request", 2048);
	request = p_new(pool, struct rpa_auth_request, 1);
	request->pool = pool;
	request->phase = 0;

	request->auth_request.pool = pool;
	return &request->auth_request;
}

const struct mech_module mech_rpa = {
	"RPA",

	.flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE |
		MECH_SEC_MUTUAL_AUTH,
	.passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS,

	mech_rpa_auth_new,
	mech_generic_auth_initial,
	mech_rpa_auth_continue,
	mech_rpa_auth_free
};
