Reverse engineering the ChefSteps Joule (and making a chrome extension)

Dennis Li
10 min readAug 14, 2017

--

Video showing off my chrome extension

FYI: I am always looking to work with interesting clients, so please free to message me at dli.dennsli@gmail.com. Aside from reverse engineering things, I am primarily a consultant in data science/machine learning, and engineering (ideally with functional/typed languages!).

The title might be misleading, so just to be clear, this isn’t about reverse engineering the hardware of the ChefSteps Joule (although that does sound like it could be a fun project…!). This post is instead about about reverse engineering how to communicate with the Joule, since there’s no publicly accessible/documented API. This proved to be a surprisingly involved project, and so I wanted to write about how I went about it.

If you just want see/use the end results, i.e. a chrome extension that lets you control your Joule, it’s available on the chrome app store, here.

If you’re looking to do a reverse engineering project yourself, you may find this interesting. If you just want to see code and documentation, that’s all available on the github repo, here.

Introduction

Anyone who’s had the (terrific) pleasure of my company over the past few months has probably quickly come to the conclusion that I cook a lot these days. I’m pretty much incapable of discussing anything else, and tend to find myself changing every topic of conversation to food science, interesting recipes/ingredients, new restaurants, etc . if you don’t want to talk about food, well… I don’t think we should be friends.

My recently acquired prep table

I’ve become particularly enamored with modernist cooking, and so I’ve come to love both Modernist Cuisine and chefsteps.com (psst psst, Modernist Bread is coming out soon and would make a pretty terrific gift for a certain someone…! ;)). Aside from their great content, ChefSteps also sells a (very conveniently sized) immersion circulator/sous vide — the Joule.

In regular use, the Joule is a delightful device. It‘s really small, which matters a lot when you’re living in a San Francisco Victorian apartment. And it’s got a great app to go along with. (If you cook, a sous vide is a great piece of equipment to have around. I don’t think it really matters too much which one you get. I’ve owned an Anova before, and it worked well, but I just personally prefer the Joule.)

My only complaint of the Joule, though, probably to minimize its form factor, is that it lacks physical controls. Instead, in the spirit of Internet of Things, it’s primarily accessible through smartphone apps. This affects me in two ways I hadn’t anticipated, and which motivated this project: (1) When I cook, I like to keep my laptop around, not my phone, so that I can take notes and document what I do. (2) Other times, I am in bed on my laptop and want to get the sous vide started. And my phone is located just far enough away to be not in arm’s length.

Yes, there are a few more practical solutions to this. I could get a second phone charger and keep my phone closer to me. I could remember to take my phone with me before I cook. And more recently, I discovered, there exists a ChefSteps Facebook bot which would’ve sufficed. But that’s besides the point — I hadn’t really thought this through when I started this project, and this is more fun, anyway. And truthfully, I only discovered the bot when I was nearly finished with this.

Listening in on network traffic

My initial thought was that in order to reverse engineer the app, I could just listen in on the network traffic from the smartphone app, and there’d likely exist a set of REST endpoints I could use to communicate with the Joule.

Since this is a smartphone app, and not a website, there’s a bit more work involved here than just firing up Chrome Developer tools or Firebug. Instead, you need to set up what’s called a man-in-the-middle proxy. The idea is that you proxy traffic from your phone to your computer, and then your computer pipes that traffic to your router. Meanwhile, WireShark spies in on and records all that network traffic going through your computer from your phone. This is also how you can setup a man in the middle attack (see here for more information).

Diagram from https://blog.heckel.xyz/2013/07/01/how-to-use-mitmproxy-to-read-and-modify-https-traffic-of-your-phone/

For anyone looking to do this something similar with an android device like me, there are a few extra initial steps to go through. After Android Nougat, Google changed the default behavior of apps to be distrustful of custom Certificate Authorities (probably for the better). There are a few solutions to this. The easiest is probably using AddSecurityExceptionAndroid.

