|
@@ -0,0 +1,233 @@
|
|
1
|
+---
|
|
2
|
+layout: post
|
|
3
|
+title: Reverse engineering the Sense API
|
|
4
|
+strapline: Who needs API docs when you can do a MITM attack?
|
|
5
|
+---
|
|
6
|
+
|
|
7
|
+### Meet Sense
|
|
8
|
+
|
|
9
|
+<div class="image right">
|
|
10
|
+ <img src="/res/images/sense/sense.jpg" alt="Sense">
|
|
11
|
+</div>
|
|
12
|
+
|
|
13
|
+<a href="https://hello.is/">Sense</a> is a little device that sits by your bedside and, in
|
|
14
|
+conjunction with a little 'pill' attached to your pillow, monitors your sleeping patterns and
|
|
15
|
+any environmental conditions that might hamper them. Android and iOS apps show you your sleep
|
|
16
|
+history, and offer suggestions for improvements.
|
|
17
|
+
|
|
18
|
+<!--more-->
|
|
19
|
+
|
|
20
|
+Sense was <a href="https://www.kickstarter.com/projects/hello/sense-know-more-sleep-better/description">Kickstarted</a>
|
|
21
|
+in August 2014, raising over 2.4 million US dollars, and shipped to backers in mid 2015. The
|
|
22
|
+campaign blurb included this snippet:
|
|
23
|
+
|
|
24
|
+> Building with Sense
|
|
25
|
+>
|
|
26
|
+> You'll always have access to your data via our API. Take it, play with it, graph it, do whatever
|
|
27
|
+> you want with it. It's yours. That's important to us.
|
|
28
|
+>
|
|
29
|
+> We enjoy tinkering with and building on-top of other products we like. Sense will let you have
|
|
30
|
+> that experience.
|
|
31
|
+>
|
|
32
|
+> We'd love to hear your thoughts on what you might want to build with Sense, and how you could
|
|
33
|
+> directly interact with the hardware, and the data it collects.
|
|
34
|
+
|
|
35
|
+Sounds great! But a year after shipping, there's no sign of an API, and some of us who enjoy
|
|
36
|
+tinkering are getting a bit restless...
|
|
37
|
+
|
|
38
|
+### Reverse engineering to the rescue
|
|
39
|
+
|
|
40
|
+Data from the Sense hardware is transmitted off onto the Internet somewhere, and then pulled
|
|
41
|
+down by the mobile apps. If we can snoop that traffic, we can probably figure out how to grab
|
|
42
|
+the data from the Sense.
|
|
43
|
+
|
|
44
|
+So the first step is to capture the raw network traffic while the Sense and the app are in use. I
|
|
45
|
+crafted an extremely over-the-top setup that involves my phone and the Sense being on a new,
|
|
46
|
+separate wireless network that gateways through a Linux server that can look in on all the
|
|
47
|
+traffic (because you never know when a dedicated capturing network will come in handy, right?...)
|
|
48
|
+
|
|
49
|
+Running `tshark` while the app and device are active showed they were making HTTPS requests an
|
|
50
|
+Amazon Elastic Cloud instance. That's not really much use, as we can't see the content of the
|
|
51
|
+requests. The next step is to move up the network stack and target the HTTPS traffic specifically.
|
|
52
|
+Step in, <a href="https://mitmproxy.org/">`mitmproxy`</a>. This automates man-in-the-middle attacks
|
|
53
|
+on HTTPS requests. When it receives such a request, it cooks up its own certificate and sends that
|
|
54
|
+to the client, and then sits in between while the client and the server communicate.
|
|
55
|
+
|
|
56
|
+The certificates generated by `mitmproxy` aren't issued by a trusted source, so (well behaved)
|
|
57
|
+browsers and apps will throw up lots of warnings and refuse to communicate with it for the most
|
|
58
|
+part. This is the mechanism that stops anyone impersonating your banking website when you access it
|
|
59
|
+over HTTPS, so is generally a very good thing. In this case, both the Sense itself and the Android
|
|
60
|
+app refused to talk with the blatant attacker.
|
|
61
|
+
|
|
62
|
+We control the Android phone, though, so can just tell it that we trust the dodgy certificate
|
|
63
|
+issuer. This involves grabbing the certificate generated by mitmproxy, and adding it to Android's
|
|
64
|
+key store. The process is <a href="http://docs.mitmproxy.org/en/stable/certinstall.html#installing-the-mitmproxy-ca-certificate-manually">documented nicely in the mitmproxy docs</a>
|
|
65
|
+and is pretty straightforward. With Android trusting our certificate authority, the Sense app
|
|
66
|
+starts talking through the proxy and we can look at the plain HTTP requests.
|
|
67
|
+
|
|
68
|
+### The API
|
|
69
|
+
|
|
70
|
+Logging in to the Android app and navigating through the various screens shows that the API is
|
|
71
|
+actually pretty nice. It's RESTful, and uses an OAuth bearer token for authorisation, as you can
|
|
72
|
+see in the `mitmproxy` output of the authentication and first few requests:
|
|
73
|
+
|
|
74
|
+{% highlight text %}
|
|
75
|
+POST https://api.hello.is/v1/oauth2/token
|
|
76
|
+ ← 200 application/json 151B 1.17s
|
|
77
|
+GET https://api.hello.is/v2/account/preferences
|
|
78
|
+ ← 200 application/json 124B 109ms
|
|
79
|
+GET https://api.hello.is/v1/account
|
|
80
|
+ ← 200 application/json 181B 388ms
|
|
81
|
+GET https://api.hello.is/v2/devices
|
|
82
|
+ ← 200 application/json 235B 126ms
|
|
83
|
+POST https://api.hello.is/v1/app/checkin
|
|
84
|
+ ← 200 application/json 106B 119ms
|
|
85
|
+GET https://api.hello.is/v2/insights
|
|
86
|
+ ← 200 application/json 1.4kB 157ms
|
|
87
|
+POST https://api.hello.is/v1/notifications/registration
|
|
88
|
+ ← 204 [no content] 364ms
|
|
89
|
+GET https://api.hello.is/v2/timeline/2016-04-09
|
|
90
|
+ ← 200 application/json 160B 550ms
|
|
91
|
+GET https://api.hello.is/v1/questions?date=2016-04-10
|
|
92
|
+ ← 200 application/json 284B 132ms
|
|
93
|
+PATCH https://api.hello.is/v1/app/stats
|
|
94
|
+ ← 202 [no content] 126ms
|
|
95
|
+GET https://api.hello.is/v1/app/stats/unread
|
|
96
|
+ ← 200 application/json 71B 171ms
|
|
97
|
+{% endhighlight %}
|
|
98
|
+
|
|
99
|
+#### Authorisation
|
|
100
|
+
|
|
101
|
+To get a bearer token, you send a POST request to `/v1/oauth2/token`, supplying the username and
|
|
102
|
+password, and a client ID and secret. Presumably if the API is ever opened up you'll be able to
|
|
103
|
+register clients and get your own ID and secret, but for now reusing the ones from the Android
|
|
104
|
+app works fine. [I've prettified and linewrapped the content to make it a bit easier to read.]
|
|
105
|
+
|
|
106
|
+{% highlight http %}
|
|
107
|
+POST /v1/oauth2/token HTTP/1.1
|
|
108
|
+Host: api.hello.is
|
|
109
|
+Accept: */*
|
|
110
|
+Content-Length: 123
|
|
111
|
+Content-Type: application/x-www-form-urlencoded
|
|
112
|
+
|
|
113
|
+grant_type=password
|
|
114
|
+ &client_id=8d3c1664-05ae-47e4-bcdb-477489590aa4
|
|
115
|
+ &client_secret=4f771f6f-5c10-4104-bbc6-3333f5b11bf9
|
|
116
|
+ &username=USERNAME
|
|
117
|
+ &password=PASSWORD
|
|
118
|
+{% endhighlight %}
|
|
119
|
+{% highlight http %}
|
|
120
|
+HTTP/1.1 200 OK
|
|
121
|
+Cache-Control: no-cache
|
|
122
|
+Content-Type: application/json
|
|
123
|
+Date: Sun, 10 Apr 2016 17:25:16 GMT
|
|
124
|
+transfer-encoding: chunked
|
|
125
|
+Connection: keep-alive
|
|
126
|
+
|
|
127
|
+{
|
|
128
|
+ "token_type":"Bearer",
|
|
129
|
+ "expires_in":31536000,
|
|
130
|
+ "account_id":"1234",
|
|
131
|
+ "access_token":"2.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
132
|
+ "refresh_token":"2.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
|
|
133
|
+}
|
|
134
|
+
|
|
135
|
+{% endhighlight %}
|
|
136
|
+
|
|
137
|
+#### Sleep timeline
|
|
138
|
+
|
|
139
|
+Getting the timeline for one night is just a straight up GET request. It gives a detailed log of
|
|
140
|
+events, including different 'depths' of sleep (I've cut out a whole bunch here for simplicity).
|
|
141
|
+It also shows metrics relating to the entire night, and their condition ('warning' or 'ideal').
|
|
142
|
+
|
|
143
|
+{% highlight http %}
|
|
144
|
+GET /v2/timeline/2016-04-08 HTTP/1.1
|
|
145
|
+Host: api.hello.is
|
|
146
|
+Accept: */*
|
|
147
|
+Authorization: Bearer 2.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
148
|
+{% endhighlight %}
|
|
149
|
+{% highlight http %}
|
|
150
|
+HTTP/1.1 200 OK
|
|
151
|
+Cache-Control: no-cache
|
|
152
|
+Content-Type: application/json
|
|
153
|
+Date: Sun, 10 Apr 2016 17:29:00 GMT
|
|
154
|
+transfer-encoding: chunked
|
|
155
|
+Connection: keep-alive
|
|
156
|
+
|
|
157
|
+{
|
|
158
|
+ "score":76,
|
|
159
|
+ "score_condition":"WARNING",
|
|
160
|
+ "message":"You were asleep for **8.2 hours**, and sleeping soundly for 3.2 hours.",
|
|
161
|
+ "date":"2016-04-08",
|
|
162
|
+ "events":[
|
|
163
|
+ {
|
|
164
|
+ "timestamp":1460168700000,
|
|
165
|
+ "timezone_offset":3600000,
|
|
166
|
+ "duration_millis":60000,
|
|
167
|
+ "message":"You went to bed.",
|
|
168
|
+ "sleep_depth":0,
|
|
169
|
+ "sleep_state":"AWAKE",
|
|
170
|
+ "event_type":"GOT_IN_BED",
|
|
171
|
+ "valid_actions":["ADJUST_TIME","VERIFY","INCORRECT"]
|
|
172
|
+ },{
|
|
173
|
+ "timestamp":1460169600000,
|
|
174
|
+ "timezone_offset":3600000,
|
|
175
|
+ "duration_millis":60000,
|
|
176
|
+ "message":"You fell asleep.",
|
|
177
|
+ "sleep_depth":100,
|
|
178
|
+ "sleep_state":"SOUND",
|
|
179
|
+ "event_type":"FELL_ASLEEP",
|
|
180
|
+ "valid_actions":["ADJUST_TIME","VERIFY","INCORRECT"]
|
|
181
|
+ },{
|
|
182
|
+ "timestamp":1460177040000,
|
|
183
|
+ "timezone_offset":3600000,
|
|
184
|
+ "duration_millis":60000,
|
|
185
|
+ "message":"You were moving around quite a bit.",
|
|
186
|
+ "sleep_depth":2,
|
|
187
|
+ "sleep_state":"AWAKE",
|
|
188
|
+ "event_type":"GENERIC_MOTION",
|
|
189
|
+ "valid_actions":["VERIFY","INCORRECT"]
|
|
190
|
+ },{
|
|
191
|
+ "timestamp":1460199000000,
|
|
192
|
+ "timezone_offset":3600000,
|
|
193
|
+ "duration_millis":60000,
|
|
194
|
+ "message":"Good morning.",
|
|
195
|
+ "sleep_depth":0,
|
|
196
|
+ "sleep_state":"AWAKE",
|
|
197
|
+ "event_type":"WOKE_UP",
|
|
198
|
+ "valid_actions":["ADJUST_TIME","VERIFY","INCORRECT"]
|
|
199
|
+ }
|
|
200
|
+ ],
|
|
201
|
+ "metrics":[
|
|
202
|
+ { "name":"total_sleep", "value":490, "unit":"MINUTES", "condition":"IDEAL" },
|
|
203
|
+ { "name":"sound_sleep", "value":190, "unit":"MINUTES", "condition":"IDEAL" },
|
|
204
|
+ { "name":"time_to_sleep", "value":15, "unit":"MINUTES", "condition":"IDEAL" },
|
|
205
|
+ { "name":"times_awake", "value":2, "unit":"QUANTITY", "condition":"IDEAL" },
|
|
206
|
+ { "name":"fell_asleep", "value":1460169600000, "unit":"TIMESTAMP", "condition":"IDEAL" },
|
|
207
|
+ { "name":"woke_up", "value":1460199000000, "unit":"TIMESTAMP", "condition":"IDEAL" },
|
|
208
|
+ { "name":"temperature", "value":null, "unit":"CONDITION", "condition":"IDEAL" },
|
|
209
|
+ { "name":"humidity", "value":null, "unit":"CONDITION", "condition":"IDEAL" },
|
|
210
|
+ { "name":"particulates", "value":null, "unit":"CONDITION", "condition":"WARNING" },
|
|
211
|
+ { "name":"light", "value":null, "unit":"CONDITION", "condition":"IDEAL" },
|
|
212
|
+ { "name":"sound", "value":null, "unit":"CONDITION", "condition":"IDEAL"}
|
|
213
|
+ ]
|
|
214
|
+}
|
|
215
|
+{% endhighlight %}
|
|
216
|
+
|
|
217
|
+#### Other interesting resources
|
|
218
|
+
|
|
219
|
+Pretty much everything displayed in the app is available as a REST resource. They clearly
|
|
220
|
+put some thought into API design, which makes it even more of a shame that they haven't made it
|
|
221
|
+public yet. Some of the other interesting resources are:
|
|
222
|
+
|
|
223
|
+* `/v1/alarms`: the alarms set on the Sense.
|
|
224
|
+* `/v1/room/current?temp_unit=c`: gives the latest environmental readouts from the Sense.
|
|
225
|
+* `/v1/room/all_sensors/hours?quantity=2&from_utc=<timestamp>`: sensor history.
|
|
226
|
+* `/v2/devices`: the paired devices, including battery status of pills.
|
|
227
|
+* `/v2/insights`: JSON representation of the cards displayed on the main screen (recommendations,
|
|
228
|
+ analysis, etc).
|
|
229
|
+* `/v2/trends/LAST_WEEK` (or `MONTH` or `3_MONTHS`): gives graph data for sleep scores, duration
|
|
230
|
+ and depths.
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+Now the API is figured out, all that's left is to build something that uses it...
|