Skip to content

Instantly share code, notes, and snippets.

@bettse
Created June 22, 2025 17:44
Show Gist options
  • Save bettse/3ed0f39b765d8e4cb86dd25c3e3c8948 to your computer and use it in GitHub Desktop.
Save bettse/3ed0f39b765d8e4cb86dd25c3e3c8948 to your computer and use it in GitHub Desktop.
HWID BLE

https://fccid.io/PIY-HID00-19A5R

BLE service/characteristics

af0a6ec70001000a84a091559fc6f0de
  af0a6ec70002000a84a091559fc6f0de: write, indicate // data
  af0a6ec70003000a84a091559fc6f0de: read // factory
  af0a6ec70004000a84a091559fc6f0de: write, indicate // session

af0a6ec70001000b84a091559fc6f0de //nxp ota service
  af0a6ec70002000b84a091559fc6f0de: write, indicate // NXPConfig::otaCmd / NXP_OTAP
  af0a6ec70003000b84a091559fc6f0de: writeWithoutResponse // NXPConfig::otaData

OTA Command characteristic (indicate)

02 00 00 01 00 00 41 11 11 11 01

Android logs:

NewImageInfoRequest 02 00 00 01 00 00 41 11 11 11 01
build version 01 00 00
app Version 41
hardware Id 11 11 11
manufacturer 01

Start

  1. subscribe to 'data' and 'session' characteristics
  2. read from 'factory' characteristic

01303739394f5331313032303938363239303730303031383902febf6d48ebd6ee1328a875b64def8641554e5bbc99d73c26b2cfc84045528e96005c919764000198967fcd604f291b4aeab61e14449a0ddc4c381b63946b9825df2d86c0548afeef07fe7b6ce4706762b2e5ef4930edb8ea88b977566df13d8f6dd1c743b33a03edf880d5c23603

NB: for the public key above (0x02febf) the private key is 9f69f7257a3bf97dcd6dee36e740a4e32c353ecb886b8a5b30184380c056d0f8

