E-Scooter Bluetooth LE insides

Source code on Github: https://github.com/irmo-de/9UnBot/

Introduction

When I began using my eScooter, I was dissatisfied with the included app. This app connects to the scooter via Bluetooth Low Energy (LE), allowing users to change settings, display speed, lock/unlock the scooter, and more.

However, it lacks automation features, such as auto-connecting when the scooter is within range. Such functionality would be highly beneficial, particularly for using the lock feature or implementing custom commands with a customized drive firmware.

Let’s explore Bluetooth on the eScooter and its associated protocol

A great starting point is a document released by a group known as ‘Scooter Hacking’. It describes the principles of the UART communication protocol between the Bluetooth module and the scooter’s hardware.

https://wiki.scooterhacking.org/doku.php?id=nbdocs

UART protocol definition from scooterhacking.org

Using the nRF Connect tool, we observe a UART interface. While this might seem straightforward at first glance, as we delve deeper, it becomes apparent that the process is more complex than it initially appears.

nRF for Android Screenshot showing UART service on the bluetooth LE interface

Let’s take a closer look at what we can observe in the screenshot. In the top right corner, we see the connected device, which in my case is ‘NBSCOOTER1777’. More interestingly, we observe the services, including the Nordic UART service. We’ll note down the UUIDs for later reference.

Bluetooth Service UUID
UART Service 6e400001-b5a3-f393-e0a9-e50e24dcca9e
TX Characteristic 6e400002-b5a3-f393-e0a9-e50e24dcca9e
RX Characteristic 6e400003-b5a3-f393-e0a9-e50e24dcca9e
    public static final UUID TX_CHARACTERISTIC_UUID = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
    public static final UUID RX_CHARACTERISTIC_UUID = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");

    public static final UUID UART_SERVICE_UUID =      UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");

Wireshark for Bluetooth Network Protocol Analysis

To understand what is transmitted over the Bluetooth UART interface, we can use Wireshark. But how do we capture Bluetooth messages? With an Android phone, this task is straightforward. Simply enable Bluetooth HCI snoop in the developer options. This action logs the complete Bluetooth traffic between the phone and any connected device to a text file, which can then be analyzed with Wireshark.

Android developer settings to enable Bluetooth paket snooping

Moving forward, the next step is to download the Bluetooth capture file from the phone and open it in Wireshark. Within Wireshark, we are able to view the traffic between the phone (1) and the scooter (2), particularly when the official Ninebot app is in use. By applying a filter for ‘send commands’, we can then proceed to analyze the UART messages.

Let’s revisit the scooter hacking document for further insights.

UART protocol definition from scooterhacking.org

In our analysis, we notice that messages begin with the frame header 0x5A 0xA5, which is also visible in the Bluetooth log. However, the subsequent data does not align, suggesting a difference in the UART communication. A quick search on Google reveals an unpleasant surprise. In their more recent Bluetooth communication versions, Ninebot and Xiaomi (both sharing the same technology platform) have introduced encryption. This complicates our task significantly. Fortunately, there are two GitHub projects that have come to our rescue. These projects have successfully reverse-engineered the encryption, offering a potential solution to our challenge.

https://github.com/scooterhacking/NinebotCrypto

NinebotCrypto, a library reverse-engineered from Ninebot/Xiaomi’s first crypto protocol, offers encryption for legacy Ninebot packets and supports a range of BLE versions across different scooter models. The guide details the process of implementing chained crypto in apps, stressing the importance of understanding the legacy Ninebot-Xiaomi protocol. Key steps include importing the library, managing encryption and decryption of scooter communications, and a thorough explanation of the pairing process with specific commands and responses for seamless integration.

At first glance, this might seem a bit complex, given that it requires implementing the entire communication process. This led me to opt for the second GitHub project initially. As we’ll discover later, this turned out to be a rather time-consuming mistake.

https://github.com/dnandha/miauth

The Xiaomi Mi / Nb BLE authentication project addresses the challenges posed by recent protocol changes in Xiaomi BLE communication, aiming to revive apps that lost compatibility. It offers detailed documentation of the authentication process and provides both Python and Java libraries for easy implementation.

The second GitHub project showcases a Python library that’s ideally suited for Linux environments, making it particularly convenient for use on a Raspberry Pi 4, which comes with a built-in Bluetooth module. Once the necessary Bluetooth libraries are installed, it’s all set to go. Let’s now take a closer look at the output this setup yields:

miauth -n -v  F6:34:CD:56:E6:1B -c 5AA5023D20037A0100

-n for ninebot
-v for verbose output

Here is the result:

