Skip to content

Commit

Permalink
Merge pull request #522 from google/gbg/rpa2
Browse files Browse the repository at this point in the history
add basic RPA support
  • Loading branch information
barbibulle authored Aug 6, 2024
2 parents ae8b83f + 312fc8d commit 4433184
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 59 deletions.
124 changes: 99 additions & 25 deletions bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
BaseBumbleError,
ConnectionParameterUpdateError,
CommandTimeoutError,
ConnectionParameters,
ConnectionPHY,
InvalidArgumentError,
InvalidOperationError,
Expand Down Expand Up @@ -259,8 +260,9 @@
DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
HCI_LE_Set_Extended_Advertising_Parameters_Command.TX_POWER_NO_PREFERENCE
)
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)

# fmt: on
# pylint: enable=line-too-long
Expand Down Expand Up @@ -1303,6 +1305,7 @@ class Connection(CompositeEventEmitter):
handle: int
transport: int
self_address: Address
self_resolvable_address: Optional[Address]
peer_address: Address
peer_resolvable_address: Optional[Address]
peer_le_features: Optional[LeFeatureMask]
Expand Down Expand Up @@ -1350,6 +1353,7 @@ def __init__(
handle,
transport,
self_address,
self_resolvable_address,
peer_address,
peer_resolvable_address,
role,
Expand All @@ -1361,6 +1365,7 @@ def __init__(
self.handle = handle
self.transport = transport
self.self_address = self_address
self.self_resolvable_address = self_resolvable_address
self.peer_address = peer_address
self.peer_resolvable_address = peer_resolvable_address
self.peer_name = None # Classic only
Expand Down Expand Up @@ -1394,6 +1399,7 @@ def incomplete(cls, device, peer_address, role):
None,
BT_BR_EDR_TRANSPORT,
device.public_address,
None,
peer_address,
None,
role,
Expand Down Expand Up @@ -1552,7 +1558,9 @@ def __str__(self):
f'Connection(handle=0x{self.handle:04X}, '
f'role={self.role_name}, '
f'self_address={self.self_address}, '
f'peer_address={self.peer_address})'
f'self_resolvable_address={self.self_resolvable_address}, '
f'peer_address={self.peer_address}, '
f'peer_resolvable_address={self.peer_resolvable_address})'
)


Expand All @@ -1567,8 +1575,9 @@ class DeviceConfiguration:
advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
le_enabled: bool = True
# LE host enable 2nd parameter
le_simultaneous_enabled: bool = False
le_privacy_enabled: bool = False
le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
classic_enabled: bool = False
classic_sc_enabled: bool = True
classic_ssp_enabled: bool = True
Expand All @@ -1584,6 +1593,7 @@ class DeviceConfiguration:
irk: bytes = bytes(16) # This really must be changed for any level of security
keystore: Optional[str] = None
address_resolution_offload: bool = False
address_generation_offload: bool = False
cis_enabled: bool = False

def __post_init__(self) -> None:
Expand Down Expand Up @@ -1736,8 +1746,9 @@ def host_event_handler(function):
# -----------------------------------------------------------------------------
class Device(CompositeEventEmitter):
# Incomplete list of fields.
random_address: Address
public_address: Address
random_address: Address # Random address that may change with RPA
public_address: Address # Public address (obtained from the controller)
static_address: Address # Random address that can be set but does not change
classic_enabled: bool
name: str
class_of_device: int
Expand Down Expand Up @@ -1867,15 +1878,19 @@ def __init__(
config = config or DeviceConfiguration()
self.config = config

self.public_address = Address('00:00:00:00:00:00')
self.name = config.name
self.public_address = Address.ANY
self.random_address = config.address
self.static_address = config.address
self.class_of_device = config.class_of_device
self.keystore = None
self.irk = config.irk
self.le_enabled = config.le_enabled
self.classic_enabled = config.classic_enabled
self.le_simultaneous_enabled = config.le_simultaneous_enabled
self.le_privacy_enabled = config.le_privacy_enabled
self.le_rpa_timeout = config.le_rpa_timeout
self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
self.classic_enabled = config.classic_enabled
self.cis_enabled = config.cis_enabled
self.classic_sc_enabled = config.classic_sc_enabled
self.classic_ssp_enabled = config.classic_ssp_enabled
Expand All @@ -1884,6 +1899,7 @@ def __init__(
self.connectable = config.connectable
self.classic_accept_any = config.classic_accept_any
self.address_resolution_offload = config.address_resolution_offload
self.address_generation_offload = config.address_generation_offload

# Extended advertising.
self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
Expand Down Expand Up @@ -1939,6 +1955,7 @@ def __init__(
if isinstance(address, str):
address = Address(address)
self.random_address = address
self.static_address = address

# Setup SMP
self.smp_manager = smp.Manager(
Expand Down Expand Up @@ -2170,6 +2187,16 @@ async def power_on(self) -> None:
)

if self.le_enabled:
# If LE Privacy is enabled, generate an RPA
if self.le_privacy_enabled:
self.random_address = Address.generate_private_address(self.irk)
logger.info(f'Initial RPA: {self.random_address}')
if self.le_rpa_timeout > 0:
# Start a task to periodically generate a new RPA
self.le_rpa_periodic_update_task = asyncio.create_task(
self._run_rpa_periodic_update()
)

# Set the controller address
if self.random_address == Address.ANY_RANDOM:
# Try to use an address generated at random by the controller
Expand Down Expand Up @@ -2249,17 +2276,53 @@ async def reset(self) -> None:

async def power_off(self) -> None:
if self.powered_on:
if self.le_rpa_periodic_update_task:
self.le_rpa_periodic_update_task.cancel()

await self.host.flush()

self.powered_on = False

async def update_rpa(self) -> bool:
"""
Try to update the RPA.
Returns:
True if the RPA was updated, False if it could not be updated.
"""

# Check if this is a good time to rotate the address
if self.is_advertising or self.is_scanning or self.is_le_connecting:
logger.debug('skipping RPA update')
return False

random_address = Address.generate_private_address(self.irk)
response = await self.send_command(
HCI_LE_Set_Random_Address_Command(random_address=self.random_address)
)
if response.return_parameters == HCI_SUCCESS:
logger.info(f'new RPA: {random_address}')
self.random_address = random_address
return True
else:
logger.warning(f'failed to set RPA: {response.return_parameters}')
return False

async def _run_rpa_periodic_update(self) -> None:
"""Update the RPA periodically"""
while self.le_rpa_timeout != 0:
await asyncio.sleep(self.le_rpa_timeout)
if not self.update_rpa():
logger.debug("periodic RPA update failed")

async def refresh_resolving_list(self) -> None:
assert self.keystore is not None

resolving_keys = await self.keystore.get_resolving_keys()
# Create a host-side address resolver
self.address_resolver = smp.AddressResolver(resolving_keys)

if self.address_resolution_offload:
if self.address_resolution_offload or self.address_generation_offload:
await self.send_command(HCI_LE_Clear_Resolving_List_Command())

# Add an empty entry for non-directed address generation.
Expand Down Expand Up @@ -4104,12 +4167,14 @@ async def read_phy():
@host_event_handler
def on_connection(
self,
connection_handle,
transport,
peer_address,
role,
connection_parameters,
):
connection_handle: int,
transport: int,
peer_address: Address,
self_resolvable_address: Optional[Address],
peer_resolvable_address: Optional[Address],
role: int,
connection_parameters: ConnectionParameters,
) -> None:
logger.debug(
f'*** Connection: [0x{connection_handle:04X}] '
f'{peer_address} {"" if role is None else HCI_Constant.role_name(role)}'
Expand All @@ -4130,15 +4195,15 @@ def on_connection(

return

# Resolve the peer address if we can
peer_resolvable_address = None
if self.address_resolver:
if peer_address.is_resolvable:
resolved_address = self.address_resolver.resolve(peer_address)
if resolved_address is not None:
logger.debug(f'*** Address resolved as {resolved_address}')
peer_resolvable_address = peer_address
peer_address = resolved_address
if peer_resolvable_address is None:
# Resolve the peer address if we can
if self.address_resolver:
if peer_address.is_resolvable:
resolved_address = self.address_resolver.resolve(peer_address)
if resolved_address is not None:
logger.debug(f'*** Address resolved as {resolved_address}')
peer_resolvable_address = peer_address
peer_address = resolved_address

self_address = None
if role == HCI_CENTRAL_ROLE:
Expand Down Expand Up @@ -4169,12 +4234,19 @@ def on_connection(
else self.random_address
)

# Convert all-zeros addresses into None.
if self_resolvable_address == Address.ANY_RANDOM:
self_resolvable_address = None
if peer_resolvable_address == Address.ANY_RANDOM:
peer_resolvable_address = None

# Create a connection.
connection = Connection(
self,
connection_handle,
transport,
self_address,
self_resolvable_address,
peer_address,
peer_resolvable_address,
role,
Expand All @@ -4185,9 +4257,10 @@ def on_connection(

if role == HCI_PERIPHERAL_ROLE and self.legacy_advertiser:
if self.legacy_advertiser.auto_restart:
advertiser = self.legacy_advertiser
connection.once(
'disconnection',
lambda _: self.abort_on('flush', self.legacy_advertiser.start()),
lambda _: self.abort_on('flush', advertiser.start()),
)
else:
self.legacy_advertiser = None
Expand Down Expand Up @@ -4871,5 +4944,6 @@ def __str__(self):
return (
f'Device(name="{self.name}", '
f'random_address="{self.random_address}", '
f'public_address="{self.public_address}")'
f'public_address="{self.public_address}", '
f'static_address="{self.static_address}")'
)
13 changes: 10 additions & 3 deletions bumble/hci.py
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,12 @@ def parse_address(data, offset):
data, offset, Address.PUBLIC_DEVICE_ADDRESS
)

@staticmethod
def parse_random_address(data, offset):
return Address.parse_address_with_type(
data, offset, Address.RANDOM_DEVICE_ADDRESS
)

@staticmethod
def parse_address_with_type(data, offset, address_type):
return offset + 6, Address(data[offset : offset + 6], address_type)
Expand Down Expand Up @@ -1965,7 +1971,8 @@ def __hash__(self):

def __eq__(self, other):
return (
self.address_bytes == other.address_bytes
isinstance(other, Address)
and self.address_bytes == other.address_bytes
and self.is_public == other.is_public
)

Expand Down Expand Up @@ -5178,8 +5185,8 @@ class HCI_LE_Data_Length_Change_Event(HCI_LE_Meta_Event):
),
('peer_address_type', Address.ADDRESS_TYPE_SPEC),
('peer_address', Address.parse_address_preceded_by_type),
('local_resolvable_private_address', Address.parse_address),
('peer_resolvable_private_address', Address.parse_address),
('local_resolvable_private_address', Address.parse_random_address),
('peer_resolvable_private_address', Address.parse_random_address),
('connection_interval', 2),
('peripheral_latency', 2),
('supervision_timeout', 2),
Expand Down
4 changes: 4 additions & 0 deletions bumble/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,8 @@ def on_hci_le_connection_complete_event(self, event):
event.connection_handle,
BT_LE_TRANSPORT,
event.peer_address,
getattr(event, 'local_resolvable_private_address', None),
getattr(event, 'peer_resolvable_private_address', None),
event.role,
connection_parameters,
)
Expand Down Expand Up @@ -817,6 +819,8 @@ def on_hci_connection_complete_event(self, event):
event.bd_addr,
None,
None,
None,
None,
)
else:
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
Expand Down
9 changes: 6 additions & 3 deletions bumble/smp.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,8 +767,11 @@ def __init__(
self.oob_data_flag = 0 if pairing_config.oob is None else 1

# Set up addresses
self_address = connection.self_address
self_address = connection.self_resolvable_address or connection.self_address
peer_address = connection.peer_resolvable_address or connection.peer_address
logger.debug(
f"pairing with self_address={self_address}, peer_address={peer_address}"
)
if self.is_initiator:
self.ia = bytes(self_address)
self.iat = 1 if self_address.is_random else 0
Expand Down Expand Up @@ -1076,9 +1079,9 @@ def send_pairing_dhkey_check_command(self) -> None:

def send_identity_address_command(self) -> None:
identity_address = {
None: self.connection.self_address,
None: self.manager.device.static_address,
Address.PUBLIC_DEVICE_ADDRESS: self.manager.device.public_address,
Address.RANDOM_DEVICE_ADDRESS: self.manager.device.random_address,
Address.RANDOM_DEVICE_ADDRESS: self.manager.device.static_address,
}[self.pairing_config.identity_address_type]
self.send_command(
SMP_Identity_Address_Information_Command(
Expand Down
7 changes: 7 additions & 0 deletions examples/device_with_rpa.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "Bumble",
"address": "F0:F1:F2:F3:F4:F5",
"keystore": "JsonKeyStore",
"irk": "865F81FF5A8B486EAAE29A27AD9F77DC",
"le_privacy_enabled": true
}
Loading

0 comments on commit 4433184

Please sign in to comment.