index length name description example
0 1 protocolVersion 1
1 24 serial ascii encoded data, see next table for decoding 0799OS110209862907000189
25 33 compressedPublicKey secp256r1/prime256v1 ECDH public key 02febf6d48ebd6ee1328a875b64def8641554e5bbc99d73c26b2cfc84045528e96
58 5 birthdate 5 byte unsigned int of unix timestamp
63 2 machineNumber signed 16bit big endian 1
65 3 keyID unsigned 24bit I think used to index into hardcoded array of public keys in app 9999999
68 64 signature sha256 based sig (I haven't validated)
132 4 salt salt used for encryption d5 c2 36 03
  • Serial
index length name description example
0 3 day 079
3 1 year 9
4 2 location OS
6 10 APNumber 1102098629
16 2 revision 07
18 6 itemNumber 000189
  1. Write 37 bytes to 'session'
  • 33 byte secp256r1/prime256v1 ECDH public key (compressed)
  • 4 byte salt of your choosing
  1. Starting receiving messages, see format and decryption below

BLE Messages

7e00000001001555fc7918da6ab8101a1c0fa0d9b19c4c590501ef7b33

index length name description example
0 1 Always starts with this byte 7e
1 4 packetNumber 00 00 00 01
5 2 length 0015 (21 bytes)
7 1 header crc8 crc8 (aka crc8atm) but with a 0xff initial value reveng -m "CRC-8" -i ff -c 7E000000010011
8 length contents the contents, encrypted
-1 1 content checksum same as header checksum, but checked after decrypt

Encryption

Derive secretKey

Reencrypt sharedsecret with itself 100x:

  1. ECDH to get a shared secret
  2. Create IV with 9 zeros, 'mattel', and a final 0 (16 bytes in total)
  3. encrypt shared secret using shared secret and IV
  4. IV[7]++
  5. repeat until IV[7] is 99

Decrypt contents

  1. IV is concat of:
  • 4 byte packetNumber
  • 4 byte salt from peer
  • 4 byte salt you chose
  • 4 bytes of 0
  1. key is first 16 bytes of secretKey
  2. algorithm is aes-128-ctr

To encrypt, reverse order of salts: put your salt before the peer salt

Parsing descrypted Messages

  • Protobuf
syntax = "proto3";
package hwid;

message PortalToApp {
  uint32 timestampMs = 1;
  Event event = 2;
  DeviceInfo deviceInfo = 3;
  CommandResponse commandResponse = 4;
  bytes accessoryMessage = 5;
}

enum EventType {
    UNKNOWN = 0;
    LOW_BATTERY = 1;
    CAR_ON = 2;
    CAR_OFF = 3;
    CAR_DRIVE_BY = 4;
    CAR_HIST = 5;
    ACC_ATTACH = 6;
    ACC_DETATCH = 7;
}

message Event {
  uint32 type = 1;
  CarInfo carInfo = 2;
  SpeedMeasurement speedMeasurement = 3;
  repeated SpeedMeasurement measurementHistory = 4;
  repeated OfflineRaceSession offlineRaceSessions = 5;
}

message OfflineRaceSession {
  uint32 timePlayed = 1;
  float topSpeed = 2;
  uint32 scanCount = 3;
}

message CarInfo {
  bytes tagUid = 1;
  bool signatureStatus = 2;
  bytes carNdef = 3;
  bytes signature = 4;
  bytes publicKey = 5;
}

message SpeedMeasurement {
  uint32 timestampMs_ = 1;
  float speed = 2;
  int32 tIr1In = 3;
  int32 tIr1Out = 4;
  int32 tIr2In = 5;
  int32 tIr2Out = 6;
}

message DeviceInfo {
  uint32 firmwareVersion = 1;
  uint32 hardwareVersion = 2;
  float batteryLevel = 3;
  uint32 deviceMode = 4;
  uint32 bootTimestamp = 5;
  string serialNumber = 6;
  uint32 batteryState = 7; // 2: charging, 1: not charging, 3: full, 4: problem
  uint32 qValue = 8;
  uint32 iValue = 9;
  string semanticFirmwareVersion = 10;
  bool accessoryAttached = 11;
}

message CommandResponse {
  int32 failed = 1;
  string failMessage = 2;
}

message AppToPortal {
  uint32 timestampSec = 1;
  Command command = 2;
  bytes accessoryMessage = 3;
}

message Command {

/*
UnknownCommandType = 0;
PortalMode = 1;
RaceMode = 2;
RequestDeviceInfo = 3;
TestMode = 4;
Reset = 5;
StartOta = 6;
SetLedcolor = 7;
ResetLedcontrol = 8;
*/

  uint32 type = 1;
  bytes otaSignature = 2;
  bytes otaPublicKey = 3;
  bytes rgbColor = 4;
}


message AccessoryToPortal {
  uint32 timestampMs = 1;
  DeviceInfo info = 2;
  bytes accessoryMessage = 3;
}

message PortalToAccessory {
  uint32 timestampMs = 1;
  Event event = 2;
  bytes accessoryMessage = 3;
}

accessoryMessage

index length name description example
0 1 start-of-message 0x8d
1 2 ? 0xffff
3 2 ? 0xfffe
5 2 Track.CommandType 0xbbbc
7 2 length 0x0002
-2 2 checksum CRC-16/CCITT-FALSE
Track.CommandType value
TableRequest 5
TableResponse 6
SetUpstreamBase 0x10
RequestBoosterBirthCert 0x20
ResponseBoosterBirthCert 0x21
RequestTrackBirthCert 0x22
ResponseTrackBirthCert 0x23
TestLED 0x30
FirmwareOtaStart 0xAAAA
FirmwareOtaManifest 0xAAAB
FirmwareOtaPackage 0xAAAC
FirmwareOtaDone 0xAAAD
Acknowledge 0xACAC
FinishLineEvent 0xBBBB
BoosterRpmEvent 0xBBBC

TableResponse

8dfffffffe0006001b020041b0b0c60041b0b0c80101ffff000201ffff020200010015116013

Didn't figure out completely, but the first byte is a count of pieces, including the booster, and there are a list of 4 byte ids for the pieces

physical name id TrackType Track.Type SmartTrackID
booster 41b0b0c6 0 SmartBooster
long 41b0b0c7 1 Track12Inch
short 41b0b0c8 3 Track6Inch
41b0b0c9 6 BankedCurve
gate 41b0b0ca 2 FinishLine
41b0b0cb 7 Loop
jump 41b0b0cc 4 JumpLaunch
41b0b0cd 5 JumpLanding
8 Unknown

ResponseTrackBirthCert

fffffffe00230058020041b0b0c8005c9099283037383973373032313998967f17b8e7d4d614172abb271d246041d83a3995fe87e7a53f3d7521efaab307461bd290988b2a7a592404c071b5ee99a69e26150004c56a430eb178a6ca5e70bb84

index length name description example
0 2 ? 0xffff
2 2 ? 0xfffe
4 2 Track.CommandType 0x0023
6 2 length 0x0058
8 1 ? 0x02
9 5 ? track part id? (seen for track connected messages) 0041b0b0c8
14 5 timestamp 0x005c909928
19 10 serialnumber ascii 0789s70219
29 3 keyID 9999999
32 64 signature? I assume a signature like the first message

ResponseBoosterBirthCert

fffffffe00210058020041b0b0c6005c8f4ad63037373973373038393998967f564a16af5f6f4fa95de9ca662f3591bde24602cdae71e95f6eb0d34b718bfd63419fe79b6fa7a052265df6f615a37baaa86465dcc69ff9a4159665ac2f281a7b

index length name description example
0 2 ? 0xffff
2 2 ? 0xfffe
4 2 Track.CommandType 0x0021
6 2 length 0x0058
8 1 ? 0x02
9 5 ? booster part id? (seen for track connected messages) 0041b0b0c6
14 5 timestamp 0x005c8f4ad6
19 10 serialnumber ascii 0779s70899
29 3 keyID 9999999
32 64 signature? I assume a signature like the first message
syntax = "proto3";
package hwid;
message PortalToApp {
uint32 timestampMs = 1;
Event event = 2;
DeviceInfo deviceInfo = 3;
CommandResponse commandResponse = 4;
bytes accessoryMessage = 5;
}
enum EventType {
UNKNOWN = 0;
LOW_BATTERY = 1;
CAR_ON = 2;
CAR_OFF = 3;
CAR_DRIVE_BY = 4;
CAR_HIST = 5;
ACC_ATTACH = 6;
ACC_DETATCH = 7;
}
message Event {
uint32 type = 1;
CarInfo carInfo = 2;
SpeedMeasurement speedMeasurement = 3;
repeated SpeedMeasurement measurementHistory = 4;
repeated OfflineRaceSession offlineRaceSessions = 5;
}
message OfflineRaceSession {
uint32 timePlayed = 1;
float topSpeed = 2;
uint32 scanCount = 3;
}
message CarInfo {
bytes tagUid = 1;
bool signatureStatus = 2;
bytes carNdef = 3;
bytes signature = 4;
bytes publicKey = 5;
}
message SpeedMeasurement {
uint32 timestampMs_ = 1;
float speed = 2;
int32 tIr1In = 3;
int32 tIr1Out = 4;
int32 tIr2In = 5;
int32 tIr2Out = 6;
}
message DeviceInfo {
uint32 firmwareVersion = 1;
uint32 hardwareVersion = 2;
float batteryLevel = 3;
uint32 deviceMode = 4;
uint32 bootTimestamp = 5;
string serialNumber = 6;
uint32 batteryState = 7; // 2: charging, 1: not charging, 3: full, 4: problem
uint32 qValue = 8;
uint32 iValue = 9;
string semanticFirmwareVersion = 10;
bool accessoryAttached = 11;
}
message CommandResponse {
int32 failed = 1;
string failMessage = 2;
}
message AppToPortal {
uint32 timestampSec = 1;
Command command = 2;
bytes accessoryMessage = 3;
}
message Command {
/*
UnknownCommandType = 0;
PortalMode = 1;
RaceMode = 2;
ReqeustDeviceInfo = 3;
TestMode = 4;
Reset = 5;
StartOta = 6;
SetLedcolor = 7;
ResetLedcontrol = 8;
*/
uint32 type = 1;
bytes otaSignature = 2;
bytes otaPublicKey = 3;
bytes rgbColor = 4;
}
message AccessoryToPortal {
uint32 timestampMs = 1;
DeviceInfo info = 2;
bytes accessoryMessage = 3;
}
message PortalToAccessory {
uint32 timestampMs = 1;
Event event = 2;
bytes accessoryMessage = 3;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment