├── .gitignore ├── README.md └── iap2 ├── __init__.py ├── __main__.py ├── carplay_bonjour.py ├── control_session_message ├── __init__.py ├── authentication.py ├── car_play.py ├── eap.py ├── identification.py ├── vehicle_status.py └── wifi.py ├── link_layer.py ├── mfi_auth_coprocessor.py ├── requirements.txt ├── tests ├── __init__.py ├── test_control_session_message.py ├── test_link_layer.py └── utils.py └── transport ├── __init__.py ├── bluetooth.py ├── usb_device.py └── usb_host.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | htmlcov/ 3 | .coverage 4 | .idea/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | More details on my [blogpost](https://wiomoc.de/misc/posts/mfi_iap.html) 2 | -------------------------------------------------------------------------------- /iap2/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["control_session_message", "mfi_auth_coprocessor", "link_layer"] 2 | 3 | import iap2.tests 4 | -------------------------------------------------------------------------------- /iap2/__main__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from iap2.control_session_message.wifi import AccessoryWiFiConfigurationInformation, \ 4 | RequestAccessoryWiFiConfigurationInformation, SecurityType 5 | from iap2.control_session_message.car_play import DeviceTransportIdentifierNotification, WirelessCarPlayUpdate, \ 6 | WirelessCarPlayStatus 7 | from iap2.control_session_message import read_csm, register_csm, write_csm, Uint16, Uint8 8 | from iap2.control_session_message.identification import IdentificationRejected, IdentificationAccepted, \ 9 | StartIdentification, IdentificationInformation, PowerProvidingCapability, ExternalAccessoryProtocol, MatchAction, \ 10 | BluetoothTransportComponent, VehicleInformationComponent, EngineType, VehicleStatusComponent, \ 11 | WirelessCarPlayTransportComponent 12 | from iap2.control_session_message.eap import StartExternalAccessoryProtocolSession, StopExternalAccessoryProtocolSession 13 | from iap2.control_session_message.vehicle_status import StartVehicleStatusUpdates, StopVehicleStatusUpdates, \ 14 | VehicleStatusUpdate 15 | from iap2.mfi_auth_coprocessor import read_certificate, generate_challenge_response 16 | from iap2.link_layer import IAP2Connection 17 | from iap2.transport.bluetooth import BluetoothTransport 18 | from iap2.control_session_message.authentication import RequestAuthenticationCertificate, AuthenticationCertificate, \ 19 | RequestAuthenticationChallengeResponse, AuthenticationResponse, AuthenticationSucceeded, AuthenticationFailed 20 | 21 | 22 | if __name__ == '__main__': 23 | loop = asyncio.get_event_loop() 24 | register_csm(RequestAuthenticationCertificate) 25 | register_csm(RequestAuthenticationChallengeResponse) 26 | register_csm(AuthenticationSucceeded) 27 | register_csm(AuthenticationFailed) 28 | 29 | register_csm(StartIdentification) 30 | register_csm(IdentificationAccepted) 31 | register_csm(IdentificationRejected) 32 | 33 | register_csm(StartVehicleStatusUpdates) 34 | register_csm(StopVehicleStatusUpdates) 35 | 36 | register_csm(DeviceTransportIdentifierNotification) 37 | register_csm(WirelessCarPlayUpdate) 38 | 39 | register_csm(RequestAccessoryWiFiConfigurationInformation) 40 | 41 | 42 | async def handle_auth(stream, cert): 43 | while True: 44 | incoming_message = await read_csm(stream) 45 | print(incoming_message) 46 | if isinstance(incoming_message, RequestAuthenticationCertificate): 47 | await write_csm(stream, AuthenticationCertificate(certificate=cert)) 48 | elif isinstance(incoming_message, RequestAuthenticationChallengeResponse): 49 | response = await loop.run_in_executor(None, 50 | lambda: generate_challenge_response(incoming_message.challenge)) 51 | await write_csm(stream, AuthenticationResponse(response=response)) 52 | elif isinstance(incoming_message, AuthenticationSucceeded): 53 | return 54 | else: 55 | raise Exception("auth failed") 56 | 57 | 58 | async def handle_identification(stream): 59 | def messages_ids(*messages): 60 | from struct import Struct 61 | word = Struct(">H") 62 | return b''.join((word.pack(m.CSM_MSG_ID) for m in messages)) 63 | 64 | while True: 65 | incoming_message = await read_csm(stream) 66 | print("incoming", str(incoming_message)) 67 | if isinstance(incoming_message, StartIdentification): 68 | identification = IdentificationInformation( 69 | name="raspberrypi", 70 | model_identifier="raspberrypi", 71 | manufacturer="wiomoc", 72 | serial_number="0122349", 73 | fireware_version="1.0.1", 74 | hardware_version="2.0", 75 | messages_sent_by_accessory=messages_ids(VehicleStatusUpdate, 76 | AccessoryWiFiConfigurationInformation), 77 | messages_received_from_accessory=messages_ids(StartExternalAccessoryProtocolSession, 78 | StopExternalAccessoryProtocolSession, 79 | StartVehicleStatusUpdates, 80 | StopVehicleStatusUpdates, 81 | WirelessCarPlayUpdate, 82 | DeviceTransportIdentifierNotification, 83 | RequestAccessoryWiFiConfigurationInformation), 84 | power_providing_capability=PowerProvidingCapability.NONE, 85 | maximum_current_drawn_from_device=Uint16(20), 86 | supported_external_accessory_protocol=[ExternalAccessoryProtocol( 87 | id=Uint8(1), 88 | name="de.wiomoc.test", 89 | match_action=MatchAction.NONE, 90 | )], 91 | current_language="de", 92 | supported_language=["de", "en"], 93 | app_match_team_id=None, 94 | bluetooth_transport_component=[BluetoothTransportComponent( 95 | id=Uint16(0), 96 | name="blue", 97 | supports_iap2_connection=True, 98 | bluetooth_transport_mac=b'\xB8\x27\xEB\x23\x6A\xF4' 99 | )], 100 | vehicle_information_component=VehicleInformationComponent( 101 | id=Uint16(0), 102 | name="Tesla Model X", 103 | engine_type=EngineType.ELECTRIC 104 | ), 105 | vehicle_status_component=VehicleStatusComponent( 106 | id=Uint16(0), 107 | name="Tesla Model X", 108 | range_warning=True 109 | ), 110 | wireless_car_play_transport_component=WirelessCarPlayTransportComponent( 111 | id=Uint16(1), 112 | name="raspberrypi", 113 | supports_iap2_connection=True, 114 | supports_car_play=True 115 | ) 116 | ) 117 | await write_csm(stream, identification) 118 | elif isinstance(incoming_message, IdentificationAccepted): 119 | return 120 | else: 121 | raise Exception("identification failed") 122 | 123 | 124 | async def main(): 125 | 126 | def on_connection(reader, writer): 127 | print(reader, writer) 128 | 129 | async def iap_handler(): 130 | cert = await loop.run_in_executor(None, lambda: read_certificate()) 131 | 132 | conn = IAP2Connection(writer, reader, loop, max_outgoing=4) 133 | conn.start() 134 | stream = conn.control_session 135 | await handle_auth(stream, cert) 136 | await handle_identification(stream) 137 | 138 | while True: 139 | incoming = await read_csm(stream) 140 | print(incoming) 141 | if isinstance(incoming, RequestAccessoryWiFiConfigurationInformation): 142 | info = AccessoryWiFiConfigurationInformation( 143 | ssid="teslamodelx", 144 | passphrase="testtest12", 145 | security_type=SecurityType.WPA_WPA2, 146 | channel=Uint8(10) 147 | ) 148 | await write_csm(stream, info) 149 | 150 | loop.create_task(iap_handler()) 151 | 152 | BluetoothTransport(on_connection, loop) 153 | 154 | 155 | loop.create_task(main()) 156 | loop.run_forever() 157 | -------------------------------------------------------------------------------- /iap2/carplay_bonjour.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | import avahi 3 | 4 | 5 | def start_service(device_id): 6 | bus = dbus.SystemBus() 7 | server = dbus.Interface(bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) 8 | 9 | group = dbus.Interface(bus.get_object(avahi.DBUS_NAME, server.EntryGroupNew()), 10 | avahi.DBUS_INTERFACE_ENTRY_GROUP) 11 | 12 | group.AddService(avahi.IF_UNSPEC, avahi.PROTO_INET, 0, "raspberrypi", "_airplay._tcp", '', '', 7000, 13 | avahi.string_array_to_txt_array([ 14 | f"deviceID={device_id}", 15 | "features=0x44540380,0x21", 16 | "model=raspberrypi", 17 | "srcvers=280.33.8", 18 | "flags=0x4"])) 19 | group.Commit() 20 | 21 | def on_service(interface, protocol, name, type, domain, flags): 22 | try: 23 | interface, protocol, name, type, domain, host, aprotocol, address, port, txt, flags = server.ResolveService( 24 | interface, protocol, name, type, domain, avahi.PROTO_INET, 0) 25 | print(txt) 26 | txt = [''.join((str(t) for t in txt_entry)) for txt_entry in txt] 27 | print(host, port, txt) 28 | 29 | mac_int = int(device_id.replace(":",""), 16) 30 | from urllib import request 31 | ip = "192.168.2.10" 32 | req = request.Request(f"http://{host}:{port}/ctrl-int/1/connect", headers={ 33 | "Host": host, 34 | "User-Agent": "AirPlay/280.33.8", 35 | "AirPlay-Receiver-Device-ID": str(mac_int), 36 | }) 37 | print(request.urlopen(req).read()) 38 | except: 39 | pass 40 | 41 | browser = server.ServiceBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_INET, '_carplay-ctrl._tcp', 'local', 0) 42 | bus.add_signal_receiver(on_service, "ItemNew", path=browser) 43 | 44 | -------------------------------------------------------------------------------- /iap2/control_session_message/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from struct import Struct 3 | from typing import get_type_hints, NewType, get_args, get_origin, Annotated, Dict, Type, Optional, Union 4 | from warnings import warn 5 | 6 | CSM_STRUCT = Struct(">HHH") 7 | CSM_PARAM_STRUCT = Struct(">HH") 8 | CSM_START = 0x4040 9 | 10 | _MESSAGE_TYPES: Dict[int, Type] = dict() 11 | 12 | 13 | def register_csm(csm_class): 14 | _MESSAGE_TYPES[csm_class.CSM_MSG_ID] = csm_class 15 | 16 | 17 | async def read_csm(reader): 18 | start, length, msg_id = CSM_STRUCT.unpack( 19 | await reader.readexactly(6)) 20 | if start != CSM_START: 21 | return 22 | payload = await reader.readexactly(length - 6) 23 | message_type = _MESSAGE_TYPES.get(msg_id) 24 | if message_type: 25 | message_instance = message_type.__new__(message_type) 26 | message_instance.csm_deserialize_params(payload) 27 | return message_instance 28 | else: 29 | return None 30 | 31 | 32 | async def write_csm(writer, message): 33 | writer.write(message.csm_serialize()) 34 | await writer.drain() 35 | 36 | 37 | Int8 = NewType("Int8", int) 38 | Int16 = NewType("Int16", int) 39 | Int32 = NewType("Int32", int) 40 | Int64 = NewType("Int64", int) 41 | Uint8 = NewType("Uint8", int) 42 | Uint16 = NewType("Uint16", int) 43 | Uint32 = NewType("Uint32", int) 44 | Uint64 = NewType("Uint64", int) 45 | NoneLike = NewType("None", Optional[bool]) 46 | 47 | 48 | def csm(msg_id: int): 49 | def decorator(clazz): 50 | def build_deserialize_params(handlers): 51 | def deserialize_params(self, payload): 52 | handlers_clone = handlers.copy() 53 | while len(payload) > 0: 54 | length, param_id = CSM_PARAM_STRUCT.unpack( 55 | payload[:4]) 56 | param_payload = payload[4:length] 57 | payload = payload[length:] 58 | if param_id in handlers_clone: 59 | _serializer, deserializer, name, is_list, _is_optional = handlers_clone.pop(param_id) 60 | value = deserializer(param_payload) 61 | if is_list: 62 | value_list = [] 63 | value_list.append(value) 64 | setattr(self, name, value_list) 65 | else: 66 | setattr(self, name, value) 67 | for _serializer, _deserializer, name, is_list, is_optional in handlers_clone.values(): 68 | if not is_optional and not is_list: 69 | raise ValueError(f"{name} is not optional") 70 | setattr(self, name, [] if is_list else None) 71 | 72 | return deserialize_params 73 | 74 | def build_serialize_params(handlers): 75 | def serialize_params(self): 76 | params_bytes = bytearray() 77 | for param_id, (serializer, _deserializer, name, is_list, is_optional) in handlers.items(): 78 | value = getattr(self, name) 79 | 80 | if value is not None: 81 | if is_list: 82 | if len(value) == 0: 83 | continue 84 | payload = b''.join((serializer(val) for val in value)) 85 | else: 86 | payload = serializer(value) 87 | params_bytes.extend(CSM_PARAM_STRUCT.pack( 88 | len(payload) + 4, param_id) + payload) 89 | elif not is_optional and not is_list: 90 | warn(f"{name} is not optional") 91 | return params_bytes 92 | 93 | return serialize_params 94 | 95 | def build_handlers(clazz): 96 | handlers = dict() 97 | hints = get_type_hints(clazz, include_extras=True).items() 98 | base_class = clazz.__base__ 99 | if base_class != object and base_class is not None: 100 | extended_hints = [] 101 | extended_hints.extend(get_type_hints(base_class, include_extras=True).items()) 102 | extended_hints.extend(hints) 103 | hints = extended_hints 104 | 105 | for param_id, (name, hint) in enumerate(hints): 106 | is_list = False 107 | is_optional = False 108 | if get_origin(hint) == Annotated: 109 | args = get_args(hint) 110 | param_id = args[1] 111 | hint = args[0] 112 | if get_origin(hint) == Union: 113 | args = get_args(hint) 114 | if args[1] == type(None): 115 | is_optional = True 116 | hint = args[0] 117 | if get_origin(hint) == list: 118 | is_list = True 119 | args = get_args(hint) 120 | hint = args[0] 121 | s = None 122 | if hint == bool: 123 | s = Struct(">?") 124 | if hint == Int8: 125 | s = Struct(">b") 126 | elif hint == Uint8: 127 | s = Struct(">B") 128 | elif hint == Int16: 129 | s = Struct(">h") 130 | elif hint == Uint16: 131 | s = Struct(">H") 132 | elif hint == Int32: 133 | s = Struct(">i") 134 | elif hint == Uint32: 135 | s = Struct(">I") 136 | elif hint == Int64: 137 | s = Struct(">q") 138 | elif hint == Uint64: 139 | s = Struct(">Q") 140 | 141 | serializer = None 142 | deserializer = None 143 | if s is not None: 144 | serializer = lambda val, s=s: s.pack(val) 145 | deserializer = lambda buffer, s=s: s.unpack(buffer)[0] if len(buffer) > 0 else None 146 | elif hint == NoneLike or hint == type(None): 147 | is_optional = True 148 | serializer = lambda val: b"" 149 | deserializer = lambda buffer: True 150 | elif issubclass(hint, IntEnum): 151 | serializer = lambda val: bytes([val.value]) 152 | deserializer = lambda buffer, hint=hint: hint(buffer[0]) 153 | elif hint == str: 154 | serializer = lambda val: val.encode("utf-8") + b"\0" 155 | deserializer = lambda buffer: buffer[:-1].decode("utf-8") 156 | elif hint == bytes: 157 | serializer = lambda val: val 158 | deserializer = lambda buffer: buffer 159 | elif isinstance(hint, type) and hint is not None: 160 | group_handlers = build_handlers(hint) 161 | serializer = build_serialize_params(group_handlers) 162 | 163 | def de(buffer, de_params=build_deserialize_params(group_handlers), param_class=hint): 164 | instance = param_class.__new__(param_class) 165 | de_params(instance, buffer) 166 | return instance 167 | 168 | deserializer = de 169 | 170 | if not serializer: 171 | raise TypeError("Invalid type for csm") 172 | 173 | handlers[param_id] = (serializer, deserializer, name, is_list, is_optional) 174 | return handlers 175 | 176 | message_handlers = build_handlers(clazz) 177 | 178 | setattr(clazz, "csm_deserialize_params", build_deserialize_params(message_handlers)) 179 | message_serialize_params = build_serialize_params(message_handlers) 180 | 181 | def serialize(self): 182 | params_bytes = message_serialize_params(self) 183 | header_bytes = CSM_STRUCT.pack( 184 | CSM_START, 185 | len(params_bytes) + 6, msg_id) 186 | return header_bytes + params_bytes 187 | 188 | setattr(clazz, "csm_serialize", serialize) 189 | setattr(clazz, "CSM_MSG_ID", msg_id) 190 | 191 | return clazz 192 | 193 | return decorator 194 | -------------------------------------------------------------------------------- /iap2/control_session_message/authentication.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from iap2.control_session_message import csm 4 | 5 | 6 | @csm(0xAA00) 7 | class RequestAuthenticationCertificate: 8 | pass 9 | 10 | 11 | @csm(0xAA01) 12 | @dataclass 13 | class AuthenticationCertificate: 14 | certificate: bytes 15 | 16 | 17 | @csm(0xAA02) 18 | class RequestAuthenticationChallengeResponse: 19 | challenge: bytes 20 | 21 | 22 | @csm(0xAA03) 23 | @dataclass 24 | class AuthenticationResponse: 25 | response: bytes 26 | 27 | 28 | @csm(0xAA04) 29 | class AuthenticationFailed: 30 | pass 31 | 32 | 33 | @csm(0xAA05) 34 | class AuthenticationSucceeded: 35 | pass 36 | -------------------------------------------------------------------------------- /iap2/control_session_message/car_play.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import IntEnum 3 | 4 | from iap2.control_session_message import csm, Uint16, Uint8 5 | 6 | 7 | @csm(0x4E0E) 8 | @dataclass 9 | class DeviceTransportIdentifierNotification: 10 | bluetooth_transport_id: str 11 | usb_transport_id: str 12 | 13 | 14 | class WirelessCarPlayStatus(IntEnum): 15 | UNAVAILABLE = 0 16 | AVAILABLE = 1 17 | 18 | 19 | @csm(0x4E0D) 20 | @dataclass 21 | class WirelessCarPlayUpdate: 22 | status: WirelessCarPlayStatus 23 | -------------------------------------------------------------------------------- /iap2/control_session_message/eap.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from iap2.control_session_message import csm, Uint16, Uint8 4 | 5 | 6 | @csm(0xEA00) 7 | class StartExternalAccessoryProtocolSession: 8 | protocol_id: Uint8 9 | session_id: Uint16 10 | 11 | 12 | @csm(0xEA01) 13 | class StopExternalAccessoryProtocolSession: 14 | session_id: Uint16 15 | 16 | 17 | class SessionStatus(IntEnum): 18 | OK = 0 19 | CLOSE = 1 20 | 21 | 22 | @csm(0xEA03) 23 | class StatusExternalAccessoryProtocolSession: 24 | session_id: Uint16 25 | status: SessionStatus 26 | -------------------------------------------------------------------------------- /iap2/control_session_message/identification.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from enum import IntEnum 3 | from typing import List, Annotated, Optional 4 | 5 | from iap2.control_session_message import csm, Uint16, Uint8, NoneLike 6 | 7 | 8 | class PowerProvidingCapability(IntEnum): 9 | NONE = 0 10 | RESERVED = 1 11 | ADVANCED = 2 12 | 13 | 14 | class MatchAction(IntEnum): 15 | NONE = 0 16 | SETTINGS_AND_PROMPT = 1 17 | SETTINGS_ONLY = 2 18 | 19 | 20 | @dataclass 21 | class ExternalAccessoryProtocol: 22 | id: Uint8 23 | name: str 24 | match_action: MatchAction 25 | native_transport_component_identifier: Optional[Uint16] = None 26 | 27 | 28 | @dataclass 29 | class TransportComponent: 30 | id: Uint16 31 | name: str 32 | supports_iap2_connection: NoneLike = None 33 | 34 | 35 | @dataclass 36 | class SerialTransportComponent(TransportComponent): 37 | pass 38 | 39 | 40 | @dataclass 41 | class BluetoothTransportComponent(TransportComponent): 42 | bluetooth_transport_mac: Annotated[bytes, 3] = None 43 | 44 | 45 | @dataclass 46 | class USBDeviceTransportComponent(TransportComponent): 47 | audio_sample_rate: Annotated[Optional[Uint8], 3] = None # Fixme 48 | 49 | 50 | @dataclass 51 | class WirelessCarPlayTransportComponent(TransportComponent): 52 | supports_car_play: Annotated[NoneLike, 4] = None 53 | 54 | 55 | @dataclass 56 | class USBHostTransportComponent(TransportComponent): 57 | car_play_interface_number: Annotated[Optional[Uint8], 3] = None 58 | 59 | 60 | class EngineType(IntEnum): 61 | GAS = 0 62 | DIESEL = 1 63 | ELECTRIC = 2 64 | CNG = 3 65 | 66 | 67 | @dataclass 68 | class VehicleInformationComponent: 69 | id: Uint16 70 | name: str 71 | engine_type: EngineType 72 | 73 | 74 | @dataclass 75 | class VehicleStatusComponent: 76 | id: Uint16 77 | name: str 78 | range: Annotated[NoneLike, 3] = None 79 | outside_temperature: Annotated[NoneLike, 4] = None 80 | range_warning: Annotated[NoneLike, 5] = None 81 | 82 | 83 | @csm(0x1D00) 84 | class StartIdentification: 85 | pass 86 | 87 | 88 | @csm(0x1D01) 89 | @dataclass 90 | class IdentificationInformation: 91 | name: str 92 | model_identifier: str 93 | manufacturer: str 94 | serial_number: str 95 | fireware_version: str 96 | hardware_version: str 97 | messages_sent_by_accessory: bytes 98 | messages_received_from_accessory: bytes 99 | power_providing_capability: PowerProvidingCapability 100 | maximum_current_drawn_from_device: Uint16 101 | supported_external_accessory_protocol: List[ExternalAccessoryProtocol] 102 | app_match_team_id: Optional[str] 103 | current_language: str 104 | supported_language: List[str] 105 | serial_transport_component: List[SerialTransportComponent] = field(default_factory=list) 106 | usb_device_transport_component: List[USBDeviceTransportComponent] = field(default_factory=list) 107 | usb_host_transport_component: List[USBHostTransportComponent] = field(default_factory=list) 108 | bluetooth_transport_component: List[BluetoothTransportComponent] = field(default_factory=list) 109 | vehicle_information_component: Annotated[Optional[VehicleInformationComponent], 20] = None 110 | vehicle_status_component: Annotated[Optional[VehicleStatusComponent], 21] = None 111 | wireless_car_play_transport_component: Annotated[Optional[WirelessCarPlayTransportComponent], 24] = None 112 | 113 | 114 | @csm(0x1D02) 115 | class IdentificationAccepted: 116 | pass 117 | 118 | 119 | @csm(0x1D03) 120 | @dataclass 121 | class IdentificationRejected: 122 | name: NoneLike 123 | model_identifier: NoneLike 124 | manufacturer: NoneLike 125 | serial_number: NoneLike 126 | fireware_version: NoneLike 127 | hardware_version: NoneLike 128 | messages_sent_by_accessory: NoneLike 129 | messages_received_from_accessory: NoneLike 130 | power_providing_capability: NoneLike 131 | maximum_current_drawn_from_device: NoneLike 132 | supported_external_accessory_protocol: NoneLike 133 | app_match_team_id: NoneLike 134 | current_language: NoneLike 135 | supported_language: NoneLike 136 | serial_transport_component: NoneLike 137 | usb_device_transport_component: NoneLike 138 | usb_host_transport_component: NoneLike 139 | bluetooth_transport_component: NoneLike 140 | vehicle_information_component: Annotated[NoneLike, 20] 141 | vehicle_status_component: Annotated[NoneLike, 21] 142 | wireless_car_play_transport_component: Annotated[NoneLike, 24] 143 | -------------------------------------------------------------------------------- /iap2/control_session_message/vehicle_status.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from iap2.control_session_message import csm, Uint16, Int16 4 | 5 | 6 | @csm(0xA100) 7 | class StartVehicleStatusUpdates: 8 | pass 9 | 10 | 11 | @csm(0xA101) 12 | class VehicleStatusUpdate: 13 | range: Annotated[Uint16, 3] 14 | outside_temperature: Annotated[Int16, 4] 15 | range_warning: Annotated[bool, 5] 16 | 17 | 18 | @csm(0xA102) 19 | class StopVehicleStatusUpdates: 20 | pass 21 | -------------------------------------------------------------------------------- /iap2/control_session_message/wifi.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import IntEnum 3 | from typing import Optional, Annotated 4 | 5 | from iap2.control_session_message import csm, Uint8 6 | 7 | 8 | @csm(0x5700) 9 | class RequestWiFiInformation: 10 | pass 11 | 12 | 13 | class WiFiRequestStatus(IntEnum): 14 | SUCCESS = 0 15 | USER_DECLINED = 1 16 | NET_WORK_INFORMATION_UNAVAILABLE = 2 17 | 18 | 19 | @csm(0x5701) 20 | class WiFiInformation: 21 | status: WiFiRequestStatus 22 | ssid: Optional[str] 23 | passphrase: Optional[str] 24 | 25 | 26 | @csm(0x5702) 27 | class RequestAccessoryWiFiConfigurationInformation: 28 | pass 29 | 30 | 31 | class SecurityType(IntEnum): 32 | NONE = 0 33 | WEP_NEW = 1 34 | WPA_WPA2 = 2 35 | 36 | 37 | @csm(0x5703) 38 | @dataclass 39 | class AccessoryWiFiConfigurationInformation: 40 | ssid: Annotated[Optional[str], 1] 41 | passphrase: Annotated[Optional[str], 2] 42 | security_type: Annotated[SecurityType, 3] 43 | channel: Annotated[Uint8, 4] 44 | -------------------------------------------------------------------------------- /iap2/link_layer.py: -------------------------------------------------------------------------------- 1 | __all__ = ["IAP2Connection", "IAP2Stream"] 2 | 3 | import asyncio 4 | from collections import namedtuple 5 | from dataclasses import dataclass 6 | from functools import reduce 7 | from struct import Struct 8 | from typing import ClassVar, List, Callable, Any 9 | 10 | CONTROL_SYN = 0x80 11 | CONTROL_ACK = 0x40 12 | CONTROL_EAK = 0x20 13 | CONTROL_RST = 0x10 14 | 15 | loop = asyncio.get_event_loop() 16 | 17 | 18 | @dataclass 19 | class LinkPacketHeader: 20 | struct: ClassVar = Struct(">HHBBBB") 21 | start: ClassVar = 0xFF5A 22 | length: int 23 | control: int 24 | seq: int 25 | ack: int 26 | session_id: int 27 | 28 | @staticmethod 29 | def from_bytes(header_bytes): 30 | if not check_checksum(header_bytes): 31 | return None 32 | (start, length, control, seq, ack, 33 | session_id) = LinkPacketHeader.struct.unpack(header_bytes[:-1]) 34 | if start != LinkPacketHeader.start: 35 | return None 36 | return LinkPacketHeader(length, control, seq, ack, session_id) 37 | 38 | def pack(self): 39 | header_bytes = LinkPacketHeader.struct.pack(LinkPacketHeader.start, 40 | self.length, self.control, 41 | self.seq, self.ack, 42 | self.session_id) 43 | return header_bytes + bytes([gen_checksum(header_bytes)]) 44 | 45 | 46 | def signed_add(a, b): 47 | return (a + b) & 0xff 48 | 49 | 50 | def gen_checksum(packet): 51 | return -reduce(signed_add, packet) & 0xff 52 | 53 | 54 | def check_checksum(packet): 55 | return reduce(signed_add, packet) == 0 56 | 57 | 58 | LSPSession = namedtuple('LSPSession', 'id type version') 59 | 60 | 61 | @dataclass 62 | class LinkSynchronizationPayload: 63 | struct: ClassVar = Struct(">BBHHHBB") 64 | version: ClassVar = 0x01 65 | max_outgoing: int 66 | max_len: int 67 | retransmission_timeout: int 68 | ack_timeout: int 69 | max_retransmissions: int 70 | max_ack: int 71 | sessions: list 72 | 73 | @staticmethod 74 | def from_bytes(payload): 75 | (version, max_outgoing, max_len, retransmission_timeout, ack_timeout, 76 | max_retransmissions, 77 | max_ack) = LinkSynchronizationPayload.struct.unpack(payload[:10]) 78 | if version != LinkSynchronizationPayload.version: 79 | return None 80 | 81 | sessions_bytes = payload[10:] 82 | sessions = [] 83 | for i in range(0, len(sessions_bytes), 3): 84 | sessions.append(LSPSession._make(sessions_bytes[i:i + 3])) 85 | return LinkSynchronizationPayload(max_outgoing, max_len, 86 | retransmission_timeout, ack_timeout, 87 | max_retransmissions, max_ack, 88 | sessions) 89 | 90 | def pack(self): 91 | payload = LinkSynchronizationPayload.struct.pack( 92 | LinkSynchronizationPayload.version, self.max_outgoing, 93 | self.max_len, self.retransmission_timeout, self.ack_timeout, 94 | self.max_retransmissions, self.max_ack) 95 | return payload + b''.join([bytes([*s]) for s in self.sessions]) 96 | 97 | 98 | STATE_DETECT_IAP2_SUPPORT = 0 99 | STATE_NEGOTIATE = 1 100 | STATE_NORMAL = 2 101 | STATE_DEAD = 3 102 | 103 | IAP2_MARKER = b'\xFF\x55\x02\x00\xEE\x10' 104 | EA_SESSION_ID_STRUCT = Struct(">H") 105 | 106 | 107 | class IAP2Packet: 108 | def __init__(self, data: bytes, psn: int = None, session_id: int = 0): 109 | self.psn = psn 110 | self.data = data 111 | self.session_id = session_id 112 | 113 | 114 | class IAP2Stream: 115 | def __init__(self, conn: "IAP2Connection", session_id: int, stream_id: int = None): 116 | self.conn = conn 117 | self.session_id = session_id 118 | self.stream_id = stream_id 119 | self.out_buffer = bytearray() 120 | self.in_buffer = bytearray() 121 | self.in_waiter_fut = None 122 | self.in_waiter_count = None 123 | if self.stream_id != None: 124 | self.out_buffer += EA_SESSION_ID_STRUCT.pack(self.stream_id) 125 | self.closed = False 126 | 127 | def write(self, data): 128 | if self.closed: 129 | raise IOError("closed") 130 | if len(self.out_buffer) == 0: 131 | self.out_buffer = data 132 | else: 133 | self.out_buffer += data 134 | while len(self.out_buffer) >= self.conn.lsp.max_len and self.conn.write_allowed_event.is_set(): 135 | self.conn.send_packet( 136 | IAP2Packet(self.out_buffer[:self.conn.lsp.max_len], 137 | session_id=self.session_id)) 138 | del self.out_buffer[:self.conn.lsp.max_len] 139 | 140 | async def drain(self): 141 | if self.closed: 142 | raise IOError("closed") 143 | if len(self.out_buffer) == 0: 144 | return 145 | await self.conn.write_allowed_event.wait() 146 | self.conn.send_packet( 147 | IAP2Packet(self.out_buffer, session_id=self.session_id)) 148 | self.out_buffer = bytearray() 149 | if self.stream_id != None: 150 | self.out_buffer += EA_SESSION_ID_STRUCT.pack(self.stream_id) 151 | 152 | def received_data(self, data): 153 | self.in_buffer += data 154 | if self.in_waiter_fut and self.in_waiter_count <= len(self.in_buffer): 155 | self.in_waiter_fut.set_result(True) 156 | self.in_waiter_fut = None 157 | 158 | async def readexactly(self, nbytes): 159 | if self.in_waiter_fut: 160 | return 161 | 162 | if len(self.in_buffer) < nbytes: 163 | if self.closed: 164 | raise asyncio.exceptions.IncompleteReadError(partial=self.in_buffer, expected=nbytes) 165 | self.in_waiter_count = nbytes 166 | fut = self.conn._loop.create_future() 167 | self.in_waiter_fut = fut 168 | await fut 169 | if self.closed: 170 | raise asyncio.exceptions.IncompleteReadError(partial=self.in_buffer, expected=nbytes) 171 | 172 | d = self.in_buffer[:nbytes] 173 | del self.in_buffer[:nbytes] 174 | return d 175 | 176 | def feed_eof(self): 177 | self.closed = True 178 | if self.in_waiter_fut: 179 | self.in_waiter_fut.set_result(True) 180 | self.in_waiter_fut = None 181 | 182 | 183 | class IAP2Connection: 184 | CONTROL_SESSION_ID = 10 185 | EA_SESSION_ID = 11 186 | 187 | def __init__(self, 188 | output: asyncio.StreamWriter, 189 | input: asyncio.StreamReader, 190 | loop: asyncio.AbstractEventLoop = asyncio.get_event_loop(), 191 | max_outgoing: int = 30, 192 | max_outgoing_delta: int = 0, 193 | ack_timeout=500, 194 | on_error: Callable[[Any], None] = None): 195 | self.on_error = on_error 196 | self.state = None 197 | self.lsp = LinkSynchronizationPayload( 198 | max_outgoing=max_outgoing, 199 | max_len=65535, 200 | retransmission_timeout=4000, 201 | ack_timeout=ack_timeout, 202 | max_retransmissions=4, 203 | max_ack=3, 204 | sessions=[ 205 | LSPSession(id=IAP2Connection.CONTROL_SESSION_ID, 206 | type=0, 207 | version=1), 208 | LSPSession(id=IAP2Connection.EA_SESSION_ID, type=2, version=1) 209 | ]) 210 | self._max_outgoing_delta = max_outgoing_delta 211 | self._sent_psn = 99 212 | self._last_sent_acknowledged_psn = None 213 | self._unack_packets = [] 214 | self._queued_packets = [] 215 | 216 | self._last_received_in_sequence_psn = 0 217 | self._last_acked_psn = None 218 | self._initial_received_psn = None 219 | self._received_out_of_sequence = [] 220 | self._cumulative_received = 0 221 | self._loop = loop 222 | self._output = output 223 | self._input = input 224 | self._send_ack_timer = None 225 | self._recv_ack_timer = None 226 | self.write_allowed_event = asyncio.Event() 227 | self.control_session = IAP2Stream(self, 228 | IAP2Connection.CONTROL_SESSION_ID) 229 | self.ea_streams = dict() 230 | self._receive_loop_task = None 231 | 232 | def create_ea_stream(self, stream_id): 233 | stream = IAP2Stream(self, IAP2Connection.EA_SESSION_ID, stream_id) 234 | self.ea_streams[stream_id] = stream 235 | return stream 236 | 237 | def start(self): 238 | if self.state: 239 | return 240 | self._receive_loop_task = self._loop.create_task(self._receive_loop()) 241 | self.state = STATE_DETECT_IAP2_SUPPORT 242 | self._send_detect_iap2_support() 243 | 244 | def close(self): 245 | self._input.feed_eof() 246 | 247 | def _write_packet(self, payload=None, seq=0, control=0, session_id=0): 248 | self._cumulative_received = 0 249 | if payload: 250 | length = len(payload) + 10 251 | else: 252 | length = 9 253 | header = LinkPacketHeader(control=control, 254 | length=length, 255 | seq=seq, 256 | ack=self._last_received_in_sequence_psn, 257 | session_id=session_id) 258 | print(">", header, payload) 259 | header_bytes = header.pack() 260 | if payload: 261 | self._output.write(header_bytes + payload + 262 | bytes([gen_checksum(payload)])) 263 | 264 | else: 265 | self._output.write(header_bytes) 266 | 267 | def _send_ack(self): 268 | self._write_packet(seq=self._sent_psn, control=CONTROL_ACK) 269 | 270 | def _send_eak(self, num): 271 | self._write_packet(bytes(num), seq=self._sent_psn, control=CONTROL_EAK) 272 | 273 | def _send_data(self, p): 274 | self._write_packet(p.data, 275 | seq=p.psn, 276 | control=CONTROL_ACK, 277 | session_id=p.session_id) 278 | 279 | def _send_detect_iap2_support(self): 280 | if self.state != STATE_DETECT_IAP2_SUPPORT: 281 | return 282 | self._output.write(IAP2_MARKER) 283 | self._loop.call_later(1, self._send_detect_iap2_support) 284 | 285 | def _send_negotiate(self): 286 | if self.state != STATE_NEGOTIATE: 287 | return 288 | lsp_bytes = self.lsp.pack() 289 | self._write_packet(lsp_bytes, self._sent_psn, CONTROL_SYN) 290 | self._loop.call_later(0.5, self._send_negotiate) 291 | 292 | async def _receive_loop(self): 293 | try: 294 | recv_marker = await self._input.readexactly(len(IAP2_MARKER)) 295 | if recv_marker != IAP2_MARKER: 296 | self._bailout("IAP2 not supported") 297 | return 298 | if hasattr(self._input, "reset"): 299 | self._input.reset() 300 | self.state = STATE_NEGOTIATE 301 | self._send_negotiate() 302 | while True: 303 | header_bytes = await self._input.readexactly(9) 304 | while True: 305 | if int(header_bytes[0]) << 8 | int( 306 | header_bytes[1]) == LinkPacketHeader.start: 307 | break 308 | header_bytes = header_bytes[1:] + await self._input.readexactly( 309 | 1) 310 | header = LinkPacketHeader.from_bytes(header_bytes) 311 | if not header: 312 | continue 313 | payload = None 314 | if header.length > 9: 315 | payload_with_checksum = await self._input.readexactly( 316 | header.length - 9) 317 | if not check_checksum(payload_with_checksum): 318 | continue 319 | payload = payload_with_checksum[:-1] 320 | print("<", header, payload) 321 | if hasattr(self._input, "reset"): 322 | self._input.reset() 323 | if (header.control & CONTROL_RST) != 0: 324 | self._bailout("device sent reset message") 325 | if (header.control & CONTROL_SYN) != 0: 326 | lsp = LinkSynchronizationPayload.from_bytes(payload) 327 | if not lsp: 328 | continue 329 | self._handle_syn(lsp, header.seq) 330 | if (header.control & CONTROL_ACK) != 0: 331 | self._cumulative_received += 1 332 | self._handle_ack(header.ack) 333 | if (header.control & CONTROL_EAK) != 0 and payload: 334 | self._handle_eak([int(x) for x in payload]) 335 | if (header.control & ~CONTROL_ACK) == 0 and payload != None: 336 | self._handle_data( 337 | IAP2Packet(payload, header.seq, header.session_id)) 338 | if self._cumulative_received >= self.lsp.max_ack: 339 | self._cumulative_received = 0 340 | self._last_acked_psn = self._last_received_in_sequence_psn 341 | self._send_ack() 342 | except asyncio.exceptions.IncompleteReadError: 343 | self._bailout(None) 344 | except Exception as e: 345 | self._bailout(e) 346 | 347 | def _bailout(self, error): 348 | if self.state == STATE_DEAD: 349 | return 350 | self._disarm_send_ack_timer() 351 | self._disarm_recv_ack_timer() 352 | self.state = STATE_DEAD 353 | try: 354 | self._output.close() 355 | except: 356 | pass 357 | try: 358 | self.control_session.feed_eof() 359 | for stream in self.ea_streams.values(): 360 | stream.feed_eof() 361 | except: 362 | pass 363 | if self._receive_loop_task: 364 | try: 365 | self._receive_loop_task.cancel() 366 | except: 367 | pass 368 | if error is not None and self.on_error: 369 | self.on_error(error) 370 | 371 | def send_packet(self, p: IAP2Packet): 372 | if distance(self._sent_psn, self._last_sent_acknowledged_psn 373 | ) > self.lsp.max_outgoing or self.state != STATE_NORMAL: 374 | self._queued_packets.append(p) 375 | self.write_allowed_event.clear() 376 | return 377 | 378 | self._sent_psn = signed_add(self._sent_psn, 1) 379 | p.counter = 0 380 | p.psn = self._sent_psn 381 | p.timeout = self._loop.time() + self.lsp.retransmission_timeout / 1000 382 | self._disarm_send_ack_timer() 383 | self._send_data(p) 384 | self._last_acked_psn = self._last_received_in_sequence_psn 385 | self._rearm_recv_ack_timer(p.timeout) 386 | self._unack_packets.append(p) 387 | 388 | def _handle_syn(self, lsp: LinkSynchronizationPayload, psn: int): 389 | if self.state != STATE_NEGOTIATE: 390 | return 391 | print("Device:", lsp) 392 | print("Accessory:", self.lsp) 393 | self.lsp = lsp 394 | self._last_received_in_sequence_psn = psn 395 | self._last_acked_psn = psn 396 | self._send_ack() 397 | 398 | def _handle_ack(self, num: int): 399 | if self.state == STATE_NEGOTIATE: 400 | self.state = STATE_NORMAL 401 | self.write_allowed_event.set() 402 | self._last_sent_acknowledged_psn = num 403 | 404 | while len(self._unack_packets) != 0: 405 | d = distance( 406 | self._unack_packets[0].psn, 407 | self._last_sent_acknowledged_psn) 408 | if 0 < d <= self.lsp.max_ack + 10: 409 | self._rearm_recv_ack_timer(self._unack_packets[0].timeout) 410 | break 411 | else: 412 | del self._unack_packets[0] 413 | else: 414 | self._disarm_recv_ack_timer() 415 | 416 | while distance(self._sent_psn, self._last_sent_acknowledged_psn 417 | ) < self.lsp.max_outgoing and len( 418 | self._queued_packets) > 0: 419 | self.send_packet(self._queued_packets.pop(0)) 420 | self.write_allowed_event.set() 421 | 422 | def _on_expect_ack_timer(self): 423 | if len(self._unack_packets) == 0 or self.state != STATE_NORMAL: 424 | return 425 | unack_packets = sorted(self._unack_packets, key=lambda x: x.timeout) 426 | p = unack_packets[0] 427 | p.timeout = self._loop.time() + self.lsp.retransmission_timeout / 1000 428 | p.counter += 1 429 | if p.counter == self.lsp.max_retransmissions: 430 | self._bailout(p) 431 | return 432 | self._send_data(p) 433 | self._rearm_recv_ack_timer(unack_packets[0 if len(unack_packets) == 1 else 1].timeout) 434 | 435 | def _handle_eak(self, nums: List[int]): 436 | if self.state != STATE_NORMAL: 437 | return 438 | for p in self._unack_packets: 439 | if p.psn in nums: 440 | p.counter += 1 441 | if p.counter == self.lsp.max_retransmissions: 442 | self._bailout(p) 443 | continue 444 | self._send_data(p) 445 | self._disarm_send_ack_timer() 446 | self._rearm_recv_ack_timer(p.timeout) 447 | 448 | def _on_send_ack_timer(self): 449 | if self.state != STATE_NORMAL: 450 | return 451 | self._last_acked_psn = self._last_received_in_sequence_psn 452 | self._send_ack() 453 | 454 | def _handle_data(self, p: IAP2Packet): 455 | d = distance(p.psn, self._last_received_in_sequence_psn) 456 | if d > self.lsp.max_outgoing + 10 or d == 0: 457 | self._send_ack() 458 | return 459 | 460 | if d > 1: 461 | self._received_out_of_sequence.append(p) 462 | if d >= self.lsp.max_outgoing: 463 | eak = [] 464 | x = self._last_received_in_sequence_psn 465 | while distance(p.psn, x) > 1: 466 | x = signed_add(x, 1) 467 | eak.append(x) 468 | self._disarm_send_ack_timer() 469 | self._send_eak(eak) 470 | return 471 | 472 | self._received_out_of_sequence.append(p) 473 | for pp in sorted(self._received_out_of_sequence, 474 | key=lambda x: distance( 475 | x.psn, self._last_received_in_sequence_psn)): 476 | if distance(pp.psn, self._last_received_in_sequence_psn) > 1: 477 | break 478 | self._received_data(pp) 479 | self._last_received_in_sequence_psn = pp.psn 480 | self._received_out_of_sequence.remove(pp) 481 | 482 | if distance(self._last_received_in_sequence_psn, self._last_acked_psn 483 | ) >= self.lsp.max_outgoing - self._max_outgoing_delta: 484 | self._disarm_send_ack_timer() 485 | self._last_acked_psn = self._last_received_in_sequence_psn 486 | self._send_ack() 487 | else: 488 | self._rearm_send_ack_timer() 489 | 490 | def _received_data(self, p: IAP2Packet): 491 | if p.session_id == IAP2Connection.CONTROL_SESSION_ID: 492 | self.control_session.received_data(p.data) 493 | elif p.session_id == IAP2Connection.EA_SESSION_ID and len(p.data) >= 2: 494 | stream_id = EA_SESSION_ID_STRUCT.unpack(p.data[:2])[0] 495 | stream = self.ea_streams.get(stream_id) 496 | if stream: 497 | stream.received_data(p.data[2:]) 498 | 499 | def _disarm_send_ack_timer(self): 500 | if self._send_ack_timer: 501 | self._send_ack_timer.cancel() 502 | self._send_ack_timer = None 503 | 504 | def _rearm_send_ack_timer(self): 505 | if self._send_ack_timer: 506 | self._send_ack_timer.cancel() 507 | self._send_ack_timer = self._loop.call_later(self.lsp.ack_timeout / 1000, 508 | self._on_send_ack_timer) 509 | 510 | def _disarm_recv_ack_timer(self): 511 | if self._recv_ack_timer: 512 | self._recv_ack_timer.cancel() 513 | self._recv_ack_timer = None 514 | 515 | def _rearm_recv_ack_timer(self, time): 516 | if self._recv_ack_timer: 517 | self._recv_ack_timer.cancel() 518 | self._recv_ack_timer = self._loop.call_at(time, self._on_expect_ack_timer) 519 | 520 | 521 | def distance(a: int, b: int): 522 | if b is None: 523 | return 0 524 | elif a >= b: 525 | return a - b 526 | else: 527 | return a + 256 - b 528 | -------------------------------------------------------------------------------- /iap2/mfi_auth_coprocessor.py: -------------------------------------------------------------------------------- 1 | import smbus2 2 | import time 3 | from struct import Struct 4 | 5 | Word = Struct(">H") 6 | # use bit-banged i2c, because coprocessor is to slow for native i2c 'dtoverlay=i2c-gpio,i2c_gpio_sda=2,i2c_gpio_scl=3,i2c_gpio_delay_us=50' 7 | bus = smbus2.SMBus("/dev/i2c-11") 8 | DEV_ADDR = 0x10 9 | 10 | 11 | def _read_i2c(addr, n): 12 | addr_msg = smbus2.i2c_msg.write(DEV_ADDR, bytes([addr])) 13 | read_msg = smbus2.i2c_msg.read(DEV_ADDR, n) 14 | for _ in range(5): 15 | try: 16 | bus.i2c_rdwr(addr_msg, read_msg) 17 | return bytes(read_msg) 18 | except OSError: 19 | time.sleep(0.0005) 20 | raise Exception("timeout") 21 | 22 | 23 | def _write_i2c(addr, arr): 24 | bus.write_i2c_block_data(DEV_ADDR, addr, [int(x) for x in arr]) 25 | 26 | 27 | def read_certificate(): 28 | size = Word.unpack(_read_i2c(0x30, 2))[0] # Read Accessory Certificate Data Length 29 | return _read_i2c(0x31, size) # Read Accessory Certificate Data 30 | 31 | 32 | def generate_challenge_response(challenge): 33 | _write_i2c(0x20, Word.pack(len(challenge))) # Write Challenge Data Length 34 | _write_i2c(0x21, challenge) # Write Challenge Data 35 | bus.write_byte_data(DEV_ADDR, 0x10, 0x01) # Write Authentication Control and Status = Start 36 | time.sleep(0.01) 37 | for _ in range(10): 38 | try: 39 | if bus.read_byte_data(DEV_ADDR, 0x10) == 0x10: # Read Authentication Control and Status == Success 40 | break 41 | except OSError: 42 | pass 43 | time.sleep(0.1) 44 | else: 45 | raise Exception("timeout") 46 | size = Word.unpack(_read_i2c(0x11, 2))[0] # Read Challenge Response Data Length 47 | return _read_i2c(0x12, size) # Read Challenge Response Data 48 | 49 | 50 | if __name__ == "__main__": 51 | print("CERT", read_certificate().hex()) 52 | print("CERT", generate_challenge_response(b"12211213131231231231").hex()) 53 | -------------------------------------------------------------------------------- /iap2/requirements.txt: -------------------------------------------------------------------------------- 1 | libusb1==2.0.1 2 | functionfs==0.8.0 3 | dbus-python==1.2.18 4 | hid==1.0.4 5 | smbus2==0.4.1 6 | zeroconf==0.38.1 7 | -------------------------------------------------------------------------------- /iap2/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import iap2.tests.test_control_session_message 2 | import iap2.tests.test_link_layer 3 | -------------------------------------------------------------------------------- /iap2/tests/test_control_session_message.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dataclasses import dataclass 3 | from typing import Annotated, List 4 | 5 | from iap2.control_session_message.identification import MatchAction, ExternalAccessoryProtocol, PowerProvidingCapability, IdentificationInformation, \ 6 | BluetoothTransportComponent 7 | from iap2.control_session_message import Uint16, register_csm, Uint8, csm, read_csm 8 | from iap2.tests.utils import gen_pipe 9 | 10 | 11 | class TestControlSessionMessage(unittest.TestCase): 12 | @dataclass 13 | @csm(0xAA01) 14 | class Test: 15 | @dataclass 16 | class TestGroup: 17 | num: Uint8 = None 18 | 19 | first: str = None 20 | second: Annotated[str, 100] = None 21 | group: Annotated[List[TestGroup], 101] = None 22 | 23 | def test_roundtrip(self): 24 | import asyncio 25 | loop = asyncio.get_event_loop() 26 | register_csm(IdentificationInformation) 27 | 28 | async def test(): 29 | reader, writer = await gen_pipe(loop) 30 | expected_csm = IdentificationInformation( 31 | name="raspberrypi", 32 | model_identifier="pi", 33 | manufacturer="wiomoc", 34 | serial_number="0122349", 35 | fireware_version="1.0.1", 36 | hardware_version="2.0", 37 | messages_sent_by_accessory=b'', 38 | messages_received_from_accessory=b'123123', 39 | power_providing_capability=PowerProvidingCapability.NONE, 40 | maximum_current_drawn_from_device=Uint16(20), 41 | supported_external_accessory_protocol=[ExternalAccessoryProtocol( 42 | id=Uint8(1), 43 | name="de.wiomoc.test", 44 | match_action=MatchAction.NONE, 45 | )], 46 | current_language="de", 47 | supported_language=["de"], #, "en"], 48 | app_match_team_id="", 49 | bluetooth_transport_component=[BluetoothTransportComponent( 50 | id=Uint16(0), 51 | name="blue", 52 | supports_iap2_connection=None, 53 | bluetooth_transport_mac=b'\xB8\x27\xEB\x23\x6A\xF4' 54 | )] 55 | ) 56 | 57 | async def write(): 58 | writer.write(expected_csm.csm_serialize()) 59 | await writer.drain() 60 | 61 | loop.create_task(write()) 62 | actual_csm = await read_csm(reader) 63 | print(actual_csm) 64 | self.assertEqual(expected_csm, actual_csm) 65 | 66 | loop.run_until_complete(test()) 67 | -------------------------------------------------------------------------------- /iap2/tests/test_link_layer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | from unittest.mock import Mock, call 4 | import random 5 | 6 | from iap2.link_layer import CONTROL_SYN, CONTROL_ACK, LinkSynchronizationPayload, LinkPacketHeader, IAP2_MARKER, \ 7 | STATE_NORMAL, gen_checksum, IAP2Connection, LSPSession 8 | from iap2.tests.utils import gen_pipe 9 | 10 | 11 | class TestLinkPacketHeader(unittest.TestCase): 12 | def test_round_trip(self): 13 | header_bytes = b'\xffZ\x00\x1a\x80+\x00\x00\xe2' 14 | header = LinkPacketHeader.from_bytes(header_bytes) 15 | self.assertEqual( 16 | header, 17 | LinkPacketHeader(length=26, 18 | control=128, 19 | seq=43, 20 | ack=0, 21 | session_id=0)) 22 | repacked_header_bytes = header.pack() 23 | self.assertEqual(repacked_header_bytes, header_bytes) 24 | 25 | def test_invalid_check(self): 26 | header_bytes = b'\xffZ\x00\x1a\x80+\x00\x00\xe1' 27 | header = LinkPacketHeader.from_bytes(header_bytes) 28 | self.assertIsNone(header) 29 | 30 | def test_invalid_start(self): 31 | header_bytes = b'\xfeZ\x00\x1a\x80+\x00\x00\xe2' 32 | header = LinkPacketHeader.from_bytes(header_bytes) 33 | self.assertIsNone(header) 34 | 35 | 36 | class TestLinkSynchronizationPayload(unittest.TestCase): 37 | def test_round_trip(self): 38 | payload = b'\x01\x05\x10\x00\x04\x0B\x00\x17\x03\x03\x0A\x00\x01\x0B\x02\x01' 39 | lsp = LinkSynchronizationPayload.from_bytes(payload) 40 | self.assertEqual( 41 | lsp, 42 | LinkSynchronizationPayload(max_outgoing=5, 43 | max_len=4096, 44 | retransmission_timeout=1035, 45 | ack_timeout=23, 46 | max_retransmissions=3, 47 | max_ack=3, 48 | sessions=[ 49 | LSPSession(id=10, type=0, 50 | version=1), 51 | LSPSession(id=11, type=2, version=1) 52 | ])) 53 | repacked_payload = lsp.pack() 54 | self.assertEqual(repacked_payload, payload) 55 | 56 | 57 | class TestIAP2Connection(unittest.TestCase): 58 | class TestPacket: 59 | def __init__(self): 60 | self.id = random.random() 61 | 62 | def test_normal(self): 63 | conn = IAP2Connection(input=None, output=None, max_outgoing=3) 64 | conn.state = STATE_NORMAL 65 | conn._sent_psn = 199 66 | conn._rearm_send_ack_timer = Mock() 67 | conn._received_data = Mock() 68 | conn._send_data = Mock() 69 | conn._disarm_send_ack_timer = Mock() 70 | conn._rearm_recv_ack_timer = Mock() 71 | conn._last_acked_psn = 99 72 | conn._last_received_in_sequence_psn = 99 73 | 74 | p1 = TestIAP2Connection.TestPacket() 75 | p1.psn = 100 76 | 77 | conn._handle_data(p1) 78 | 79 | conn._received_data.assert_called_with(p1) 80 | conn._rearm_send_ack_timer.assert_called() 81 | 82 | p2 = TestIAP2Connection.TestPacket() 83 | 84 | conn.send_packet(p2) 85 | 86 | conn._send_data.assert_called_with(p2) 87 | self.assertEqual(conn._last_received_in_sequence_psn, p1.psn) 88 | conn._disarm_send_ack_timer.assert_called() 89 | conn._rearm_recv_ack_timer.assert_called() 90 | self.assertEqual(p2.psn, 200) 91 | self.assertEqual(conn._unack_packets, [p2]) 92 | 93 | p3 = TestIAP2Connection.TestPacket() 94 | p3.psn = 101 95 | 96 | conn._handle_data(p3) 97 | 98 | conn._received_data.assert_called_with(p3) 99 | conn._rearm_send_ack_timer.assert_called() 100 | 101 | conn._disarm_recv_ack_timer = Mock() 102 | 103 | conn._handle_ack(200) 104 | 105 | conn._disarm_recv_ack_timer.assert_called() 106 | self.assertEqual(conn._unack_packets, []) 107 | 108 | p4 = TestIAP2Connection.TestPacket() 109 | p4.psn = 102 110 | 111 | conn._handle_data(p4) 112 | 113 | conn._handle_ack(200) 114 | conn._disarm_recv_ack_timer.assert_called() 115 | 116 | p5 = TestIAP2Connection.TestPacket() 117 | 118 | conn.send_packet(p5) 119 | 120 | conn._send_data.assert_called_with(p5) 121 | self.assertEqual(conn._last_received_in_sequence_psn, p4.psn) 122 | conn._disarm_send_ack_timer.assert_called() 123 | conn._rearm_recv_ack_timer.assert_called() 124 | self.assertEqual(p5.psn, 201) 125 | 126 | def test_ack_timeout(self): 127 | conn = IAP2Connection(input=None, output=None, max_outgoing=3, ack_timeout=20) 128 | conn.state = STATE_NORMAL 129 | conn._sent_psn = 199 130 | conn._rearm_recv_ack_timer = Mock() 131 | conn._send_data = Mock() 132 | conn._disarm_send_ack_timer = Mock() 133 | conn._last_received_in_sequence_psn = 99 134 | conn._disarm_recv_ack_timer = Mock() 135 | 136 | p1 = TestIAP2Connection.TestPacket() 137 | 138 | conn.send_packet(p1) 139 | 140 | conn._send_data.assert_called_with(p1) 141 | conn._rearm_recv_ack_timer.assert_called() 142 | 143 | p2 = TestIAP2Connection.TestPacket() 144 | 145 | conn.send_packet(p2) 146 | 147 | conn._send_data.assert_called_with(p2) 148 | conn._rearm_recv_ack_timer.assert_called() 149 | 150 | conn._on_expect_ack_timer() 151 | conn._send_data.assert_called_with(p1) 152 | 153 | conn._rearm_recv_ack_timer.assert_called() 154 | conn._on_expect_ack_timer() 155 | conn._send_data.assert_called_with(p2) 156 | 157 | conn._handle_ack(p1.psn) 158 | self.assertEqual(conn._unack_packets, [p2]) 159 | 160 | conn._on_expect_ack_timer() 161 | conn._send_data.assert_called_with(p2) 162 | 163 | conn._handle_ack(p2.psn) 164 | 165 | conn._disarm_recv_ack_timer.asser_called() 166 | 167 | def test_buffer(self): 168 | conn = IAP2Connection(input=None, output=None, max_outgoing=2) 169 | conn.state = STATE_NORMAL 170 | conn._sent_psn = 199 171 | conn._last_sent_acknowledged_psn = 198 172 | conn._rearm_recv_ack_timer = Mock() 173 | conn._disarm_send_ack_timer = Mock() 174 | conn._disarm_recv_ack_timer = Mock() 175 | conn._send_data = Mock() 176 | 177 | p1 = TestIAP2Connection.TestPacket() 178 | conn.send_packet(p1) 179 | conn._send_data.reset_mock() 180 | conn._rearm_recv_ack_timer.assert_called() 181 | 182 | p2 = TestIAP2Connection.TestPacket() 183 | conn.send_packet(p2) 184 | conn._send_data.assert_called_with(p2) 185 | conn._send_data.reset_mock() 186 | conn._rearm_recv_ack_timer.assert_called() 187 | 188 | p3 = TestIAP2Connection.TestPacket() 189 | conn.send_packet(p3) 190 | conn._send_data.assert_not_called() 191 | 192 | conn._handle_ack(p2.psn) 193 | conn._send_data.assert_called_with(p3) 194 | 195 | def test_cumulative(self): 196 | conn = IAP2Connection(input=None, output=None, max_outgoing=2) 197 | conn.state = STATE_NORMAL 198 | conn._last_acked_psn = 99 199 | conn._last_received_in_sequence_psn = 99 200 | conn._rearm_send_ack_timer = Mock() 201 | conn._received_data = Mock() 202 | 203 | p1 = TestIAP2Connection.TestPacket() 204 | p1.psn = 100 205 | 206 | conn._handle_data(p1) 207 | 208 | conn._received_data.assert_called_with(p1) 209 | conn._rearm_send_ack_timer.assert_called() 210 | 211 | p2 = TestIAP2Connection.TestPacket() 212 | p2.psn = 101 213 | conn._disarm_send_ack_timer = Mock() 214 | conn._send_ack = Mock() 215 | 216 | conn._handle_data(p2) 217 | 218 | conn._received_data.assert_called_with(p2) 219 | conn._send_ack.assert_called() 220 | self.assertEqual(conn._last_received_in_sequence_psn, p2.psn) 221 | conn._disarm_send_ack_timer.assert_called() 222 | 223 | def test_out_of_order(self): 224 | conn = IAP2Connection(input=None, output=None, max_outgoing=10) 225 | conn.state = STATE_NORMAL 226 | conn._last_acked_psn = 102 227 | conn._last_received_in_sequence_psn = 102 228 | conn._rearm_send_ack_timer = Mock() 229 | conn._received_data = Mock() 230 | 231 | p1 = TestIAP2Connection.TestPacket() 232 | p1.psn = 103 233 | 234 | conn._handle_data(p1) 235 | 236 | conn._received_data.assert_called_with(p1) 237 | conn._rearm_send_ack_timer.assert_called() 238 | 239 | p2 = TestIAP2Connection.TestPacket() 240 | p2.psn = 107 241 | 242 | conn._handle_data(p2) 243 | 244 | p3 = TestIAP2Connection.TestPacket() 245 | p3.psn = 105 246 | 247 | conn._handle_data(p3) 248 | 249 | p4 = TestIAP2Connection.TestPacket() 250 | p4.psn = 104 251 | conn._disarm_send_ack_timer = Mock() 252 | conn._send_ack = Mock() 253 | 254 | conn._handle_data(p4) 255 | 256 | conn._received_data.assert_has_calls([call(p4), call(p3)]) 257 | self.assertEqual(conn._last_received_in_sequence_psn, p3.psn) 258 | conn._rearm_send_ack_timer.assert_called() 259 | 260 | def test_out_of_order_overflow(self): 261 | conn = IAP2Connection(input=None, output=None, max_outgoing=3) 262 | conn.state = STATE_NORMAL 263 | conn._last_acked_psn = 253 264 | conn._last_received_in_sequence_psn = 253 265 | conn._rearm_send_ack_timer = Mock() 266 | conn._received_data = Mock() 267 | 268 | p1 = TestIAP2Connection.TestPacket() 269 | p1.psn = 254 270 | 271 | conn._handle_data(p1) 272 | 273 | conn._received_data.assert_called_with(p1) 274 | conn._rearm_send_ack_timer.assert_called() 275 | 276 | p2 = TestIAP2Connection.TestPacket() 277 | p2.psn = 0 278 | 279 | conn._handle_data(p2) 280 | 281 | p3 = TestIAP2Connection.TestPacket() 282 | p3.psn = 255 283 | conn._disarm_send_ack_timer = Mock() 284 | conn._send_ack = Mock() 285 | 286 | conn._handle_data(p3) 287 | 288 | conn._received_data.assert_has_calls([call(p3), call(p2)]) 289 | conn._send_ack.assert_called() 290 | self.assertEqual(conn._last_received_in_sequence_psn, p2.psn) 291 | conn._disarm_send_ack_timer.assert_called() 292 | 293 | def test_eak(self): 294 | conn = IAP2Connection(input=None, output=None, max_outgoing=2) 295 | conn.state = STATE_NORMAL 296 | conn._last_received_in_sequence_psn = 102 297 | conn._last_acked_psn = 102 298 | conn._rearm_send_ack_timer = Mock() 299 | conn._received_data = Mock() 300 | 301 | p1 = TestIAP2Connection.TestPacket() 302 | p1.psn = 103 303 | 304 | conn._handle_data(p1) 305 | 306 | conn._received_data.assert_called_with(p1) 307 | conn._rearm_send_ack_timer.assert_called() 308 | 309 | p2 = TestIAP2Connection.TestPacket() 310 | p2.psn = 105 311 | conn._disarm_send_ack_timer = Mock() 312 | conn._send_ack = Mock() 313 | conn._send_eak = Mock() 314 | 315 | conn._handle_data(p2) 316 | 317 | conn._disarm_send_ack_timer.assert_called() 318 | conn._send_eak.assert_called_with([104]) 319 | self.assertEqual(conn._last_received_in_sequence_psn, p1.psn) 320 | 321 | def test_eak_overflow(self): 322 | conn = IAP2Connection(input=None, output=None, max_outgoing=2) 323 | conn.state = STATE_NORMAL 324 | conn._last_received_in_sequence_psn = 254 325 | conn._last_acked_psn = 254 326 | conn._rearm_send_ack_timer = Mock() 327 | conn._received_data = Mock() 328 | 329 | p1 = TestIAP2Connection.TestPacket() 330 | p1.psn = 255 331 | 332 | conn._handle_data(p1) 333 | 334 | conn._received_data.assert_called_with(p1) 335 | conn._rearm_send_ack_timer.assert_called() 336 | 337 | p2 = TestIAP2Connection.TestPacket() 338 | p2.psn = 1 339 | conn._disarm_send_ack_timer = Mock() 340 | conn._send_ack = Mock() 341 | conn._send_eak = Mock() 342 | 343 | conn._handle_data(p2) 344 | 345 | conn._disarm_send_ack_timer.assert_called() 346 | conn._send_eak.assert_called_with([0]) 347 | self.assertEqual(conn._last_received_in_sequence_psn, p1.psn) 348 | 349 | 350 | def async_test(f): 351 | def wrapper(*args, **kwargs): 352 | coro = asyncio.coroutine(f) 353 | future = coro(*args, **kwargs) 354 | loop = asyncio.get_event_loop() 355 | loop.run_until_complete(future) 356 | 357 | return wrapper 358 | 359 | 360 | class SmokeTest(unittest.TestCase): 361 | @async_test 362 | async def test(self): 363 | loop = asyncio.get_event_loop() 364 | input_rx, input_tx = await gen_pipe(loop) 365 | output_rx, output_tx = await gen_pipe(loop) 366 | conn = IAP2Connection( 367 | output_tx, 368 | input_rx, 369 | loop, 370 | max_outgoing=8, 371 | ) 372 | conn.start() 373 | 374 | init = await output_rx.readexactly(6) 375 | self.assertEqual(init, IAP2_MARKER) 376 | input_tx.write(IAP2_MARKER) 377 | await input_tx.drain() 378 | 379 | header_bytes = await output_rx.readexactly(9) 380 | header = LinkPacketHeader.from_bytes(header_bytes) 381 | lsp_bytes = (await output_rx.readexactly(header.length - 9))[:-1] 382 | lsp = LinkSynchronizationPayload.from_bytes(lsp_bytes) 383 | self.assertEqual(lsp, conn.lsp) 384 | 385 | header.control = CONTROL_SYN | CONTROL_ACK 386 | header.ack = header.seq 387 | header.seq = 200 388 | 389 | input_tx.write(b'\x30') # Fault 390 | input_tx.write(header.pack()) 391 | input_tx.write(lsp_bytes) 392 | input_tx.write(bytes([gen_checksum(lsp_bytes)])) 393 | await input_tx.drain() 394 | await asyncio.sleep(1) 395 | self.assertEqual(conn.state, STATE_NORMAL) 396 | header_bytes = await output_rx.readexactly(9) 397 | header = LinkPacketHeader.from_bytes(header_bytes) 398 | 399 | header.ack = 50 400 | header.seq = 201 401 | header.control = 0 402 | header.session_id = 10 403 | 404 | payload = b"hello world!" 405 | header.length = 10 + len(payload) 406 | input_tx.write(header.pack()) 407 | input_tx.write(payload) 408 | input_tx.write(bytes([gen_checksum(payload)])) 409 | 410 | await asyncio.sleep(1) 411 | 412 | header_bytes = await output_rx.readexactly(9) 413 | header = LinkPacketHeader.from_bytes(header_bytes) 414 | 415 | stream = conn.create_ea_stream(0x42) 416 | stream.write(b'life') 417 | await stream.drain() 418 | 419 | header_bytes = await output_rx.readexactly(9) 420 | header = LinkPacketHeader.from_bytes(header_bytes) 421 | payload = (await output_rx.readexactly(header.length - 9))[:-1] 422 | self.assertEqual(payload, b'\x00\x42life') 423 | 424 | header.ack = 51 425 | header.seq = 202 426 | header.control = 0 427 | header.session_id = 11 428 | payload = b"\x00\x42abc" 429 | header.length = 10 + len(payload) 430 | input_tx.write(header.pack()) 431 | input_tx.write(payload) 432 | input_tx.write(bytes([gen_checksum(payload)])) 433 | await input_tx.drain() 434 | self.assertEqual(await stream.readexactly(3), b'abc') 435 | 436 | control_session = conn.control_session 437 | control_session.write(b'pong') 438 | await control_session.drain() 439 | header_bytes = await output_rx.readexactly(9) 440 | header = LinkPacketHeader.from_bytes(header_bytes) 441 | payload = (await output_rx.readexactly(header.length - 9))[:-1] 442 | self.assertEqual(payload, b'pong') 443 | self.assertEqual(await control_session.readexactly(5), b'hello') 444 | loop.call_soon(lambda: input_rx.feed_eof()) 445 | self.assertEqual(await control_session.readexactly(5), b' worl') 446 | 447 | with self.assertRaises(asyncio.exceptions.IncompleteReadError): 448 | await control_session.readexactly(5) 449 | 450 | with self.assertRaises(asyncio.exceptions.IncompleteReadError): 451 | await stream.readexactly(5) 452 | 453 | @async_test 454 | async def test_bailout(self): 455 | on_error = Mock() 456 | loop = asyncio.get_event_loop() 457 | input_rx, input_tx = await gen_pipe(loop) 458 | output_rx, output_tx = await gen_pipe(loop) 459 | conn = IAP2Connection( 460 | output_tx, 461 | input_rx, 462 | loop, 463 | max_outgoing=8, 464 | on_error=on_error 465 | ) 466 | conn.start() 467 | 468 | init = await output_rx.readexactly(6) 469 | self.assertEqual(init, IAP2_MARKER) 470 | input_tx.write(IAP2_MARKER) 471 | await input_tx.drain() 472 | 473 | exception = Exception("42") 474 | input_rx.set_exception(exception) 475 | await asyncio.sleep(1) 476 | 477 | on_error.assert_called_with(exception) 478 | -------------------------------------------------------------------------------- /iap2/tests/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | 5 | async def gen_pipe(loop): 6 | read_fd, write_fd = os.pipe() 7 | reader = asyncio.StreamReader() 8 | read_protocol = asyncio.StreamReaderProtocol(reader) 9 | read_transport, _ = await loop.connect_read_pipe(lambda: read_protocol, 10 | os.fdopen(read_fd)) 11 | write_protocol = asyncio.StreamReaderProtocol(asyncio.StreamReader()) 12 | write_transport, _ = await loop.connect_write_pipe( 13 | lambda: write_protocol, os.fdopen(write_fd, 'w')) 14 | writer = asyncio.StreamWriter(write_transport, write_protocol, None, loop) 15 | return reader, writer 16 | -------------------------------------------------------------------------------- /iap2/transport/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["bluetooth", "usb_host", "usb_device"] 2 | -------------------------------------------------------------------------------- /iap2/transport/bluetooth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # sudo hciconfig hci0 inqdata 0e0972617370626572727970693031020a00091002006b1d460237051107FFCACADEAFDECADEDEFACADE00000000 3 | # 0d0950455547454f2d3633383400110600000000DECAFADEDECADEAFDECACAFF1107D31FBF505D572797A24041CD484388EC 4 | import asyncio 5 | import socket 6 | import threading 7 | import time 8 | 9 | import dbus 10 | import dbus.service 11 | from dbus.mainloop.glib import DBusGMainLoop 12 | from gi.repository import GLib 13 | 14 | import iap2.carplay_bonjour as carplay_bonjour 15 | 16 | BUS_NAME = 'org.bluez' 17 | PROFILE_INTERFACE = 'org.bluez.Profile1' 18 | AGENT_INTERFACE = 'org.bluez.Agent1' 19 | 20 | 21 | def ask(prompt): 22 | try: 23 | return raw_input(prompt) 24 | except: 25 | return input(prompt) 26 | 27 | 28 | # def set_trusted(path): 29 | # props = dbus.Interface(bus.get_object(BUS_NAME, path), 30 | # "org.freedesktop.DBus.Properties") 31 | # props.Set("org.bluez.Device1", "Trusted", True) 32 | 33 | 34 | class Rejected(dbus.DBusException): 35 | _dbus_error_name = "org.bluez.Error.Rejected" 36 | 37 | 38 | class Agent(dbus.service.Object): 39 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="") 40 | def Release(self): 41 | print("Release") 42 | 43 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="") 44 | def AuthorizeService(self, device, uuid): 45 | print("AuthorizeService (%s, %s)" % (device, uuid)) 46 | 47 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="s") 48 | def RequestPinCode(self, device): 49 | print("RequestPinCode (%s)" % (device)) 50 | return ask("Enter PIN Code: ") 51 | 52 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="u") 53 | def RequestPasskey(self, device): 54 | print("RequestPasskey (%s)" % (device)) 55 | passkey = ask("Enter passkey: ") 56 | return dbus.UInt32(passkey) 57 | 58 | @dbus.service.method(AGENT_INTERFACE, in_signature="ouq", out_signature="") 59 | def DisplayPasskey(self, device, passkey, entered): 60 | print("DisplayPasskey (%s, %06u entered %u)" % 61 | (device, passkey, entered)) 62 | 63 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="") 64 | def DisplayPinCode(self, device, pincode): 65 | print("DisplayPinCode (%s, %s)" % (device, pincode)) 66 | 67 | @dbus.service.method(AGENT_INTERFACE, in_signature="ou", out_signature="") 68 | def RequestConfirmation(self, device, passkey): 69 | print("RequestConfirmation (%s, %06d)" % (device, passkey)) 70 | time.sleep(2) 71 | 72 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="") 73 | def RequestAuthorization(self, device): 74 | print("RequestAuthorization (%s)" % (device)) 75 | 76 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="") 77 | def Cancel(self): 78 | print("Cancel") 79 | 80 | 81 | class IAPProfile(dbus.service.Object): 82 | def __init__(self, bus, path, on_connection, loop): 83 | dbus.service.Object.__init__(self, bus, path) 84 | self.__path = path 85 | self.on_connection = on_connection 86 | self._loop = loop 87 | 88 | @dbus.service.method(dbus_interface=PROFILE_INTERFACE, in_signature='') 89 | def Release(self): 90 | print("Release") 91 | 92 | @dbus.service.method(dbus_interface=PROFILE_INTERFACE, 93 | in_signature='oha{sv}') 94 | def NewConnection(self, device, fd, opts): 95 | print("new bluetooth connection", device, self.__path) 96 | raw_fd = fd.take() 97 | s = socket.fromfd(raw_fd, socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) 98 | s.settimeout(None) 99 | 100 | async def on_connection(): 101 | reader, writer = await asyncio.open_connection(sock=s) 102 | self.on_connection(reader, writer) 103 | 104 | self._loop.call_soon_threadsafe(lambda: asyncio.create_task(on_connection())) 105 | 106 | @dbus.service.method(dbus_interface=PROFILE_INTERFACE, in_signature='o') 107 | def RequestDisconnection(self, device): 108 | print("Disconnect") 109 | 110 | 111 | IAP_SERVER_UUID = "00000000-deca-fade-deca-deafdecacaff" 112 | IAP_CLIENT_UUID = "00000000-deca-fade-deca-deafdecacafe" 113 | CHANNEL = 3 114 | IAP_RECORD = f""" 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | """ 173 | 174 | 175 | class BluetoothTransport: 176 | def __init__(self, on_connection, loop): 177 | 178 | self._glib_loop = GLib.MainLoop() 179 | self._loop = loop 180 | self.on_connection = on_connection 181 | threading.Thread(target=self._bluetooth_thread).start() 182 | 183 | def close(self): 184 | if self._glib_loop: 185 | self._glib_loop.quit() 186 | self._glib_loop = None 187 | 188 | def _bluetooth_thread(self): 189 | DBusGMainLoop(set_as_default=True) 190 | carplay_bonjour.start_service("DC:A6:32:63:35:20") 191 | bus = dbus.SystemBus() 192 | bluez = bus.get_object(BUS_NAME, '/org/bluez') 193 | iapServerProfile = IAPProfile(bus, "/org/bluez/iap_server", self.on_connection, self._loop) 194 | iapClientProfile = IAPProfile(bus, "/org/bluez/iap_client", self.on_connection, self._loop) 195 | profileManager = dbus.Interface(bluez, 'org.bluez.ProfileManager1') 196 | profileManager.RegisterProfile(iapServerProfile, IAP_SERVER_UUID, { 197 | 'Role': 'server', 198 | 'Channel': dbus.types.UInt16(CHANNEL), 199 | 'ServiceRecord': IAP_RECORD, 200 | 'RequireAuthentication': False, 201 | 'RequireAuthorization': False 202 | }) 203 | 204 | profileManager.RegisterProfile(iapClientProfile, IAP_CLIENT_UUID, { 205 | 'Role': 'client', 206 | 'AutoConnect': True 207 | }) 208 | 209 | agent = Agent(bus, "/org/bluez/iap_agent") 210 | agent_manager = dbus.Interface(bluez, "org.bluez.AgentManager1") 211 | agent_manager.RegisterAgent(agent, "KeyboardDisplay") 212 | agent_manager.RequestDefaultAgent(agent) 213 | adapter = dbus.Interface(bus.get_object(BUS_NAME, "/org/bluez/hci0"), dbus.PROPERTIES_IFACE) 214 | adapter.Set("org.bluez.Adapter1", 'Powered', True) 215 | adapter.Set("org.bluez.Adapter1", 'Discoverable', True) 216 | adapter.Set("org.bluez.Adapter1", 'Pairable', True) 217 | if self._glib_loop: 218 | self._glib_loop.run() 219 | profileManager.UnregisterProfile(iapServerProfile) 220 | profileManager.UnregisterProfile(iapClientProfile) 221 | agent_manager.UnregisterAgent(agent) 222 | -------------------------------------------------------------------------------- /iap2/transport/usb_device.py: -------------------------------------------------------------------------------- 1 | import usb1 2 | import threading 3 | import queue 4 | import hid 5 | import asyncio 6 | 7 | 8 | class BaseUSBDeviceHandler: 9 | def __init__(self): 10 | context = usb1.USBContext() 11 | context.open() 12 | 13 | loop = asyncio.get_event_loop() 14 | 15 | def added_cb(fd, events): 16 | if events & 1: 17 | loop.add_reader(fd, context.handleEventsTimeout) 18 | if events & 4: 19 | loop.add_writer(fd, context.handleEventsTimeout) 20 | 21 | def removed_cb(fd): 22 | loop.remove_reader(fd) 23 | loop.remove_writer(fd) 24 | 25 | for fd, events in context.getPollFDList(): 26 | added_cb(fd, events) 27 | 28 | context._USBContext__has_pollfd_finalizer = True 29 | context.setPollFDNotifiers(added_cb=added_cb, removed_cb=removed_cb) 30 | context.setDebug(usb1.LOG_LEVEL_DEBUG) 31 | 32 | def hotplug_callback(context, device, event): 33 | print("event", repr(device), event) 34 | if event == usb1.HOTPLUG_EVENT_DEVICE_ARRIVED: 35 | loop.create_task(self._handle_new_device(device)) 36 | 37 | context.hotplugRegisterCallback(callback=hotplug_callback, vendor_id=0x5ac) 38 | self._context = context 39 | 40 | def close(self): 41 | self._context.setPollFDNotifiers(None, None) 42 | self._context.close() 43 | 44 | 45 | class USBRoleSwitchHandler(BaseUSBDeviceHandler): 46 | def __init__(self, after_role_switch, car_play=False): 47 | super().__init__() 48 | self._after_role_switch = after_role_switch 49 | self._car_play = car_play 50 | 51 | async def _handle_new_device(self, device): 52 | open_device = device.open() 53 | 54 | await _usb_control_transfer( 55 | open_device, usb1.RECIPIENT_DEVICE | usb1.LIBUSB_REQUEST_TYPE_VENDOR, 56 | 0x51, 1 if self._car_play else 0, 0, 0) 57 | self._after_role_switch() 58 | 59 | 60 | class USBDeviceTransport(BaseUSBDeviceHandler): 61 | def __init__(self, on_connection): 62 | super().__init__() 63 | self._on_connection = on_connection 64 | 65 | async def _handle_new_device(self, device): 66 | CONFIGURATION_VALUE = 2 67 | configs = [ 68 | c for c in device.iterConfigurations() 69 | if c.getConfigurationValue() == CONFIGURATION_VALUE 70 | ] 71 | config = configs[0] 72 | 73 | interfaces = [ 74 | s for i in config.iterInterfaces() for s in i.iterSettings() 75 | if s.getClassTuple() == (3, 0) 76 | ] 77 | interface_setting = interfaces[0] 78 | interface_num = interface_setting.getNumber() 79 | endpoints = list(interface_setting.iterEndpoints()) 80 | endpoint = endpoints[0] 81 | print(interface_num) 82 | 83 | open_device = device.open() 84 | open_device.setConfiguration(CONFIGURATION_VALUE) 85 | 86 | report_descriptor = await _usb_control_transfer( 87 | open_device, usb1.ENDPOINT_IN | usb1.RECIPIENT_INTERFACE, 88 | usb1.REQUEST_GET_DESCRIPTOR, (usb1.DT_REPORT << 8), interface_num, 89 | 2000) 90 | print(report_descriptor) 91 | output_report_ids = [] 92 | input_report_ids = dict() 93 | report_id = None 94 | report_count = None 95 | for tag, item in get_descriptor_items(report_descriptor): 96 | if tag == 0x84: 97 | report_id = item[0] 98 | elif tag == 0x94: 99 | report_count = int(item[0]) if len( 100 | item) == 1 else int(item[1]) << 8 | int(item[0]) 101 | elif tag == 0x90: 102 | output_report_ids.append((report_id, report_count)) 103 | elif tag == 0x80: 104 | input_report_ids[report_id] = report_count 105 | 106 | output_report_ids.sort(key=lambda a:a[0]) 107 | 108 | hid_device = hid.Device(vid=device.getVendorID(), 109 | pid=device.getProductID(), 110 | serial=device.getSerialNumber()) 111 | w = HIDWriter(hid_device, output_report_ids) 112 | r = HIDReader(hid_device, input_report_ids) 113 | self._on_connection(w, r) 114 | 115 | 116 | def _usb_control_transfer(device, request_type, request, value, index, length): 117 | transfer = device.getTransfer() 118 | future = asyncio.get_event_loop().create_future() 119 | 120 | def cb(transfer): 121 | status = transfer.getStatus() 122 | if status == usb1.LIBUSB_TRANSFER_COMPLETED: 123 | future.set_result(transfer.getBuffer()[:transfer.getActualLength()]) 124 | else: 125 | future.set_exception(IOError(f"USB error {status}")) 126 | 127 | transfer.setControl(request_type, request, value, index, length, callback=cb) 128 | transfer.submit() 129 | return future 130 | 131 | 132 | def get_descriptor_items(descriptor): 133 | i = 0 134 | while i < len(descriptor): 135 | tag = descriptor[i] 136 | if tag == 0xFE: 137 | size = descriptor[i + 1] 138 | tag = descriptor[i + 2] 139 | i += 3 140 | data = descriptor[i:i + size] 141 | i += size 142 | else: 143 | size = (1 << (tag & 3)) >> 1 144 | i += 1 145 | data = descriptor[i:i + size] 146 | tag &= 0xFC 147 | i += size 148 | yield (tag, data) 149 | 150 | 151 | LCB_CONTINUATION = 1 152 | LCB_MORE_TO_FOLLOW = 2 153 | 154 | 155 | class HIDReader: 156 | def __init__(self, hid_device, input_report_ids): 157 | self._loop = asyncio.get_event_loop() 158 | self._hid_device = hid_device 159 | self._input_report_ids = input_report_ids 160 | self._read_buffer_semaphore = threading.Semaphore(value=3) 161 | self._read_buffer_queue = asyncio.Queue() 162 | self._max_len = max(input_report_ids.values()) 163 | self.eof = False 164 | self._read_buffer = None 165 | threading.Thread(target=self._read_loop).start() 166 | 167 | async def readexactly(self, nbytes): 168 | if not self._read_buffer or len(self._read_buffer) == 0: 169 | self._read_buffer_semaphore.release() 170 | if self.eof: 171 | raise asyncio.exceptions.IncompleteReadError(partial=self._read_buffer, expected=nbytes) 172 | self._read_buffer = await self._read_buffer_queue.get() 173 | 174 | if len(self._read_buffer) >= nbytes: 175 | b = self._read_buffer[:nbytes] 176 | self._read_buffer = self._read_buffer[nbytes:] 177 | return b 178 | else: 179 | b = bytearray(self._read_buffer) 180 | while len(b) <= nbytes: 181 | self._read_buffer_semaphore.release() 182 | if self.eof: 183 | raise asyncio.exceptions.IncompleteReadError(partial=self._read_buffer, expected=nbytes) 184 | b.extend(await self._read_buffer_queue.get()) 185 | self._read_buffer = b[nbytes:] 186 | return b[:nbytes] 187 | 188 | def reset(self): 189 | self._read_buffer = None 190 | 191 | def _read_loop(self): 192 | buf = bytearray() 193 | try: 194 | while not self.eof: 195 | self._read_buffer_semaphore.acquire() 196 | while not self.eof: 197 | report = self._hid_device.read(self._max_len + 2) 198 | if len(report) <= 2: 199 | continue 200 | lcb = report[1] 201 | payload = report[2:] 202 | if (lcb & LCB_CONTINUATION) == 0: 203 | buf.clear() 204 | if (lcb & LCB_MORE_TO_FOLLOW) != 0: 205 | buf.extend(payload) 206 | else: 207 | if len(buf) > 0: 208 | buf.extend(payload) 209 | packet = bytes(buf) 210 | buf.clear() 211 | else: 212 | packet = payload 213 | self._loop.call_soon_threadsafe( 214 | lambda: self._read_buffer_queue.put_nowait(packet)) 215 | break 216 | except: 217 | self.feed_eof() 218 | 219 | def feed_eof(self): 220 | self.eof = True 221 | self._read_buffer_semaphore.acquire() 222 | 223 | 224 | class HIDWriter: 225 | def __init__(self, hid_device, output_report_ids): 226 | self.closed = False 227 | self._hid_device = hid_device 228 | self._output_report_ids = output_report_ids 229 | self._write_buffer_queue = queue.Queue() 230 | threading.Thread(target=self._write_loop).start() 231 | 232 | def write(self, buffer): 233 | if self.closed: 234 | raise IOError("closed") 235 | self._write_buffer_queue.put_nowait(buffer) 236 | 237 | def _write_loop(self): 238 | while True: 239 | first = True 240 | buf = self._write_buffer_queue.get() 241 | if buf is None or self.closed: 242 | return 243 | while len(buf) > 0: 244 | report_id = None 245 | report_count = None 246 | for id, count in self._output_report_ids: 247 | count -= 1 # take lcb into account 248 | report_id = id 249 | report_count = count 250 | if count > len(buf): 251 | break 252 | lcb = 0 253 | if first: 254 | first = False 255 | else: 256 | lcb |= LCB_CONTINUATION 257 | if report_count < len(buf): 258 | lcb |= LCB_MORE_TO_FOLLOW 259 | padding = b'\0' * max(report_count - len(buf), 0) 260 | self._hid_device.write( 261 | bytes([report_id, lcb]) + buf[:report_count] + padding) 262 | buf = buf[report_count:] 263 | 264 | def close(self): 265 | self.closed = True 266 | self._write_buffer_queue.put_nowait(None) 267 | -------------------------------------------------------------------------------- /iap2/transport/usb_host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python -u 2 | # This file is part of python-functionfs 3 | # Copyright (C) 2016-2021 Vincent Pelletier 4 | # 5 | # python-functionfs is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-functionfs is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-functionfs. If not, see . 17 | 18 | from collections import deque 19 | import errno 20 | import fcntl 21 | import functools 22 | import os 23 | import select 24 | import sys 25 | import functionfs 26 | from functionfs.gadget import ( 27 | GadgetSubprocessManager, 28 | ConfigFunctionFFS, 29 | ) 30 | import functionfs.ch9 31 | import asyncio 32 | 33 | # Large-ish buffer, to tolerate bursts without becoming a context switch storm. 34 | BUF_SIZE = 1024 * 1024 35 | 36 | trace = functools.partial(print, file=sys.stderr) 37 | 38 | class EndpointOUTFile(functionfs.EndpointOUTFile, asyncio.StreamReader): 39 | def __init__(self, *args, **kw): 40 | functionfs.EndpointOUTFile.__init__(self, *args, **kw) 41 | asyncio.StreamReader.__init__(self) 42 | 43 | def onComplete(self, data, status): 44 | if data is None: 45 | trace('aio read completion error:', -status) 46 | else: 47 | trace('aio read completion received', len(data), 'bytes') 48 | self.feed_data(data) 49 | 50 | class EndpointINFile(functionfs.EndpointINFile): 51 | def __init__(self, *args, **kw): 52 | self.__stranded_buffer_list_queue = deque() 53 | self._full = False 54 | super().__init__(*args, **kw) 55 | 56 | def write(self, data): 57 | if self._full: 58 | return 59 | if type(data) == bytes: 60 | data = bytearray(data) 61 | self.submit([bytearray(data)]) 62 | 63 | def onComplete(self, buffer_list, user_data, status): 64 | if status < 0: 65 | trace('aio write completion error:', -status) 66 | else: 67 | trace('aio write completion sent', status, 'bytes') 68 | if status != -errno.ESHUTDOWN and self.__stranded_buffer_list_queue: 69 | buffer_list = self.__stranded_buffer_list_queue.popleft() 70 | self._full = not self.__stranded_buffer_list_queue 71 | return buffer_list 72 | return None 73 | 74 | def onSubmitEAGAIN(self, buffer_list, user_data): 75 | self.__stranded_buffer_list_queue.append(buffer_list) 76 | trace('send queue full, pause sending') 77 | self._full = True 78 | 79 | def forgetStranded(self): 80 | self.__stranded_buffer_list_queue.clear() 81 | 82 | class USBCat(functionfs.Function): 83 | 84 | def __init__(self, path): 85 | fs_list, hs_list, ss_list = functionfs.getInterfaceInAllSpeeds( 86 | interface={ 87 | 'bInterfaceClass': functionfs.ch9.USB_CLASS_VENDOR_SPEC, 88 | 'bInterfaceSubClass': 0xF0, 89 | 'iInterface': 1, 90 | }, 91 | endpoint_list=[ 92 | { 93 | 'endpoint': { 94 | 'bEndpointAddress': functionfs.ch9.USB_DIR_IN, 95 | 'bmAttributes': functionfs.ch9.USB_ENDPOINT_XFER_BULK, 96 | }, 97 | }, { 98 | 'endpoint': { 99 | 'bEndpointAddress': functionfs.ch9.USB_DIR_OUT, 100 | 'bmAttributes': functionfs.ch9.USB_ENDPOINT_XFER_BULK, 101 | }, 102 | }, 103 | ], 104 | ) 105 | super().__init__( 106 | path, 107 | fs_list=fs_list, 108 | hs_list=hs_list, 109 | ss_list=ss_list, 110 | lang_dict={ 111 | 0x0409: [ 112 | "iAP Interface", 113 | ], 114 | }, 115 | ) 116 | 117 | def getEndpointClass(self, is_in, descriptor): 118 | return (EndpointINFile if is_in else EndpointOUTFile) 119 | 120 | def __enter__(self): 121 | result = super().__enter__() 122 | self.in_ep = self.getEndpoint(1) 123 | self.out_ep = self.getEndpoint(2) 124 | return result 125 | 126 | 127 | def onBind(self): 128 | trace('onBind') 129 | super().onBind() 130 | 131 | def onUnbind(self): 132 | trace('onUnbind') 133 | super().onUnbind() 134 | 135 | def onEnable(self): 136 | trace('onEnable') 137 | super().onEnable() 138 | 139 | def onDisable(self): 140 | trace('onDisable') 141 | self.in_ep.forgetStranded() 142 | super().onDisable() 143 | 144 | def onSuspend(self): 145 | trace('onSuspend') 146 | super().onSuspend() 147 | 148 | def onResume(self): 149 | trace('onResume') 150 | super().onResume() 151 | 152 | class SubprocessCat(ConfigFunctionFFS): 153 | def getFunction(self): 154 | return USBCat(path=self._mountpoint) 155 | 156 | 157 | 158 | 159 | loop = asyncio.get_event_loop() 160 | 161 | 162 | def main(): 163 | s = SubprocessCat() 164 | s.start(path="/sys/kernel/config/usb_gadget/isticktoit/ffs.sda") 165 | s.function = function = s.getFunction() 166 | loop.add_reader(function.eventfd.fileno(), function.processEvents) 167 | try: 168 | with function as f: 169 | async def g(): 170 | input = f.getEndpoint(2) 171 | print(await input.readexactly(4)) 172 | 173 | loop.create_task(g()) 174 | out = f.getEndpoint(1) 175 | def w(): 176 | out.write(b'\xFF\x55\x02\x00\xEE\x10') 177 | print("w") 178 | loop.call_later(1, w) 179 | w() 180 | loop.run_forever() 181 | except BaseException as e: 182 | s.join() 183 | raise e 184 | 185 | 186 | if __name__ == '__main__': 187 | main() --------------------------------------------------------------------------------