Skip to content

Add klap support for TAPO protocol by splitting out Transports and Protocols #557

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

Merged
merged 4 commits into from
Dec 4, 2023

Conversation

sdb9696
Copy link
Collaborator

@sdb9696 sdb9696 commented Nov 29, 2023

This PR adds KLAP support for the TAPO/SMART protocol. The way the encryption works is that the the original message that worked with #552 is instead encrypted with KLAP:

"method": method,
"params": params,
"requestID": self.request_id_generator.generate_id(),
"request_time_milis": round(time.time() * 1000),
"terminal_uuid": self.terminal_uuid,

As the current klapprotocol implementation assumes the old kasa style of request/response this PR proposes to separate the transport from the protocol. The changes are:

  1. Introduce a base TPLinkTransport abstract class.
  2. Rename TplinkKlap to TPlinkIotProtocol. It seems this is the distinction in the discovery result, device_type beginning with IOT. uses the old protocol, device_type beginning with SMART. uses the new.
  3. Rename TPLinkAes to TPLinkSmartProtocol.
  4. Break out klap logic from TPLinkKlap into a new TPLinkKlapTransport class.
  5. Break out AES logic from TPLinkAes into a new TPLinkAesTransport class.
  6. The klap handshake hashes are slightly different with the SMART. devices so there's a TPLinkKlapTransportV2 class derived from TPLinkKlapTransport that overrides the three hash functions.

A good starting point to understand the changes here should be the get_protocol_from_connection_name diff in device_factory.py

@staticmethod
def create_key_pair(key_size: int = 1024):
"""Create a key pair."""
private_key = rsa.generate_private_key(public_exponent=65537, key_size=key_size)

Check failure

Code scanning / CodeQL

Use of weak cryptographic key

Creation of an RSA key uses [1024](1) bits, which is below 2048 and considered breakable.
Copy link

codecov bot commented Nov 29, 2023

Codecov Report

Attention: 148 lines in your changes are missing coverage. Please review.

Comparison is base (347cbfe) 75.76% compared to head (63310d3) 79.50%.

Files Patch % Lines
kasa/aestransport.py 46.70% 97 Missing ⚠️
kasa/smartprotocol.py 78.57% 16 Missing and 8 partials ⚠️
kasa/klaptransport.py 85.26% 6 Missing and 8 partials ⚠️
kasa/iotprotocol.py 85.96% 4 Missing and 4 partials ⚠️
kasa/discover.py 75.00% 1 Missing and 2 partials ⚠️
kasa/device_factory.py 89.47% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #557      +/-   ##
==========================================
+ Coverage   75.76%   79.50%   +3.74%     
==========================================
  Files          34       36       +2     
  Lines        2876     2991     +115     
  Branches      751      777      +26     
==========================================
+ Hits         2179     2378     +199     
+ Misses        640      541      -99     
- Partials       57       72      +15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@sdb9696 sdb9696 force-pushed the add_tapo_klap branch 2 times, most recently from dd5c584 to f28ff9b Compare November 30, 2023 12:17
@sdb9696 sdb9696 mentioned this pull request Nov 30, 2023
@sdb9696 sdb9696 marked this pull request as ready for review November 30, 2023 12:20
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.

Just a couple of very brief comments, mostly about things to make it easier to review.

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Dec 1, 2023

Tests have now been added and the test framework is working with the new tapo devices. I will add more tests but wanted to get this in to ease review.

@sdb9696 sdb9696 requested a review from rytilahti December 1, 2023 15:19
@rytilahti rytilahti added the enhancement New feature or request label Dec 1, 2023
@rytilahti rytilahti mentioned this pull request Dec 2, 2023
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.

That's quite a bit of code to review and test, but here's the first round of comments. In general, separating the concerns of the encryption and the transport sounds good to me 👍

A couple of things:

  • Update the PR title to be more descriptive.
  • Class naming: let's try to avoid useless TPLink*, Smart etc. prefixes where feasible, that pattern came from the pyhs100 project to keep backwards compatibility, but we should not introduce any more of them.
  • Using a self-created str<->(protocol, transport) mapping feels wrong and potentially error-prone when new versions will be deployed. I think we should decide soon what's a good abstraction for the discovery-like information to device instances..

@@ -22,11 +22,13 @@
]


