robertklep / nefit-easy-client

Nefit Easy™ client for Node.js
MIT License
14 stars 3 forks source link

port to python #7

Closed patvdleer closed 7 years ago

patvdleer commented 7 years ago

Awesome work Robert but I'm running Home Assistant(.io) which runs on Python, so I would like to port it.

Based on https://github.com/robertklep/nefit-easy-client/blob/master/lib/encryption.js#L11-L12

Would you be able to provide me with an example of the input and output of the function so I can verify that my code is working since I'm getting an empty string back. Random values should suffice.

function generateKey(magicKey, id_key_uuid, privatePassword)

patvdleer commented 7 years ago

I've updated my code and verified that your generate key functions returns the same as my key (minus the dict for chat). Somehow the data still isn't decrypting...

class AESCipher(object):
    def __init__(self, magic, access_key, password):
        self.bs = 32
        self.key = hashlib.md5(bytearray(access_key, "utf8") + magic).digest() + \
                   hashlib.md5(magic + bytearray(password, "utf8")).digest()

    def encrypt(self, raw):
        raw = self._pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_ECB, iv)
        return base64.b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_ECB, iv)
        return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s) - 1:])]
patvdleer commented 7 years ago

Decrypt is fixed, will work on the rest later

class AESCipher(object):
    def __init__(self, magic, access_key, password):
        self.bs = 16
        self.key = hashlib.md5(bytearray(access_key, "utf8") + magic).digest() + \
                   hashlib.md5(magic + bytearray(password, "utf8")).digest()

    def encrypt(self, raw):
        raw = self._pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_ECB, iv)
        return base64.b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        cipher = AES.new(self.key, AES.MODE_ECB)
        return cipher.decrypt(enc).decode("utf8")

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s) - 1:])]
patvdleer commented 7 years ago

Sorry for bugging you again, I've almost completed the port but I've one last big issue which I seem unable to fix... I've compared my data to yours and the encryption content, the base64's, match.

What I send is:

DEBUG:sleekxmpp.xmlstream.xmlstream:SEND: <message from="rrccontact_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de" to="rrcgateway_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de"><body>PUT /heatingCircuits/hc1/temperatureRoomManual HTTP/1.1
Content-Type: application/json
Content-Length: 24
User-Agent: NefitEasy

0BlCRtWBHVpf458IXp2ipQ==</body></message>
DEBUG:sleekxmpp.xmlstream.xmlstream:SEND: <message from="rrccontact_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de" to="rrcgateway_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de"><body>PUT /heatingCircuits/hc1/manualTempOverride/status HTTP/1.1
Content-Type: application/json
Content-Length: 24
User-Agent: NefitEasy

1H2/Nt03I7zYLhe4Bx15kg==</body></message>
DEBUG:sleekxmpp.xmlstream.xmlstream:SEND: <message from="rrccontact_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de" to="rrcgateway_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de"><body>PUT /heatingCircuits/hc1/manualTempOverride/temperature HTTP/1.1
Content-Type: application/json
Content-Length: 24
User-Agent: NefitEasy

But the response is the following:

DEBUG:sleekxmpp.xmlstream.xmlstream:RECV: <message xml:lang="en" to="rrccontact_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de/927e9e62" from="rrcgateway_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de/RRC-RestApi" type="chat"><body>HTTP/1.0 400 Bad Request
Content-Type: application/json
connection: close

</body></message>
DEBUG:sleekxmpp.xmlstream.xmlstream:Event triggered: message
DEBUG:sleekxmpp.xmlstream.xmlstream:RECV: <message xml:lang="en" to="rrccontact_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de/927e9e62" from="rrcgateway_[SERIAL_NUMBER]@wa2-mz36-qrmzh6.bosch.de/RRC-RestApi" type="chat"><body>HTTP/1.0 400 Bad Request
Content-Type: application/json
connection: close

</body></message>
robertklep commented 7 years ago

