jasonacox / tinytuya

Python API for Tuya WiFi smart devices using a direct local area network (LAN) connection or the cloud (TuyaCloud API).
MIT License
937 stars 167 forks source link

Support for v3.5: there was an attempt #256

Closed uzlonewolf closed 1 year ago

uzlonewolf commented 1 year ago

It currently only works with PyCryptodome. We can probably make it work with pyaes as well, I just haven't had time to research it yet.

Due to it encrypting the retcode I ended up moving the encryption/decryption into pack_message()/unpack_message() so it can populate TuyaMessage.retcode. TuyaMessage was also extended to add .prefix (which is required no matter which way we go) and .iv (not required but I thought it might be useful). The new 6699 format has an additional 16-bit field that I'm not currently packing into TuyaMessage as I have no idea what to call it.

jasonacox commented 1 year ago

Brilliant work @uzlonewolf ! I hope we can eventually find v3.5 devices for testing.

It currently only works with PyCryptodome.

Right now, PyCryptodome is the only library that works/installs cleanly on all platforms (esp. Windows) so I don't think that is terrible. I do think we should update the README to reflect the limitation (pyaes support deprecated).

I do think this gives enough reason to minor-rev the release of TinyTuya to 1.10.0 (which is what we did for 3.4 support).

TuyaMessage was also extended to add .prefix (which is required no matter which way we go) and .iv (not required but I thought it might be useful).

That makes perfect sense. I do think I will add a comment to describe these (keep me honest, iv is the initialization vector, correct?)

The new 6699 format has an additional 16-bit field that I'm not currently packing into TuyaMessage as I have no idea what to call it.

Were you able to tell what it does or why they would include it?

jasonacox commented 1 year ago

@uzlonewolf - using latest, the scanner is having some problem with broadcast from my old 3.1 devices:

*  Unexpected payload from '10.0.1.31' to port 6666: b'\x00\x00U\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9b\x00\x00\x00\x00{"ip":"10.0.1.31","gwId":"03200160ecfabc8d9700","active":2,"ability":0,"mode":0,"encrypt":true,"productKey":"iXfg9AQVUPhlfyGw","version":"3.1"}m|\x1f>\x00\x00\xaaU'

However, force scan works...

Christmas Tree Stand Light   Product ID = ?  [Force-Scanned]:
    Address = 10.0.1.31   Device ID = 03200160ecfabc8d9700 (len:20)  Local Key = 78c2e271b3dcfae4  Version = 3.1  Type = default, MAC = ec:fa:bc:8d:97:00
    Status: {'1': True, '2': 0, '4': 79, '5': 84, '6': 1194}
uzlonewolf commented 1 year ago

scanner is having some problem with broadcast from my old 3.1 devices

Ugh, I forgot to strip the heater/footer for unencrypted packets. I never liked that try/except block anyway, so I rewrote it based on which port the broadcast was received on. https://github.com/uzlonewolf/tinytuya/commit/5e066415e6513239c2ddaa9e6697bd5145af700e I'll make a new PR once I have a few other updates ready.

jasonacox commented 1 year ago

Awesome! Found fix for other errors. I have an edge case where new devices are added (no key yet) and getting exceptions in scanner. I suspect we can catch this edge case in constructor and use a b'0'*16 key - This seems to fix it. Any obvious side effects or should we treat this edge case upstream?

# Cryptography Helpers
class AESCipher(object):
    def __init__(self, key):
        self.bs = 16
        self.key = key
        if self.key == b'':    # Missing key use zero
            self.key = b'0'*16
uzlonewolf commented 1 year ago

I'd rather not deprecate pyaes just because I was too lazy to read the api docs lol. I'll look into it once I'm done with what I'm currently working on.

iv is the initialization vector, correct?

Yep, initialization vector slash nonce (number used once) depending on who you're talking to. It gets regenerated for every message.

Were you able to tell what it does or why they would include it?

No, it's been set to 0 for every packet I've seen. I would have named and included it if I knew what it did :lol:. I was debating tackling it onto the front of the seqnum or something but decided that could be a disaster later.

uzlonewolf commented 1 year ago

Any obvious side effects or should we treat this edge case upstream?

Using all 0's will cause the decryption to throw an exception (unless nopad+nodecode is set) so I don't really see a point. Figuring out how it got there in the first place sounds to me like the better option.

uzlonewolf commented 1 year ago

Regarding pyaes, I finally looked into it. Unfortunately pyaes does not support GCM mode. I can work around encrypting/decrypting by using CTR mode with the IV set correctly, however there is no way to do the GMAC hashing with it. So it's going to have to remain PyCryptodome only.

jasonacox commented 1 year ago

No worries... we can add the note about deprecation. To be clear, with users of pyaes still be able to use tinytuya for other non-3.5 devices, or will they get an error (or do we know)?

uzlonewolf commented 1 year ago

Yep, pyaes still works fine for <=3.4, it's only 3.5 it won't work for (it will throw a NotImplementedError if someone tries). The message in the thrown error should probably be updated... Perhaps "pyaes does not support GCM, please install PyCryptodome"

jasonacox commented 1 year ago

I like that. How about this for the README?

Encryption notes

Tuya devices use AES encryption which is not available in the Python standard library. PyCryptodome is recommended and installed by default. Other options include PyCrypto and pyaes.

uzlonewolf commented 1 year ago

Looks good to me :)