Namespace(mac='F6:34:CD:56:E6:1B', m365=False, nb=True, command='5AA5023D20037A0100', serial=False, fwver=False, verbose=True, register=False, register_did=None, token_file='./mi_token')
Using Nb
Connecting
enabling notifications for: 6e400003-b5a3-f393-e0a9-e50e24dcca9e
enabling notifications for: None
Authenticating
Current state: State.CON
b'Z\xa5\x00=![\x00'
bytearray(b'Z\xa5\x003\xa9CE\x00\x00F\xff\x00\x00')
Sending message: 5a a5 00 33 a9 43 45 00 00 46 ff 00 00
Current state: State.CON
b'Z\xa5\x00=![\x00'
bytearray(b'Z\xa5\x003\xa9CE\x00\x00F\xff\x00\x00')
Sending message: 5a a5 00 33 a9 43 45 00 00 46 ff 00 00
Received message: 5a a5 1e 2f b5 43 44 44 ab b5 9d d7 21 61 20 88 5d 89 9c a0
Decoded message: 5a a5 1e 21 3d 5b 01 7d a2 78 b7 97 3e 26
Received message: 5a a5 1e 2f b5 43 44 44 ab b5 9d d7 21 61 20 88 5d 89 9c a0 da 60 7f 77 3d 8a 6f 18 2d 75 93 96 b0 74 32 39 bf 00 00 3b
Decoded message: 5a a5 1e 21 3d 5b 01 7d a2 78 b7 97 3e 26 86 2c ae cc 99 ae 52 78 3a 4e 34 47 45 58 32 32 35 32 43 31
Received message: 5a a5 1e 2f b5 43 44 44 ab b5 9d d7 21 61 20 88 5d 89 9c a0 da 60 7f 77 3d 8a 6f 18 2d 75 93 96 b0 74 32 39 bf 00 00 3b f4 00 00
Decoded message: 5a a5 1e 21 3d 5b 01 7d a2 78 b7 97 3e 26 86 2c ae cc 99 ae 52 78 3a 4e 34 47 45 58 32 32 35 32 43 31 37 37 37
Got cmd: 5a a5 1e 21 3d 5b 01
> BLE Key: 7d a2 78 b7 97 3e 26 86 2c ae cc 99 ae 52 78 3a
> Serial: N4GEX2252C1777
Setting ble data/key in crypto
Current state: State.PING
Sending message: 5a a5 10 fe 42 1f 5a cf 31 5a 3f 72 0b 5e 14 51 23 cd 54 c2 42 36 4f 00 00 27 f8 00 00
Current state: State.PING
Sending message: 5a a5 10 fe 42 1f 5a cf 31 5a 3f 72 0b 5e 14 51 23 cd 54 c2 42 36 4f 00 00 27 f8 00 00
Received message: 5a a5 00 2c 38 ac e5 9b 04 d5 bd 00 02
Decoded message: 5a a5 00 21 3d 5c 00
Got cmd: 5a a5 00 21 3d 5c 00
Current state: State.PRE
>> Please press the POWER button on the device
Sending message: 5a a5 0e 7f 1f 6b 20 b8 42 b9 b5 6b 7f 8e 6b bd 4e d3 ef 67 69 7c f5 9b ac 00 03
Received message: 5a a5 00 63 03 6a 20 9c f9 fe 2c 00 03
Decoded message: 5a a5 00 21 3d 5c 00
Got cmd: 5a a5 00 21 3d 5c 00
Current state: State.PRE
>> Please press the POWER button on the device
Sending message: 5a a5 0e 95 6c 61 62 bf 0a 8b 83 7e b0 d3 a4 9e 95 f1 0d 4b 91 99 59 b1 79 00 04
Received message: 5a a5 00 89 70 61 63 cf fa b2 39 00 04
Decoded message: 5a a5 00 21 3d 5d 01
Got cmd: 5a a5 00 21 3d 5d 01
Nb authentication successful!
Sending message: 5a a5 02 b7 46 18 0b 74 91 70 fc de b6 00 05
Received message: 5a a5 00 ab 5b 46 70 be 63 67 c9 00 05
Decoded message: 5a a5 00 21 3d 5d 01
Got cmd: 5a a5 00 21 3d 5d 01
Nb authentication successful!
UART reply: 5a a5 00 ab 5b 46 70 be 63 67 c9 00 05
Disconnecting

After successfully sending a custom python command, I initially believed integrating the Java library from the same Github into my project would be straightforward. However, this turned out to be a misconception. The integration of the library for Bluetooth communication revealed that the Ninebot implementation was incomplete. Despite my efforts to add the missing code, I ultimately found it too time-consuming to replicate what was lacking. Fortunately, we have another GitHub project which contains the library for encryption and decryption, meaning I still needed to implement the authentication communication myself.

Before delving into the output, let’s first examine the crypto implementation from NinebotCrypto. It utilizes cryptographic methods for secure data transmission, incorporating SHA-1 for key generation, AES for encryption, and CRC for error checking. However, its use of ECB mode in AES and SHA-1 may raise security concerns, given the existence of more secure alternatives.

