/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2017 Nexenta Systems, Inc.  All rights reserved.
 */

/*
 * SPNEGO back-end for NTLMSSP.  See [MS-NLMP]
 */

#include <sys/types.h>
#include <sys/byteorder.h>
#include <strings.h>
#include "smbd.h"
#include "smbd_authsvc.h"
#include "netsmb/ntlmssp.h"
#include <assert.h>

/* A shorter alias for a crazy long name from [MS-NLMP] */
#define	NTLMSSP_NEGOTIATE_NTLM2 \
	NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY

/* Need this in a header somewhere */
#ifdef _LITTLE_ENDIAN
/* little-endian values on little-endian */
#define	htolel(x)	((uint32_t)(x))
#define	letohl(x)	((uint32_t)(x))
#else	/* (BYTE_ORDER == LITTLE_ENDIAN) */
/* little-endian values on big-endian (swap) */
#define	letohl(x)	BSWAP_32(x)
#define	htolel(x)	BSWAP_32(x)
#endif	/* (BYTE_ORDER == LITTLE_ENDIAN) */

typedef struct ntlmssp_backend {
	uint32_t expect_type;
	uint32_t clnt_flags;
	uint32_t srv_flags;
	char srv_challenge[8];
} ntlmssp_backend_t;

struct genhdr {
	char h_id[8];	/* "NTLMSSP" */
	uint32_t h_type;
};

struct sec_buf {
	uint16_t sb_length;
	uint16_t sb_maxlen;
	uint32_t sb_offset;
};

struct nego_hdr {
	char h_id[8];
	uint32_t h_type;
	uint32_t h_flags;
	/* workstation domain, name (place holders) */
	uint16_t ws_dom[4];
	uint16_t ws_name[4];
};

struct auth_hdr {
	char h_id[8];
	uint32_t h_type;
	struct sec_buf h_lm_resp;
	struct sec_buf h_nt_resp;
	struct sec_buf h_domain;
	struct sec_buf h_user;
	struct sec_buf h_wksta;
	struct sec_buf h_essn_key; /* encrypted session key */
	uint32_t h_flags;
	/* Version struct (optional) */
	/* MIC hash (optional) */
};

/* Allow turning these off for debugging, etc. */
int smbd_signing_enabled = 1;

int smbd_constant_challenge = 0;
static uint8_t constant_chal[8] = {
    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 };

static int smbd_ntlmssp_negotiate(authsvc_context_t *);
static int smbd_ntlmssp_authenticate(authsvc_context_t *);
static int encode_avpair_str(smb_msgbuf_t *, uint16_t, char *);
static int decode_secbuf_bin(smb_msgbuf_t *, struct sec_buf *, void **);
static int decode_secbuf_str(smb_msgbuf_t *, struct sec_buf *, char **);

/*
 * Initialize this context for NTLMSSP, if possible.
 */
int
smbd_ntlmssp_init(authsvc_context_t *ctx)
{
	ntlmssp_backend_t *be;

	be = malloc(sizeof (*be));
	if (be == 0)
		return (NT_STATUS_NO_MEMORY);
	bzero(be, sizeof (*be));
	be->expect_type = NTLMSSP_MSGTYPE_NEGOTIATE;
	ctx->ctx_backend = be;

	return (0);
}

void
smbd_ntlmssp_fini(authsvc_context_t *ctx)
{
	free(ctx->ctx_backend);
}

/*
 * Handle an auth message
 */
int
smbd_ntlmssp_work(authsvc_context_t *ctx)
{
	struct genhdr *ihdr = ctx->ctx_ibodybuf;
	ntlmssp_backend_t *be = ctx->ctx_backend;
	uint32_t mtype;
	int rc;

	if (ctx->ctx_ibodylen < sizeof (*ihdr))
		return (NT_STATUS_INVALID_PARAMETER);

	if (bcmp(ihdr->h_id, "NTLMSSP", 8))
		return (NT_STATUS_INVALID_PARAMETER);
	mtype = letohl(ihdr->h_type);
	if (mtype != be->expect_type)
		return (NT_STATUS_INVALID_PARAMETER);

	switch (mtype) {
	case NTLMSSP_MSGTYPE_NEGOTIATE:
		ctx->ctx_orawtype = LSA_MTYPE_ES_CONT;
		rc = smbd_ntlmssp_negotiate(ctx);
		break;
	case NTLMSSP_MSGTYPE_AUTHENTICATE:
		ctx->ctx_orawtype = LSA_MTYPE_ES_DONE;
		rc = smbd_ntlmssp_authenticate(ctx);
		break;

	default:
	case NTLMSSP_MSGTYPE_CHALLENGE:
		/* Sent by servers, not received. */
		rc = NT_STATUS_INVALID_PARAMETER;
		break;
	}

	return (rc);
}

