commit 9550f9fb7cfb7ee17e4e2ba869a97c6ab07de909 Author: John Stephani Date: Sat Jan 10 16:53:07 2026 -0600 Yoink diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6946d37 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# TP-Link WiFi SmartPlug Client and Wireshark Dissector + +For the full story, see [Reverse Engineering the TP-Link +HS110](https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/) + +## tplink_smartplug.py + +A python client for the proprietary TP-Link Smart Home protocol to control +TP-Link HS100, HS110 and KP115 WiFi Smart Plugs. The SmartHome protocol runs on TCP +port 9999 and uses a trivial XOR autokey encryption that provides no security. + +There is no authentication mechanism and commands are accepted independent of +device state (configured/unconfigured). + +Commands are formatted using JSON, for example: + +`{"system":{"get_sysinfo":null}}` + +Instead of `null` we can also write `{}`. Commands can be nested, for example: + +`{"system":{"get_sysinfo":null},"time":{"get_time":null}}` + +A full list of commands is provided in +[tplink-smarthome-commands.txt](tplink-smarthome-commands.txt). + +### Usage + + `./tplink_smartplug.py -t [-c || -j ]` + +Provide the target IP using `-t` and a command to send using either `-c` or +`-j`. Commands for the `-c` flag: + +| Command | Description | +|---------------|---------------------------------------| +| antitheft | Lists configured antitheft rules | +| cloudinfo | Returns cloud connectivity info | +| countdown | Lists configured countdown rules | +| energy | Return realtime voltage/current/power | +| energy_reset | Reset energy meters | +| info | Returns device info | +| ledoff | Turn off the LED indicator | +| ledon | Turn on the LED indicator | +| off | Turns off the plug | +| on | Turns on the plug | +| reboot | Reboot the device | +| reset | Reset the device to factory settings | +| runtime_reset | erase runtime statistics | +| schedule | Lists configured schedule rules | +| time | Returns the system time | +| wlanscan | Scan for nearby access points | + +More advanced commands such as creating or editing rules can be issued using the +`-j` flag by providing the full JSON string for the command. Please consult +[tplink-smarthome-commands.txt](tplink-smarthome-commands.txt) for a +comprehensive list of commands. + +> [!TIP] +> For pretty printing the response, use the quiet parameter (-q) and pipe the +> output through `jq` For example: +> `./tplink_smartplug.py -t 192.168.178.49 -c info -q | jq` + +## Wireshark Dissector + +Wireshark dissector to decrypt TP-Link Smart Home Protocol packets (TCP port +9999). + +![ScreenShot](wireshark-dissector.png) + +> [!NOTE] +> If you have Wireshark > 3.5.0 you can use the built-in dissector. + +### Installation + +Copy [tplink-smarthome.lua](tplink-smarthome.lua) into: + +| OS | Installation Path | +| ----------- | ---------------------------------- | +| Windows | %APPDATA%\Wireshark\plugins | +| Linux/MacOS | $HOME/.local/lib/wireshark/plugins | + +## tddp-client.py + +A proof-of-concept python client to talk to a TP-Link device using the **TP-Link +Device Debug Protocol (TDDP)**. + +TDDP is implemented across a whole range of TP-Link devices including routers, +access points, cameras and smartplugs. TDDP can read and write a device's +configuration and issue special commands. UDP port 1040 is used to send +commands, replies come back on UDP port 61000. This client has been tested with +a TP-Link Archer C9 Wireless Router and a TP-Link HS-110 WiFi Smart Plug. + +TDDP is a binary protocol documented in patent +[CN102096654A](https://www.google.com/patents/CN102096654A?cl=en). + +Commands are issued by setting the appropriate values in the Type and SubType +header fields. Data is returned DES-encrypted and requires the username and +password of the device to decrypt. Likewise, configuration data to be written to +the device needs to be sent encrypted. The DES key is constructed by taking the +MD5 hash of username and password concatenated together, and then taking the +first 8 bytes of the MD5 hash. + +### Usage + +`./tddp-client.py -t -u username -p password -c 0A` + +Provide the target IP using -t. You can provide a username and password, +otherwise admin/admin is used as a default. They are necessary to decrypt the +data that is returned. + +Provide the command as a two-character hex string, e.g. -c 0A. What type of data +a command might read out will be different for various TP-Link devices. + +### Example + +Reading out the WAN link status on an Archer C9 in default configuration shows +the link is down (0): + +```text +./tddp-client.py -t 192.168.0.1 -c 0E +Request Data: Version 02 Type 03 Status 00 Length 00000000 ID 0001 Subtype 0e +Reply Data: Version 02 Type 03 Status 00 Length 00000018 ID 0001 Subtype 0e +Decrypted: wan_ph_link 1 0 +``` diff --git a/tddp-client/LICENSE.txt b/tddp-client/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/tddp-client/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/tddp-client/README.md b/tddp-client/README.md new file mode 100644 index 0000000..0873602 --- /dev/null +++ b/tddp-client/README.md @@ -0,0 +1,28 @@ +## TP-Link Device Debug Protocol (TDDP) Client ## + +A proof-of-concept python client to talk to a TP-Link device using the **TP-Link Device Debug Protocol (TDDP)**. + +TDDP is implemented across a whole range of TP-Link devices including routers, access points, cameras and smartplugs. +TDDP can read and write a device's configuration and issue special commands. UDP port 1040 is used to send commands, replies come back on UDP port 61000. This client has been tested with a TP-Link Archer C9 Wireless Router and a TP-Link HS-110 WiFi Smart Plug. + +TDDP is a binary protocol documented in patent [CN102096654A](https://www.google.com/patents/CN102096654A?cl=en). + +Commands are issued by setting the appropriate values in the Type and SubType header fields. +Data is returned DES-encrypted and requires the username and password of the device to decrypt. Likewise, configuration data to be written to the device needs to be sent encrypted. The DES key is constructed by taking the MD5 hash of username and password concatenated together, and then taking the first 8 bytes of the MD5 hash. + +#### Usage #### + + `./tddp-client.py -t -u username -p password -c 0A` + +Provide the target IP using -t. You can provide a username and password, otherwise admin/admin is used as a default. They are necessary to decrypt the data that is returned. + +Provide the command as a two-character hex string, e.g. -c 0A. What type of data a command might read out will be different for various TP-Link devices. + +#### Example #### +Reading out the WAN link status on an Archer C9 in default configuration shows the link is down (0): + ``` + ./tddp-client.py -t 192.168.0.1 -c 0E + Request Data: Version 02 Type 03 Status 00 Length 00000000 ID 0001 Subtype 0e + Reply Data: Version 02 Type 03 Status 00 Length 00000018 ID 0001 Subtype 0e + Decrypted: wan_ph_link 1 0 + ``` diff --git a/tddp-client/pyDes.py b/tddp-client/pyDes.py new file mode 100644 index 0000000..a297763 --- /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 100755 index 0000000..0ef05fe --- /dev/null +++ b/tddp-client/tddp_client.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 + +"""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. +""" + +import argparse +import hashlib +import logging +import socket +import string +from binascii import hexlify, unhexlify + +from pyDes import ECB, des + +version = 0.4 + + +def generate_tddp_key(username: str, password: str) -> str: + """Generate TDDP DES Key.""" + return hashlib.md5(username.encode() + password.encode()).hexdigest()[:16] + + +def build_tddp_packet(cmd: str, tddp_key: str) -> str: + """Build TDDP packet.""" + tddp_ver = "02" + tddp_type = "03" + tddp_code = "01" + tddp_reply = "00" + tddp_length = "0000002A" + tddp_id = "0001" + tddp_subtype = cmd + tddp_reserved = "00" + tddp_digest = f"{00:0032X}" + tddp_data = "" + + tddp_length = len(tddp_data) // 2 + tddp_length = f"{tddp_length:008X}" + + key = des(unhexlify(tddp_key), ECB) + data = key.encrypt(unhexlify(tddp_data)) + + tddp_packet = "".join( + [ + tddp_ver, + tddp_type, + tddp_code, + tddp_reply, + tddp_length, + tddp_id, + tddp_subtype, + tddp_reserved, + tddp_digest, + hexlify(data.encode()).decode(), + ] + ) + + tddp_digest = hashlib.md5(unhexlify(tddp_packet)).hexdigest() + tddp_packet = tddp_packet[:24] + tddp_digest + tddp_packet[56:] + + logging.debug(f"Raw Request:\t{tddp_packet}") + + return tddp_packet + + +def send_request(ip: str, port_send: int, tddp_packet: str): + """Send TDDP request.""" + sock_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock_send.sendto(unhexlify(tddp_packet), (ip, port_send)) + logging.debug( + f"Req Data:\tVersion {tddp_packet[0:2]} " + f"Type {tddp_packet[2:4]} " + f"Status {tddp_packet[6:8]} " + f"Length {tddp_packet[8:16]} " + f"ID {tddp_packet[16:20]} " + f"Subtype {tddp_packet[20:22]}" + ) + sock_send.close() + + +def receive_reply(port_receive: int) -> str: + """Receive TDDP reply.""" + sock_receive = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock_receive.bind(("", port_receive)) + response, addr = sock_receive.recvfrom(1024) + resp = hexlify(response).decode() + logging.info(f"Raw Reply:\t{resp}") + sock_receive.close() + return resp + + +def decrypt_and_print_response(response: str, tddp_key: str): + """Decrypt and print TDDP response.""" + key = des(unhexlify(tddp_key), ECB) + recv_data = response[56:] + if recv_data: + logging.info(f"Decrypted:\t{key.decrypt(unhexlify(recv_data))}") + + +def parse_args(): + """Parse commandline arguments.""" + + def valid_ip(ip: str) -> str: + """Check if IP is valid.""" + try: + socket.inet_pton(socket.AF_INET, ip) + except OSError: + parser.error("Invalid IP Address.") + return ip + + def valid_hex(cmd: str) -> str: + """Check if command is two hex chars.""" + ishex = all(c in string.hexdigits for c in cmd) + if not (len(cmd) == 2 and ishex): + parser.error("Please issue a two-character hex command, e.g. 0A") + return cmd + + parser = argparse.ArgumentParser( + description=f"Experimental TP-Link TDDPv2 Client v.{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=valid_ip, + ) + parser.add_argument( + "-u", + "--username", + metavar="", + help="Username (default: admin)", + default="admin", + ) + parser.add_argument( + "-p", + "--password", + metavar="", + help="Password (default: admin)", + default="admin", + ) + parser.add_argument( + "-c", + "--command", + metavar="", + required=True, + help="Command value to send as hex (e.g. 0A)", + type=valid_hex, + ) + return parser.parse_args() + + +def main(): + """Parse arguments, generate key, build and send tddp packet, + decrypt and print response. + """ + args = parse_args() + ip = args.target + cmd = args.command + username = args.username + password = args.password + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) + + port_send = 1040 + port_receive = 61000 + + tddp_key = generate_tddp_key(username, password) + logging.info(f"TDDP Key:\t{tddp_key} ({username}:{password})") + tddp_packet = build_tddp_packet(cmd, tddp_key) + send_request(ip, port_send, tddp_packet) + response = receive_reply(port_receive) + decrypt_and_print_response(response, tddp_key) + + +if __name__ == "__main__": + main() diff --git a/tplink-smarthome-commands.txt b/tplink-smarthome-commands.txt new file mode 100644 index 0000000..0bc7a47 --- /dev/null +++ b/tplink-smarthome-commands.txt @@ -0,0 +1,189 @@ +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 On Device LED (Night mode) +{"system":{"set_led_off":{"off":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 100755 index 0000000..6bbdb88 --- /dev/null +++ b/tplink_smartplug.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 + +"""TP-Link Wi-Fi Smart Plug Protocol client (TP-Link HS-100, HS-110, ...). + +Orignal author: 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 argparse +import socket +from struct import pack + +version = 0.4 + + +# Predefined Smart Plug Commands +# For a full list of commands, consult tplink-smarthome-commands.txt +commands = { + "antitheft": '{"anti_theft":{"get_rules":{}}}', + "cloudinfo": '{"cnCloud":{"get_info":{}}}', + "countdown": '{"count_down":{"get_rules":{}}}', + "energy_reset": '{"emeter":{"erase_emeter_stat":{}}}', + "energy": '{"emeter":{"get_realtime":{}}}', + "info": '{"system":{"get_sysinfo":{}}}', + "ledoff": '{"system":{"set_led_off":{"off":1}}}', + "ledon": '{"system":{"set_led_off":{"off":0}}}', + "off": '{"system":{"set_relay_state":{"state":0}}}', + "on": '{"system":{"set_relay_state":{"state":1}}}', + "reboot": '{"system":{"reboot":{"delay":1}}}', + "reset": '{"system":{"reset":{"delay":1}}}', + "runtime_reset": '{"schedule":{"erase_runtime_stat":{}}}', + "schedule": '{"schedule":{"get_rules":{}}}', + "time": '{"time":{"get_time":{}}}', + "wlanscan": '{"netif":{"get_scaninfo":{"refresh":0}}}', +} + + +def encrypt(string: str) -> bytes: + """Encryption of TP-Link Smart Home Protocol. + XOR Autokey Cipher with starting key = 171. + """ + key = 171 + result = pack(">I", len(string)) + for i in string: + a = key ^ ord(i) + key = a + result += bytes([a]) + return result + + +def decrypt(ciphertext: bytes) -> bytearray: + """Decryption of TP-Link Smart Home Protocol. + XOR Autokey Cipher with starting key = 171. + """ + key = 171 + result = [] + for i in ciphertext: + a = key ^ i + key = i + result.append(a) + return bytearray(result).decode() + + +def parse_args(): + """Parse commandline arguments.""" + + def valid_hostname(hostname: str) -> str: + """Check if hostname is valid.""" + try: + socket.gethostbyname(hostname) + except OSError: + parser.error("Invalid hostname.") + return hostname + + def valid_port(port: str) -> int: + """Check if port is valid.""" + print(type(port)) + try: + port = int(port) + except ValueError: + parser.error("Invalid port number.") + + if (port <= 1024) or (port > 65535): + parser.error("Invalid port number.") + + return port + + parser = argparse.ArgumentParser( + description=f"TP-Link Wi-Fi Smart Plug Client v{version}" + ) + parser.add_argument( + "-t", + "--target", + metavar="", + required=True, + help="Target hostname or IP address", + type=valid_hostname, + ) + parser.add_argument( + "-p", + "--port", + metavar="", + default=9999, + required=False, + help="Target port", + type=valid_port, + ) + parser.add_argument( + "-q", "--quiet", dest="quiet", action="store_true", help="Only show result" + ) + parser.add_argument( + "--timeout", default=10, required=False, help="Timeout to establish connection" + ) + 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", + ) + return parser.parse_args() + + +def main(): + """Read argument, send encrypted commands, output decrypted answer.""" + args = parse_args() + # Set target IP, port and command to send + ip = args.target + port = args.port + cmd = args.json if args.command is None else commands[args.command] + + # Send command and receive reply + try: + sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_tcp.settimeout(int(args.timeout)) + sock_tcp.connect((ip, port)) + sock_tcp.settimeout(None) + sock_tcp.send(encrypt(cmd)) + data = sock_tcp.recv(2048) + sock_tcp.close() + + decrypted = decrypt(data[4:]) + + if args.quiet: + print(decrypted) + else: + print("Sent: ", cmd) + print("Received: ", decrypted) + + except OSError: + print(f"Could not connect to host {ip}:{port}") + + +if __name__ == "__main__": + main() diff --git a/wireshark-dissector.png b/wireshark-dissector.png new file mode 100644 index 0000000..475adf1 Binary files /dev/null and b/wireshark-dissector.png differ