Security Considerations:

  • Use of SHA-1: Although SHA-1 is perceived as weak in certain applications due to collision attack vulnerabilities, its role here appears limited to key generation, not security hashing.
  • AES in ECB Mode: While AES is robust, ECB mode’s vulnerability lies in its lack of serious message confidentiality, especially evident through identical plaintext blocks producing identical ciphertext blocks.
  • Absence of IV in AES: The lack of an Initialization Vector (IV) in ECB mode is a significant shortfall, especially since modes like CBC or GCM typically use an IV for enhanced security.

One can only speculate about the developers’ choice of ECB mode. My best guess includes:

  • Simplicity in Implementation: ECB’s straightforward block-by-block processing makes it easier to implement than more complex modes like CBC or GCM.
  • Efficiency for Small Data: ECB is efficient for small data blocks, especially when the data size matches or is less than the cipher’s block size (16 bytes for AES).
  • Low Resource Requirement: ECB’s simplicity translates into lower computational resource demands, making it suitable for systems with constrained processing capabilities.
  • No Error Propagation: ECB encrypts each block independently, preventing transmission errors in one block from affecting others, unlike modes like CBC where a single error can corrupt subsequent blocks.”

Since the project primarily focuses on the encryption aspect, I found myself tasked with implementing the Bluetooth UART communication from scratch. For this, I opted to use rxandroidble2, which significantly eases the handling of Bluetooth Low Energy (BLE) communications. The complete code for this implementation is available on my GitHub.

 private void establishConnection() {


        connectionDisposable = rxBleDevice.establishConnection(false)
                .timeout(50000, TimeUnit.SECONDS) // Set timeout for 50 seconds
                .flatMap(rxBleConnection -> {
                    // Store the connection
                    this.rxBleConnection = rxBleConnection;
                    // Start service discovery and convert Single to Observable
                    return rxBleConnection.discoverServices().toObservable();
                })
                .flatMap(rxBleDeviceServices -> {
                    // Check if the UART service is available on the device
                    if (rxBleDeviceServices.getService(UART_SERVICE_UUID) != null) {
                        // UART service is available, continue with setting up notification
                        return Observable.just(this.rxBleConnection);
                    } else {
                        // UART service is not available, complete the observable with an error
                        return Observable.error(new IllegalStateException("UART service not found"));
                    }
                })
                .subscribe(
                        connection -> {
                            // At this point, we know that the UART service is available
                            setupNotification();
                        },
                        throwable -> {
                            // Handle errors
                            if (throwable instanceof TimeoutException) {
                                Log.e("nb_ble", "Failed to connect to BLE device within 5 seconds.");
                            } else {
                                Log.e("nb_ble", "Error establishing BLE connection: " + throwable.getMessage());
                            }
                        }
                );

        // Optionally store connectionDisposable for later disposal if needed
    }
    private void setupNotification() {
        if (rxBleConnection != null) {
            Log.i("nb_ble", "Setup notification receiver... ");
            rxBleConnection.setupNotification(RX_CHARACTERISTIC_UUID)
                    .flatMap(notificationObservable -> notificationObservable)
                    .subscribe(
                            bytes -> {
                                Log.i("nb_ble", "Received notification: " + bytesToHex(bytes));
                                notificationQueue.offer(bytes);
                            },
                            throwable -> {
                                Log.e("nb_ble", "Error in setupNotification: " + throwable.getMessage(), throwable);
                            }
                    );
        }
    }

Key insights that would have been beneficial to know beforehand, and that took some time to figure out, include:

  • Messages from the Ninebot are divided into chunks of 20 bytes.
  • When sending messages longer than 20 bytes, it’s necessary to break them down into smaller chunks.
  • The library conveniently incorporates the CRC checksum

Here is the needed bytecode to be send to the scooter. For details have a look at the Github Code for this project.

CommandByte Code
InitMessage5AA5003E215B00
pingMessage5AA5103E215C00
pairMessage5AA50E3E215D00
unlockFirmware5AA5023D20037A0000
lockFirmware5AA5023D20037A0100
pingAck15AA500213E5C00
pingAck25AA500213E5C01
pairAck5AA500213E5D01

This table organizes the commands alongside their corresponding byte codes for easy reference.

But after diligently piecing everything together, I finally had a fully functional Android app capable of sending messages to my scooter!

4 thoughts on “E-Scooter Bluetooth LE insides

  1. Alex Reply

    I’m working on something similar – could you provide more details on how you configured the Pi for BLE ❤️

  2. Wan Zhu Reply

    Can you help me with Bluetooth capturing. I cannot find the Bleutooth caputure file on my Android phone. I have a Galaxy S23. Please help me. I need the capture file.

  3. ScooterHobbyist Reply

    Really awesome post! I’ve been wanting to tweak my scooter’s firmware for a while. Your blog has inspired me to finally give it a go

  4. TechGuru101 Reply

    Incredible breakdown of the Blutooth LE process with your. Your detailed analysis really helps in understanding the complexities involved. Keep up the great work!

Leave a Reply

Your email address will not be published. Required fields are marked *