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> |
||
|---|---|---|
| discovery | ||
| OpenDicta | ||
| .gitignore | ||
| AIREC_BLE_Protocol_Specification.md | ||
| airec_debug.py | ||
| airec_enum_full.py | ||
| airec_probe.py | ||
| airec_probe_batch.py | ||
| airec_scan.py | ||
| CLAUDE.md | ||
| PLAUD_BLE_Protocol_Specification.md | ||
| README.md | ||
| SET.bin.png | ||
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:
- Forgetting the device in iOS Settings → Bluetooth
- Waiting 2-3 minutes for the device's BLE supervision timeout to expire
- Plugging in the USB-C charging cable (resets BLE state on some firmware versions)
- 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-signwith{"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:
- Counter initialization — decompiled code shows
Hstarts at 1, incremented before use → first packet counter=2. Currently set to start at 1 → first packet=2, but may need different value. - Write method — using
writeWithoutResponse, the decompiled code useswriteCharacteristic()which defaults to write-with-response on Android. iOS BLE behavior may differ. - Timing — the decompiled code has a 30ms delay queue between writes. We send immediately after pre-handshake completion.
- ChaCha20 implementation difference —
@noble/ciphersvs Java'sjavax.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 communicationnode-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 flowwi/r4.java— GATT operations, write queue (30ms delay), encryption wrapper (line 1087-1093)wi/q4.java— BLE UUID definitionssi/a0.java— Handshake command buildersi/c0.java— SN signature chunk buildersi/d0.java— RSA public key chunk builderyi/m.java— ChaCha20-Poly1305 / AES-GCM encryption utilitiescom/tinnotech/penblesdk/entity/BleFile.java— File model (sessionId, fileSize)
Next Steps
- Fix encrypted handshake — try different counter start values (0, 1, 2), write-with-response, or adding a delay after pre-handshake
- Test file list and download once handshake succeeds
- Investigate markers/bookmarks — not found in BLE protocol, may be embedded in audio files or cloud-only
- Build login screen — replace hardcoded Bearer token with email/password login flow
- Remove debug logging — strip verbose BLE debug logs for production