#if (MAXHOSTNAMELEN < NETBIOS_NAME_SZ)
#error "MAXHOSTNAMELEN < NETBIOS_NAME_SZ"
#endif

/*
 * Handle an NTLMSSP_MSGTYPE_NEGOTIATE message, and reply
 * with an NTLMSSP_MSGTYPE_CHALLENGE message.
 * See: [MS-NLMP] 2.2.1.1, 3.2.5.1.1
 */
static int
smbd_ntlmssp_negotiate(authsvc_context_t *ctx)
{
	char tmp_name[MAXHOSTNAMELEN];
	ntlmssp_backend_t *be = ctx->ctx_backend;
	struct nego_hdr *ihdr = ctx->ctx_ibodybuf;
	smb_msgbuf_t mb;
	uint8_t *save_scan;
	int secmode;
	int mbflags;
	int rc;
	size_t var_start, var_end;
	uint16_t var_size;

	if (ctx->ctx_ibodylen < sizeof (*ihdr))
		return (NT_STATUS_INVALID_PARAMETER);
	be->clnt_flags = letohl(ihdr->h_flags);

	/*
	 * Looks like we can ignore ws_dom, ws_name.
	 * Otherwise would parse those here.
	 */

	secmode = smb_config_get_secmode();
	if (smbd_constant_challenge) {
		(void) memcpy(be->srv_challenge, constant_chal,
		    sizeof (be->srv_challenge));
	} else {
		randomize(be->srv_challenge, sizeof (be->srv_challenge));
	}

	/*
	 * Compute srv_flags
	 */
	be->srv_flags =
	    NTLMSSP_REQUEST_TARGET |
	    NTLMSSP_NEGOTIATE_NTLM |
	    NTLMSSP_NEGOTIATE_TARGET_INFO;
	be->srv_flags |= be->clnt_flags & (
	    NTLMSSP_NEGOTIATE_NTLM2 |
	    NTLMSSP_NEGOTIATE_128 |
	    NTLMSSP_NEGOTIATE_KEY_EXCH |
	    NTLMSSP_NEGOTIATE_56);

	if (smbd_signing_enabled) {
		be->srv_flags |= be->clnt_flags & (
		    NTLMSSP_NEGOTIATE_SIGN |
		    NTLMSSP_NEGOTIATE_SEAL |
		    NTLMSSP_NEGOTIATE_ALWAYS_SIGN);
	}

	if (be->clnt_flags & NTLMSSP_NEGOTIATE_UNICODE)
		be->srv_flags |= NTLMSSP_NEGOTIATE_UNICODE;
	else if (be->clnt_flags & NTLMSSP_NEGOTIATE_OEM)
		be->srv_flags |= NTLMSSP_NEGOTIATE_OEM;

	/* LM Key is mutually exclusive with NTLM2 */
	if ((be->srv_flags & NTLMSSP_NEGOTIATE_NTLM2) == 0 &&
	    (be->clnt_flags & NTLMSSP_NEGOTIATE_LM_KEY) != 0)
		be->srv_flags |= NTLMSSP_NEGOTIATE_LM_KEY;

	/* Get our "target name" */
	if (secmode == SMB_SECMODE_DOMAIN) {
		be->srv_flags |= NTLMSSP_TARGET_TYPE_DOMAIN;
		rc = smb_getdomainname(tmp_name, NETBIOS_NAME_SZ);
	} else {
		be->srv_flags |= NTLMSSP_TARGET_TYPE_SERVER;
		rc = smb_getnetbiosname(tmp_name, NETBIOS_NAME_SZ);
	}
	if (rc)
		goto errout;

	/*
	 * Build the NTLMSSP_MSGTYPE_CHALLENGE message.
	 */
	mbflags = SMB_MSGBUF_NOTERM;
	if (be->srv_flags & NTLMSSP_NEGOTIATE_UNICODE)
		mbflags |= SMB_MSGBUF_UNICODE;
	smb_msgbuf_init(&mb, ctx->ctx_obodybuf, ctx->ctx_obodylen, mbflags);

	/*
	 * Fixed size parts
	 */
	rc = smb_msgbuf_encode(
	    &mb, "8clwwll8cllwwl",	/* offset, name (fmt) */
	    "NTLMSSP",			/* 0: signature (8c) */
	    NTLMSSP_MSGTYPE_CHALLENGE,	/* 8: type	(l) */
	    0, 0, 0,	/* filled later:   12: target name (wwl) */
	    be->srv_flags,		/* 20: flags	(l) */
	    be->srv_challenge,		/* 24:		(8c) */
	    0, 0,			/* 32: reserved (ll) */
	    0, 0, 0);	/* filled later:   40: target info (wwl) */
#define	TARGET_NAME_OFFSET	12
#define	TARGET_INFO_OFFSET	40
	if (rc < 0)
		goto errout;

	/*
	 * Variable length parts.
	 *
	 * Target name
	 */
	var_start = smb_msgbuf_used(&mb);
	rc = smb_msgbuf_encode(&mb, "u", tmp_name);
	var_end = smb_msgbuf_used(&mb);
	var_size = (uint16_t)(var_end - var_start);
	if (rc < 0)
		goto errout;

	/* overwrite target name offset+lengths */
	save_scan = mb.scan;
	mb.scan = mb.base + TARGET_NAME_OFFSET;
	(void) smb_msgbuf_encode(&mb, "wwl", var_size, var_size, var_start);
	mb.scan = save_scan;

	/*
	 * Target info (AvPairList)
	 *
	 * These AV pairs are like our name/value pairs, but have
	 * numeric identifiers instead of names.  There are many
	 * of these, but we put only the four expected by Windows:
	 *	NetBIOS computer name
	 *	NetBIOS domain name
	 *	DNS computer name
	 *	DNS domain name
	 * Note that "domain" above (even "DNS domain") refers to
	 * the AD domain of which we're a member, which may be
	 * _different_ from the configured DNS domain.
	 *
	 * Also note that in "workgroup" mode (not a domain member)
	 * all "domain" fields should be set to the same values as
	 * the "computer" fields ("bare" host name, not FQDN).
	 */
	var_start = smb_msgbuf_used(&mb);

	/* NetBIOS Computer Name */
	if (smb_getnetbiosname(tmp_name, NETBIOS_NAME_SZ))
		goto errout;
	if (encode_avpair_str(&mb, MsvAvNbComputerName, tmp_name) < 0)
		goto errout;

	if (secmode != SMB_SECMODE_DOMAIN) {
		/*
		 * Workgroup mode.  Set all to hostname.
		 * tmp_name = netbios hostname from above.
		 */
		if (encode_avpair_str(&mb, MsvAvNbDomainName, tmp_name) < 0)
			goto errout;
		/*
		 * Want the bare computer name here (not FQDN).
		 */
		if (smb_gethostname(tmp_name, MAXHOSTNAMELEN, SMB_CASE_LOWER))
			goto errout;
		if (encode_avpair_str(&mb, MsvAvDnsComputerName, tmp_name) < 0)
			goto errout;
		if (encode_avpair_str(&mb, MsvAvDnsDomainName, tmp_name) < 0)
			goto errout;
	} else {
		/*
		 * Domain mode.  Use real host and domain values.
		 */

		/* NetBIOS Domain Name */
		if (smb_getdomainname(tmp_name, NETBIOS_NAME_SZ))
			goto errout;
		if (encode_avpair_str(&mb, MsvAvNbDomainName, tmp_name) < 0)
			goto errout;

		/* DNS Computer Name */
		if (smb_getfqhostname(tmp_name, MAXHOSTNAMELEN))
			goto errout;
		if (encode_avpair_str(&mb, MsvAvDnsComputerName, tmp_name) < 0)
			goto errout;

		/* DNS Domain Name */
		if (smb_getfqdomainname(tmp_name, MAXHOSTNAMELEN))
			goto errout;
		if (encode_avpair_str(&mb, MsvAvDnsDomainName, tmp_name) < 0)
			goto errout;
	}

	/* End marker */
	if (smb_msgbuf_encode(&mb, "ww", MsvAvEOL, 0) < 0)
		goto errout;
	var_end = smb_msgbuf_used(&mb);
	var_size = (uint16_t)(var_end - var_start);

	/* overwrite target  offset+lengths */
	save_scan = mb.scan;
	mb.scan = mb.base + TARGET_INFO_OFFSET;
	(void) smb_msgbuf_encode(&mb, "wwl", var_size, var_size, var_start);
	mb.scan = save_scan;

	ctx->ctx_obodylen = smb_msgbuf_used(&mb);
	smb_msgbuf_term(&mb);

	be->expect_type = NTLMSSP_MSGTYPE_AUTHENTICATE;

	return (0);

errout:
	smb_msgbuf_term(&mb);
	return (NT_STATUS_INTERNAL_ERROR);
}

