Dyson Pure Cool Link Internals

Posted on Jan 11, 2016

I got a Dyson Pure Cool Link fan a few days ago and started to tinker with it :-) (after having a look at the app and deleting it after ~5 Minutes. Ugh).

Device Overview

Well: It is a Fan. But a fan with a remote control and Wifi!

Plug it in

The fan can be used without all the Wifi stuff, but that wouldn’t be much fun. Unfortunately, you are forced to install Dysons app which has tons of ‘analytics-tracking’ built in. So we are going to fix that.

Boot it up!

The initial configuration of the device is almost the same as bootstrapping a chromecast: The Fan will act as a Wifi Hotspot. You are supposed to join this network an let the App configure the fan for you (by telling it the ESSID and Password to connect to). After this has been done, the fan will restart, join the Wifi network and start doing things that fans are doing: Such as sending DHCP requests and getting the current time via HTTP (because NTP isn’t web 2.0 ready or so…)

The Fan connects to some interesting domains such as:

  • time.cp.dyson.com (To get the current time from the HTTP header, really.)
  • api.cp.dyson.com (Probably to sync the schedules and upload collected environmental data)
  • broker1.cp.dyson.com (MQTT?!)

Out of these 3 domains, only ’time.cp.dyson.com’ is available via HTTP and doing a MITM attack to the other domains won’t work as the device verifies the certificate signature (Dyson uses its own CA). Patching the Android app to ignore the certificate signature would be pretty simple, but my plan is to get rid of the Dyson cloud, so i didn’t really look into this and started to capture the internal App <-> Fan traffic.

Protocol analysis

Capturing the communication between the Fan and the App is a no brainer as the traffic is unencrypted. I was expecting some strange self-grown protocol, but it turns out that the device uses MQTT which seems to be a rather nice protocol. (Never heard of MQTT before? Me neither - but it doesn’t look that bad :-) )

As you can see from the above screenshot, every MQTT message has a ’topic’ and a ‘payload’. The topic includes the device model (475 in my case), the serial number (NN4-…) and the actual ‘command’ (status/faults).

The payload is simply a ’topic related’ json encoded message.

Writing a client should therefore be pretty easy: My first attempt looked like this:

package main
import (
   "fmt"
   mqtt "github.com/eclipse/paho.mqtt.golang"
)
func main() {
        oson     := "OFF"
        speed    := 9
        opts := mqtt.NewClientOptions().AddBroker("tcp://192.168.1.196:1883")
        opts.SetUsername("NN4-CH-HEA0429A")
        opts.SetPassword("TheTopSecretPassword!")
        client := mqtt.NewClient(opts)
        if token := client.Connect(); token.Wait() && token.Error() != nil {
                panic(token.Error())
        }
        x := client.Publish("475/NN4-CH-HEA0429A/command", 1, false, fmt.Sprintf("{\"msg\":\"STATE-SET\",\"time\":\"2016-10-24T19:45:09Z\",\"mode-reason\":\"LAPP\",\"data\":{\"fmod\":\"FAN\",\"fnsp\":\"%04d\",\"oson\":\"%s\",\"sltm\":\"STET\",\"rhtm\":\"OFF\",\"rstf\":\"STET\",\"qtar\":\"0003\",\"nmod\":\"OFF\"}}", speed, oson))
        x.Wait()
}

Pretty ugly but it works. But wait: How did i get the password and username? Well: They are transmitted as plain text while the App connects to the fan. But how does the App know the credentials?

It turns out that the fan hands them out during the initial bootstrap process (= when it acts as its own access point). The only thing i had to do was to ‘subscribe’ to the 475/initialconnection/credentials topic while the fan is unconfigured: This causes the device to send you the username and password. (Which seems to be per-device-hardcoded: Neither changed even after a hard reset of the device).

What’s next?

I started to write a small golang library to interact with the fan. So far the library is able to:

  • Bootstrap an unconfigured device and dump out the credentials
  • Set the fan speed and other properties
  • Receive status updates from the fan

The source of the library is hosted at https://github.com/adrian-bl/dyslink - but keep in mind that the whole thing is in its early stages: The API is ugly, might change and there isn’t even a sample client included. But this will hopefully change soon as i’m planing to write a simple cli-client and maybe a web frontend. Stay tuned!