micolous / helvetic

Hacking / reverse engineering the Fitbit Aria (WiFi enabled bathroom scales)
GNU Affero General Public License v3.0
61 stars 11 forks source link

scale/validate api #1

Closed urandom2 closed 7 years ago

urandom2 commented 7 years ago

While playing around with this tool set, I realized that for my scale (tested on v37 and v39) the scale hit the scale/register endpoint, then the scale/validate endpoint; this code (in helvetic/views/aria_api.py) seems to solve the problem:

class ScaleRegisterView(View):
        def get(self, request):
                if 'serialNumber' not in request.GET:
                        return HttpResponseBadRequest('serialNumber missing')
                if 'token' not in request.GET:
                        return HttpResponseBadRequest('token missing')
                if 'ssid' not in request.GET:
                        return HttpResponseBadRequest('ssid missing')

                serial = request.GET['serialNumber'].upper()
                token = request.GET['token']
                ssid = request.GET['ssid']

                if len(serial) != 12:
                        return HttpResponseBadRequest('serialNumber must be 12 bytes')
                if any(((x not in hexdigits) for x in serial)):
                        return HttpResponseBadRequest('serial must only contain hex')

                # Lookup the authorisation token
                auth_token = AuthorisationToken.lookup_token(token)

                if auth_token is None:
                        return HttpResponseForbidden('Bad auth token')

                owner = auth_token.user

                # Register the Aria
                scale = Scale.objects.create(
                        hw_address=serial,
                        ssid=ssid,
                        owner=owner,
                )

                # Only return 200 OK
                return HttpResponse('')

class ScaleValidateView(View):
        def get(self, request):
                if 'serialNumber' not in request.GET:
                        return HttpResponseBadRequest('serialNumber missing')
                if 'token' not in request.GET:
                        return HttpResponseBadRequest('token missing')

                serial = request.GET['serialNumber'].upper()
                token = request.GET['token']

                if len(serial) != 12:
                        return HttpResponseBadRequest('serialNumber must be 12 bytes')
                if any(((x not in hexdigits) for x in serial)):
                        return HttpResponseBadRequest('serial must only contain hex')

                # Lookup the authorisation token
                auth_token = AuthorisationToken.lookup_token(token)

                # Delete the token.
                auth_token.delete()

                if auth_token is None:
                        return HttpResponseForbidden('Bad auth token')

                # Only return 200 OK
                return HttpResponse('T')

But, when I log the traffic between the scale and the actual endpoint, it never queries scale/verify; prototyping was done by manually hitting this endpoint: http://fitbit.com/scale/verify?serialNumber=<serial_number>&token=<token> This always returns a 200 OK with the character F as the body, I assumed that this this was the failure state, swapped it to a T and kept the 200 OK. Finally I do not like the dangling auth_token, but since scale/verify needs it, there does not appear to be any other way.

PR can be generated upon request.

micolous commented 7 years ago

I'm not sure where that verify call is coming from:

$ strings dev/helvetic/firmware/firmware-35.dat  | grep verify
$ strings dev/helvetic/firmware/firmware-39.dat  | grep verify

But there is a /validate call, maybe you mean this?

$ strings dev/helvetic/firmware/firmware-35.dat | grep scale/
...SNIP...
/scale/upload
/scale/register?serialNumber=%02X%02X%02X%02X%02X%02X&token=%.31s&ssid=
/scale/validate?serialNumber=%02X%02X%02X%02X%02X%02X&token=%.31s
/scale/register
/scale/validate
/scale/firmware
/scale/ssid_info.js
...SNIP...

There's also similar hits in the V39 firmware. I don't have a V37 firmware to compare, though.

I don't recall seeing /scale/validate hits, but it would have been a while ago (2014) that I last tried to configure this.

I'm not sure where those hits are coming from though. I'm half tempted just to have a /scale/validate endpoint that always returns T, because the only thing we actually care about in the process is /scale/register.

(Also aware that firmware/ is missing from the repo -- they're binary blobs that aren't mine to publish. There's details on how to acquire it with firmware.md.)

micolous commented 7 years ago

My theory would be that /scale/register is run as an asynchronous process on the real Fitbit server, and then /scale/validate is a callback in order to make sure the transaction was actually processed.

Because ScaleRegisterView does the entire process synchronously and would throw a non-200 response code in the event of a failure, we can always shortcut that part of the process.

urandom2 commented 7 years ago

Yeah, I mistyped the endpoint name; it is now fixed.

urandom2 commented 7 years ago

I agree that an endpoint that always returns 200 OK "T", is probably the cleanest solution, despite it being inaccurate to the api name scale/validate, but we are limited to the poor choices that fitbit made.