No description
Find a file
naeemarsalan d362afad72 AIREC BLE protocol spec + probe scripts
Static analysis of libapp.so plus live HCI capture decoding to document
the GATT service, command/notify framing (55AA / AA55 magic), opcode
registry, and a concrete handshake trace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:03:24 +01:00
discovery new recorder things 2026-05-18 12:21:00 +01:00
OpenDicta Added platofrm docs 2026-03-24 12:30:05 +00:00
.gitignore Initial commit: PLAUD BLE protocol specification 2026-03-23 11:12:46 +00:00
AIREC_BLE_Protocol_Specification.md AIREC BLE protocol spec + probe scripts 2026-05-18 18:03:24 +01:00
airec_debug.py AIREC BLE protocol spec + probe scripts 2026-05-18 18:03:24 +01:00
airec_enum_full.py AIREC BLE protocol spec + probe scripts 2026-05-18 18:03:24 +01:00
airec_probe.py AIREC BLE protocol spec + probe scripts 2026-05-18 18:03:24 +01:00
airec_probe_batch.py AIREC BLE protocol spec + probe scripts 2026-05-18 18:03:24 +01:00
airec_scan.py AIREC BLE protocol spec + probe scripts 2026-05-18 18:03:24 +01:00
CLAUDE.md Initial commit: PLAUD BLE protocol specification 2026-03-23 11:12:46 +00:00
PLAUD_BLE_Protocol_Specification.md Fixed prod 2026-03-23 19:50:43 +00:00
README.md Added platofrm docs 2026-03-24 12:30:05 +00:00
SET.bin.png test 2026-05-18 15:25:18 +01:00

PLAUD Note Pro BLE Protocol Reverse Engineering

Status: Pre-handshake complete, encrypted handshake pending

We have successfully reverse-engineered and implemented the PLAUD Note Pro (project code 881) BLE pre-handshake flow including RSA key exchange and ChaCha20-Poly1305 session key establishment. The device accepts our connection, verifies the SN signature, and sends back encrypted session keys that we successfully decrypt and verify ("PLAUD.AI" verification passes).

The remaining blocker is the encrypted handshake command (CMD 0x01) — the device receives it but does not respond. The encryption keys are verified correct, the packet format matches the decompiled code, but the device silently drops the encrypted handshake. The counter initialization value may still be wrong, or there may be a subtle difference in how the encryption wraps the handshake payload.


CAUTION: DEVICE BRICKING RISK

The PLAUD Note Pro cannot be factory reset without a BLE connection. The physical "hold button 12-15 seconds" reset only restarts the device — it does NOT clear the binding state or show the QR code for re-pairing.

If the device enters a state where:

  • It is bound to a session that no longer exists
  • The iOS Bluetooth bond is corrupted
  • Multiple failed connection attempts confuse the BLE stack

You may be unable to connect from ANY app (including the official PLAUD app). Recovery requires:

  1. Forgetting the device in iOS Settings → Bluetooth
  2. Waiting 2-3 minutes for the device's BLE supervision timeout to expire
  3. Plugging in the USB-C charging cable (resets BLE state on some firmware versions)
  4. In worst case, letting the battery fully drain and recharging

Do not repeatedly attempt connections without clearing iOS Bluetooth bonds between attempts.

Tested on PLAUD Note Pro — bricked successfully. The device is unresponsive to both the official PLAUD app and our custom app after multiple connection/disconnection cycles. Proceed at your own risk.


Architecture

OpenDicta/src/
├── ble/
│   ├── PlaudBleManager.ts    — BLE scanning, GATT connection
│   ├── PlaudConnection.ts    — Notification handling, command queue, write methods
│   ├── PlaudCrypto.ts        — Pre-handshake, RSA key exchange, ChaCha20 encrypt/decrypt
│   ├── PlaudFileSync.ts      — File list and download protocol
│   ├── PacketBuilder.ts      — Command packet assembly
│   ├── PacketParser.ts       — Response packet parsing
│   ├── ConnectionManager.ts  — Credential caching, reconnect logic
│   └── constants.ts          — UUIDs, command IDs, opcodes
├── plaud-sdk/
│   ├── PlaudAPI.ts           — PLAUD cloud API (login, SN signing)
│   └── environments.ts       — API region configuration
└── screens/
    ├── ScannerScreen.tsx     — Device scanning and connection initiation
    ├── HomeScreen.tsx        — File list, download, reconnect UI
    └── SettingsScreen.tsx    — Clear cache, forget device, logout

What Works

BLE Scanning & Device Discovery

  • Scans for BLE devices with company ID 93 (0x5D) or project codes [712, 880, 881, 882, 888]
  • Parses Note Pro advertisement data: project code at bytes[3-4] LE, portVersion at byte[20], serial number at bytes[11-18] as uppercase hex
  • Device name "Plaud Note Pro" with portVersion=20 confirmed

PLAUD API Authentication

  • Login via POST https://api-euc1.plaud.ai/auth/access-token (email/password, form-encoded)
  • Or direct Bearer token from web.plaud.ai (localStorage.getItem("tokenstr"))
  • SN signature via POST https://api-euc1.plaud.ai/device/sn-sign with {"sns": ["<SN>"]}
  • Token valid ~300 days, EU region (api-euc1.plaud.ai)

