Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add support for the new encryption protocol #117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from

Conversation

SimonWilkinson
Copy link
Contributor

@SimonWilkinson SimonWilkinson commented Nov 17, 2020

Here's a very rough implementation of the new encryption protocol.

Tested against an HS110 running the new firmware.

I haven't written much python recently, and even less using the asyncio framework, so comments and criticism very welcome.

Things that I know work:
*) Discovery
*) Most appropriate command line options when paired with the '--plug' option

Username and password support is still required, but should be simple. Single device discovery support is also not yet complete.

Fixes #170 fixes #115

This adds support for the new TP-Link discovery and encryption
protocols. It is currently incomplete - only devices without
username and password are current supported, and single device
discovery is not implemented.

Discovery should find both old and new devices. When accessing
a device by IP the --klap option can be specified on the command
line to active the new connection protocol.

def datagram_received(self, data, addr) -> None:
"""Handle discovery responses."""
ip, port = addr
if ip in self.discovered_devices:
return

info = json.loads(self.protocol.decrypt(data))
if port == 9999:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest that instead of relying on a hardcoded ports, to either have a command line option, or even better - attempt the old encryption mechanism, and if that fails - to try the new one.

In respect to this I've already opened a PR #109 that would extract the port and make it configurable and I think it'd quite handy for this implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that there are only two discovery methods, I think it's fine to hard-code (or rather use a const from the class) for this use case.

I'd still separate the klap discovery handling into its own method that gets called only when necessary (so instead of else checking for the port for this, too), and log an error when something arrives from an unknown port.

Open question is how to handle the type detection, as the type information I saw for the new discovery responses does not give out enough information to decide between a smartplug and a strip, for example.

I'm personally for making a sysinfo query, which adds another round-trip but would allow direct reuse of the existing detection logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example discovery payload is:

{'result': 
    {'ip': '<ip-address>',
      'mac': '<mac-address>', 
      'device_id': '<16 octet hex>', 
      'owner': '<16 octet hex>',
      'device_type': 'IOT.SMARTPLUGSWITCH', 
      'device_model': 'HS110(UK)', 
      'hw_ver': '4.1', 
      'factory_default': True, 
      'mgt_encrypt_schm': 
        {'is_support_https': False, 
         'encrypt_type': 'KLAP', 
         'http_port': 80}
    }, 
    'error_code': 0
}

So we could use the device_model as a way of determining the different between a plug (HS100 or HS110) and a strip (KP303)

@kirichkov
Copy link
Contributor

Thanks for the PR! Looks like a good start! Do you think you can provide some tests and fixtures too? There's some linting changes needed, but this can be handled as a last step after the code is ready to be merged.

@SimonWilkinson
Copy link
Contributor Author

Those failures look like problems with adding the new dependencies to poetry. Is there something that needs to happen to tell the builder that they are safe?

@kirichkov
Copy link
Contributor

It looks like you either forgot to run poetry add or didn't commit the changes to poetry.lock which poetry did.

If you forgot to run poetry add, just do:

poetry add aiohttp@^3.7.2 pycryptodome@^3.9.9

And commit pyproject.toml and poetry.lock
The documentation for poetry add.

@SimonWilkinson
Copy link
Contributor Author

I did poetry add in order to get the new pyproject.toml, but didn't commit poetry.lock, as it seemed like there were a load of changes to other modules in there. I'll push the new poetry.lock and see what happens!

@kirichkov
Copy link
Contributor

Now it fails because the changed files are not properly formatted using black. poetry run pre-commit run black --all-files would fix that.

To get this to pass check lines 23 to 68 in https://github.com/python-kasa/python-kasa/blob/master/azure-pipelines.yml#L23 and execute each command. They should reformat the files so the styling "tests" can pass.

Restore the incorrectly commented out old discovery mechanism, so
both old and new devices are found.
@codecov-io
Copy link

codecov-io commented Nov 18, 2020

Codecov Report

Merging #117 (d9a75e9) into master (56eb2cd) will decrease coverage by 5.01%.
The diff coverage is 35.09%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #117      +/-   ##
==========================================
- Coverage   73.02%   68.00%   -5.02%     
==========================================
  Files          10       12       +2     
  Lines        1227     1394     +167     
  Branches      183      203      +20     
