Credential QR Setup¶
The open-source Flutter client in this repo needs two per-device secrets to fully control an E-series vehicle:
| Secret | Size | Purpose | iOS backup key |
|---|---|---|---|
| Session password | 16 bytes | Derives the AES key for Phase 3 (AUTH) and all subsequent traffic. Without it the app cannot reconnect silently. | {serialNumber}_decrypt (stored as raw bytes, then base64-encoded by NSUserDefaults) |
| Command ident | 16 bytes | Payload required by cmd=0x64 vehicle actions. Without it, power/seat commands are silently dropped by the device. See $ident. |
{serialNumber}_ident (stored as a 32-char hex string) |
Both values are issued by the Segway cloud during first-time pairing and persisted locally for offline use. A third-party app cannot regenerate them — they have to be transferred from an install that already has them.
This page documents the QR-based transfer format used by the Flutter client, so other tools can produce the same code. It also explains, in the Provenance section below, why the ident is not derivable from any public input.
URI format¶
| Parameter | Required | Format | Notes |
|---|---|---|---|
sn |
optional | ASCII serial number | Informational; lets the scanner associate the creds with a named device. |
pwd |
required | 32 lowercase hex chars | Session password. Decoded to 16 bytes before use. |
ident |
optional¹ | 32 lowercase hex chars | Command ident. Decoded to 16 bytes. If the QR contains an ident key it must be well-formed — a malformed one causes the whole payload to be rejected, to avoid silently ending up with a read-only setup the user thought included writes. |
¹ An ident-less QR yields a read-capable session (all register reads work) but every vehicle-action button will return the error "No command ident — reconnect and paste {sn}_ident from your iOS backup."
Example (all values are dummies — replace with your own):
segway://credentials?sn=SNXXXXXXXXXXXX&pwd=00112233445566778899aabbccddeeff&ident=ffeeddccbbaa99887766554433221100
~114 characters. Fits easily inside a medium-density, error-correction-level-M QR.
Extracting values from an iOS backup¶
An unencrypted iTunes/Finder backup contains the com.ninebot.segway.plist UserDefaults file at:
(The file id is stable; the first two chars are the directory name.)
import plistlib
with open('<path-to-plist>', 'rb') as f:
d = plistlib.load(f)
sn = 'SNXXXXXXXXXXXX' # your device's serial number
password_bytes = d[f'{sn}_decrypt'] # raw 16 bytes
ident_hex = d[f'{sn}_ident'] # 32-char hex string
print('pwd ', password_bytes.hex())
print('ident', ident_hex)
_decrypt is stored as NSData (binary), _ident as NSString (hex text). The QR format normalises both to lowercase hex.
Generating a QR¶
Using the bundled CLI (recommended)¶
tools/make_creds_qr.py in this repo wraps the qrcode library with argument validation and produces both PNGs and terminal-friendly ASCII previews:
pip install 'qrcode[pil]'
python3 tools/make_creds_qr.py \
--sn SNXXXXXXXXXXXX \
--password 00112233445566778899aabbccddeeff \
--ident ffeeddccbbaa99887766554433221100 \
--out my_scooter_qr.png
All flags:
| Flag | Required | Purpose |
|---|---|---|
--password HEX32 |
yes | Session password. |
--ident HEX32 |
no | Vehicle-action ident. Omit for a read-only QR. |
--sn TEXT |
no | Informational serial number baked into the payload. |
--out FILE |
no | Output PNG path (default creds_qr.png). |
--ascii |
no | Print QR to terminal as text instead of writing a file. Handy for airgapped previews. |
--print-payload |
no | Also echo the raw segway:// URI — useful for debugging. |
--box-size N |
no | Pixels per module (default 10). |
--border N |
no | Quiet-zone width in modules (default 4). |
Directly, using the qrcode library¶
import qrcode
sn = 'SNXXXXXXXXXXXX' # replace with your values
pwd = '00112233445566778899aabbccddeeff'
ident = 'ffeeddccbbaa99887766554433221100'
payload = f'segway://credentials?sn={sn}&pwd={pwd}&ident={ident}'
img = qrcode.make(payload,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=10, border=4)
img.save('segway_creds_qr.png')
Display on screen, print on paper, or store alongside your other device backups — scanning takes seconds; pasting 64 hex characters into a mobile keyboard doesn't.
These values are sensitive
Anyone with both the password and the command ident for a given serial number can unlock and power on that vehicle over BLE. Treat the QR code with the same care you would a door key.
Using it in the app¶
The Flutter client at app/ in this repository ships a scanner built on mobile_scanner.
- Scan for your device on the home screen and tap it.
- Choose Connect with password.
- Tap Scan setup QR at the top of the Import Credentials dialog.
- Grant the camera permission on first run.
- Point the camera at the QR. Both hex fields fill in automatically; tap Connect.
The values are written to saved_devices.command_ident / saved_devices.password in the local SQLite DB. Subsequent reconnects to the same MAC reuse them — no QR needed again unless the app data is wiped.
If the camera isn't an option (e.g., on a device without one, or in an emulator), the dialog still accepts manual paste of each 32-char hex value.
Provenance¶
A common question: the scooter works offline, so surely the ident can be computed from something the scooter already knows (serial, ECU PN, …)? The answer is no — it's a factory-programmed per-unit secret, not a derivation. Here is how each side actually gets the value.
Factory¶
During manufacturing each scooter receives a random 16-byte secret burned into its non-volatile storage, alongside the ECU part number and other per-unit identifiers. Segway's cloud keeps a parallel database keyed by serial number. The same pattern is used for nfc_secret (fetched via POST /vehicle/vehicle/get-nfc-secret with the same {uid, wnumber} params, also cache_enable=true) — any per-device secret the app needs follows this model.
App side (first pairing)¶
Once the user has signed in and completed the BLE handshake, the app makes an authenticated HTTPS request:
POST /vehicle/vehicle/vehicle-basic-info
Content-Type: application/json
Authorization: <user JWT>
{"uid": "<user id>", "wnumber": "<scooter serial>"}
The response body includes an ident field whose value is a 32-character lowercase hex string. The app's HttpCacheClient.cacheIfNecessary(deviceTag, requestConfig) stores the whole response; later, when a vehicle action is about to fire, MotorDevice.applyNetData(key="ident", data=…) pulls the field out and calls setIdentifier(str) → identifierData = HexUtilsKt.hexStr2Bytes(str). That identifierData byte array is then passed as the payload of every cmd=0x64 frame.
Scooter side¶
There is no BLE command that sets the ident — no writeIdentifier, no pairing-time handshake exchange. The scooter already has the value; it simply compares the 16-byte payload of each incoming cmd=0x64 frame to the copy in its flash and either applies or ignores the action. That comparison is why mismatches fail silently: at the protocol layer nothing went wrong, the frame MAC-verified cleanly, the device just has no reason to respond to a "write" that doesn't address one of its registers.
Why it isn't derivable¶
If the ident were a deterministic function of public inputs (serial number, ECU PN, BLE device name), anyone within radio range who read those three values off a PRE_COMM response could open any scooter. Security works precisely because the 16 bytes were drawn randomly at factory time and planted on both sides of a database that neither the owner nor an attacker has access to.
Empirically: md5, sha1[:16], sha256[:16] of ecu_pn, sn, ecu_pn + sn, sn + ecu_pn, and other obvious combinations were all tried against a real observed ident. None match — as expected if it's factory-random.
Reconstruction paths (all out of scope)¶
| Path | Requires |
|---|---|
| Factory secrets database | Access to Segway's internal provisioning systems. |
| Scooter flash dump | Physical disassembly + firmware reverse-engineering of the ECU. |
| Cloud call with a valid user session | One authenticated POST to /vehicle/vehicle/vehicle-basic-info. |
| Extract from a paired install | iOS backup (UserDefaults {sn}_ident) or Android SharedPreferences — this is what the QR transfer flow documented above actually does. |
Because offline operation is permanent after any one of those paths succeeds once, there's no ongoing dependency on Segway's cloud — but there's also no math shortcut that lets you generate an ident "from scratch" for a device you haven't paired through a Segway-issued account at least once.
Source of truth¶
- URI format & parser:
lib/data/credential_qr.dart - Scanner screen:
lib/screens/qr_scanner_screen.dart - Import dialog:
lib/screens/scan_screen.dart(_PasswordImportDialog) - Round-trip tests:
test/credential_qr_test.dart