Open jcgorla-dev opened 4 years ago
I'm having the exact same issue. I'm new to Homebridge and am trying to figure this out. The code is trying to parse JSON but it's only receiving Hex values. The second hex value in many cases is actually your MAC address.
Which version of the LC7001 are you running?
My LC7001 is running firmware 4.1.0-59-g96b3 which is the latest update available on the Legrand Light Control iOS app.
My firmware is 4.1.2 and it is running without problems if that helps in any way (and to clarify - I am not running this project, but used it as the basis for a Java based implementation of an OpenHAB binding https://github.com/openhab/openhab-addons/pull/6553. Over the past 2 years - including version 4.1.2 now - I have not seen any changes in the JSON format that the hub sends).
My firmware is 4.1.2 NodeJS is 12.17.0 Homebridge - just upgraded to 1.1.1 homebridge-lc7001 is 0.6.3
I still have the problem though. It appears that the hub is only returning HEX instead of JSON. Not sure why. Some of the Hex values include the MAC address of the Hub. I want to try and troubleshoot by sending some REST requests directly to the Hub with Postman. Does anyone have any suggestions for REST requests to try? I can't find any API documentation and since I'm new the Homebridge it will take me a little while to reverse engineer the code to determine the proper request format.
Knowing that Node is known for introducing breaking changes, I downgraded Node to 10.17.0, which is the earliest version that Homebridge claims to support. So with the earliest possible Node version, but with the latest firmware, latest homebridge version, and latest homebridge-lc7001, I still have this problem. I'd really love to get this to work but am stumped. My only option at this point is to try and reverse engineer the code to figure out what the API is so that I can start troubleshooting. If anyone has any pointers that would be really helpful!!
Anyone have any thoughts on this problem? I have a hunch that Legrand started to encrypt their API. The responses in the log look like encrypted data. I also looked at the Legrand app release history and found an entry for v 4.1 (5 months ago) that says: "For existing users, this app update features miscellaneous bug fixes and updates. For new users, in order to enhance the security of the device, users will be required to create a unique system password to set up the system." My thought is that the "secure" password is being used to encrypt the connection. If that's the case, it's probably just a matter of time before they require all users to use a password and encrypt the connection. This would only affect new users for now.
Note that I am not using HOOBS as was mentioned in the original comment. This appears to be a general issue.
Looks like the latest update to the LC7001 may have solved the JSON errors. My LC7001 is now running 4.1.2-7-gb79ba without any errors reported to the log!
@jcgorla-dev, I am running the same firmware version and still have this problem. I think I always was running that version. It turns out that 4.1.2 is the iOS app version and 4.1.2-7-gb79ba is the firmware version on the hub. Did you do anything else to get it working? Is it working correctly with HomeKit now?
Sorry to report it is back to reporting the same errors. I just see today v1.0.0 was posted. I'm using HOOBS, but it doesn't show the new version being available.
I have a similar JSON parsing issue, but I am not using HOOBS. My LC7001, homebridge, and this plugin are running the latest version. Homebridge console log follows:
[9/20/2020, 14:45:06] [LC7001] Error on connection to LC7001.
[9/20/2020, 14:45:06] [LC7001] Error: read ECONNRESET
at TCP.onStreamRead (internal/stream_base_commons.js:205:27) {
errno: 'ECONNRESET',
code: 'ECONNRESET',
syscall: 'read'
}
[9/20/2020, 14:45:06] [LC7001] Connection to LC7001 closed.
[9/20/2020, 14:45:06] [LC7001] Reconnecting to LC7001.
[9/20/2020, 14:45:06] [LC7001] Connection to LC7001 established.
Error parsing JSON.
SyntaxError: Unexpected token H in JSON at position 0
at JSON.parse (
These messages loop infinitely. I'm unsure what to do about this. Any suggestions?
I did some packet sniffing and it appears that when connecting, the LC first sends "Hello V1 <32 char hex> <12 char hex>", where the first string varies but the second is the same each time. The Legrand program responds with a 32 char hex result. Encrypted or hashed with saved device password is my guess. LC then responds with [OK]\lf\cr\lf\0 string. From then on it appears that everything else continues on in plain text with ReportSystemProperties etc.
I did some packet sniffing and it appears that when connecting, the LC first sends "Hello V1 <32 char hex> <12 char hex>", where the first string varies but the second is the same each time. The Legrand program responds with a 32 char hex result. Encrypted or hashed with saved device password is my guess. LC then responds with [OK]\lf\cr\lf\0 string. From then on it appears that everything else continues on in plain text with ReportSystemProperties etc.
The second hex value in many cases is the LC7001 MAC address.
It is the MAC address. Did a quick python script to pretend to be the LC and log a reply. MAC address is ignored and only first 23 of the 32 characters affect the reply.
Upgraded today to HOOBS v3.2.8, and the JSON error persists. 10/21/2020, 4:33:51 PM [LC7001] Connecting to LC7001.... 10/21/2020, 4:33:51 PM [LC7001] Connection to LC7001 established. 10/21/2020, 4:33:51 PM [LC7001] Unable to parse JSON: Hello V1 10/21/2020, 4:33:54 PM [LC7001] Unable to parse JSON: BE8CC4CA127A3F9C3D4CF2A4D7C0EBE5 0026EC02EE11[INVALID] 10/21/2020, 4:33:57 PM [LC7001] Error on LC7001 connection: Error: read ECONNRESET 10/21/2020, 4:33:57 PM [LC7001] Connection to LC7001 closed due to error. Waiting 30 seconds to reconnect....
Having the same issue here:
[10/21/2020, 6:46:45 PM] [LegrandHub] Connecting to LC7001....
[10/21/2020, 6:46:45 PM] [LegrandHub] Connection to LC7001 established.
[10/21/2020, 6:46:45 PM] [LegrandHub] Unable to parse JSON:
Hello V1
[10/21/2020, 6:46:48 PM] [LegrandHub] Unable to parse JSON:
BB0BE0C34915674AA87554702486348A 0026EC02D64E[INVALID]
[10/21/2020, 6:46:51 PM] [LegrandHub] Error on LC7001 connection: Error: read ECONNRESET
[10/21/2020, 6:46:51 PM] [LegrandHub] Connection to LC7001 closed due to error. Waiting 30 seconds to reconnect....
Unless someone figures out the handshake for the initial connection, we may be SOL. For example, if the password is "aaaaaaaa" and I send these strings pretending to be an LC7001 to the Legrand Light Control app, here is what gets returned:
sending: Hello V1 00000000000000000000000000000000 000000000000
received: "BBE791C8534BD8DFBC0F5AE57BB1027D"
sending: Hello V1 00000000000000000000000000000001 000000000000
received: "10F8EB44513AAAAA73D0A61E5A61407F"
sending: Hello V1 00000000000000000000000000000002 000000000000
received: "82AF567C204A02F6ABB071F84AE1D4AA"
etc.
The LC7001 sends a random 128 bit value, the app takes the value mixes in the password and returns a new (hashed?) string. The returned string is compared against the expected value and if it doesn't match, the controller drops the connection. I have emailed Legrand tech support on the faint hope they will provide the algorithm that they use to validate the password. Another option would be downgrade the firmware on the LC somehow.
Hi guys,
I got from Legrand technical team the document explaining how the new authentication is done so we can fix the plugin. Since I'm short on time to work on the fix I hope any of you guys can take a look before me. There is also a doc explaining the API, with that I think we can go above and beyond using the LC7001 directly as well, some doors are open now :)
https://www.dropbox.com/sh/mof0fr2bqbfovv5/AAB4L7LNx6b3BqZvHwbV0HNka?dl=0
Felipe
I got from Legrand technical team the document explaining how the new authentication is done so we can fix the plugin. Since I'm short on time to work on the fix I hope any of you guys can take a look before me. There is also a doc explaining the API, with that I think we can go above and beyond using the LC7001 directly as well, some doors are open now :)
Outstanding! Thanks Felipe.
This is great news, and exactly what we need. I've read through both the security addendum and the ICD. They're pretty straight forward. The security addendum has all the details on the challenge protocol to login.
Thanks Felipe!
Just made a proof of concept in python and got the authentication working.
$ python legrand.py
Socket Created
Socket Connected to LCM1.local on ip 192.168.1.112
Challenge: 049464DA7B188260344A659EAF0B9B22
Encrypted Response F1996FE3A5A2EEAF1672E3EB9A405BDD
[OK]
{"ID":0,"Service":"ping","CurrentTime":1605073644,"PingSeq":1,"Status":"Success"}
I just don't know how to port it to javascript as my JS knowledge is close to none :)
Do we know in which source file the challenge/handshake occurs? I looked through the code, but haven't found it yet.
In case somebody wants to take a look in the authentication code in python I put it here: https://github.com/fsalum/legrand-lc7001/blob/main/legrand.py
Basically I get the challenge key from the Hello message, encrypt it with AES 128 using the MD5 of my LC7001 user password and the result is sent back to the Hub.
Don't really know JavaScript but it looks like when the connection comes up (see lc7001.ts:118-120 interface.on('ready'), it currently sends the command to get the initial info from the LC. This is the old behaviour. Instead, the code would first need to check if there is an incoming message after connecting. If the message starts with "Hello V1", get the challenge hex string, generate the response, and wait for "[OK]" before carrying on the same as before by sending cmdGetSystemInfo(). Anyone know the JavaScript library/functions for MD5 and AES128.ECB available?
The code to check for "Hello V1" looks like it would need to be in processBuffer() before it tries to JSON the data.
Is @sbozarth still active in developing this plugin?
I am. Was not aware there was a whole issue thread going on GitHub. (Guess I have notifications off?)
I might be able to go read the thread later today.
On Nov 13, 2020, at 8:25 AM, Felipe Salum notifications@github.com wrote:
Is @sbozarth still active in developing this plugin?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.
So, it looks like starting 2020-01-01, changes were made to require authentication of some kind. My person system is “non-compliant” because I added devices prior to 2020-01-01. Thus, my personal system does not require authentication and I had no idea about this.
I will take a look at the Python script linked above and see if I can recreate in TS/JS. I’m not really jazzed to turn authentication on for my personal system. Does anyone want to volunteer to test?
Sure.
I'd be happy to help test.
Should we keep the issue open for testing ?
My bad, closed it by mistake.
Okay, I have created a new branch "1.1.x" containing my first crack at coding the authentication portion.
I am flying blind with absolutely no way to test this, and I really do not want to break my setup at this moment. For testing purposes, you may wish to uncomment this line (145) from lib/lc7001.js:
//this.platform.log.debug('Using password hash:',this.passwordHash.toString('hex').toUpperCase());
I did a few tests using examples from @aep61. Using password "aaaaaaaa" I received the following:
Challenge: 00000000000000000000000000000000 Password hash: 3DBE00A167653A1AAEE01D93E77E730E Answer: CF2A47F5DA9ACD35AD8C93578FED25CB
Challenge: 00000000000000000000000000000001 Password hash: 3DBE00A167653A1AAEE01D93E77E730E Answer: 2A5E276CE949E0DD456B80C1F0C467C2
Challenge: 00000000000000000000000000000002 Password hash: 3DBE00A167653A1AAEE01D93E77E730E Answer: BCE5F93835B5FA7BEC1D652B40B4B629
This clearly does not match, but I cannot troubleshoot more without knowing the hash @aep61 got (or that "aaaaaaaa" was not actually the password).
Same problem with the example from @fsalum: Without the password, I cannot reproduce.
@fsalum, can you provide me with a confirmed:
1) password 2) hash of password 3) challenge 4) correct answer
I can then check my use of the cryptographic functions.
@sbozarth
my LC7001 password is 12345678
$ python legrand.py
Socket Created
Socket Connected to LCM1.local on ip 192.168.1.112
Challenge: 680CFDC8257A174606198DE7AB0CC282
Password MD5 25d55ad283aa400af464c76d713c07ad
Encrypted Response 8EA25157CD5BC8A41671A7F391FE87C3
[OK]
{"ID":0,"Service":"ping","CurrentTime":1605321209,"PingSeq":1,"Status":"Success"}{"ID":0,"Service":"ping","CurrentTime":1605321214,"PingSeq":2,"Status":"Success"}{"ID":0,"Service":"ping","CurrentTime":1605321219,"PingSeq":3,"Status":"Success"}
My results:
[LC7001] Using challenge: 680CFDC8257A174606198DE7AB0CC282 [LC7001] Using password hash: 25D55AD283AA400AF464C76D713C07AD [LC7001] Answer generated: 8EA25157CD5BC8A41671A7F391FE87C3
We have a match! The cyrpto calls work. They are dependent on Node.js crypto module, which is itself apparently dependent on OpenSSL. Some Node.js implementations are compiled without crypto support. I did not test for any of that.
Hopefully someone can test this code with an authentication-enabled LC7001.
The regular expression doesn't seem to match. I added some code
this.platform.log.error('Raw:', data);
this.platform.log.error('Equal:', /Hello V1/.test(data));
this.platform.log.error('Equal1:', /Hello V1 [0-9A-F]{32} [0-9A-F]{12}/.test(data));
this.platform.log.error('Equal2:', /^Hello V1 [0-9A-F]{32} [0-9A-F]{12}/.test(data));
Here's the log
11/13/2020, 9:07:20 PM [LC7001] Data received from LC7001 (stringified): "Hello V1 \u0000480B4489DDBBA926A5D604DF0C0054DA 0026EC02F57E" 11/13/2020, 9:07:20 PM [LC7001] Raw: Hello V1 480B4489DDBBA926A5D604DF0C0054DA 0026EC02F57E 11/13/2020, 9:07:20 PM [LC7001] Equal: true 11/13/2020, 9:07:20 PM [LC7001] Equal1: false 11/13/2020, 9:07:20 PM [LC7001] Equal2: false
Also, note that sometimes the Hello string comes in separate packets so they need to be merged. Also, if I use the simplest match for 'Hello V1', it fails to generate an answer string
[LC7001] Raw: Hello V1 4982273C6C9928E06A61075B7AED0B3B 0026EC02F57E
11/13/2020, 9:39:11 PM [LC7001] Equal: true
11/13/2020, 9:39:11 PM [LC7001] Equal1: false
11/13/2020, 9:39:11 PM [LC7001] Equal2: false
11/13/2020, 9:39:11 PM [LC7001] splitData: [
'Hello',
'V1',
'\u00004982273C6C9928E06A61075B7AED0B3B',
'0026EC02F57E'
]
11/13/2020, 9:39:11 PM [LC7001] LC7001 has sent an authentication challenge: 4982273C6C9928E06A61075B7AED0B3B
11/13/2020, 9:39:11 PM [LC7001] answerCipher: Cipheriv { _decoder: null, _options: undefined, [Symbol(kHandle)]: {} }
11/13/2020, 9:39:11 PM [LC7001] Hash: <Buffer 81 02 47 41 90 84 c8 2d 03 80 9f c8 86 fe da ad>
11/13/2020, 9:39:11 PM [LC7001] challenge:
I was looking a the incoming packets and it appears that the data sent includes a NULL character (\u0000) after the V1. You need to trim() that off before sending the challenge. This regexp matches: /^Hello V1 \u0000[0-9A-F]{32} [0-9A-F]{12}/
\u0000 is the delimiter. This was not mentioned in the documentation, but easily handled as I am already splitting the buffer at the delimiter.
Can you give me any idea how the challenge gets split across packets? If 'Hello V1 ' is in one packet and the hex in another, that is not a problem. If the 'Hello V1 ' or hex is split....that's going to be a pain.
It looks like it can be delivered split up in different ways depending on timing, based on when you ask for the data. I have seen all 3 cases:
Data received from LC7001 (stringified): "Hello V1 \u0000610D37B3AF8DE6BD9A8F711D9ECF02F8 0026EC02F57E"
Data received from LC7001 (stringified): "Hello V1 \u0000" Data received from LC7001 (stringified): "6573619582EB3747" Data received from LC7001 (stringified): "238225D133D7B025 0026EC02F57E"
[LC7001] Data received from LC7001 (stringified): "Hello V1 \u0000" [LC7001] Data received from LC7001 (stringified): "8AA4FF85A26247D4" [LC7001] Data received from LC7001 (stringified): "B37F3FA90EB4F304" [LC7001] Data received from LC7001 (stringified): " 0026EC02F57E"
Looks like you'll have to check the length and fetch new data until the string is long enough.
Also don't know why Buffer.from(splitData[2], 'hex') generates an empty buffer.
Probably because it has the leading \u0000. That is not a hex character, so it cannot generate a clean binary buffer.
If you simply removed the \u0000 it would probably work fine.....as long as it came in one full packet.
Yup. I thought trim() would get it but it doesn't. substring(1) fixes it. Now it generates the correct reply but it's still returning [INVALID]. The string sent can't include a termination byte. Don't know if this.interface.write(answer, 'ascii'); does that.
@sbozarth how do I install your branch into my homebridge for testing? Can you maybe generate a package to install via homebridge?
Also are you planning to add a config via the homebridge UI so we can set our own passwords?
Looking forward to use this plugin :)
By the way in all my tests I never get a split Hello message from LC7001. It always came at once.
I did a config for Homebridge UI. I even added the password field. Do you not see it? Most of that was part of 1.0.x.
Getting the branch for testing..... That's a more complicated matter. I am not proficient in git, so don't blame me if this is not correct.
You'd do a git pull to retrieve the repository. You'd do a git checkout 1.1.x to switch to the 1.1.x branch. Then you'd do a "npm link" in the root of my project to have npm link the 1.1.0 version rather than install it.
If you are on Windows, I don't know if any of that works.
When you are done, you npm unlink in the root of the project to return to the installed version.
The split seems to happen after the first failed attempt to connect.
On Nov 13, 2020, at 11:37 PM, Felipe Salum notifications@github.com wrote:
By the way in all my tests I never get a split Hello message from LC7001. It always came at once.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/sbozarth/homebridge-lc7001/issues/7#issuecomment-727161127, or unsubscribe https://github.com/notifications/unsubscribe-auth/ARHDS2GGHXSLG6FSOMTXPV3SPYXR3ANCNFSM4N5TNPYQ.
this.interface.write(answer, 'ascii') should not send a termination byte.
When sending JSON commands, I build the command and add the \u0000 delimiter at the end. I then send that string the same way I am sending "answer". So, since I do not add the termination byte to "answer", it should not send it.
I do see where the python script that @fsalum sent picked up on the incoming \u0000, but I do not see him adding anything to the outgoing string.
I don't suppose you can packet sniff it?
Correct I had to clean the \u0000 from the Hello message but you don’t need to add anything to the challenge answer just the Ascii hex.
I will test the npm link tomorrow. Thanks for working on the fix. 🤞🏻
On Fri, Nov 13, 2020 at 11:59 PM sbozarth notifications@github.com wrote:
this.interface.write(answer, 'ascii') should not send a termination byte.
When sending JSON commands, I build the command and add the \u0000 delimiter at the end. I then send that string the same way I am sending "answer". So, since I do not add the termination byte to "answer", it should not send it.
I do see where the python script that @fsalum https://github.com/fsalum sent picked up on the incoming \u0000, but I do not see him adding anything to the outgoing string.
I don't suppose you can packet sniff it?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/sbozarth/homebridge-lc7001/issues/7#issuecomment-727163302, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAMBOO2LXE6G2AV5OX6VESDSPY2EPANCNFSM4N5TNPYQ .
-- Sent from Gmail Mobile
I made a lot of changes to the processBuffer function to try to deal with split authentication messages. I now blindly prepend any carryover from the previous data received. I doubt there was really ever anything in there, but I used to check if the new data was complete without it and then only prepend if it was not. Now it is just blindly doing it and we will see what happens.
This was committed to the 1.1.x branch.
If the LC7001 is giving an [INVALID] ....
I literally print exactly what it sends in the log. You can try changing this line
this.platform.log.debug('Answer generated:',answer);
to
this.platform.log.debug('Answer generated:',JSON.stringify(answer));
That will uncover any hidden characters.
Having trouble with getting the latest version to work on HOOBS. I'm getting continual JSON parsing errors. Any ideas if this is a bug in the plugin or configuration?
Buffer Contents: 6/14/2020, 4:33:31 PM 8DBF4302B1B882D522F53491F54A0DA3 0026EC02EE11Hello V1 6/14/2020, 4:33:31 PM Error parsing JSON. 6/14/2020, 4:33:31 PM SyntaxError: Unexpected token F in JSON at position 1 6/14/2020, 4:33:31 PM at JSON.parse ()
6/14/2020, 4:33:31 PM at Object. (/home/hoobs/.hoobs/node_modules/homebridge-lc7001/lib/lc7001.js:273:34)
6/14/2020, 4:33:31 PM at Array.forEach ()
6/14/2020, 4:33:31 PM at Object. (/home/hoobs/.hoobs/node_modules/homebridge-lc7001/lib/lc7001.js:270:12)
6/14/2020, 4:33:31 PM at Socket.emit (events.js:210:5)
6/14/2020, 4:33:31 PM at addChunk (_stream_readable.js:309:12)
6/14/2020, 4:33:31 PM at readableAddChunk (_stream_readable.js:286:13)
6/14/2020, 4:33:31 PM at Socket.Readable.push (_stream_readable.js:224:10)
6/14/2020, 4:33:31 PM at TCP.onStreamRead (internal/stream_base_commons.js:182:23)
6/14/2020, 4:33:31 PM Buffer Contents:
6/14/2020, 4:33:31 PM 7F158C6E56C3627E
6/14/2020, 4:33:31 PM Error parsing JSON.
6/14/2020, 4:33:31 PM SyntaxError: Unexpected token F in JSON at position 1
6/14/2020, 4:33:31 PM at JSON.parse ()
6/14/2020, 4:33:31 PM at Object. (/home/hoobs/.hoobs/node_modules/homebridge-lc7001/lib/lc7001.js:273:34)
6/14/2020, 4:33:31 PM at Array.forEach ()
6/14/2020, 4:33:31 PM at Object. (/home/hoobs/.hoobs/node_modules/homebridge-lc7001/lib/lc7001.js:270:12)
6/14/2020, 4:33:31 PM at Socket.emit (events.js:210:5)
6/14/2020, 4:33:31 PM at addChunk (_stream_readable.js:309:12)
6/14/2020, 4:33:31 PM at readableAddChunk (_stream_readable.js:286:13)
6/14/2020, 4:33:31 PM at Socket.Readable.push (_stream_readable.js:224:10)
6/14/2020, 4:33:31 PM at TCP.onStreamRead (internal/stream_base_commons.js:182:23)
6/14/2020, 4:33:31 PM Buffer Contents:
6/14/2020, 4:33:31 PM 7F158C6E56C3627E557DE4B7BA6FD4A3
6/14/2020, 4:33:31 PM Error parsing JSON.
6/14/2020, 4:33:31 PM SyntaxError: Unexpected token F in JSON at position 1
6/14/2020, 4:33:31 PM at JSON.parse ()
6/14/2020, 4:33:31 PM at Object. (/home/hoobs/.hoobs/node_modules/homebridge-lc7001/lib/lc7001.js:273:34)
6/14/2020, 4:33:31 PM at Array.forEach ()
6/14/2020, 4:33:31 PM at Object. (/home/hoobs/.hoobs/node_modules/homebridge-lc7001/lib/lc7001.js:270:12)
6/14/2020, 4:33:31 PM at Socket.emit (events.js:210:5)
6/14/2020, 4:33:31 PM at addChunk (_stream_readable.js:309:12)
6/14/2020, 4:33:31 PM at readableAddChunk (_stream_readable.js:286:13)
6/14/2020, 4:33:31 PM at Socket.Readable.push (_stream_readable.js:224:10)
6/14/2020, 4:33:31 PM at TCP.onStreamRead (internal/stream_base_commons.js:182:23)
6/14/2020, 4:33:31 PM Buffer Contents:
6/14/2020, 4:33:31 PM 7F158C6E56C3627E557DE4B7BA6FD4A3 0026EC02EE11