wayne piekarski Intercept Fitbit Aria Wi-Fi Smart Scale scale logging to local database

 
Share Blog article posted December 2024

Intercept Fitbit Aria Wi-Fi Smart Scale scale logging to local database

How to perform a MITM of Fitbit Aria Wi-Fi Smart Scale HTTP uploads to do local IoT logging.

Overview

I have my own IoT device logging system that records interesting data from various sensors, some custom designed and others just purchased as-is. I have also done work with adding logging to devices that are old and not designed for this, such as a water meter and computer vision gas meter.

For devices which log to some kind of proprietary server, I use either a (hopefully available) open API, or worst case a Linux machine running Selenium to login to a web site, scrape the HTML, and extract the value of interest. This has worked for many years, but then Google decided to completely remove any web interface for Fitbit, requiring you to use either the Android or iOS app and limiting your ability to get to the raw data.

I decided to investigate the protocol being used by my Fitbit Aria Wi-Fi Smart Scale and it turns out that it uses unencrypted HTTP requests to communicate with its proprietary service.

The first step is to configure your local router (I'm using Ubiquiti UniFi) to redirect all DNS lookups for www.fitbit.com to a web server under your control. The Fitbit scale needs to be on the network of the local router, but the web server can be anywhere on the Internet. The URL requested by the Wi-Fi scale is http://www.fitbit.com/scale/upload - I have an Apache2 web server with PHP running to handle that URL.

The PHP code reads the binary Fitbit protocol and extracts out the user id and weight, and then logs it to a custom IoT logging service which you can customize to whatever you are using. Most importantly, the PHP code also takes the entire request and replays it to the hard-coded IP address for www.fitbit.com so that the scale gets to communicate with the official server and receive whatever response it would normally get. This allows the existing Fitbit app functionality to keep working, and for the scale to know that it logged the value successfully. Without this replay support, the scale will keep trying to upload older weight measurements forever and show a failure icon on the screen.

References

In order to understand the Fitbit binary protocol, I found this documentation quite helpful, although I'm only parsing the bare minimum to get the weight information out and ignoring everything else:

https://github.com/micolous/helvetic/blob/master/protocol.md

https://www.hackerspace-bamberg.de/Fitbit_Aria_Wi-Fi_Smart_Scale


PHP script

Create a HTML file at /scale/upload and include the PHP code below. Configure your web server to run this as PHP.

  <?php
  // -- Begin PHP code --

  function get_http_data($url) {
         error_log ("Logging $url");
         $ch = curl_init();
         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Return result as string from curl_exec
         curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
         curl_setopt($ch, CURLOPT_URL, $url);
         $data = curl_exec($ch);
         curl_close($ch);
         return $data;
  }

  // Get all incoming headers and POST data for the MITM
  $headers = getallheaders();
  $postData = file_get_contents('php://input');

  // Write all post data to a temp file for testing, it is a binary blob
  $blob = file_get_contents("php://input");
  $ts = time();
  file_put_contents("/tmp/fitbit-blob-post-$ts.bin", $blob);
  error_log ("sizeof: " . strlen($blob));

  // Fitbit stores up multiple updates if there has been a failure to log to the server.
  // Typically 0x3C is the first offset which always has an update, then +0x20 offsets
  $addr = 0x3C;

  $data = substr($blob, $addr, 4);
  $bytes = unpack('C*', $data);
  $value = ($bytes[2] << 24) | ($bytes[1] << 16) | ($bytes[4] << 8) | $bytes[3];
  error_log ("TimestampDEC: " . $value );
  error_log ("TimestampHEX: " . dechex($value) );

  $data = substr($blob, $addr - 6, 4);
  $bytes = unpack('C*', $data);
  $value = ($bytes[4] << 24) | ($bytes[3] << 16) | ($bytes[2] << 8) | $bytes[1];
  error_log ("Weight grams : " . $value );
  error_log ("Weight HEX   : " . dechex($value) );
  $weight = $value / 453.592;
  $weight = round($weight, 1); // Round the float so it isnt 20 digits long when logged
  error_log ("Weight pounds: " . $weight );

  $data = substr($blob, $addr + 4);
  $bytes = unpack('C*', $data);
  $value = $bytes[1];
  $userid = $value;
  error_log ("User id: " . $value );

  // Handle all the $userid values that you have registered with Fitbit
  // NOTE: Replace example.com with your web server name!
  if ($userid == 127) {
    $LABEL = 'fitbit_user1_weight';
    $newurl = "https://example.com/log.php?LABEL=$LABEL&VALUE=$weight";
    $result = get_http_data($newurl);
  } else if ($userid == 137) {
    $LABEL = 'fitbit_user2_weight';
    $newurl = "https://example.com/log.php?LABEL=$LABEL&VALUE=$weight";
    $result = get_http_data($newurl);
  } else if ($userid == 188) {
    $LABEL = 'fitbit_user3_weight';
    $newurl = "https://example.com/log.php?LABEL=$LABEL&VALUE=$weight";
    $result = get_http_data($newurl);
  } else {
    error_log ("Ignoring invalid user id $userid");
  }

  // Replay the whole request to fitbit.com and try to preserve all the headers as best as possible
  if (true) {
    $url = 'http://www.fitbit.com/scale/upload';
    $ip = '35.244.211.136'; // Provide manual IP address to avoid DNS lookup which we are rewriting
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, 1);               // Set request method to POST
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); // Set the POST data
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  // Return the response as a string
    curl_setopt($ch, CURLOPT_RESOLVE, array("www.fitbit.com:80:$ip"));
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);  // Set the headers from the request
    $response = curl_exec($ch);
    if (curl_errno($ch)) {
      error_log('fitbut curl error: ' . curl_error($ch));
    }
    // Get the response headers from the target site
    $responseHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT);

    // Send the response headers back to the original caller
    foreach (explode("\r\n", $responseHeaders) as $header) {
        if (!empty($header)) {
            header($header);
        }
    }

    curl_close($ch);
    echo $response;
    error_log ("fitbit response: $response");
  }
  // -- End PHP code --
  ?>

Share Blog article posted December 2024


Google Developer Advocate 2014-2023


X-Plane plugins and apps for flight simulation


IoT water meter monitoring


IoT computer vision monitoring


Tiny and cheap offline Wikipedia project 2017


Outdoor augmented reality research
Tinmith 1998-2007


Outdoor augmented reality 3D modelling
Tinmith 1998-2007


Outdoor augmented reality gaming
ARQuake 1999-2007


Scanned physical objects outdoors
Hand of God 3D 2006


Google Developer Advocate 2014-2023


X-Plane plugins and apps for flight simulation


IoT water meter monitoring


IoT computer vision monitoring


Tiny and cheap offline Wikipedia project 2017


Outdoor augmented reality research
Tinmith 1998-2007


Outdoor augmented reality 3D modelling
Tinmith 1998-2007


Outdoor augmented reality gaming
ARQuake 1999-2007


Scanned physical objects outdoors
Hand of God 3D 2006


Contact Wayne Piekarski via email wayne AT tinmith.net for more information

Last Updated 2025