/* packet-tplink-smarthome.c
 *
 * Routines for TP-Link Smart Home Protocol dissection
 *
 * Copyright 2020-2021, Fulko Hew <fulko.hew@gmail.com>
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

/*
 * TP-Link Smart Home Protocol (Port 9999) Wireshark Dissector
 * For decrypting local network traffic between TP-Link Smart Home Devices (such as a KP400)
 * and the Kasa Smart Home App (or equivalent)
 *
 * Protocol	Message
 *
 *		+--+--+--+--+--+--+--+--+--+--+
 *  UDP		| Autokey XOR'ed message ...  |
 *		+--+--+--+--+--+--+--+--+--+--+
 *
 *		+-------+-------+-------+-------+--+--+--+--+--+--+--+--+--+--+
 *  TCP		| Big-endian 32-bit byte count  + Autokey XOR'ed message ...  |
 *		+-------+-------+-------+-------+--+--+--+--+--+--+--+--+--+--+
 *
 * I.e. They are both the same except TCP is prefixed with a byte count.
 */

#include <config.h>
#include <epan/packet.h>
#include <epan/address.h>
#include <epan/conversation.h>
#include <epan/wmem_scopes.h>
#include "packet-tcp.h"

#define TPLINK_SMARTHOME_PORT	9999 /* Not IANA registered */
/* TP-Link Smart Home devices use this port on both TCP and UDP */
#define FRAME_HEADER_LEN	4			/* 4 bytes of TCP frame length header info */

	/* Prototypes */
	/* (Required to prevent [-Wmissing-prototypes] warnings */

void proto_reg_handoff_tplink_smarthome(void);
void proto_register_tplink_smarthome(void);

static dissector_handle_t tplink_smarthome_handle;
static dissector_handle_t tplink_smarthome_message_handle;

		/* Initialize the protocol and registered fields */

static int	proto_tplink_smarthome	= -1;
static gint	ett_tplink_smarthome	= -1;		/* Initialize the subtree pointers */

static int	hf_tplink_smarthome_Len	= -1;
static int	hf_tplink_smarthome_Msg	= -1;

static gboolean
test_tplink_smarthome(packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_)
{
	guint8		key = 171;
	guint8		c, d;
	if (tvb_captured_length_remaining(tvb, offset) < 2) {
		return FALSE;
	}

	/* The message is always JSON, so test the first two characters.
         * They must be {" or {}, as the protocol doesn't appear to
         * have whitespace.). */
	c = tvb_get_guint8(tvb, offset);
	d = c ^ key;
	if (d != '{') {
		return FALSE;
	}
	d = c ^ tvb_get_guint8(tvb, offset+1);
	if (d != '"' && d != '}') {
		return FALSE;
	}

	return TRUE;
}

static int
dissect_tplink_smarthome_message(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree,
		void *data _U_)
{
	proto_item	*ti;
	proto_tree	*tplink_smarthome_tree;
	gint8		start = 0;
	guint8		c, d;
	guint8		key = 171;
	gint32		len = tvb_captured_length(tvb);

	switch (pinfo->ptype) {                                                                 /* look at the IP port type */
	       case PT_UDP:
		       start = 0;
		       break;
	       case PT_TCP:
		       start = 4;
		       break;
	       default:
		       return 0;
	}

	if (!test_tplink_smarthome(pinfo, tvb, start, data)) {
		return 0;
	}

	col_set_str(pinfo->cinfo, COL_PROTOCOL, "TPLINK-SMARTHOME");				/* show the protocol name of what we're dissecting */
	col_clear(pinfo->cinfo, COL_INFO);							/* and clear anything that might be in the Info field on the UI */

	ti = proto_tree_add_item(tree, proto_tplink_smarthome, tvb, 0, -1, ENC_NA);		/* create display subtree for this protocol */
	tplink_smarthome_tree = proto_item_add_subtree(ti, ett_tplink_smarthome);		/* and add it to the display tree */

	if (pinfo->ptype == PT_TCP) {
		proto_tree_add_item(tplink_smarthome_tree, hf_tplink_smarthome_Len,
					tvb, 0, FRAME_HEADER_LEN, ENC_BIG_ENDIAN);		/* decode the 4 byte message length field pre-pended in a TCP message, */
	}
	gint	i_offset	= start;
	gint	o_offset	= 0;
	gint	decode_len	= len - start;
	char	*ascii_buffer	= (char *)wmem_alloc(pinfo->pool, 1 + len - start);		/* create a buffer for the decoded (JSON) message */

	for (; o_offset < decode_len; i_offset++, o_offset++) {					/* decrypt 'Autokey XOR' message (into ASCII) */
		c	= tvb_get_guint8(tvb, i_offset);
		d	= c ^ key;								/* XOR the byte with the key to get the decoded byte */
		key	= c;									/* then use that decoded byte as the value for the next key */
		*(ascii_buffer + o_offset) = g_ascii_isprint(d) ? d : '.';			/* buffer a printable version (for display and JSON decoding) */
	}
	*(ascii_buffer + o_offset) = '\0';

	char *mtype;										/* categorize the message's intent: */
	if	(pinfo->destport == TPLINK_SMARTHOME_PORT)	{ mtype = "Cmd"; }		/*	'Cmd' - if it's  TO  the TP_Link port */
	else if	(pinfo->srcport  == TPLINK_SMARTHOME_PORT)	{ mtype = "Rsp"; }		/*	'Rsp' - if it's FROM the TP_Link port */
	else							{ mtype = "Msg"; }		/* impossible... because we're registered on this port so src or dest must have matched */

	proto_tree_add_string_format(tplink_smarthome_tree, hf_tplink_smarthome_Msg, tvb,
					start, -1, ascii_buffer, "%s: %s", mtype, ascii_buffer);	    /* add the decrypted data to the subtree so you can 'expand' on it */

	tvbuff_t *next_tvb = tvb_new_child_real_data(tvb, (guint8 *)ascii_buffer, decode_len, decode_len);	/* create a new TVB and insert the decrypted ASCII string, and */
	add_new_data_source(pinfo, next_tvb, "JSON Message");					    	/* add it so you can click on this JSON entry and see the decoded buffer */
	call_dissector(find_dissector("json"), next_tvb, pinfo, ti);			    		/* and decode/dissect it as JSON so you can drill down into it as well */

	col_add_fstr(pinfo->cinfo, COL_INFO, "%s %s: %s",
		(pinfo->ptype == PT_UDP) ? "UDP" : "TCP",
		mtype, ascii_buffer);									/* add the decoded string to the INFO column for a quick and easy read */

	return tvb_captured_length(tvb);								/* finally return the amount of data this dissector was able to dissect */
}

