Repository navigation
Play Sound silently no-ops on a real third-party accessory (RAVEMEN ABF01) - likely needs BLE encryption Android can't provide #193
Description
Context
Testing BleGattSoundTrigger (from #139) against a real-world "Licensed 3rd Party Find My Device" accessory: a RAVEMEN ABF01 bike finder. The GATT write completes successfully (Write Response, no error), but the accessory never actually plays a sound.
What real capture shows the actual protocol to be
Captured the iPhone's own HCI trace for a successful Play Sound against this same accessory (PacketLogger + Apple's iOS Bluetooth logging profile). Two things stood out:
-
The characteristic AirGuard documents (
4F860003) isn't the one this accessory uses. The iPhone subscribes to indications on all fourfd44characteristics (4F860001-4) plus one on a second, vendor service (FD43/94110001), then writes two opcodes in sequence to4F860002:01 00 02(a handshake/query write - the client always sends this first)01 09 02(the actual start-sound opcode)
There's no separate stop opcode - the client just disconnects after ~30s and that ends the sound. The accessory also proactively sends unsolicited indications on
4F860002reporting its own state (01 0D 02= idle,01 0C 02 60 00 00 00= sounding). -
Reproducing this exact byte sequence from Android still doesn't work. Implemented the full sequence above (all 4+1 indications, both writes, in order) - GATT-level success throughout, but the accessory stays silent. Captured Android's own HCI snoop log (
Enable Bluetooth HCI snoop login Developer Options) for the same attempt and compared it against the iPhone's trace side by side:- The iPhone, immediately after connecting, issues
HCI_LE_Start_Encryptionwith a stable, pre-shared LTK (the same value across multiple separate sessions) and RAND/EDIV both zero - i.e. no SMP pairing exchange happens at all. The key is already known to both sides out-of-band, presumably derived from the same account-level Find My master key that produces the OF rolling public keys. - The Android capture shows zero encryption or SMP activity whatsoever for the same connection. The GATT write is accepted at the ATT level regardless (no permission bit enforces encryption on the characteristic), but the accessory's firmware appears to gate the actual sound on the link being encrypted with its expected key - a check that has nothing to do with ATT permissions.
- The iPhone, immediately after connecting, issues
Android's public BluetoothGatt/BluetoothDevice API has no method to request encryption with an app-supplied LTK - createBond() is the only path to an encrypted link, and it negotiates a fresh key via standard SMP pairing rather than using a pre-known one. As far as I can tell this makes it impossible for a non-rooted third-party Android app to replicate what Find My/the native app does here, for this class of accessory.
(For due diligence: also tried directly injecting the captured LTK into a rooted test device's Bluetooth bond store, to confirm this conclusively by side-stepping the API gap entirely. Ran into the OS's own Bluetooth HAL and a userspace BlueZ stack fighting over the same radio, which crashed the test device before producing a clean result - so this is corroborating evidence, not a confirmed proof.)
Why post this
Not claiming this is unfixable - just documenting what was actually observed in case:
- it explains other "connects fine, never sounds" reports against third-party accessories,
- someone has a workaround I haven't thought of (a different Android-side approach, a way to get the account's derived LTK into the stack, etc.),
- it's useful context for anyone picking this up later.
Happy to share more of the raw capture if useful. Credit to SEEMOO Lab's AirGuard for the original fd44/DULT/AirTag protocol reverse-engineering that #139 (and this investigation) built on.
Activity
LiamJ74 commentedon Sep 13, 2026
Update: found the exact protocol byte-for-byte (from a real iOS sysdiagnose), still no sound on Android
Since opening this issue I captured a full iOS
sysdiagnose(system logarchive, not just the BLE HCI trace) while triggering "Play Sound" against the same RAVEMEN ABF01.locationdtags every write to the4F860002characteristic with an internal task name, which resolves the exact command sequence unambiguously (previously this was reverse-engineered from raw bytes alone, without knowing what each write actually meant to iOS):This is a correction to what was previously documented: the order was reversed (the "status" opcode was assumed to be the real start), and there is in fact an explicit stop opcode (previously assumed to not exist, with the real client just disconnecting instead).
I implemented this exact corrected sequence in a test build (same 4-characteristic indication subscription, then the three writes above in the right order, each gated on the previous one's write callback firing before sending the next). Every write is acknowledged by the accessory at the ATT level, in the same order as the real iPhone. The accessory still does not make any sound.
This is stronger evidence for the conclusion in the original post: the accessory's firmware is almost certainly gating on link encryption (a pre-shared LTK Apple devices obtain out-of-band, e.g. via iCloud Keychain sync from the accessory's initial pairing), not on the specific bytes written. Getting the application-layer protocol byte-perfect changes nothing, because the write itself is accepted and acknowledged either way - it's what the firmware does after accepting it that differs based on whether the link is encrypted.
Not claiming this closes the door on a fix - just documenting that the protocol-level explanation can be ruled out now that it's been replicated exactly, leaving link encryption as the remaining candidate.
LiamJ74 commentedon Sep 13, 2026
Update: attempted to directly prove the link-encryption theory via a rooted device, blocked by a kernel limitation
To try to directly confirm the link-encryption theory rather than just infer it, I attempted the following on a separate rooted Android device (Magisk, with the Kali NetHunter chroot installed), specifically to avoid touching any device with real paired accessories:
HCI_LE_Start_Encryption, correlated to the same connection handle that then discovered theFD44GATT service - so this is confirmed to be the accessory's actual key, not another paired device's).stop vendor.bluetooth-1-0-qti, not justsvc bluetooth disablewhich leaves the HAL process running) to free up the internal chip's UART without the two Bluetooth stacks fighting over it.btattach -B /dev/ttyHS0 -P qca), intending to usebtmgmt's LTK-injection support (ltk-add) to load the real key without going through SMP pairing, then issue the GATT writes frombluetoothctl/gatttoolto see if the accessory would actually sound with a correctly-encrypted link.Step 3 failed cleanly:
Failed set serial line discipline: Invalid argument. This device's Android kernel does not build in the standard LinuxN_HCIline discipline (CONFIG_BT_HCIUART) that BlueZ's userspace tooling needs to attach to a UART-connected Bluetooth controller - it's been left out in favor of the vendor's own userspace HAL implementation, which is a closed path with no equivalent LTK-injection capability exposed to apps or shell.So: root access alone isn't sufficient to test this hands-on, on stock Android kernels at least. Getting further would need a custom-built kernel with that line discipline enabled, which is out of scope for this investigation. Documenting this so it's clear this avenue was tried, not skipped - and so anyone else who has a device with a custom kernel/BlueZ-capable Bluetooth setup knows what to try to get a fully hands-on confirmation.
parawanderer commentedon Sep 13, 2026
An initial overview from @parawanderer and Claude Code, having read this against the DULT draft, a vendor Find My SDK and AirGuard's source.
Your conclusion holds for the owner path.
4F860002is the owner's configuration control point. In a vendor Find My SDK (ble_fmy.c), a paired accessory treats an unencrypted connection as a non-owner and disables4F860002for it, and switches to owner mode only once encryption succeeds. The comment there says any phone on the paired Apple ID can encrypt, so the bond key belongs to the account. Zero EDIV and Rand is also what an LE Secure Connections bond looks like on reconnection, which fits a stored bond being resumed rather than a key derived fresh each time.4F860003is a different interface with a different gate. It is the non-owner control point:01 00 03there mirrors your01 00 02on the owner side, and it is what AirGuard writes. Per the DULT accessory draft, non-owner Sound_Start works over an unencrypted link but "SHALL only be available … when the accessory is in the separated state", and is answered with Invalid_command otherwise. Every test here had the owner's iPhone next to the accessory, which is the one state where that write cannot work.@parawanderer's understanding from #139 is that @ubrt got a third-party accessory to ring through that path while it was separated from its owner. @ubrt can confirm. If so, the non-owner path may cover most users of this app, who usually have no Apple device near their tag.
The test that would settle it for the ABF01: take every device signed in to its Apple ID out of range (or turn Bluetooth off on each), wait for it to go separated, then write
01 00 03to4F860003while subscribed to indications on it, and read the Command_Response rather than listening for a sound.One small correction: in that SDK,
94110001is the firmware update (UARP) characteristic rather than a vendor service.Your findings are now summarised in
docs/owner-connected-tags.md(#202), which is the index for ringing a tag near its owner. If you'd like, feel free to open PRs adding to that document yourself. #201 now tracks the owner-connected case as a whole.Merge pull request #202 from parawanderer/docs/owner-connected-ringing
LiamJ74 commentedon Sep 26, 2026
Update: got the injection mechanically working, but it crashes the Bluetooth stack rather than producing sound
Further progress on the rooted-device proof-of-concept from the previous update.
What's confirmed working now:
hci_uart/btqca/btbcmpresent), hookingwrite()on the process that owns the Bluetooth UART revealed something not obvious from any documentation: this HAL does not write a full H4-framed HCI packet in onewrite()call. It writes the 1-byte H4 type (0x01Command,0x02ACL Data) in its own separate call, then the packet body in the next call, with no type byte. Any tooling assuming one write() = one packet (ours included, initially) will silently never match anything.HCI_LE_Start_Encryption(opcode0x2019) directly to the controller - using the real captured LTK - without ever callingcreateBond()(which the accessory refuses outright, see the previous update).What happens next: the Bluetooth HAL process crashes.
Shortly after the injected command, the process handling the UART terminates and Android restarts it automatically (recovered cleanly, no reboot needed). No sound. The most likely explanation: Bluedroid's own state machine tracks in-flight HCI commands and expected responses per connection, and receiving an
Encryption Changeevent it never itself requested - for a command it has no record of sending - is enough to violate an internal invariant it isn't written to tolerate.So the encryption command genuinely reaches the controller with the correct key material, which is further confirmation of the root cause - but doing this cleanly from a normal HAL process appears to need real cooperation from the Bluetooth stack (a proper local HAL/stack patch that intends to do this, rather than injecting from outside it), not a hook layered on top after the fact. Still an open problem, not a dead end - picking this up again.
Hello1024 commentedon Oct 11, 2026
Keep going AI, you can do it!
Might be handy to test playing sound from a linux laptop first where you get far more control of the bluetooth stack.... Figure out exactly what needs to be sent, and later figure out if android can be made to do the sending...
parawanderer commentedon Oct 13, 2026
@LiamJ74 — thanks for looking into this; the captures and the rooted attempts above are the most direct evidence anyone has produced on this question. If you want to go looking for the source of the certificate in your own iCloud account, a script for that now exists —
sweep_keychain_for_bytes.pyin the FindMy.py fork's examples. It signs in, recovers the trust circle, and searches every keychain view the account can read for a byte needle you supply, reporting where a match is. One caveat before you build anything on it: sign-in itself is currently broken ecosystem-wide (-80045, Apple refusing at the first step since 2026-10-12), so the script may not get past sign-in until that is understood.Record the two Find My control points, from @LiamJ74's parawanderer#193…
Merge pull request parawanderer#202 from parawanderer/docs/owner-conn…