forked from auracaster/bumble_mirror
Compare commits
33 Commits
gbg/bt-ben
...
gbg/bt-ben
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
775b2d5d7f | ||
|
|
3b399ea1a2 | ||
|
|
84f7cad678 | ||
|
|
778f439e1c | ||
|
|
1b95d4e1df | ||
|
|
512f6d4ee1 | ||
|
|
c52b614abb | ||
|
|
7b7afc7179 | ||
|
|
b1c6044533 | ||
|
|
38499dfe3c | ||
|
|
b58c29202a | ||
|
|
ca759ca967 | ||
|
|
3858bf80c1 | ||
|
|
a88a034ce2 | ||
|
|
6b2cd1147d | ||
|
|
bb8dcaf63e | ||
|
|
8e84b528ce | ||
|
|
8b59b4f515 | ||
|
|
dcc72e49a2 | ||
|
|
ce04c163db | ||
|
|
9f1e95d87f | ||
|
|
088bcbed0b | ||
|
|
57fbad6fa4 | ||
|
|
6926d5cb70 | ||
|
|
00c7df6a11 | ||
|
|
fbd03ed4a5 | ||
|
|
d3bd5a759f | ||
|
|
dedef79bef | ||
|
|
8db974877e | ||
|
|
e7d1531eae | ||
|
|
4785fe6002 | ||
|
|
22d6a7bf05 | ||
|
|
97757c0c3d |
26
.github/ci-gradle.properties
vendored
Normal file
26
.github/ci-gradle.properties
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
# Declare we support AndroidX
|
||||
android.useAndroidX=true
|
||||
|
||||
org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=1536m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
kotlin.compiler.execution.strategy=in-process
|
||||
33
.github/workflows/gradle-btbench.yml
vendored
Normal file
33
.github/workflows/gradle-btbench.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Gradle Android Build & test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'extras/android/BtBench/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
|
||||
- name: Build with Gradle
|
||||
run: cd extras/android/BtBench && ./gradlew build
|
||||
158
apps/bench.py
158
apps/bench.py
@@ -23,6 +23,7 @@ import os
|
||||
import statistics
|
||||
import struct
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
@@ -75,6 +76,7 @@ DEFAULT_CENTRAL_ADDRESS = 'F0:F0:F0:F0:F0:F0'
|
||||
DEFAULT_CENTRAL_NAME = 'Speed Central'
|
||||
DEFAULT_PERIPHERAL_ADDRESS = 'F1:F1:F1:F1:F1:F1'
|
||||
DEFAULT_PERIPHERAL_NAME = 'Speed Peripheral'
|
||||
DEFAULT_ADVERTISING_INTERVAL = 100
|
||||
|
||||
SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5'
|
||||
SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
|
||||
@@ -121,9 +123,9 @@ def print_connection(connection):
|
||||
|
||||
params.append(
|
||||
'Parameters='
|
||||
f'{connection.parameters.connection_interval * 1.25:.2f}/'
|
||||
f'{connection.parameters.connection_interval:.2f}/'
|
||||
f'{connection.parameters.peripheral_latency}/'
|
||||
f'{connection.parameters.supervision_timeout * 10} '
|
||||
f'{connection.parameters.supervision_timeout:.2f} '
|
||||
)
|
||||
|
||||
params.append(f'MTU={connection.att_mtu}')
|
||||
@@ -197,6 +199,51 @@ async def switch_roles(connection, role):
|
||||
logging.info(f'{color("### Role switch failed:", "red")} {error}')
|
||||
|
||||
|
||||
async def pre_power_on(device: Device, classic: bool) -> None:
|
||||
device.classic_enabled = classic
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
device.config.keystore = "JsonKeyStore"
|
||||
device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
|
||||
async def post_power_on(
|
||||
device: Device,
|
||||
le_scan: Optional[tuple[int, int]],
|
||||
le_advertise: Optional[int],
|
||||
classic_page_scan: bool,
|
||||
classic_inquiry_scan: bool,
|
||||
) -> None:
|
||||
if classic_page_scan:
|
||||
logging.info(color("*** Enabling page scan", "blue"))
|
||||
await device.set_connectable(True)
|
||||
if classic_inquiry_scan:
|
||||
logging.info(color("*** Enabling inquiry scan", "blue"))
|
||||
await device.set_discoverable(True)
|
||||
|
||||
if le_scan:
|
||||
scan_window, scan_interval = le_scan
|
||||
logging.info(
|
||||
color(
|
||||
f"*** Starting LE scanning [{scan_window}ms/{scan_interval}ms]",
|
||||
"blue",
|
||||
)
|
||||
)
|
||||
await device.start_scanning(
|
||||
scan_interval=scan_interval, scan_window=scan_window
|
||||
)
|
||||
|
||||
if le_advertise:
|
||||
logging.info(color(f"*** Starting LE advertising [{le_advertise}ms]", "blue"))
|
||||
await device.start_advertising(
|
||||
advertising_interval_min=le_advertise,
|
||||
advertising_interval_max=le_advertise,
|
||||
auto_restart=True,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Packet
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1194,6 +1241,10 @@ class Central(Connection.Listener):
|
||||
encrypt,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
le_advertise,
|
||||
classic_page_scan,
|
||||
classic_inquiry_scan,
|
||||
):
|
||||
super().__init__()
|
||||
self.transport = transport
|
||||
@@ -1205,6 +1256,10 @@ class Central(Connection.Listener):
|
||||
self.encrypt = encrypt or authenticate
|
||||
self.extended_data_length = extended_data_length
|
||||
self.role_switch = role_switch
|
||||
self.le_scan = le_scan
|
||||
self.le_advertise = le_advertise
|
||||
self.classic_page_scan = classic_page_scan
|
||||
self.classic_inquiry_scan = classic_inquiry_scan
|
||||
self.device = None
|
||||
self.connection = None
|
||||
|
||||
@@ -1253,18 +1308,16 @@ class Central(Connection.Listener):
|
||||
)
|
||||
mode = self.mode_factory(self.device)
|
||||
scenario = self.scenario_factory(mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
|
||||
if self.classic:
|
||||
await self.device.set_discoverable(False)
|
||||
await self.device.set_connectable(False)
|
||||
await post_power_on(
|
||||
self.device,
|
||||
self.le_scan,
|
||||
self.le_advertise,
|
||||
self.classic_page_scan,
|
||||
self.classic_inquiry_scan,
|
||||
)
|
||||
|
||||
logging.info(
|
||||
color(f'### Connecting to {self.peripheral_address}...', 'cyan')
|
||||
@@ -1377,6 +1430,10 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
classic,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
le_advertise,
|
||||
classic_page_scan,
|
||||
classic_inquiry_scan,
|
||||
):
|
||||
self.transport = transport
|
||||
self.classic = classic
|
||||
@@ -1384,12 +1441,20 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.mode_factory = mode_factory
|
||||
self.extended_data_length = extended_data_length
|
||||
self.role_switch = role_switch
|
||||
self.le_scan = le_scan
|
||||
self.classic_page_scan = classic_page_scan
|
||||
self.classic_inquiry_scan = classic_inquiry_scan
|
||||
self.scenario = None
|
||||
self.mode = None
|
||||
self.device = None
|
||||
self.connection = None
|
||||
self.connected = asyncio.Event()
|
||||
|
||||
if le_advertise:
|
||||
self.le_advertise = le_advertise
|
||||
else:
|
||||
self.le_advertise = 0 if classic else DEFAULT_ADVERTISING_INTERVAL
|
||||
|
||||
async def run(self):
|
||||
logging.info(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
@@ -1405,20 +1470,16 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.device.listener = self
|
||||
self.mode = self.mode_factory(self.device)
|
||||
self.scenario = self.scenario_factory(self.mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
|
||||
if self.classic:
|
||||
await self.device.set_discoverable(True)
|
||||
await self.device.set_connectable(True)
|
||||
else:
|
||||
await self.device.start_advertising(auto_restart=True)
|
||||
await post_power_on(
|
||||
self.device,
|
||||
self.le_scan,
|
||||
self.le_advertise,
|
||||
self.classic or self.classic_page_scan,
|
||||
self.classic or self.classic_inquiry_scan,
|
||||
)
|
||||
|
||||
if self.classic:
|
||||
logging.info(
|
||||
@@ -1449,10 +1510,14 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.connection = connection
|
||||
self.connected.set()
|
||||
|
||||
# Stop being discoverable and connectable
|
||||
# Stop being discoverable and connectable if possible
|
||||
if self.classic:
|
||||
AsyncRunner.spawn(self.device.set_discoverable(False))
|
||||
AsyncRunner.spawn(self.device.set_connectable(False))
|
||||
if not self.classic_inquiry_scan:
|
||||
logging.info(color("*** Stopping inquiry scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_discoverable(False))
|
||||
if not self.classic_page_scan:
|
||||
logging.info(color("*** Stopping page scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_connectable(False))
|
||||
|
||||
# Request a new data length if needed
|
||||
if not self.classic and self.extended_data_length:
|
||||
@@ -1473,7 +1538,9 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.scenario.reset()
|
||||
|
||||
if self.classic:
|
||||
logging.info(color("*** Enabling inquiry scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_discoverable(True))
|
||||
logging.info(color("*** Enabling page scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_connectable(True))
|
||||
|
||||
def on_connection_parameters_update(self):
|
||||
@@ -1619,6 +1686,7 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
)
|
||||
@click.option(
|
||||
'--extended-data-length',
|
||||
metavar='<TX-OCTETS>/<TX-TIME>',
|
||||
help='Request a data length upon connection, specified as tx_octets/tx_time',
|
||||
)
|
||||
@click.option(
|
||||
@@ -1626,6 +1694,26 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
type=click.Choice(['central', 'peripheral']),
|
||||
help='Request role switch upon connection (central or peripheral)',
|
||||
)
|
||||
@click.option(
|
||||
'--le-scan',
|
||||
metavar='<WINDOW>/<INTERVAL>',
|
||||
help='Perform an LE scan with a given window and interval (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--le-advertise',
|
||||
metavar='<INTERVAL>',
|
||||
help='Advertise with a given interval (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--classic-page-scan',
|
||||
is_flag=True,
|
||||
help='Enable Classic page scanning',
|
||||
)
|
||||
@click.option(
|
||||
'--classic-inquiry-scan',
|
||||
is_flag=True,
|
||||
help='Enable Classic enquiry scanning',
|
||||
)
|
||||
@click.option(
|
||||
'--rfcomm-channel',
|
||||
type=int,
|
||||
@@ -1751,6 +1839,10 @@ def bench(
|
||||
att_mtu,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
le_advertise,
|
||||
classic_page_scan,
|
||||
classic_inquiry_scan,
|
||||
packet_size,
|
||||
packet_count,
|
||||
start_delay,
|
||||
@@ -1799,6 +1891,10 @@ def bench(
|
||||
else None
|
||||
)
|
||||
ctx.obj['role_switch'] = role_switch
|
||||
ctx.obj['le_scan'] = [float(x) for x in le_scan.split('/')] if le_scan else None
|
||||
ctx.obj['le_advertise'] = float(le_advertise) if le_advertise else None
|
||||
ctx.obj['classic_page_scan'] = classic_page_scan
|
||||
ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
|
||||
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
|
||||
|
||||
|
||||
@@ -1843,6 +1939,10 @@ def central(
|
||||
encrypt or authenticate,
|
||||
ctx.obj['extended_data_length'],
|
||||
ctx.obj['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
ctx.obj['le_advertise'],
|
||||
ctx.obj['classic_page_scan'],
|
||||
ctx.obj['classic_inquiry_scan'],
|
||||
).run()
|
||||
|
||||
asyncio.run(run_central())
|
||||
@@ -1864,6 +1964,10 @@ def peripheral(ctx, transport):
|
||||
ctx.obj['classic'],
|
||||
ctx.obj['extended_data_length'],
|
||||
ctx.obj['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
ctx.obj['le_advertise'],
|
||||
ctx.obj['classic_page_scan'],
|
||||
ctx.obj['classic_inquiry_scan'],
|
||||
).run()
|
||||
|
||||
asyncio.run(run_peripheral())
|
||||
|
||||
@@ -335,9 +335,9 @@ class ConsoleApp:
|
||||
elif self.connected_peer:
|
||||
connection = self.connected_peer.connection
|
||||
connection_parameters = (
|
||||
f'{connection.parameters.connection_interval}/'
|
||||
f'{connection.parameters.connection_interval:.2f}/'
|
||||
f'{connection.parameters.peripheral_latency}/'
|
||||
f'{connection.parameters.supervision_timeout}'
|
||||
f'{connection.parameters.supervision_timeout:.2f}'
|
||||
)
|
||||
if self.connection_phy is not None:
|
||||
phy_state = (
|
||||
|
||||
217
apps/pair.py
217
apps/pair.py
@@ -18,9 +18,12 @@
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import struct
|
||||
|
||||
import click
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
|
||||
from bumble.a2dp import make_audio_sink_service_sdp_records
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport_or_link
|
||||
@@ -30,16 +33,20 @@ from bumble.smp import error_name as smp_error_name
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.core import (
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ProtocolError,
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
)
|
||||
from bumble.gatt import (
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Service,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
)
|
||||
from bumble.hci import OwnAddressType
|
||||
from bumble.att import (
|
||||
ATT_Error,
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
@@ -62,7 +69,7 @@ class Waiter:
|
||||
self.linger = linger
|
||||
|
||||
def terminate(self):
|
||||
if not self.linger:
|
||||
if not self.linger and not self.done.done:
|
||||
self.done.set_result(None)
|
||||
|
||||
async def wait_until_terminated(self):
|
||||
@@ -193,7 +200,7 @@ class Delegate(PairingDelegate):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_peer_name(peer, mode):
|
||||
if mode == 'classic':
|
||||
if peer.connection.transport == PhysicalTransport.BR_EDR:
|
||||
return await peer.request_name()
|
||||
|
||||
# Try to get the peer name from GATT
|
||||
@@ -225,13 +232,14 @@ def read_with_error(connection):
|
||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
||||
|
||||
|
||||
def write_with_error(connection, _value):
|
||||
if not connection.is_encrypted:
|
||||
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
|
||||
|
||||
if not AUTHENTICATION_ERROR_RETURNED[1]:
|
||||
AUTHENTICATION_ERROR_RETURNED[1] = True
|
||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
||||
# -----------------------------------------------------------------------------
|
||||
def sdp_records():
|
||||
service_record_handle = 0x00010001
|
||||
return {
|
||||
service_record_handle: make_audio_sink_service_sdp_records(
|
||||
service_record_handle
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -239,15 +247,19 @@ def on_connection(connection, request):
|
||||
print(color(f'<<< Connection: {connection}', 'green'))
|
||||
|
||||
# Listen for pairing events
|
||||
connection.on('pairing_start', on_pairing_start)
|
||||
connection.on('pairing', lambda keys: on_pairing(connection, keys))
|
||||
connection.on(connection.EVENT_PAIRING_START, on_pairing_start)
|
||||
connection.on(connection.EVENT_PAIRING, lambda keys: on_pairing(connection, keys))
|
||||
connection.on(
|
||||
'pairing_failure', lambda reason: on_pairing_failure(connection, reason)
|
||||
connection.EVENT_CLASSIC_PAIRING, lambda: on_classic_pairing(connection)
|
||||
)
|
||||
connection.on(
|
||||
connection.EVENT_PAIRING_FAILURE,
|
||||
lambda reason: on_pairing_failure(connection, reason),
|
||||
)
|
||||
|
||||
# Listen for encryption changes
|
||||
connection.on(
|
||||
'connection_encryption_change',
|
||||
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
||||
lambda: on_connection_encryption_change(connection),
|
||||
)
|
||||
|
||||
@@ -288,6 +300,20 @@ async def on_pairing(connection, keys):
|
||||
Waiter.instance.terminate()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_classic_pairing(connection):
|
||||
print(color('***-----------------------------------', 'cyan'))
|
||||
print(
|
||||
color(
|
||||
f'*** Paired [Classic]! (peer identity={connection.peer_address})', 'cyan'
|
||||
)
|
||||
)
|
||||
print(color('***-----------------------------------', 'cyan'))
|
||||
await asyncio.sleep(POST_PAIRING_DELAY)
|
||||
Waiter.instance.terminate()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_pairing_failure(connection, reason):
|
||||
@@ -305,6 +331,7 @@ async def pair(
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
advertising_address,
|
||||
identity_address,
|
||||
linger,
|
||||
io,
|
||||
@@ -313,6 +340,8 @@ async def pair(
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
advertise_service_uuids,
|
||||
advertise_appearance,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name,
|
||||
@@ -328,29 +357,33 @@ async def pair(
|
||||
|
||||
# Expose a GATT characteristic that can be used to trigger pairing by
|
||||
# responding with an authentication error when read
|
||||
if mode == 'le':
|
||||
device.le_enabled = True
|
||||
if mode in ('le', 'dual'):
|
||||
device.add_service(
|
||||
Service(
|
||||
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
[
|
||||
Characteristic(
|
||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
||||
Characteristic.Properties.READ
|
||||
| Characteristic.Properties.WRITE,
|
||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||
CharacteristicValue(
|
||||
read=read_with_error, write=write_with_error
|
||||
),
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READ_REQUIRES_AUTHENTICATION,
|
||||
bytes(1),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# Select LE or Classic
|
||||
if mode == 'classic':
|
||||
# LE and Classic support
|
||||
if mode in ('classic', 'dual'):
|
||||
device.classic_enabled = True
|
||||
device.classic_smp_enabled = ctkd
|
||||
if mode in ('le', 'dual'):
|
||||
device.le_enabled = True
|
||||
if mode == 'dual':
|
||||
device.le_simultaneous_enabled = True
|
||||
|
||||
# Setup SDP
|
||||
if mode in ('classic', 'dual'):
|
||||
device.sdp_service_records = sdp_records()
|
||||
|
||||
# Get things going
|
||||
await device.power_on()
|
||||
@@ -436,13 +469,109 @@ async def pair(
|
||||
print(color(f'Pairing failed: {error}', 'red'))
|
||||
|
||||
else:
|
||||
if mode == 'le':
|
||||
# Advertise so that peers can find us and connect
|
||||
await device.start_advertising(auto_restart=True)
|
||||
else:
|
||||
if mode in ('le', 'dual'):
|
||||
# Advertise so that peers can find us and connect.
|
||||
# Include the heart rate service UUID in the advertisement data
|
||||
# so that devices like iPhones can show this device in their
|
||||
# Bluetooth selector.
|
||||
service_uuids_16 = []
|
||||
service_uuids_32 = []
|
||||
service_uuids_128 = []
|
||||
if advertise_service_uuids:
|
||||
for uuid in advertise_service_uuids:
|
||||
uuid = uuid.replace("-", "")
|
||||
if len(uuid) == 4:
|
||||
service_uuids_16.append(UUID(uuid))
|
||||
elif len(uuid) == 8:
|
||||
service_uuids_32.append(UUID(uuid))
|
||||
elif len(uuid) == 32:
|
||||
service_uuids_128.append(UUID(uuid))
|
||||
else:
|
||||
print(color('Invalid UUID format', 'red'))
|
||||
return
|
||||
else:
|
||||
service_uuids_16.append(GATT_HEART_RATE_SERVICE)
|
||||
|
||||
flags = AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE
|
||||
if mode == 'le':
|
||||
flags |= AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||
if mode == 'dual':
|
||||
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||
|
||||
ad_structs = [
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([flags]),
|
||||
),
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
||||
]
|
||||
if service_uuids_16:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_16),
|
||||
)
|
||||
)
|
||||
if service_uuids_32:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_32),
|
||||
)
|
||||
)
|
||||
if service_uuids_128:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_128),
|
||||
)
|
||||
)
|
||||
|
||||
if advertise_appearance:
|
||||
advertise_appearance = advertise_appearance.upper()
|
||||
try:
|
||||
advertise_appearance_int = int(advertise_appearance)
|
||||
except ValueError:
|
||||
category, subcategory = advertise_appearance.split('/')
|
||||
try:
|
||||
category_enum = Appearance.Category[category]
|
||||
except ValueError:
|
||||
print(
|
||||
color(f'Invalid appearance category {category}', 'red')
|
||||
)
|
||||
return
|
||||
subcategory_class = Appearance.SUBCATEGORY_CLASSES[
|
||||
category_enum
|
||||
]
|
||||
try:
|
||||
subcategory_enum = subcategory_class[subcategory]
|
||||
except ValueError:
|
||||
print(color(f'Invalid subcategory {subcategory}', 'red'))
|
||||
return
|
||||
advertise_appearance_int = int(
|
||||
Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.APPEARANCE,
|
||||
struct.pack('<H', advertise_appearance_int),
|
||||
)
|
||||
)
|
||||
device.advertising_data = bytes(AdvertisingData(ad_structs))
|
||||
await device.start_advertising(
|
||||
auto_restart=True,
|
||||
own_address_type=(
|
||||
OwnAddressType.PUBLIC
|
||||
if advertising_address == 'public'
|
||||
else OwnAddressType.RANDOM
|
||||
),
|
||||
)
|
||||
|
||||
if mode in ('classic', 'dual'):
|
||||
# Become discoverable and connectable
|
||||
await device.set_discoverable(True)
|
||||
await device.set_connectable(True)
|
||||
print(color('Ready for connections on', 'blue'), device.public_address)
|
||||
|
||||
# Run until the user asks to exit
|
||||
await Waiter.instance.wait_until_terminated()
|
||||
@@ -462,7 +591,10 @@ class LogHandler(logging.Handler):
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.command()
|
||||
@click.option(
|
||||
'--mode', type=click.Choice(['le', 'classic']), default='le', show_default=True
|
||||
'--mode',
|
||||
type=click.Choice(['le', 'classic', 'dual']),
|
||||
default='le',
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
'--sc',
|
||||
@@ -484,6 +616,10 @@ class LogHandler(logging.Handler):
|
||||
help='Enable CTKD',
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
'--advertising-address',
|
||||
type=click.Choice(['random', 'public']),
|
||||
)
|
||||
@click.option(
|
||||
'--identity-address',
|
||||
type=click.Choice(['random', 'public']),
|
||||
@@ -512,9 +648,20 @@ class LogHandler(logging.Handler):
|
||||
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
||||
@click.option(
|
||||
'--keystore-file',
|
||||
metavar='<filename>',
|
||||
metavar='FILENAME',
|
||||
help='File in which to store the pairing keys',
|
||||
)
|
||||
@click.option(
|
||||
'--advertise-service-uuid',
|
||||
metavar="UUID",
|
||||
multiple=True,
|
||||
help="Advertise a GATT service UUID (may be specified more than once)",
|
||||
)
|
||||
@click.option(
|
||||
'--advertise-appearance',
|
||||
metavar='APPEARANCE',
|
||||
help='Advertise an Appearance ID (int value or string)',
|
||||
)
|
||||
@click.argument('device-config')
|
||||
@click.argument('hci_transport')
|
||||
@click.argument('address-or-name', required=False)
|
||||
@@ -524,6 +671,7 @@ def main(
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
advertising_address,
|
||||
identity_address,
|
||||
linger,
|
||||
io,
|
||||
@@ -532,6 +680,8 @@ def main(
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
advertise_service_uuid,
|
||||
advertise_appearance,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name,
|
||||
@@ -550,6 +700,7 @@ def main(
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
advertising_address,
|
||||
identity_address,
|
||||
linger,
|
||||
io,
|
||||
@@ -558,6 +709,8 @@ def main(
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
advertise_service_uuid,
|
||||
advertise_appearance,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name,
|
||||
|
||||
@@ -770,27 +770,25 @@ class AttributeValue(Generic[_T]):
|
||||
def __init__(
|
||||
self,
|
||||
read: Union[
|
||||
Callable[[Optional[Connection]], _T],
|
||||
Callable[[Optional[Connection]], Awaitable[_T]],
|
||||
Callable[[Connection], _T],
|
||||
Callable[[Connection], Awaitable[_T]],
|
||||
None,
|
||||
] = None,
|
||||
write: Union[
|
||||
Callable[[Optional[Connection], _T], None],
|
||||
Callable[[Optional[Connection], _T], Awaitable[None]],
|
||||
Callable[[Connection, _T], None],
|
||||
Callable[[Connection, _T], Awaitable[None]],
|
||||
None,
|
||||
] = None,
|
||||
):
|
||||
self._read = read
|
||||
self._write = write
|
||||
|
||||
def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
|
||||
def read(self, connection: Connection) -> Union[_T, Awaitable[_T]]:
|
||||
if self._read is None:
|
||||
raise InvalidOperationError('AttributeValue has no read function')
|
||||
return self._read(connection)
|
||||
|
||||
def write(
|
||||
self, connection: Optional[Connection], value: _T
|
||||
) -> Union[Awaitable[None], None]:
|
||||
def write(self, connection: Connection, value: _T) -> Union[Awaitable[None], None]:
|
||||
if self._write is None:
|
||||
raise InvalidOperationError('AttributeValue has no write function')
|
||||
return self._write(connection, value)
|
||||
@@ -836,6 +834,9 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
||||
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
||||
|
||||
EVENT_READ = "read"
|
||||
EVENT_WRITE = "write"
|
||||
|
||||
value: Union[AttributeValue[_T], _T, None]
|
||||
|
||||
def __init__(
|
||||
@@ -868,7 +869,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
def decode_value(self, value: bytes) -> _T:
|
||||
return value # type: ignore
|
||||
|
||||
async def read_value(self, connection: Optional[Connection]) -> bytes:
|
||||
async def read_value(self, connection: Connection) -> bytes:
|
||||
if (
|
||||
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
|
||||
and connection is not None
|
||||
@@ -906,11 +907,11 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
else:
|
||||
value = self.value
|
||||
|
||||
self.emit('read', connection, b'' if value is None else value)
|
||||
self.emit(self.EVENT_READ, connection, b'' if value is None else value)
|
||||
|
||||
return b'' if value is None else self.encode_value(value)
|
||||
|
||||
async def write_value(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
async def write_value(self, connection: Connection, value: bytes) -> None:
|
||||
if (
|
||||
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
|
||||
and connection is not None
|
||||
@@ -947,7 +948,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
else:
|
||||
self.value = decoded_value
|
||||
|
||||
self.emit('write', connection, decoded_value)
|
||||
self.emit(self.EVENT_WRITE, connection, decoded_value)
|
||||
|
||||
def __repr__(self):
|
||||
if isinstance(self.value, bytes):
|
||||
|
||||
@@ -166,8 +166,8 @@ class Protocol:
|
||||
|
||||
# Register to receive PDUs from the channel
|
||||
l2cap_channel.sink = self.on_pdu
|
||||
l2cap_channel.on("open", self.on_l2cap_channel_open)
|
||||
l2cap_channel.on("close", self.on_l2cap_channel_close)
|
||||
l2cap_channel.on(l2cap_channel.EVENT_OPEN, self.on_l2cap_channel_open)
|
||||
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||
|
||||
def on_l2cap_channel_open(self):
|
||||
logger.debug(color("<<< AVCTP channel open", "magenta"))
|
||||
|
||||
190
bumble/avdtp.py
190
bumble/avdtp.py
@@ -896,7 +896,7 @@ class Set_Configuration_Reject(Message):
|
||||
self.service_category = self.payload[0]
|
||||
self.error_code = self.payload[1]
|
||||
|
||||
def __init__(self, service_category, error_code):
|
||||
def __init__(self, error_code: int, service_category: int = 0) -> None:
|
||||
super().__init__(payload=bytes([service_category, error_code]))
|
||||
self.service_category = service_category
|
||||
self.error_code = error_code
|
||||
@@ -1132,6 +1132,14 @@ class Security_Control_Command(Message):
|
||||
See Bluetooth AVDTP spec - 8.17.1 Security Control Command
|
||||
'''
|
||||
|
||||
def init_from_payload(self):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.acp_seid = self.payload[0] >> 2
|
||||
self.data = self.payload[1:]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.to_string([f'ACP_SEID: {self.acp_seid}', f'data: {self.data}'])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@Message.subclass
|
||||
@@ -1200,6 +1208,9 @@ class Protocol(utils.EventEmitter):
|
||||
transaction_results: List[Optional[asyncio.Future[Message]]]
|
||||
channel_connector: Callable[[], Awaitable[l2cap.ClassicChannel]]
|
||||
|
||||
EVENT_OPEN = "open"
|
||||
EVENT_CLOSE = "close"
|
||||
|
||||
class PacketType(enum.IntEnum):
|
||||
SINGLE_PACKET = 0
|
||||
START_PACKET = 1
|
||||
@@ -1239,8 +1250,8 @@ class Protocol(utils.EventEmitter):
|
||||
|
||||
# Register to receive PDUs from the channel
|
||||
l2cap_channel.sink = self.on_pdu
|
||||
l2cap_channel.on('open', self.on_l2cap_channel_open)
|
||||
l2cap_channel.on('close', self.on_l2cap_channel_close)
|
||||
l2cap_channel.on(l2cap_channel.EVENT_OPEN, self.on_l2cap_channel_open)
|
||||
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||
|
||||
def get_local_endpoint_by_seid(self, seid: int) -> Optional[LocalStreamEndPoint]:
|
||||
if 0 < seid <= len(self.local_endpoints):
|
||||
@@ -1410,20 +1421,20 @@ class Protocol(utils.EventEmitter):
|
||||
self.transaction_results[transaction_label] = None
|
||||
self.transaction_semaphore.release()
|
||||
|
||||
def on_l2cap_connection(self, channel):
|
||||
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
|
||||
# Forward the channel to the endpoint that's expecting it
|
||||
if self.channel_acceptor is None:
|
||||
logger.warning(color('!!! l2cap connection with no acceptor', 'red'))
|
||||
return
|
||||
self.channel_acceptor.on_l2cap_connection(channel)
|
||||
|
||||
def on_l2cap_channel_open(self):
|
||||
def on_l2cap_channel_open(self) -> None:
|
||||
logger.debug(color('<<< L2CAP channel open', 'magenta'))
|
||||
self.emit('open')
|
||||
self.emit(self.EVENT_OPEN)
|
||||
|
||||
def on_l2cap_channel_close(self):
|
||||
def on_l2cap_channel_close(self) -> None:
|
||||
logger.debug(color('<<< L2CAP channel close', 'magenta'))
|
||||
self.emit('close')
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
|
||||
def send_message(self, transaction_label: int, message: Message) -> None:
|
||||
logger.debug(
|
||||
@@ -1541,28 +1552,34 @@ class Protocol(utils.EventEmitter):
|
||||
async def abort(self, seid: int) -> Abort_Response:
|
||||
return await self.send_command(Abort_Command(seid))
|
||||
|
||||
def on_discover_command(self, _command):
|
||||
def on_discover_command(self, command: Discover_Command) -> Optional[Message]:
|
||||
endpoint_infos = [
|
||||
EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep)
|
||||
for endpoint in self.local_endpoints
|
||||
]
|
||||
return Discover_Response(endpoint_infos)
|
||||
|
||||
def on_get_capabilities_command(self, command):
|
||||
def on_get_capabilities_command(
|
||||
self, command: Get_Capabilities_Command
|
||||
) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Get_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
|
||||
return Get_Capabilities_Response(endpoint.capabilities)
|
||||
|
||||
def on_get_all_capabilities_command(self, command):
|
||||
def on_get_all_capabilities_command(
|
||||
self, command: Get_All_Capabilities_Command
|
||||
) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Get_All_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
|
||||
return Get_All_Capabilities_Response(endpoint.capabilities)
|
||||
|
||||
def on_set_configuration_command(self, command):
|
||||
def on_set_configuration_command(
|
||||
self, command: Set_Configuration_Command
|
||||
) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Set_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
@@ -1578,7 +1595,9 @@ class Protocol(utils.EventEmitter):
|
||||
result = stream.on_set_configuration_command(command.capabilities)
|
||||
return result or Set_Configuration_Response()
|
||||
|
||||
def on_get_configuration_command(self, command):
|
||||
def on_get_configuration_command(
|
||||
self, command: Get_Configuration_Command
|
||||
) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Get_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
@@ -1587,7 +1606,7 @@ class Protocol(utils.EventEmitter):
|
||||
|
||||
return endpoint.stream.on_get_configuration_command()
|
||||
|
||||
def on_reconfigure_command(self, command):
|
||||
def on_reconfigure_command(self, command: Reconfigure_Command) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Reconfigure_Reject(0, AVDTP_BAD_ACP_SEID_ERROR)
|
||||
@@ -1597,7 +1616,7 @@ class Protocol(utils.EventEmitter):
|
||||
result = endpoint.stream.on_reconfigure_command(command.capabilities)
|
||||
return result or Reconfigure_Response()
|
||||
|
||||
def on_open_command(self, command):
|
||||
def on_open_command(self, command: Open_Command) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Open_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
@@ -1607,25 +1626,26 @@ class Protocol(utils.EventEmitter):
|
||||
result = endpoint.stream.on_open_command()
|
||||
return result or Open_Response()
|
||||
|
||||
def on_start_command(self, command):
|
||||
def on_start_command(self, command: Start_Command) -> Optional[Message]:
|
||||
for seid in command.acp_seids:
|
||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||
if endpoint is None:
|
||||
return Start_Reject(seid, AVDTP_BAD_ACP_SEID_ERROR)
|
||||
if endpoint.stream is None:
|
||||
return Start_Reject(AVDTP_BAD_STATE_ERROR)
|
||||
return Start_Reject(seid, AVDTP_BAD_STATE_ERROR)
|
||||
|
||||
# Start all streams
|
||||
# TODO: deal with partial failures
|
||||
for seid in command.acp_seids:
|
||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||
result = endpoint.stream.on_start_command()
|
||||
if result is not None:
|
||||
if not endpoint or not endpoint.stream:
|
||||
raise InvalidStateError("Should already be checked!")
|
||||
if (result := endpoint.stream.on_start_command()) is not None:
|
||||
return result
|
||||
|
||||
return Start_Response()
|
||||
|
||||
def on_suspend_command(self, command):
|
||||
def on_suspend_command(self, command: Suspend_Command) -> Optional[Message]:
|
||||
for seid in command.acp_seids:
|
||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||
if endpoint is None:
|
||||
@@ -1637,13 +1657,14 @@ class Protocol(utils.EventEmitter):
|
||||
# TODO: deal with partial failures
|
||||
for seid in command.acp_seids:
|
||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||
result = endpoint.stream.on_suspend_command()
|
||||
if result is not None:
|
||||
if not endpoint or not endpoint.stream:
|
||||
raise InvalidStateError("Should already be checked!")
|
||||
if (result := endpoint.stream.on_suspend_command()) is not None:
|
||||
return result
|
||||
|
||||
return Suspend_Response()
|
||||
|
||||
def on_close_command(self, command):
|
||||
def on_close_command(self, command: Close_Command) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Close_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
@@ -1653,7 +1674,7 @@ class Protocol(utils.EventEmitter):
|
||||
result = endpoint.stream.on_close_command()
|
||||
return result or Close_Response()
|
||||
|
||||
def on_abort_command(self, command):
|
||||
def on_abort_command(self, command: Abort_Command) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None or endpoint.stream is None:
|
||||
return Abort_Response()
|
||||
@@ -1661,15 +1682,17 @@ class Protocol(utils.EventEmitter):
|
||||
endpoint.stream.on_abort_command()
|
||||
return Abort_Response()
|
||||
|
||||
def on_security_control_command(self, command):
|
||||
def on_security_control_command(
|
||||
self, command: Security_Control_Command
|
||||
) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return Security_Control_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
|
||||
result = endpoint.on_security_control_command(command.payload)
|
||||
result = endpoint.on_security_control_command(command.data)
|
||||
return result or Security_Control_Response()
|
||||
|
||||
def on_delayreport_command(self, command):
|
||||
def on_delayreport_command(self, command: DelayReport_Command) -> Optional[Message]:
|
||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||
if endpoint is None:
|
||||
return DelayReport_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||
@@ -1682,6 +1705,8 @@ class Protocol(utils.EventEmitter):
|
||||
class Listener(utils.EventEmitter):
|
||||
servers: Dict[int, Protocol]
|
||||
|
||||
EVENT_CONNECTION = "connection"
|
||||
|
||||
@staticmethod
|
||||
def create_registrar(device: device.Device):
|
||||
warnings.warn("Please use Listener.for_device()", DeprecationWarning)
|
||||
@@ -1716,7 +1741,7 @@ class Listener(utils.EventEmitter):
|
||||
l2cap_server = device.create_l2cap_server(
|
||||
spec=l2cap.ClassicChannelSpec(psm=AVDTP_PSM)
|
||||
)
|
||||
l2cap_server.on('connection', listener.on_l2cap_connection)
|
||||
l2cap_server.on(l2cap_server.EVENT_CONNECTION, listener.on_l2cap_connection)
|
||||
return listener
|
||||
|
||||
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
|
||||
@@ -1732,14 +1757,14 @@ class Listener(utils.EventEmitter):
|
||||
logger.debug('setting up new Protocol for the connection')
|
||||
server = Protocol(channel, self.version)
|
||||
self.set_server(channel.connection, server)
|
||||
self.emit('connection', server)
|
||||
self.emit(self.EVENT_CONNECTION, server)
|
||||
|
||||
def on_channel_close():
|
||||
logger.debug('removing Protocol for the connection')
|
||||
self.remove_server(channel.connection)
|
||||
|
||||
channel.on('open', on_channel_open)
|
||||
channel.on('close', on_channel_close)
|
||||
channel.on(channel.EVENT_OPEN, on_channel_open)
|
||||
channel.on(channel.EVENT_CLOSE, on_channel_close)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1788,6 +1813,7 @@ class Stream:
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""[Source] Start streaming."""
|
||||
# Auto-open if needed
|
||||
if self.state == AVDTP_CONFIGURED_STATE:
|
||||
await self.open()
|
||||
@@ -1804,6 +1830,7 @@ class Stream:
|
||||
self.change_state(AVDTP_STREAMING_STATE)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""[Source] Stop streaming and transit to OPEN state."""
|
||||
if self.state != AVDTP_STREAMING_STATE:
|
||||
raise InvalidStateError('current state is not STREAMING')
|
||||
|
||||
@@ -1816,6 +1843,7 @@ class Stream:
|
||||
self.change_state(AVDTP_OPEN_STATE)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""[Source] Close channel and transit to IDLE state."""
|
||||
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE):
|
||||
raise InvalidStateError('current state is not OPEN or STREAMING')
|
||||
|
||||
@@ -1847,7 +1875,7 @@ class Stream:
|
||||
self.change_state(AVDTP_CONFIGURED_STATE)
|
||||
return None
|
||||
|
||||
def on_get_configuration_command(self, configuration):
|
||||
def on_get_configuration_command(self):
|
||||
if self.state not in (
|
||||
AVDTP_CONFIGURED_STATE,
|
||||
AVDTP_OPEN_STATE,
|
||||
@@ -1855,7 +1883,7 @@ class Stream:
|
||||
):
|
||||
return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
|
||||
|
||||
return self.local_endpoint.on_get_configuration_command(configuration)
|
||||
return self.local_endpoint.on_get_configuration_command()
|
||||
|
||||
def on_reconfigure_command(self, configuration):
|
||||
if self.state != AVDTP_OPEN_STATE:
|
||||
@@ -1935,20 +1963,20 @@ class Stream:
|
||||
# Wait for the RTP channel to be closed
|
||||
self.change_state(AVDTP_ABORTING_STATE)
|
||||
|
||||
def on_l2cap_connection(self, channel):
|
||||
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
|
||||
logger.debug(color('<<< stream channel connected', 'magenta'))
|
||||
self.rtp_channel = channel
|
||||
channel.on('open', self.on_l2cap_channel_open)
|
||||
channel.on('close', self.on_l2cap_channel_close)
|
||||
channel.on(channel.EVENT_OPEN, self.on_l2cap_channel_open)
|
||||
channel.on(channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||
|
||||
# We don't need more channels
|
||||
self.protocol.channel_acceptor = None
|
||||
|
||||
def on_l2cap_channel_open(self):
|
||||
def on_l2cap_channel_open(self) -> None:
|
||||
logger.debug(color('<<< stream channel open', 'magenta'))
|
||||
self.local_endpoint.on_rtp_channel_open()
|
||||
|
||||
def on_l2cap_channel_close(self):
|
||||
def on_l2cap_channel_close(self) -> None:
|
||||
logger.debug(color('<<< stream channel closed', 'magenta'))
|
||||
self.local_endpoint.on_rtp_channel_close()
|
||||
self.local_endpoint.in_use = 0
|
||||
@@ -2065,6 +2093,19 @@ class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy):
|
||||
class LocalStreamEndPoint(StreamEndPoint, utils.EventEmitter):
|
||||
stream: Optional[Stream]
|
||||
|
||||
EVENT_CONFIGURATION = "configuration"
|
||||
EVENT_OPEN = "open"
|
||||
EVENT_START = "start"
|
||||
EVENT_STOP = "stop"
|
||||
EVENT_RTP_PACKET = "rtp_packet"
|
||||
EVENT_SUSPEND = "suspend"
|
||||
EVENT_CLOSE = "close"
|
||||
EVENT_ABORT = "abort"
|
||||
EVENT_DELAY_REPORT = "delay_report"
|
||||
EVENT_SECURITY_CONTROL = "security_control"
|
||||
EVENT_RTP_CHANNEL_OPEN = "rtp_channel_open"
|
||||
EVENT_RTP_CHANNEL_CLOSE = "rtp_channel_close"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protocol: Protocol,
|
||||
@@ -2080,52 +2121,65 @@ class LocalStreamEndPoint(StreamEndPoint, utils.EventEmitter):
|
||||
self.configuration = configuration if configuration is not None else []
|
||||
self.stream = None
|
||||
|
||||
async def start(self):
|
||||
pass
|
||||
async def start(self) -> None:
|
||||
"""[Source Only] Handles when receiving start command."""
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
async def stop(self) -> None:
|
||||
"""[Source Only] Handles when receiving stop command."""
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
async def close(self) -> None:
|
||||
"""[Source Only] Handles when receiving close command."""
|
||||
|
||||
def on_reconfigure_command(self, command):
|
||||
pass
|
||||
def on_reconfigure_command(self, command) -> Optional[Message]:
|
||||
return None
|
||||
|
||||
def on_set_configuration_command(self, configuration):
|
||||
def on_set_configuration_command(self, configuration) -> Optional[Message]:
|
||||
logger.debug(
|
||||
'<<< received configuration: '
|
||||
f'{",".join([str(capability) for capability in configuration])}'
|
||||
)
|
||||
self.configuration = configuration
|
||||
self.emit('configuration')
|
||||
self.emit(self.EVENT_CONFIGURATION)
|
||||
return None
|
||||
|
||||
def on_get_configuration_command(self):
|
||||
def on_get_configuration_command(self) -> Optional[Message]:
|
||||
return Get_Configuration_Response(self.configuration)
|
||||
|
||||
def on_open_command(self):
|
||||
self.emit('open')
|
||||
def on_open_command(self) -> Optional[Message]:
|
||||
self.emit(self.EVENT_OPEN)
|
||||
return None
|
||||
|
||||
def on_start_command(self):
|
||||
self.emit('start')
|
||||
def on_start_command(self) -> Optional[Message]:
|
||||
self.emit(self.EVENT_START)
|
||||
return None
|
||||
|
||||
def on_suspend_command(self):
|
||||
self.emit('suspend')
|
||||
def on_suspend_command(self) -> Optional[Message]:
|
||||
self.emit(self.EVENT_SUSPEND)
|
||||
return None
|
||||
|
||||
def on_close_command(self):
|
||||
self.emit('close')
|
||||
def on_close_command(self) -> Optional[Message]:
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
return None
|
||||
|
||||
def on_abort_command(self):
|
||||
self.emit('abort')
|
||||
def on_abort_command(self) -> Optional[Message]:
|
||||
self.emit(self.EVENT_ABORT)
|
||||
return None
|
||||
|
||||
def on_delayreport_command(self, delay: int):
|
||||
self.emit('delay_report', delay)
|
||||
def on_delayreport_command(self, delay: int) -> Optional[Message]:
|
||||
self.emit(self.EVENT_DELAY_REPORT, delay)
|
||||
return None
|
||||
|
||||
def on_rtp_channel_open(self):
|
||||
self.emit('rtp_channel_open')
|
||||
def on_security_control_command(self, data: bytes) -> Optional[Message]:
|
||||
self.emit(self.EVENT_SECURITY_CONTROL, data)
|
||||
return None
|
||||
|
||||
def on_rtp_channel_close(self):
|
||||
self.emit('rtp_channel_close')
|
||||
def on_rtp_channel_open(self) -> None:
|
||||
self.emit(self.EVENT_RTP_CHANNEL_OPEN)
|
||||
return None
|
||||
|
||||
def on_rtp_channel_close(self) -> None:
|
||||
self.emit(self.EVENT_RTP_CHANNEL_CLOSE)
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -2156,13 +2210,13 @@ class LocalSource(LocalStreamEndPoint):
|
||||
if self.packet_pump and self.stream and self.stream.rtp_channel:
|
||||
return await self.packet_pump.start(self.stream.rtp_channel)
|
||||
|
||||
self.emit('start')
|
||||
self.emit(self.EVENT_START)
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self.packet_pump:
|
||||
return await self.packet_pump.stop()
|
||||
|
||||
self.emit('stop')
|
||||
self.emit(self.EVENT_STOP)
|
||||
|
||||
def on_start_command(self):
|
||||
asyncio.create_task(self.start())
|
||||
@@ -2203,4 +2257,4 @@ class LocalSink(LocalStreamEndPoint):
|
||||
f'{color("<<< RTP Packet:", "green")} '
|
||||
f'{rtp_packet} {rtp_packet.payload[:16].hex()}'
|
||||
)
|
||||
self.emit('rtp_packet', rtp_packet)
|
||||
self.emit(self.EVENT_RTP_PACKET, rtp_packet)
|
||||
|
||||
@@ -996,6 +996,10 @@ class Delegate:
|
||||
class Protocol(utils.EventEmitter):
|
||||
"""AVRCP Controller and Target protocol."""
|
||||
|
||||
EVENT_CONNECTION = "connection"
|
||||
EVENT_START = "start"
|
||||
EVENT_STOP = "stop"
|
||||
|
||||
class PacketType(enum.IntEnum):
|
||||
SINGLE = 0b00
|
||||
START = 0b01
|
||||
@@ -1456,9 +1460,11 @@ class Protocol(utils.EventEmitter):
|
||||
|
||||
def _on_avctp_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||
logger.debug("AVCTP connection established")
|
||||
l2cap_channel.on("open", lambda: self._on_avctp_channel_open(l2cap_channel))
|
||||
l2cap_channel.on(
|
||||
l2cap_channel.EVENT_OPEN, lambda: self._on_avctp_channel_open(l2cap_channel)
|
||||
)
|
||||
|
||||
self.emit("connection")
|
||||
self.emit(self.EVENT_CONNECTION)
|
||||
|
||||
def _on_avctp_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||
logger.debug("AVCTP channel open")
|
||||
@@ -1473,15 +1479,15 @@ class Protocol(utils.EventEmitter):
|
||||
self.avctp_protocol.register_response_handler(
|
||||
AVRCP_PID, self._on_avctp_response
|
||||
)
|
||||
l2cap_channel.on("close", self._on_avctp_channel_close)
|
||||
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self._on_avctp_channel_close)
|
||||
|
||||
self.emit("start")
|
||||
self.emit(self.EVENT_START)
|
||||
|
||||
def _on_avctp_channel_close(self) -> None:
|
||||
logger.debug("AVCTP channel closed")
|
||||
self.avctp_protocol = None
|
||||
|
||||
self.emit("stop")
|
||||
self.emit(self.EVENT_STOP)
|
||||
|
||||
def _on_avctp_command(
|
||||
self, transaction_label: int, command: avc.CommandFrame
|
||||
|
||||
@@ -809,7 +809,7 @@ class Appearance:
|
||||
STICK_PC = 0x0F
|
||||
|
||||
class WatchSubcategory(utils.OpenIntEnum):
|
||||
GENENERIC_WATCH = 0x00
|
||||
GENERIC_WATCH = 0x00
|
||||
SPORTS_WATCH = 0x01
|
||||
SMARTWATCH = 0x02
|
||||
|
||||
@@ -1127,7 +1127,7 @@ class Appearance:
|
||||
TURNTABLE = 0x05
|
||||
CD_PLAYER = 0x06
|
||||
DVD_PLAYER = 0x07
|
||||
BLUERAY_PLAYER = 0x08
|
||||
BLURAY_PLAYER = 0x08
|
||||
OPTICAL_DISC_PLAYER = 0x09
|
||||
SET_TOP_BOX = 0x0A
|
||||
|
||||
@@ -1351,6 +1351,12 @@ class AdvertisingData:
|
||||
THREE_D_INFORMATION_DATA = 0x3D
|
||||
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
||||
|
||||
class Flags(enum.IntFlag):
|
||||
LE_LIMITED_DISCOVERABLE_MODE = 1 << 0
|
||||
LE_GENERAL_DISCOVERABLE_MODE = 1 << 1
|
||||
BR_EDR_NOT_SUPPORTED = 1 << 2
|
||||
SIMULTANEOUS_LE_BR_EDR_CAPABLE = 1 << 3
|
||||
|
||||
# For backward-compatibility
|
||||
FLAGS = Type.FLAGS
|
||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = Type.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS
|
||||
@@ -1407,11 +1413,11 @@ class AdvertisingData:
|
||||
THREE_D_INFORMATION_DATA = Type.THREE_D_INFORMATION_DATA
|
||||
MANUFACTURER_SPECIFIC_DATA = Type.MANUFACTURER_SPECIFIC_DATA
|
||||
|
||||
LE_LIMITED_DISCOVERABLE_MODE_FLAG = 0x01
|
||||
LE_GENERAL_DISCOVERABLE_MODE_FLAG = 0x02
|
||||
BR_EDR_NOT_SUPPORTED_FLAG = 0x04
|
||||
BR_EDR_CONTROLLER_FLAG = 0x08
|
||||
BR_EDR_HOST_FLAG = 0x10
|
||||
LE_LIMITED_DISCOVERABLE_MODE_FLAG = Flags.LE_LIMITED_DISCOVERABLE_MODE
|
||||
LE_GENERAL_DISCOVERABLE_MODE_FLAG = Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||
BR_EDR_NOT_SUPPORTED_FLAG = Flags.BR_EDR_NOT_SUPPORTED
|
||||
BR_EDR_CONTROLLER_FLAG = Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||
BR_EDR_HOST_FLAG = 0x10 # Deprecated
|
||||
|
||||
ad_structures: list[tuple[int, bytes]]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
@@ -12,12 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Crypto support
|
||||
#
|
||||
# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -25,19 +19,15 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import secrets
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
generate_private_key,
|
||||
ECDH,
|
||||
EllipticCurvePrivateKey,
|
||||
EllipticCurvePublicNumbers,
|
||||
EllipticCurvePrivateNumbers,
|
||||
SECP256R1,
|
||||
)
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
from typing import Tuple
|
||||
|
||||
try:
|
||||
from bumble.crypto.cryptography import EccKey, e, aes_cmac
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).debug(
|
||||
"Unable to import cryptography, use built-in primitives."
|
||||
)
|
||||
from bumble.crypto.builtin import EccKey, e, aes_cmac # type: ignore[assignment]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -46,55 +36,6 @@ from typing import Tuple
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
class EccKey:
|
||||
def __init__(self, private_key: EllipticCurvePrivateKey) -> None:
|
||||
self.private_key = private_key
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> EccKey:
|
||||
private_key = generate_private_key(SECP256R1())
|
||||
return cls(private_key)
|
||||
|
||||
@classmethod
|
||||
def from_private_key_bytes(
|
||||
cls, d_bytes: bytes, x_bytes: bytes, y_bytes: bytes
|
||||
) -> EccKey:
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
x = int.from_bytes(x_bytes, byteorder='big', signed=False)
|
||||
y = int.from_bytes(y_bytes, byteorder='big', signed=False)
|
||||
private_key = EllipticCurvePrivateNumbers(
|
||||
d, EllipticCurvePublicNumbers(x, y, SECP256R1())
|
||||
).private_key()
|
||||
return cls(private_key)
|
||||
|
||||
@property
|
||||
def x(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.x.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
@property
|
||||
def y(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.y.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
|
||||
public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key()
|
||||
shared_key = self.private_key.exchange(ECDH(), public_key)
|
||||
|
||||
return shared_key
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -132,19 +73,6 @@ def r() -> bytes:
|
||||
return secrets.token_bytes(16)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
'''
|
||||
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
|
||||
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
|
||||
'''
|
||||
|
||||
cipher = Cipher(algorithms.AES(reverse(key)), modes.ECB())
|
||||
encryptor = cipher.encryptor()
|
||||
return reverse(encryptor.update(reverse(data)))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
@@ -187,18 +115,6 @@ def s1(k: bytes, r1: bytes, r2: bytes) -> bytes:
|
||||
return e(k, r2[0:8] + r1[0:8])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def aes_cmac(m: bytes, k: bytes) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
|
||||
|
||||
NOTE: the input and output of this internal function are in big-endian byte order
|
||||
'''
|
||||
mac = cmac.CMAC(algorithms.AES(k))
|
||||
mac.update(m)
|
||||
return mac.finalize()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
|
||||
'''
|
||||
@@ -209,7 +125,7 @@ def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> Tuple[bytes, bytes]:
|
||||
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> tuple[bytes, bytes]:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
|
||||
Function f5
|
||||
652
bumble/crypto/builtin.py
Normal file
652
bumble/crypto/builtin.py
Normal file
@@ -0,0 +1,652 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# The implementation is modified from:
|
||||
# * AES - https://github.com/ricmoo/pyaes by Richard Moore under MIT License
|
||||
# * CMAC - https://github.com/pycrypto/pycrypto by contributors under pycrypto License.
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Built-in implementation of cryptography primitives.
|
||||
#
|
||||
# Note: It's very dangerous to use this library in the real world.
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import copy
|
||||
import secrets
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from bumble import core
|
||||
|
||||
|
||||
def _compact_word(word: bytes) -> int:
|
||||
return int.from_bytes(word, "big")
|
||||
|
||||
|
||||
def _shift_bytes(bs: bytes, xor_lsb: int = 0) -> bytes:
|
||||
return ((int.from_bytes(bs, "big") << 1) ^ xor_lsb).to_bytes(len(bs) + 1, "big")[1:]
|
||||
|
||||
|
||||
def _xor(a: bytes, b: bytes) -> bytes:
|
||||
return bytes(x ^ y for x, y in zip(a, b))
|
||||
|
||||
|
||||
# Based *largely* on the Rijndael implementation
|
||||
# See: http://csrc.nist.gov/publications/FIPS/FIPS197/FIPS-197.pdf
|
||||
class _AES:
|
||||
'''Encapsulates the AES block cipher.
|
||||
|
||||
You generally should not need this. Use the AESModeOfOperation classes
|
||||
below instead.'''
|
||||
|
||||
# fmt: off
|
||||
# Number of rounds by key size
|
||||
_NUMBER_OF_ROUNDS = {16: 10, 24: 12, 32: 14}
|
||||
|
||||
# Round constant words
|
||||
_RCON = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
|
||||
|
||||
# S-box and Inverse S-box (S is for Substitution)
|
||||
_S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
|
||||
_S_INV =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ]
|
||||
|
||||
# Transformations for encryption
|
||||
_T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
|
||||
_T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
|
||||
_T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
|
||||
_T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
|
||||
|
||||
# Transformations for decryption
|
||||
_T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
|
||||
_T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
|
||||
_T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
|
||||
_T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
|
||||
|
||||
# Transformations for decryption key expansion
|
||||
_U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
|
||||
_U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
|
||||
_U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
|
||||
_U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, key: bytes) -> None:
|
||||
|
||||
if len(key) not in (16, 24, 32):
|
||||
raise core.InvalidArgumentError(f'Invalid key size {len(key)}')
|
||||
|
||||
rounds = self._NUMBER_OF_ROUNDS[len(key)]
|
||||
|
||||
# Encryption round keys
|
||||
self._ke = [[0] * 4 for i in range(rounds + 1)]
|
||||
|
||||
# Decryption round keys
|
||||
self._kd = [[0] * 4 for i in range(rounds + 1)]
|
||||
|
||||
round_key_count = (rounds + 1) * 4
|
||||
kc = len(key) // 4
|
||||
|
||||
# Convert the key into ints
|
||||
tk = [struct.unpack('>i', key[i : i + 4])[0] for i in range(0, len(key), 4)]
|
||||
|
||||
# Copy values into round key arrays
|
||||
for i in range(0, kc):
|
||||
self._ke[i // 4][i % 4] = tk[i]
|
||||
self._kd[rounds - (i // 4)][i % 4] = tk[i]
|
||||
|
||||
# Key expansion (FIPS-197 section 5.2)
|
||||
r_con_pointer = 0
|
||||
t = kc
|
||||
while t < round_key_count:
|
||||
|
||||
tt = tk[kc - 1]
|
||||
tk[0] ^= (
|
||||
(self._S[(tt >> 16) & 0xFF] << 24)
|
||||
^ (self._S[(tt >> 8) & 0xFF] << 16)
|
||||
^ (self._S[tt & 0xFF] << 8)
|
||||
^ self._S[(tt >> 24) & 0xFF]
|
||||
^ (self._RCON[r_con_pointer] << 24)
|
||||
)
|
||||
r_con_pointer += 1
|
||||
|
||||
if kc != 8:
|
||||
for i in range(1, kc):
|
||||
tk[i] ^= tk[i - 1]
|
||||
|
||||
# Key expansion for 256-bit keys is "slightly different" (FIPS-197)
|
||||
else:
|
||||
for i in range(1, kc // 2):
|
||||
tk[i] ^= tk[i - 1]
|
||||
tt = tk[kc // 2 - 1]
|
||||
|
||||
tk[kc // 2] ^= (
|
||||
self._S[tt & 0xFF]
|
||||
^ (self._S[(tt >> 8) & 0xFF] << 8)
|
||||
^ (self._S[(tt >> 16) & 0xFF] << 16)
|
||||
^ (self._S[(tt >> 24) & 0xFF] << 24)
|
||||
)
|
||||
|
||||
for i in range(kc // 2 + 1, kc):
|
||||
tk[i] ^= tk[i - 1]
|
||||
|
||||
# Copy values into round key arrays
|
||||
j = 0
|
||||
while j < kc and t < round_key_count:
|
||||
self._ke[t // 4][t % 4] = tk[j]
|
||||
self._kd[rounds - (t // 4)][t % 4] = tk[j]
|
||||
j += 1
|
||||
t += 1
|
||||
|
||||
# Inverse-Cipher-ify the decryption round key (FIPS-197 section 5.3)
|
||||
for r in range(1, rounds):
|
||||
for j in range(0, 4):
|
||||
tt = self._kd[r][j]
|
||||
self._kd[r][j] = (
|
||||
self._U1[(tt >> 24) & 0xFF]
|
||||
^ self._U2[(tt >> 16) & 0xFF]
|
||||
^ self._U3[(tt >> 8) & 0xFF]
|
||||
^ self._U4[tt & 0xFF]
|
||||
)
|
||||
|
||||
def encrypt(self, plaintext: bytes) -> bytes:
|
||||
"""Encrypt a block of plain text using the AES block cipher."""
|
||||
|
||||
if len(plaintext) != 16:
|
||||
raise core.InvalidArgumentError(f'wrong block length {len(plaintext)}')
|
||||
|
||||
rounds = len(self._ke) - 1
|
||||
(s1, s2, s3) = [1, 2, 3]
|
||||
a = [0, 0, 0, 0]
|
||||
|
||||
# Convert plaintext to (ints ^ key)
|
||||
t = [
|
||||
(_compact_word(plaintext[4 * i : 4 * i + 4]) ^ self._ke[0][i])
|
||||
for i in range(0, 4)
|
||||
]
|
||||
|
||||
# Apply round transforms
|
||||
for r in range(1, rounds):
|
||||
for i in range(0, 4):
|
||||
a[i] = (
|
||||
self._T1[(t[i] >> 24) & 0xFF]
|
||||
^ self._T2[(t[(i + s1) % 4] >> 16) & 0xFF]
|
||||
^ self._T3[(t[(i + s2) % 4] >> 8) & 0xFF]
|
||||
^ self._T4[t[(i + s3) % 4] & 0xFF]
|
||||
^ self._ke[r][i]
|
||||
)
|
||||
t = copy.copy(a)
|
||||
|
||||
# The last round is special
|
||||
result = []
|
||||
for i in range(0, 4):
|
||||
tt = self._ke[rounds][i]
|
||||
result.append((self._S[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
|
||||
result.append((self._S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
|
||||
result.append((self._S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
|
||||
result.append((self._S[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
|
||||
|
||||
return bytes(result)
|
||||
|
||||
def decrypt(self, cipher_text: bytes) -> bytes:
|
||||
"""Decrypt a block of cipher text using the AES block cipher."""
|
||||
|
||||
if len(cipher_text) != 16:
|
||||
raise core.InvalidArgumentError(f'wrong block length {len(cipher_text)}')
|
||||
|
||||
rounds = len(self._kd) - 1
|
||||
(s1, s2, s3) = [3, 2, 1]
|
||||
a = [0, 0, 0, 0]
|
||||
|
||||
# Convert ciphertext to (ints ^ key)
|
||||
t = [
|
||||
(_compact_word(cipher_text[4 * i : 4 * i + 4]) ^ self._kd[0][i])
|
||||
for i in range(0, 4)
|
||||
]
|
||||
|
||||
# Apply round transforms
|
||||
for r in range(1, rounds):
|
||||
for i in range(0, 4):
|
||||
a[i] = (
|
||||
self._T5[(t[i] >> 24) & 0xFF]
|
||||
^ self._T6[(t[(i + s1) % 4] >> 16) & 0xFF]
|
||||
^ self._T7[(t[(i + s2) % 4] >> 8) & 0xFF]
|
||||
^ self._T8[t[(i + s3) % 4] & 0xFF]
|
||||
^ self._kd[r][i]
|
||||
)
|
||||
t = copy.copy(a)
|
||||
|
||||
# The last round is special
|
||||
result = []
|
||||
for i in range(0, 4):
|
||||
tt = self._kd[rounds][i]
|
||||
result.append((self._S_INV[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
|
||||
result.append(
|
||||
(self._S_INV[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF
|
||||
)
|
||||
result.append(
|
||||
(self._S_INV[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF
|
||||
)
|
||||
result.append((self._S_INV[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
|
||||
|
||||
return bytes(result)
|
||||
|
||||
|
||||
class _ECB:
|
||||
def __init__(self, key: bytes):
|
||||
self._aes = _AES(key)
|
||||
|
||||
def encrypt(self, plaintext: bytes) -> bytes:
|
||||
return b"".join(
|
||||
[
|
||||
self._aes.encrypt(
|
||||
plaintext[offset : offset + 16].ljust(16, b"\x00") # Pad 0.
|
||||
)
|
||||
for offset in range(0, len(plaintext), 16)
|
||||
]
|
||||
)
|
||||
|
||||
def decrypt(self, cipher_text: bytes) -> bytes:
|
||||
return b"".join(
|
||||
[
|
||||
self._aes.encrypt(cipher_text[offset : offset + 16])
|
||||
for offset in range(0, len(cipher_text), 16)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class _CBC:
|
||||
|
||||
def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None:
|
||||
if len(iv) != 16:
|
||||
raise core.InvalidArgumentError(
|
||||
f'initialization vector must be 16 bytes, get {len(iv)}'
|
||||
)
|
||||
else:
|
||||
self._last_cipher_block = iv
|
||||
self._aes = _AES(key)
|
||||
|
||||
def encrypt(self, plaintext: bytes) -> bytes:
|
||||
cipher_text = b""
|
||||
for offset in range(0, len(plaintext), 16):
|
||||
pre_cipher_block = _xor(
|
||||
plaintext[offset : offset + 16], self._last_cipher_block
|
||||
)
|
||||
self._last_cipher_block = self._aes.encrypt(pre_cipher_block)
|
||||
cipher_text += self._last_cipher_block
|
||||
return cipher_text
|
||||
|
||||
def decrypt(self, cipher_text: bytes) -> bytes:
|
||||
plaintext = b""
|
||||
for offset in range(0, len(cipher_text), 16):
|
||||
plaintext += _xor(
|
||||
self._aes.decrypt(cipher_text[offset : offset + 16]),
|
||||
self._last_cipher_block,
|
||||
)
|
||||
self._last_cipher_block = cipher_text[offset : offset + 16]
|
||||
|
||||
return plaintext
|
||||
|
||||
|
||||
class _CMAC:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: bytes,
|
||||
msg: bytes = bytes(16),
|
||||
mac_len: int = 16,
|
||||
update_after_digest: bool = False,
|
||||
) -> None:
|
||||
self.digest_size = mac_len
|
||||
self._key = key
|
||||
self._block_size = bs = 16
|
||||
self._mac_tag: Optional[bytes] = None
|
||||
self._update_after_digest = update_after_digest
|
||||
|
||||
# Section 5.3 of NIST SP 800 38B and Appendix B
|
||||
if bs == 8:
|
||||
const_Rb = 0x1B
|
||||
self._max_size = 8 * (2**21)
|
||||
elif bs == 16:
|
||||
const_Rb = 0x87
|
||||
self._max_size = 16 * (2**48)
|
||||
else:
|
||||
raise core.InvalidArgumentError(
|
||||
f"CMAC requires a cipher with a block size of 8 or 16 bytes, not {bs}"
|
||||
)
|
||||
|
||||
# Compute sub-keys
|
||||
zero_block = bytes(bs)
|
||||
self._ecb = _ECB(key)
|
||||
L = self._ecb.encrypt(zero_block)
|
||||
if L[0] & 0x80:
|
||||
self._k1 = _shift_bytes(L, const_Rb)
|
||||
else:
|
||||
self._k1 = _shift_bytes(L)
|
||||
if self._k1[0] & 0x80:
|
||||
self._k2 = _shift_bytes(self._k1, const_Rb)
|
||||
else:
|
||||
self._k2 = _shift_bytes(self._k1)
|
||||
|
||||
# Initialize CBC cipher with zero IV
|
||||
self._cbc = _CBC(key, zero_block)
|
||||
|
||||
# Cache for outstanding data to authenticate
|
||||
self._cache = bytearray(bs)
|
||||
self._cache_n = 0
|
||||
|
||||
# Last piece of cipher text produced
|
||||
self._last_ct = zero_block
|
||||
|
||||
# Last block that was encrypted with AES
|
||||
self._last_pt: Optional[bytes] = None
|
||||
|
||||
# Counter for total message size
|
||||
self._data_size = 0
|
||||
|
||||
if msg:
|
||||
self.update(msg)
|
||||
|
||||
def update(self, msg: bytes) -> _CMAC:
|
||||
"""Authenticate the next chunk of message.
|
||||
|
||||
Args:
|
||||
data (byte string/byte array/memoryview): The next chunk of data
|
||||
"""
|
||||
|
||||
if self._mac_tag is not None and not self._update_after_digest:
|
||||
raise core.InvalidStateError(
|
||||
"update() cannot be called after digest() or verify()"
|
||||
)
|
||||
|
||||
self._data_size += len(msg)
|
||||
bs = self._block_size
|
||||
|
||||
if self._cache_n > 0:
|
||||
filler = min(bs - self._cache_n, len(msg))
|
||||
self._cache[self._cache_n : self._cache_n + filler] = msg[:filler]
|
||||
self._cache_n += filler
|
||||
|
||||
if self._cache_n < bs:
|
||||
return self
|
||||
|
||||
msg = msg[filler:]
|
||||
self._update(self._cache)
|
||||
self._cache_n = 0
|
||||
|
||||
remain = len(msg) % bs
|
||||
if remain > 0:
|
||||
self._update(msg[:-remain])
|
||||
self._cache[:remain] = msg[-remain:]
|
||||
else:
|
||||
self._update(msg)
|
||||
self._cache_n = remain
|
||||
return self
|
||||
|
||||
def _update(self, data_block: bytes) -> None:
|
||||
"""Update a block aligned to the block boundary"""
|
||||
|
||||
bs = self._block_size
|
||||
assert len(data_block) % bs == 0
|
||||
|
||||
if len(data_block) == 0:
|
||||
return
|
||||
|
||||
ct = self._cbc.encrypt(data_block)
|
||||
if len(data_block) == bs:
|
||||
second_last = self._last_ct
|
||||
else:
|
||||
second_last = ct[-bs * 2 : -bs]
|
||||
self._last_ct = ct[-bs:]
|
||||
self._last_pt = _xor(second_last, data_block[-bs:])
|
||||
|
||||
def digest(self) -> bytes:
|
||||
|
||||
bs = self._block_size
|
||||
|
||||
if self._mac_tag is not None and not self._update_after_digest:
|
||||
return self._mac_tag
|
||||
|
||||
if self._data_size > self._max_size:
|
||||
raise core.InvalidArgumentError("MAC is unsafe for this message")
|
||||
|
||||
if self._cache_n == 0 and self._data_size > 0 and self._last_pt:
|
||||
# Last block was full
|
||||
pt = _xor(self._last_pt, self._k1)
|
||||
else:
|
||||
# Last block is partial (or message length is zero)
|
||||
partial = self._cache[:]
|
||||
partial[self._cache_n :] = b'\x80' + b'\x00' * (bs - self._cache_n - 1)
|
||||
pt = _xor(_xor(self._last_ct, partial), self._k2)
|
||||
|
||||
self._mac_tag = self._ecb.encrypt(pt)[: self.digest_size]
|
||||
|
||||
return self._mac_tag
|
||||
|
||||
|
||||
# Define the original Point class for clarity and conversion purposes
|
||||
@dataclasses.dataclass
|
||||
class _Point:
|
||||
"""Represents a point on the elliptic curve in affine coordinates."""
|
||||
|
||||
curve: _EllipticCurve
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
infinite: bool = False
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _JacobianPoint:
|
||||
"""Represents a point on the elliptic curve in Jacobian coordinates."""
|
||||
|
||||
curve: _EllipticCurve
|
||||
x: int = 1 # For point at infinity (1:1:0)
|
||||
y: int = 1
|
||||
z: int = 0 # z = 0 indicates point at infinity
|
||||
|
||||
@classmethod
|
||||
def point_at_infinity(cls, curve: _EllipticCurve) -> _JacobianPoint:
|
||||
return _JacobianPoint(curve=curve, x=1, y=1, z=0)
|
||||
|
||||
@classmethod
|
||||
def from_affine(cls, affine_point: _Point) -> _JacobianPoint:
|
||||
if affine_point.infinite:
|
||||
return _JacobianPoint.point_at_infinity(affine_point.curve)
|
||||
# A simple conversion is (x, y, 1)
|
||||
return _JacobianPoint(
|
||||
curve=affine_point.curve, x=affine_point.x, y=affine_point.y, z=1
|
||||
)
|
||||
|
||||
def to_affine(self) -> _Point:
|
||||
if self.z == 0:
|
||||
return _Point(infinite=True, curve=self.curve)
|
||||
|
||||
p = self.curve.p
|
||||
inv_z = pow(self.z, -1, p)
|
||||
affine_x = (self.x * inv_z**2) % p
|
||||
affine_y = (self.y * inv_z**3) % p
|
||||
|
||||
return _Point(curve=self.curve, x=affine_x, y=affine_y, infinite=False)
|
||||
|
||||
def double(self) -> _JacobianPoint:
|
||||
if self.z == 0 or self.y == 0:
|
||||
return _JacobianPoint.point_at_infinity(self.curve)
|
||||
|
||||
s = 4 * self.x * self.y**2
|
||||
m = 3 * self.x**2 + self.curve.a * self.z**4
|
||||
x2 = m**2 - 2 * s
|
||||
y2 = m * (s - x2) - 8 * self.y**4
|
||||
z2 = 2 * self.y * self.z
|
||||
p = self.curve.p
|
||||
|
||||
return _JacobianPoint(curve=self.curve, x=x2 % p, y=y2 % p, z=z2 % p)
|
||||
|
||||
def __add__(self, other: _JacobianPoint) -> _JacobianPoint:
|
||||
if self.z == 0 and other.z == 0:
|
||||
return _JacobianPoint.point_at_infinity(self.curve)
|
||||
elif self.z == 0:
|
||||
return other
|
||||
elif other.z == 0:
|
||||
return self
|
||||
|
||||
x1 = self.x
|
||||
y1 = self.y
|
||||
z1 = self.z
|
||||
x2 = other.x
|
||||
y2 = other.y
|
||||
z2 = other.z
|
||||
p = self.curve.p
|
||||
u1 = (x1 * z2**2) % p
|
||||
u2 = (x2 * z1**2) % p
|
||||
s1 = (y1 * z2**3) % p
|
||||
s2 = (y2 * z1**3) % p
|
||||
|
||||
if u1 == u2:
|
||||
if s1 != s2:
|
||||
return _JacobianPoint.point_at_infinity(self.curve)
|
||||
else:
|
||||
return self.double()
|
||||
else:
|
||||
h = u2 - u1
|
||||
r = s2 - s1
|
||||
|
||||
h3 = h**3 % p
|
||||
u1h2 = (u1 * h**2) % p
|
||||
x3 = r**2 - h3 - 2 * u1h2
|
||||
y3 = r * (u1h2 - x3) - s1 * h3
|
||||
z3 = h * z1 * z2
|
||||
|
||||
return _JacobianPoint(self.curve, x3 % p, y3 % p, z3 % p)
|
||||
|
||||
def __mul__(self, k: int) -> _JacobianPoint:
|
||||
addend = self
|
||||
result = _JacobianPoint.point_at_infinity(self.curve)
|
||||
|
||||
while k > 0:
|
||||
if k % 2 != 0:
|
||||
result = result + addend
|
||||
addend = addend.double()
|
||||
k = k >> 1
|
||||
return result
|
||||
|
||||
def __rmul__(self, k: int) -> _JacobianPoint:
|
||||
return self * k
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _EllipticCurve:
|
||||
p: int
|
||||
a: int
|
||||
b: int
|
||||
n: int
|
||||
g_x: int
|
||||
g_y: int
|
||||
|
||||
_generator_jacobian: _JacobianPoint = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self._generator_jacobian = _JacobianPoint(
|
||||
curve=self, x=self.g_x, y=self.g_y, z=1
|
||||
)
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PrivateKey:
|
||||
key: int
|
||||
curve: _EllipticCurve
|
||||
|
||||
def generate_private_key(self) -> PrivateKey:
|
||||
"""Generates a random private key."""
|
||||
return self.PrivateKey(key=secrets.randbelow(self.n), curve=self)
|
||||
|
||||
def generate_public_key(self, private_key: int) -> _Point:
|
||||
"""Generates a public key from a private key using Jacobian coordinates for scalar multiplication."""
|
||||
public_key_jacobian = self._generator_jacobian * private_key
|
||||
return public_key_jacobian.to_affine()
|
||||
|
||||
def ecdh_shared_secret(self, private_key: int, other_public_key: _Point) -> bytes:
|
||||
"""Computes the shared secret using ECDH."""
|
||||
other_public_key_jacobian = _JacobianPoint.from_affine(other_public_key)
|
||||
shared_point_jacobian = other_public_key_jacobian * private_key
|
||||
shared_point_affine = shared_point_jacobian.to_affine()
|
||||
if shared_point_affine.infinite:
|
||||
raise core.InvalidPacketError(
|
||||
"Shared secret calculation resulted in the point at infinite"
|
||||
)
|
||||
return shared_point_affine.x.to_bytes(32, 'big')
|
||||
|
||||
@classmethod
|
||||
def SECP256R1(cls) -> _EllipticCurve:
|
||||
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
|
||||
a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
|
||||
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
|
||||
n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 # Curve order
|
||||
g_x = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
|
||||
g_y = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
|
||||
|
||||
return _EllipticCurve(p=p, a=a, b=b, n=n, g_x=g_x, g_y=g_y)
|
||||
|
||||
|
||||
class EccKey:
|
||||
def __init__(self, private_key: _EllipticCurve.PrivateKey) -> None:
|
||||
self.private_key = private_key
|
||||
|
||||
@functools.cached_property
|
||||
def x(self) -> bytes:
|
||||
return self.private_key.curve.generate_public_key(
|
||||
self.private_key.key
|
||||
).x.to_bytes(32, byteorder='big')
|
||||
|
||||
@functools.cached_property
|
||||
def y(self) -> bytes:
|
||||
return self.private_key.curve.generate_public_key(
|
||||
self.private_key.key
|
||||
).y.to_bytes(32, byteorder='big')
|
||||
|
||||
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
|
||||
return self.private_key.curve.ecdh_shared_secret(
|
||||
self.private_key.key,
|
||||
_Point(x=x, y=y, curve=self.private_key.curve),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> EccKey:
|
||||
return EccKey(_EllipticCurve.SECP256R1().generate_private_key())
|
||||
|
||||
@classmethod
|
||||
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
return EccKey(_EllipticCurve.PrivateKey(d, _EllipticCurve.SECP256R1()))
|
||||
|
||||
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
'''
|
||||
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
|
||||
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
|
||||
'''
|
||||
|
||||
return _ECB(key[::-1]).encrypt(data[::-1])[::-1]
|
||||
|
||||
|
||||
def aes_cmac(m: bytes, k: bytes) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
|
||||
|
||||
NOTE: the input and output of this internal function are in big-endian byte order
|
||||
'''
|
||||
return _CMAC(key=k, msg=m).digest()
|
||||
84
bumble/crypto/cryptography.py
Normal file
84
bumble/crypto/cryptography.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
|
||||
from cryptography.hazmat.primitives import ciphers
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
|
||||
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
'''
|
||||
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
|
||||
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
|
||||
'''
|
||||
|
||||
cipher = ciphers.Cipher(algorithms.AES(key[::-1]), modes.ECB())
|
||||
encryptor = cipher.encryptor()
|
||||
return encryptor.update(data[::-1])[::-1]
|
||||
|
||||
|
||||
class EccKey:
|
||||
def __init__(self, private_key: ec.EllipticCurvePrivateKey) -> None:
|
||||
self.private_key = private_key
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> EccKey:
|
||||
return EccKey(ec.generate_private_key(ec.SECP256R1()))
|
||||
|
||||
@classmethod
|
||||
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
return EccKey(ec.derive_private_key(d, ec.SECP256R1()))
|
||||
|
||||
@functools.cached_property
|
||||
def x(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.x.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def y(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.y.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
|
||||
return self.private_key.exchange(
|
||||
ec.ECDH(),
|
||||
ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key(),
|
||||
)
|
||||
|
||||
|
||||
def aes_cmac(m: bytes, k: bytes) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
|
||||
|
||||
NOTE: the input and output of this internal function are in big-endian byte order
|
||||
'''
|
||||
mac = cmac.CMAC(algorithms.AES(k))
|
||||
mac.update(m)
|
||||
return mac.finalize()
|
||||
551
bumble/device.py
551
bumble/device.py
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
INTEL_USB_PRODUCTS = {
|
||||
(0x8087, 0x0032), # AX210
|
||||
(0x8087, 0x0033), # AX211
|
||||
(0x8087, 0x0036), # BE200
|
||||
}
|
||||
|
||||
@@ -293,6 +294,7 @@ class HardwareVariant(utils.OpenIntEnum):
|
||||
# This is a just a partial list.
|
||||
# Add other constants here as new hardware is encountered and tested.
|
||||
TYPHOON_PEAK = 0x17
|
||||
GARFIELD_PEAK = 0x19
|
||||
GALE_PEAK = 0x1C
|
||||
|
||||
|
||||
@@ -471,6 +473,7 @@ class Driver(common.Driver):
|
||||
raise DriverError("hardware platform not supported")
|
||||
if hardware_info.variant not in (
|
||||
HardwareVariant.TYPHOON_PEAK,
|
||||
HardwareVariant.GARFIELD_PEAK,
|
||||
HardwareVariant.GALE_PEAK,
|
||||
):
|
||||
raise DriverError("hardware variant not supported")
|
||||
|
||||
@@ -448,6 +448,8 @@ class Characteristic(Attribute[_T]):
|
||||
uuid: UUID
|
||||
properties: Characteristic.Properties
|
||||
|
||||
EVENT_SUBSCRIPTION = "subscription"
|
||||
|
||||
class Properties(enum.IntFlag):
|
||||
"""Property flags"""
|
||||
|
||||
@@ -577,11 +579,7 @@ class Descriptor(Attribute):
|
||||
if isinstance(self.value, bytes):
|
||||
value_str = self.value.hex()
|
||||
elif isinstance(self.value, CharacteristicValue):
|
||||
value = self.value.read(None)
|
||||
if isinstance(value, bytes):
|
||||
value_str = value.hex()
|
||||
else:
|
||||
value_str = '<async>'
|
||||
value_str = '<dynamic>'
|
||||
else:
|
||||
value_str = '<...>'
|
||||
return (
|
||||
|
||||
@@ -202,6 +202,8 @@ class CharacteristicProxy(AttributeProxy[_T]):
|
||||
descriptors: List[DescriptorProxy]
|
||||
subscribers: Dict[Any, Callable[[_T], Any]]
|
||||
|
||||
EVENT_UPDATE = "update"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Client,
|
||||
@@ -308,7 +310,7 @@ class Client:
|
||||
self.services = []
|
||||
self.cached_values = {}
|
||||
|
||||
connection.on('disconnection', self.on_disconnection)
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
|
||||
def send_gatt_pdu(self, pdu: bytes) -> None:
|
||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||
@@ -1142,7 +1144,7 @@ class Client:
|
||||
if callable(subscriber):
|
||||
subscriber(notification.attribute_value)
|
||||
else:
|
||||
subscriber.emit('update', notification.attribute_value)
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value)
|
||||
|
||||
def on_att_handle_value_indication(self, indication):
|
||||
# Call all subscribers
|
||||
@@ -1157,7 +1159,7 @@ class Client:
|
||||
if callable(subscriber):
|
||||
subscriber(indication.attribute_value)
|
||||
else:
|
||||
subscriber.emit('update', indication.attribute_value)
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value)
|
||||
|
||||
# Confirm that we received the indication
|
||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||
|
||||
@@ -110,6 +110,8 @@ class Server(utils.EventEmitter):
|
||||
indication_semaphores: defaultdict[int, asyncio.Semaphore]
|
||||
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
|
||||
|
||||
EVENT_CHARACTERISTIC_SUBSCRIPTION = "characteristic_subscription"
|
||||
|
||||
def __init__(self, device: Device) -> None:
|
||||
super().__init__()
|
||||
self.device = device
|
||||
@@ -313,11 +315,8 @@ class Server(utils.EventEmitter):
|
||||
self.add_service(service)
|
||||
|
||||
def read_cccd(
|
||||
self, connection: Optional[Connection], characteristic: Characteristic
|
||||
self, connection: Connection, characteristic: Characteristic
|
||||
) -> bytes:
|
||||
if connection is None:
|
||||
return bytes([0, 0])
|
||||
|
||||
subscribers = self.subscribers.get(connection.handle)
|
||||
cccd = None
|
||||
if subscribers:
|
||||
@@ -347,10 +346,13 @@ class Server(utils.EventEmitter):
|
||||
notify_enabled = value[0] & 0x01 != 0
|
||||
indicate_enabled = value[0] & 0x02 != 0
|
||||
characteristic.emit(
|
||||
'subscription', connection, notify_enabled, indicate_enabled
|
||||
characteristic.EVENT_SUBSCRIPTION,
|
||||
connection,
|
||||
notify_enabled,
|
||||
indicate_enabled,
|
||||
)
|
||||
self.emit(
|
||||
'characteristic_subscription',
|
||||
self.EVENT_CHARACTERISTIC_SUBSCRIPTION,
|
||||
connection,
|
||||
characteristic,
|
||||
notify_enabled,
|
||||
|
||||
@@ -29,13 +29,12 @@ from typing_extensions import Self
|
||||
from bumble import crypto
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
AdvertisingData,
|
||||
DeviceClass,
|
||||
InvalidArgumentError,
|
||||
InvalidPacketError,
|
||||
ProtocolError,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
bit_flags_to_strings,
|
||||
name_or_number,
|
||||
padded_bytes,
|
||||
@@ -225,6 +224,7 @@ HCI_CONNECTIONLESS_PERIPHERAL_BROADCAST_CHANNEL_MAP_CHANGE_EVENT = 0X55
|
||||
HCI_INQUIRY_RESPONSE_NOTIFICATION_EVENT = 0X56
|
||||
HCI_AUTHENTICATED_PAYLOAD_TIMEOUT_EXPIRED_EVENT = 0X57
|
||||
HCI_SAM_STATUS_CHANGE_EVENT = 0X58
|
||||
HCI_ENCRYPTION_CHANGE_V2_EVENT = 0x59
|
||||
|
||||
HCI_VENDOR_EVENT = 0xFF
|
||||
|
||||
@@ -3364,6 +3364,20 @@ class HCI_Set_Event_Mask_Page_2_Command(HCI_Command):
|
||||
See Bluetooth spec @ 7.3.69 Set Event Mask Page 2 Command
|
||||
'''
|
||||
|
||||
@staticmethod
|
||||
def mask(event_codes: Iterable[int]) -> bytes:
|
||||
'''
|
||||
Compute the event mask value for a list of events.
|
||||
'''
|
||||
# NOTE: this implementation takes advantage of the fact that as of version 6.0
|
||||
# of the core specification, the bit number for each event code is equal to 64
|
||||
# less than the event code.
|
||||
# If future versions of the specification deviate from that, a different
|
||||
# implementation would be needed.
|
||||
return sum((1 << event_code - 64) for event_code in event_codes).to_bytes(
|
||||
8, 'little'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
@@ -5971,6 +5985,33 @@ class HCI_LE_Enhanced_Connection_Complete_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
(
|
||||
'role',
|
||||
{'size': 1, 'mapper': lambda x: 'CENTRAL' if x == 0 else 'PERIPHERAL'},
|
||||
),
|
||||
('peer_address_type', Address.ADDRESS_TYPE_SPEC),
|
||||
('peer_address', Address.parse_address_preceded_by_type),
|
||||
('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),
|
||||
('central_clock_accuracy', 1),
|
||||
('advertising_handle', 1),
|
||||
('sync_handle', 2),
|
||||
]
|
||||
)
|
||||
class HCI_LE_Enhanced_Connection_Complete_V2_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.10 LE Enhanced Connection Complete Event
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event(
|
||||
[
|
||||
@@ -6950,6 +6991,30 @@ class HCI_Encryption_Change_Event(HCI_Event):
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
(
|
||||
'encryption_enabled',
|
||||
{
|
||||
'size': 1,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name(
|
||||
x
|
||||
),
|
||||
},
|
||||
),
|
||||
('encryption_key_size', 1),
|
||||
]
|
||||
)
|
||||
class HCI_Encryption_Change_V2_Event(HCI_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.8 Encryption Change Event
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event(
|
||||
[('status', STATUS_SPEC), ('connection_handle', 2), ('lmp_features', 8)]
|
||||
|
||||
@@ -720,6 +720,14 @@ class HfProtocol(utils.EventEmitter):
|
||||
vrec: VoiceRecognitionState
|
||||
"""
|
||||
|
||||
EVENT_CODEC_NEGOTIATION = "codec_negotiation"
|
||||
EVENT_AG_INDICATOR = "ag_indicator"
|
||||
EVENT_SPEAKER_VOLUME = "speaker_volume"
|
||||
EVENT_MICROPHONE_VOLUME = "microphone_volume"
|
||||
EVENT_RING = "ring"
|
||||
EVENT_CLI_NOTIFICATION = "cli_notification"
|
||||
EVENT_VOICE_RECOGNITION = "voice_recognition"
|
||||
|
||||
class HfLoopTermination(HfpProtocolError):
|
||||
"""Termination signal for run() loop."""
|
||||
|
||||
@@ -777,7 +785,8 @@ class HfProtocol(utils.EventEmitter):
|
||||
self.dlc.sink = self._read_at
|
||||
# Stop the run() loop when L2CAP is closed.
|
||||
self.dlc.multiplexer.l2cap_channel.on(
|
||||
'close', lambda: self.unsolicited_queue.put_nowait(None)
|
||||
self.dlc.multiplexer.l2cap_channel.EVENT_CLOSE,
|
||||
lambda: self.unsolicited_queue.put_nowait(None),
|
||||
)
|
||||
|
||||
def supports_hf_feature(self, feature: HfFeature) -> bool:
|
||||
@@ -1034,7 +1043,7 @@ class HfProtocol(utils.EventEmitter):
|
||||
# ID. The HF shall be ready to accept the synchronous connection
|
||||
# establishment as soon as it has sent the AT commands AT+BCS=<Codec ID>.
|
||||
self.active_codec = AudioCodec(codec_id)
|
||||
self.emit('codec_negotiation', self.active_codec)
|
||||
self.emit(self.EVENT_CODEC_NEGOTIATION, self.active_codec)
|
||||
|
||||
logger.info("codec connection setup completed")
|
||||
|
||||
@@ -1095,7 +1104,7 @@ class HfProtocol(utils.EventEmitter):
|
||||
# CIEV is in 1-index, while ag_indicators is in 0-index.
|
||||
ag_indicator = self.ag_indicators[index - 1]
|
||||
ag_indicator.current_status = value
|
||||
self.emit('ag_indicator', ag_indicator)
|
||||
self.emit(self.EVENT_AG_INDICATOR, ag_indicator)
|
||||
logger.info(f"AG indicator updated: {ag_indicator.indicator}, {value}")
|
||||
|
||||
async def handle_unsolicited(self):
|
||||
@@ -1110,19 +1119,21 @@ class HfProtocol(utils.EventEmitter):
|
||||
int(result.parameters[0]), int(result.parameters[1])
|
||||
)
|
||||
elif result.code == "+VGS":
|
||||
self.emit('speaker_volume', int(result.parameters[0]))
|
||||
self.emit(self.EVENT_SPEAKER_VOLUME, int(result.parameters[0]))
|
||||
elif result.code == "+VGM":
|
||||
self.emit('microphone_volume', int(result.parameters[0]))
|
||||
self.emit(self.EVENT_MICROPHONE_VOLUME, int(result.parameters[0]))
|
||||
elif result.code == "RING":
|
||||
self.emit('ring')
|
||||
self.emit(self.EVENT_RING)
|
||||
elif result.code == "+CLIP":
|
||||
self.emit(
|
||||
'cli_notification', CallLineIdentification.parse_from(result.parameters)
|
||||
self.EVENT_CLI_NOTIFICATION,
|
||||
CallLineIdentification.parse_from(result.parameters),
|
||||
)
|
||||
elif result.code == "+BVRA":
|
||||
# TODO: Support Enhanced Voice Recognition.
|
||||
self.emit(
|
||||
'voice_recognition', VoiceRecognitionState(int(result.parameters[0]))
|
||||
self.EVENT_VOICE_RECOGNITION,
|
||||
VoiceRecognitionState(int(result.parameters[0])),
|
||||
)
|
||||
else:
|
||||
logging.info(f"unhandled unsolicited response {result.code}")
|
||||
@@ -1179,6 +1190,19 @@ class AgProtocol(utils.EventEmitter):
|
||||
volume: Int
|
||||
"""
|
||||
|
||||
EVENT_SLC_COMPLETE = "slc_complete"
|
||||
EVENT_SUPPORTED_AUDIO_CODECS = "supported_audio_codecs"
|
||||
EVENT_CODEC_NEGOTIATION = "codec_negotiation"
|
||||
EVENT_VOICE_RECOGNITION = "voice_recognition"
|
||||
EVENT_CALL_HOLD = "call_hold"
|
||||
EVENT_HF_INDICATOR = "hf_indicator"
|
||||
EVENT_CODEC_CONNECTION_REQUEST = "codec_connection_request"
|
||||
EVENT_ANSWER = "answer"
|
||||
EVENT_DIAL = "dial"
|
||||
EVENT_HANG_UP = "hang_up"
|
||||
EVENT_SPEAKER_VOLUME = "speaker_volume"
|
||||
EVENT_MICROPHONE_VOLUME = "microphone_volume"
|
||||
|
||||
supported_hf_features: int
|
||||
supported_hf_indicators: Set[HfIndicator]
|
||||
supported_audio_codecs: List[AudioCodec]
|
||||
@@ -1371,7 +1395,7 @@ class AgProtocol(utils.EventEmitter):
|
||||
|
||||
def _check_remained_slc_commands(self) -> None:
|
||||
if not self._remained_slc_setup_features:
|
||||
self.emit('slc_complete')
|
||||
self.emit(self.EVENT_SLC_COMPLETE)
|
||||
|
||||
def _on_brsf(self, hf_features: bytes) -> None:
|
||||
self.supported_hf_features = int(hf_features)
|
||||
@@ -1390,17 +1414,17 @@ class AgProtocol(utils.EventEmitter):
|
||||
|
||||
def _on_bac(self, *args) -> None:
|
||||
self.supported_audio_codecs = [AudioCodec(int(value)) for value in args]
|
||||
self.emit('supported_audio_codecs', self.supported_audio_codecs)
|
||||
self.emit(self.EVENT_SUPPORTED_AUDIO_CODECS, self.supported_audio_codecs)
|
||||
self.send_ok()
|
||||
|
||||
def _on_bcs(self, codec: bytes) -> None:
|
||||
self.active_codec = AudioCodec(int(codec))
|
||||
self.send_ok()
|
||||
self.emit('codec_negotiation', self.active_codec)
|
||||
self.emit(self.EVENT_CODEC_NEGOTIATION, self.active_codec)
|
||||
|
||||
def _on_bvra(self, vrec: bytes) -> None:
|
||||
self.send_ok()
|
||||
self.emit('voice_recognition', VoiceRecognitionState(int(vrec)))
|
||||
self.emit(self.EVENT_VOICE_RECOGNITION, VoiceRecognitionState(int(vrec)))
|
||||
|
||||
def _on_chld(self, operation_code: bytes) -> None:
|
||||
call_index: Optional[int] = None
|
||||
@@ -1427,7 +1451,7 @@ class AgProtocol(utils.EventEmitter):
|
||||
# Real three-way calls have more complicated situations, but this is not a popular issue - let users to handle the remaining :)
|
||||
|
||||
self.send_ok()
|
||||
self.emit('call_hold', operation, call_index)
|
||||
self.emit(self.EVENT_CALL_HOLD, operation, call_index)
|
||||
|
||||
def _on_chld_test(self) -> None:
|
||||
if not self.supports_ag_feature(AgFeature.THREE_WAY_CALLING):
|
||||
@@ -1553,7 +1577,7 @@ class AgProtocol(utils.EventEmitter):
|
||||
return
|
||||
|
||||
self.hf_indicators[index].current_status = int(value_bytes)
|
||||
self.emit('hf_indicator', self.hf_indicators[index])
|
||||
self.emit(self.EVENT_HF_INDICATOR, self.hf_indicators[index])
|
||||
self.send_ok()
|
||||
|
||||
def _on_bia(self, *args) -> None:
|
||||
@@ -1562,21 +1586,21 @@ class AgProtocol(utils.EventEmitter):
|
||||
self.send_ok()
|
||||
|
||||
def _on_bcc(self) -> None:
|
||||
self.emit('codec_connection_request')
|
||||
self.emit(self.EVENT_CODEC_CONNECTION_REQUEST)
|
||||
self.send_ok()
|
||||
|
||||
def _on_a(self) -> None:
|
||||
"""ATA handler."""
|
||||
self.emit('answer')
|
||||
self.emit(self.EVENT_ANSWER)
|
||||
self.send_ok()
|
||||
|
||||
def _on_d(self, number: bytes) -> None:
|
||||
"""ATD handler."""
|
||||
self.emit('dial', number.decode())
|
||||
self.emit(self.EVENT_DIAL, number.decode())
|
||||
self.send_ok()
|
||||
|
||||
def _on_chup(self) -> None:
|
||||
self.emit('hang_up')
|
||||
self.emit(self.EVENT_HANG_UP)
|
||||
self.send_ok()
|
||||
|
||||
def _on_clcc(self) -> None:
|
||||
@@ -1602,11 +1626,11 @@ class AgProtocol(utils.EventEmitter):
|
||||
self.send_ok()
|
||||
|
||||
def _on_vgs(self, level: bytes) -> None:
|
||||
self.emit('speaker_volume', int(level))
|
||||
self.emit(self.EVENT_SPEAKER_VOLUME, int(level))
|
||||
self.send_ok()
|
||||
|
||||
def _on_vgm(self, level: bytes) -> None:
|
||||
self.emit('microphone_volume', int(level))
|
||||
self.emit(self.EVENT_MICROPHONE_VOLUME, int(level))
|
||||
self.send_ok()
|
||||
|
||||
|
||||
|
||||
@@ -201,6 +201,13 @@ class HID(ABC, utils.EventEmitter):
|
||||
l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None
|
||||
connection: Optional[device.Connection] = None
|
||||
|
||||
EVENT_INTERRUPT_DATA = "interrupt_data"
|
||||
EVENT_CONTROL_DATA = "control_data"
|
||||
EVENT_SUSPEND = "suspend"
|
||||
EVENT_EXIT_SUSPEND = "exit_suspend"
|
||||
EVENT_VIRTUAL_CABLE_UNPLUG = "virtual_cable_unplug"
|
||||
EVENT_HANDSHAKE = "handshake"
|
||||
|
||||
class Role(enum.IntEnum):
|
||||
HOST = 0x00
|
||||
DEVICE = 0x01
|
||||
@@ -215,7 +222,7 @@ class HID(ABC, utils.EventEmitter):
|
||||
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection)
|
||||
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
|
||||
|
||||
device.on('connection', self.on_device_connection)
|
||||
device.on(device.EVENT_CONNECTION, self.on_device_connection)
|
||||
|
||||
async def connect_control_channel(self) -> None:
|
||||
# Create a new L2CAP connection - control channel
|
||||
@@ -258,15 +265,20 @@ class HID(ABC, utils.EventEmitter):
|
||||
def on_device_connection(self, connection: device.Connection) -> None:
|
||||
self.connection = connection
|
||||
self.remote_device_bd_address = connection.peer_address
|
||||
connection.on('disconnection', self.on_device_disconnection)
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_device_disconnection)
|
||||
|
||||
def on_device_disconnection(self, reason: int) -> None:
|
||||
self.connection = None
|
||||
|
||||
def on_l2cap_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||
logger.debug(f'+++ New L2CAP connection: {l2cap_channel}')
|
||||
l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
|
||||
l2cap_channel.on('close', lambda: self.on_l2cap_channel_close(l2cap_channel))
|
||||
l2cap_channel.on(
|
||||
l2cap_channel.EVENT_OPEN, lambda: self.on_l2cap_channel_open(l2cap_channel)
|
||||
)
|
||||
l2cap_channel.on(
|
||||
l2cap_channel.EVENT_CLOSE,
|
||||
lambda: self.on_l2cap_channel_close(l2cap_channel),
|
||||
)
|
||||
|
||||
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||
if l2cap_channel.psm == HID_CONTROL_PSM:
|
||||
@@ -290,7 +302,7 @@ class HID(ABC, utils.EventEmitter):
|
||||
|
||||
def on_intr_pdu(self, pdu: bytes) -> None:
|
||||
logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}')
|
||||
self.emit("interrupt_data", pdu)
|
||||
self.emit(self.EVENT_INTERRUPT_DATA, pdu)
|
||||
|
||||
def send_pdu_on_ctrl(self, msg: bytes) -> None:
|
||||
assert self.l2cap_ctrl_channel
|
||||
@@ -363,17 +375,17 @@ class Device(HID):
|
||||
self.handle_set_protocol(pdu)
|
||||
elif message_type == Message.MessageType.DATA:
|
||||
logger.debug('<<< HID CONTROL DATA')
|
||||
self.emit('control_data', pdu)
|
||||
self.emit(self.EVENT_CONTROL_DATA, pdu)
|
||||
elif message_type == Message.MessageType.CONTROL:
|
||||
if param == Message.ControlCommand.SUSPEND:
|
||||
logger.debug('<<< HID SUSPEND')
|
||||
self.emit('suspend')
|
||||
self.emit(self.EVENT_SUSPEND)
|
||||
elif param == Message.ControlCommand.EXIT_SUSPEND:
|
||||
logger.debug('<<< HID EXIT SUSPEND')
|
||||
self.emit('exit_suspend')
|
||||
self.emit(self.EVENT_EXIT_SUSPEND)
|
||||
elif param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
|
||||
logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
|
||||
self.emit('virtual_cable_unplug')
|
||||
self.emit(self.EVENT_VIRTUAL_CABLE_UNPLUG)
|
||||
else:
|
||||
logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
|
||||
else:
|
||||
@@ -538,14 +550,14 @@ class Host(HID):
|
||||
message_type = pdu[0] >> 4
|
||||
if message_type == Message.MessageType.HANDSHAKE:
|
||||
logger.debug(f'<<< HID HANDSHAKE: {Message.Handshake(param).name}')
|
||||
self.emit('handshake', Message.Handshake(param))
|
||||
self.emit(self.EVENT_HANDSHAKE, Message.Handshake(param))
|
||||
elif message_type == Message.MessageType.DATA:
|
||||
logger.debug('<<< HID CONTROL DATA')
|
||||
self.emit('control_data', pdu)
|
||||
self.emit(self.EVENT_CONTROL_DATA, pdu)
|
||||
elif message_type == Message.MessageType.CONTROL:
|
||||
if param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
|
||||
logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
|
||||
self.emit('virtual_cable_unplug')
|
||||
self.emit(self.EVENT_VIRTUAL_CABLE_UNPLUG)
|
||||
else:
|
||||
logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
|
||||
else:
|
||||
|
||||
@@ -435,6 +435,14 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
)
|
||||
)
|
||||
if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND):
|
||||
await self.send_command(
|
||||
hci.HCI_Set_Event_Mask_Page_2_Command(
|
||||
event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask(
|
||||
[hci.HCI_ENCRYPTION_CHANGE_V2_EVENT]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.local_version is not None
|
||||
@@ -456,6 +464,7 @@ class Host(utils.EventEmitter):
|
||||
hci.HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT,
|
||||
hci.HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT,
|
||||
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT,
|
||||
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||
hci.HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT,
|
||||
hci.HCI_LE_PHY_UPDATE_COMPLETE_EVENT,
|
||||
hci.HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT,
|
||||
@@ -1383,6 +1392,21 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_change',
|
||||
event.connection_handle,
|
||||
event.encryption_enabled,
|
||||
0,
|
||||
)
|
||||
else:
|
||||
self.emit(
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_v2_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'connection_encryption_change',
|
||||
event.connection_handle,
|
||||
event.encryption_enabled,
|
||||
event.encryption_key_size,
|
||||
)
|
||||
else:
|
||||
self.emit(
|
||||
|
||||
108
bumble/keys.py
108
bumble/keys.py
@@ -22,14 +22,15 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Any
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.hci import Address
|
||||
from bumble import hci
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device
|
||||
@@ -42,16 +43,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class PairingKeys:
|
||||
@dataclasses.dataclass
|
||||
class Key:
|
||||
def __init__(self, value, authenticated=False, ediv=None, rand=None):
|
||||
self.value = value
|
||||
self.authenticated = authenticated
|
||||
self.ediv = ediv
|
||||
self.rand = rand
|
||||
value: bytes
|
||||
authenticated: bool = False
|
||||
ediv: Optional[int] = None
|
||||
rand: Optional[bytes] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, key_dict):
|
||||
def from_dict(cls, key_dict: dict[str, Any]) -> PairingKeys.Key:
|
||||
value = bytes.fromhex(key_dict['value'])
|
||||
authenticated = key_dict.get('authenticated', False)
|
||||
ediv = key_dict.get('ediv')
|
||||
@@ -61,7 +63,7 @@ class PairingKeys:
|
||||
|
||||
return cls(value, authenticated, ediv, rand)
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated}
|
||||
if self.ediv is not None:
|
||||
key_dict['ediv'] = self.ediv
|
||||
@@ -70,39 +72,42 @@ class PairingKeys:
|
||||
|
||||
return key_dict
|
||||
|
||||
def __init__(self):
|
||||
self.address_type = None
|
||||
self.ltk = None
|
||||
self.ltk_central = None
|
||||
self.ltk_peripheral = None
|
||||
self.irk = None
|
||||
self.csrk = None
|
||||
self.link_key = None # Classic
|
||||
address_type: Optional[hci.AddressType] = None
|
||||
ltk: Optional[Key] = None
|
||||
ltk_central: Optional[Key] = None
|
||||
ltk_peripheral: Optional[Key] = None
|
||||
irk: Optional[Key] = None
|
||||
csrk: Optional[Key] = None
|
||||
link_key: Optional[Key] = None # Classic
|
||||
link_key_type: Optional[int] = None # Classic
|
||||
|
||||
@staticmethod
|
||||
def key_from_dict(keys_dict, key_name):
|
||||
@classmethod
|
||||
def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Optional[Key]:
|
||||
key_dict = keys_dict.get(key_name)
|
||||
if key_dict is None:
|
||||
return None
|
||||
|
||||
return PairingKeys.Key.from_dict(key_dict)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(keys_dict):
|
||||
keys = PairingKeys()
|
||||
@classmethod
|
||||
def from_dict(cls, keys_dict: dict[str, Any]) -> PairingKeys:
|
||||
return PairingKeys(
|
||||
address_type=(
|
||||
hci.AddressType(t)
|
||||
if (t := keys_dict.get('address_type')) is not None
|
||||
else None
|
||||
),
|
||||
ltk=PairingKeys.key_from_dict(keys_dict, 'ltk'),
|
||||
ltk_central=PairingKeys.key_from_dict(keys_dict, 'ltk_central'),
|
||||
ltk_peripheral=PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral'),
|
||||
irk=PairingKeys.key_from_dict(keys_dict, 'irk'),
|
||||
csrk=PairingKeys.key_from_dict(keys_dict, 'csrk'),
|
||||
link_key=PairingKeys.key_from_dict(keys_dict, 'link_key'),
|
||||
link_key_type=keys_dict.get('link_key_type'),
|
||||
)
|
||||
|
||||
keys.address_type = keys_dict.get('address_type')
|
||||
keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk')
|
||||
keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
|
||||
keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral')
|
||||
keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
|
||||
keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
|
||||
keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
|
||||
|
||||
return keys
|
||||
|
||||
def to_dict(self):
|
||||
keys = {}
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
keys: dict[str, Any] = {}
|
||||
|
||||
if self.address_type is not None:
|
||||
keys['address_type'] = self.address_type
|
||||
@@ -125,9 +130,12 @@ class PairingKeys:
|
||||
if self.link_key is not None:
|
||||
keys['link_key'] = self.link_key.to_dict()
|
||||
|
||||
if self.link_key_type is not None:
|
||||
keys['link_key_type'] = self.link_key_type
|
||||
|
||||
return keys
|
||||
|
||||
def print(self, prefix=''):
|
||||
def print(self, prefix: str = '') -> None:
|
||||
keys_dict = self.to_dict()
|
||||
for container_property, value in keys_dict.items():
|
||||
if isinstance(value, dict):
|
||||
@@ -156,20 +164,28 @@ class KeyStore:
|
||||
all_keys = await self.get_all()
|
||||
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
|
||||
|
||||
async def get_resolving_keys(self):
|
||||
async def get_resolving_keys(self) -> list[tuple[bytes, hci.Address]]:
|
||||
all_keys = await self.get_all()
|
||||
resolving_keys = []
|
||||
for name, keys in all_keys:
|
||||
if keys.irk is not None:
|
||||
if keys.address_type is None:
|
||||
address_type = Address.RANDOM_DEVICE_ADDRESS
|
||||
else:
|
||||
address_type = keys.address_type
|
||||
resolving_keys.append((keys.irk.value, Address(name, address_type)))
|
||||
resolving_keys.append(
|
||||
(
|
||||
keys.irk.value,
|
||||
hci.Address(
|
||||
name,
|
||||
(
|
||||
keys.address_type
|
||||
if keys.address_type is not None
|
||||
else hci.Address.RANDOM_DEVICE_ADDRESS
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return resolving_keys
|
||||
|
||||
async def print(self, prefix=''):
|
||||
async def print(self, prefix: str = '') -> None:
|
||||
entries = await self.get_all()
|
||||
separator = ''
|
||||
for name, keys in entries:
|
||||
@@ -177,8 +193,8 @@ class KeyStore:
|
||||
keys.print(prefix=prefix + ' ')
|
||||
separator = '\n'
|
||||
|
||||
@staticmethod
|
||||
def create_for_device(device: Device) -> KeyStore:
|
||||
@classmethod
|
||||
def create_for_device(cls, device: Device) -> KeyStore:
|
||||
if device.config.keystore is None:
|
||||
return MemoryKeyStore()
|
||||
|
||||
@@ -266,9 +282,9 @@ class JsonKeyStore(KeyStore):
|
||||
filename = params[0]
|
||||
|
||||
# Use a namespace based on the device address
|
||||
if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
|
||||
if device.public_address not in (hci.Address.ANY, hci.Address.ANY_RANDOM):
|
||||
namespace = str(device.public_address)
|
||||
elif device.random_address != Address.ANY_RANDOM:
|
||||
elif device.random_address != hci.Address.ANY_RANDOM:
|
||||
namespace = str(device.random_address)
|
||||
else:
|
||||
namespace = JsonKeyStore.DEFAULT_NAMESPACE
|
||||
|
||||
@@ -744,6 +744,9 @@ class ClassicChannel(utils.EventEmitter):
|
||||
WAIT_FINAL_RSP = 0x16
|
||||
WAIT_CONTROL_IND = 0x17
|
||||
|
||||
EVENT_OPEN = "open"
|
||||
EVENT_CLOSE = "close"
|
||||
|
||||
connection_result: Optional[asyncio.Future[None]]
|
||||
disconnection_result: Optional[asyncio.Future[None]]
|
||||
response: Optional[asyncio.Future[bytes]]
|
||||
@@ -847,7 +850,7 @@ class ClassicChannel(utils.EventEmitter):
|
||||
def abort(self) -> None:
|
||||
if self.state == self.State.OPEN:
|
||||
self._change_state(self.State.CLOSED)
|
||||
self.emit('close')
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
|
||||
def send_configure_request(self) -> None:
|
||||
options = L2CAP_Control_Frame.encode_configuration_options(
|
||||
@@ -940,7 +943,7 @@ class ClassicChannel(utils.EventEmitter):
|
||||
if self.connection_result:
|
||||
self.connection_result.set_result(None)
|
||||
self.connection_result = None
|
||||
self.emit('open')
|
||||
self.emit(self.EVENT_OPEN)
|
||||
elif self.state == self.State.WAIT_CONFIG_REQ_RSP:
|
||||
self._change_state(self.State.WAIT_CONFIG_RSP)
|
||||
|
||||
@@ -956,7 +959,7 @@ class ClassicChannel(utils.EventEmitter):
|
||||
if self.connection_result:
|
||||
self.connection_result.set_result(None)
|
||||
self.connection_result = None
|
||||
self.emit('open')
|
||||
self.emit(self.EVENT_OPEN)
|
||||
else:
|
||||
logger.warning(color('invalid state', 'red'))
|
||||
elif (
|
||||
@@ -991,7 +994,7 @@ class ClassicChannel(utils.EventEmitter):
|
||||
)
|
||||
)
|
||||
self._change_state(self.State.CLOSED)
|
||||
self.emit('close')
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
self.manager.on_channel_closed(self)
|
||||
else:
|
||||
logger.warning(color('invalid state', 'red'))
|
||||
@@ -1012,7 +1015,7 @@ class ClassicChannel(utils.EventEmitter):
|
||||
if self.disconnection_result:
|
||||
self.disconnection_result.set_result(None)
|
||||
self.disconnection_result = None
|
||||
self.emit('close')
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
self.manager.on_channel_closed(self)
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -1047,6 +1050,9 @@ class LeCreditBasedChannel(utils.EventEmitter):
|
||||
connection: Connection
|
||||
sink: Optional[Callable[[bytes], Any]]
|
||||
|
||||
EVENT_OPEN = "open"
|
||||
EVENT_CLOSE = "close"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
manager: ChannelManager,
|
||||
@@ -1098,9 +1104,9 @@ class LeCreditBasedChannel(utils.EventEmitter):
|
||||
self.state = new_state
|
||||
|
||||
if new_state == self.State.CONNECTED:
|
||||
self.emit('open')
|
||||
self.emit(self.EVENT_OPEN)
|
||||
elif new_state == self.State.DISCONNECTED:
|
||||
self.emit('close')
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
|
||||
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
||||
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
|
||||
@@ -1381,6 +1387,8 @@ class LeCreditBasedChannel(utils.EventEmitter):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ClassicChannelServer(utils.EventEmitter):
|
||||
EVENT_CONNECTION = "connection"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
manager: ChannelManager,
|
||||
@@ -1395,7 +1403,7 @@ class ClassicChannelServer(utils.EventEmitter):
|
||||
self.mtu = mtu
|
||||
|
||||
def on_connection(self, channel: ClassicChannel) -> None:
|
||||
self.emit('connection', channel)
|
||||
self.emit(self.EVENT_CONNECTION, channel)
|
||||
if self.handler:
|
||||
self.handler(channel)
|
||||
|
||||
@@ -1406,6 +1414,8 @@ class ClassicChannelServer(utils.EventEmitter):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class LeCreditBasedChannelServer(utils.EventEmitter):
|
||||
EVENT_CONNECTION = "connection"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
manager: ChannelManager,
|
||||
@@ -1424,7 +1434,7 @@ class LeCreditBasedChannelServer(utils.EventEmitter):
|
||||
self.mps = mps
|
||||
|
||||
def on_connection(self, channel: LeCreditBasedChannel) -> None:
|
||||
self.emit('connection', channel)
|
||||
self.emit(self.EVENT_CONNECTION, channel)
|
||||
if self.handler:
|
||||
self.handler(channel)
|
||||
|
||||
|
||||
@@ -296,12 +296,12 @@ class HostService(HostServicer):
|
||||
def on_disconnection(_: None) -> None:
|
||||
disconnection_future.set_result(None)
|
||||
|
||||
connection.on('disconnection', on_disconnection)
|
||||
connection.on(connection.EVENT_DISCONNECTION, on_disconnection)
|
||||
try:
|
||||
await disconnection_future
|
||||
self.log.debug("Disconnected")
|
||||
finally:
|
||||
connection.remove_listener('disconnection', on_disconnection) # type: ignore
|
||||
connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore
|
||||
|
||||
return empty_pb2.Empty()
|
||||
|
||||
@@ -383,7 +383,7 @@ class HostService(HostServicer):
|
||||
):
|
||||
connections.put_nowait(connection)
|
||||
|
||||
self.device.on('connection', on_connection)
|
||||
self.device.on(self.device.EVENT_CONNECTION, on_connection)
|
||||
|
||||
try:
|
||||
# Advertise until RPC is canceled
|
||||
@@ -501,7 +501,7 @@ class HostService(HostServicer):
|
||||
):
|
||||
connections.put_nowait(connection)
|
||||
|
||||
self.device.on('connection', on_connection)
|
||||
self.device.on(self.device.EVENT_CONNECTION, on_connection)
|
||||
|
||||
try:
|
||||
while True:
|
||||
@@ -531,7 +531,7 @@ class HostService(HostServicer):
|
||||
await asyncio.sleep(1)
|
||||
finally:
|
||||
if request.connectable:
|
||||
self.device.remove_listener('connection', on_connection) # type: ignore
|
||||
self.device.remove_listener(self.device.EVENT_CONNECTION, on_connection) # type: ignore
|
||||
|
||||
try:
|
||||
self.log.debug('Stop advertising')
|
||||
@@ -557,7 +557,7 @@ class HostService(HostServicer):
|
||||
scanning_phys = [int(Phy.LE_1M), int(Phy.LE_CODED)]
|
||||
|
||||
scan_queue: asyncio.Queue[Advertisement] = asyncio.Queue()
|
||||
handler = self.device.on('advertisement', scan_queue.put_nowait)
|
||||
handler = self.device.on(self.device.EVENT_ADVERTISEMENT, scan_queue.put_nowait)
|
||||
await self.device.start_scanning(
|
||||
legacy=request.legacy,
|
||||
active=not request.passive,
|
||||
@@ -602,7 +602,7 @@ class HostService(HostServicer):
|
||||
yield sr
|
||||
|
||||
finally:
|
||||
self.device.remove_listener('advertisement', handler) # type: ignore
|
||||
self.device.remove_listener(self.device.EVENT_ADVERTISEMENT, handler) # type: ignore
|
||||
try:
|
||||
self.log.debug('Stop scanning')
|
||||
await bumble.utils.cancel_on_event(
|
||||
@@ -621,10 +621,10 @@ class HostService(HostServicer):
|
||||
Optional[Tuple[Address, int, AdvertisingData, int]]
|
||||
] = asyncio.Queue()
|
||||
complete_handler = self.device.on(
|
||||
'inquiry_complete', lambda: inquiry_queue.put_nowait(None)
|
||||
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
|
||||
)
|
||||
result_handler = self.device.on( # type: ignore
|
||||
'inquiry_result',
|
||||
self.device.EVENT_INQUIRY_RESULT,
|
||||
lambda address, class_of_device, eir_data, rssi: inquiry_queue.put_nowait( # type: ignore
|
||||
(address, class_of_device, eir_data, rssi) # type: ignore
|
||||
),
|
||||
@@ -643,8 +643,8 @@ class HostService(HostServicer):
|
||||
)
|
||||
|
||||
finally:
|
||||
self.device.remove_listener('inquiry_complete', complete_handler) # type: ignore
|
||||
self.device.remove_listener('inquiry_result', result_handler) # type: ignore
|
||||
self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore
|
||||
self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore
|
||||
try:
|
||||
self.log.debug('Stop inquiry')
|
||||
await bumble.utils.cancel_on_event(
|
||||
|
||||
@@ -83,7 +83,7 @@ class L2CAPService(L2CAPServicer):
|
||||
close_future.set_result(None)
|
||||
|
||||
l2cap_channel.sink = on_channel_sdu
|
||||
l2cap_channel.on('close', on_close)
|
||||
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, on_close)
|
||||
|
||||
return ChannelContext(close_future, sdu_queue)
|
||||
|
||||
@@ -151,7 +151,7 @@ class L2CAPService(L2CAPServicer):
|
||||
spec=spec, handler=on_l2cap_channel
|
||||
)
|
||||
else:
|
||||
l2cap_server.on('connection', on_l2cap_channel)
|
||||
l2cap_server.on(l2cap_server.EVENT_CONNECTION, on_l2cap_channel)
|
||||
|
||||
try:
|
||||
self.log.debug('Waiting for a channel connection.')
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Awaitable
|
||||
import grpc
|
||||
import logging
|
||||
|
||||
@@ -24,6 +25,7 @@ from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
InvalidArgumentError,
|
||||
)
|
||||
import bumble.utils
|
||||
from bumble.device import Connection as BumbleConnection, Device
|
||||
@@ -188,35 +190,6 @@ class PairingDelegate(BasePairingDelegate):
|
||||
self.service.event_queue.put_nowait(event)
|
||||
|
||||
|
||||
BR_LEVEL_REACHED: Dict[SecurityLevel, Callable[[BumbleConnection], bool]] = {
|
||||
LEVEL0: lambda connection: True,
|
||||
LEVEL1: lambda connection: connection.encryption == 0 or connection.authenticated,
|
||||
LEVEL2: lambda connection: connection.encryption != 0 and connection.authenticated,
|
||||
LEVEL3: lambda connection: connection.encryption != 0
|
||||
and connection.authenticated
|
||||
and connection.link_key_type
|
||||
in (
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||
),
|
||||
LEVEL4: lambda connection: connection.encryption
|
||||
== hci.HCI_Encryption_Change_Event.AES_CCM
|
||||
and connection.authenticated
|
||||
and connection.link_key_type
|
||||
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||
}
|
||||
|
||||
LE_LEVEL_REACHED: Dict[LESecurityLevel, Callable[[BumbleConnection], bool]] = {
|
||||
LE_LEVEL1: lambda connection: True,
|
||||
LE_LEVEL2: lambda connection: connection.encryption != 0,
|
||||
LE_LEVEL3: lambda connection: connection.encryption != 0
|
||||
and connection.authenticated,
|
||||
LE_LEVEL4: lambda connection: connection.encryption != 0
|
||||
and connection.authenticated
|
||||
and connection.sc,
|
||||
}
|
||||
|
||||
|
||||
class SecurityService(SecurityServicer):
|
||||
def __init__(self, device: Device, config: Config) -> None:
|
||||
self.log = utils.BumbleServerLoggerAdapter(
|
||||
@@ -248,6 +221,59 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
self.device.pairing_config_factory = pairing_config_factory
|
||||
|
||||
async def _classic_level_reached(
|
||||
self, level: SecurityLevel, connection: BumbleConnection
|
||||
) -> bool:
|
||||
if level == LEVEL0:
|
||||
return True
|
||||
if level == LEVEL1:
|
||||
return connection.encryption == 0 or connection.authenticated
|
||||
if level == LEVEL2:
|
||||
return connection.encryption != 0 and connection.authenticated
|
||||
|
||||
link_key_type: Optional[int] = None
|
||||
if (keystore := connection.device.keystore) and (
|
||||
keys := await keystore.get(str(connection.peer_address))
|
||||
):
|
||||
link_key_type = keys.link_key_type
|
||||
self.log.debug("link_key_type: %d", link_key_type)
|
||||
|
||||
if level == LEVEL3:
|
||||
return (
|
||||
connection.encryption != 0
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
in (
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||
)
|
||||
)
|
||||
if level == LEVEL4:
|
||||
return (
|
||||
connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
|
||||
)
|
||||
raise InvalidArgumentError(f"Unexpected level {level}")
|
||||
|
||||
def _le_level_reached(
|
||||
self, level: LESecurityLevel, connection: BumbleConnection
|
||||
) -> bool:
|
||||
if level == LE_LEVEL1:
|
||||
return True
|
||||
if level == LE_LEVEL2:
|
||||
return connection.encryption != 0
|
||||
if level == LE_LEVEL3:
|
||||
return connection.encryption != 0 and connection.authenticated
|
||||
if level == LE_LEVEL4:
|
||||
return (
|
||||
connection.encryption != 0
|
||||
and connection.authenticated
|
||||
and connection.sc
|
||||
)
|
||||
raise InvalidArgumentError(f"Unexpected level {level}")
|
||||
|
||||
@utils.rpc
|
||||
async def OnPairing(
|
||||
self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
|
||||
@@ -290,7 +316,7 @@ class SecurityService(SecurityServicer):
|
||||
] == oneof
|
||||
|
||||
# security level already reached
|
||||
if self.reached_security_level(connection, level):
|
||||
if await self.reached_security_level(connection, level):
|
||||
return SecureResponse(success=empty_pb2.Empty())
|
||||
|
||||
# trigger pairing if needed
|
||||
@@ -302,15 +328,15 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
with contextlib.closing(bumble.utils.EventWatcher()) as watcher:
|
||||
|
||||
@watcher.on(connection, 'pairing')
|
||||
@watcher.on(connection, connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
security_result.set_result('success')
|
||||
|
||||
@watcher.on(connection, 'pairing_failure')
|
||||
@watcher.on(connection, connection.EVENT_PAIRING_FAILURE)
|
||||
def on_pairing_failure(*_: Any) -> None:
|
||||
security_result.set_result('pairing_failure')
|
||||
|
||||
@watcher.on(connection, 'disconnection')
|
||||
@watcher.on(connection, connection.EVENT_DISCONNECTION)
|
||||
def on_disconnection(*_: Any) -> None:
|
||||
security_result.set_result('connection_died')
|
||||
|
||||
@@ -361,7 +387,7 @@ class SecurityService(SecurityServicer):
|
||||
return SecureResponse(encryption_failure=empty_pb2.Empty())
|
||||
|
||||
# security level has been reached ?
|
||||
if self.reached_security_level(connection, level):
|
||||
if await self.reached_security_level(connection, level):
|
||||
return SecureResponse(success=empty_pb2.Empty())
|
||||
return SecureResponse(not_reached=empty_pb2.Empty())
|
||||
|
||||
@@ -388,13 +414,10 @@ class SecurityService(SecurityServicer):
|
||||
pair_task: Optional[asyncio.Future[None]] = None
|
||||
|
||||
async def authenticate() -> None:
|
||||
assert connection
|
||||
if (encryption := connection.encryption) != 0:
|
||||
self.log.debug('Disable encryption...')
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
await connection.encrypt(enable=False)
|
||||
except:
|
||||
pass
|
||||
self.log.debug('Disable encryption: done')
|
||||
|
||||
self.log.debug('Authenticate...')
|
||||
@@ -413,15 +436,13 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
return wrapper
|
||||
|
||||
def try_set_success(*_: Any) -> None:
|
||||
assert connection
|
||||
if self.reached_security_level(connection, level):
|
||||
async def try_set_success(*_: Any) -> None:
|
||||
if await self.reached_security_level(connection, level):
|
||||
self.log.debug('Wait for security: done')
|
||||
wait_for_security.set_result('success')
|
||||
|
||||
def on_encryption_change(*_: Any) -> None:
|
||||
assert connection
|
||||
if self.reached_security_level(connection, level):
|
||||
async def on_encryption_change(*_: Any) -> None:
|
||||
if await self.reached_security_level(connection, level):
|
||||
self.log.debug('Wait for security: done')
|
||||
wait_for_security.set_result('success')
|
||||
elif (
|
||||
@@ -436,7 +457,7 @@ class SecurityService(SecurityServicer):
|
||||
if self.need_pairing(connection, level):
|
||||
pair_task = asyncio.create_task(connection.pair())
|
||||
|
||||
listeners: Dict[str, Callable[..., None]] = {
|
||||
listeners: Dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
|
||||
'disconnection': set_failure('connection_died'),
|
||||
'pairing_failure': set_failure('pairing_failure'),
|
||||
'connection_authentication_failure': set_failure('authentication_failure'),
|
||||
@@ -455,7 +476,7 @@ class SecurityService(SecurityServicer):
|
||||
watcher.on(connection, event, listener)
|
||||
|
||||
# security level already reached
|
||||
if self.reached_security_level(connection, level):
|
||||
if await self.reached_security_level(connection, level):
|
||||
return WaitSecurityResponse(success=empty_pb2.Empty())
|
||||
|
||||
self.log.debug('Wait for security...')
|
||||
@@ -465,24 +486,20 @@ class SecurityService(SecurityServicer):
|
||||
# wait for `authenticate` to finish if any
|
||||
if authenticate_task is not None:
|
||||
self.log.debug('Wait for authentication...')
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
await authenticate_task # type: ignore
|
||||
except:
|
||||
pass
|
||||
self.log.debug('Authenticated')
|
||||
|
||||
# wait for `pair` to finish if any
|
||||
if pair_task is not None:
|
||||
self.log.debug('Wait for authentication...')
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
await pair_task # type: ignore
|
||||
except:
|
||||
pass
|
||||
self.log.debug('paired')
|
||||
|
||||
return WaitSecurityResponse(**kwargs)
|
||||
|
||||
def reached_security_level(
|
||||
async def reached_security_level(
|
||||
self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
|
||||
) -> bool:
|
||||
self.log.debug(
|
||||
@@ -492,15 +509,14 @@ class SecurityService(SecurityServicer):
|
||||
'encryption': connection.encryption,
|
||||
'authenticated': connection.authenticated,
|
||||
'sc': connection.sc,
|
||||
'link_key_type': connection.link_key_type,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(level, LESecurityLevel):
|
||||
return LE_LEVEL_REACHED[level](connection)
|
||||
return self._le_level_reached(level, connection)
|
||||
|
||||
return BR_LEVEL_REACHED[level](connection)
|
||||
return await self._classic_level_reached(level, connection)
|
||||
|
||||
def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
|
||||
@@ -198,8 +198,7 @@ class AudioInputControlPoint:
|
||||
audio_input_state: AudioInputState
|
||||
gain_settings_properties: GainSettingsProperties
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
assert connection
|
||||
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||
|
||||
opcode = AudioInputControlPointOpCode(value[0])
|
||||
|
||||
@@ -320,11 +319,10 @@ class AudioInputDescription:
|
||||
audio_input_description: str = "Bluetooth"
|
||||
attribute: Optional[Attribute] = None
|
||||
|
||||
def on_read(self, _connection: Optional[Connection]) -> str:
|
||||
def on_read(self, _connection: Connection) -> str:
|
||||
return self.audio_input_description
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: str) -> None:
|
||||
assert connection
|
||||
async def on_write(self, connection: Connection, value: str) -> None:
|
||||
assert self.attribute
|
||||
|
||||
self.audio_input_description = value
|
||||
|
||||
@@ -250,6 +250,8 @@ class AncsClient(utils.EventEmitter):
|
||||
_expected_response_tuples: int
|
||||
_response_accumulator: bytes
|
||||
|
||||
EVENT_NOTIFICATION = "notification"
|
||||
|
||||
def __init__(self, ancs_proxy: AncsProxy) -> None:
|
||||
super().__init__()
|
||||
self._ancs_proxy = ancs_proxy
|
||||
@@ -284,7 +286,7 @@ class AncsClient(utils.EventEmitter):
|
||||
|
||||
def _on_notification(self, notification: Notification) -> None:
|
||||
logger.debug(f"ANCS NOTIFICATION: {notification}")
|
||||
self.emit("notification", notification)
|
||||
self.emit(self.EVENT_NOTIFICATION, notification)
|
||||
|
||||
def _on_data(self, data: bytes) -> None:
|
||||
logger.debug(f"ANCS DATA: {data.hex()}")
|
||||
|
||||
@@ -276,6 +276,8 @@ class AseStateMachine(gatt.Characteristic):
|
||||
DISABLING = 0x05
|
||||
RELEASING = 0x06
|
||||
|
||||
EVENT_STATE_CHANGE = "state_change"
|
||||
|
||||
cis_link: Optional[device.CisLink] = None
|
||||
|
||||
# Additional parameters in CODEC_CONFIGURED State
|
||||
@@ -329,8 +331,12 @@ class AseStateMachine(gatt.Characteristic):
|
||||
value=gatt.CharacteristicValue(read=self.on_read),
|
||||
)
|
||||
|
||||
self.service.device.on('cis_request', self.on_cis_request)
|
||||
self.service.device.on('cis_establishment', self.on_cis_establishment)
|
||||
self.service.device.on(
|
||||
self.service.device.EVENT_CIS_REQUEST, self.on_cis_request
|
||||
)
|
||||
self.service.device.on(
|
||||
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
||||
)
|
||||
|
||||
def on_cis_request(
|
||||
self,
|
||||
@@ -356,7 +362,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
and cis_link.cis_id == self.cis_id
|
||||
and self.state == self.State.ENABLING
|
||||
):
|
||||
cis_link.on('disconnection', self.on_cis_disconnection)
|
||||
cis_link.on(cis_link.EVENT_DISCONNECTION, self.on_cis_disconnection)
|
||||
|
||||
async def post_cis_established():
|
||||
await cis_link.setup_data_path(direction=self.role)
|
||||
@@ -525,7 +531,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
def state(self, new_state: State) -> None:
|
||||
logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
|
||||
self._state = new_state
|
||||
self.emit('state_change')
|
||||
self.emit(self.EVENT_STATE_CHANGE)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
@@ -584,7 +590,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
# Readonly. Do nothing in the setter.
|
||||
pass
|
||||
|
||||
def on_read(self, _: Optional[device.Connection]) -> bytes:
|
||||
def on_read(self, _: device.Connection) -> bytes:
|
||||
return self.value
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -88,6 +88,11 @@ class AudioStatus(utils.OpenIntEnum):
|
||||
class AshaService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_ASHA_SERVICE
|
||||
|
||||
EVENT_STARTED = "started"
|
||||
EVENT_STOPPED = "stopped"
|
||||
EVENT_DISCONNECTED = "disconnected"
|
||||
EVENT_VOLUME_CHANGED = "volume_changed"
|
||||
|
||||
audio_sink: Optional[Callable[[bytes], Any]]
|
||||
active_codec: Optional[Codec] = None
|
||||
audio_type: Optional[AudioType] = None
|
||||
@@ -195,7 +200,7 @@ class AshaService(gatt.TemplateService):
|
||||
|
||||
# Handler for audio control commands
|
||||
async def _on_audio_control_point_write(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
) -> None:
|
||||
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||
opcode = value[0]
|
||||
@@ -211,14 +216,14 @@ class AshaService(gatt.TemplateService):
|
||||
f'volume={self.volume}, '
|
||||
f'other_state={self.other_state}'
|
||||
)
|
||||
self.emit('started')
|
||||
self.emit(self.EVENT_STARTED)
|
||||
elif opcode == OpCode.STOP:
|
||||
_logger.debug('### STOP')
|
||||
self.active_codec = None
|
||||
self.audio_type = None
|
||||
self.volume = None
|
||||
self.other_state = None
|
||||
self.emit('stopped')
|
||||
self.emit(self.EVENT_STOPPED)
|
||||
elif opcode == OpCode.STATUS:
|
||||
_logger.debug('### STATUS: %s', PeripheralStatus(value[1]).name)
|
||||
|
||||
@@ -231,7 +236,7 @@ class AshaService(gatt.TemplateService):
|
||||
self.audio_type = None
|
||||
self.volume = None
|
||||
self.other_state = None
|
||||
self.emit('disconnected')
|
||||
self.emit(self.EVENT_DISCONNECTED)
|
||||
|
||||
connection.once('disconnection', on_disconnection)
|
||||
|
||||
@@ -242,10 +247,10 @@ class AshaService(gatt.TemplateService):
|
||||
)
|
||||
|
||||
# Handler for volume control
|
||||
def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
def _on_volume_write(self, connection: Connection, value: bytes) -> None:
|
||||
_logger.debug(f'--- VOLUME Write:{value[0]}')
|
||||
self.volume = value[0]
|
||||
self.emit('volume_changed')
|
||||
self.emit(self.EVENT_VOLUME_CHANGED)
|
||||
|
||||
# Register an L2CAP CoC server
|
||||
def _on_connection(self, channel: l2cap.LeCreditBasedChannel) -> None:
|
||||
|
||||
@@ -164,12 +164,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
|
||||
super().__init__(characteristics)
|
||||
|
||||
async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes:
|
||||
async def on_sirk_read(self, connection: device.Connection) -> bytes:
|
||||
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
|
||||
sirk_bytes = self.set_identity_resolving_key
|
||||
else:
|
||||
assert connection
|
||||
|
||||
if connection.transport == core.PhysicalTransport.LE:
|
||||
key = await connection.device.get_long_term_key(
|
||||
connection_handle=connection.handle, rand=b'', ediv=0
|
||||
|
||||
@@ -127,9 +127,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
|
||||
return b''
|
||||
|
||||
def get_database_hash(self, connection: device.Connection | None) -> bytes:
|
||||
assert connection
|
||||
|
||||
def get_database_hash(self, connection: device.Connection) -> bytes:
|
||||
m = b''.join(
|
||||
[
|
||||
self.get_attribute_data(attribute)
|
||||
|
||||
@@ -266,13 +266,13 @@ class HearingAccessService(gatt.TemplateService):
|
||||
# associate the lowest index as the current active preset at startup
|
||||
self.active_preset_index = sorted(self.preset_records.keys())[0]
|
||||
|
||||
@device.on('connection') # type: ignore
|
||||
@device.on(device.EVENT_CONNECTION)
|
||||
def on_connection(connection: Connection) -> None:
|
||||
@connection.on('disconnection') # type: ignore
|
||||
@connection.on(connection.EVENT_DISCONNECTION)
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.remove(connection)
|
||||
|
||||
@connection.on('pairing') # type: ignore
|
||||
@connection.on(connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
|
||||
@@ -335,9 +335,8 @@ class HearingAccessService(gatt.TemplateService):
|
||||
|
||||
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||
|
||||
def _on_read_active_preset_index(
|
||||
self, __connection__: Optional[Connection]
|
||||
) -> bytes:
|
||||
def _on_read_active_preset_index(self, connection: Connection) -> bytes:
|
||||
del connection # Unused
|
||||
return bytes([self.active_preset_index])
|
||||
|
||||
# TODO this need to be triggered when device is unbonded
|
||||
@@ -345,18 +344,13 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.preset_changed_operations_history_per_device.pop(addr)
|
||||
|
||||
async def _on_write_hearing_aid_preset_control_point(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
assert connection
|
||||
|
||||
opcode = HearingAidPresetControlPointOpcode(value[0])
|
||||
handler = getattr(self, '_on_' + opcode.name.lower())
|
||||
await handler(connection, value)
|
||||
|
||||
async def _on_read_presets_request(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
assert connection
|
||||
async def _on_read_presets_request(self, connection: Connection, value: bytes):
|
||||
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
|
||||
logging.warning(f'HAS require MTU >= 49: {connection}')
|
||||
|
||||
@@ -471,10 +465,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
for connection in self.currently_connected_clients:
|
||||
await self._preset_changed_operation(connection)
|
||||
|
||||
async def _on_write_preset_name(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
assert connection
|
||||
async def _on_write_preset_name(self, connection: Connection, value: bytes):
|
||||
|
||||
if self.read_presets_request_in_progress:
|
||||
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
||||
@@ -522,10 +513,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
for connection in self.currently_connected_clients:
|
||||
await self.notify_active_preset_for_connection(connection)
|
||||
|
||||
async def set_active_preset(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
) -> None:
|
||||
assert connection
|
||||
async def set_active_preset(self, connection: Connection, value: bytes) -> None:
|
||||
index = value[1]
|
||||
preset = self.preset_records.get(index, None)
|
||||
if (
|
||||
@@ -542,16 +530,11 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.active_preset_index = index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_active_preset(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
async def _on_set_active_preset(self, connection: Connection, value: bytes):
|
||||
await self.set_active_preset(connection, value)
|
||||
|
||||
async def set_next_or_previous_preset(
|
||||
self, connection: Optional[Connection], is_previous
|
||||
):
|
||||
async def set_next_or_previous_preset(self, connection: Connection, is_previous):
|
||||
'''Set the next or the previous preset as active'''
|
||||
assert connection
|
||||
|
||||
if self.active_preset_index == 0x00:
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
@@ -581,17 +564,17 @@ class HearingAccessService(gatt.TemplateService):
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_next_preset(
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
self, connection: Connection, __value__: bytes
|
||||
) -> None:
|
||||
await self.set_next_or_previous_preset(connection, False)
|
||||
|
||||
async def _on_set_previous_preset(
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
self, connection: Connection, __value__: bytes
|
||||
) -> None:
|
||||
await self.set_next_or_previous_preset(connection, True)
|
||||
|
||||
async def _on_set_active_preset_synchronized_locally(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
@@ -602,7 +585,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
# TODO (low priority) inform other server of the change
|
||||
|
||||
async def _on_set_next_preset_synchronized_locally(
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
self, connection: Connection, __value__: bytes
|
||||
):
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
@@ -613,7 +596,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
# TODO (low priority) inform other server of the change
|
||||
|
||||
async def _on_set_previous_preset_synchronized_locally(
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
self, connection: Connection, __value__: bytes
|
||||
):
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
|
||||
@@ -287,11 +287,8 @@ class MediaControlService(gatt.TemplateService):
|
||||
)
|
||||
|
||||
async def on_media_control_point(
|
||||
self, connection: Optional[device.Connection], data: bytes
|
||||
self, connection: device.Connection, data: bytes
|
||||
) -> None:
|
||||
if not connection:
|
||||
raise core.InvalidStateError()
|
||||
|
||||
opcode = MediaControlPointOpcode(data[0])
|
||||
|
||||
await connection.device.notify_subscriber(
|
||||
@@ -338,6 +335,12 @@ class MediaControlServiceProxy(
|
||||
'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
|
||||
}
|
||||
|
||||
EVENT_MEDIA_STATE = "media_state"
|
||||
EVENT_TRACK_CHANGED = "track_changed"
|
||||
EVENT_TRACK_TITLE = "track_title"
|
||||
EVENT_TRACK_DURATION = "track_duration"
|
||||
EVENT_TRACK_POSITION = "track_position"
|
||||
|
||||
media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
@@ -432,20 +435,20 @@ class MediaControlServiceProxy(
|
||||
self.media_control_point_notifications.put_nowait(data)
|
||||
|
||||
def _on_media_state(self, data: bytes) -> None:
|
||||
self.emit('media_state', MediaState(data[0]))
|
||||
self.emit(self.EVENT_MEDIA_STATE, MediaState(data[0]))
|
||||
|
||||
def _on_track_changed(self, data: bytes) -> None:
|
||||
del data
|
||||
self.emit('track_changed')
|
||||
self.emit(self.EVENT_TRACK_CHANGED)
|
||||
|
||||
def _on_track_title(self, data: bytes) -> None:
|
||||
self.emit('track_title', data.decode("utf-8"))
|
||||
self.emit(self.EVENT_TRACK_TITLE, data.decode("utf-8"))
|
||||
|
||||
def _on_track_duration(self, data: bytes) -> None:
|
||||
self.emit('track_duration', struct.unpack_from('<i', data)[0])
|
||||
self.emit(self.EVENT_TRACK_DURATION, struct.unpack_from('<i', data)[0])
|
||||
|
||||
def _on_track_position(self, data: bytes) -> None:
|
||||
self.emit('track_position', struct.unpack_from('<i', data)[0])
|
||||
self.emit(self.EVENT_TRACK_POSITION, struct.unpack_from('<i', data)[0])
|
||||
|
||||
|
||||
class GenericMediaControlServiceProxy(MediaControlServiceProxy):
|
||||
|
||||
@@ -91,6 +91,8 @@ class VolumeState:
|
||||
class VolumeControlService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_VOLUME_CONTROL_SERVICE
|
||||
|
||||
EVENT_VOLUME_STATE_CHANGE = "volume_state_change"
|
||||
|
||||
volume_state: gatt.Characteristic[bytes]
|
||||
volume_control_point: gatt.Characteristic[bytes]
|
||||
volume_flags: gatt.Characteristic[bytes]
|
||||
@@ -144,14 +146,12 @@ class VolumeControlService(gatt.TemplateService):
|
||||
included_services=list(included_services),
|
||||
)
|
||||
|
||||
def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes:
|
||||
def _on_read_volume_state(self, _connection: device.Connection) -> bytes:
|
||||
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
|
||||
|
||||
def _on_write_volume_control_point(
|
||||
self, connection: Optional[device.Connection], value: bytes
|
||||
self, connection: device.Connection, value: bytes
|
||||
) -> None:
|
||||
assert connection
|
||||
|
||||
opcode = VolumeControlPointOpcode(value[0])
|
||||
change_counter = value[1]
|
||||
|
||||
@@ -166,7 +166,7 @@ class VolumeControlService(gatt.TemplateService):
|
||||
'disconnection',
|
||||
connection.device.notify_subscribers(attribute=self.volume_state),
|
||||
)
|
||||
self.emit('volume_state_change')
|
||||
self.emit(self.EVENT_VOLUME_STATE_CHANGE)
|
||||
|
||||
def _on_relative_volume_down(self) -> bool:
|
||||
old_volume = self.volume_setting
|
||||
|
||||
@@ -86,7 +86,7 @@ class VolumeOffsetState:
|
||||
assert self.attribute is not None
|
||||
await connection.device.notify_subscribers(attribute=self.attribute)
|
||||
|
||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
||||
def on_read(self, _connection: Connection) -> bytes:
|
||||
return bytes(self)
|
||||
|
||||
|
||||
@@ -103,11 +103,10 @@ class VocsAudioLocation:
|
||||
audio_location = AudioLocation(struct.unpack('<I', data)[0])
|
||||
return cls(audio_location)
|
||||
|
||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
||||
def on_read(self, _connection: Connection) -> bytes:
|
||||
return bytes(self)
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
assert connection
|
||||
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||
assert self.attribute
|
||||
|
||||
self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
|
||||
@@ -118,8 +117,7 @@ class VocsAudioLocation:
|
||||
class VolumeOffsetControlPoint:
|
||||
volume_offset_state: VolumeOffsetState
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
assert connection
|
||||
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||
|
||||
opcode = value[0]
|
||||
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
|
||||
@@ -159,11 +157,10 @@ class AudioOutputDescription:
|
||||
def __bytes__(self) -> bytes:
|
||||
return self.audio_output_description.encode('utf-8')
|
||||
|
||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
||||
def on_read(self, _connection: Connection) -> bytes:
|
||||
return bytes(self)
|
||||
|
||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
assert connection
|
||||
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||
assert self.attribute
|
||||
|
||||
self.audio_output_description = value.decode('utf-8')
|
||||
|
||||
@@ -442,6 +442,9 @@ class RFCOMM_MCC_MSC:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DLC(utils.EventEmitter):
|
||||
EVENT_OPEN = "open"
|
||||
EVENT_CLOSE = "close"
|
||||
|
||||
class State(enum.IntEnum):
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
@@ -529,7 +532,7 @@ class DLC(utils.EventEmitter):
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
|
||||
self.change_state(DLC.State.CONNECTED)
|
||||
self.emit('open')
|
||||
self.emit(self.EVENT_OPEN)
|
||||
|
||||
def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
if self.state == DLC.State.CONNECTING:
|
||||
@@ -550,7 +553,7 @@ class DLC(utils.EventEmitter):
|
||||
self.disconnection_result.set_result(None)
|
||||
self.disconnection_result = None
|
||||
self.multiplexer.on_dlc_disconnection(self)
|
||||
self.emit('close')
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
else:
|
||||
logger.warning(
|
||||
color(
|
||||
@@ -733,7 +736,7 @@ class DLC(utils.EventEmitter):
|
||||
self.disconnection_result.cancel()
|
||||
self.disconnection_result = None
|
||||
self.change_state(DLC.State.RESET)
|
||||
self.emit('close')
|
||||
self.emit(self.EVENT_CLOSE)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
@@ -763,6 +766,8 @@ class Multiplexer(utils.EventEmitter):
|
||||
DISCONNECTED = 0x05
|
||||
RESET = 0x06
|
||||
|
||||
EVENT_DLC = "dlc"
|
||||
|
||||
connection_result: Optional[asyncio.Future]
|
||||
disconnection_result: Optional[asyncio.Future]
|
||||
open_result: Optional[asyncio.Future]
|
||||
@@ -785,7 +790,7 @@ class Multiplexer(utils.EventEmitter):
|
||||
# Become a sink for the L2CAP channel
|
||||
l2cap_channel.sink = self.on_pdu
|
||||
|
||||
l2cap_channel.on('close', self.on_l2cap_channel_close)
|
||||
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||
|
||||
def change_state(self, new_state: State) -> None:
|
||||
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
||||
@@ -901,7 +906,7 @@ class Multiplexer(utils.EventEmitter):
|
||||
self.dlcs[pn.dlci] = dlc
|
||||
|
||||
# Re-emit the handshake completion event
|
||||
dlc.on('open', lambda: self.emit('dlc', dlc))
|
||||
dlc.on(dlc.EVENT_OPEN, lambda: self.emit(self.EVENT_DLC, dlc))
|
||||
|
||||
# Respond to complete the handshake
|
||||
dlc.accept()
|
||||
@@ -1076,6 +1081,8 @@ class Client:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(utils.EventEmitter):
|
||||
EVENT_START = "start"
|
||||
|
||||
def __init__(
|
||||
self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
|
||||
) -> None:
|
||||
@@ -1122,7 +1129,9 @@ class Server(utils.EventEmitter):
|
||||
|
||||
def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||
l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
|
||||
l2cap_channel.on(
|
||||
l2cap_channel.EVENT_OPEN, lambda: self.on_l2cap_channel_open(l2cap_channel)
|
||||
)
|
||||
|
||||
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
||||
@@ -1130,10 +1139,10 @@ class Server(utils.EventEmitter):
|
||||
# Create a new multiplexer for the channel
|
||||
multiplexer = Multiplexer(l2cap_channel, Multiplexer.Role.RESPONDER)
|
||||
multiplexer.acceptor = self.accept_dlc
|
||||
multiplexer.on('dlc', self.on_dlc)
|
||||
multiplexer.on(multiplexer.EVENT_DLC, self.on_dlc)
|
||||
|
||||
# Notify
|
||||
self.emit('start', multiplexer)
|
||||
self.emit(self.EVENT_START, multiplexer)
|
||||
|
||||
def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
|
||||
return self.dlc_configs.get(channel_number)
|
||||
|
||||
@@ -724,12 +724,13 @@ class Session:
|
||||
self.is_responder = not self.is_initiator
|
||||
|
||||
# Listen for connection events
|
||||
connection.on('disconnection', self.on_disconnection)
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
connection.on(
|
||||
'connection_encryption_change', self.on_connection_encryption_change
|
||||
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
||||
self.on_connection_encryption_change,
|
||||
)
|
||||
connection.on(
|
||||
'connection_encryption_key_refresh',
|
||||
connection.EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH,
|
||||
self.on_connection_encryption_key_refresh,
|
||||
)
|
||||
|
||||
@@ -1310,12 +1311,15 @@ class Session:
|
||||
)
|
||||
|
||||
def on_disconnection(self, _: int) -> None:
|
||||
self.connection.remove_listener('disconnection', self.on_disconnection)
|
||||
self.connection.remove_listener(
|
||||
'connection_encryption_change', self.on_connection_encryption_change
|
||||
self.connection.EVENT_DISCONNECTION, self.on_disconnection
|
||||
)
|
||||
self.connection.remove_listener(
|
||||
'connection_encryption_key_refresh',
|
||||
self.connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
||||
self.on_connection_encryption_change,
|
||||
)
|
||||
self.connection.remove_listener(
|
||||
self.connection.EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH,
|
||||
self.on_connection_encryption_key_refresh,
|
||||
)
|
||||
self.manager.on_session_end(self)
|
||||
@@ -1376,8 +1380,10 @@ class Session:
|
||||
ediv=self.ltk_ediv,
|
||||
rand=self.ltk_rand,
|
||||
)
|
||||
if not self.peer_ltk:
|
||||
logger.error("peer_ltk is None")
|
||||
peer_ltk_key = PairingKeys.Key(
|
||||
value=self.peer_ltk,
|
||||
value=self.peer_ltk or b'',
|
||||
authenticated=authenticated,
|
||||
ediv=self.peer_ediv,
|
||||
rand=self.peer_rand,
|
||||
@@ -1962,7 +1968,7 @@ class Manager(utils.EventEmitter):
|
||||
def on_smp_security_request_command(
|
||||
self, connection: Connection, request: SMP_Security_Request_Command
|
||||
) -> None:
|
||||
connection.emit('security_request', request.auth_req)
|
||||
connection.emit(connection.EVENT_SECURITY_REQUEST, request.auth_req)
|
||||
|
||||
def on_smp_pdu(self, connection: Connection, pdu: bytes) -> None:
|
||||
# Parse the L2CAP payload into an SMP Command object
|
||||
|
||||
@@ -33,12 +33,6 @@ from bumble.avdtp import (
|
||||
from bumble.a2dp import (
|
||||
make_audio_sink_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
SbcMediaCodecInformation,
|
||||
)
|
||||
|
||||
|
||||
7
extras/android/BtBench/.gitignore
vendored
7
extras/android/BtBench/.gitignore
vendored
@@ -1,12 +1,7 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.idea/
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||
|
||||
@@ -6,13 +6,14 @@ import android.bluetooth.le.AdvertiseCallback
|
||||
import android.bluetooth.le.AdvertiseData
|
||||
import android.bluetooth.le.AdvertiseSettings
|
||||
import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.logging.Logger
|
||||
|
||||
private val Log = Logger.getLogger("btbench.advertiser")
|
||||
|
||||
class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCallback() {
|
||||
@SuppressLint("MissingPermission")
|
||||
@RequiresApi(34)
|
||||
fun start() {
|
||||
val advertiseSettingsBuilder = AdvertiseSettings.Builder()
|
||||
.setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
|
||||
|
||||
@@ -26,6 +26,8 @@ import android.bluetooth.BluetoothGattService
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothStatusCodes
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@@ -174,6 +176,7 @@ class GattServer(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
override fun run() {
|
||||
viewModel.running = true
|
||||
|
||||
|
||||
@@ -16,14 +16,9 @@ package com.github.google.bumble.btbench
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.le.AdvertiseCallback
|
||||
import android.bluetooth.le.AdvertiseData
|
||||
import android.bluetooth.le.AdvertiseSettings
|
||||
import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
|
||||
import android.os.Build
|
||||
import java.io.IOException
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.logging.Logger
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
private val Log = Logger.getLogger("btbench.l2cap-server")
|
||||
|
||||
@@ -34,6 +29,7 @@ class L2capServer(
|
||||
) : Mode {
|
||||
private var socketServer: SocketServer? = null
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun run() {
|
||||
// Advertise so that the peer can find us and connect.
|
||||
|
||||
@@ -13,11 +13,11 @@ dependencies = [
|
||||
"aiohttp ~= 3.8; platform_system!='Emscripten'",
|
||||
"appdirs >= 1.4; platform_system!='Emscripten'",
|
||||
"click >= 8.1.3; platform_system!='Emscripten'",
|
||||
"cryptography >= 39; platform_system!='Emscripten'",
|
||||
"cryptography >= 44.0.3; platform_system!='Emscripten'",
|
||||
# Pyodide bundles a version of cryptography that is built for wasm, which may not match the
|
||||
# versions available on PyPI. Relax the version requirement since it's better than being
|
||||
# completely unable to import the package in case of version mismatch.
|
||||
"cryptography >= 39.0; platform_system=='Emscripten'",
|
||||
"cryptography >= 44.0.3; platform_system=='Emscripten'",
|
||||
"grpcio >= 1.62.1; platform_system!='Emscripten'",
|
||||
"humanize >= 4.6.0; platform_system!='Emscripten'",
|
||||
"libusb1 >= 2.0.1; platform_system!='Emscripten'",
|
||||
@@ -26,7 +26,7 @@ dependencies = [
|
||||
"prompt_toolkit >= 3.0.16; platform_system!='Emscripten'",
|
||||
"prettytable >= 3.6.0; platform_system!='Emscripten'",
|
||||
"protobuf >= 3.12.4; platform_system!='Emscripten'",
|
||||
"pyee >= 8.2.2",
|
||||
"pyee >= 13.0.0",
|
||||
"pyserial-asyncio >= 0.5; platform_system!='Emscripten'",
|
||||
"pyserial >= 3.5; platform_system!='Emscripten'",
|
||||
"pyusb >= 1.2; platform_system!='Emscripten'",
|
||||
|
||||
@@ -136,9 +136,9 @@ async def test_characteristic_encoding():
|
||||
Characteristic.READABLE,
|
||||
123,
|
||||
)
|
||||
x = await c.read_value(None)
|
||||
x = await c.read_value(Mock())
|
||||
assert x == bytes([123])
|
||||
await c.write_value(None, bytes([122]))
|
||||
await c.write_value(Mock(), bytes([122]))
|
||||
assert c.value == 122
|
||||
|
||||
class FooProxy(CharacteristicProxy):
|
||||
@@ -334,7 +334,7 @@ async def test_CharacteristicAdapter() -> None:
|
||||
)
|
||||
|
||||
v = bytes([3, 4, 5])
|
||||
await c.write_value(None, v)
|
||||
await c.write_value(Mock(), v)
|
||||
assert c.value == v
|
||||
|
||||
# Simple delegated adapter
|
||||
@@ -342,11 +342,11 @@ async def test_CharacteristicAdapter() -> None:
|
||||
c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x))
|
||||
)
|
||||
|
||||
delegated_value = await delegated.read_value(None)
|
||||
delegated_value = await delegated.read_value(Mock())
|
||||
assert delegated_value == bytes(reversed(v))
|
||||
|
||||
delegated_value2 = bytes([3, 4, 5])
|
||||
await delegated.write_value(None, delegated_value2)
|
||||
await delegated.write_value(Mock(), delegated_value2)
|
||||
assert delegated.value == bytes(reversed(delegated_value2))
|
||||
|
||||
# Packed adapter with single element format
|
||||
@@ -355,10 +355,10 @@ async def test_CharacteristicAdapter() -> None:
|
||||
c.value = packed_value_ref
|
||||
packed = PackedCharacteristicAdapter(c, '>H')
|
||||
|
||||
packed_value_read = await packed.read_value(None)
|
||||
packed_value_read = await packed.read_value(Mock())
|
||||
assert packed_value_read == packed_value_bytes
|
||||
c.value = b''
|
||||
await packed.write_value(None, packed_value_bytes)
|
||||
await packed.write_value(Mock(), packed_value_bytes)
|
||||
assert packed.value == packed_value_ref
|
||||
|
||||
# Packed adapter with multi-element format
|
||||
@@ -368,10 +368,10 @@ async def test_CharacteristicAdapter() -> None:
|
||||
c.value = (v1, v2)
|
||||
packed_multi = PackedCharacteristicAdapter(c, '>HH')
|
||||
|
||||
packed_multi_read_value = await packed_multi.read_value(None)
|
||||
packed_multi_read_value = await packed_multi.read_value(Mock())
|
||||
assert packed_multi_read_value == packed_multi_value_bytes
|
||||
packed_multi.value = b''
|
||||
await packed_multi.write_value(None, packed_multi_value_bytes)
|
||||
await packed_multi.write_value(Mock(), packed_multi_value_bytes)
|
||||
assert packed_multi.value == (v1, v2)
|
||||
|
||||
# Mapped adapter
|
||||
@@ -382,10 +382,10 @@ async def test_CharacteristicAdapter() -> None:
|
||||
c.value = mapped
|
||||
packed_mapped = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2'))
|
||||
|
||||
packed_mapped_read_value = await packed_mapped.read_value(None)
|
||||
packed_mapped_read_value = await packed_mapped.read_value(Mock())
|
||||
assert packed_mapped_read_value == packed_mapped_value_bytes
|
||||
c.value = b''
|
||||
await packed_mapped.write_value(None, packed_mapped_value_bytes)
|
||||
await packed_mapped.write_value(Mock(), packed_mapped_value_bytes)
|
||||
assert packed_mapped.value == mapped
|
||||
|
||||
# UTF-8 adapter
|
||||
@@ -394,10 +394,10 @@ async def test_CharacteristicAdapter() -> None:
|
||||
c.value = string_value
|
||||
string_c = UTF8CharacteristicAdapter(c)
|
||||
|
||||
string_read_value = await string_c.read_value(None)
|
||||
string_read_value = await string_c.read_value(Mock())
|
||||
assert string_read_value == string_value_bytes
|
||||
c.value = b''
|
||||
await string_c.write_value(None, string_value_bytes)
|
||||
await string_c.write_value(Mock(), string_value_bytes)
|
||||
assert string_c.value == string_value
|
||||
|
||||
# Class adapter
|
||||
@@ -419,10 +419,10 @@ async def test_CharacteristicAdapter() -> None:
|
||||
c.value = class_value
|
||||
class_c = SerializableCharacteristicAdapter(c, BlaBla)
|
||||
|
||||
class_read_value = await class_c.read_value(None)
|
||||
class_read_value = await class_c.read_value(Mock())
|
||||
assert class_read_value == class_value_bytes
|
||||
class_c.value = b''
|
||||
await class_c.write_value(None, class_value_bytes)
|
||||
await class_c.write_value(Mock(), class_value_bytes)
|
||||
assert isinstance(class_c.value, BlaBla)
|
||||
assert class_c.value.a == class_value.a
|
||||
assert class_c.value.b == class_value.b
|
||||
@@ -436,10 +436,10 @@ async def test_CharacteristicAdapter() -> None:
|
||||
enum_value_bytes = int(enum_value).to_bytes(3, 'big')
|
||||
c.value = enum_value
|
||||
enum_c = EnumCharacteristicAdapter(c, MyEnum, 3, 'big')
|
||||
enum_read_value = await enum_c.read_value(None)
|
||||
enum_read_value = await enum_c.read_value(Mock())
|
||||
assert enum_read_value == enum_value_bytes
|
||||
enum_c.value = b''
|
||||
await enum_c.write_value(None, enum_value_bytes)
|
||||
await enum_c.write_value(Mock(), enum_value_bytes)
|
||||
assert isinstance(enum_c.value, MyEnum)
|
||||
assert enum_c.value == enum_value
|
||||
|
||||
@@ -1254,7 +1254,7 @@ Characteristic(handle=0x0005, end=0x0005, uuid=UUID-16:2A01 (Appearance), READ)
|
||||
Service(handle=0x0006, end=0x000D, uuid=UUID-16:1801 (Generic Attribute))
|
||||
CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=UUID-16:2A05 (Service Changed), INDICATE)
|
||||
Characteristic(handle=0x0008, end=0x0009, uuid=UUID-16:2A05 (Service Changed), INDICATE)
|
||||
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)
|
||||
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=<dynamic>)
|
||||
CharacteristicDeclaration(handle=0x000A, value_handle=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
|
||||
Characteristic(handle=0x000B, end=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
|
||||
CharacteristicDeclaration(handle=0x000C, value_handle=0x000D, uuid=UUID-16:2B2A (Database Hash), READ)
|
||||
@@ -1262,7 +1262,7 @@ Characteristic(handle=0x000D, end=0x000D, uuid=UUID-16:2B2A (Database Hash), REA
|
||||
Service(handle=0x000E, end=0x0011, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829)
|
||||
CharacteristicDeclaration(handle=0x000F, value_handle=0x0010, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
|
||||
Characteristic(handle=0x0010, end=0x0011, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
|
||||
Descriptor(handle=0x0011, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)"""
|
||||
Descriptor(handle=0x0011, type=UUID-16:2902 (Client Characteristic Configuration), value=<dynamic>)"""
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -21,17 +21,30 @@ from unittest import mock
|
||||
|
||||
from bumble import smp
|
||||
from bumble import pairing
|
||||
from bumble import crypto
|
||||
from bumble.crypto import EccKey, aes_cmac, ah, c1, f4, f5, f6, g2, h6, h7, s1
|
||||
from bumble.pairing import OobData, OobSharedData, LeRole
|
||||
from bumble.hci import Address
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Any
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# pylint: disable=invalid-name
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.fixture(
|
||||
scope="session", params=["bumble.crypto.builtin", "bumble.crypto.cryptography"]
|
||||
)
|
||||
def crypto_backend(request):
|
||||
backend = pytest.importorskip(request.param)
|
||||
with (
|
||||
mock.patch.object(crypto, "e", backend.e),
|
||||
mock.patch.object(crypto, "aes_cmac", backend.aes_cmac),
|
||||
mock.patch.object(crypto, "EccKey", backend.EccKey),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -40,7 +53,7 @@ def reversed_hex(hex_str: str) -> bytes:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_ecc():
|
||||
def test_ecc(crypto_backend):
|
||||
key = EccKey.generate()
|
||||
x = key.x
|
||||
y = key.y
|
||||
@@ -69,21 +82,17 @@ def test_ecc():
|
||||
)
|
||||
dhkey = 'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698'
|
||||
|
||||
key_a = EccKey.from_private_key_bytes(
|
||||
bytes.fromhex(private_A), bytes.fromhex(public_A_x), bytes.fromhex(public_A_y)
|
||||
)
|
||||
key_a = EccKey.from_private_key_bytes(bytes.fromhex(private_A))
|
||||
shared_key = key_a.dh(bytes.fromhex(public_B_x), bytes.fromhex(public_B_y))
|
||||
assert shared_key == bytes.fromhex(dhkey)
|
||||
|
||||
key_b = EccKey.from_private_key_bytes(
|
||||
bytes.fromhex(private_B), bytes.fromhex(public_B_x), bytes.fromhex(public_B_y)
|
||||
)
|
||||
key_b = EccKey.from_private_key_bytes(bytes.fromhex(private_B))
|
||||
shared_key = key_b.dh(bytes.fromhex(public_A_x), bytes.fromhex(public_A_y))
|
||||
assert shared_key == bytes.fromhex(dhkey)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_c1():
|
||||
def test_c1(crypto_backend):
|
||||
k = bytes(16)
|
||||
r = reversed_hex('5783D52156AD6F0E6388274EC6702EE0')
|
||||
pres = reversed_hex('05000800000302')
|
||||
@@ -97,7 +106,7 @@ def test_c1():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_s1():
|
||||
def test_s1(crypto_backend):
|
||||
k = bytes(16)
|
||||
r1 = reversed_hex('000F0E0D0C0B0A091122334455667788')
|
||||
r2 = reversed_hex('010203040506070899AABBCCDDEEFF00')
|
||||
@@ -106,7 +115,7 @@ def test_s1():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_aes_cmac():
|
||||
def test_aes_cmac(crypto_backend):
|
||||
m = b''
|
||||
k = bytes.fromhex('2b7e1516 28aed2a6 abf71588 09cf4f3c')
|
||||
cmac = aes_cmac(m, k)
|
||||
@@ -135,7 +144,7 @@ def test_aes_cmac():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_f4():
|
||||
def test_f4(crypto_backend):
|
||||
u = reversed_hex(
|
||||
'20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6'
|
||||
)
|
||||
@@ -149,7 +158,7 @@ def test_f4():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_f5():
|
||||
def test_f5(crypto_backend):
|
||||
w = reversed_hex(
|
||||
'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698'
|
||||
)
|
||||
@@ -163,7 +172,7 @@ def test_f5():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_f6():
|
||||
def test_f6(crypto_backend):
|
||||
n1 = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab')
|
||||
n2 = reversed_hex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf')
|
||||
mac_key = reversed_hex('2965f176 a1084a02 fd3f6a20 ce636e20')
|
||||
@@ -176,7 +185,7 @@ def test_f6():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_g2():
|
||||
def test_g2(crypto_backend):
|
||||
u = reversed_hex(
|
||||
'20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6'
|
||||
)
|
||||
@@ -190,21 +199,21 @@ def test_g2():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_h6():
|
||||
def test_h6(crypto_backend):
|
||||
KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
|
||||
KEY_ID = bytes.fromhex('6c656272')
|
||||
assert h6(KEY, KEY_ID) == reversed_hex('2d9ae102 e76dc91c e8d3a9e2 80b16399')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_h7():
|
||||
def test_h7(crypto_backend):
|
||||
KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
|
||||
SALT = bytes.fromhex('00000000 00000000 00000000 746D7031')
|
||||
assert h7(SALT, KEY) == reversed_hex('fb173597 c6a3c0ec d2998c2a 75a57011')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_ah():
|
||||
def test_ah(crypto_backend):
|
||||
irk = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
|
||||
prand = reversed_hex('708194')
|
||||
value = ah(irk, prand)
|
||||
@@ -213,7 +222,7 @@ def test_ah():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_oob_data():
|
||||
def test_oob_data(crypto_backend):
|
||||
oob_data = OobData(
|
||||
address=Address("F0:F1:F2:F3:F4:F5"),
|
||||
role=LeRole.BOTH_PERIPHERAL_PREFERRED,
|
||||
@@ -237,7 +246,7 @@ def test_oob_data():
|
||||
(True, '287ad379 dca40253 0a39f1f4 3047b835'),
|
||||
],
|
||||
)
|
||||
def test_ltk_to_link_key(ct2: bool, expected: str):
|
||||
def test_ltk_to_link_key(ct2: bool, expected: str, crypto_backend: Any):
|
||||
LTK = reversed_hex('368df9bc e3264b58 bd066c33 334fbf64')
|
||||
assert smp.Session.derive_link_key(LTK, ct2) == reversed_hex(expected)
|
||||
|
||||
@@ -250,7 +259,7 @@ def test_ltk_to_link_key(ct2: bool, expected: str):
|
||||
(True, 'e85e09eb 5eccb3e2 69418a13 3211bc79'),
|
||||
],
|
||||
)
|
||||
def test_link_key_to_ltk(ct2: bool, expected: str):
|
||||
def test_link_key_to_ltk(ct2: bool, expected: str, crypto_backend: Any):
|
||||
LINK_KEY = reversed_hex('05040302 01000908 07060504 03020100')
|
||||
assert smp.Session.derive_ltk(LINK_KEY, ct2) == reversed_hex(expected)
|
||||
|
||||
@@ -291,6 +300,7 @@ async def test_send_identity_address_command(
|
||||
public_address: Address,
|
||||
random_address: Address,
|
||||
expected_identity_address: Address,
|
||||
crypto_backend: Any,
|
||||
):
|
||||
device = Device()
|
||||
device.public_address = public_address
|
||||
@@ -304,19 +314,3 @@ async def test_send_identity_address_command(
|
||||
actual_command = mock_method.call_args.args[0]
|
||||
assert actual_command.addr_type == expected_identity_address.address_type
|
||||
assert actual_command.bd_addr == expected_identity_address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
test_ecc()
|
||||
test_c1()
|
||||
test_s1()
|
||||
test_aes_cmac()
|
||||
test_f4()
|
||||
test_f5()
|
||||
test_f6()
|
||||
test_g2()
|
||||
test_h6()
|
||||
test_h7()
|
||||
test_ah()
|
||||
test_oob_data()
|
||||
|
||||
Reference in New Issue
Block a user