static guint
get_tplink_smarthome_message_len(packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_)
{													/* the PDU size is... the value in the length field */
    return (guint)tvb_get_ntohl(tvb, offset) + FRAME_HEADER_LEN;					/* plus the 'size of' the length field itself */
}

static int
dissect_tplink_smarthome(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
{
	conversation_t *conv = find_or_create_conversation(pinfo);
	if (!conversation_get_proto_data(conv, proto_tplink_smarthome)) {
		if (!test_tplink_smarthome(pinfo, tvb, FRAME_HEADER_LEN, data)) {
			return 0;
		}
		conversation_add_proto_data(conv, proto_tplink_smarthome, GUINT_TO_POINTER(1));
	}
	tcp_dissect_pdus(tvb, pinfo, tree, TRUE, FRAME_HEADER_LEN,
		get_tplink_smarthome_message_len, dissect_tplink_smarthome_message, data);
	return tvb_captured_length(tvb);
}

	/* Register the protocol with Wireshark. */

void
proto_register_tplink_smarthome(void)
{
	static hf_register_info hf[] = {								/* setup list of header fields */
		{ &hf_tplink_smarthome_Len,
			{ "Len", "tplink_smarthome.len",
				FT_UINT32, BASE_DEC, NULL, 0,
				"Message Length", HFILL }
		},
		{ &hf_tplink_smarthome_Msg,
			{ "Msg", "tplink_smarthome.msg",
				FT_STRING, BASE_NONE, NULL, 0,
				"Message", HFILL }
		}
	};

	static gint *ett[] = {										/* setup protocol subtree array */
		&ett_tplink_smarthome
	};

	proto_tplink_smarthome = proto_register_protocol("TP-Link Smart Home Protocol",			/* register the protocol name and description */
			"TPLINK-SMARTHOME", "tplink-smarthome");
	tplink_smarthome_handle = register_dissector("tplink-smarthome",
			dissect_tplink_smarthome, proto_tplink_smarthome);
	tplink_smarthome_message_handle = register_dissector("tplink-smarthome-message",
			dissect_tplink_smarthome_message, proto_tplink_smarthome);

	proto_register_field_array(proto_tplink_smarthome, hf, array_length(hf));			/* register the header fields */
	proto_register_subtree_array(ett, array_length(ett));						/* and subtrees */
}

void
proto_reg_handoff_tplink_smarthome(void)
{

	dissector_add_uint_with_preference("tcp.port", TPLINK_SMARTHOME_PORT, tplink_smarthome_handle);
	dissector_add_uint_with_preference("udp.port", TPLINK_SMARTHOME_PORT, tplink_smarthome_message_handle);
}

/*
 * Editor modelines - https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 4
 * tab-width: 8
 * indent-tabs-mode: t
 * End:
 *
 * vi: set shiftwidth=4 tabstop=8 noexpandtab:
 * :indentSize=4:tabSize=8:noTabs=false:
 */
