iburistu / cync-lan

LAN controller for CYNC smart devices
MIT License
10 stars 3 forks source link

New Servers? #1

Open matthew-mclaren opened 1 year ago

matthew-mclaren commented 1 year ago

Has anyone tested this with newer firmware?

I see my lights are calling out to the below servers

cm.gelighting.com api.gelighting.com

I was able to forward these with Pi-hole and see my server receiving the connection.

I tried adjusting the openssl certs for these but I can't things to connect and talk properly.

"reqId":"req-2","msg":"Route POST:/192.168.1.6 not found"} "reqId":"req-2","res":{"statusCode":404},"responseTime":0.7721000001765788,"msg":"request completed"}

running socat, the device never comes online through the official cync app and I don't get anything useful in the dumps.

iburistu commented 1 year ago

Thanks for the heads-up on the firmware update. I haven't updated my devices in a long time, so didn't even see this come through.

It looks like the new server, cm.gelighting.com, is just a CNAME for cm-iot-ge.xlink.cloud, which has an A record for 35.196.85.236. Checking the cert for that with openssl s_client -showcerts -connect 35.196.85.236:23779 gives that the CN for our self-signed certificate should be *.xlink.cn. The cert is the same for the old server DNS name and IP (cm-ge.xlink.cn & 34.73.130.191) - you can verify with openssl s_client -showcerts -connect 34.73.130.191:23779. I think the old one may still work, because the cert itself expired March of 2021...

Try changing the CN in the certificate creation script to *.xlink.cn and see if that works. I'm currently updating one of my lights to see if I can troubleshoot async.

matthew-mclaren commented 1 year ago

Thanks for the quick response! Changing the certs to *.xlink.cn did help. I am now able to run socat, see the device online through the cync app and capture the traffic.

I also solved the Not Found error by adding "/api/devices" to the curl command. Not sure if this is correct though as the server returns the change but the light does not actually change.

curl -X POST 'http://192.168.1.1:8080/api/devices/192.168.1.2' -H 'Content-Type: application/json' -d '{"status":1}'

iburistu commented 1 year ago

Glad to hear the cert CN change worked! If the redirection is working (which it sounds like it is due to socat relaying the traffic to the Cync servers) then hopefully the server can start picking up on the traffic.

To verify that the LAN server is accepting the traffic, you can set the CYNC_DEBUG environment variable to 1 prior to launching the server. Note that you'll need to stop socat; the smart lights don't like connecting to more than one source at once, and the LAN server will be the only way to control your devices in this configuration (at least with this code, someone smarter than I could find a way to proxy to the Cync servers concurrently). You should hopefully start seeing more details from the LAN server on data being sent back and forth. There's some rudimentary logic that tries to detect what the device is; it should announce it's a "smart light" once the server/client handshake is complete. Once that back-and-forth is done it'll be ready to accept commands. The LAN server just sends the TCP command and hopes for the best...there's no verification that the state has changed, but I think the lights do announce when they change state, but that isn't captured and isn't used ATM.

Apologies for the confusion on the API routes - haven't updated the documentation in a long while!

matthew-mclaren commented 1 year ago

Making some progress! It seems the buffers for the lights (or firmware version) I have are different than what you had. Notable changes so far which have allowed me to turn the lights on and off:

// There is a specific handshake that needs to occur before the client // will accept commands const CLIENT_CONNECTION_REQUEST = Buffer.from([ 0xc3, 0x00, 0x00, 0x00, 0x01, 0x0c, ]); const SERVER_CONNECTION_RESPONSE = Buffer.from([ 0xc8, 0x00, 0x00, 0x00, 0x0b, 0x0d, 0x07, 0xe7, 0x05, 0x16, 0x02, 0x14, 0x2a, 0x3a, 0xfe, 0x0c, ]);

