From 244b0f9b621a6dbaa1bca7ff5bafb72918249de8 Mon Sep 17 00:00:00 2001 From: softScheck GmbH Date: Thu, 28 Jul 2016 13:52:52 +0200 Subject: [PATCH] Initial commit --- tddp-client/pyDes.py | 852 ++++++++++++++++++++++++++++++++++ tddp-client/tddp-client.py | 207 +++++++++ tplink-smarthome-commands.txt | 186 ++++++++ tplink-smarthome.lua | 88 ++++ tplink-smartplug.py | 99 ++++ 5 files changed, 1432 insertions(+) create mode 100644 tddp-client/pyDes.py create mode 100644 tddp-client/tddp-client.py create mode 100644 tplink-smarthome-commands.txt create mode 100644 tplink-smarthome.lua create mode 100644 tplink-smartplug.py diff --git a/tddp-client/pyDes.py b/tddp-client/pyDes.py new file mode 100644 index 0000000..c7f8a79 --- /dev/null +++ b/tddp-client/pyDes.py @@ -0,0 +1,852 @@ +############################################################################# +# Documentation # +############################################################################# + +# Author: Todd Whiteman +# Date: 28th April, 2010 +# Version: 2.0.1 +# License: MIT +# Homepage: http://twhiteman.netfirms.com/des.html +# +# This is a pure python implementation of the DES encryption algorithm. +# It's pure python to avoid portability issues, since most DES +# implementations are programmed in C (for performance reasons). +# +# Triple DES class is also implemented, utilizing the DES base. Triple DES +# is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. +# +# See the README.txt that should come with this python module for the +# implementation methods used. +# +# Thanks to: +# * David Broadwell for ideas, comments and suggestions. +# * Mario Wolff for pointing out and debugging some triple des CBC errors. +# * Santiago Palladino for providing the PKCS5 padding technique. +# * Shaya for correcting the PAD_PKCS5 triple des CBC errors. +# +"""A pure python implementation of the DES and TRIPLE DES encryption algorithms. + +Class initialization +-------------------- +pyDes.des(key, [mode], [IV], [pad], [padmode]) +pyDes.triple_des(key, [mode], [IV], [pad], [padmode]) + +key -> Bytes containing the encryption key. 8 bytes for DES, 16 or 24 bytes + for Triple DES +mode -> Optional argument for encryption type, can be either + pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) +IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Length must be 8 bytes. +pad -> Optional argument, set the pad character (PAD_NORMAL) to use during + all encrypt/decrypt operations done with this instance. +padmode -> Optional argument, set the padding mode (PAD_NORMAL or PAD_PKCS5) + to use during all encrypt/decrypt operations done with this instance. + +I recommend to use PAD_PKCS5 padding, as then you never need to worry about any +padding issues, as the padding can be removed unambiguously upon decrypting +data that was encrypted using PAD_PKCS5 padmode. + +Common methods +-------------- +encrypt(data, [pad], [padmode]) +decrypt(data, [pad], [padmode]) + +data -> Bytes to be encrypted/decrypted +pad -> Optional argument. Only when using padmode of PAD_NORMAL. For + encryption, adds this characters to the end of the data block when + data is not a multiple of 8 bytes. For decryption, will remove the + trailing characters that match this pad character from the last 8 + bytes of the unencrypted data block. +padmode -> Optional argument, set the padding mode, must be one of PAD_NORMAL + or PAD_PKCS5). Defaults to PAD_NORMAL. + + +Example +------- +from pyDes import * + +data = "Please encrypt my data" +k = des("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +# For Python3, you'll need to use bytes, i.e.: +# data = b"Please encrypt my data" +# k = des(b"DESCRYPT", CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +d = k.encrypt(data) +print "Encrypted: %r" % d +print "Decrypted: %r" % k.decrypt(d) +assert k.decrypt(d, padmode=PAD_PKCS5) == data + + +See the module source (pyDes.py) for more examples of use. +You can also run the pyDes.py file without and arguments to see a simple test. + +Note: This code was not written for high-end systems needing a fast + implementation, but rather a handy portable solution with small usage. + +""" + +import sys + +# _pythonMajorVersion is used to handle Python2 and Python3 differences. +_pythonMajorVersion = sys.version_info[0] + +# Modes of crypting / cyphering +ECB = 0 +CBC = 1 + +# Modes of padding +PAD_NORMAL = 1 +PAD_PKCS5 = 2 + +# PAD_PKCS5: is a method that will unambiguously remove all padding +# characters after decryption, when originally encrypted with +# this padding mode. +# For a good description of the PKCS5 padding technique, see: +# http://www.faqs.org/rfcs/rfc1423.html + +# The base class shared by des and triple des. +class _baseDes(object): + def __init__(self, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + if IV: + IV = self._guardAgainstUnicode(IV) + if pad: + pad = self._guardAgainstUnicode(pad) + self.block_size = 8 + # Sanity checking of arguments. + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if IV and len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + + # Set the passed in variables + self._mode = mode + self._iv = IV + self._padding = pad + self._padmode = padmode + + def getKey(self): + """getKey() -> bytes""" + return self.__key + + def setKey(self, key): + """Will set the crypting key for this object.""" + key = self._guardAgainstUnicode(key) + self.__key = key + + def getMode(self): + """getMode() -> pyDes.ECB or pyDes.CBC""" + return self._mode + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + self._mode = mode + + def getPadding(self): + """getPadding() -> bytes of length 1. Padding character.""" + return self._padding + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + if pad is not None: + pad = self._guardAgainstUnicode(pad) + self._padding = pad + + def getPadMode(self): + """getPadMode() -> pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + return self._padmode + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + self._padmode = mode + + def getIV(self): + """getIV() -> bytes""" + return self._iv + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + if not IV or len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + IV = self._guardAgainstUnicode(IV) + self._iv = IV + + def _padData(self, data, pad, padmode): + # Pad data depending on the mode + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + + if padmode == PAD_NORMAL: + if len(data) % self.block_size == 0: + # No padding required. + return data + + if not pad: + # Get the default padding. + pad = self.getPadding() + if not pad: + raise ValueError("Data must be a multiple of " + str(self.block_size) + " bytes in length. Use padmode=PAD_PKCS5 or set the pad character.") + data += (self.block_size - (len(data) % self.block_size)) * pad + + elif padmode == PAD_PKCS5: + pad_len = 8 - (len(data) % self.block_size) + if _pythonMajorVersion < 3: + data += pad_len * chr(pad_len) + else: + data += bytes([pad_len] * pad_len) + + return data + + def _unpadData(self, data, pad, padmode): + # Unpad data depending on the mode. + if not data: + return data + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + + if padmode == PAD_NORMAL: + if not pad: + # Get the default padding. + pad = self.getPadding() + if pad: + data = data[:-self.block_size] + \ + data[-self.block_size:].rstrip(pad) + + elif padmode == PAD_PKCS5: + if _pythonMajorVersion < 3: + pad_len = ord(data[-1]) + else: + pad_len = data[-1] + data = data[:-pad_len] + + return data + + def _guardAgainstUnicode(self, data): + # Only accept byte strings or ascii unicode values, otherwise + # there is no way to correctly decode the data into bytes. + if _pythonMajorVersion < 3: + if isinstance(data, unicode): + raise ValueError("pyDes can only work with bytes, not Unicode strings.") + else: + if isinstance(data, str): + # Only accept ascii unicode values. + try: + return data.encode('ascii') + except UnicodeEncodeError: + pass + raise ValueError("pyDes can only work with encoded strings, not Unicode.") + return data + +############################################################################# +# DES # +############################################################################# +class des(_baseDes): + """DES encryption/decrytpion class + + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key,[mode], [IV]) + + key -> Bytes containing the encryption key, must be exactly 8 bytes + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrypt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrypt operations done + with this instance. + """ + + + # Permutation and translation tables for DES + __pc1 = [56, 48, 40, 32, 24, 16, 8, + 0, 57, 49, 41, 33, 25, 17, + 9, 1, 58, 50, 42, 34, 26, + 18, 10, 2, 59, 51, 43, 35, + 62, 54, 46, 38, 30, 22, 14, + 6, 61, 53, 45, 37, 29, 21, + 13, 5, 60, 52, 44, 36, 28, + 20, 12, 4, 27, 19, 11, 3 + ] + + # number left rotations of pc1 + __left_rotations = [ + 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 + ] + + # permuted choice key (table 2) + __pc2 = [ + 13, 16, 10, 23, 0, 4, + 2, 27, 14, 5, 20, 9, + 22, 18, 11, 3, 25, 7, + 15, 6, 26, 19, 12, 1, + 40, 51, 30, 36, 46, 54, + 29, 39, 50, 44, 32, 47, + 43, 48, 38, 55, 33, 52, + 45, 41, 49, 35, 28, 31 + ] + + # initial permutation IP + __ip = [57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, + 56, 48, 40, 32, 24, 16, 8, 0, + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6 + ] + + # Expansion table for turning 32 bit blocks into 48 bits + __expansion_table = [ + 31, 0, 1, 2, 3, 4, + 3, 4, 5, 6, 7, 8, + 7, 8, 9, 10, 11, 12, + 11, 12, 13, 14, 15, 16, + 15, 16, 17, 18, 19, 20, + 19, 20, 21, 22, 23, 24, + 23, 24, 25, 26, 27, 28, + 27, 28, 29, 30, 31, 0 + ] + + # The (in)famous S-boxes + __sbox = [ + # S1 + [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], + + # S2 + [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], + + # S3 + [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], + + # S4 + [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], + + # S5 + [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], + + # S6 + [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], + + # S7 + [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], + + # S8 + [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], + ] + + + # 32-bit permutation function P used on the output of the S-boxes + __p = [ + 15, 6, 19, 20, 28, 11, + 27, 16, 0, 14, 22, 25, + 4, 17, 30, 9, 1, 7, + 23,13, 31, 26, 2, 8, + 18, 12, 29, 5, 21, 10, + 3, 24 + ] + + # final permutation IP^-1 + __fp = [ + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, + 32, 0, 40, 8, 48, 16, 56, 24 + ] + + # Type of crypting being done + ENCRYPT = 0x00 + DECRYPT = 0x01 + + # Initialisation + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + # Sanity checking of arguments. + if len(key) != 8: + raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") + _baseDes.__init__(self, mode, IV, pad, padmode) + self.key_size = 8 + + self.L = [] + self.R = [] + self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) + self.final = [] + + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Must be 8 bytes.""" + _baseDes.setKey(self, key) + self.__create_sub_keys() + + def __String_to_BitList(self, data): + """Turn the string data, into a list of bits (1, 0)'s""" + if _pythonMajorVersion < 3: + # Turn the strings into integers. Python 3 uses a bytes + # class, which already has this behaviour. + data = [ord(c) for c in data] + l = len(data) * 8 + result = [0] * l + pos = 0 + for ch in data: + i = 7 + while i >= 0: + if ch & (1 << i) != 0: + result[pos] = 1 + else: + result[pos] = 0 + pos += 1 + i -= 1 + + return result + + def __BitList_to_String(self, data): + """Turn the list of bits -> data, into a string""" + result = [] + pos = 0 + c = 0 + while pos < len(data): + c += data[pos] << (7 - (pos % 8)) + if (pos % 8) == 7: + result.append(c) + c = 0 + pos += 1 + + if _pythonMajorVersion < 3: + return ''.join([ chr(c) for c in result ]) + else: + return bytes(result) + + def __permutate(self, table, block): + """Permutate this block with the specified table""" + return list(map(lambda x: block[x], table)) + + # Transform the secret key, so that it is ready for data processing + # Create the 16 subkeys, K[1] - K[16] + def __create_sub_keys(self): + """Create the 16 subkeys K[1] to K[16] from the given key""" + key = self.__permutate(des.__pc1, self.__String_to_BitList(self.getKey())) + i = 0 + # Split into Left and Right sections + self.L = key[:28] + self.R = key[28:] + while i < 16: + j = 0 + # Perform circular left shifts + while j < des.__left_rotations[i]: + self.L.append(self.L[0]) + del self.L[0] + + self.R.append(self.R[0]) + del self.R[0] + + j += 1 + + # Create one of the 16 subkeys through pc2 permutation + self.Kn[i] = self.__permutate(des.__pc2, self.L + self.R) + + i += 1 + + # Main part of the encryption algorithm, the number cruncher :) + def __des_crypt(self, block, crypt_type): + """Crypt the block of data through DES bit-manipulation""" + block = self.__permutate(des.__ip, block) + self.L = block[:32] + self.R = block[32:] + + # Encryption starts from Kn[1] through to Kn[16] + if crypt_type == des.ENCRYPT: + iteration = 0 + iteration_adjustment = 1 + # Decryption starts from Kn[16] down to Kn[1] + else: + iteration = 15 + iteration_adjustment = -1 + + i = 0 + while i < 16: + # Make a copy of R[i-1], this will later become L[i] + tempR = self.R[:] + + # Permutate R[i - 1] to start creating R[i] + self.R = self.__permutate(des.__expansion_table, self.R) + + # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here + self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) + B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] + # Optimization: Replaced below commented code with above + #j = 0 + #B = [] + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.Kn[iteration][j] + # j += 1 + # if j % 6 == 0: + # B.append(self.R[j-6:j]) + + # Permutate B[1] to B[8] using the S-Boxes + j = 0 + Bn = [0] * 32 + pos = 0 + while j < 8: + # Work out the offsets + m = (B[j][0] << 1) + B[j][5] + n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] + + # Find the permutation value + v = des.__sbox[j][(m << 4) + n] + + # Turn value into bits, add it to result: Bn + Bn[pos] = (v & 8) >> 3 + Bn[pos + 1] = (v & 4) >> 2 + Bn[pos + 2] = (v & 2) >> 1 + Bn[pos + 3] = v & 1 + + pos += 4 + j += 1 + + # Permutate the concatination of B[1] to B[8] (Bn) + self.R = self.__permutate(des.__p, Bn) + + # Xor with L[i - 1] + self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) + # Optimization: This now replaces the below commented code + #j = 0 + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.L[j] + # j += 1 + + # L[i] becomes R[i - 1] + self.L = tempR + + i += 1 + iteration += iteration_adjustment + + # Final permutation of R[16]L[16] + self.final = self.__permutate(des.__fp, self.R + self.L) + return self.final + + + # Data to be encrypted/decrypted + def crypt(self, data, crypt_type): + """Crypt the data in blocks, running it through des_crypt()""" + + # Error check the data + if not data: + return '' + if len(data) % self.block_size != 0: + if crypt_type == des.DECRYPT: # Decryption must work on 8 byte blocks + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") + if not self.getPadding(): + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") + else: + data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() + # print "Len of data: %f" % (len(data) / self.block_size) + + if self.getMode() == CBC: + if self.getIV(): + iv = self.__String_to_BitList(self.getIV()) + else: + raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") + + # Split the data into blocks, crypting each one seperately + i = 0 + dict = {} + result = [] + #cached = 0 + #lines = 0 + while i < len(data): + # Test code for caching encryption results + #lines += 1 + #if dict.has_key(data[i:i+8]): + #print "Cached result for: %s" % data[i:i+8] + # cached += 1 + # result.append(dict[data[i:i+8]]) + # i += 8 + # continue + + block = self.__String_to_BitList(data[i:i+8]) + + # Xor with IV if using CBC mode + if self.getMode() == CBC: + if crypt_type == des.ENCRYPT: + block = list(map(lambda x, y: x ^ y, block, iv)) + #j = 0 + #while j < len(block): + # block[j] = block[j] ^ iv[j] + # j += 1 + + processed_block = self.__des_crypt(block, crypt_type) + + if crypt_type == des.DECRYPT: + processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) + #j = 0 + #while j < len(processed_block): + # processed_block[j] = processed_block[j] ^ iv[j] + # j += 1 + iv = block + else: + iv = processed_block + else: + processed_block = self.__des_crypt(block, crypt_type) + + + # Add the resulting crypted block to our list + #d = self.__BitList_to_String(processed_block) + #result.append(d) + result.append(self.__BitList_to_String(processed_block)) + #dict[data[i:i+8]] = d + i += 8 + + # print "Lines: %d, cached: %d" % (lines, cached) + + # Return the full crypted string + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self._padData(data, pad, padmode) + return self.crypt(data, des.ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after decrypting. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self.crypt(data, des.DECRYPT) + return self._unpadData(data, pad, padmode) + + + +############################################################################# +# Triple DES # +############################################################################# +class triple_des(_baseDes): + """Triple DES encryption/decrytpion class + + This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or + the DES-EDE2 (when a 16 byte key is supplied) encryption methods. + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key, [mode], [IV]) + + key -> Bytes containing the encryption key, must be either 16 or + 24 bytes long + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrypt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrypt operations done + with this instance. + """ + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + _baseDes.__init__(self, mode, IV, pad, padmode) + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Either 16 or 24 bytes long.""" + self.key_size = 24 # Use DES-EDE3 mode + if len(key) != self.key_size: + if len(key) == 16: # Use DES-EDE2 mode + self.key_size = 16 + else: + raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") + if self.getMode() == CBC: + if not self.getIV(): + # Use the first 8 bytes of the key + self._iv = key[:self.block_size] + if len(self.getIV()) != self.block_size: + raise ValueError("Invalid IV, must be 8 bytes in length") + self.__key1 = des(key[:8], self._mode, self._iv, + self._padding, self._padmode) + self.__key2 = des(key[8:16], self._mode, self._iv, + self._padding, self._padmode) + if self.key_size == 16: + self.__key3 = self.__key1 + else: + self.__key3 = des(key[16:], self._mode, self._iv, + self._padding, self._padmode) + _baseDes.setKey(self, key) + + # Override setter methods to work on all 3 keys. + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + _baseDes.setMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setMode(mode) + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + _baseDes.setPadding(self, pad) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadding(pad) + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + _baseDes.setPadMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadMode(mode) + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + _baseDes.setIV(self, IV) + for key in (self.__key1, self.__key2, self.__key3): + key.setIV(IV) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + # Pad the data accordingly. + data = self._padData(data, pad, padmode) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + block = self.__key1.crypt(data[i:i+8], ENCRYPT) + block = self.__key2.crypt(block, DECRYPT) + block = self.__key3.crypt(block, ENCRYPT) + self.__key1.setIV(block) + self.__key2.setIV(block) + self.__key3.setIV(block) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + else: + data = self.__key1.crypt(data, ENCRYPT) + data = self.__key2.crypt(data, DECRYPT) + return self.__key3.crypt(data, ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after + decrypting, no pad character is required for PAD_PKCS5. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + iv = data[i:i+8] + block = self.__key3.crypt(iv, DECRYPT) + block = self.__key2.crypt(block, ENCRYPT) + block = self.__key1.crypt(block, DECRYPT) + self.__key1.setIV(iv) + self.__key2.setIV(iv) + self.__key3.setIV(iv) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + data = ''.join(result) + else: + data = bytes.fromhex('').join(result) + else: + data = self.__key3.crypt(data, DECRYPT) + data = self.__key2.crypt(data, ENCRYPT) + data = self.__key1.crypt(data, DECRYPT) + return self._unpadData(data, pad, padmode) diff --git a/tddp-client/tddp-client.py b/tddp-client/tddp-client.py new file mode 100644 index 0000000..059b3d6 --- /dev/null +++ b/tddp-client/tddp-client.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# +# TP-Link Device Debug Protocol (TDDP) v2 Client +# Based on https://www.google.com/patents/CN102096654A?cl=en +# +# HIGHLY EXPERIMENTAL and untested! +# The protocol is available on all kinds of TP-Link devices such as routers, cameras, smart plugs etc. +# +# by Lubomir Stroetmann +# Copyright 2016 softScheck GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# + +from pyDes import * +import hashlib +import argparse +import socket +import struct +import binascii + +version = 0.1 + +# Default username and password +username = "admin" +password = "admin" + +# Check if IP is valid +def validIP(ip): + try: + socket.inet_pton(socket.AF_INET, ip) + except socket.error: + parser.error("Invalid IP Address.") + return ip + +# Parse commandline arguments +parser = argparse.ArgumentParser(description="Experimental TP-Link TDDPv2 Client v" + str(version)) +parser.add_argument("-v", "--verbose", help="Verbose mode", action="store_true") +parser.add_argument("-t", "--target", metavar="", required=True, help="Target IP Address", type=validIP) +parser.add_argument("-u", "--username", metavar="", help="Username (default: admin)") +parser.add_argument("-p", "--password", metavar="", help="Password (default: admin)") + + +# We only allow three different CMD_SPE_OPR commands that read out values from the target device +# Other values can potentially be write commands and modify the device +commands = {'test1':"0A", 'test2':"12", 'test3':"14"} +parser.add_argument("-c", "--command", metavar="", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands) +args = parser.parse_args() + +# Set Target IP, username and password to calculate DES decryption key for data and command to execute +ip = args.target +if args.username: + username = args.username +if args.password: + password = args.password +if args.command is None: + cmd = "12" +else: + cmd = commands[args.command] + +# TDDP runs on UDP Port 1040 +# Response is sent to UDP Port 61000 +port_send = 1040 +port_receive = 61000 + + +# TDDP DES Key = MD5 of username and password concatenated +# Key is first 8 bytes only +tddp_key = hashlib.md5(username + password).hexdigest()[:16] +if args.verbose: + print "TDDP Key:\t", tddp_key, "(" + username + password + ")" + +## TDDP Header +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Ver | Type | Code | ReplyInfo | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | PktLength | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | PktID | SubType | Reserve | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | MD5 Digest[0-3] | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | MD5 Digest[4-7] | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | MD5 Digest[8-11] | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | MD5 Digest[12-15] | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +## TDDP Protocol Version +tddp_ver = "02" + +## Packet Type +# 0x01 SET_USR_CFG - set configuration information +# 0x02 GET_SYS_INF - get configuration information +# 0x03 CMD_SPE_OPR - special configuration commands +# 0x04 HEART_BEAT - the heartbeat package +tddp_type = "03" + +## Code Request Type +# 0x01 TDDP_REQUEST +# 0x02 TDDP_REPLY +tddp_code = "01" + +## Reply Info Status +# 0x00 REPLY_OK +# 0x02 ? +# 0x03 ? +# 0x09 REPLY_ERROR +# 0xFF ? +tddp_reply = "00" + +## Packet Length (not including header) +# 4 bytes +tddp_length = "00000000" + +## Packet ID +# 2 bytes +# supposed to be incremented +1 for each packet +tddp_id = "0001" + +# Subtype for CMD_SPE_OPR (Special Operations Command) +# Set to 0x00 for SET_USR_CFG and GET_SYS_INF +# +# Subtypes described in patent application, hex value unknown: +# CMD_SYS_OPR Router system operation, including: init, save, reboot, reset, clr dos +# CMD_AUTO_TEST MAC for writing operation, the user replies CMD_SYS_INIT broadcast packet +# CMD_CONFIG_MAC Factory settings MAC operations +# CMD_CANCEL_TEST Cancel automatic test, stop receiving broadcast packets +# CMD_GET_PROD_ID Get product ID +# CMD_SYS_INIT Initialize a router +# CMD_CONFIG_PIN Router PIN code +# +# Subtypes that seem to work for a HS-110 Smart Plug: +# 0x0A returns "ABCD0110" +# 0x12 returns the deviceID +# 0x14 returns the hwID +# 0x06 changes MAC +# 0x13 changes deviceID +# 0x15 changes deviceID +tddp_subtype = cmd + +# Reserved +tddp_reserved = "00" + +# Digest 0-15 (32char/128bit/16byte) +# MD5 digest of entire packet +# Set to 0 initially for building the digest, then overwrite with result +tddp_digest = "%0.32X" % 00 + +# TDDP Data +# Always pad with 0x00 to a length divisible by 8 +# We're not sending any data since we're only sending read commands +tddp_data = "" + +# Recalculate length if sending data +tddp_length = len(tddp_data)/2 +tddp_length = "%0.8X" % tddp_length + +## Encrypt data with key +key = des(binascii.unhexlify(tddp_key), ECB) +data = key.encrypt(binascii.unhexlify(tddp_data)) + +## Assemble packet +tddp_packet = "".join([tddp_ver, tddp_type, tddp_code, tddp_reply, tddp_length, tddp_id, tddp_subtype, tddp_reserved, tddp_digest, data.encode('hex')]) + +# Calculate MD5 +tddp_digest = hashlib.md5(binascii.unhexlify(tddp_packet)).hexdigest() +tddp_packet = tddp_packet[:24] + tddp_digest + tddp_packet[56:] + +# Send a request +sock_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock_send.sendto(binascii.unhexlify(tddp_packet), (ip, port_send)) +if args.verbose: + print "Raw Request:\t", tddp_packet +t = tddp_packet +print "Request Data:\tVersion", t[0:2], "Type", t[2:4], "Status", t[6:8], "Length", t[8:16], "ID", t[16:20], "Subtype", t[20:22] +sock_send.close() + +# Receive the reply +sock_receive = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock_receive.bind(('', port_receive)) +response, addr = sock_receive.recvfrom(1024) +r = response.encode('hex') +if args.verbose: + print "Raw Reply:\t", r +sock_receive.close() +print "Reply Data:\tVersion", r[0:2], "Type", r[2:4], "Status", r[6:8], "Length", r[8:16], "ID", r[16:20], "Subtype", r[20:22] + +# Take payload and decrypt using key +recv_data = r[56:] +if recv_data: + print "Decrypted:\t" + key.decrypt(binascii.unhexlify(recv_data)) + diff --git a/tplink-smarthome-commands.txt b/tplink-smarthome-commands.txt new file mode 100644 index 0000000..52ff048 --- /dev/null +++ b/tplink-smarthome-commands.txt @@ -0,0 +1,186 @@ +TP-Link Smart Home Protocol Command List +======================================== +(for TP-Link HS100 and HS110) + +System Commands +======================================== +Get System Info (Software & Hardware Versions, MAC, deviceID, hwID etc.) +{"system":{"get_sysinfo":null}} + +Reboot +{"system":{"reboot":{"delay":1}}} + +Reset (To Factory Settings) +{"system":{"reset":{"delay":1}}} + +Turn On +{"system":{"set_relay_state":{"state":1}}} + +Turn Off +{"system":{"set_relay_state":{"state":0}}} + +Turn Off Device LED (Night mode) +{"system":{"set_led_off":{"off":1}}} + +Set Device Alias +{"system":{"set_dev_alias":{"alias":"supercool plug"}}} + +Set MAC Address +{"system":{"set_mac_addr":{"mac":"50-C7-BF-01-02-03"}}} + +Set Device ID +{"system":{"set_device_id":{"deviceId":"0123456789ABCDEF0123456789ABCDEF01234567"}}} + +Set Hardware ID +{"system":{"set_hw_id":{"hwId":"0123456789ABCDEF0123456789ABCDEF"}}} + +Set Location +{"system":{"set_dev_location":{"longitude":6.9582814,"latitude":50.9412784}}} + +Perform uBoot Bootloader Check +{"system":{"test_check_uboot":null}} + +Get Device Icon +{"system":{"get_dev_icon":null}} + +Set Device Icon +{"system":{"set_dev_icon":{"icon":"xxxx","hash":"ABCD"}}} + +Set Test Mode (command only accepted coming from IP 192.168.1.100) +{"system":{"set_test_mode":{"enable":1}}} + +Download Firmware from URL +{"system":{"download_firmware":{"url":"http://...."}}} + +Get Download State +{"system":{"get_download_state":{}}} + +Flash Downloaded Firmware +{"system":{"flash_firmware":{}}} + +Check Config +{"system":{"check_new_config":null}} + + +WLAN Commands +======================================== +Scan for list of available APs +{"netif":{"get_scaninfo":{"refresh":1}}} + +Connect to AP with given SSID and Password +{"netif":{"set_stainfo":{"ssid":"WiFi","password":"secret","key_type":3}}} + + +Cloud Commands +======================================== +Get Cloud Info (Server, Username, Connection Status) +{"cnCloud":{"get_info":null}} + +Get Firmware List from Cloud Server +{"cnCloud":{"get_intl_fw_list":{}}} + +Set Server URL +{"cnCloud":{"set_server_url":{"server":"devs.tplinkcloud.com"}}} + +Connect with Cloud username & Password +{"cnCloud":{"bind":{"username":"your@email.com", "password":"secret"}}} + +Unregister Device from Cloud Account +{"cnCloud":{"unbind":null}} + + +Time Commands +======================================== +Get Time +{"time":{"get_time":null}} + +Get Timezone +{"time":{"get_timezone":null}} + +Set Timezone +{"time":{"set_timezone":{"year":2016,"month":1,"mday":1,"hour":10,"min":10,"sec":10,"index":42}}} + + +EMeter Energy Usage Statistics Commands +(for TP-Link HS110) +======================================== +Get Realtime Current and Voltage Reading +{"emeter":{"get_realtime":{}}} + +Get EMeter VGain and IGain Settings +{"emeter":{"get_vgain_igain":{}}} + +Set EMeter VGain and Igain +{"emeter":{"set_vgain_igain":{"vgain":13462,"igain":16835}}} + +Start EMeter Calibration +{"emeter":{"start_calibration":{"vtarget":13462,"itarget":16835}}} + +Get Daily Statistic for given Month +{"emeter":{"get_daystat":{"month":1,"year":2016}}} + +Get Montly Statistic for given Year +{"emeter":{""get_monthstat":{"year":2016}}} + +Erase All EMeter Statistics +{"emeter":{"erase_emeter_stat":null}} + + +Schedule Commands +(action to perform regularly on given weekdays) +======================================== +Get Next Scheduled Action +{"schedule":{"get_next_action":null}} + +Get Schedule Rules List +{"schedule":{"get_rules":null}} + +Add New Schedule Rule +{"schedule":{"add_rule":{"stime_opt":0,"wday":[1,0,0,1,1,0,0],"smin":1014,"enable":1,"repeat":1,"etime_opt":-1,"name":"lights on","eact":-1,"month":0,"sact":1,"year":0,"longitude":0,"day":0,"force":0,"latitude":0,"emin":0},"set_overall_enable":{"enable":1}}} + +Edit Schedule Rule with given ID +{"schedule":{"edit_rule":{"stime_opt":0,"wday":[1,0,0,1,1,0,0],"smin":1014,"enable":1,"repeat":1,"etime_opt":-1,"id":"4B44932DFC09780B554A740BC1798CBC","name":"lights on","eact":-1,"month":0,"sact":1,"year":0,"longitude":0,"day":0,"force":0,"latitude":0,"emin":0}}} + +Delete Schedule Rule with given ID +{"schedule":{"delete_rule":{"id":"4B44932DFC09780B554A740BC1798CBC"}}} + +Delete All Schedule Rules and Erase Statistics +{"schedule":{"delete_all_rules":null,"erase_runtime_stat":null}} + + +Countdown Rule Commands +(action to perform after number of seconds) +======================================== +Get Rule (only one allowed) +{"count_down":{"get_rules":null}} + +Add New Countdown Rule +{"count_down":{"add_rule":{"enable":1,"delay":1800,"act":1,"name":"turn on"}}} + +Edit Countdown Rule with given ID +{"count_down":{"edit_rule":{"enable":1,"id":"7C90311A1CD3227F25C6001D88F7FC13","delay":1800,"act":1,"name":"turn on"}}} + +Delete Countdown Rule with given ID +{"count_down":{"delete_rule":{"id":"7C90311A1CD3227F25C6001D88F7FC13"}}} + +Delete All Coundown Rules +{"count_down":{"delete_all_rules":null}} + + +Anti-Theft Rule Commands (aka Away Mode) +(period of time during which device will be randomly turned on and off to deter thieves) +======================================== +Get Anti-Theft Rules List +{"anti_theft":{"get_rules":null}} + +Add New Anti-Theft Rule +{"anti_theft":{"add_rule":{"stime_opt":0,"wday":[0,0,0,1,0,1,0],"smin":987,"enable":1,"frequency":5,"repeat":1,"etime_opt":0,"duration":2,"name":"test","lastfor":1,"month":0,"year":0,"longitude":0,"day":0,"latitude":0,"force":0,"emin":1047},"set_overall_enable":1}} + +Edit Anti-Theft Rule with given ID +{"anti_theft":{"edit_rule":{"stime_opt":0,"wday":[0,0,0,1,0,1,0],"smin":987,"enable":1,"frequency":5,"repeat":1,"etime_opt":0,"id":"E36B1F4466B135C1FD481F0B4BFC9C30","duration":2,"name":"test","lastfor":1,"month":0,"year":0,"longitude":0,"day":0,"latitude":0,"force":0,"emin":1047},"set_overall_enable":1}} + +Delete Anti-Theft Rule with given ID +{"anti_theft":{"delete_rule":{"id":"E36B1F4466B135C1FD481F0B4BFC9C30"}}} + +Delete All Anti-Theft Rules +"anti_theft":{"delete_all_rules":null}} diff --git a/tplink-smarthome.lua b/tplink-smarthome.lua new file mode 100644 index 0000000..595409b --- /dev/null +++ b/tplink-smarthome.lua @@ -0,0 +1,88 @@ +-- TP-Link Smart Home Protocol (Port 9999) Wireshark Dissector +-- For decrypting local network traffic between TP-Link +-- Smart Home Devices and the Kasa Smart Home App +-- +-- Install under: +-- (Windows) %APPDATA%\Wireshark\plugins\ +-- (Linux, Mac) $HOME/.wireshark/plugins +-- +-- by Lubomir Stroetmann +-- Copyright 2016 softScheck GmbH +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- + +-- Create TP-Link Smart Home protocol and its fields +p_tplink = Proto ("TPLink-SmartHome","TP-Link Smart Home Protocol") + +-- Dissector function +function p_tplink.dissector (buf, pkt, root) + -- Validate packet length + if buf:len() == 0 then return end + pkt.cols.protocol = p_tplink.name + + -- Decode data + local ascii = "" + local hex = "" + + -- Skip first 4 bytes (header) + start = 4 + endPosition = buf:len() - 1 + + -- Decryption key is -85 (256-85=171) + local key = 171 + + -- Decrypt Autokey XOR + -- Save results as ascii and hex + for index = start, endPosition do + local c = buf(index,1):uint() + -- XOR first byte with key + d = bit32.bxor(c,key) + -- Use byte as next key + key = c + + hex = hex .. string.format("%x", d) + -- Convert to printable characters + if d >= 0x20 and d <= 0x7E then + ascii = ascii .. string.format("%c", d) + else + -- Use dot for non-printable bytes + ascii = ascii .. "." + end + end + + + -- Create subtree + subtree = root:add(p_tplink, buf(0)) + + -- Add data to subtree + subtree:add(ascii) + -- Description of payload + subtree:append_text(" (decrypted)") + + -- Call JSON Dissector with decrypted data + local b = ByteArray.new(hex) + local tvb = ByteArray.tvb(b, "JSON TVB") + Dissector.get("json"):call(tvb, pkt, root) + +end + +-- Initialization routine +function p_tplink.init() +end + +-- Register a chained dissector for port 9999 +local tcp_dissector_table = DissectorTable.get("tcp.port") +dissector = tcp_dissector_table:get_dissector(9999) +tcp_dissector_table:add(9999, p_tplink) \ No newline at end of file diff --git a/tplink-smartplug.py b/tplink-smartplug.py new file mode 100644 index 0000000..3a2fde8 --- /dev/null +++ b/tplink-smartplug.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# TP-Link Wi-Fi Smart Plug Protocol Client +# For use with TP-Link HS-100 or HS-110 +# +# by Lubomir Stroetmann +# Copyright 2016 softScheck GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +import socket +import argparse + +version = 0.1 + +# Check if IP is valid +def validIP(ip): + try: + socket.inet_pton(socket.AF_INET, ip) + except socket.error: + parser.error("Invalid IP Address.") + return ip + +# Predefined Smart Plug Commands +# For a full list of commands, consult tplink_commands.txt +commands = {'info' : '{"system":{"get_sysinfo":{}}}', + 'on' : '{"system":{"set_relay_state":{"state":1}}}', + 'off' : '{"system":{"set_relay_state":{"state":0}}}', + 'cloudinfo': '{"cnCloud":{"get_info":{}}}', + 'wlanscan' : '{"netif":{"get_scaninfo":{"refresh":0}}}', + 'time' : '{"time":{"get_time":{}}}', + 'schedule' : '{"schedule":{"get_rules":{}}}', + 'countdown': '{"count_down":{"get_rules":{}}}', + 'antitheft': '{"anti_theft":{"get_rules":{}}}', + 'reboot' : '{"system":{"reboot":{"delay":1}}}', + 'reset' : '{"system":{"reset":{"delay":1}}}' +} + +# Encryption and Decryption of TP-Link Smart Home Protocol +# XOR Autokey Cipher with starting key = 171 +def encrypt(string): + key = 171 + result = "\0\0\0\0" + for i in string: + a = key ^ ord(i) + key = a + result += chr(a) + return result + +def decrypt(string): + key = 171 + result = "" + for i in string: + a = key ^ ord(i) + key = ord(i) + result += chr(a) + return result + +# Parse commandline arguments +parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client v" + str(version)) +parser.add_argument("-t", "--target", metavar="", required=True, help="Target IP Address", type=validIP) +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("-c", "--command", metavar="", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands) +group.add_argument("-j", "--json", metavar="", help="Full JSON string of command to send") +args = parser.parse_args() + +# Set target IP, port and command to send +ip = args.target +port = 9999 +if args.command is None: + cmd = args.json +else: + cmd = commands[args.command] + + + +# Send command and receive reply +try: + sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_tcp.connect((ip, port)) + sock_tcp.send(encrypt(cmd)) + data = sock_tcp.recv(2048) + sock_tcp.close() + + print "Sent: ", cmd + print "Received: ", decrypt(data[4:]) +except socket.error: + quit("Cound not connect to host " + ip + ":" + str(port))