When the company behind his electric scooter went bankrupt, a developer set out to reverse engineer the app to maintain control over his vehicle. What he discovered was a critical security flaw that allowed him to unlock not just his own scooter, but any Äike scooter in the world.
The story begins with a simple practical problem. After years of relying on rental scooters, I purchased an Äike T, a locally manufactured Estonian scooter, to avoid the morning hunt for a ride. My choice was partly sentimental—supporting a local company with ambitious, fully custom hardware and a sister rental service, Tuul. This decision, however, led me into a technical labyrinth when Äike went bankrupt last year.
My immediate concern was the scooter's dependency on a cloud-connected app for basic functions like starting, stopping, and unlocking the battery tray. As the company's servers began to wind down services, the risk of being permanently locked out of my own property became real. This motivated a reverse engineering project, which quickly revealed a flaw far more significant than my personal inconvenience: a master key that could unlock any Äike scooter.
The Reverse Engineering Challenge
The first obstacle was the app itself. Built with React Native, it used the newer Hermes bytecode compilation method. This approach, while efficient for runtime, presents a significant hurdle for reverse engineers. Unlike the older JavaScript bundle method where code is relatively readable, Hermes bytecode lacks mature decompilers. The output from existing tools like hermes_rs is noisy and difficult to parse, forcing a reliance on dynamic analysis.
The app's Bluetooth communication with the scooter was the critical path. Since Bluetooth operations in Android require native OS interaction, the React Native layer had to bridge to native Kotlin code. This provided a foothold. Using the Java decompiler Vineflower, I confirmed the Kotlin code was merely a glue layer, with the core logic residing in the opaque Hermes bytecode.
This led to the use of Frida, a dynamic instrumentation toolkit. I wrote a Frida script to hook into Android's Bluetooth GATT (Generic Attribute Profile) classes, specifically BluetoothGatt and BluetoothGattCallback. This allowed me to sniff all communication—reads, writes, and notifications—in real-time as the official app interacted with the scooter.
Cracking the Authentication Protocol
The sniffed traffic revealed a clear authentication sequence:
- The app connects to the scooter.
- It reads a 20-byte random value (the "challenge") from characteristic
00002556-1212-efde-1523-785feabcd123. - It writes a 20-byte response to characteristic
00002557-1212-efde-1523-785feabcd123. - Only after this handshake can the app send commands to characteristic
0000155f-1212-efde-1523-785feabcd123.
The 20-byte length suggested SHA-1 hashing. To confirm, I hooked Java's java.security.MessageDigest class with another Frida script. The logs were definitive: the app read the challenge, concatenated it with 20 bytes of 0xFF, hashed the result with SHA-1, and sent that hash as the response.
The key question was the origin of the 0xFF bytes. The answer lay in the app's cloud backend, which used Firebase. With root access to the device, I queried the Firestore database and found the scooter's configuration, including a field named blePrivateKey. The value was ffffffffffffffff. This was the secret key used in the challenge-response.
Further investigation pointed to the IoT module vendor's SDK, where 0xFF was the default key—a placeholder that the Äike development team was supposed to replace with a unique key for each scooter. They never did. A representative from the module vendor later confirmed this was a client-side configuration error.
The Universal Key
The consequence is a critical security failure. Every Äike scooter shares the same Bluetooth private key: 0xFF * 20. This means any device that knows this key can authenticate to any Äike scooter and issue commands.
I developed a proof-of-concept Python script using the bleak library to demonstrate this. The script scans for nearby Äike devices, connects, performs the challenge-response authentication using the universal 0xFF key, and sends an unlock command. The entire process takes seconds.
Command Structure and Notifications
Beyond unlocking, I reverse-engineered the command protocol. Commands sent to characteristic 0000155f-... follow a 10-byte structure:
- Byte 0: Header (
0x00) - Byte 1: Registry ID (
0xD4for most commands) - Byte 3: Command ID
- Byte 7: Parameter value (if applicable)
Examples include:
- Unlock:
00 D4 00 01 00 00 00 00 00 00 - Lock:
00 D4 00 02 00 00 00 00 00 00 - Open Battery Tray:
00 D4 00 04 00 00 00 00 00 00
A separate registry (0xD2) controls transport mode.
The scooter also broadcasts notifications on characteristic 0000155e-.... These include battery level (0x00C0), lock status (0x00C1), and eco mode (0x00C6). These notifications can also be manually requested by writing a registry ID to characteristic 00001564-... and reading the response.
Disclosure and Resolution
Reporting the vulnerability was complicated by Äike's bankruptcy. I contacted the IoT module vendor in September 2025. They confirmed the default key issue and that it was the client's responsibility to configure unique keys. With no active vendor to patch, the flaw remains in the wild.
As a practical outcome, I built my own app to control my scooter, ensuring continued functionality independent of defunct cloud servers. This project transformed from a simple maintenance task into a stark lesson in IoT security: the failure to implement basic, unique cryptographic keys can render an entire product line vulnerable to universal compromise.
Related Resources:

Comments
Please log in or register to join the discussion