Security¶
This page covers the Bluetti BLE encryption protocols: the legacy challenge-response handshake, ECDH+ECDSA mutual authentication (protocol v2), the Bluetooth password PIN mechanism, and the complete cryptographic material.
Section 12 — BLE Encryption Key Retrieval¶
12.1 Overview¶
The app uses two distinct BLE security protocols depending on device firmware generation. Neither protocol fetches a session key from the cloud at runtime — all cryptographic material is either hardcoded in the APK or derived on-device from a challenge-response exchange.
12.2 Path 1 — Legacy Challenge-Response (Protocol v1)¶
Applies to older devices where protocolVer < 2000.
Flow:
Device sends a BLE “hello” packet containing a 4-byte random value at bytes 4–7.
App reverses the 4 random bytes, computes MD5 → stores as
randomMd5.App responds with a challenge packet:
"2A2A0204" + randomMd5.substring(16, 24)+ 1-byte checksum.Session AES key is derived as:
bleConnAESKey = XOR(randomMd5, LOCAL_AES_KEY)
Hardcoded key (ConnConstantsV2.LOCAL_AES_KEY):
459FC535808941F17091E0993EE3E93D
All subsequent BLE data is encrypted/decrypted with
AES-CBCusingbleConnAESKey.
Key source files:
ConnectManager.java:1909–1925— challenge-response derivationConnConstantsV2.java:98—LOCAL_AES_KEYconstantProtocolParse.java:1424—buildAESCBCCmd(cmd, aesKey, iv)
12.3 Path 2 — ECDH + ECDSA Mutual Authentication (Protocol v2)¶
Applies to newer devices (protocol v2+, e.g., EPAD, PLP025, and other 2nd-gen IoT modules). This path adds ECDSA-based device identity verification and ECDH-based forward-secret session key derivation.
Flow:
Challenge-response (same as Path 1, establishes
bleConnAESKeyas a temporary transport key).Device sends ECDH public key + ECDSA signature (encrypted with
bleConnAESKey):Response type
0x04: payload containsiotPkHexStr(64-byte uncompressed SECP-256R1 public key) +signature(64-byte raw ECDSA-SHA256 signature overiotPublicKey || randomMd5).
App verifies signature using hardcoded ECDSA verification key
PUBLIC_KEY_K2:
PUBLIC_KEY_K2 =
"3059301306072a8648ce3d020106082a8648ce3d03010703420004
A73ABF5D2232C8C1C72E68304343C272495E3A8FD6F30EA96DE2F4B3CE60B251
EE21AC667CF8A71E18B46B664EAEFFE3C489F24F695B6411DB7E22CCC85A8594"
This is an X.509-encoded SECP-256R1 (P-256) public key.
App generates ephemeral ECDH keypair (SECP-256R1), then signs
(appPublicKey || randomMd5)with hardcoded private keyPRIVATE_KEY_L1:
PRIVATE_KEY_L1 = "4F19A16E3E87BDD9BD24D3E5495B88041511943CBC8B969ADE9641D0F56AF337"
App sends its ephemeral public key + own ECDSA signature to the device (
0x2A2A0580packet).Device responds with response type
0x06confirming acceptance.Session key derived via ECDH:
bleConnShareKey = ECDH_SharedSecret(appEphemeralPrivKey, deviceIoTPublicKey)
bleConnAESKey is discarded; all subsequent BLE traffic is encrypted with bleConnShareKey using AES-CBC.
Key source files:
ConnectManager.java:1934–1961— ECDH key exchange logicConnectManager$bleEncryptedHandle$2$1.java:67–154— ECDSA verify + ECDH keypair generationSignatureCrypt.java:32–33— hardcodedPUBLIC_KEY_K2andPRIVATE_KEY_L1ECDHUtils.java:29–33— SECP-256R1 ASN.1 DER prefixes
12.5 Security Observations¶
Issue |
Detail |
|---|---|
Hardcoded local AES key |
|
Hardcoded ECDSA private key |
|
Hardcoded ECDSA verification key |
|
No cloud key fetch for sessions |
BLE session keys are fully self-contained; revocation or re-keying without an app update is not possible. |
getQrCodeEncrypt endpoint |
|
BLE password checked client-side only |
The 6-digit PIN is read from the device over BLE, then compared against user input in the app. An attacker who completes the BLE handshake can read the register holding the PIN and bypass the check. |
BLE password stored on device, readable over BLE |
Once the encrypted session is established, the PIN value is transmitted in the clear (encrypted at the transport layer by the global session key). |
15.2 BLE Connection and Handshake Flow¶
Step 1: Scan & Connect¶
Start BLE scan filtered by service
0000ff00-...On device found, call
connectGatt()withtransport=LEWait for services discovery (500ms delay auto-triggered)
Find
BluetoothGattServiceusing service UUID from scanned deviceGet write characteristic (
ff02) and read/notify characteristic (ff01)Enable notifications on
ff01with descriptor00002902-...usingENABLE_NOTIFICATION_VALUE({0x01, 0x00})
Step 2: Read Device Snapshot¶
After connection, the app reads the base config to determine protocol version and capabilities:
Read protocol version from Modbus register 16 (v1:
0103 0010 0001 ...) or V2 equivalentRead base config from register 1
Read device SN from register 21
Read real-time data from register 10
Step 3: Encryption Handshake (for encrypted devices)¶
For devices where isESP32Encrypted == true or isBLEEncrypted == true:
3a. Challenge-Response:
Device sends:
2A 2A 01followed by 4 random bytes (position 4-7)App reverses the 4 random bytes, computes MD5 →
randomMd5App responds:
2A 2A 02 04+randomMd5.substring(16, 24)+ 2-byte checksumApp derives temporary key:
bleConnAESKey = XOR(randomMd5, LOCAL_AES_KEY)
3b. ECDH Key Exchange (protocol v2+):
Device sends
2A 2A 04 ...(AES-CBC encrypted withbleConnAESKey, IV=MD5(randomMd5))Bytes 4-67: device’s SECP-256R1 public key (64 bytes, raw X+Y, no 04 prefix)
Bytes 68 to (end-2): raw ECDSA signature (r||s, 64 bytes) over
(devicePublicKey || randomMd5)Last 2 bytes: checksum (little-endian sum of preceding bytes)
App verifies ECDSA signature using hardcoded
PUBLIC_KEY_K2App generates ephemeral SECP-256R1 keypair
App signs
(appPublicKey || randomMd5)with hardcodedPRIVATE_KEY_L1App sends
2A 2A 05 80 ...with app public key + signatureDevice responds
2A 2A 06 00 ...confirming acceptanceApp derives shared secret via ECDH:
bleConnShareKey = ECDH(appPrivKey, devicePubKey)bleConnAESKeydiscarded; all subsequent traffic encrypted withbleConnShareKey
3c. Post-handshake encryption:
All subsequent Modbus frames are AES-CBC encrypted using bleConnShareKey (or bleConnAESKey if ECDH not completed):
Written via
buildAESCBCCmd(cmd, aesKey, iv)The IV for the first block is derived from MD5(randomMd5), then chained from previous ciphertext
Each 16-byte block padded to exactly 16 bytes (no PKCS padding)
Step 4: Modbus Data Protocol (post-handshake)¶
Once encrypted channel is established, device communication uses standard Modbus RTU-style frames.
15.8 Complete Crypto Material¶
All keys are hardcoded in the APK and identical across all installations.
Key |
Value |
Source File |
|---|---|---|
|
|
|
|
|
|
|
|
|
Key derivation formulas:
# Legacy challenge-response:
random_bytes = data[4:8] # from device hello packet
randomMd5 = MD5(reverse(random_bytes)) # 32 hex chars
bleConnAESKey = XOR(randomMd5, LOCAL_AES_KEY) # 32 hex chars → 16 bytes
# ECDH (protocol v2+):
ecdh_shared_secret = ECDH_secp256r1(app_ephemeral_privkey, device_iot_pubkey)
bleConnShareKey = ecdh_shared_secret # 32 hex chars → 16 bytes
Cipher: AES-128-CBC, IV chained from MD5(randomMd5), 16-byte blocks, no padding.