static int
encode_avpair_str(smb_msgbuf_t *mb, uint16_t AvId, char *name)
{
	int rc;
	uint16_t len;

	len = smb_wcequiv_strlen(name);
	rc = smb_msgbuf_encode(mb, "wwU", AvId, len, name);
	return (rc);
}

/*
 * Handle an NTLMSSP_MSGTYPE_AUTHENTICATE message.
 * See: [MS-NLMP] 2.2.1.3, 3.2.5.1.2
 */
static int
smbd_ntlmssp_authenticate(authsvc_context_t *ctx)
{
	struct auth_hdr hdr;
	smb_msgbuf_t mb;
	smb_logon_t	user_info;
	smb_token_t	*token = NULL;
	ntlmssp_backend_t *be = ctx->ctx_backend;
	void *lm_resp;
	void *nt_resp;
	char *domain;
	char *user;
	char *wksta;
	void *essn_key;	/* encrypted session key (optional) */
	int mbflags;
	uint_t status = NT_STATUS_INTERNAL_ERROR;
	char combined_challenge[SMBAUTH_CHAL_SZ];
	unsigned char kxkey[SMBAUTH_HASH_SZ];
	boolean_t ntlm_v1x = B_FALSE;

	bzero(&user_info, sizeof (user_info));

	/*
	 * Parse the NTLMSSP_MSGTYPE_AUTHENTICATE message.
	 */
	if (ctx->ctx_ibodylen < sizeof (hdr))
		return (NT_STATUS_INVALID_PARAMETER);
	mbflags = SMB_MSGBUF_NOTERM;
	if (be->srv_flags & NTLMSSP_NEGOTIATE_UNICODE)
		mbflags |= SMB_MSGBUF_UNICODE;
	smb_msgbuf_init(&mb, ctx->ctx_ibodybuf, ctx->ctx_ibodylen, mbflags);
	bzero(&hdr, sizeof (hdr));

	if (smb_msgbuf_decode(&mb, "12.") < 0)
		goto errout;
	if (decode_secbuf_bin(&mb, &hdr.h_lm_resp, &lm_resp) < 0)
		goto errout;
	if (decode_secbuf_bin(&mb, &hdr.h_nt_resp, &nt_resp) < 0)
		goto errout;
	if (decode_secbuf_str(&mb, &hdr.h_domain, &domain) < 0)
		goto errout;
	if (decode_secbuf_str(&mb, &hdr.h_user, &user) < 0)
		goto errout;
	if (decode_secbuf_str(&mb, &hdr.h_wksta, &wksta) < 0)
		goto errout;
	if (decode_secbuf_bin(&mb, &hdr.h_essn_key, &essn_key) < 0)
		goto errout;
	if (smb_msgbuf_decode(&mb, "l", &be->clnt_flags) < 0)
		goto errout;

	if (be->clnt_flags & NTLMSSP_NEGOTIATE_KEY_EXCH) {
		if (hdr.h_essn_key.sb_length < 16 || essn_key == NULL)
			goto errout;
	}

	user_info.lg_level = NETR_NETWORK_LOGON;
	user_info.lg_flags = 0;

	user_info.lg_ntlm_flags = be->clnt_flags;
	user_info.lg_username = (user) ? user : "";
	user_info.lg_domain = (domain) ? domain : "";
	user_info.lg_workstation = (wksta) ? wksta : "";

	user_info.lg_clnt_ipaddr =
	    ctx->ctx_clinfo.lci_clnt_ipaddr;
	user_info.lg_local_port = 445;

	user_info.lg_challenge_key.len = SMBAUTH_CHAL_SZ;
	user_info.lg_challenge_key.val = (uint8_t *)be->srv_challenge;

	user_info.lg_nt_password.len = hdr.h_nt_resp.sb_length;
	user_info.lg_nt_password.val = nt_resp;

	user_info.lg_lm_password.len = hdr.h_lm_resp.sb_length;
	user_info.lg_lm_password.val = lm_resp;

	user_info.lg_native_os = ctx->ctx_clinfo.lci_native_os;
	user_info.lg_native_lm = ctx->ctx_clinfo.lci_native_lm;

	/*
	 * If we're doing extended session security, the challenge
	 * this OWF was computed with is different. [MS-NLMP 3.3.1]
	 * It's: MD5(concat(ServerChallenge,ClientChallenge))
	 * where the ClientChallenge is in the LM resp. field.
	 */
	if (user_info.lg_nt_password.len == SMBAUTH_LM_RESP_SZ &&
	    user_info.lg_lm_password.len >= SMBAUTH_CHAL_SZ &&
	    (be->clnt_flags & NTLMSSP_NEGOTIATE_NTLM2) != 0) {
		smb_auth_ntlm2_mkchallenge(combined_challenge,
		    be->srv_challenge, lm_resp);
		user_info.lg_challenge_key.val =
		    (uint8_t *)combined_challenge;
		user_info.lg_lm_password.len = 0;
		ntlm_v1x = B_TRUE;
	}

	/*
	 * This (indirectly) calls smb_auth_validate() to
	 * check that the client gave us a valid hash.
	 */
	token = smbd_user_auth_logon(&user_info);
	if (token == NULL) {
		status = user_info.lg_status;
		if (status == 0) /* should not happen */
			status = NT_STATUS_INTERNAL_ERROR;
		goto errout;
	}

	if (token->tkn_ssnkey.val != NULL &&
	    token->tkn_ssnkey.len == SMBAUTH_HASH_SZ) {

		/*
		 * At this point, token->tkn_session_key is the
		 * "Session Base Key" [MS-NLMP] 3.2.5.1.2
		 * Compute the final session key.  First need the
		 * "Key Exchange Key" [MS-NLMP] 3.4.5.1
		 */
		if (ntlm_v1x) {
			smb_auth_ntlm2_kxkey(kxkey,
			    be->srv_challenge, lm_resp,
			    token->tkn_ssnkey.val);
		} else {
			/* KXKEY is the Session Base Key. */
			(void) memcpy(kxkey, token->tkn_ssnkey.val,
			    SMBAUTH_HASH_SZ);
		}

		/*
		 * If the client give us an encrypted session key,
		 * decrypt it (RC4) using the "key exchange key".
		 */
		if (be->clnt_flags & NTLMSSP_NEGOTIATE_KEY_EXCH) {
			/* RC4 args: result, key, data */
			(void) smb_auth_RC4(token->tkn_ssnkey.val,
			    SMBAUTH_HASH_SZ, kxkey, SMBAUTH_HASH_SZ,
			    essn_key, hdr.h_essn_key.sb_length);
		} else {
			/* Final key is the KXKEY */
			(void) memcpy(token->tkn_ssnkey.val, kxkey,
			    SMBAUTH_HASH_SZ);
		}
	}

	ctx->ctx_token = token;
	ctx->ctx_obodylen = 0;

	smb_msgbuf_term(&mb);
	return (0);

errout:
	smb_msgbuf_term(&mb);
	return (status);
}