On your computer, you’ll need to get a proxy running. I just used the aptly named MITMProxy. You’ll be able to see the individual network requests just fine through MITMProxy, but I wanted to see them through WireShark. In which case, you’ll need to set up a Key Log File, so that WireShark can decrypt each TLS session. Setup WireShark as described here to point to the Key Log File from MITMProxy.

Once I got this all set up, it seemed to work. I saw a few POSTs to chefsteps.com/v0/api/cookHistory which looked promising — each request body had fields for the target cook temperature, cook time, etc etc with an oauth token. The request body looks something like the following:

{  
"externalId":"e5KMMznx",
"startTime":"1502381284115",
"startedFrom":"jouleApp",
"program":{
"cookTime":4800,
"guide":null,
"programType":"MANUAL",
"setPoint":80, // target cook temperature, in centigrade
"cookId":"e2c1fe961e3d4bd9a44fa39a095f79a1",
"programMetadata":{
"guideId":null,
"cookId":"e2c1fe961e3d4bd9a44fa39a095f79a1",
"timerId":null,
"programId":null
}
}
}

After a bit of fumbling around in Postman, “Aha!,” I thought. I had successfully replicated a few requests and saw a new item in my cook history. And so, I pulled myself out of bed, and waddled over to my kitchen, hoping to see my Joule chugging along. Of course, as these things tend to go, my Joule was idle, staring back at me, blissfully unaware (or intentionally disobedient?) of any supposed cooking responsibilities it had been tasked.

I poked at a few other HTTP endpoints I saw in WireShark. Still no success. It turns out, as the endpoint name would’ve suggested, I was only modifying my cook history.

I suspected that the WebSocket packets I was ignoring were being used to communicate with the Joule, not the HTTP requests. Unfortunately, the data of all of these WebSocket packets just appeared as nonsensical binary. In an attempt to decode this binary, I found myself in a rabbit hole of docs, including the WebSocket spec. At which point I reflected that unless I was planning on writing my own WebSockets implementation from scratch, this was probably unnecessary. Maybe trying to read some of the ChefSteps Joule compiled app code was not so unreasonable of a way to go about things, after all.

Reverse-engineering the Joule Android app

In Android land, you can just rename an app’s extension from .apk to .zip and extract to get the compiled java and app resources. Or you can use stuff like apktool, which will also decompile the assembled java .dex files back to .java. Either way, you’ll get a directory structure that looks like this:

├── com.chefsteps.circulator
│ ├── AndroidManifest.xml
│ ├── apktool.yml
│ ├── assets
│ │ ├── cdvasset.manifest
│ │ ├── www
.
.
.

If you look through the assets directory, in addition to the font/image files, you’ll also find a lot of javascript. And thankfully, this is where all the relevant logic happens. (I was curious about this, and after some googling, it turns out that the Joule app was written in Ionic — a javascript framework built on Angular that compiles into both.).

While some of the javascript was uglified (chefsteps.js), some of it was only bundled and still had comments(bundle.js). This meant reverse engineering the mobile app wasn’t quite as unreasonable as expected. There was also a proto/ subdirectory which explained of what was happening in the WebSockets binary — it was encoded in protobuffers.

With the protobuf files, I tried my hand again at decoding the WebSocket packets. No luck. It seems that although some protobuf dissectors exist for WireShark, they’re limited to UDP for now. I also briefly tried setting up a barebones web app which used these protobuf files with protobuf.js, but was unsuccessful. I could initiate a WebSockets connection, but I couldn’t send replicate any of the packets I saw in WireShark. Perhaps someone with more experience can chime in here — I don’t see why this approach didn’t work. I suspect that it has something to do something with differences in how the protobuf messages are framed when used in TCP (which WebSockets uses).

Bootstrapping with Joule javascript

After all but giving up on the WireShark approach, I decided to do this by bootstrapping the javascript from my chrome extension with the javascript extracted from their Android App.

