d4rken-org / capod

A companion app for AirPods on Android.
https://play.google.com/store/apps/details?id=eu.darken.capod
GNU General Public License v3.0
491 stars 39 forks source link

Help wanted: Using L2CAP connection to read from and write settings to AirPods #215

Open d4rken opened 1 month ago

d4rken commented 1 month ago

Our sister project for Windows, MagicPods, has reverse engineered reading and writing data to AirPods.

If anyone can figure out how to establish the connection, then I could start on adding support for this.


AirPods require a Bluetooth connection that uses the L2CAP protocol. I was unable to establish this connection on a Pixel 6 running Android 15 and Pixel 8 running Android 14.

This is what I tried so far

Use the public createInsecureL2capChannel method

06:50:53.800 bt_l2cap                             E  packages/modules/Bluetooth/system/stack/l2cap/l2c_api.cc:417 - L2CA_RegisterLECoc: Invalid BLE PSM value, PSM: 0x1001
06:50:53.800 bt_stack                             E  [ERROR:gap_conn.cc(255)] GAP_ConnOpen: Failure registering PSM 0x1001

but that seems to have limits on the PSM value that you can provide

https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/system/stack/l2cap/l2c_api.cc;l=416?q=%22Invalid%20BLE%20PSM%20value%22

#define L2C_IS_VALID_LE_PSM(psm) (((psm) > 0x0000) && ((psm) < 0x0100))

Then I tried the hidden method createInsecureL2capSocket

https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/framework/java/android/bluetooth/BluetoothDevice.java;l=2924?q=createInsecureL2capSocket&sq=&ss=android%2Fplatform%2Fsuperproject%2Fmain

Accessing hidden method Landroid/bluetooth/BluetoothDevice;->createInsecureL2capSocket(I)Landroid/bluetooth/BluetoothSocket; (max-target-o, reflection, denied)

Which needed some convincing to execute (https://github.com/LSPosed/AndroidHiddenApiBypass) but

07:44:04.151 bluetooth                            W  packages/modules/Bluetooth/system/stack/l2cap/l2c_fcr.cc:1603 - l2c_fcr_chk_chan_modes: L2CAP - Peer does not support our desired channel types
07:44:04.151 bt_btif_sock                         I  packages/modules/Bluetooth/system/btif/src/btif_sock.cc:164 - btif_sock_connection_logger: address=xx:xx:xx:xx:e4:0a, state=4, role=2, server_name=, channel=4097
07:44:04.151 bluetooth                            I  packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:363 - send_app_err_code: Sending l2cap failure reason socket_id:34 reason code:1
07:44:04.151 bt_btif_sock                         I  packages/modules/Bluetooth/system/btif/src/btif_sock.cc:164 - btif_sock_connection_logger: address=xx:xx:xx:xx:e4:0a, state=5, role=2, server_name=, channel=4097
07:44:04.151 bluetooth                            I  packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:234 - btsock_l2cap_free_l: Application has already closed l2cap socket socket_id:34

android.bluetooth.BluetoothSocketException: Connection failed for unknown reason 

still no dice.

A friend has tried simulating AirPods on a Desktop computer and was able to connect to that mock via createInsecureL2capSocket from an Android phone, but wasn't able to do this with actual AirPods.

d4rken commented 1 month ago

See here for details from the MagicPods project:

and here is a python script that can connect to AirPods from a desktop computer:

import bluetooth

# Change to MAC address of your AirPods
address = "MAC_ADDRESS"

aap_service = "74EC2172-0BAD-4D01-8F77-997B2BE0722A"
aap_port = 0x1001

# Commands taken from https://github.com/steam3d/MagicPodsCore/blob/master/src/aap/Aap.h
cmd_handshake = b"\x00\x00\x04\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00"
cmd_off = b"\x04\x00\x04\x00\x09\x00\x0d\x01\x00\x00\x00"
cmd_on = b"\x04\x00\x04\x00\x09\x00\x0d\x02\x00\x00\x00"
cmd_transparency = b"\x04\x00\x04\x00\x09\x00\x0d\x03\x00\x00\x00"

services = bluetooth.find_service(address=address)

service = [s for s in services if s["service-classes"] == [aap_service]]

if not service:
    print("Device does not have AAP service")
    exit()

sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)

sock.connect((address, aap_port))

print("Connected to AirPods")
print("Sending handshake...")

sock.send(cmd_handshake)

while True:
    # Ignore the responses because I am unsure what they mean
    # res = sock.recv(1024)
    # print(f"Response: {res.hex()}")

    print("Select command:")
    print("1. Turn off")
    print("2. Turn on")
    print("3. Toggle transparency")
    print("4. Exit")

    cmd = input("Enter command: ")

    if cmd == "1":
        sock.send(cmd_off)
    elif cmd == "2":
        sock.send(cmd_on)
    elif cmd == "3":
        sock.send(cmd_transparency)
    elif cmd == "4":
        break

sock.close()
d4rken commented 1 month ago

Enabling this could enable/fix #38,#186,#187,#191,#204,#212

fynngodau commented 1 month ago

and here is a python script (ty @fynngodau)

I'm pretty sure I have nothing to do with that script, must have been somebody else :blush:

vulpes2 commented 4 days ago

Tried playing with this and I didn't get very far unfortunately. Was able to establish the connection on a desktop computer too, but didn't manage to get anything to happen on Android 14.

The names for createInsecureL2capChannel and createL2capChannel are pretty misleading, if you look at the source closely, you can see they're for BLE only, not BR/EDR. The 0x1001 PSM was reported as invalid because BLE uses SPSM instead of PSM, which are only two bytes in size. It turns out both createL2capSocket and createInsecureL2capSocket are hidden APIs, and I didn't have xposed on my phone, so I couldn't get any further. Maybe some HCI packet logs will help figuring out why the connection is failing.

d4rken commented 4 days ago

Thanks for trying @vulpes2 :beers:. I had some hope that Android 15 would add more APIs here, but didn't see anything in the changelogs so far :frowning:

vulpes2 commented 4 days ago

Seems like L2CAP on BR/EDR was never supported for 3rd party use on Android, which is kind of surprising. Not sure why did they make a new API to create L2CAP sockets for LE, but still wouldn't allow it over BR/EDR to this day.

xerootg commented 3 days ago

Any chance someone with an understanding of L2CAP (I'm certainly not an L2CAP understander) could do a PR into AOSP?

vulpes2 commented 2 days ago

The function exists, you are just not allowed to use it from a normal app. Neither Android nor iOS allow you to create raw L2CAP connections on BR/EDR, but obviously both use them heavily at the system level.