static int
decode_secbuf_bin(smb_msgbuf_t *mb, struct sec_buf *sb, void **binp)
{
	int rc;

	*binp = NULL;
	rc = smb_msgbuf_decode(
	    mb, "wwl",
	    &sb->sb_length,
	    &sb->sb_maxlen,
	    &sb->sb_offset);
	if (rc < 0)
		return (rc);

	if (sb->sb_offset > mb->max)
		return (SMB_MSGBUF_UNDERFLOW);
	if (sb->sb_length > (mb->max - sb->sb_offset))
		return (SMB_MSGBUF_UNDERFLOW);
	if (sb->sb_length == 0)
		return (rc);

	*binp = mb->base + sb->sb_offset;
	return (0);
}

static int
decode_secbuf_str(smb_msgbuf_t *mb, struct sec_buf *sb, char **cpp)
{
	uint8_t *save_scan;
	int rc;

	*cpp = NULL;
	rc = smb_msgbuf_decode(
	    mb, "wwl",
	    &sb->sb_length,
	    &sb->sb_maxlen,
	    &sb->sb_offset);
	if (rc < 0)
		return (rc);

	if (sb->sb_offset > mb->max)
		return (SMB_MSGBUF_UNDERFLOW);
	if (sb->sb_length > (mb->max - sb->sb_offset))
		return (SMB_MSGBUF_UNDERFLOW);
	if (sb->sb_length == 0)
		return (rc);

	save_scan = mb->scan;
	mb->scan = mb->base + sb->sb_offset;
	rc = smb_msgbuf_decode(mb, "#u", (int)sb->sb_length, cpp);
	mb->scan = save_scan;

	return (rc);
}
