Skip to content

Commit 325ef1e

Browse files
committed
add cross-platform tool for retrieving proxmity key
1 parent 5e30531 commit 325ef1e

1 file changed

Lines changed: 209 additions & 0 deletions

File tree

proximity_keys.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
import asyncio
4+
import argparse
5+
import logging
6+
import platform
7+
from typing import Any, Optional
8+
9+
from colorama import Fore, Style, init as colorama_init
10+
colorama_init(autoreset=True)
11+
12+
handler = logging.StreamHandler()
13+
class ColorFormatter(logging.Formatter):
14+
COLORS = {
15+
logging.DEBUG: Fore.BLUE,
16+
logging.INFO: Fore.GREEN,
17+
logging.WARNING: Fore.YELLOW,
18+
logging.ERROR: Fore.RED,
19+
logging.CRITICAL: Fore.MAGENTA,
20+
}
21+
def format(self, record):
22+
color = self.COLORS.get(record.levelno, "")
23+
prefix = f"{color}[{record.levelname}:{record.name}]{Style.RESET_ALL}"
24+
return f"{prefix} {record.getMessage()}"
25+
handler.setFormatter(ColorFormatter())
26+
logging.basicConfig(level=logging.INFO, handlers=[handler])
27+
logger = logging.getLogger("proximitykeys")
28+
29+
PROXIMITY_KEY_TYPES = {0x01: "IRK", 0x04: "ENC_KEY"}
30+
31+
def parse_proximity_keys_response(data: bytes):
32+
if len(data) < 7 or data[4] != 0x31:
33+
return None
34+
key_count = data[6]
35+
keys = []
36+
offset = 7
37+
for _ in range(key_count):
38+
if offset + 3 >= len(data):
39+
break
40+
key_type = data[offset]
41+
key_length = data[offset + 2]
42+
offset += 4
43+
if offset + key_length > len(data):
44+
break
45+
key_bytes = data[offset:offset + key_length]
46+
keys.append((PROXIMITY_KEY_TYPES.get(key_type, f"TYPE_{key_type:02X}"), key_bytes))
47+
offset += key_length
48+
return keys
49+
50+
def hexdump(data: bytes) -> str:
51+
return " ".join(f"{b:02X}" for b in data)
52+
53+
async def run_bumble(bdaddr: str):
54+
try:
55+
from bumble.l2cap import ClassicChannelSpec
56+
from bumble.transport import open_transport
57+
from bumble.device import Device
58+
from bumble.host import Host
59+
from bumble.core import PhysicalTransport
60+
from bumble.pairing import PairingConfig, PairingDelegate
61+
from bumble.hci import HCI_Error
62+
except ImportError:
63+
logger.error("Bumble not installed")
64+
return 1
65+
66+
PSM_PROXIMITY = 0x1001
67+
HANDSHAKE = bytes.fromhex("00 00 04 00 01 00 02 00 00 00 00 00 00 00 00 00")
68+
KEY_REQ = bytes.fromhex("04 00 04 00 30 00 05 00")
69+
70+
class KeyStore:
71+
async def delete(self, name: str): pass
72+
async def update(self, name: str, keys: Any): pass
73+
async def get(self, _name: str) -> Optional[Any]: return None
74+
async def get_all(self): return []
75+
76+
async def get_resolving_keys(self) -> list[tuple[bytes, Any]]:
77+
all_keys = await self.get_all()
78+
resolving_keys = []
79+
for name, keys in all_keys:
80+
if getattr(keys, "irk", None) is not None:
81+
resolving_keys.append((
82+
keys.irk.value,
83+
getattr(keys, "address", "UNKNOWN")
84+
))
85+
return resolving_keys
86+
87+
async def exchange_keys(channel, timeout=5.0):
88+
recv_q: asyncio.Queue = asyncio.Queue()
89+
channel.sink = lambda sdu: recv_q.put_nowait(sdu)
90+
logger.info("Sending handshake packet...")
91+
channel.send_pdu(HANDSHAKE)
92+
await asyncio.sleep(0.5)
93+
logger.info("Sending key request packet...")
94+
channel.send_pdu(KEY_REQ)
95+
while True:
96+
try:
97+
pkt = await asyncio.wait_for(recv_q.get(), timeout)
98+
except asyncio.TimeoutError:
99+
logger.error("Timed out waiting for SDU response")
100+
return None
101+
logger.debug("Received SDU (%d bytes): %s", len(pkt), hexdump(pkt))
102+
keys = parse_proximity_keys_response(pkt)
103+
if keys:
104+
return keys
105+
106+
async def get_device():
107+
logger.info("Opening transport...")
108+
transport = await open_transport("usb:0")
109+
device = Device(host=Host(controller_source=transport.source, controller_sink=transport.sink))
110+
device.classic_enabled = True
111+
device.le_enabled = False
112+
device.keystore = KeyStore()
113+
device.pairing_config_factory = lambda conn: PairingConfig(
114+
sc=True, mitm=False, bonding=True,
115+
delegate=PairingDelegate(io_capability=PairingDelegate.NO_OUTPUT_NO_INPUT)
116+
)
117+
await device.power_on()
118+
logger.info("Device powered on")
119+
return transport, device
120+
121+
async def create_channel_and_exchange(conn):
122+
spec = ClassicChannelSpec(psm=PSM_PROXIMITY, mtu=2048)
123+
logger.info("Requesting L2CAP channel on PSM = 0x%04X", spec.psm)
124+
if not conn.is_encrypted:
125+
logger.info("Enabling link encryption...")
126+
await conn.encrypt()
127+
await asyncio.sleep(0.05)
128+
channel = await conn.create_l2cap_channel(spec=spec)
129+
keys = await exchange_keys(channel, timeout=8.0)
130+
if not keys:
131+
logger.warning("No proximity keys found")
132+
return
133+
logger.info("Keys successfully retrieved")
134+
print(f"{Fore.CYAN}{Style.BRIGHT}Proximity Keys:{Style.RESET_ALL}")
135+
for name, key_bytes in keys:
136+
print(f" {Fore.MAGENTA}{name}{Style.RESET_ALL}: {hexdump(key_bytes)}")
137+
138+
transport, device = await get_device()
139+
logger.info("Connecting to %s (BR/EDR)...", bdaddr)
140+
try:
141+
connection = await device.connect(bdaddr, PhysicalTransport.BR_EDR)
142+
logger.info("Connected to %s (handle %s)", connection.peer_address, connection.handle)
143+
logger.info("Authenticating...")
144+
await connection.authenticate()
145+
if not connection.is_encrypted:
146+
logger.info("Encrypting link...")
147+
await connection.encrypt()
148+
await create_channel_and_exchange(connection)
149+
except HCI_Error as e:
150+
if "PAIRING_NOT_ALLOWED_ERROR" in str(e):
151+
logger.error("Put your device into pairing mode and run the script again")
152+
else:
153+
logger.error("HCI error: %s", e)
154+
except Exception as e:
155+
logger.error("Unexpected error: %s", e)
156+
finally:
157+
if hasattr(transport, "close"):
158+
logger.info("Closing transport...")
159+
await transport.close()
160+
logger.info("Transport closed")
161+
return 0
162+
163+
def run_linux(bdaddr: str):
164+
import socket
165+
PSM = 0x1001
166+
handshake = bytes.fromhex("00 00 04 00 01 00 02 00 00 00 00 00 00 00 00 00")
167+
key_req = bytes.fromhex("04 00 04 00 30 00 05 00")
168+
169+
logger.info("Connecting to %s (L2CAP)...", bdaddr)
170+
sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
171+
try:
172+
sock.connect((bdaddr, PSM))
173+
logger.info("Connected, sending handshake and key request...")
174+
sock.send(handshake)
175+
sock.send(key_req)
176+
177+
while True:
178+
pkt = sock.recv(1024)
179+
logger.debug("Received packet (%d bytes): %s", len(pkt), hexdump(pkt))
180+
keys = parse_proximity_keys_response(pkt)
181+
if keys:
182+
logger.info("Keys successfully retrieved")
183+
print(f"{Fore.CYAN}{Style.BRIGHT}Proximity Keys:{Style.RESET_ALL}")
184+
for name, key_bytes in keys:
185+
print(f" {Fore.MAGENTA}{name}{Style.RESET_ALL}: {hexdump(key_bytes)}")
186+
break
187+
else:
188+
logger.warning("Received packet did not contain keys, waiting...")
189+
except Exception as e:
190+
logger.error("Error during L2CAP exchange: %s", e)
191+
finally:
192+
sock.close()
193+
logger.info("Connection closed")
194+
195+
def main():
196+
parser = argparse.ArgumentParser()
197+
parser.add_argument("bdaddr")
198+
parser.add_argument("--debug", action="store_true")
199+
parser.add_argument("--bumble", action="store_true")
200+
args = parser.parse_args()
201+
logging.getLogger().setLevel(logging.DEBUG if args.debug else logging.INFO)
202+
203+
if args.bumble or platform.system() == "Windows":
204+
asyncio.run(run_bumble(args.bdaddr))
205+
else:
206+
run_linux(args.bdaddr)
207+
208+
if __name__ == "__main__":
209+
main()

0 commit comments

Comments
 (0)