const CMD_TURN_ON = Buffer.from([ 0x73, 0x00, 0x00, 0x00, 0x1f, 0x76, 0xeb, 0x9c, 0x88, 0x3a, 0x5e, 0x00, 0x7e, 0x96, 0x00, 0x00, 0x00, 0xf8, 0xd0, 0x0d, 0x00, 0x96, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd0, 0x11, 0x02, 0x01, 0x00, 0x00, 0x55, 0x7e, ]); const CMD_TURN_OFF = Buffer.from([ 0x73, 0x00, 0x00, 0x00, 0x1f, 0x4b, 0x05, 0xba, 0xbd, 0x3a, 0x43, 0x00, 0x7e, 0x8b, 0x00, 0x00, 0x00, 0xf8, 0xd0, 0x0d, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd0, 0x11, 0x02, 0x00, 0x00, 0x00, 0x49, 0x7e, ]);

If it's any interest to you or any future person who stumble upon this, I've extracted the stock firmware from one of my lights. It's encrypted so I haven't made much progress on it. There are some interesting strings you can pull from the .bin but it's mostly UART commands/messages. I did get into the UART as well but it was pretty locked down. There's a BT_Debug port which may be JTAG but I haven't messed with that. Chipset on my light is RTL8720CM cync_firmware.zip

iburistu commented 1 year ago

Thanks for sharing this zip - I'll poke around when I get some time.

One thing I did to try to minimize the variance between lights and smart switches was zero out any non-essential (through trial and error) bytes of the message. It looks like those might be required? For reference, I'm using the Direct Connect White Smart Bulbs with version 1.0.241 (latest available ATM). There might be different initialization sequences based on the product, but what I had worked with these bulbs, the smart switch, and RGB light strip when I was first developing this last year.

If you could - try zeroing out different bytes within the messaging for your lights & firmware. Let's see if there's a specific bit different that's causing grief here. There might be hints of what to send based on initial setup data.

matthew-mclaren commented 1 year ago

I made a few big breakthroughs. The protocol for my lights does seem to be a little different. The lights I have are CYNC Full Color Model CLEDA199CD1B. While they are direct connect, they also create a bluetooth mesh network which needs to be taken into account as each light is assigned an ID which is sent in the command. This ID is more important than the IP the command is sent to.

I used Cync_Data to find the IDs of all my devices. The IDs show up in the Device ID Array for the lowest group. In my case, this was 1-3 for my 3 lights in the order which they were added.

Then through analyzing hundreds of commands and banging my head against the wall, I discovered the below. Pasting everything in decimal as it's easier to read. I zeroed out all the bits I could.

For turning the lights on.:

115, 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 126, 134 (this number is incremented by the server for each command but it does not need to change), 0, 0, 0, 248, 208, 13, 0, 134 (duplicate of the incremental number), 0, 0, 0, 0, 3 (This is the light ID), 0, 208, 17, 2, 0 (off), 0, 0, 73 (checksum = incremental number - 64 (not sure why) + ID), 126

To turn on, the 0 changes to a 1 and the checksum increases by 1

To change color:

115, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 126, 65 (incremental number), 0, 0, 0, 248, 240, 16, 0, 65, 0, 0, 0, 0, 1 (ID), 0, 240, 17, 2, 1, 255, 254, 255 (R), 4 (G), 0 (B), 70 (Checksum = incremental + ID + RGB, 126

To change brightness 48%

115, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 126, 136 (incremental number), 0, 0, 0, 248, 240, 16, 0, 136, 0, 0, 0, 0, 1 (ID), 0, 240, 17, 2, 1, 48 (Brightness), 255, 255, 255, 255, 185 (Checksum = incremental + ID + Brightness), 126

I have also seen the device ID set to 0 and a 128 or 255 in the next place which can be used to control groups. Managing each light independently is a good start for me. I'm not super familiar with JavaScript but I will try to integrate my changes and get this server to work with this protocol.

iburistu commented 1 year ago

Thanks for putting this together! This is really useful information to know, especially the findings with the incremental bits and the checksum.

I think I may have inadvertently noticed (and removed) the device ID field you found. When I was building this originally I found that sometimes sending a command to one light over TCP turned the other on - I think by zeroing out the device ID bit the recipient of the TCP message takes the action / command, but setting the device ID bit to anything but zero sends it to the targeted IP, but gets forwarded to the mesh if it doesn't match the ID set in device setup (?). Does sending a message to a light directly cause it to take action? Or for your setup is the device ID bit required? I will try messing around with groups to see if I can recreate your findings - I think having individual control + group control would be very very useful. I wonder if there's any data that is transferred during the initial setup sequence that can indicate what bits should be set per model / if there's device & group ID data included. If so, it could be dynamically set on initial connect.

I tried poking around in the binary you shared and I only have a small lead. There's two pieces of information that might help us out: an XLink simulator repo, and the product key and product ID found in the binary. I got very close to determining how to call the XLink APIs for a OTA update binary, but I got stuck on the authorization code (the API calls are also in the binary).

curl https://api-ge.xlink.cn/v2/device_login -H 'Content-Type: application/json' -d '{"product_id":"xxx","mac":"11111111111","authorize_code":"???"}'

and

curl https://api-ge.xlink.cn/v2/upgrade/firmware/check/

I think with a valid authorization code and MAC address we could get an OTA binary, unencrypted, to investigate further. I had a little luck with decompiling the binary with the Xtensa architecture, but I'm not experienced enough with that architecture to understand any of it.

Let me try to recreate what you found - I'm in the process of updating my local infrastructure with Pi 4's so may be a bit :)