@patvdleer sorry for the late response, I was on holiday for a few days :)

From what I remember, the backend is pretty strict about what it accepts as valid messages.

It looks like you're adding an additional empty line between the header and the body, where there should only be one (empty line, that is).

Also, IIRC, the lines should be separated using just \r (not \r\n as with regular HTTP requests).

patvdleer commented 7 years ago

I did that but still no luck, https://github.com/patvdleer/nefit-client-python

I did notice you replace the \r with old style enter &#13; but my library translates that into &amp#13;

robertklep commented 7 years ago

Oh yeah, you're right, they get translated to &#13;\n, even.

So try and see if \r\n works. If not, you probably do have to translate the \r to &#13; (did I mention the backend is a bit weird in that way? 😉)

patvdleer commented 7 years ago

Ugh... Currently looking into how I can prevent this, by the looks of it it's the send function that replaces the & with &amp;

DEBUG:sleekxmpp.xmlstream.xmlstream:SEND: <message to="rrcgateway_@wa2-mz36-qrmzh6.bosch.de" from="rrccontact_@wa2-mz36-qrmzh6.bosch.de"><body>PUT /heatingCircuits/hc1/manualTempOverride/temperature HTTP/1.1&amp;#13;
Content-Type: application/json&amp;#13;
Content-Length: 24&amp;#13;
User-Agent: NefitEasy&amp;#13;
&amp;#13;
0BlCRtWBHVpf458IXp2ipQ==
patvdleer commented 7 years ago

Forcing the &#13; (in violation of xml) results in No Content...

&#13;

DEBUG:sleekxmpp.xmlstream.xmlstream:SEND: <message from="rrccontact_@wa2-mz36-qrmzh6.bosch.de" to="rrcgateway_@wa2-mz36-qrmzh6.bosch.de"><body>PUT /heatingCircuits/hc1/manualTempOverride/temperature HTTP/1.1&#13;
Content-Type: application/json&#13;
Content-Length: 24&#13;
User-Agent: NefitEasy&#13;
&#13;
0BlCRtWBHVpf458IXp2ipQ==</body></message>
DEBUG:sleekxmpp.xmlstream.xmlstream:RECV: <message from="rrcgateway_@wa2-mz36-qrmzh6.bosch.de/RRC-RestApi" to="rrccontact_@wa2-mz36-qrmzh6.bosch.de/54924df" xml:lang="en" type="chat"><body>HTTP/1.0 204 No Content
Content-Type: application/json
connection: close

</body></message>

&#13;\n

DEBUG:sleekxmpp.xmlstream.xmlstream:SEND: <message to="rrcgateway_@wa2-mz36-qrmzh6.bosch.de" from="rrccontact_@wa2-mz36-qrmzh6.bosch.de"><body>PUT /heatingCircuits/hc1/manualTempOverride/temperature HTTP/1.1&#13;
Content-Type: application/json&#13;
Content-Length: 24&#13;
User-Agent: NefitEasy&#13;
&#13;
0BlCRtWBHVpf458IXp2ipQ==</body></message>

Results in:

DEBUG:sleekxmpp.xmlstream.xmlstream:RECV: <message to="rrccontact_@wa2-mz36-qrmzh6.bosch.de/799a0817" type="chat" from="rrcgateway_@wa2-mz36-qrmzh6.bosch.de/RRC-RestApi" xml:lang="en"><body>HTTP/1.0 204 No Content
Content-Type: application/json
connection: close

</body></message>
robertklep commented 7 years ago

204 is actually what's expected to be returned when a PUT request was successful 👍

patvdleer commented 7 years ago

Seriously??!?!??!?! 204 is the new 200 OK? It works... THANKS!!!

https://github.com/patvdleer/nefit-client-python/commit/03c7d4d457a1fe4a25d3e5b8c2bb7d79527744b1

robertklep commented 7 years ago

204 is quite common for REST API's 😃