@device_iot
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if it would be better to run these tests on all devices for now, just to keep the interfaces consistent.

In the future, it makes sense to separate common interfaces as discussed elsewhere to avoid mixing implementation & interfaces like we currently do by inheriting from Smart* for the new devices.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the review @rytilahti. I know it was a lot and I thought hard about how to make it smaller but couldn’t come up with anything that made much sense.

Most of the comments are agreeable and doable and I will hopefully get sorted Monday. There is one question I have to clarify with you first. I restricted these tests with @device_iot as a short term measure to keep the PR from getting bigger (yes I know :-). Currently too many tests access sysinfo and break.

So I think we have two options:

  1. Move all the SMART fixtures into a different directory so they don’t run on all tests.
  2. Restrict them for now with @device_iot and then gradually re-enable test_file by test_file as we introduce the interfaces.

I prefer 2. because it will mean we have a single test suite and a good interface. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, so those tests that test for specific payloads (like those schema checks in test_smartdevice) should only be tested against "device_iot"/"legacy" payloads, and indeed worth guarding for the time being.

Even then, we probably want to keep the "smart" fixtures separated from the legacy ones, so creating a new directory to host them from the get-go is a good idea if that's simple to achieve. If not, let's keep it simple for the time being and revisit it later.

But maybe the more generic ones (like testing for state_attributes structure, tests for cli outputs, ..) could still test everything if doing so doesn't require too many changes?

class TPLinkKlap(TPLinkProtocol):
def _sha1(payload: bytes) -> bytes:
digest = hashes.Hash(hashes.SHA1()) # noqa: S303
digest.update(payload)

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data

[Sensitive data (password)](1) is used in a hashing algorithm (SHA1) that is insecure for password hashing, since it is not a computationally expensive hash function. [Sensitive data (id)](2) is used in a hashing algorithm (SHA1) that is insecure.
def md5(payload: bytes) -> bytes:
"""Return an md5 hash of the payload."""
digest = hashes.Hash(hashes.MD5()) # noqa: S303
digest.update(payload)

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data

[Sensitive data (password)](1) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. [Sensitive data (id)](2) is used in a hashing algorithm (MD5) that is insecure. [Sensitive data (id)](3) is used in a hashing algorithm (MD5) that is insecure.
@sdb9696 sdb9696 changed the title Add tapo klap Add klap support for TAPO protocol by splitting out Transports and Protocols Dec 3, 2023
@sdb9696
Copy link
Collaborator Author

sdb9696 commented Dec 3, 2023

HI @rytilahti, I had a bit of time today to address this review. There's still work needed on the test framework but I think it will be better to perfect that in further PRs. After we get this in we can hopefully start to focus on separate streams, i.e. TAPO PRs, test framework PRs, test coverage PRs, fixture PRs and interface PRs. I hope we have the baseline now :)

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.

This is looking good, I added some extra comments on top of the few already existing unresolveds. @sdb9696 take a look and see what makes sense to fix prior merging this so that we can start again having smaller PRs :-)


def _sha1(payload: bytes) -> str:
sha1_algo = hashlib.sha1() # noqa: S324
sha1_algo.update(payload)

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data

[Sensitive data (password)](1) is used in a hashing algorithm (SHA1) that is insecure for password hashing, since it is not a computationally expensive hash function. [Sensitive data (id)](2) is used in a hashing algorithm (SHA1) that is insecure. [Sensitive data (id)](3) is used in a hashing algorithm (SHA1) that is insecure.
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.

LGTM, let's get this merged. Thanks @sdb9696! 🥇

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Dec 4, 2023

This is looking good, I added some extra comments on top of the few already existing unresolveds. @sdb9696 take a look and see what makes sense to fix prior merging this so that we can start again having smaller PRs :-)

Hi @rytilahti , I've addressed most of the comments from today and the open ones I missed from the other day (for some reason they keep becoming hidden for me in github).

If we're good to merge this now I'll go through afterwards and collate all of the "Subsequent PR" items we discussed and add them to the discussion so we don't lose sight of them.

@sdb9696 sdb9696 requested a review from rytilahti December 4, 2023 18:45
@rytilahti
Copy link
Member

I've addressed most of the comments from today and the open ones I missed from the other day (for some reason they keep becoming hidden for me in github).

I think github sometimes loses track of things when doing force pushes but it may be just a feeling. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants