1/29/2022 A few months ago, a friend and I worked on a yet-to-be-revealed iPhone accessory prototype. We required a bigger data rate than Bluetooth LE could offer. Also, we don't want that the user has to integrate the accessory into his home wifi or worse has to connect his iPhone to the accessories wifi hotspot. Note: From now on I use iPhone as a synonym for iPhone, iPad and iPod touch. The protocol works on all these Platforms. In the end, only two technologies were feasible; Bluetooth Classic and USB (Lightning). If you want to adopt both standards in an Android App it's not a big deal. The SDK is well documented and the interfaces are public usable. Not so in Apples ecosystem. Their design philosophy is to keep the whole platform under control. In order to use the mentioned standards, you have to join the so-called MFi Program, which allows you access to the specifications. The specs are covered by an NDA, but you can still find them over Google. For every product you sell, a fee has to be paid to Apple. The exact amount of them is not disclosed, but there are rumours that Apple gets about 4 € per device. Apple has employed a clever trick, so you can't fool them on these charges. For example, you can't tell them you sold 100 accessories, but in reality, you sold 1000 because every accessory has an "Apple Authentication Coprocessor" built into it, that is only available directly from Apple. The iPhone can detect if such a chip is built into the accessory and won't connect if not. More about that later. Also if you want to market and sell your accessory you have to get through Apples certification first.
The MFi protocol for communication over Bluetooth or Lightning between an iPhone to an Accessory is called iAP. Which is short for iPod Accessory Protocol. To my best knowledge to most recent release is Version 2. iAP2 is usable on four different types of transports - Bluetooth, Serial and USB (Lightning) in Device and Host mode. I highlight them in the next section. One layer above iAP2 applies its own link layer. This layer is used for transmission and flow control. The uppermost layer provides services for the accessories app as well as for the operation system. It is divided into three separate sessions.
As mentioned earlier iAP2 works on top of four distinct transport types. Bluetooth is the fasted option for connecting an iPhone to an accessory over the air, without using WiFi. Then there are three options for connecting an accessory over Lightning. Serial, for simple accessories that don't require massive data rates (A Baudrate of 57600bps is used). This transport is quite simple, so I won't go into it further. USB in Device Mode, in this setting the iPhone acts as a USB Device and the accessory as the Host. And last the recommended approach; USB in Host Mode, in the configuration the iPhone acts as USB Host and the accessory as Device.
For Bluetooth Classic RFCOMM (Radio Frequency Communication) is used as the transport protocol. RFCOMM was developed to emulate RS232 over Bluetooth in a Client-Server manner. iAP doesn't really need that emulated features as it only uses RFCOMM as a bi-directional channel between the iPhone and the accessory. When a connection between an iPhone and an accessory is instantiated first and bonding is established, the iPhone connects to the channel that the accessory provides. On subsequent connections, the accessory connects to the channel that the iPhone provides. The channel id of the listening RFCOMM socket is not fixed. Therefore the initiator has to look up the channel id first using the SDP (Service Discovery Protocol) first. SDP provides information about services, which a client can retrieve by the UUID of the corresponding service. iAP2 relies on two services, one is provided by the Accessory (UUID: 00000000-deca-fade-deca-deafdecacaff ) and by the iPhone (UUID: 00000000-deca-fade-deca-deafdecacafe ).
When connected to a Host, the iPhones USB configuration 1 has to be changed into Configuration 2 afterwards, it exposes a USB HID endpoint 1 . In USB HID data is transferred in so-called reports. The size and structure of each record type are fixed. The iPhone defines a few different report types with various sizes for the payload. If the accessory wants to send some data it has to decide which report type to choose that fits the Link layer packet. If a Link layer packet is too big for one report it is fragmented in multiple packets. Because I didn't want to dig too deep into the USB HID standard, I used libhid for the handling of all the low-level stuff. The library provides a read , write API for exchanging reports with the iPhone.
In this mode, the accessory has to provide an USB interface 1 with a bulk- in and out endpoint 1 . If a standard USB-to-Lightning cable is used the iPhone still starts in USB Device Mode, therefore an USB Role Switch has to be performed. This is accomplished by sending a request to the USB control-endpoint 1 of the iPhone, soon after the iPhone will reappear on the Bus as a Host.
The Link layer ensures that all data sent from the upper layer is received on the other end. It additionally allows to route a received packet to the correct session in the overlying layer. The Link layers starts by detecting if the iPhone support iAP2. After success, some Parameters are negotiated, like the ids of the used sessions, the retransmission timeout or the maximum packet size. Eventually, data packets are exchanged. The mechanism works similar to the TCP sliding window protocol. The main difference is that the seq and ack fields are not specified in bytes, but in messages sent. It is worth noting that in the TCP/IP model this layer would rather be named Transport layer.
The uppermost layer is split into three different session types. The control session handles the authentication and identification procedure and provides access to iOS system information. The File Transfer Session enables the transfer of album covers from the iPhone to the accessory. And lastly, the External Accessory Session allows apps to exchange data with the accessory.
This session is used to exchange all kinds of messages between the accessory and the iPhone. Each message is encoded in a 16-bit type-length-value" format. Once a Layer layer connection is established the iPhone starts a challenge-response authentication scheme. Therefore it first sends a message to the accessory requesting the certificate of the Authentication Coprocessor. The accessory retrieves this certificate from the Coprocessor and send it in a response message. The iPhone checks if this certificate is signed by apple. Upon successful validation, the iPhone generates a challenge and transmits it to the accessory. The accessory relays this challenge to the Authentication Coprocessor for further processing. The Coprocessor signs the challenge using RSA-1024 with SHA-1. Only the Coprocessor knows the used private key matching the public key contained in the certificate. Thus the iPhone can validate the response and, if successful, trust that an Authentication Coprocessor is built into the accessory. After this authentication procedure is completed, the accessory sends some identification information, like name, serial number, supported transports or offered EA protocols to the iPhone. After an acknowledgement from the iPhone, the iAP2 Connection is fully established. Now the accessory is able to access information of the iOS system e.g. about the media library, the now playing, the telephony state. Such information is also exchanged in control session messages.
This session is used to exchange data with apps on the iPhone. The accessory sends a list of supported protocols that run on top of this session to the iPhone during the identification procedure. An App is able to access an accessory using the External Accessory Framework.
import ExternalAccessory let PROTOCOL = "com.protocol.fancy.my" let accessories = EAAccessoryManager.shared().connectedAccessories if (accessories.isEmpty) < print("no accessories connected") > else if (!accessories[0].protocolString.contains(PROTOCOL)) < print("accessory doesn't supports protocol '\(PROTOCOL)'") > else < let session = EASession(accessory: accessories[0], forProtocol: Protocol) let outputStream: OutputStream = session.outputStream let inputStream: InputStream = inputStream // . >
iAP2 allow multiple concurrent EA connections multiplexed over a single iAP2 Link. The EA connections are coordinated over control session messages. If USB Host Mode is used the EA data stream could be sent directly over an USB bulk-in and -out endpoint 1 in order to increase data rates.
For obvious reasons, the Apple Authentication Coprocessor is only available if you are part of the MFi Program. Because I am not a member, I had to find a different way to get hold of such a Coprocessor. Luckily my friend had MFi compatible accessory laying around that I was able to tear apart. On that, I did not only found the Coprocessor in version 2.0, but also the test points for the I²C Bus that connects the Coprocessor to the CPU of the accessory.
In the first picture, you can see the incredible small coprocessor marked in red. One wire is connected to the GND of the USB, another is connected to the test point of the I²C Buses SDA line. In the second picture, a wire is soldered to the test point of the I²C Buses SCL line. I was able to connect this chip to a Raspberry Pi and get it to spit out its certificate and process the given challenges. I also scanned the whole bus and found no other device than the Authentication Coprocessor, so I'm wondering why they placed test points for that bus.
import smbus2 import time from struct import Struct Word = Struct(">H") # use bit-banged i2c, because coprocessor is too slow for native i2c 'dtoverlay=i2c-gpio,i2c_gpio_sda=2,i2c_gpio_scl=3,i2c_gpio_delay_us=50' bus = smbus2.SMBus("/dev/i2c-11") DEV_ADDR = 0x10 def _read_i2c(addr, n): addr_msg = smbus2.i2c_msg.write(DEV_ADDR, bytes([addr])) read_msg = smbus2.i2c_msg.read(DEV_ADDR, n) for _ in range(5): try: bus.i2c_rdwr(addr_msg, read_msg) return bytes(read_msg) except OSError: time.sleep(0.0005) raise Exception("timeout") def _write_i2c(addr, arr): bus.write_i2c_block_data(DEV_ADDR, addr, [int(x) for x in arr]) def read_certificate(): print(bus.read_word_data(DEV_ADDR, 0x30)) size = Word.unpack(_read_i2c(0x30, 2))[0] # Read Accessory Certificate Data Length return _read_i2c(0x31, size) # Read Accessory Certificate Data def generate_challenge_response(challenge): _write_i2c(0x20, Word.pack(len(challenge))) # Write Challenge Data Length _write_i2c(0x21, challenge) # Write Challenge Data bus.write_byte_data(DEV_ADDR, 0x10, 0x01) # Write Authentication Control and Status = Start time.sleep(0.01) for _ in range(10): try: if bus.read_byte_data(DEV_ADDR, 0x10) == 0x10: # Read Authentication Control and Status == Success break except OSError: pass time.sleep(0.1) else: raise Exception("timeout") size = Word.unpack(_read_i2c(0x11, 2))[0] # Read Challenge Response Data Length return _read_i2c(0x12, size) # Read Challenge Response Data