==========================================
+ Hits          896      948      +52     
- Misses        298      412     +114     
- Partials       33       34       +1     
Impacted Files Coverage Δ
kasa/discover.py 34.86% <17.02%> (-5.32%) ⬇️
kasa/klapprotocol.py 23.25% <23.25%> (ø)
kasa/cli.py 53.37% <23.52%> (-1.37%) ⬇️
kasa/protocol.py 75.32% <61.76%> (+0.69%) ⬆️
kasa/smartdevice.py 82.40% <80.00%> (-0.34%) ⬇️
kasa/auth.py 83.33% <83.33%> (ø)
kasa/smartplug.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 56eb2cd...d9a75e9. Read the comment docs.

@SimonWilkinson
Copy link
Contributor Author

Thanks @kirichkov I think I finally got there in terms of the tests.

@kirichkov
Copy link
Contributor

@rytilahti Can you take a look as well? Should we merge #109 before that to refactor this on top of it or do it the other way around?

Add the --user and --password command line options which can
be used to provide a username and password to authenticate to
the smart device using the KLAP protocol.

Check the 'owner' field in the discovery packet, and only attempt
to authenticate to devices for which we have a password.
@SimonWilkinson
Copy link
Contributor Author

mypy running locally is happy with this, so I'm not sure what the problem is.

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey and thanks for the great PR! 👍 I did an initial review and left some comments behind, there are also some open questions here and there, but I hope this will give a good start before doing a second round of reviews & adding tests.

kasa/protocol.py Outdated
try:
session = aiohttp.ClientSession(cookie_jar=self.jar)

async with self.handshake_lock:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this lock should be generic for all requests, to avoid potential reuse of the seq number in case several queries are done concurrently?

kasa/protocol.py Outdated
signature = hashlib.sha256(
self.hmacKey + seq.to_bytes(4, "big", signed=True) + ciphertext
).digest()
return signature + ciphertext
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return signature + ciphertext
return signature + ciphertext

Please separate exit points with an empty line for readability.

kasa/protocol.py Outdated
if resp.status != 200:
raise SmartDeviceException(
"Device responded with %d to handshake1" % resp.status
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
)
)

I'd separate the exit point here with an empty line.

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some more suggestions. For tests we want to have at least unit tests for:

  • Discovery handling
  • Encryption
  • Decryption
  • Different handshake phases
  • Handling of error cases (e.g., what happens when incorrect user/pw combination is used)

Comment on lines +74 to +75
def __init__(self, host: str):
super().__init__(host=host)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __init__(self, host: str):
super().__init__(host=host)

raise SmartDeviceException("Not reached")

async def _ask(self, request: str) -> str:
raise SmartDeviceException("ask should be overridden")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise NotImplementedError like SmartDevice's "abstract" methods do.

@@ -255,7 +263,7 @@ async def _query_helper(
request = self._create_request(target, cmd, arg, child_ids)

try:
response = await self.protocol.query(host=self.host, request=request)
response = await self.protocol.query(request=request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
response = await self.protocol.query(request=request)
response = await self.protocol.query(request)

I think in this case it is not necessary to use kwarg, it is pretty clear already what is going in :-)

@@ -300,7 +308,7 @@ async def update(self):
# Check for emeter if we were never updated, or if the device has emeter
if self._last_update is None or self.has_emeter:
req.update(self._create_emeter_request())
self._last_update = await self.protocol.query(self.host, req)
self._last_update = await self.protocol.query(request=req)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._last_update = await self.protocol.query(request=req)
self._last_update = await self.protocol.query(req)

async def _handshake(self, session) -> None:
_LOGGER.debug("[KLAP] Starting handshake with %s", self.host)

# Handshake 1 has a payload of client_challenge
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please separate handling of different handshake phases to their own methods, that will make it easier to write unit tests.

owner_bin,
self.emptyUser,
)
if owner is None or owner == "" or owner_bin == self.emptyUser:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The owner cannot be none anymore at this point, so checking if the owner == Auth.empty_user should suffice?