This meant also getting a rough understanding of how their app code worked. And reading bundled/uglified javascript is a pretty hairy task. (Who’d have thought it’s not designed to with human readability in mind?!) I optimistically hoped that there was a silver bullet (single module) that I could import over to handle all of the communication logic. So I’d grep for things like WebSocket, and find stuff like ...this.app.factory(“WebSocketAddressConnection”, and then grep for WebSocketAddressConnection, ad infinitum... As usual, my hopes were dashed and there was no silver bullet. Instead, there was a whole bunch of interconnected modules responsible for communication with the Joule, i.e. something any reasonable software engineer probably would’ve assumed. So I had to diagram it all out.

I used unbundle.js to unbundlebundle.js(it didn’t work for chefsteps.js) and then used madge to draw out a dependency graph of its many modules. Seeing the diagram was pretty helpful in understanding how everything fit together. bundle.js contained a lot of Joule (also referred to as a circulator) specific code, including an SDK for interacting with it. chefsteps.js involved a lot of app specific code, and implementations of some of the abstract classes contained in the SDK. There was also a circulator simulator in there, but I never got around to reading through it.

Diagram of the code that was based on the output of madge, with comments copied over from bundle.js. It’s not completely accurate, but was good enough for me to have a rough idea of how the SDK worked

Trying to make sense of chefsteps.js was much more arduous of a process. You can run uglified code through an autoformatter, but the variable names will still be pretty nonsensical. However, because of how angular works, you can quickly figure out what most of the variables in any module did. For example, in this.app.factory("ModuleName", ["import1", "import2"], function(a, b){...} , a points to import1 and b points to import2. And then when combined with VSCode, which makes javascript development almost sane, you can safely rename variables (see what I mean?). And then this sort of becomes a game of sudoku. Through process of elimination, you can figure out what the remaining variables do within a module.

So, I had a a pretty handy diagram of bundle.js and a slightly more human readable version of chefsteps.js as models. But I stalled off writing code, because even if it was all javascript, I still was a little bit (read: very) scared of the abstract prospect of possible possible framework interoperability issues from library versioning issues, node specific libraries, etc all running in the same context. After Googling around a bit, I learned that thankfully, Webpack lets you shim external libraries and protect yourself from these sorts of things. Webpack is pretty magical! All it took was this line require(“exports-loader?window!./bundle.js”).

I think the README in the repo provides a decent and more in depth understanding of how the modules in bundle.js and chefsteps.js work. But on a high level, the SDK didn’t deal with the details of the connection method (bluetooth, websockets, etc), but instead, managing the data and such going through that connection. It was up to the app specific code (chefsteps.js) to provide implementations of the connections. So to port this over to Web, all I had to do was to replicate the connection logic from chefsteps.js. I’m not going to describe this — if you’re curious, I’d just read the code in the repo.

Anyway, after I did all this, and gave it a whirl, it surprisingly, for the first time so far, basically “just worked”. I had gotten my Joule to listen to my commands, and it obediently obliged.

After this, developing the view logic of the chrome extension was comparatively much more straightforward. And I even tried putting on my designer hat with material-ui to make it look pretty! And the circulator SDK was pretty great to work with, which made it pretty easy to add useful features to the chrome extension like live data/updates.

Original UI. I think this was pretty great tbh
Look at me, using design libraries and stuff!

Well, that’s about all I have to say. And if you’ve read this to the end, thanks. Let me know if you have any questions or thoughts.

Other interesting discoveries:

  • There’s a secret developer menu in the app. I won’t tell you how to get access though, you’ll have to figure it out yourself ;)
  • The max temperature is dynamically set to avoid a roiling boil. The Joule has an on board pressure sensor to figure this out! I thought this was really cool and shows off some of the unexpected engineering that goes into hardware like the Joule.
  • After a while of staring at uglified code, you realize that the uglifier does some pretty curious things, like: turning if/else statements to ternary operations; converting multiple lines into a single massive return statement; etc. Or at least, I assume ChefSteps engineers don’t write their code that way :) It’d be interesting to see a javascript beautifier that can make more human readable code that can also undo each of these uglify.js strategies. Or maybe they already do this, and I’m just unaware.

--

--