matthew-mclaren commented 1 year ago

I checked my cync data file and that pulled the authorization code from Cync's server.

I plugged that in and got the below response

{"access_token":"xxxx","refresh_token":"xxxx","device_id"xxx,"expire_in":7200}

I'm not sure what to do with this though. When I run

curl https://api-ge.xlink.cn/v2/upgrade/firmware/check/

I get "Unresolvable URL:"

iburistu commented 1 year ago

This is huge. How did you pull the authorization code? I've been stumped on that step for a bit now.

Once we have the access token, we can use that in future requests. I would assume it's something like bearer auth, which you can find details for here. If we include the provided access token in the Authorization header, we might be able to get somewhere.

There may be an endpoint at https://api-ge.xlink.cn/v2/upgrade/firmware/check/%d, where %d is some number or string (version number)? We may need to fuzz it a bit. For example, here's what I can get without an access token:

$ curl -H 'Authorization: Bearer xxx' https://api-ge.xlink.cn/v2/upgrade/firmware/check/1
{"error":{"msg":"have not Access-Token ","code":4031002}}

$ curl -H 'Access-Token: xxx' https://api-ge.xlink.cn/v2/upgrade/firmware/check/1
{"error":{"msg":"Access-Token Expired","code":4031021}}

Could also be something along the lines of https://api-ge.xlink.cn/v2/upgrade/firmware/report/%d? There appears to be some way to get a download URL that we can snag with a GET request. Once we get that we'll have a raw OTA binary to dissect.

matthew-mclaren commented 1 year ago

I used this script to get the authorization code https://github.com/nikshriv/cync_data/blob/main/cync_data.py

It requires a cync login/2FA like signing into the app and saves a cync_data.json with all the device/account info.

I ran this weeks ago to pull all the data for my devices and the authorization code it pulled at that time still works.

iburistu commented 1 year ago

It looks like nikshriv has a much more complete understanding of the Cync devices in his cync_lights repo. I'll see what can be adapted here!

I wasn't able to get any of the binaries from the OTA routes - were you able to get anything?

baudneo commented 8 months ago

Hey, I have ported the LAN server logic over to python but I need some help decoding packets. I am hoping someone finds this and can help me out a bit. The goal is to have an async mqtt pub/sub instance as well to integrate with home assistant.

This script is for newer firmware devices. The bulb I am using to test all of this with is a direct connect full color a19 bulb on firmware: 1.0.361

For newer firmware

Gist to python LAN server: https://gist.github.com/baudneo/4844c65505ff3809655fb2122dbb910f

The only dep should be uvloop. Copy and paste into a file (for this example cync-lan.py) chnage the CYNC_CERT and CYNC_KEY vars to proper locations and then run python3 /path/to/cync-lan.py.

I muted some of the chattier packets that seem to be some sort of ping. Some of the original data structures still work but the status packets are different. I havent touched control packets, I did socat traffic between APP<->Cloud Server but need help decoding that data as well.

Looking forward to any help. My fork of your project has a python branch with the python version in it as well: https://github.com/baudneo/cync-lan/tree/python