Pre-Handshake (RSA Key Exchange)

  • RSA 2048-bit key pair generated via node-forge (pure JS)
  • SN signature sent in 100-byte chunks using opcode 0xFE20 (Note Pro alternate opcode)
  • Device confirms with 0xFE11 response
  • RSA public key sent in 100-byte chunks using opcode 0xFE12
  • Device responds with 256 bytes of RSA-encrypted key material (3 chunks)
  • RSA decryption yields 80 bytes: key(32) + nonce(12) + AD(12) + verification_ciphertext(24)
  • ChaCha20-Poly1305 decryption of verification ciphertext produces "PLAUD.AI" — keys verified correct

Credential Caching

  • RSA keys and SN signature cached per device serial number
  • Reconnection reuses cached credentials (skips slow RSA key generation)
  • Cache clearable from Settings screen

What Doesn't Work Yet

Encrypted Handshake (CMD 0x01)

After establishing ChaCha20 session keys, the encrypted handshake command is sent but the device does not respond. The handshake packet format has been verified against the decompiled APK:

Plaintext being encrypted:

[counter(4 bytes LE)] [0x01] [0x01, 0x00] [0x02, 0x00] [0x14] [32-byte bind token]
 └── sequence counter   type   cmd=handshake  MTU indicator  portVer=20  ASCII token

Encrypted with ChaCha20-Poly1305 using session key/nonce/AD → 58 bytes (42 plaintext + 16 tag)

Possible remaining issues:

  1. Counter initialization — decompiled code shows H starts at 1, incremented before use → first packet counter=2. Currently set to start at 1 → first packet=2, but may need different value.
  2. Write method — using writeWithoutResponse, the decompiled code uses writeCharacteristic() which defaults to write-with-response on Android. iOS BLE behavior may differ.
  3. Timing — the decompiled code has a 30ms delay queue between writes. We send immediately after pre-handshake completion.
  4. ChaCha20 implementation difference@noble/ciphers vs Java's javax.crypto.Cipher("ChaCha20-Poly1305"). Both should produce identical output for same inputs, but edge cases may exist.

File Download

File list fetching (CMD 0x0A) and file download (CMD 0x14/0x1C) are implemented but untested (requires successful handshake first).


Key Protocol Findings

BLE Services

Service UUID Purpose
PLAUD Service 00001910-... Main protocol (write 0x2BB1, notify 0x2BB0)
Battery 0000180F-... Battery level (char 0x2A19)
Device Info 0000180A-... Firmware "1.0.0", "experimental"
BLE Chip 000002FD-... Chip-level bonding (triggers iOS pairing dialog)
Device Meta 87290102-... Manufacturer "PLAUD", model "Plaud Note Pro"

Pre-Handshake Packet Format (NO 0x01 type prefix!)

Bytes 0-1: Opcode (uint16 LE) — 0xFE20 for SN sig, 0xFE12 for RSA key
Byte 2:    totalChunks
Byte 3:    chunkIndex
Bytes 4+:  chunk data (up to 100 bytes)

Regular Command Packet Format (HAS 0x01 type prefix)

Byte 0:    Packet type (0x01 = command)
Bytes 1-2: Command ID (uint16 LE)
Bytes 3+:  Payload

Encrypted Packet Format

ChaCha20-Poly1305(key, nonce, AD, plaintext=[counter(4) + command_packet])

Counter is INSIDE the encryption. Key/nonce/AD are fixed per session (from RSA key exchange).

Note Pro vs Older Devices

Aspect Older (portVersion < 20) Note Pro (portVersion >= 20)
Pre-handshake None Required (RSA + ChaCha20)
SN sig opcode 0xFE10 0xFE20
Encryption None ChaCha20-Poly1305
Ad data SN offset bytes[9-16] bytes[11-18]
Ad data portVersion offset byte[17] byte[20]
Token length 16 chars (portVer < 9) 32 chars

Dependencies

  • react-native-ble-plx — BLE communication
  • node-forge — RSA key generation and decryption (pure JS, no native module needed)
  • @noble/ciphers — ChaCha20-Poly1305 encrypt/decrypt (pure JS)
  • @react-native-async-storage/async-storage — Token/credential persistence

Decompiled Source Reference

The APK is decompiled at decompiled/sources/. Key files:

  • wi/l3.java — BLE agent, connection state machine, handshake flow
  • wi/r4.java — GATT operations, write queue (30ms delay), encryption wrapper (line 1087-1093)
  • wi/q4.java — BLE UUID definitions
  • si/a0.java — Handshake command builder
  • si/c0.java — SN signature chunk builder
  • si/d0.java — RSA public key chunk builder
  • yi/m.java — ChaCha20-Poly1305 / AES-GCM encryption utilities
  • com/tinnotech/penblesdk/entity/BleFile.java — File model (sessionId, fileSize)

Next Steps

  1. Fix encrypted handshake — try different counter start values (0, 1, 2), write-with-response, or adding a delay after pre-handshake
  2. Test file list and download once handshake succeeds
  3. Investigate markers/bookmarks — not found in BLE protocol, may be embedded in audio files or cloud-only
  4. Build login screen — replace hardcoded Bearer token with email/password login flow
  5. Remove debug logging — strip verbose BLE debug logs for production