_LOGGER.debug("[DISCOVERY] Device %s has no owner", ip)
device = device_class(ip, Auth())
elif (
self.authentication is not None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The self.authentication needs to be already there if we get this far, so checking for the owner should be enough.

https://github.com/softScheck/tplink-smartplug/

which are licensed under the Apache License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module docstring was valid only for the legacy implementation, please adapt (or remove altogether?).

agreed = self.client_challenge + self.server_challenge + self.authenticator
self.encrypt_key = self._sha256(b"lsk" + agreed)[:16]
self.hmac_key = self._sha256(b"ldk" + agreed)[:28]
fulliv = self._sha256(b"iv" + agreed)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Group local & instance assignments separately, consider adding some newlines to make it nicer to read.

# In theory we should verify the hmac here too
return Padding.unpad(cipher.decrypt(payload[32:]), AES.block_size)

async def _ask(self, request: str) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing docstring.

@ghostseven
Copy link

Just wanted to check if this is likely to move forward and be integrated?

@SimonWilkinson
Copy link
Contributor Author

Just wanted to check if this is likely to move forward and be integrated?

I need to find the time to complete the necessary cleanup and testing. It's at a point where it works fine for day to day use for me, and so it has dropped down the priority list somewhat.

My main goal in doing this work was Home Assistant integration (I'm using roughly the same changes against pyHS110), so this is less urgent for me until the Home Assistant TP-Link library moves over to python-kasa.

@ghostseven
Copy link

Just wanted to check if this is likely to move forward and be integrated?

I need to find the time to complete the necessary cleanup and testing. It's at a point where it works fine for day to day use for me, and so it has dropped down the priority list somewhat.

My main goal in doing this work was Home Assistant integration (I'm using roughly the same changes against pyHS110), so this is less urgent for me until the Home Assistant TP-Link library moves over to python-kasa.

My apologies I did not want to add any pressure on you.

I will play with your fork as a starting point and take it from there, it is my goal to get this working in homebridge. I think that will require a new plugin based on python-kasa as that also does not currently use it (as far as I am aware).

May I ask or may I email you (or you can email me [email protected]) regarding your basic setup using your version?

Making an emeter request against an HS100 plug crashes it.

So don't.
@jasonbouknight
Copy link

What’s needed to complete this item?

@bdraco
Copy link
Member

bdraco commented Sep 21, 2021

What’s needed to complete this item?

As @SimonWilkinson stated in #117 (comment), its not a priority until Home Assistant moves over to using this library, which isn't likely to happen until October.

@mystcb
Copy link

mystcb commented Nov 8, 2021

Heya

Just wanted to say thank you to the work put in by @SimonWilkinson and @ghostseven for their updates to this! I have been playing around behind the scenes, when I can, using both your forks to test out my HS110(UK) v4.1 (v1.1.0 FW) plugs and ran into all sorts of issues, eventually realised it was down to my own fault of using the wrong password in the command like to run the discovery. (Although resetting one of my plugs to "Local Mode" meant I was able to access without the username/password).

@bdraco Looks like you were able to get the integration working with this module (awesome work too!), and it looks like everything is falling into place!

I am going to attempt to see if I can work my HA around to use this fork of the python module to see if I can get anywhere with assisting with this, but before I wonder down a rabbit hole (or step on anyone's toes/add additional work for anyone here) - is there anything anyone would like me to have a look at / or stay away from?

I don't want to add any more pressure onto anyone here, but I would like to try and do whatever I can to help with this!

Thanks again for all your help and support getting through these annoying changes!

@bdraco
Copy link
Member

bdraco commented Nov 8, 2021

I don't have any of the affected devices so I'm not able to test this.

@mystcb
Copy link

mystcb commented Nov 17, 2021

Just to keep people up to date here, @Yamakiroshi and I have been working on and off trying to just "hack" this back into a working state, both with the Kasa CLI and through Home Assistant, and as of a few moments ago we succeeded! There has been a lot of really bad coding to make this all work, but I will try and share as we push through this.

One key element is we took @SimonWilkinson latest forked, forked that again, merged in all the current code from the main branch here, along with some very "non-best practice" commits just to try and get the whole thing working.

The unsorted code for this repo is currently here - This needs a lot of work, but using changes here, and some hacks to the tplink custom component, we got control and power usage, using the new KLAP protocol.

It is very late here, so I am going to have to drop off for the night, but I will have a go at cleaning up the code as much as possible, try and bring in some of the code review comments from here, and then see what needs to be done next. I am not an expert at this at all, so would really appreciate any support to see if we can get this over the line.

Thank you again to everyone for the work so far!

@bdraco
Copy link
Member

bdraco commented Nov 17, 2021 via email

@deftdawg
Copy link

deftdawg commented Dec 4, 2021

The unsorted code for this repo is currently here - This needs a lot of work, but using changes here, and some hacks to the tplink custom component, we got control and power usage, using the new KLAP protocol.

@mystcb can you do a PR?

@mystcb
Copy link

mystcb commented Dec 7, 2021

Apologies all, had a few things come up - let me get the PR in - I am going to reset everything and pull all the changes into one place so it is all up to date. I will get this done ASAP

PR Raised: #267

@rytilahti
Copy link
Member

#509 is now merged so I'm closing this as obsolete, thanks @SimonWilkinson!

@rytilahti rytilahti closed this Nov 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for EP10 Plug Implement the new protocol (HTTP over 80/tcp, 20002/udp for discovery)
9 participants