diff --git a/.gitmodules b/.gitmodules index 0e9d4cac..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "pair_ap"] - path = pair_ap - url = https://github.com/ejurgensen/pair_ap diff --git a/ADVANCED/CONFIGURATION_OPTIONS.md b/ADVANCED/CONFIGURATION_OPTIONS.md deleted file mode 100644 index 4553a48b..00000000 --- a/ADVANCED/CONFIGURATION_OPTIONS.md +++ /dev/null @@ -1,76 +0,0 @@ -Under Construction -===== - -Configuration Flags -=== -Configuration flags determine the features of Shairport Sync that are included. The idea is to have a compact core set of features, so that Shairport Sync can be installed in low-capacity devices. Extra features that take up extra space can be added using configuration flags. - -Back Ends ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-alsa` | Choose ALSA API support (GNU/Linux only) | - | `--with-pa` | Choose PulseAudio support. | - | `--with-sndio` | Choose SNDIO API support | - | `--with-jack` | Include a Jack Audio Connection Kit (jack) backend | - | `--with-stdout` | Include the stdout audio back end | - | `--with-pipe` | Include the pipe audio back end | - | `--with-dummy` | Include the dummy audio back end | - | `--with-ao` | Choose AO (Audio Output?) API support. | - | `--with-soundio` | Choose soundio API support. | - -Operating System Support ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-os=OSType` | Specify the distribution to target: One of `linux`, `freebsd` or `darwin`. | - -Installation Support ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-systemv` | Install a System V startup script during a make install | - | `--with-systemd` | Install a systemd startup script during a make install | - | `--with-freebsd-service` | Install a FreeBSD startup script during a make install | - | `--with-cygwin-service` | Install a CYGWIN config script during a make install | - | `--with-configfiles` | Install configuration files during a make install | - -Zeroconf/Bonjour Support ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-external-mdns` | Support the use of `avahi-publish-service` or `mDNSPublish` to advertise the serviceon Bonjour/ZeroConf | - | `--with-avahi` | Choose Avahi-based mDNS support | - | `--with-tinysvcmdns` | Choose tinysvcmdns-based mDNS support | - | `--with-dns_sd` | Choose `dns_sd` mDNS support | - -Interprocess Communication ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-dbus-interface` | Include support for the native Shairport Sync D-Bus interface | - | `--with-dbus-test-client` | Compile dbus test client | - | `--with-mpris-interface` | Include support for a D-Bus interface conforming to the MPRIS standard | - | `--with-mpris-test-client` | Compile an MPRIS test client | - | `--with-mqtt-client` | Include a client for the Message Queuing Telemetry Transport (MQTT) protocol | - -Audio Features ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-apple-alac` | Include support for the Apple ALAC decoder | - | `--with-soxr` | Choose `libsoxr` for high-quality interpolation | - | `--with-convolution` | Choose audio DSP convolution support | - - -General ---- - | Configuration Flag | Brief Explanation | - | --- | --- | - | `--with-pkg-config` | Use pkg-config to find libraries | - | `--with-libdaemon` | Include support for daemonising in older non-systemd systems | - | `--with-piddir=` | Specify a pathname to a directory in which to write the PID file. - | `--with-ssl=` | Choose `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption services. | - | `--with-metadata` | Include support for a metadata feed | - | `--with-systemdsystemunitdir=DIR` | Specify the directory for `systemd` service files | - diff --git a/ADVANCED/README.md b/ADVANCED/README.md deleted file mode 100644 index 055b176a..00000000 --- a/ADVANCED/README.md +++ /dev/null @@ -1,10 +0,0 @@ -Advanced Topics [Under Construction] -====== - -Shairport Sync has many advanced features. -Some may enhance the audio experience and some may be of interest if you want to integrate Shairport Sync into an existing system -Some advanced features allow you to get metadata about the material being played, including cover art, and some remote control of the audio source may be available. - -Configuration Options ------- -Please take a look at [CONFIGURATION_OPTIONS.md](CONFIGURATION_OPTIONS.md) for a discussion of the options you can use when configuring the building of Shairport Sync. diff --git a/AIRPLAY2.md b/AIRPLAY2.md new file mode 100644 index 00000000..f9d1face --- /dev/null +++ b/AIRPLAY2.md @@ -0,0 +1,68 @@ +AirPlay 2 +=== + +Shairport Sync offers limited AirPlay 2 support for audio sources on iOS devices and Macs. In addition, it partly works if the audio source is a HomePod mini or an Apple TV. It does not work for iTunes on Windows. Unfortunately, it does not integrate with the Home app or with Siri. + +The focus of the development effort has been on getting good audio performance for iOS and Mac. Features outside this focus may be missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. + +For AirPlay 2, Shairport Sync uses another application called [`nqptp`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. `nqptp` must run as `root` and must have exclusive access to ports `319` and `320`. + +AirPlay 2 functionality is available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. + +AirPlay 2 support definitely does not work on macOS. This is because [`nqptp`](https://github.com/mikebrady/nqptp) uses ports `319` and `320` but these ports are unavailable because there are used by macOS to support its implementation of AirPlay 2. + +The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM. + +### What Works +- AirPlay 2 for iOS and Mac players. + +### What Does Not Work +- No integration with Siri or HomeKit +- Lossless or High Definition Lossless material is transcoded to AAC before transmission over AirPlay 2. +- No Remote Control +- No Artwork in the Metadata +- No AirPlay 2 for Windows iTunes + +### What Partly Works +- Incomplete functionality using AppleTV or HomePod as player. + A Shairport Sync player will work -- i.e. it will play -- but the selection button won't persist and the volume can't be set. + +### Limitations +- Only a single instance of Shairport Sync can run on a device. +- The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. + +### Known Issues +- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, please let us know. + +AirPlay 2 -- What You Need +--- +AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS, Ubuntu 20.04 on a VM and Ubuntu 20.04.2 64-bit Server Edition have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. + +So, here are some guideline requirements: +* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 3. +* A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. +* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. + +Guides +--- +* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A guide for building on FreeBSD is forthcoming. + +AirPlay 2 -- More About What Works +--- +* Two types of audio are received by Shairport Sync -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. +* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) +* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. + +Acknowledgements +---- +Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. + +Big thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: +1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. +2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. +3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). + +Finally +---- +Did we mention AirPlay 2 support is experimental? diff --git a/BUILDFORAP1.md b/BUILDFORAP1.md index 5d4e26db..07dac81d 100644 --- a/BUILDFORAP1.md +++ b/BUILDFORAP1.md @@ -46,36 +46,48 @@ First, install the packages needed by Shairport Sync: # apt-get install build-essential git xmltoman autoconf automake libtool \ libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev ``` -Next, download Shairport Sync, configure it, compile and install it: +Next, download Shairport Sync, check out the `development` branch, configure it, compile and install it: ``` $ git clone https://github.com/aillwee/shairport-sync.git $ cd shairport-sync $ git checkout development -$ git submodule init -$ git submodule update $ autoreconf -fi $ ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd $ make $ sudo make install ``` -By the way, the `autoreconf` step may take quite a while on a Raspberry Pi -- be patient! +By the way, the `autoreconf` step may take quite a while -- be patient! + +Now to configure Shairport Sync. In this walkthrough, it will be configured for an `alsa` output device. A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: -Now to configure Shairport Sync. Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`: ``` -// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC: +... +Settings and options for the audio backend "alsa": + -d output-device set the output device, default is "default". + -c mixer-control set the mixer control name, default is to use no mixer. + -m mixer-device set the mixer device, default is the output device. + -i mixer-index set the mixer index, default is 0. + hardware output devices: + "hw:Headphones" +``` +Using a program such as `alsamixer`, the name of a mixer (i.e. an attenuator or volume control that could be used to control the output level) can be determined. In this case, the name of the mixer is `Headphone`. + +Following this, here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`. Note that everything is case-sensitive: +``` +// Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC general = { - volume_range_db = 60; + volume_range_db = 60; }; alsa = { - output_device = "hw:0"; + output_device = "hw:Headphones"; mixer_control_name = "Headphone"; }; ``` -The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the built-in audio card mixer's attenuation range. +The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the Raspberry Pi's built-in audio card mixer's attenuation range. It may not be necessary for other output devices. The next step is to enable Shairport Sync to start automatically on boot up: ``` diff --git a/BUILDFORAP2.md b/BUILDFORAP2.md index e044c865..65bc3599 100644 --- a/BUILDFORAP2.md +++ b/BUILDFORAP2.md @@ -1,18 +1,20 @@ Experimental Build Instructions for AirPlay 2 == -* This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected! +* This code is experimental and much of the normal functionality of Shairport Sync is broken. Please do not use it in situations where reliable service is expected. * Be especially careful with audio systems capable of very high volume output -- the volume control in this software may not be reliable! * Initially, at least, leave the settings in the configuration file at default except as noted below. * At the time of writing, May 2, 2021, everything is on the latest version of the software -- macOS 11..3, iOS 14.5, Raspberry Pi OS 5.10.17-v7l+ (Buster), Ubuntu 20.04 -- fully updated. -* At present, Shairport Sync will compile *but not work* on Raspbian Stretch (thanks JD Smith). -* AirPlay 2 seems to use a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. +* AirPlay 2 uses a timing system based on the [IEEE-1588 Precision Timing Protocol (PTP)](https://standards.ieee.org/standard/1588-2008.html). Shairport Sync relies on a program called [`nqptp`](https://github.com/mikebrady/nqptp) to monitor PTP signals. This program uses ports 319 and 320 and replaces any PTP service you have on the computer. In addition, `nqptp` must run with `root` privileges. - (FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) -* When Shairport Sync is updated, you should check and update `nqptp` *before* building the update. + (FYI, most computers do not have a PTP clock running. They often use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time.) +* Shairport Sync has a build dependency on `nqptp`, so you should check and update `nqptp` *before* building or updating Shairport Sync. +* The SMI Interface Version Numbers of `nqptp` and Shairport Sync must match. If they don't, you'll get a message in the logs. It means that one of the programs is out of date with respect to the other. * Build instructions are likely to change. Build and Install Instructions == +Overall, you'll be building and installing two programs. The first one is `nqptp` and the second one is Shairport Sync itself. Build `nqptp` first. + In the commands below, for a Debian-like Linux, note the convention that a `#` prompt means you are in superuser mode and a `$` prompt means you are in a regular unprivileged user mode. You can use `sudo` *("SUperuser DO")* to temporarily promote yourself from user to superuser, if permitted. For example, if you want to execute `apt-get update` in superuser mode and you are in user mode, enter `sudo apt-get update`. ### Turn Off WiFi Power Management @@ -20,9 +22,9 @@ If you are using WiFi, you should turn off WiFi Power Management: ``` # iwconfig wlan0 power off ``` -WiFi Power Management will put the WiFi system in low-power mode when the WiFi system considered inactive, and in this mode it may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) +WiFi Power Management will put the WiFi system in low-power mode when the WiFi system is considered inactive. In this mode, the system may not respond to events initiated from the network, such as AirPlay requests. Hence, WiFi Power Management should be turned off. (See [TROUBLESHOOTING.md](https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode) for more details.) -### Configure and Update +### Update Your System Do the usual update and upgrade: ``` # apt-get update @@ -60,27 +62,42 @@ First, install the packages needed by Shairport Sync: (Note: if you don't want the `D-Bus` interface, you may omit `libglib2.0-dev`.) ### nqptp ### -Download, install and start/enable `nqptp` from [here](https://github.com/mikebrady/nqptp). +Download, install, enable and start `nqptp` from [here](https://github.com/mikebrady/nqptp). -***Note:*** At present, Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. +***Note:*** Shairport Sync expects the `nqptp` repository folder to be in the same directory as the `shairport-sync` repository folder, as it will look for a header file at `../nqptp/nqptp-shm-structures.h`. -The `nqptp` service monitors PTP clocks. It provides a POSIX Shared Memory Interface (SMI) at `/nqptp`. A shared -pthread mutex is contained within the interface, and to use it you need write access. +The `nqptp` service monitors PTP clocks. It provides a [POSIX Shared Memory](https://man7.org/linux/man-pages/man7/shm_overview.7.html) Interface at `/nqptp`. A shared pthread mutex is contained within the interface, and to use it you need write access. -Next, download Shairport Sync, configure it, compile and install it: +Next, download Shairport Sync, check out the `development` branch and configure, compile and install it: ``` $ git clone git@github.com:aillwee/shairport-sync.git $ cd shairport-sync $ git checkout development -$ git submodule init -$ git submodule update $ autoreconf -fi -$ CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --sysconfdir=/etc --with-metadata \ - --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-airplay-2 +$ ./configure --sysconfdir=/etc --with-alsa \ + --with-soxr --with-avahi --with-ssl=openssl --with-systemd --with-dbus-interface --with-airplay-2 $ make -j # make install ``` -Now to configure Shairport Sync. Here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`: +By the way, the `autoreconf` step may take quite a while -- be patient! + +Now to configure Shairport Sync. In this walkthrough, it is configured for an `alsa` output device. + +A list of `alsa` output devices is given at the end of the help information. For example, on a Raspberry Pi, at the end of the output from the command `$ shairport-sync -h`, the following appears: + +``` +... +Settings and options for the audio backend "alsa": + -d output-device set the output device, default is "default". + -c mixer-control set the mixer control name, default is to use no mixer. + -m mixer-device set the mixer device, default is the output device. + -i mixer-index set the mixer index, default is 0. + hardware output devices: + "hw:Headphones" +``` +Using a program such as `alsamixer`, the name of a mixer (i.e. an attenuator or volume control that could be used to control the output level) can be determined. In this case, the name of the mixer is `Headphone`. + +Following this, here are the important options for the Shairport Sync configuration file at `/etc/shairport-sync.conf`. Note that everything is case-sensitive: ``` // Sample Configuration File for Shairport Sync on a Raspberry Pi using the built-in audio DAC general = @@ -90,12 +107,12 @@ general = alsa = { - output_device = "hw:0"; + output_device = "hw:Headphones"; mixer_control_name = "Headphone"; }; ``` -The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the built-in audio card mixer's attenuation range. +The `volume_range_db = 60;` setting makes Shairport Sync use only the usable part of the Raspberry Pi's built-in audio card mixer's attenuation range. It may not be necessary for other output devices. ### Automatic Start ### @@ -115,7 +132,7 @@ You may wish to run Shairport Sync from the command line (but remember to ensure ``` $ shairport-sync -vu --statistics ``` -The user doesn't need to be privileged, but should be a member of the `audio` group for access to the `alsa` subsystem. +The user doesn't need to be privileged, but must be a member of the `audio` group for access to the `alsa` subsystem. ### Using Shairport Sync ### @@ -123,3 +140,6 @@ The Shairport Sync AirPlay service should appear on the network with a service n Connect and enjoy... +### Restart Your Mac! ### + +At the time of writing, there seems to be a bug in the Mac Music app. It appears that, when the Mac has been awoken after sleeping, the Music app may not fully reawaken, and it will be unable to drive AirPlay 2 devices for more than a few seconds. The only know solution is to reboot. diff --git a/CAR INSTALL.md b/CAR INSTALL.md index 062b6df5..0a82b68a 100644 --- a/CAR INSTALL.md +++ b/CAR INSTALL.md @@ -1,4 +1,4 @@ -# Shairport Sync for Cars +# Shairport Sync for Cars [Needs updating for Airplay 2] If your car audio has an AUX input, you can get AirPlay in your car using Shairport Sync. Together, Shairport Sync and an iPhone can give you access to internet radio, YouTube, Apple Music, Spotify, etc. on the move. While Shairport Sync is no substitute for CarPlay, the audio quality is often much better than Bluetooth. Your passengers can enjoy movies with the soundtrack on the car speakers. You can even listen to Siri's traffic directions on your car audio. ## The Basic Idea diff --git a/CYGWIN.md b/CYGWIN.md index 80cb3517..491ca7c1 100644 --- a/CYGWIN.md +++ b/CYGWIN.md @@ -1,4 +1,4 @@ -Installing Shairport Sync into Cygwin +Installing Shairport Sync into Cygwin [Airplay 2 Not Supported] ==== This guide is based on installing onto a fresh installation of Cygwin 2.895 (64-bit installation) running in Windows 10 diff --git a/FEDORA.md b/FEDORA.md index bb57d58b..cf8cbc50 100644 --- a/FEDORA.md +++ b/FEDORA.md @@ -1,4 +1,4 @@ -Fedora Installation Guide +Fedora Installation Guide [Needs updating for Airplay 2] ----- Install the toolchain and pre-requisites, if necessary: diff --git a/FREEBSD.md b/FREEBSD.md index fe3a2a5e..7ec87e1d 100644 --- a/FREEBSD.md +++ b/FREEBSD.md @@ -1,4 +1,4 @@ -Shairport Sync on FreeBSD using `sndio` +Shairport Sync on FreeBSD using `sndio` [Needs updating for Airplay 2] ---- Shairport Sync runs natively on FreeBSD using the `sndio` back end, thanks to the work of [Tobias Kortkamp (t6)](https://github.com/t6). diff --git a/MOREINFO.md b/MOREINFO.md index 1b024340..7b957974 100644 --- a/MOREINFO.md +++ b/MOREINFO.md @@ -1,9 +1,8 @@ - More Information ---------- Shairport Sync offers *full audio synchronisation*, a feature of AirPlay that previous implementations do not provide. Full audio synchronisation means that audio is played on the output device at exactly the time specified by the audio source. To accomplish this, Shairport Sync needs access to audio systems – such as `alsa` on Linux and `sndio` on FreeBSD – that provide very accurate timing information about output devices. Shairport Sync must have direct access to the output device used, which must be a real sound card capable of working with 44,100, 88,200 or 176,400 samples per second, interleaved PCM stereo of 8, 16, 24 or 32 bits. The default is 44,100 samples per second / 16 bits (you'll get a message in the logfile if there's a problem). -Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. An older backend for PulseAudio called `pulse` does not support synchronisation and is deprecated. +Alternatively, Shairport Sync works well with PulseAudio, a widely used sound server found on many desktop Linuxes. While the timing information is not as accurate as that of `alsa` or `sndio`, it is often impractical to remove or disable PulseAudio. In that case, the `pa` backend can be used. For other use cases, Shairport Sync can provide synchronised audio output to a unix pipe or to standard output, or to audio systems that do not provide timing information. This could perhaps be described as *partial audio synchronisation*, where synchronised audio is provided by Shairport Sync, but what happens to it in the subsequent processing chain, before it reaches the listener's ear, is outside the control of Shairport Sync. @@ -11,13 +10,13 @@ For more about the motivation behind Shairport Sync, please see the wiki at http Synchronisation, Latency, "Stuffing" --------- -The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. Most sources set a latency of two seconds. Recent versions of iTunes and forkedDaapd use a latency of just over 2.25 seconds. A latency of this length allows AirPlay players to correct for network delays, processing time variations and so on. +The AirPlay protocol uses an agreed *latency* – the time difference, or delay, between the time represented by a sound sample's `timestamp` and the time it is actually played by the audio output device, typically a Digital to Audio Converter (DAC). The latency to be used is specified by the audio source when it negotiates with Shairport Sync. As mentioned previously, Shairport Sync implements full audio synchronisation when used with `alsa`, `sndio` or PulseAudio systems. This is done by monitoring the timestamps present in data coming from the audio source and the timing information from the audio system, e.g. `alsa`. To maintain the latency required for exact synchronisation, if the output device is running slow relative to the source, Shairport Sync will delete frames of audio to allow the device to keep up. If the output device is running fast, Shairport Sync will insert frames to keep time. The number of frames inserted or deleted is so small as to be almost inaudible on normal audio material. Frames are inserted or deleted as necessary at pseudorandom intervals. Alternatively, with `libsoxr` support, Shairport Sync can resample the audio feed to ensure the output device can keep up. This is less obtrusive than insertion and deletion but requires a good deal of processing power — most embedded devices probably can't support it. The process of insertion/deletion or resampling is rather inelegantly called “stuffing”. Stuffing is not done for partial audio synchronisation – the audio samples are simply presented at exactly the right time to the next stage in the processing chain. -Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using a variant of NTP synchronisation protocols. +Timestamps are referenced relative to the source computer's clock – the `source clock`, but timing must be done relative to the clock of the computer running Shairport Sync – the `local clock`. So, another thing Shairport Sync has to do is to synchronize the source clock and the local clock, and it does this usually to within a fraction of a millisecond, using either the PTP or NTP synchronisation protocols. What else? -------------- @@ -27,9 +26,8 @@ What else? * Output bit depths of 8, 16, 24 and 32 bits, rather than the standard 16 bits. * Output frame rates of 44,100, 88,200, 176,000 or 352,000 frames per second. * Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.20 seconds with `alsa`, 0.35 seconds with `sndio`. -* Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.) * Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in macOS and iOS. -* Compiles on Linux, Cygwin, FreeBSD, OpenBSD. +* Compiles on Linux and FreeBSD. * Outputs to [`alsa`](https://www.alsa-project.org/wiki/Main_Page), [`sndio`](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [JACK](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has limited support for [libao](https://xiph.org/ao/) and for [`soundio`](http://libsound.io). * An [MPRIS](https://specifications.freedesktop.org/mpris-spec/2.2/) interface, partially complete and very functional, including access to metadata and artwork, and some limited remote control. * An interface to [MQTT](https://en.wikipedia.org/wiki/MQTT), an often-used protocol in home automation projects. @@ -39,268 +37,10 @@ Heritage ------- Shairport Sync is a substantial rewrite of the fantastic work done in Shairport 1.0 by James Laird and others — please see https://github.com/abrasive/shairport/blob/master/README.md#contributors-to-version-1x for a list of the contributors to Shairport 1.x and Shairport 0.x. From a "heritage" point of view, Shairport Sync is a fork of Shairport 1.0. -Status ------- -Shairport Sync works on a wide variety of Linux devices, FreeBSD and OpenBSD. It works with built-in audio and with a variety of USB-connected audio amplifiers and DACs. Shairport Sync runs well on the Raspberry Pi. It can drive the built-in sound card – see the note below on configuring the Raspberry Pi to make best use of it. It runs well on the Raspberry Pi Zero W with a suitable USB or I2S card. - -Shairport Sync will work with PulseAudio, which is installed in many desktop Linuxes. PulseAudio normally runs in the *user mode* but can be configured to run in *system mode*, though this is not recommended. Shairport Sync can work with it in either mode. - -Shairport Sync runs natively on FreeBSD and OpenBSD using the `sndio` sound system. - -Shairport Sync runs on Ubuntu, OpenWrt, Debian, Arch Linux, Fedora, FreeBSD and OpenBSD inside VMWare Fusion on a Mac, but synchronisation in inaccurate — possibly because the sound card is being emulated. - -For information about changes and updates, please refer to the RELEASENOTES.md file in the distribution. - -Building And Installing ---------------------- -See [here](https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md) for simple installation instructions for modern Linux systems like Rapsberry Pis. - -Shairport Sync may already be available as a package in your Linux distribution (search for `shairport-sync` – the package named `shairport` is a different program). Packages are available on recent versions of Debian, Ubuntu, Arch, OpenWrt and possibly more: - -**Ubuntu:** A `shairport-sync` installer package is available for Ubuntu. Additionally, a Personal Package Archives for Shairport Sync master and development branches are available at https://launchpad.net/~dantheperson. - -**Debian:** shairport-sync is in the Debian archive. - -**OpenWrt:** There is a Shairport Sync package in OpenWrt trunk. Also, there's an OpenWrt package at https://github.com/mikebrady/shairport-sync-for-openwrt, including one that builds back to Barrier Breaker. - -**Arch Linux:** Shairport Sync is available for x86_64 and i686 platforms in the Arch Linux Community Repository -- search for `shairport-sync`. See also https://www.archlinux.org/packages/. - -To build and install the latest version of Shairport Sync, an [Arch Linux build and installation guide](https://github.com/mikebrady/shairport-sync-for-arch-linux) is available, based on original work by [Elia Cereda](https://github.com/EliaCereda). - -**Mac OS X:** A HomeBrew package exists for Shairport Sync. With HomeBrew installed, Shairport Sync can be installed using the command: -``` -$brew install shairport-sync -``` -Note that the installation uses the libao library and so synchronisation is not available — playback glitches will occur occasionally, when the ao system's buffers overflow or underflow. - -**Fedora:** Please see the guide at [FEDORA.md](https://github.com/mikebrady/shairport-sync/blob/master/FEDORA.md). - -**Cygwin:** Please see the guide at [CYGWIN.md](https://github.com/mikebrady/shairport-sync/blob/master/CYGWIN.md). - -Sincere thanks to all package contributors! - -If you wish to build and install the latest version of Shairport Sync on Debian, Ubuntu, Fedora or Arch platforms, please continue to follow these instructions. When the program has been compiled and installed, refer to the section on Configuring Shairport Sync that follows. To build Shairport Sync from sources on FreeBSD please refer to [FREEBSD.md](https://github.com/mikebrady/shairport-sync/blob/master/FREEBSD.md). - -**Remove Old Versions of Shairport Sync and its Startup Scripts** - -You should check to see if `shairport-sync` is already installed – you can use the command `$ which shairport-sync` to find where it is located, if installed. If it is installed you should delete it – you may need superuser privileges. After deleting, check again in case further copies are installed elsewhere. - -You should also remove the startup script files `/etc/systemd/system/shairport-sync.service` and `/etc/init.d/shairport-sync` if they exist – new ones will be installed in necessary. - -If you removed any installations of Shairport Sync or any of its startup script files, you should reboot. - -**Determine The Configuration Needed** - -Shairport Sync has a number of different "backends" that connect it to the system's audio handling infrastructure. Most recent Linux distributions that have a GUI – including Ubuntu, Debian and others – use PulseAudio to handle sound. In such cases, it is inadvisable to attempt to disable or remove PulseAudio. Thus, if your system uses PulseAudio, you should build Shairport Sync with the PulseAudio backend. You can check to see if PulseAudio is running by opening a Terminal window and entering the command `$ pactl info`. Here is an example of what you'll get if PulseAudio is installed, though the exact details may vary: -``` -$ pactl info -Server String: unix:/run/user/1000/pulse/native -Library Protocol Version: 30 -Server Protocol Version: 30 -Is Local: yes -Client Index: 9 -Tile Size: 65472 -User Name: mike -Host Name: ubuntu -Server Name: pulseaudio -Server Version: 8.0 -Default Sample Specification: s16le 2ch 44100Hz -Default Channel Map: front-left,front-right -Default Sink: alsa_output.pci-0000_02_02.0.analog-stereo -Default Source: alsa_input.pci-0000_02_02.0.analog-stereo -Cookie: 96f9:3e8d -$ -``` -If PulseAudio in not installed, you'll get something like this: -``` -$ pactl info --bash: pactl: command not found -$ -``` -If your system does not use PulseAudio, then it is likely that it uses the Advanced Linux Sound Architecture (ALSA), so you should build Shairport Sync with the ALSA backend. By the way, many systems with PulseAudio also have ALSA (in fact, PulseAudio is effectively a client of ALSA); in those cases you should choose the PulseAudio backend. - -If PulseAudio is not installed, there is no necessity to install it for Shairport Sync. In fact, Shairport Sync works better without it. - -**Building** - -To build Shairport Sync from sources on Debian, Ubuntu, Raspbian, etc. follow these instructions. - -The following libraries are required: -* OpenSSL or mbed TLS (PolarSSL is supported but deprecated) -* Avahi -* ALSA and/or PulseAudio -* libdaemon -* autoconf -* automake -* libtool -* libpopt -* libconfig - -Optional: -* libsoxr -* libalac (This is a library containing the Apple ALAC decoder.) - -Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power. - -Debian, Ubuntu and Raspbian users can get the basics with: - -- `# apt install build-essential git xmltoman` – these may already be installed. -- `# apt install autoconf automake libtool libdaemon-dev libpopt-dev libconfig-dev` -- `# apt install libasound2-dev` for the ALSA libraries -- `# apt install libpulse-dev` for the PulseAudio libraries -- `# apt install avahi-daemon libavahi-client-dev` if you want to use Avahi (recommended). -- `# apt install libssl-dev` if you want to use OpenSSL and libcrypto, or use mbed TLS otherwise. -- `# apt install libmbedtls-dev` if you want to use mbed TLS, or use OpenSSL/libcrypto otherwise. You can still use PolarSSL with `apt install libpolarssl-dev` if you want to use PolarSSL, but it is deprecated as it's not longer being supported. -- `# apt install libsoxr-dev` if you want support for libsoxr-based resampling. This library is in recent distributions; if not, instructions for how to build it from source on Linux are available at [LIBSOXR.md](https://github.com/mikebrady/shairport-sync/blob/master/LIBSOXR.md). -- `# apt install libsndfile1-dev` if you want to use the convolution filter. - -If you wish to include the Apple ALAC decoder, you need install it first – please refer to the [ALAC](https://github.com/mikebrady/alac) repository for more information. - -**Download Shairport Sync:** -``` -$ git clone https://github.com/mikebrady/shairport-sync.git -``` - -Next, `cd` into the shairport-sync directory and execute the following commands: -``` -$ autoreconf -i -f -``` -(Note that the `autoreconf...` step may take some time on less powerful machines.) - -**Choose the appropriate `--with-*` options:** - -(Don't worry -- there's a recommended set of configuration options further down.) - -- `--with-alsa` include the ALSA backend module to audio to be output through the Advanced Linux Sound Architecture (ALSA) system directly. This is recommended for highest quality. -- `--with-pa` include the PulseAudio audio back end. This is recommended if your Linux installation already has PulseAudio installed. Although ALSA would be better, it requires direct and exclusive access to to a real (hardware) soundcard, and this is often impractical if PulseAudio is installed. -- `--with-stdout` include an optional backend module to enable raw audio to be output through standard output (stdout). -- `--with-pipe` include an optional backend module to enable raw audio to be output through a unix pipe. -- `--with-soundio` include an optional backend module to enable raw audio to be output through the soundio system. -- `--with-avahi` or `--with-tinysvcmdns` for mdns support. Avahi is a widely-used system-wide zero-configuration networking (zeroconf) service — it may already be in your system. If you don't have Avahi, or similar, then consider including tinysvcmdns, which is a tiny zeroconf service embedded inside the shairport-sync application itself. To enable multicast for `tinysvcmdns`, you may have to add a default route with the following command: `route add -net 224.0.0.0 netmask 224.0.0.0 eth0` (substitute the correct network port for `eth0`). You should not have more than one zeroconf service on the same system — bad things may happen, according to RFC 6762, §15. -- `--with-ssl=openssl`, `--with-ssl=mbedtls` or `--with-ssl=polarssl` (deprecated) for encryption and related utilities using either OpenSSL, mbed TLS or PolarSSL. -- `--with-libdaemon` include a demonising library needed if you want to be able to demonise Shairport Sync with the `-d` option. Not needed for `systemd`-based systems which demonise programs differently. -- `--with-soxr` for libsoxr-based resampling. -- `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location. -- `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader. -- `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. -- `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. -- `--with-apple-alac` to include the Apple ALAC Decoder. -- `--with-convolution` to include a convolution filter that can be used to apply effects such as frequency and phase correction, and a loudness filter that compensates for human ear non-linearity. Requires `libsndfile`. -- `--with-systemd` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on `systemd`-based Linuxes. Default is not to to install. -- `--with-systemv` to include a script to create a Shairport Sync service that can optionally launch automatically at startup on System V based Linuxes. Default is not to to install. - -**Determine if it's a `systemd` or a "System V" installation:** - -If you wish to have Shairport Sync start automatically when your system boots, you need to figure out what so-called "init system" your system is using. (If you are using Shairport Sync with PulseAudio, as installed in many desktop systems, this section doesn't apply.) - -There are a number of init systems in use: `systemd`, `upstart` and "System V" among others, and it's actually difficult to be certain which one your system is using. Fortunately, for Shairport Sync, all you have to do is figure out if it's a `systemd` init system or not. If it is not a `systemd` init system, you can assume that it is either a System V init system or else it is compatible with a System V init system. Recent systems tend to use `systemd`, whereas older systems use `upstart` or the earlier System V init system. - -The easiest way is to look at the first few lines of the `init` manual. Enter the command: - -``` -$ man init -``` -In a `systemd` system, the top lines of the `man` page make it clear that it's a `systemd` system, as follows (from Ubuntu 16.04): -``` -SYSTEMD(1) systemd SYSTEMD(1) - -NAME - systemd, init - systemd system and service manager -... -``` -Other init systems will look considerably different. For instance, in an `upstart` system, the top lines of the `man` page indicate it's using `upstart` system, as follows (from Ubuntu 14.04): - -``` -init(8) System Manager's Manual init(8) - -NAME - init - Upstart process management daemon - -SYNOPSIS - init [OPTION]... -... -``` -In a System V system, the top lines of the `man` page are as follows (from Debian 7.11): -``` -INIT(8) Linux System Administrator's Manual INIT(8) - -NAME - init, telinit - process control initialization - -SYNOPSIS - /sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ] - /sbin/telinit [ -t SECONDS ] [ 0123456sSQqabcUu ] - /sbin/telinit [ -e VAR[=VAL] ] - -... -``` -If your system is definitely a `systemd` system, choose `--with-systemd` below. Otherwise, choose `--with-libdaemon --with-systemv`. - -**Choose the location of the configuration file** - -A final consideration is the location of the configuration file `shairport-sync.conf`. This will be placed in the directory specified by the `sysconfdir` configuration variable, which defaults to `/usr/local/etc`. This is normal in BSD Unixes, but is unusual in Linux. Hence, for Linux installations, you need to set the `sysconfdir` variable to `/etc` using the configuration setting `--sysconfdir=/etc`. - -**Sample `./configure` command with parameters for a typical Linux `systemd` installation:** - -Here is a recommended set of configuration options suitable for Linux installations that use `systemd`, such as Ubuntu 15.10 and later, and Raspbian Buster, Stretch and Jessie. It specifies both the ALSA and PulseAudio backends and includes a sample configuration file and an script for automatic startup on system boot: - -`$ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd` - -* Omit the `--with-soxr` if the libsoxr library is not available. -* For installation into a System V system, replace the `--with-systemd` with `--with-libdaemon --with-systemv`. -* If you intend to use Shairport Sync with PulseAudio in the standard user mode, it can not be a system service, so you should omit both `--with-systemd` and `--with-systemv`. - -**Build and Install the Application:** - -Enter: - -``` -$ make -``` - -to build the application. - -Assuming you have used the `./configure` settings suggested above, you can now install the application: - -``` -$ sudo make install -``` -The user and group `shairport-sync` will be created if necessary, and a runtime folder will be created at `/var/run/shairport-sync` if you have chosen `--with-systemv`. In addition, a `man` page, a default configuration file and automatic power-on startup script will be installed. - - -**Complete installation into to a `systemd` system** - -If you have chosen the `--with-systemd` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: - -``` -$ sudo systemctl enable shairport-sync -``` -You should then reboot the system. - -**Complete installation into a System V system** - -If you have chosen the `--with-systemv` configuration option, then, to enable Shairport Sync to start automatically at system startup, enter: -``` -$ sudo update-rc.d shairport-sync defaults 90 10 -$ sudo update-rc.d shairport-sync enable -``` -You should then reboot the system. - -**Man Page** - -You can view the man page here: http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html Configuring Shairport Sync -------- -There are two logically distinct parts to getting Shairport Sync to run properly on your machine: (1) starting and stopping it and (2) ensuring it has the right settings. - -**(1) Starting and Stopping:** -Starting and stopping Shairport Sync automatically is taken care of differently in different versions of Linux – see the previous section for an example of installing into a `systemd` or a System V based system. - -If you are using PulseAudio in the standard "user" mode, you can not start Shairport Sync automatically at boot time – it must be started when the user logs in to a GUI session. Various GUI tools exist to enable you to build a Startup Application. However, there does not appear to be a similar tool for terminating the program when you log out. Also, if the GUI switches to another user, the user's PulseAudio session is disconnected from the output device. - -**(2) Settings:** -To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. +To get the best from Shairport Sync, you’ll need to (a) give Shairport Sync a service name by which it will be seen in iTunes etc. and (b) specify the backend you wish to use - `alsa` for the ALSA backend, or `pa` for the PulseAudio back end, etc. If only one backend is included at compilation, or if the backend is ALSA, there is no need to explicitly specify the backend. For the ALSA backend you may need to (c) specify the output device to use and (d) specify the name of the mixer volume control to use to control the output level. To get values for (c) and (d) you might need to explore the ALSA output devices with a program like `alsamixer` or similar. @@ -355,21 +95,14 @@ Note: Shairport Sync can take configuration settings from command line options. **Raspberry Pi** -The Raspberry Pi Models A and B have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. +Many Raspberry Pi Models have a built-in audio DAC that is connected to the device's headphone jack. Apart from a loud click when used for the first time after power-up, it is now quite adequate for casual listening. -To get the benefits of improvements in the Pi's software and firmware, you should update to the Raspian release of October 2018 or later, as a number of improvements have been made to the built-in DAC. - -Do the usual update and upgrade: -``` -# apt update -# apt upgrade -``` To make Shairport Sync output to the built-in audio DAC and use its hardware mixer, in the `alsa` section of the configuration file, set the output device and mixer as follows: ``` alsa = { output_device = "hw:0"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. - mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. + mixer_control_name = "Headphone"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. // ... other alsa settings ``` (Remember to uncomment the lines by removing the `//` at the start of each.) When these changes have been made, reboot the machine. @@ -378,20 +111,6 @@ A problem with the built-in DAC that it declares itself to have a very large mix From a user's point of view, the effect of using this setting is to move the minimum usable volume all the way down to the bottom of the user's volume control, rather than have the minimum usable volume concentrated very close to the maximum volume. -*Command Line Arguments* - -As previously mentioned, you can use command line arguments to provide settings to Shairport Sync as before, though newer settings will only be available via the configuration file. For full information, please read the Shairport Sync `man` page, also available at http://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/master/man/shairport-sync.html. - -Apart from the following options, all command line options can be replaced by settings in the configuration file. Here is a brief description of command line options that are not replicated by settings in the settings file. - -* The `-c` option allows you to specify the location of the configuration file. -* The `-V` option gives you version information about Shairport Sync and then quits. -* The `-d` option causes Shairport Sync to properly daemonise itself, that is, to run in the background. You may need sudo privileges for this. -* The `-k` option causes Shairport Sync to kill an existing Shairport Sync daemon. You may need to have sudo privileges for this. - -The System V init script at `/etc/init.d/shairport-sync` has a bare minimum : -`-d`. Basically all it does is put the program in daemon mode. The program will read its settings from the configuration file. - Examples -------- @@ -419,7 +138,7 @@ general = { alsa = { output_device = "hw:0"; - mixer_control_name = "PCM"; + mixer_control_name = "Headphone"; }; ``` @@ -447,41 +166,6 @@ alsa = { }; ``` -For a first generation Griffin iMic on a Raspberry Pi: -``` -general = { - name = "Attic"; -}; - -alsa = { - output_device = "hw:1"; - mixer_control_name = "PCM"; -}; -``` - -For an NSLU2, which has no internal sound card, there appears to be a bug in ALSA — you can not specify a device other than "default". Thus: - -On an NSLU2, to drive a first generation Griffin iMic: -``` -general = { - name = "Den"; -}; - -alsa = { - mixer_control_name = "PCM"; -}; -``` - -On an NSLU2, to drive the "3D Sound" USB card: -``` -general = { - name = "TV Room"; -}; - -alsa = { - mixer_control_name = "Speaker"; -}; -``` Finally, here is an example of using the PulseAudio backend: ``` @@ -510,9 +194,7 @@ Latency ------- Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. -Shairport Sync uses latencies supplied by the source, typically either 2 seconds or just over 2.25 seconds. You shouldn't need to change them. - -Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset (in frames) to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. +Problems can arise when you are trying to synchronise with speaker systems — typically surround-sound home theatre systems — that have their own inherent delays. You can compensate for an inherent delay using the appropriate backend (typically `alsa`) `audio_backend_latency_offset_in_seconds`. Set this offset to compensate for a fixed delay in the audio back end; for example, if the output device delays by 100 ms, set this to -0.1. Resynchronisation ------------- diff --git a/OPENBSD.md b/OPENBSD.md index 219d7402..6b18f979 100644 --- a/OPENBSD.md +++ b/OPENBSD.md @@ -1,4 +1,4 @@ -Shairport Sync on OpenBSD +Shairport Sync on OpenBSD [AirPlay 2 Not Supported] ---- This is an initial note about installing Shairport Sync on OpenBSD. Shairport Sync compiles and runs natively on OpenBSD using the `sndio` back end. diff --git a/README.md b/README.md index 6966cce6..fe9eec75 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,23 @@ Shairport Sync ============= -* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such [ForkedDaapd](http://ejurgensen.github.io/forked-daapd/). -* Shairport Sync can be built to support AirPlay 1 only or it can be built to support Airplay 2, which includes AirPlay 1 support. +* Shairport Sync is an AirPlay audio player – it plays audio streamed from Apple devices and from AirPlay sources such as [OwnTone](https://github.com/owntone/owntone-server) (formerly `forked-daapd`). +* Shairport Sync can provide "classic" AirPlay 1 or limited Airplay 2 support. The AirPlay 2 build is much more demanding. It requires a faster system, more storage and more RAM both at build time and run time. +* Shairport Sync does not support AirPlay video or photo streaming. -AirPlay 2 Matters -==== -* The AirPlay 2 build requires a good deal of extra library support and may not fit into smaller devices. It also requires more CPU power and more RAM -- the minimum recommended device is a Raspberry Pi 3. -* **AirPlay 2 support is experimental and incomplete.** The focus of the development effort has been on getting the best possible audio performance. Many features outside that focus are missing or broken. So, for example, integration with Apple's Home app is missing; remote control doesn't work. -* For AirPlay 2, Shairport Sync requires the services of another application called [`NQPTP`](https://github.com/mikebrady/nqptp) ("Not Quite PTP") for timing and synchronisation. NQPTP must run as `root` and must have exclusive access to ports `319` and `320`. -* AirPlay 2 support is only available on recent Linux and FreeBSD builds. The FreeBSD implementation has not been extensively tested. OpenBSD and Cygwin are not supported. AirPlay 2 support will definitely not work on Mac OS X. The reason is that Shairport Sync needs to use [`NQPTP`](https://github.com/mikebrady/nqptp), which uses ports `319` and `320`. These ports are already used by Mac OS X to support its implementation of AirPlay 2. - -Did we mention AirPlay 2 support is experimental? You should also note that it is possible that changes could be made that would stop AirPlay 2 support from working. Let's hope that doesn't happen -- due to its limitations, there seems little prospect of it having any kind of commercial significance. - -What Works +AirPlay 2 --- -* AirPlay 2 audio for players hosted on macOS and iOS. -* AirPlay 2 audio for System Sound Output from macOS. -* The `ALSA`, `stdout` and `pipe` backends definitely work, as does the `sndio` backend for FreeBSD. - -What Does Not Work ---- -* Shairport Sync AirPlay 2 can not be used from a PC or from an Apple TV. -* There is no integration with Siri or with the Home app or HomeKit, so nothing to do with the Home app works. -* You can not run multiple instances of Shairport Sync on one device. This is because an instance of Shairport Sync requires exclusive access to [`NQPTP`](https://github.com/mikebrady/nqptp), and only one instance of that can run on a processor. (Docker may offer a workaround, but this has not been tested.) -* Remote Control from Shairport Sync back to the player doesn't work. The `D-Bus` and `MPRIS` interfaces are working, but anything requiring sending commands to the player doesn't work. -* Artwork is not downloaded. -* Playing from certain Apple devices to Shairport Sync does not work as expected. For example: - * When using a service with the ability to play directly on HomePod (e.g. Apple Music). If the music starts on the HomePod, or is the first device selected from the AirPlay list on iOS, when Shairport Sync is then selected iOS shows it as not connected (even though it is). Therefore the expected volume controls are not displayed. A workaround for this is to force an AirPlay stream to the HomePod (rather than just acting as a remote controller), this can be done by selected Shairport Sync _before_ the HomePod. Note, this issue is not present for other services such as SoundCloud which do not integrate into the HomePod like Apple Music does. - * The above example is true also when trying to select Shairport Sync from the AirPlay menu on the HomePod from the Home app. - -More About What Works ---- -* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Limitation: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync may get confused about which is the current master.) -* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. - -What You Need ---- -* A Raspberry Pi. See table below for tested devices, feel free to open an issue or pull request if you have Shairport Sync working on a device not listed here! -* [`NQPTP`](https://github.com/mikebrady/nqptp) is needed and must be run as `root`. It will use ports `319` and `320`, normally reserved for [PTP](https://standards.ieee.org/standard/1588-2008.html) clocks. If you are using PTP clock services for something else, you can't install [`NQPTP`](https://github.com/mikebrady/nqptp) and so you can't use Shairport Sync.(FYI, most computers do not have a PTP clock running -- instead, they may use a [Network Timing Protocol (NTP)](http://www.ntp.org) service to keep the system clock synchronised with world time. However, as noted above, macOS _does_ use a PTP clock to support AirPlay 2.) - -| Device | Native | Docker | Notes | -|---------------------|----------|----------------|-------| -| Raspberry Pi 4 | Working | Working | | -| Raspberry Pi 3 | Working | Working | | -| Raspberry Pi Zero W | Untested | Mostly working | Since the Pi Zero is low powered, issues can arise fairly quickly when trying to run additional programs, this currently includes quickly changing the volume repeatedly. Total CPU usage sits at around 75% when using Shairport Sync. Updates to Shairport Sync could make this better or worse. | +For the latest on AirPlay 2, please visit [AIRPLAY2.md](https://github.com/aillwee/shairport-sync/blob/development/AIRPLAY2.md). Guides --- -* A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). -* A guide for building on FreeBSD is forthcoming. - -Note ---- -Shairport Sync does not support AirPlay video or photo streaming. +* A brief guide to building classic Shairport Sync (for AirPlay 1) is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). +* A guide to building Shairport Sync for AirPlay 2 on Linux is available at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). +* A guide to building for AirPlay 2 on FreeBSD is forthcoming. More Information --- -For more information, and for a more complete account of how to build Shairport Sync for AirPlay 1, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). +For more information, please visit [MOREINFO.md](https://github.com/aillwee/shairport-sync/blob/development/MOREINFO.md). Acknowledgements --- @@ -68,6 +26,6 @@ For the development of AirPlay 2 support, special thanks are due to: * [ejurgensen](https://github.com/ejurgensen) for advice and [code to deal with pairing and encryption](https://github.com/ejurgensen/pair_ap). * [ckdo](https://github.com/ckdo) for pointing the way, particularly with pairing and encryption protocols, with a [functional Python implementation](https://github.com/ckdo/airplay2-receiver) of AirPlay 2. * [invano](https://github.com/invano) for showing what might be possible and for initial Python development. -* [Charles Omer](https://github.com/charlesomer) for testing, encouragement and enthusiasm. +* [Charles Omer](https://github.com/charlesomer) for Docker automation (forthcoming), testing, encouragement, enthusiasm. And of course, thanks to everyone who has supported and improved Shairport Sync over the years. diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index f64308e8..5f73973d 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -5,70 +5,17 @@ Big Update ---- Version 4 brings limited AirPlay 2 functionality to Shairport Sync. -A major feature of Shairport Sync's AirPlay 2 support is Audio Synchronisation. This is achieved using an purpose-built passive [PTP](https://standards.ieee.org/standard/1588-2008.html) client called [`nqptp`](https://github.com/mikebrady/nqptp), which provides precise timing information that Shairport Sync uses to maintain tight synchronisation. (BTW, `nqptp` is not a "PTP slave". Instead, it passively monitors PTP traffic for timing information.) +For more information about AirPlay 2, including acknowledgements, please visit [AIRPLAY2.md]. -Acknowledgements ----- -Much of Shairport Sync's AirPlay 2 functionality is based on ideas developed at the [openairplay airplay2-receiver]( https://github.com/openairplay/airplay2-receiver) repository. - -Big thanks are due to number of individuals made direct and valuable contributions to Shairport Sync: -1. [ejurgensen](https://github.com/ejurgensen) contributed ideas and code. The `pair_ap` submodule and related code in `rtsp.c` is theirs. -2. [JD Smith](https://github.com/jdtsmith) contributed ideas, comments and suggestions along with extensive and thorough testing. -3. [Charles Omer](https://github.com/charlesomer) contributed ideas, comments, bug fixes and Docker automation (forthcoming). +#### Known Issues +- In the AirPlay 2 build, a crash can occur occasionally when the player is being selected. If you can get this to happen reliably, please let us know. Warning ---- -1. Big changes have been made to the codebase to enable it to be used to build either a "traditional" AirPlay 1 player or a new AirPlay 2 player. These changes may have introduced bugs into the AirPlay 1 build. +1. Big changes have been made to the codebase to enable it to be used to build either as the "classic" AirPlay 1 player or as a new AirPlay 2 player. These changes have probably introduced bugs into the classic AirPlay 1 build. 2. There is lots we don't know about AirPlay 2. This can result in incorrect behaviour. Watch out for very high volume levels, especially if you have a powerful audio system. 3. This is a `development` branch and is probably more buggy than usual. Normal support will not be provided -- instead, we will be interested in bug reports that help improve the program. You need to be adept with Linux or FreeBSD to get involved. -AirPlay 2 Overview ----- - -#### What Works -- AirPlay 2 for iOS and Mac players. - -#### What Does Not Work -- No AirPlay 2 for Windows iTunes -- No integration with Siri or HomeKit -- No Lossless or High Definition Lossless -- No Remote Control -- No Artwork in the Metadata - -#### What Partly Works -- Incomplete functionality using AppleTV or HomePod as player. - A Shairport Sync player will work, but the selection button won't persist and the volume can't be set. - -#### Limitations -- You can only run a single instance of Shairport Sync on a device. -- The AirPlay 2 version of Shairport Sync will not run on macOS, since `nqptp` can not be installed on it. - -#### Known Issues -- A crash can occur occasionally when a Shairport Sync player is being selected for output. If you can get this to happen reliably, pleasse let us know. - -AirPlay 2 -- What You Need ---- -AirPlay 2 support needs a more powerful CPU for decoding and synchronisation and more memory for bigger buffers and larger libraries. Raspberry Pi OS and Ubuntu 20.04 on a VM have been used extensively in development, with Alpine Linux and FreeBSD 12.2 being used to a lesser extent. - -Here are some suggestions: -* Full access, including `root` privileges, to a system at least as powerful as a Raspberry Pi 3. -* A fully up-to-date Linux. This is important, as some of the libraries must be the latest available. -* An audio output, for example an `alsa` device (or `sndio` in FreeBSD). The `stdout` and `pipe` backends continue to work as before. Other backends have not been tested. - -Guides ---- -Instructions for building Shairport Sync are different for AirPlay 2 than for AirPlay 1 -- please follow them very carefully. - -* A brief guide to building Shairport Sync for AirPlay 1 is available at [BUILDFORAP1.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP1.md). -* To build Shairport Sync for AirPlay 2 on Linux, please follow the guide at [BUILDFORAP2.md](https://github.com/aillwee/shairport-sync/blob/development/BUILDFORAP2.md). -* A guide for building on FreeBSD is forthcoming. - -AirPlay 2 -- More About What Works ---- -* Two types of audio are received -- "Realtime" streams of CD quality ALAC (like AirPlay 1) and "Buffered Audio" streams of AAC stereo at 44,100 frames per second. The selection of stream type is made by the player. -* Audio is synchronised with other AirPlay 2 devices, including AirPlay 2 devices that have their own master clocks. (Note: This has not been tested with multiple nearly-identical master clock devices such as with two HomePod minis -- Shairport Sync will get confused about which is the current master.) -* Shairport Sync continues to support AirPlay 1, and offers an AirPlay 1 compatibility mode for situations where iTunes on macOS or macOS Music plays to multiple speakers and one of more of them is compatible with AirPlay 1 only. - Version 3.3.8d7 ==== **Bug Fix** diff --git a/UPDATING.md b/UPDATING.md index 66916847..73e18fb0 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,5 +1,5 @@ -### Updating Shairport Sync +### Updating Shairport Sync [Needs updating for Airplay 2] This guide is for updating an installation of Shairport Sync. If you installed Shairport Sync from a package, most of these instructions don't apply – please go to the section at the end called "Post Update Tasks" To do an update, you basically have to go through the whole process of building Shairport Sync again, diff --git a/pair_ap b/pair_ap deleted file mode 160000 index d955c447..00000000 --- a/pair_ap +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d955c447dac0545803dee9c90312c9d42310e9b1 diff --git a/pair_ap/README.md b/pair_ap/README.md new file mode 100644 index 00000000..1c855444 --- /dev/null +++ b/pair_ap/README.md @@ -0,0 +1,72 @@ +# pair_ap +C client implementation of pairing for: +* Apple TV device verification, which became mandatory with tvOS 10.2 (this is + called fruit mode in pair_ap) +* Homekit pairing (for AirPlay 2, not working for Home app) + +Credit goes to @funtax and @ViktoriiaKh for doing some of the heavy lifting. +## Requirements +- libsodium +- libgcrypt or libopenssl +- libplist (only for Apple TV device verification) + +To build the example client and server you also need libevent2. If the +dependencies are met you can build simply by running 'make'. + +## Homekit pairing +Since I haven't been able to find much information on the internet on how +Homekit pairing is designed, here is a write-up of my current understanding. If +you know better, please help improve this. + +With Homekit pairing, there may be a controller (e.g. the Home app), and a +number of devices/accessories (e.g. speakers). The controller acts as a client +and can make requests for pairing. After it is paired it can also add other +"third-party" pairings to the device, it can remove pairings and it can ask for +a list of pairings. + +Other parties, e.g. the Music app or just iOS as an Airplay sender, can also +pair with devices/accesssories in a similar manner, but they are not full- +fledged Homekit controllers and thus don't make requests for adding, removing +or listing pairings. + +The controller uses `/pair-add` to make sure that all devices on a network get +the ID and public key of all the other devices, so that the user only needs to +pair a device once. + +### Normal pairing +For a normal first-time pairing, the client needs a one-time code (the device +announces via mDNS whether a code is required). The client calls +`/pair-pin-start` and the device displays the code. There is also QR-based +pairing, which is (probably?) an encoded code. + +After obtaining the code, the client initiates a three step `/pair-setup` +sequence, which results in both peers registering each other's ID and public +key. Henceforth, a pairing is verified with the two step `/pair-verify`, where +the parties check each-others identify. Saving the peer's ID + public key isn't +strictly necessary if client or server doesn't care about verifying the peer, +i.e. that `/pair-setup` has actually been completed. + +The result of `/pair-verify` is a shared secret that is used for symmetric +encryption of the following communinacation between the parties. + +### Transient pairing +Some devices don't require a code from the user for pairing (e.g. an Airport +Express 2). If so, the client just needs to go through a two-step `/pair-setup` +sequence which results in a shared secret, which is then used for encrypted +communication. A fixed code of 3939 is used. + +Such devices don't appear to be fully Homekit compatible - they will not, for +instance - appear in the Home app. + +## "fruit" pairing +Like normal Homekit pairing, this consists of first requesting a code with +`/pair-pin-start`, then a three-step `/pair-setup` and finally a two-step +`/pair-verify`. After that the communication is encrypted with the resulting +shared secret. + + +## Acknowledgments +- [AirPlayAuth](https://github.com/funtax/AirPlayAuth) +- [AirPlayAuth-ObjC](https://github.com/ViktoriiaKh/AirPlayAuth-ObjC) +- [ap2-sender](https://github.com/ViktoriiaKh/ap2-sender) +- [csrp](https://github.com/cocagne/csrp) diff --git a/pair_ap/client-example.c b/pair_ap/client-example.c new file mode 100644 index 00000000..35e424ad --- /dev/null +++ b/pair_ap/client-example.c @@ -0,0 +1,578 @@ +#include +#include +#include +#include + +#include +#include + +#include "evrtsp/evrtsp.h" +#include "pair.h" + +#define DEVICE_ID "AABBCCDD11223344" +#define ACTIVE_REMOTE "3515324763" +#define DACP_ID "FF1DB45949E6CBD3" +#define USER_AGENT "AirPlay/381.13" + +#define ENDPOINT_SETUP_FRUIT "/pair-setup-pin" +#define CONTENTTYPE_SETUP_FRUIT "application/x-apple-binary-plist" + +#define ENDPOINT_SETUP_HOMEKIT "/pair-setup" +#define CONTENTTYPE_SETUP_HOMEKIT "application/octet-stream" + + +typedef void (*request_cb)(struct evrtsp_request *, void *); + +static struct event_base *evbase; +static struct evrtsp_connection *evcon; +static int cseq; + +static const char *endpoint_setup; +static const char *content_type_setup; + +static enum pair_type pair_type; + +static struct pair_cipher_context *cipher_ctx; +static struct pair_verify_context *verify_ctx; +static struct pair_setup_context *setup_ctx; + + +static char * +prompt_pin(void) +{ + char *pin = NULL; + size_t len; + + printf ("Enter pin: "); + fflush (stdout); + + len = getline(&pin, &len, stdin); + if (len != 5) // Includes EOL + { + printf ("Bad pin length %zu\n", len); + return NULL; + } + + return pin; +} + +static int +response_process(uint8_t **response, struct evrtsp_request *req) +{ + if (!req) + { + printf("failed, could not read response\n"); + return -1; + } + else if (req->response_code != 200) + { + printf("failed with error code %d: %s (body is %zu bytes)\n\n", req->response_code, req->response_code_line, evbuffer_get_length(req->input_buffer)); + return -1; + } + + printf("success\n\n"); + + *response = evbuffer_pullup(req->input_buffer, -1); + + return evbuffer_get_length(req->input_buffer); +} + +static int +make_request(const char *url, const void *data, size_t len, const char *content_type, request_cb cb) +{ + struct evrtsp_request *req; + char buffer[1024]; + + req = evrtsp_request_new(cb, NULL); + + if (data) + evbuffer_add(req->output_buffer, data, len); + + if (content_type) + evrtsp_add_header(req->output_headers, "Content-Type", content_type); + + cseq++; + snprintf(buffer, sizeof(buffer), "%d", cseq); + evrtsp_add_header(req->output_headers, "CSeq", buffer); + + evrtsp_add_header(req->output_headers, "User-Agent", USER_AGENT); +// evrtsp_add_header(req->output_headers, "DACP-ID", DACP_ID); +// evrtsp_add_header(req->output_headers, "Active-Remote", ACTIVE_REMOTE); + + if (pair_type == PAIR_CLIENT_HOMEKIT_NORMAL) + evrtsp_add_header(req->output_headers, "X-Apple-HKP", "3"); + else if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT) + evrtsp_add_header(req->output_headers, "X-Apple-HKP", "4"); + + printf("Making request %d to '%s'... ", cseq, url); + + return evrtsp_make_request(evcon, req, EVRTSP_REQ_POST, url); +} + +static int +make_request_options(const char *url, const void *data, size_t len, const char *content_type, request_cb cb) +{ + struct evrtsp_request *req; + char buffer[1024]; + + req = evrtsp_request_new(cb, NULL); + + if (data) + evbuffer_add(req->output_buffer, data, len); + + if (content_type) + evrtsp_add_header(req->output_headers, "Content-Type", content_type); + + cseq++; + snprintf(buffer, sizeof(buffer), "%d", cseq); + evrtsp_add_header(req->output_headers, "CSeq", buffer); + + evrtsp_add_header(req->output_headers, "User-Agent", USER_AGENT); +// evrtsp_add_header(req->output_headers, "DACP-ID", DACP_ID); +// evrtsp_add_header(req->output_headers, "Active-Remote", ACTIVE_REMOTE); + evrtsp_add_header(req->output_headers, "X-Apple-HKP", "3"); + + printf("Making request %d to '%s'... ", cseq, url); + + return evrtsp_make_request(evcon, req, EVRTSP_REQ_OPTIONS, url); +} + + +static void +options_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + int ret; + + ret = response_process(&response, req); + if (ret >= 0) + printf("OPTIONS complete\n"); + + printf("Done\n"); + + event_base_loopbreak(evbase); +} + +static int +options_request(void) +{ + return make_request_options("*", NULL, 0, NULL, options_response); +} + +static void +rtsp_cipher(struct evbuffer *evbuf, void *arg, int encrypt) +{ + uint8_t *out = NULL; + size_t out_len = 0; + ssize_t processed; + + uint8_t *in = evbuffer_pullup(evbuf, -1); + size_t in_len = evbuffer_get_length(evbuf); + + if (encrypt) + processed = pair_encrypt(&out, &out_len, in, in_len, cipher_ctx); + else + processed = pair_decrypt(&out, &out_len, in, in_len, cipher_ctx); + + evbuffer_drain(evbuf, in_len); + + if (processed < 0) + { + printf("Error while ciphering: %s\n", pair_cipher_errmsg(cipher_ctx)); + return; + } + else if (processed != in_len) + { + printf("Partial ciphering, only %zd of %zu input bytes were processed\n", processed, in_len); + } + + evbuffer_add(evbuf, out, out_len); +} + +static void +verify_step2_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + struct pair_result *result; + int ret; + + ret = response_process(&response, req); + if (ret < 0) + goto error; + + ret = pair_verify_response2(verify_ctx, response, ret); + if (ret < 0) + goto error; + + printf("Verify complete!\n\n"); + + ret = pair_verify_result(&result, verify_ctx); + if (ret < 0) + goto error; + + cipher_ctx = pair_cipher_new(pair_type, 0, result->shared_secret, result->shared_secret_len); + if (!cipher_ctx) + goto error; + + evrtsp_connection_set_ciphercb(evcon, rtsp_cipher, NULL); + + ret = options_request(); + if (ret < 0) + goto error; + + pair_verify_free(verify_ctx); + + return; + + error: + printf("Error: %s\n", pair_verify_errmsg(verify_ctx)); + pair_verify_free(verify_ctx); + pair_cipher_free(cipher_ctx); + event_base_loopbreak(evbase); +} + +static int +verify_step2_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_verify_request2(&len, verify_ctx); + if (!request) + return -1; + + ret = make_request("/pair-verify", request, len, "application/octet-stream", verify_step2_response); + + free(request); + + return ret; +} + +static void +verify_step1_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_verify_response1(verify_ctx, response, ret); + if (ret < 0) + goto error; + + ret = verify_step2_request(); + if (ret < 0) + goto error; + + return; + + error: + printf("Error: %s\n", pair_verify_errmsg(verify_ctx)); + pair_verify_free(verify_ctx); + event_base_loopbreak(evbase); +} + +static int +verify_step1_request(const char *authorisation_key) +{ + uint8_t *request = NULL; + size_t len; + int ret; + + verify_ctx = pair_verify_new(pair_type, authorisation_key, NULL, NULL, DEVICE_ID); + if (!verify_ctx) + return -1; + + request = pair_verify_request1(&len, verify_ctx); + if (!request) + goto error; + + ret = make_request("/pair-verify", request, len, "application/octet-stream", verify_step1_response); + if (ret < 0) + goto error; + + free(request); + return ret; + + error: + printf("Error: %s\n", pair_verify_errmsg(verify_ctx)); + pair_verify_free(verify_ctx); + free(request); + return -1; +} + +static void +setup_step3_response(struct evrtsp_request *req, void *arg) +{ + const char *key; + uint8_t *response; + struct pair_result *result; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_setup_response3(setup_ctx, response, ret); + if (ret < 0) + goto error; + + ret = pair_setup_result(&key, &result, setup_ctx); + if (ret < 0) + goto error; + + printf("Setup of device ID %s complete, got key: %s\n", result->device_id, key); + + ret = verify_step1_request(key); + if (ret < 0) + goto error; + + pair_setup_free(setup_ctx); + + return; + + error: + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_step3_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_setup_request3(&len, setup_ctx); + if (!request) + return -1; + + ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step3_response); + + free(request); + + return ret; +} + +static void +setup_step2_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + struct pair_result *result; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_setup_response2(setup_ctx, response, ret); + if (ret < 0) + goto error; + + printf("Setup SRP stage complete\n"); + + if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT) + { + ret = pair_setup_result(NULL, &result, setup_ctx); + if (ret < 0) + goto error; + + cipher_ctx = pair_cipher_new(pair_type, 0, result->shared_secret, result->shared_secret_len); + if (!cipher_ctx) + goto error; + + evrtsp_connection_set_ciphercb(evcon, rtsp_cipher, NULL); + + ret = options_request(); + if (ret < 0) + goto error; + + pair_setup_free(setup_ctx); + + return; + } + + ret = setup_step3_request(); + if (ret < 0) + goto error; + + return; + + error: + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_step2_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_setup_request2(&len, setup_ctx); + if (!request) + return -1; + + ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step2_response); + + free(request); + + return ret; +} + +static void +setup_step1_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + int ret; + + ret = response_process(&response, req); + if (ret <= 0) + goto error; + + ret = pair_setup_response1(setup_ctx, response, ret); + if (ret < 0) + goto error; + + ret = setup_step2_request(); + if (ret < 0) + goto error; + + return; + + error: + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_step1_request(void) +{ + uint8_t *request; + size_t len; + int ret; + + request = pair_setup_request1(&len, setup_ctx); + if (!request) + return -1; + + ret = make_request(endpoint_setup, request, len, content_type_setup, setup_step1_response); + + free(request); + return ret; +} + +static void +setup_start_response(struct evrtsp_request *req, void *arg) +{ + uint8_t *response; + char *pin = NULL; + int ret; + + if (req) + { + ret = response_process(&response, req); + if (ret < 0) + goto error; + } + + if (pair_type != PAIR_CLIENT_HOMEKIT_TRANSIENT) + { + pin = prompt_pin(); + if (!pin) + goto error; + } + + setup_ctx = pair_setup_new(pair_type, pin, NULL, NULL, DEVICE_ID); + if (!setup_ctx) + goto error; + + ret = setup_step1_request(); + if (ret < 0) + goto error; + + free(pin); + return; + + error: + if (setup_ctx) + printf("Error: %s\n", pair_setup_errmsg(setup_ctx)); + free(pin); + pair_setup_free(setup_ctx); + event_base_loopbreak(evbase); +} + +static int +setup_start_request(void) +{ + return make_request("/pair-pin-start", NULL, 0, "application/x-apple-binary-plist", setup_start_response); +} + + +int +main( int argc, char * argv[] ) +{ + int ret; + + if (argc < 4 || argc > 5) + { + printf("%s ip_address port homekit|fruit|transient [skip_pin]\n", argv[0]); + return -1; + } + + const char *address = argv[1]; + const char *port = argv[2]; + int skip_pin = (argc == 5); + + if (strcmp(argv[3], "fruit") == 0) + { + printf("Pair type is fruit\n"); + pair_type = PAIR_CLIENT_FRUIT; + endpoint_setup = ENDPOINT_SETUP_FRUIT; + content_type_setup = CONTENTTYPE_SETUP_FRUIT; + } + else if (strcmp(argv[3], "homekit") == 0) + { + printf("Pair type is homekit (normal)\n"); + pair_type = PAIR_CLIENT_HOMEKIT_NORMAL; + endpoint_setup = ENDPOINT_SETUP_HOMEKIT; + content_type_setup = CONTENTTYPE_SETUP_HOMEKIT; + } + else if (strcmp(argv[3], "transient") == 0) + { + printf("Pair type is homekit (transient)\n"); + pair_type = PAIR_CLIENT_HOMEKIT_TRANSIENT; + endpoint_setup = ENDPOINT_SETUP_HOMEKIT; + content_type_setup = CONTENTTYPE_SETUP_HOMEKIT; + } + + evbase = event_base_new(); + evcon = evrtsp_connection_new(address, atoi(port)); + evrtsp_connection_set_base(evcon, evbase); + + if (pair_type == PAIR_CLIENT_HOMEKIT_TRANSIENT || skip_pin) + { + setup_start_response(NULL, NULL); + } + else + { + ret = setup_start_request(); + if (ret < 0) + goto the_end; + } + + event_base_dispatch(evbase); + + the_end: + evrtsp_connection_free(evcon); + event_base_free(evbase); + + return 0; +} diff --git a/pair_ap/pair-internal.h b/pair_ap/pair-internal.h new file mode 100644 index 00000000..e7cd85e3 --- /dev/null +++ b/pair_ap/pair-internal.h @@ -0,0 +1,381 @@ +#include +#include +#include + +#include "pair.h" + +#define RETURN_ERROR(s, m) \ + do { handle->status = (s); handle->errmsg = (m); goto error; } while(0) + + +struct SRPUser; +struct SRPVerifier; + +struct pair_client_setup_context +{ + struct SRPUser *user; + + uint8_t pin[4]; + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + pair_cb add_cb; + void *add_cb_arg; + + uint8_t public_key[crypto_sign_PUBLICKEYBYTES]; + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + + const uint8_t *pkA; + int pkA_len; + + uint8_t *pkB; + uint64_t pkB_len; + + const uint8_t *M1; + int M1_len; + + uint8_t *M2; + uint64_t M2_len; + + uint8_t *salt; + uint64_t salt_len; + + // We don't actually use the server's epk and authtag for anything + uint8_t *epk; + uint64_t epk_len; + uint8_t *authtag; + uint64_t authtag_len; +}; + +struct pair_server_setup_context +{ + struct SRPVerifier *verifier; + + uint8_t pin[4]; + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + pair_cb add_cb; + void *add_cb_arg; + + uint8_t public_key[crypto_sign_PUBLICKEYBYTES]; + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + + bool is_transient; + + uint8_t *pkA; + uint64_t pkA_len; + + uint8_t *pkB; + int pkB_len; + + uint8_t *b; + int b_len; + + uint8_t *M1; + uint64_t M1_len; + + const uint8_t *M2; + int M2_len; + + uint8_t *v; + int v_len; + + uint8_t *salt; + int salt_len; +}; + +enum pair_status +{ + PAIR_STATUS_IN_PROGRESS, + PAIR_STATUS_COMPLETED, + PAIR_STATUS_AUTH_FAILED, + PAIR_STATUS_INVALID, +}; + +struct pair_setup_context +{ + struct pair_definition *type; + + enum pair_status status; + const char *errmsg; + + struct pair_result result; + char result_str[256]; // Holds the hex string version of the keys that pair_verify_new() needs + + // Hex-formatet concatenation of public + private, 0-terminated + char auth_key[2 * (crypto_sign_PUBLICKEYBYTES + crypto_sign_SECRETKEYBYTES) + 1]; + + union pair_setup_union + { + struct pair_client_setup_context client; + struct pair_server_setup_context server; + } sctx; +}; + +struct pair_client_verify_context +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + // These are the keys that were registered with the server in pair-setup + uint8_t client_public_key[crypto_sign_PUBLICKEYBYTES]; // 32 + uint8_t client_private_key[crypto_sign_SECRETKEYBYTES]; // 64 + + bool verify_server_signature; + uint8_t server_fruit_public_key[64]; // Not sure why it has this length in fruit mode + uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES]; // 32 + + // For establishing the shared secret for encrypted communication + uint8_t client_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + uint8_t client_eph_private_key[crypto_box_SECRETKEYBYTES]; // 32 + + uint8_t server_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + + uint8_t shared_secret[crypto_scalarmult_BYTES]; // 32 +}; + +struct pair_server_verify_context +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + + // Same keys as used for pair-setup, derived from device_id + uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES]; // 32 + uint8_t server_private_key[crypto_sign_SECRETKEYBYTES]; // 64 + + bool verify_client_signature; + pair_cb get_cb; + void *get_cb_arg; + + // For establishing the shared secret for encrypted communication + uint8_t server_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + uint8_t server_eph_private_key[crypto_box_SECRETKEYBYTES]; // 32 + + uint8_t client_eph_public_key[crypto_box_PUBLICKEYBYTES]; // 32 + + uint8_t shared_secret[crypto_scalarmult_BYTES]; // 32 +}; + +struct pair_verify_context +{ + struct pair_definition *type; + + enum pair_status status; + const char *errmsg; + + struct pair_result result; + + union pair_verify_union + { + struct pair_client_verify_context client; + struct pair_server_verify_context server; + } vctx; +}; + +struct pair_cipher_context +{ + struct pair_definition *type; + + uint8_t encryption_key[32]; + uint8_t decryption_key[32]; + + uint64_t encryption_counter; + uint64_t decryption_counter; + + // For rollback + uint64_t encryption_counter_prev; + uint64_t decryption_counter_prev; + + const char *errmsg; +}; + +struct pair_definition +{ + int (*pair_setup_new)(struct pair_setup_context *sctx, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id); + void (*pair_setup_free)(struct pair_setup_context *sctx); + int (*pair_setup_result)(struct pair_setup_context *sctx); + + uint8_t *(*pair_setup_request1)(size_t *len, struct pair_setup_context *sctx); + uint8_t *(*pair_setup_request2)(size_t *len, struct pair_setup_context *sctx); + uint8_t *(*pair_setup_request3)(size_t *len, struct pair_setup_context *sctx); + + int (*pair_setup_response1)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + int (*pair_setup_response2)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + int (*pair_setup_response3)(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + + int (*pair_verify_new)(struct pair_verify_context *vctx, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id); + void (*pair_verify_free)(struct pair_verify_context *vctx); + int (*pair_verify_result)(struct pair_verify_context *vctx); + + uint8_t *(*pair_verify_request1)(size_t *len, struct pair_verify_context *vctx); + uint8_t *(*pair_verify_request2)(size_t *len, struct pair_verify_context *vctx); + + int (*pair_verify_response1)(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); + int (*pair_verify_response2)(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); + + int (*pair_add)(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len); + int (*pair_remove)(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len); + int (*pair_list)(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, const uint8_t *in, size_t in_len); + + struct pair_cipher_context *(*pair_cipher_new)(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len); + void (*pair_cipher_free)(struct pair_cipher_context *cctx); + + ssize_t (*pair_encrypt)(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx); + ssize_t (*pair_decrypt)(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx); + + int (*pair_state_get)(const char **errmsg, const uint8_t *in, size_t in_len); + void (*pair_public_key_get)(uint8_t server_public_key[32], const char *device_id); +}; + + +/* -------------------- GCRYPT AND OPENSSL COMPABILITY --------------------- */ +/* partly borrowed from ffmpeg (rtmpdh.c) */ + +#if CONFIG_GCRYPT +#include +#define SHA512_DIGEST_LENGTH 64 +#define bnum_new(bn) \ + do { \ + if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { \ + if (!gcry_check_version("1.5.4")) \ + abort(); \ + gcry_control(GCRYCTL_DISABLE_SECMEM, 0); \ + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); \ + } \ + bn = gcry_mpi_new(1); \ + } while (0) +#define bnum_free(bn) gcry_mpi_release(bn) +#define bnum_num_bytes(bn) (gcry_mpi_get_nbits(bn) + 7) / 8 +#define bnum_is_zero(bn) (gcry_mpi_cmp_ui(bn, (unsigned long)0) == 0) +#define bnum_bn2bin(bn, buf, len) gcry_mpi_print(GCRYMPI_FMT_USG, buf, len, NULL, bn) +#define bnum_bin2bn(bn, buf, len) gcry_mpi_scan(&bn, GCRYMPI_FMT_USG, buf, len, NULL) +#define bnum_hex2bn(bn, buf) gcry_mpi_scan(&bn, GCRYMPI_FMT_HEX, buf, 0, 0) +#define bnum_random(bn, num_bits) gcry_mpi_randomize(bn, num_bits, GCRY_WEAK_RANDOM) +#define bnum_add(bn, a, b) gcry_mpi_add(bn, a, b) +#define bnum_sub(bn, a, b) gcry_mpi_sub(bn, a, b) +#define bnum_mul(bn, a, b) gcry_mpi_mul(bn, a, b) +#define bnum_mod(bn, a, b) gcry_mpi_mod(bn, a, b) +typedef gcry_mpi_t bnum; +__attribute__((unused)) static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p) +{ + gcry_mpi_powm(bn, y, q, p); +} +__attribute__((unused)) static void bnum_modadd(bnum bn, bnum a, bnum b, bnum m) +{ + gcry_mpi_addm(bn, a, b, m); +} +#elif CONFIG_OPENSSL +#include +#include +#include +#include +#include +#define bnum_new(bn) bn = BN_new() +#define bnum_free(bn) BN_free(bn) +#define bnum_num_bytes(bn) BN_num_bytes(bn) +#define bnum_is_zero(bn) BN_is_zero(bn) +#define bnum_bn2bin(bn, buf, len) BN_bn2bin(bn, buf) +#define bnum_bin2bn(bn, buf, len) bn = BN_bin2bn(buf, len, 0) +#define bnum_hex2bn(bn, buf) BN_hex2bn(&bn, buf) +#define bnum_random(bn, num_bits) BN_rand(bn, num_bits, 0, 0) +#define bnum_add(bn, a, b) BN_add(bn, a, b) +#define bnum_sub(bn, a, b) BN_sub(bn, a, b) +typedef BIGNUM* bnum; +__attribute__((unused)) static void bnum_mul(bnum bn, bnum a, bnum b) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mul(bn, a, b, ctx); + BN_CTX_free(ctx); +} +__attribute__((unused)) static void bnum_mod(bnum bn, bnum a, bnum b) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mod(bn, a, b, ctx); + BN_CTX_free(ctx); +} +__attribute__((unused)) static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mod_exp(bn, y, q, p, ctx); + BN_CTX_free(ctx); +} +__attribute__((unused)) static void bnum_modadd(bnum bn, bnum a, bnum b, bnum m) +{ + // No error handling + BN_CTX *ctx = BN_CTX_new(); + BN_mod_add(bn, a, b, m, ctx); + BN_CTX_free(ctx); +} +#endif + + +/* -------------------------- SHARED HASHING HELPERS ------------------------ */ + +#ifdef CONFIG_OPENSSL +enum hash_alg +{ + HASH_SHA1, + HASH_SHA224, + HASH_SHA256, + HASH_SHA384, + HASH_SHA512, +}; +#elif CONFIG_GCRYPT +enum hash_alg +{ + HASH_SHA1 = GCRY_MD_SHA1, + HASH_SHA224 = GCRY_MD_SHA224, + HASH_SHA256 = GCRY_MD_SHA256, + HASH_SHA384 = GCRY_MD_SHA384, + HASH_SHA512 = GCRY_MD_SHA512, +}; +#endif + +#if CONFIG_OPENSSL +typedef union +{ + SHA_CTX sha; + SHA256_CTX sha256; + SHA512_CTX sha512; +} HashCTX; +#elif CONFIG_GCRYPT +typedef gcry_md_hd_t HashCTX; +#endif + +int +hash_init(enum hash_alg alg, HashCTX *c); + +int +hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len); + +int +hash_final(enum hash_alg alg, HashCTX *c, unsigned char *md); + +unsigned char * +hash(enum hash_alg alg, const unsigned char *d, size_t n, unsigned char *md); + +int +hash_length(enum hash_alg alg); + +int +hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len); + +bnum +H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2); + +bnum +H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes); + +void +update_hash_n(enum hash_alg alg, HashCTX *ctx, const bnum n); + +void +hash_num(enum hash_alg alg, const bnum n, unsigned char *dest); + + +/* ----------------------------- OTHER HELPERS -------------------------------*/ + +#ifdef DEBUG_PAIR +void +hexdump(const char *msg, uint8_t *mem, size_t len); +#endif diff --git a/pair_ap/pair-tlv.c b/pair_ap/pair-tlv.c new file mode 100644 index 00000000..ea1a7614 --- /dev/null +++ b/pair_ap/pair-tlv.c @@ -0,0 +1,218 @@ +/* + * TLV helpers are adapted from ESP homekit: + * + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include +#include +#include + +#include "pair-tlv.h" +#include "pair-internal.h" + + +static int +tlv_add_value_(pair_tlv_values_t *values, uint8_t type, uint8_t *value, size_t size) { + pair_tlv_t *tlv = malloc(sizeof(pair_tlv_t)); + if (!tlv) { + return PAIR_TLV_ERROR_MEMORY; + } + tlv->type = type; + tlv->size = size; + tlv->value = value; + tlv->next = NULL; + + if (!values->head) { + values->head = tlv; + } else { + pair_tlv_t *t = values->head; + while (t->next) { + t = t->next; + } + t->next = tlv; + } + + return 0; +} + + +pair_tlv_values_t * +pair_tlv_new() { + pair_tlv_values_t *values = malloc(sizeof(pair_tlv_values_t)); + if (!values) + return NULL; + + values->head = NULL; + return values; +} + +void +pair_tlv_free(pair_tlv_values_t *values) { + pair_tlv_t *t = values->head; + while (t) { + pair_tlv_t *t2 = t; + t = t->next; + if (t2->value) + free(t2->value); + free(t2); + } + free(values); +} + +int +pair_tlv_add_value(pair_tlv_values_t *values, uint8_t type, const uint8_t *value, size_t size) { + uint8_t *data = NULL; + int ret; + if (size) { + data = malloc(size); + if (!data) { + return PAIR_TLV_ERROR_MEMORY; + } + memcpy(data, value, size); + } + ret = tlv_add_value_(values, type, data, size); + if (ret < 0) + free(data); + return ret; +} + +pair_tlv_t * +pair_tlv_get_value(const pair_tlv_values_t *values, uint8_t type) { + pair_tlv_t *t = values->head; + while (t) { + if (t->type == type) + return t; + t = t->next; + } + return NULL; +} + +int +pair_tlv_format(const pair_tlv_values_t *values, uint8_t *buffer, size_t *size) { + size_t required_size = 0; + pair_tlv_t *t = values->head; + while (t) { + required_size += t->size + 2 * ((t->size + 254) / 255); + t = t->next; + } + + if (*size < required_size) { + *size = required_size; + return PAIR_TLV_ERROR_INSUFFICIENT_SIZE; + } + + *size = required_size; + + t = values->head; + while (t) { + uint8_t *data = t->value; + if (!t->size) { + buffer[0] = t->type; + buffer[1] = 0; + buffer += 2; + t = t->next; + continue; + } + + size_t remaining = t->size; + + while (remaining) { + buffer[0] = t->type; + size_t chunk_size = (remaining > 255) ? 255 : remaining; + buffer[1] = chunk_size; + memcpy(&buffer[2], data, chunk_size); + remaining -= chunk_size; + buffer += chunk_size + 2; + data += chunk_size; + } + + t = t->next; + } + + return 0; +} + +int +pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values) { + size_t i = 0; + int ret; + while (i < length) { + uint8_t type = buffer[i]; + size_t size = 0; + uint8_t *data = NULL; + + // scan TLVs to accumulate total size of subsequent TLVs with same type (chunked data) + size_t j = i; + while (j < length && buffer[j] == type && buffer[j+1] == 255) { + size_t chunk_size = buffer[j+1]; + size += chunk_size; + j += chunk_size + 2; + } + if (j < length && buffer[j] == type) { + size_t chunk_size = buffer[j+1]; + size += chunk_size; + } + + // allocate memory to hold all pieces of chunked data and copy data there + if (size != 0) { + data = malloc(size); + if (!data) + return PAIR_TLV_ERROR_MEMORY; + + uint8_t *p = data; + + size_t remaining = size; + while (remaining) { + size_t chunk_size = buffer[i+1]; + memcpy(p, &buffer[i+2], chunk_size); + p += chunk_size; + i += chunk_size + 2; + remaining -= chunk_size; + } + } + + ret = tlv_add_value_(values, type, data, size); + if (ret < 0) { + free(data); + return ret; + } + } + + return 0; +} + +#ifdef DEBUG_PAIR +void +pair_tlv_debug(const pair_tlv_values_t *values) +{ + printf("Received TLV values\n"); + for (pair_tlv_t *t=values->head; t; t=t->next) + { + printf("Type %d value (%zu bytes): \n", t->type, t->size); + hexdump("", t->value, t->size); + } +} +#endif + diff --git a/pair_ap/pair-tlv.h b/pair_ap/pair-tlv.h new file mode 100644 index 00000000..6ef8db20 --- /dev/null +++ b/pair_ap/pair-tlv.h @@ -0,0 +1,77 @@ +#ifndef __PAIR_AP_TLV_H__ +#define __PAIR_AP_TLV_H__ + +#include + +#define PAIR_TLV_ERROR_MEMORY -1 +#define PAIR_TLV_ERROR_INSUFFICIENT_SIZE -2 + +typedef enum { + TLVType_Method = 0, // (integer) Method to use for pairing. See PairMethod + TLVType_Identifier = 1, // (UTF-8) Identifier for authentication + TLVType_Salt = 2, // (bytes) 16+ bytes of random salt + TLVType_PublicKey = 3, // (bytes) Curve25519, SRP public key or signed Ed25519 key + TLVType_Proof = 4, // (bytes) Ed25519 or SRP proof + TLVType_EncryptedData = 5, // (bytes) Encrypted data with auth tag at end + TLVType_State = 6, // (integer) State of the pairing process. 1=M1, 2=M2, etc. + TLVType_Error = 7, // (integer) Error code. Must only be present if error code is + // not 0. See TLVError + TLVType_RetryDelay = 8, // (integer) Seconds to delay until retrying a setup code + TLVType_Certificate = 9, // (bytes) X.509 Certificate + TLVType_Signature = 10, // (bytes) Ed25519 + TLVType_Permissions = 11, // (integer) Bit value describing permissions of the controller + // being added. + // None (0x00): Regular user + // Bit 1 (0x01): Admin that is able to add and remove + // pairings against the accessory + TLVType_FragmentData = 13, // (bytes) Non-last fragment of data. If length is 0, + // it's an ACK. + TLVType_FragmentLast = 14, // (bytes) Last fragment of data + TLVType_Flags = 19, // Added from airplay2_receiver + TLVType_Separator = 0xff, +} TLVType; + + +typedef enum { + TLVError_Unknown = 1, // Generic error to handle unexpected errors + TLVError_Authentication = 2, // Setup code or signature verification failed + TLVError_Backoff = 3, // Client must look at the retry delay TLV item and + // wait that many seconds before retrying + TLVError_MaxPeers = 4, // Server cannot accept any more pairings + TLVError_MaxTries = 5, // Server reached its maximum number of + // authentication attempts + TLVError_Unavailable = 6, // Server pairing method is unavailable + TLVError_Busy = 7, // Server is busy and cannot accept a pairing + // request at this time +} TLVError; + +typedef struct _tlv { + struct _tlv *next; + uint8_t type; + uint8_t *value; + size_t size; +} pair_tlv_t; + + +typedef struct { + pair_tlv_t *head; +} pair_tlv_values_t; + + +pair_tlv_values_t *pair_tlv_new(); + +void pair_tlv_free(pair_tlv_values_t *values); + +int pair_tlv_add_value(pair_tlv_values_t *values, uint8_t type, const uint8_t *value, size_t size); + +pair_tlv_t *pair_tlv_get_value(const pair_tlv_values_t *values, uint8_t type); + +int pair_tlv_format(const pair_tlv_values_t *values, uint8_t *buffer, size_t *size); + +int pair_tlv_parse(const uint8_t *buffer, size_t length, pair_tlv_values_t *values); + +#ifdef DEBUG_PAIR +void pair_tlv_debug(const pair_tlv_values_t *values); +#endif + +#endif /* !__PAIR_AP_TLV_H__ */ diff --git a/pair_ap/pair.c b/pair_ap/pair.c new file mode 100644 index 00000000..6908e8bd --- /dev/null +++ b/pair_ap/pair.c @@ -0,0 +1,771 @@ +/* + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include +#include +#include // for isprint() + +#include + +#include "pair.h" +#include "pair-internal.h" + +extern struct pair_definition pair_client_fruit; +extern struct pair_definition pair_client_homekit_normal; +extern struct pair_definition pair_client_homekit_transient; +extern struct pair_definition pair_server_homekit; + +// Must be in sync with enum pair_type +static struct pair_definition *pair[] = { + &pair_client_fruit, + &pair_client_homekit_normal, + &pair_client_homekit_transient, + &pair_server_homekit, +}; + +/* -------------------------- SHARED HASHING HELPERS ------------------------ */ + +int +hash_init(enum hash_alg alg, HashCTX *c) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1_Init(&c->sha); + case HASH_SHA224: return SHA224_Init(&c->sha256); + case HASH_SHA256: return SHA256_Init(&c->sha256); + case HASH_SHA384: return SHA384_Init(&c->sha512); + case HASH_SHA512: return SHA512_Init(&c->sha512); + default: + return -1; + }; +#elif CONFIG_GCRYPT + gcry_error_t err; + + err = gcry_md_open(c, alg, 0); + + if (err) + return -1; + + return 0; +#endif +} + +int +hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1_Update(&c->sha, data, len); + case HASH_SHA224: return SHA224_Update(&c->sha256, data, len); + case HASH_SHA256: return SHA256_Update(&c->sha256, data, len); + case HASH_SHA384: return SHA384_Update(&c->sha512, data, len); + case HASH_SHA512: return SHA512_Update(&c->sha512, data, len); + default: + return -1; + }; +#elif CONFIG_GCRYPT + gcry_md_write(*c, data, len); + return 0; +#endif +} + +int +hash_final(enum hash_alg alg, HashCTX *c, unsigned char *md) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1_Final(md, &c->sha); + case HASH_SHA224: return SHA224_Final(md, &c->sha256); + case HASH_SHA256: return SHA256_Final(md, &c->sha256); + case HASH_SHA384: return SHA384_Final(md, &c->sha512); + case HASH_SHA512: return SHA512_Final(md, &c->sha512); + default: + return -1; + }; +#elif CONFIG_GCRYPT + unsigned char *buf = gcry_md_read(*c, alg); + if (!buf) + return -1; + + memcpy(md, buf, gcry_md_get_algo_dlen(alg)); + gcry_md_close(*c); + return 0; +#endif +} + +unsigned char * +hash(enum hash_alg alg, const unsigned char *d, size_t n, unsigned char *md) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA1(d, n, md); + case HASH_SHA224: return SHA224(d, n, md); + case HASH_SHA256: return SHA256(d, n, md); + case HASH_SHA384: return SHA384(d, n, md); + case HASH_SHA512: return SHA512(d, n, md); + default: + return NULL; + }; +#elif CONFIG_GCRYPT + gcry_md_hash_buffer(alg, md, d, n); + return md; +#endif +} + +int +hash_length(enum hash_alg alg) +{ +#if CONFIG_OPENSSL + switch (alg) + { + case HASH_SHA1 : return SHA_DIGEST_LENGTH; + case HASH_SHA224: return SHA224_DIGEST_LENGTH; + case HASH_SHA256: return SHA256_DIGEST_LENGTH; + case HASH_SHA384: return SHA384_DIGEST_LENGTH; + case HASH_SHA512: return SHA512_DIGEST_LENGTH; + default: + return -1; + }; +#elif CONFIG_GCRYPT + return gcry_md_get_algo_dlen(alg); +#endif +} + +int +hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len) +{ + HashCTX ctx; + + hash_init(alg, &ctx); + hash_update(alg, &ctx, m1, m1_len); + hash_update(alg, &ctx, m2, m2_len); + return hash_final(alg, &ctx, md); +} + +bnum +H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2) +{ + bnum bn; + unsigned char *bin; + unsigned char buff[SHA512_DIGEST_LENGTH]; + int len_n1 = bnum_num_bytes(n1); + int len_n2 = bnum_num_bytes(n2); + int nbytes = 2 * len_n1; + + if ((len_n2 < 1) || (len_n2 > len_n1)) + return 0; + + bin = calloc( 1, nbytes ); + + bnum_bn2bin(n1, bin, len_n1); + bnum_bn2bin(n2, bin + nbytes - len_n2, len_n2); + hash( alg, bin, nbytes, buff ); + free(bin); + bnum_bin2bn(bn, buff, hash_length(alg)); + return bn; +} + +bnum +H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes) +{ + bnum bn; + unsigned char buff[SHA512_DIGEST_LENGTH]; + int len_n = bnum_num_bytes(n); + int nbytes = len_n + len_bytes; + unsigned char *bin = malloc(nbytes); + + bnum_bn2bin(n, bin, len_n); + memcpy( bin + len_n, bytes, len_bytes ); + hash( alg, bin, nbytes, buff ); + free(bin); + bnum_bin2bn(bn, buff, hash_length(alg)); + return bn; +} + +void +update_hash_n(enum hash_alg alg, HashCTX *ctx, const bnum n) +{ + unsigned long len = bnum_num_bytes(n); + unsigned char *n_bytes = malloc(len); + + bnum_bn2bin(n, n_bytes, len); + hash_update(alg, ctx, n_bytes, len); + free(n_bytes); +} + +void +hash_num(enum hash_alg alg, const bnum n, unsigned char *dest) +{ + int nbytes = bnum_num_bytes(n); + unsigned char *bin = malloc(nbytes); + + bnum_bn2bin(n, bin, nbytes); + hash( alg, bin, nbytes, dest ); + free(bin); +} + + +/* ----------------------------- OTHER HELPERS -------------------------------*/ + +#ifdef DEBUG_PAIR +void +hexdump(const char *msg, uint8_t *mem, size_t len) +{ + int i, j; + int hexdump_cols = 16; + + if (msg) + printf("%s", msg); + + for (i = 0; i < len + ((len % hexdump_cols) ? (hexdump_cols - len % hexdump_cols) : 0); i++) + { + if(i % hexdump_cols == 0) + printf("0x%06x: ", i); + + if (i < len) + printf("%02x ", 0xFF & ((char*)mem)[i]); + else + printf(" "); + + if (i % hexdump_cols == (hexdump_cols - 1)) + { + for (j = i - (hexdump_cols - 1); j <= i; j++) + { + if (j >= len) + putchar(' '); + else if (isprint(((char*)mem)[j])) + putchar(0xFF & ((char*)mem)[j]); + else + putchar('.'); + } + + putchar('\n'); + } + } +} +#endif + + +/* ----------------------------------- API -----------------------------------*/ + +struct pair_setup_context * +pair_setup_new(enum pair_type type, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_setup_context *sctx; + + if (!pair[type]->pair_setup_new) + return NULL; + + sctx = calloc(1, sizeof(struct pair_setup_context)); + if (!sctx) + return NULL; + + sctx->type = pair[type]; + + if (pair[type]->pair_setup_new(sctx, pin, add_cb, cb_arg, device_id) < 0) + { + free(sctx); + return NULL; + } + + return sctx; +} + +void +pair_setup_free(struct pair_setup_context *sctx) +{ + if (!sctx) + return; + + if (sctx->type->pair_setup_free) + sctx->type->pair_setup_free(sctx); + + free(sctx); +} + +const char * +pair_setup_errmsg(struct pair_setup_context *sctx) +{ + return sctx->errmsg; +} + +int +pair_setup(uint8_t **out, size_t *out_len, struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + int state; + int ret = -1; + + if (!sctx->type->pair_state_get) + { + sctx->errmsg = "Getting pair state unsupported"; + return -1; + } + + *out = NULL; + *out_len = 0; + + state = sctx->type->pair_state_get(&sctx->errmsg, in, in_len); + if (state < 0) + return -1; + + switch (state) + { + case 0: + *out = pair_setup_request1(out_len, sctx); + break; + case 1: + ret = pair_setup_response1(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request1(out_len, sctx); + break; + case 2: + ret = pair_setup_response1(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request2(out_len, sctx); + break; + case 3: + ret = pair_setup_response2(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request2(out_len, sctx); + break; + case 4: + ret = pair_setup_response2(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request3(out_len, sctx); + break; + case 5: + ret = pair_setup_response3(sctx, in, in_len); + if (ret < 0) + break; + *out = pair_setup_request3(out_len, sctx); + break; + case 6: + ret = pair_setup_response3(sctx, in, in_len); + if (ret < 0) + break; + break; + default: + sctx->errmsg = "Setup: Unsupported state"; + ret = -1; + } + + if (ret < 0 || !(*out)) + return -1; + + return 0; +} + +uint8_t * +pair_setup_request1(size_t *len, struct pair_setup_context *sctx) +{ + if (!sctx->type->pair_setup_request1) + { + sctx->errmsg = "Setup request 1: Unsupported"; + return NULL; + } + + return sctx->type->pair_setup_request1(len, sctx); +} + +uint8_t * +pair_setup_request2(size_t *len, struct pair_setup_context *sctx) +{ + if (!sctx->type->pair_setup_request2) + { + sctx->errmsg = "Setup request 2: Unsupported"; + return NULL; + } + + return sctx->type->pair_setup_request2(len, sctx); +} + +uint8_t * +pair_setup_request3(size_t *len, struct pair_setup_context *sctx) +{ + if (!sctx->type->pair_setup_request3) + { + sctx->errmsg = "Setup request 3: Unsupported"; + return NULL; + } + + return sctx->type->pair_setup_request3(len, sctx); +} + +int +pair_setup_response1(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + if (!sctx->type->pair_setup_response1) + { + sctx->errmsg = "Setup response 1: Unsupported"; + return -1; + } + + return sctx->type->pair_setup_response1(sctx, in, in_len); +} + +int +pair_setup_response2(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + if (!sctx->type->pair_setup_response2) + { + sctx->errmsg = "Setup response 2: Unsupported"; + return -1; + } + + return sctx->type->pair_setup_response2(sctx, in, in_len); +} + +int +pair_setup_response3(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len) +{ + if (!sctx->type->pair_setup_response3) + { + sctx->errmsg = "Setup response 3: Unsupported"; + return -1; + } + + if (sctx->type->pair_setup_response3(sctx, in, in_len) != 0) + return -1; + + return 0; +} + +int +pair_setup_result(const char **client_setup_keys, struct pair_result **result, struct pair_setup_context *sctx) +{ + if (sctx->status != PAIR_STATUS_COMPLETED) + { + sctx->errmsg = "Setup result: Pair setup has not been completed"; + return -1; + } + + if (sctx->type->pair_setup_result) + { + if (sctx->type->pair_setup_result(sctx) != 0) + return -1; + } + + if (client_setup_keys) + *client_setup_keys = sctx->result_str; + if (result) + *result = &sctx->result; + return 0; +} + +struct pair_verify_context * +pair_verify_new(enum pair_type type, const char *client_setup_keys, pair_cb get_cb, void *cb_arg, const char *device_id) +{ + struct pair_verify_context *vctx; + + if (!pair[type]->pair_verify_new) + return NULL; + + vctx = calloc(1, sizeof(struct pair_verify_context)); + if (!vctx) + return NULL; + + vctx->type = pair[type]; + + if (pair[type]->pair_verify_new(vctx, client_setup_keys, get_cb, cb_arg, device_id) < 0) + { + free(vctx); + return NULL; + } + + return vctx; +} + +void +pair_verify_free(struct pair_verify_context *vctx) +{ + if (!vctx) + return; + + if (vctx->type->pair_verify_free) + vctx->type->pair_verify_free(vctx); + + free(vctx); +} + +const char * +pair_verify_errmsg(struct pair_verify_context *vctx) +{ + return vctx->errmsg; +} + +int +pair_verify(uint8_t **out, size_t *out_len, struct pair_verify_context *vctx, const uint8_t *in, size_t in_len) +{ + int state; + int ret = -1; + + if (!vctx->type->pair_state_get) + { + vctx->errmsg = "Getting pair state unsupported"; + return -1; + } + + *out = NULL; + *out_len = 0; + + state = vctx->type->pair_state_get(&vctx->errmsg, in, in_len); + if (state < 0) + return -1; + + switch (state) + { + case 0: + *out = pair_verify_request1(out_len, vctx); + break; + case 1: + ret = pair_verify_response1(vctx, in, in_len); + if (ret < 0) + break; + *out = pair_verify_request1(out_len, vctx); + break; + case 2: + ret = pair_verify_response1(vctx, in, in_len); + if (ret < 0) + break; + *out = pair_verify_request2(out_len, vctx); + break; + case 3: + ret = pair_verify_response2(vctx, in, in_len); + if (ret < 0) + break; + *out = pair_verify_request2(out_len, vctx); + break; + case 4: + ret = pair_verify_response2(vctx, in, in_len); + if (ret < 0) + break; + break; + default: + vctx->errmsg = "Verify: Unsupported state"; + ret = -1; + } + + if (ret < 0 || !(*out)) + return -1; + + return 0; +} + +uint8_t * +pair_verify_request1(size_t *len, struct pair_verify_context *vctx) +{ + if (!vctx->type->pair_verify_request1) + { + vctx->errmsg = "Verify request 1: Unsupported"; + return NULL; + } + + return vctx->type->pair_verify_request1(len, vctx); +} + +uint8_t * +pair_verify_request2(size_t *len, struct pair_verify_context *vctx) +{ + if (!vctx->type->pair_verify_request2) + { + vctx->errmsg = "Verify request 2: Unsupported"; + return NULL; + } + + return vctx->type->pair_verify_request2(len, vctx); +} + +int +pair_verify_response1(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len) +{ + if (!vctx->type->pair_verify_response1) + { + vctx->errmsg = "Verify response 1: Unsupported"; + return -1; + } + + return vctx->type->pair_verify_response1(vctx, in, in_len); +} + +int +pair_verify_response2(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len) +{ + if (!vctx->type->pair_verify_response2) + { + vctx->errmsg = "Verify response 2: Unsupported"; + return -1; + } + + if (vctx->type->pair_verify_response2(vctx, in, in_len) != 0) + return -1; + + return 0; +} + +int +pair_verify_result(struct pair_result **result, struct pair_verify_context *vctx) +{ + if (vctx->status != PAIR_STATUS_COMPLETED) + { + vctx->errmsg = "Verify result: The pairing verification did not complete"; + return -1; + } + + if (vctx->type->pair_verify_result) + { + if (vctx->type->pair_verify_result(vctx) != 0) + return -1; + } + + if (result) + *result = &vctx->result; + return 0; +} + +struct pair_cipher_context * +pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len) +{ + if (!pair[type]->pair_cipher_new) + return NULL; + + return pair[type]->pair_cipher_new(pair[type], channel, shared_secret, shared_secret_len); +} + +void +pair_cipher_free(struct pair_cipher_context *cctx) +{ + if (!cctx) + return; + + if (!cctx->type->pair_cipher_free) + return; + + return cctx->type->pair_cipher_free(cctx); +} + +const char * +pair_cipher_errmsg(struct pair_cipher_context *cctx) +{ + return cctx->errmsg; +} + +ssize_t +pair_encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx) +{ + if (!cctx->type->pair_encrypt) + { + cctx->errmsg = "Encryption unsupported"; + return -1; + } + + return cctx->type->pair_encrypt(ciphertext, ciphertext_len, plaintext, plaintext_len, cctx); +} + +ssize_t +pair_decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx) +{ + if (!cctx->type->pair_decrypt) + { + cctx->errmsg = "Decryption unsupported"; + return -1; + } + + return cctx->type->pair_decrypt(plaintext, plaintext_len, ciphertext, ciphertext_len, cctx); +} + +void +pair_encrypt_rollback(struct pair_cipher_context *cctx) +{ + cctx->encryption_counter = cctx->encryption_counter_prev; +} + +void +pair_decrypt_rollback(struct pair_cipher_context *cctx) +{ + cctx->decryption_counter = cctx->decryption_counter_prev; +} + +int +pair_add(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb add_cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_add) + { + return -1; + } + + return pair[type]->pair_add(out, out_len, add_cb, cb_arg, in, in_len); +} + +int +pair_remove(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb remove_cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_remove) + { + return -1; + } + + return pair[type]->pair_remove(out, out_len, remove_cb, cb_arg, in, in_len); +} + +int +pair_list(enum pair_type type, uint8_t **out, size_t *out_len, pair_list_cb list_cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_list) + { + return -1; + } + + return pair[type]->pair_list(out, out_len, list_cb, cb_arg, in, in_len); +} + +int +pair_state_get(enum pair_type type, const char **errmsg, const uint8_t *in, size_t in_len) +{ + if (!pair[type]->pair_state_get) + { + *errmsg = "Getting pair state unsupported"; + return -1; + } + + return pair[type]->pair_state_get(errmsg, in, in_len); +} + +void +pair_public_key_get(enum pair_type type, uint8_t server_public_key[32], const char *device_id) +{ + if (!pair[type]->pair_public_key_get) + { + return; + } + + pair[type]->pair_public_key_get(server_public_key, device_id); +} diff --git a/pair_ap/pair.h b/pair_ap/pair.h new file mode 100644 index 00000000..6324bc15 --- /dev/null +++ b/pair_ap/pair.h @@ -0,0 +1,266 @@ +#ifndef __PAIR_AP_H__ +#define __PAIR_AP_H__ + +#include + +#define PAIR_AP_VERSION_MAJOR 0 +#define PAIR_AP_VERSION_MINOR 5 + +#define PAIR_AP_DEVICE_ID_LEN_MAX 64 + +#define PAIR_AP_POST_PIN_START "POST /pair-pin-start" +#define PAIR_AP_POST_SETUP "POST /pair-setup" +#define PAIR_AP_POST_VERIFY "POST /pair-verify" +#define PAIR_AP_POST_ADD "POST /pair-add" +#define PAIR_AP_POST_LIST "POST /pair-list" +#define PAIR_AP_POST_REMOVE "POST /pair-remove" + +enum pair_type +{ + // This is the pairing type required for Apple TV device verification, which + // became mandatory with tvOS 10.2. + PAIR_CLIENT_FRUIT, + // This is the Homekit type required for AirPlay 2 with both PIN setup and + // verification + PAIR_CLIENT_HOMEKIT_NORMAL, + // Same as normal except PIN is fixed to 3939 and stops after setup step 2, + // when session key is established + PAIR_CLIENT_HOMEKIT_TRANSIENT, + // Server side implementation supporting both transient and normal mode, + // letting client choose mode. If a PIN is with pair_setup_new() then only + // normal mode will be possible. + PAIR_SERVER_HOMEKIT, +}; + +/* This struct stores the various forms of pairing results. The shared secret + * is used to initialise an encrypted session via pair_cipher_new(). For + * non-transient client pair setup, you also get a key string (client_setup_keys) from + * pair_setup_result() that you can store and use to later initialise + * pair_verify_new(). For non-transient server pair setup, you can either: + * - Register an "add pairing" callback (add_cb) with pair_setup_new(), and + * then save the client id and key in the callback (see server-example.c for + * this approach). + * - Check pairing result with pair_setup_result() and if successful read and + * store the client id and key from the result struct. + * - Decide not to authenticate clients during pair-verify (set get_cb to NULL) + * in which case you don't need to save client ids and keys from pair-setup. + * + * Table showing returned data (everything else will be zeroed): + * + * | pair-setup | pair-verify + * --------------------------------|-------------------------------|-------------- + * PAIR_CLIENT_FRUIT | client keys | shared secret + * PAIR_CLIENT_HOMEKIT_NORMAL | client keys, server public | shared secret + | key, server id | shared secret + * PAIR_CLIENT_HOMEKIT_TRANSIENT | shared secret | n/a + * PAIR_SERVER_HOMEKIT (normal) | client public key, client id | shared secret + * PAIR_SERVER_HOMEKIT (transient) | shared secret | n/a + */ +struct pair_result +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; // ID of the peer + uint8_t client_private_key[64]; + uint8_t client_public_key[32]; + uint8_t server_public_key[32]; + uint8_t shared_secret[64]; + size_t shared_secret_len; // Will be 32 (normal) or 64 (transient) +}; + +struct pair_setup_context; +struct pair_verify_context; +struct pair_cipher_context; + +typedef int (*pair_cb)(uint8_t public_key[32], const char *device_id, void *cb_arg); +typedef void (*pair_list_cb)(pair_cb list_cb, void *list_cb_arg, void *cb_arg); + + +/* ------------------------------- pair setup ------------------------------- */ + +/* Client + * When you have the pin-code (must be 4 chars), create a new context with this + * function and then call pair_setup() or pair_setup_request1(). device_id is + * only required for Homekit pairing. If the client previously paired + * (non-transient) and has saved credentials, it should instead skip setup and + * only do verification. The callback is only for Homekit, and you can leave it + * at NULL if you don't care about saving ID and key of the server for later + * verification (then you also set get_cb to NULL in pair_verify_new), or if you + * will read the id and key via pair_setup_result. + * + * Server + * The client will make a connection and then at some point make a /pair-setup + * or a /pair-verify. The server should: + * - new /pair-setup: create a setup context with a pin-code (or NULL to allow + * transient pairing), and then call pair_setup() to process request and + * construct reply (also for subsequent /pair-setup requests) + * - new /pair_verify: create a verify context and then call pair_verify() + * to process request and construct reply (also for subsequent /pair-verify + * requests) + */ +struct pair_setup_context * +pair_setup_new(enum pair_type type, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id); +void +pair_setup_free(struct pair_setup_context *sctx); + +/* Returns last error message + */ +const char * +pair_setup_errmsg(struct pair_setup_context *sctx); + +/* Will create a request (if client) or response (if server) based on the setup + * context and last message from the peer. If this is the first client request + * then set *in to NULL. Returns negative on error. + */ +int +pair_setup(uint8_t **out, size_t *out_len, struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + +/* Returns the result of a pairing, or negative if pairing is not completed. See + * 'struct pair_result' for info about pairing results. The string is a + * representation of the result that is easy to persist and can be used to feed + * back into pair_verify_new. The result and string becomes invalid when you + * free sctx. + */ +int +pair_setup_result(const char **client_setup_keys, struct pair_result **result, struct pair_setup_context *sctx); + +/* These are for constructing specific message types and reading specific + * message types. Not needed for Homekit pairing if you use pair_setup(). + */ +uint8_t * +pair_setup_request1(size_t *len, struct pair_setup_context *sctx); +uint8_t * +pair_setup_request2(size_t *len, struct pair_setup_context *sctx); +uint8_t * +pair_setup_request3(size_t *len, struct pair_setup_context *sctx); + +int +pair_setup_response1(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); +int +pair_setup_response2(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); +int +pair_setup_response3(struct pair_setup_context *sctx, const uint8_t *in, size_t in_len); + + +/* ------------------------------ pair verify ------------------------------- */ + +/* Client + * When you have completed pair setup you get a string containing some keys + * from pair_setup_result(). Give the string as input to this function to create + * a verification context. Set the callback to NULL. Then call pair_verify(). + * The device_id is required for Homekit pairing. + * + * Server + * When you get a pair verify request from a new peer, create a new context with + * client_setup_keys set to NULL, with a callback set and the server's device ID + * (same as for setup). Then call pair_verify(). The callback is used to get + * the persisted client public key (saved after pair setup), so the client can + * be verified. You can set the callback to NULL if you don't care about that. + * If set, the callback is made as part of pair_verify_response2. The job of the + * callback is to fill out the public_key with the public key from the setup + * stage (see 'struct pair_result'). If the client device id is not known (i.e. + * it has not completed pair-setup), return -1. + */ +struct pair_verify_context * +pair_verify_new(enum pair_type type, const char *client_setup_keys, pair_cb get_cb, void *cb_arg, const char *device_id); +void +pair_verify_free(struct pair_verify_context *vctx); + +/* Returns last error message + */ +const char * +pair_verify_errmsg(struct pair_verify_context *vctx); + +/* Will create a request (if client) or response (if server) based on the verify + * context and last message from the peer. If this is the first client request + * then set *in to NULL. Returns negative on error. + */ +int +pair_verify(uint8_t **out, size_t *out_len, struct pair_verify_context *sctx, const uint8_t *in, size_t in_len); + +/* Returns a pointer to the result of the pairing. Only the shared secret will + * be filled out. Note that the result become invalid when you free vctx. + */ +int +pair_verify_result(struct pair_result **result, struct pair_verify_context *vctx); + +/* These are for constructing specific message types and reading specific + * message types. Not needed for Homekit pairing where you can use pair_verify(). + */ +uint8_t * +pair_verify_request1(size_t *len, struct pair_verify_context *vctx); +uint8_t * +pair_verify_request2(size_t *len, struct pair_verify_context *vctx); + +int +pair_verify_response1(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); +int +pair_verify_response2(struct pair_verify_context *vctx, const uint8_t *in, size_t in_len); + + +/* ------------------------------- ciphering -------------------------------- */ + +/* When you have completed the verification you can extract a shared secret with + * pair_verify_result() - or, in case of transient pairing, from + * pair_setup_result(). Give the shared secret as input to this function to + * create a ciphering context. + */ +struct pair_cipher_context * +pair_cipher_new(enum pair_type type, int channel, const uint8_t *shared_secret, size_t shared_secret_len); +void +pair_cipher_free(struct pair_cipher_context *cctx); + +/* Returns last error message + */ +const char * +pair_cipher_errmsg(struct pair_cipher_context *cctx); + +/* The return value equals length of plaintext that was encrypted, so if the + * return value == plaintext_len then everything was encrypted. On error -1 is + * returned. + */ +ssize_t +pair_encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx); + +/* The return value equals length of ciphertext that was decrypted, so if the + * return value == ciphertext_len then everything was decrypted. On error -1 is + * returned. + */ +ssize_t +pair_decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx); + +/* Rolls back the nonce + */ +void +pair_encrypt_rollback(struct pair_cipher_context *cctx); +void +pair_decrypt_rollback(struct pair_cipher_context *cctx); + + +/* --------------------------------- other ---------------------------------- */ + +/* These are for Homekit pairing where they are called by the controller, e.g. + * the Home app + * + * TODO this part is currenly not working + */ +int +pair_add(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb add_cb, void *cb_arg, const uint8_t *in, size_t in_len); + +int +pair_remove(enum pair_type type, uint8_t **out, size_t *out_len, pair_cb remove_cb, void *cb_arg, const uint8_t *in, size_t in_len); + +int +pair_list(enum pair_type type, uint8_t **out, size_t *out_len, pair_list_cb list_cb, void *cb_arg, const uint8_t *in, size_t in_len); + +/* For parsing an incoming message to see what type ("state") it is. Mostly + * useful for servers. Returns 1-6 for pair-setup and 1-4 for pair-verify. + */ +int +pair_state_get(enum pair_type type, const char **errmsg, const uint8_t *in, size_t in_len); + +/* For servers, pair_ap calculates the public key using device_id as a seed. + * This function returns that public key. + */ +void +pair_public_key_get(enum pair_type type, uint8_t server_public_key[32], const char *device_id); + +#endif /* !__PAIR_AP_H__ */ diff --git a/pair_ap/pair_ap-master/.gitignore b/pair_ap/pair_ap-master/.gitignore new file mode 100644 index 00000000..a7eeb67d --- /dev/null +++ b/pair_ap/pair_ap-master/.gitignore @@ -0,0 +1,10 @@ +*.o +*.lo +*.a +*.la +.dirstamp +.deps/ +.libs/ + +client-example +server-example diff --git a/pair_ap/pair_ap-master/evrtsp/evrtsp.h b/pair_ap/pair_ap-master/evrtsp/evrtsp.h new file mode 100644 index 00000000..5a03cd6d --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/evrtsp.h @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2010 Julien BLACHE + * Based on evhttp from libevent 1.4.x + * + * Copyright (c) 2000-2004 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef _EVRTSP_H_ +#define _EVRTSP_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#undef WIN32_LEAN_AND_MEAN +#endif + +/* Response codes */ +#define RTSP_OK 200 +#define RTSP_UNAUTHORIZED 401 +#define RTSP_FORBIDDEN 403 + +struct evrtsp_connection; + +/* + * Interfaces for making requests + */ +enum evrtsp_cmd_type { + EVRTSP_REQ_ANNOUNCE, + EVRTSP_REQ_OPTIONS, + EVRTSP_REQ_SETUP, + EVRTSP_REQ_RECORD, + EVRTSP_REQ_PAUSE, + EVRTSP_REQ_GET_PARAMETER, // Careful using this, some devices do not support it or will hang up eg RAOP_DEV_APEX1_80211G + EVRTSP_REQ_SET_PARAMETER, + EVRTSP_REQ_FLUSH, + EVRTSP_REQ_TEARDOWN, + EVRTSP_REQ_POST, +}; + +enum evrtsp_request_kind { EVRTSP_REQUEST, EVRTSP_RESPONSE }; + +struct evrtsp_request { +#if defined(TAILQ_ENTRY) + TAILQ_ENTRY(evrtsp_request) next; +#else +struct { + struct evrtsp_request *tqe_next; + struct evrtsp_request **tqe_prev; +} next; +#endif + + /* the connection object that this request belongs to */ + struct evrtsp_connection *evcon; + int flags; +#define EVRTSP_REQ_OWN_CONNECTION 0x0001 + + struct evkeyvalq *input_headers; + struct evkeyvalq *output_headers; + + enum evrtsp_request_kind kind; + enum evrtsp_cmd_type type; + + char *uri; /* uri after RTSP request was parsed */ + + char major; /* RTSP Major number */ + char minor; /* RTSP Minor number */ + + int response_code; /* RTSP Response code */ + char *response_code_line; /* Readable response */ + + struct evbuffer *input_buffer; /* read data */ + ev_int64_t ntoread; + + struct evbuffer *output_buffer; /* outgoing post or data */ + + /* Callback */ + void (*cb)(struct evrtsp_request *, void *); + void *cb_arg; +}; + +/** + * Creates a new request object that needs to be filled in with the request + * parameters. The callback is executed when the request completed or an + * error occurred. + */ +struct evrtsp_request *evrtsp_request_new( + void (*cb)(struct evrtsp_request *, void *), void *arg); + +/** Frees the request object and removes associated events. */ +void evrtsp_request_free(struct evrtsp_request *req); + +/** + * A connection object that can be used to for making RTSP requests. The + * connection object tries to establish the connection when it is given an + * rtsp request object. + */ +struct evrtsp_connection *evrtsp_connection_new( + const char *address, unsigned short port); + +/** Frees an rtsp connection */ +void evrtsp_connection_free(struct evrtsp_connection *evcon); + +/** Set a callback for connection close. */ +void evrtsp_connection_set_closecb(struct evrtsp_connection *evcon, + void (*)(struct evrtsp_connection *, void *), void *); + +/** Set a callback for encryption/decryption. */ +void evrtsp_connection_set_ciphercb(struct evrtsp_connection *evcon, + void (*)(struct evbuffer *, void *, int encrypt), void *); + +/** + * Associates an event base with the connection - can only be called + * on a freshly created connection object that has not been used yet. + */ +void evrtsp_connection_set_base(struct evrtsp_connection *evcon, + struct event_base *base); + +/** Get the remote address and port associated with this connection. */ +void evrtsp_connection_get_peer(struct evrtsp_connection *evcon, + char **address, u_short *port); + +/** Get the local address, port and family associated with this connection. */ +void +evrtsp_connection_get_local_address(struct evrtsp_connection *evcon, + char **address, u_short *port, int *family); + +/** The connection gets ownership of the request */ +int evrtsp_make_request(struct evrtsp_connection *evcon, + struct evrtsp_request *req, + enum evrtsp_cmd_type type, const char *uri); + +const char *evrtsp_request_uri(struct evrtsp_request *req); + +/* Interfaces for dealing with headers */ + +const char *evrtsp_find_header(const struct evkeyvalq *, const char *); +int evrtsp_remove_header(struct evkeyvalq *, const char *); +int evrtsp_add_header(struct evkeyvalq *, const char *, const char *); +void evrtsp_clear_headers(struct evkeyvalq *); + +const char *evrtsp_method(enum evrtsp_cmd_type type); + +#ifdef __cplusplus +} +#endif + +#endif /* !_EVRTSP_H_ */ diff --git a/pair_ap/pair_ap-master/evrtsp/log.h b/pair_ap/pair_ap-master/evrtsp/log.h new file mode 100644 index 00000000..7bc6632b --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/log.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2000-2004 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef _LOG_H_ +#define _LOG_H_ + +#ifdef __GNUC__ +#define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b))) +#else +#define EV_CHECK_FMT(a,b) +#endif + +void event_err(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3); +void event_warn(const char *fmt, ...) EV_CHECK_FMT(1,2); +void event_errx(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3); +void event_warnx(const char *fmt, ...) EV_CHECK_FMT(1,2); +void event_msgx(const char *fmt, ...) EV_CHECK_FMT(1,2); +void _event_debugx(const char *fmt, ...) EV_CHECK_FMT(1,2); + +#ifdef USE_DEBUG +#define event_debug(x) _event_debugx x +#else +#define event_debug(x) do {;} while (0) +#endif + +#undef EV_CHECK_FMT + +#endif diff --git a/pair_ap/pair_ap-master/evrtsp/rtsp-internal.h b/pair_ap/pair_ap-master/evrtsp/rtsp-internal.h new file mode 100644 index 00000000..dcab48a1 --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/rtsp-internal.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2010 Julien BLACHE + * Based on evhttp from libevent 1.4.x + * + * Copyright 2001 Niels Provos + * All rights reserved. + * + * This header file contains definitions for dealing with RTSP requests + * that are internal to libevent. As user of the library, you should not + * need to know about these. + */ + +#ifndef _RTSP_H_ +#define _RTSP_H_ + +#include +#include + +#define RTSP_CONNECT_TIMEOUT 5 +#define RTSP_WRITE_TIMEOUT 30 +#define RTSP_READ_TIMEOUT 30 + +#define RTSP_PREFIX "rtsp://" + +enum message_read_status { + ALL_DATA_READ = 1, + MORE_DATA_EXPECTED = 0, + DATA_CORRUPTED = -1, + REQUEST_CANCELED = -2 +}; + +enum evrtsp_connection_error { + EVCON_RTSP_TIMEOUT, + EVCON_RTSP_EOF, + EVCON_RTSP_INVALID_HEADER +}; + +struct evbuffer; +struct addrinfo; +struct evrtsp_request; + +/* A stupid connection object - maybe make this a bufferevent later */ + +enum evrtsp_connection_state { + EVCON_DISCONNECTED, /**< not currently connected not trying either*/ + EVCON_CONNECTING, /**< tries to currently connect */ + EVCON_IDLE, /**< connection is established */ + EVCON_READING_FIRSTLINE,/**< reading Request-Line (incoming conn) or + **< Status-Line (outgoing conn) */ + EVCON_READING_HEADERS, /**< reading request/response headers */ + EVCON_READING_BODY, /**< reading request/response body */ + EVCON_READING_TRAILER, /**< reading request/response chunked trailer */ + EVCON_WRITING /**< writing request/response headers/body */ +}; + +struct event_base; + +struct evrtsp_connection { + int fd; + struct event ev; + struct event close_ev; + struct evbuffer *input_buffer; + struct evbuffer *output_buffer; + + char *bind_address; /* address to use for binding the src */ + u_short bind_port; /* local port for binding the src */ + + char *address; /* address to connect to */ + int family; + u_short port; + + int flags; +#define EVRTSP_CON_CLOSEDETECT 0x0004 /* detecting if persistent close */ + + int timeout; /* timeout in seconds for events */ + + enum evrtsp_connection_state state; + int cseq; + + TAILQ_HEAD(evcon_requestq, evrtsp_request) requests; + + void (*cb)(struct evrtsp_connection *, void *); + void *cb_arg; + + void (*closecb)(struct evrtsp_connection *, void *); + void *closecb_arg; + + void (*ciphercb)(struct evbuffer *evbuf, void *, int encrypt); + void *ciphercb_arg; + + struct event_base *base; +}; + +/* resets the connection; can be reused for more requests */ +void evrtsp_connection_reset(struct evrtsp_connection *); + +/* connects if necessary */ +int evrtsp_connection_connect(struct evrtsp_connection *); + +/* notifies the current request that it failed; resets connection */ +void evrtsp_connection_fail(struct evrtsp_connection *, + enum evrtsp_connection_error error); + +int evrtsp_hostportfile(char *, char **, u_short *, char **); + +int evrtsp_parse_firstline(struct evrtsp_request *, struct evbuffer*); +int evrtsp_parse_headers(struct evrtsp_request *, struct evbuffer*); + +void evrtsp_start_read(struct evrtsp_connection *); +void evrtsp_make_header(struct evrtsp_connection *, struct evrtsp_request *); + +void evrtsp_write_buffer(struct evrtsp_connection *, + void (*)(struct evrtsp_connection *, void *), void *); + +#endif /* _RTSP_H */ diff --git a/pair_ap/pair_ap-master/evrtsp/rtsp.c b/pair_ap/pair_ap-master/evrtsp/rtsp.c new file mode 100644 index 00000000..82efc548 --- /dev/null +++ b/pair_ap/pair_ap-master/evrtsp/rtsp.c @@ -0,0 +1,1816 @@ +/* + * Copyright (C) 2010 Julien BLACHE + * Based on evhttp from libevent 1.4.x + * + * Copyright (c) 2002-2006 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#undef timeout_pending +#undef timeout_initialized + +#include "evrtsp.h" +/* #define USE_DEBUG */ +#include "log.h" +#include "rtsp-internal.h" + +// For compability with libevent 2.0 (HAVE_LIBEVENT2_OLD) +#if defined(_EVENT_HAVE_GETNAMEINFO) +# define EVENT__HAVE_GETNAMEINFO 1 +#endif +#if defined(_EVENT_HAVE_GETADDRINFO) +# define EVENT__HAVE_GETADDRINFO 1 +#endif +#if defined(_EVENT_HAVE_STRSEP) +# define EVENT__HAVE_STRSEP 1 +#endif + +#ifndef EVENT__HAVE_GETNAMEINFO +#define NI_MAXSERV 32 +#define NI_MAXHOST 1025 + +#define NI_NUMERICHOST 1 +#define NI_NUMERICSERV 2 + +static int +fake_getnameinfo(const struct sockaddr *sa, size_t salen, char *host, + size_t hostlen, char *serv, size_t servlen, int flags) +{ + struct sockaddr_in *sin = (struct sockaddr_in *)sa; + int ret; + + if (serv != NULL) { + char tmpserv[16]; + evutil_snprintf(tmpserv, sizeof(tmpserv), + "%d", ntohs(sin->sin_port)); + ret = evutil_snprintf(serv, servlen, "%s", tmpserv); + if ((ret < 0) || (ret >= servlen)) + return (-1); + } + + if (host != NULL) { + if (flags & NI_NUMERICHOST) { + ret = evutil_snprintf(host, hostlen, "%s", inet_ntoa(sin->sin_addr)); + if ((ret < 0) || (ret >= hostlen)) + return (-1); + else + return (0); + } else { + struct hostent *hp; + hp = gethostbyaddr((char *)&sin->sin_addr, + sizeof(struct in_addr), AF_INET); + if (hp == NULL) + return (-2); + + ret = evutil_snprintf(host, hostlen, "%s", hp->h_name); + if ((ret < 0) || (ret >= hostlen)) + return (-1); + else + return (0); + } + } + return (0); +} + +#endif + +#ifndef EVENT__HAVE_GETADDRINFO +struct addrinfo { + int ai_family; + int ai_socktype; + int ai_protocol; + size_t ai_addrlen; + struct sockaddr *ai_addr; + struct addrinfo *ai_next; +}; +static int +fake_getaddrinfo(const char *hostname, struct addrinfo *ai) +{ + struct hostent *he = NULL; + struct sockaddr_in *sa; + if (hostname) { + he = gethostbyname(hostname); + if (!he) + return (-1); + } + ai->ai_family = he ? he->h_addrtype : AF_INET; + ai->ai_socktype = SOCK_STREAM; + ai->ai_protocol = 0; + ai->ai_addrlen = sizeof(struct sockaddr_in); + if (NULL == (ai->ai_addr = malloc(ai->ai_addrlen))) + return (-1); + sa = (struct sockaddr_in*)ai->ai_addr; + memset(sa, 0, ai->ai_addrlen); + if (he) { + sa->sin_family = he->h_addrtype; + memcpy(&sa->sin_addr, he->h_addr_list[0], he->h_length); + } else { + sa->sin_family = AF_INET; + sa->sin_addr.s_addr = INADDR_ANY; + } + ai->ai_next = NULL; + return (0); +} +static void +fake_freeaddrinfo(struct addrinfo *ai) +{ + free(ai->ai_addr); +} +#endif + +#ifndef MIN +#define MIN(a,b) (((a)<(b))?(a):(b)) +#endif + +/* wrapper for setting the base from the rtsp server */ +#define EVRTSP_BASE_SET(x, y) do { \ + if ((x)->base != NULL) event_base_set((x)->base, y); \ +} while (0) + +extern int debug; + +static int socket_connect(int fd, const char *address, unsigned short port); +static int bind_socket_ai(int family, struct addrinfo *, int reuse); +static int bind_socket(int family, const char *, u_short, int reuse); +static void name_from_addr(struct sockaddr *, socklen_t, char **, char **); +static void evrtsp_connection_start_detectclose( + struct evrtsp_connection *evcon); +static void evrtsp_connection_stop_detectclose( + struct evrtsp_connection *evcon); +static void evrtsp_request_dispatch(struct evrtsp_connection* evcon); +static void evrtsp_read_firstline(struct evrtsp_connection *evcon, + struct evrtsp_request *req); +static void evrtsp_read_header(struct evrtsp_connection *evcon, + struct evrtsp_request *req); +static int evrtsp_add_header_internal(struct evkeyvalq *headers, + const char *key, const char *value); + +void evrtsp_read(int, short, void *); +void evrtsp_write(int, short, void *); + +#ifndef EVENT__HAVE_STRSEP +/* strsep replacement for platforms that lack it. Only works if + * del is one character long. */ +static char * +strsep(char **s, const char *del) +{ + char *d, *tok; + assert(strlen(del) == 1); + if (!s || !*s) + return NULL; + tok = *s; + d = strstr(tok, del); + if (d) { + *d = '\0'; + *s = d + 1; + } else + *s = NULL; + return tok; +} +#endif + +const char * +evrtsp_method(enum evrtsp_cmd_type type) +{ + const char *method; + + switch (type) { + case EVRTSP_REQ_ANNOUNCE: + method = "ANNOUNCE"; + break; + + case EVRTSP_REQ_OPTIONS: + method = "OPTIONS"; + break; + + case EVRTSP_REQ_SETUP: + method = "SETUP"; + break; + + case EVRTSP_REQ_RECORD: + method = "RECORD"; + break; + + case EVRTSP_REQ_PAUSE: + method = "PAUSE"; + break; + + case EVRTSP_REQ_GET_PARAMETER: + method = "GET_PARAMETER"; + break; + + case EVRTSP_REQ_SET_PARAMETER: + method = "SET_PARAMETER"; + break; + + case EVRTSP_REQ_FLUSH: + method = "FLUSH"; + break; + + case EVRTSP_REQ_TEARDOWN: + method = "TEARDOWN"; + break; + + case EVRTSP_REQ_POST: + method = "POST"; + break; + + default: + method = NULL; + break; + } + + return (method); +} + +static void +evrtsp_add_event(struct event *ev, int timeout, int default_timeout) +{ + if (timeout != 0) { + struct timeval tv; + + evutil_timerclear(&tv); + tv.tv_sec = timeout != -1 ? timeout : default_timeout; + event_add(ev, &tv); + } else { + event_add(ev, NULL); + } +} + +void +evrtsp_write_buffer(struct evrtsp_connection *evcon, + void (*cb)(struct evrtsp_connection *, void *), void *arg) +{ + event_debug(("%s: preparing to write buffer", __func__)); + + /* Set call back */ + evcon->cb = cb; + evcon->cb_arg = arg; + + /* check if the event is already pending */ + if (event_pending(&evcon->ev, EV_WRITE|EV_TIMEOUT, NULL)) + event_del(&evcon->ev); + + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_WRITE, evrtsp_write, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_WRITE_TIMEOUT); +} + +static int +evrtsp_connected(struct evrtsp_connection *evcon) +{ + switch (evcon->state) { + case EVCON_DISCONNECTED: + case EVCON_CONNECTING: + return (0); + case EVCON_IDLE: + case EVCON_READING_FIRSTLINE: + case EVCON_READING_HEADERS: + case EVCON_READING_BODY: + case EVCON_READING_TRAILER: + case EVCON_WRITING: + default: + return (1); + } +} + +/* + * Create the headers needed for an RTSP request + */ +static void +evrtsp_make_header_request(struct evrtsp_connection *evcon, + struct evrtsp_request *req) +{ + const char *method; + + /* Generate request line */ + method = evrtsp_method(req->type); + evbuffer_add_printf(evcon->output_buffer, "%s %s RTSP/%d.%d\r\n", + method, req->uri, req->major, req->minor); + + /* Content-Length is mandatory, absent means 0 */ + if ((evbuffer_get_length(req->output_buffer) > 0) + && (evrtsp_find_header(req->output_headers, "Content-Length") == NULL)) + { + char size[12]; + evutil_snprintf(size, sizeof(size), "%ld", + (long)evbuffer_get_length(req->output_buffer)); + evrtsp_add_header(req->output_headers, "Content-Length", size); + } +} + +void +evrtsp_make_header(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + struct evkeyval *header; + + evrtsp_make_header_request(evcon, req); + + TAILQ_FOREACH(header, req->output_headers, next) { + evbuffer_add_printf(evcon->output_buffer, "%s: %s\r\n", + header->key, header->value); + } + evbuffer_add(evcon->output_buffer, "\r\n", 2); + + if (evbuffer_get_length(req->output_buffer) > 0) { + evbuffer_add_buffer(evcon->output_buffer, req->output_buffer); + } +} + +/* Separated host, port and file from URI */ + +int /* FIXME: needed? */ +evrtsp_hostportfile(char *url, char **phost, u_short *pport, char **pfile) +{ + /* XXX not threadsafe. */ + static char host[1024]; + static char file[1024]; + char *p; + const char *p2; + int len; + int ret; + u_short port; + + len = strlen(RTSP_PREFIX); + if (strncasecmp(url, RTSP_PREFIX, len)) + return (-1); + + url += len; + + /* We might overrun */ + ret = evutil_snprintf(host, sizeof(host), "%s", url); + if ((ret < 0) || (ret >= sizeof(host))) + return (-1); + + p = strchr(host, '/'); + if (p != NULL) { + *p = '\0'; + p2 = p + 1; + } else + p2 = NULL; + + if (pfile != NULL) { + /* Generate request file */ + if (p2 == NULL) + p2 = ""; + evutil_snprintf(file, sizeof(file), "/%s", p2); + } + + p = strchr(host, ':'); + if (p != NULL) { + *p = '\0'; + port = atoi(p + 1); + + if (port == 0) + return (-1); + } else + return -1; + + if (phost != NULL) + *phost = host; + if (pport != NULL) + *pport = port; + if (pfile != NULL) + *pfile = file; + + return (0); +} + +void +evrtsp_connection_fail(struct evrtsp_connection *evcon, + enum evrtsp_connection_error error) +{ + struct evrtsp_request* req = TAILQ_FIRST(&evcon->requests); + void (*cb)(struct evrtsp_request *, void *); + void *cb_arg; + assert(req != NULL); + + /* save the callback for later; the cb might free our object */ + cb = req->cb; + cb_arg = req->cb_arg; + + TAILQ_REMOVE(&evcon->requests, req, next); + evrtsp_request_free(req); + + /* xxx: maybe we should fail all requests??? */ + + /* reset the connection */ + evrtsp_connection_reset(evcon); + + /* We are trying the next request that was queued on us */ + if (TAILQ_FIRST(&evcon->requests) != NULL) + evrtsp_connection_connect(evcon); + + /* inform the user */ + if (cb != NULL) + (*cb)(NULL, cb_arg); +} + +void +evrtsp_write(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + int n; + + if (what == EV_TIMEOUT) { + event_warn("%s: write timeout", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_TIMEOUT); + return; + } + + n = evbuffer_write(evcon->output_buffer, fd); + if (n == -1) { + event_warn("%s: evbuffer_write", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); + return; + } + + if (n == 0) { + event_warn("%s: write nothing", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); + return; + } + + if (evbuffer_get_length(evcon->output_buffer) != 0) { + evrtsp_add_event(&evcon->ev, + evcon->timeout, RTSP_WRITE_TIMEOUT); + return; + } + + /* Activate our call back */ + if (evcon->cb != NULL) + (*evcon->cb)(evcon, evcon->cb_arg); +} + +/** + * Advance the connection state. + * - If this is an outgoing connection, we've just processed the response; + * idle or close the connection. + */ +static void +evrtsp_connection_done(struct evrtsp_connection *evcon) +{ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + + /* idle or close the connection */ + TAILQ_REMOVE(&evcon->requests, req, next); + req->evcon = NULL; + + evcon->state = EVCON_IDLE; + + if (TAILQ_FIRST(&evcon->requests) != NULL) { + /* + * We have more requests; reset the connection + * and deal with the next request. + */ + if (!evrtsp_connected(evcon)) + evrtsp_connection_connect(evcon); + else + evrtsp_request_dispatch(evcon); + } else { + /* + * The connection is going to be persistent, but we + * need to detect if the other side closes it. + */ + evrtsp_connection_start_detectclose(evcon); + } + + /* notify the user of the request */ + (*req->cb)(req, req->cb_arg); + + evrtsp_request_free(req); +} + +static void /* FIXME: needed? */ +evrtsp_read_trailer(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + struct evbuffer *buf = evcon->input_buffer; + + switch (evrtsp_parse_headers(req, buf)) { + case DATA_CORRUPTED: + event_warn("%s: invalid header", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + break; + case ALL_DATA_READ: + event_del(&evcon->ev); + evrtsp_connection_done(evcon); + break; + case MORE_DATA_EXPECTED: + default: + evrtsp_add_event(&evcon->ev, evcon->timeout, + RTSP_READ_TIMEOUT); + break; + } +} + +static void +evrtsp_read_body(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + struct evbuffer *buf = evcon->input_buffer; + + if (req->ntoread < 0) { + /* Read until connection close. */ + evbuffer_add_buffer(req->input_buffer, buf); + } else if (evbuffer_get_length(buf) >= req->ntoread) { + /* Completed content length */ + evbuffer_add(req->input_buffer, evbuffer_pullup(buf,-1), + (size_t)req->ntoread); + evbuffer_drain(buf, (size_t)req->ntoread); + req->ntoread = 0; + evrtsp_connection_done(evcon); + return; + } + /* Read more! */ + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_READ, evrtsp_read, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); +} + +/* + * Reads data into a buffer structure until no more data + * can be read on the file descriptor or we have read all + * the data that we wanted to read. + * Execute callback when done. + */ + +void +evrtsp_read(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + struct evbuffer *buf = evcon->input_buffer; + int n; + + if (what == EV_TIMEOUT) { + event_warn("%s: read timeout", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_TIMEOUT); + return; + } + n = evbuffer_read(buf, fd, -1); + event_debug(("%s: got %d on %d", __func__, n, fd)); + + if (n == -1) { + if (errno != EINTR && errno != EAGAIN) { + event_warn("%s: evbuffer_read", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); + } else { + evrtsp_add_event(&evcon->ev, evcon->timeout, + RTSP_READ_TIMEOUT); + } + return; + } else if (n == 0) { + /* Connection closed */ + evcon->state = EVCON_DISCONNECTED; + evrtsp_connection_done(evcon); + return; + } + + if (evcon->ciphercb) + evcon->ciphercb(evcon->input_buffer, evcon->ciphercb_arg, 0); + + switch (evcon->state) { + case EVCON_READING_FIRSTLINE: + evrtsp_read_firstline(evcon, req); + break; + case EVCON_READING_HEADERS: + evrtsp_read_header(evcon, req); + break; + case EVCON_READING_BODY: + evrtsp_read_body(evcon, req); + break; + case EVCON_READING_TRAILER: + evrtsp_read_trailer(evcon, req); + break; + case EVCON_DISCONNECTED: + case EVCON_CONNECTING: + case EVCON_IDLE: + case EVCON_WRITING: + default: + event_errx(1, "%s: illegal connection state %d", + __func__, evcon->state); + } +} + +static void +evrtsp_write_connectioncb(struct evrtsp_connection *evcon, void *arg) +{ + /* This is after writing the request to the server */ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + assert(req != NULL); + + assert(evcon->state == EVCON_WRITING); + + /* We are done writing our header and are now expecting the response */ + req->kind = EVRTSP_RESPONSE; + + evrtsp_start_read(evcon); +} + + + +/* + * Clean up a connection object + */ + +void +evrtsp_connection_free(struct evrtsp_connection *evcon) +{ + struct evrtsp_request *req; + + /* notify interested parties that this connection is going down */ + if (evcon->fd != -1) { + if (evrtsp_connected(evcon) && evcon->closecb != NULL) + (*evcon->closecb)(evcon, evcon->closecb_arg); + } + + /* remove all requests that might be queued on this connection */ + while ((req = TAILQ_FIRST(&evcon->requests)) != NULL) { + TAILQ_REMOVE(&evcon->requests, req, next); + evrtsp_request_free(req); + } + + if (event_initialized(&evcon->close_ev)) + event_del(&evcon->close_ev); + + if (event_initialized(&evcon->ev)) + event_del(&evcon->ev); + + if (evcon->fd != -1) + EVUTIL_CLOSESOCKET(evcon->fd); + + if (evcon->bind_address != NULL) + free(evcon->bind_address); + + if (evcon->address != NULL) + free(evcon->address); + + if (evcon->input_buffer != NULL) + evbuffer_free(evcon->input_buffer); + + if (evcon->output_buffer != NULL) + evbuffer_free(evcon->output_buffer); + + free(evcon); +} + +static void +evrtsp_request_dispatch(struct evrtsp_connection* evcon) +{ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + + /* this should not usually happy but it's possible */ + if (req == NULL) + return; + + /* delete possible close detection events */ + evrtsp_connection_stop_detectclose(evcon); + + /* we assume that the connection is connected already */ + assert(evcon->state == EVCON_IDLE); + + evcon->state = EVCON_WRITING; + + /* Create the header from the store arguments */ + evrtsp_make_header(evcon, req); + + /* forked-daapd customisation for encryption */ + if (evcon->ciphercb) + evcon->ciphercb(evcon->output_buffer, evcon->ciphercb_arg, 1); + + evrtsp_write_buffer(evcon, evrtsp_write_connectioncb, NULL); +} + +/* Reset our connection state */ +void +evrtsp_connection_reset(struct evrtsp_connection *evcon) +{ + if (event_initialized(&evcon->ev)) + event_del(&evcon->ev); + + if (evcon->fd != -1) { + /* inform interested parties about connection close */ + if (evrtsp_connected(evcon) && evcon->closecb != NULL) + (*evcon->closecb)(evcon, evcon->closecb_arg); + + EVUTIL_CLOSESOCKET(evcon->fd); + evcon->fd = -1; + } + evcon->state = EVCON_DISCONNECTED; + + evbuffer_drain(evcon->input_buffer, + evbuffer_get_length(evcon->input_buffer)); + evbuffer_drain(evcon->output_buffer, + evbuffer_get_length(evcon->output_buffer)); +} + +static void +evrtsp_detect_close_cb(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + + evrtsp_connection_reset(evcon); +} + +static void +evrtsp_connection_start_detectclose(struct evrtsp_connection *evcon) +{ + evcon->flags |= EVRTSP_CON_CLOSEDETECT; + + if (event_initialized(&evcon->close_ev)) + event_del(&evcon->close_ev); + event_assign(&evcon->close_ev, evcon->base, evcon->fd, EV_READ, + evrtsp_detect_close_cb, evcon); + event_add(&evcon->close_ev, NULL); +} + +static void +evrtsp_connection_stop_detectclose(struct evrtsp_connection *evcon) +{ + evcon->flags &= ~EVRTSP_CON_CLOSEDETECT; + + if (event_initialized(&evcon->close_ev)) + event_del(&evcon->close_ev); +} + +/* + * Call back for asynchronous connection attempt. + */ + +static void +evrtsp_connectioncb(int fd, short what, void *arg) +{ + struct evrtsp_connection *evcon = arg; + int error; + socklen_t errsz = sizeof(error); + + if (what == EV_TIMEOUT) { + event_warnx("%s: connection timeout for \"%s:%d\" on %d", + __func__, evcon->address, evcon->port, evcon->fd); + goto cleanup; + } + + /* Check if the connection completed */ + if (getsockopt(evcon->fd, SOL_SOCKET, SO_ERROR, (void*)&error, + &errsz) == -1) { + event_warnx("%s: getsockopt for \"%s:%d\" on %d", + __func__, evcon->address, evcon->port, evcon->fd); + goto cleanup; + } + + if (error) { + event_warnx("%s: connect failed for \"%s:%d\" on %d: %s", + __func__, evcon->address, evcon->port, evcon->fd, + strerror(error)); + goto cleanup; + } + + /* We are connected to the server now */ + event_debug(("%s: connected to \"%s:%d\" on %d", + __func__, evcon->address, evcon->port, evcon->fd)); + + evcon->state = EVCON_IDLE; + + /* try to start requests that have queued up on this connection */ + evrtsp_request_dispatch(evcon); + return; + + cleanup: + evrtsp_connection_reset(evcon); + + /* for now, we just signal all requests by executing their callbacks */ + while (TAILQ_FIRST(&evcon->requests) != NULL) { + struct evrtsp_request *request = TAILQ_FIRST(&evcon->requests); + TAILQ_REMOVE(&evcon->requests, request, next); + request->evcon = NULL; + + /* we might want to set an error here */ + request->cb(request, request->cb_arg); + evrtsp_request_free(request); + } +} + +/* + * Check if we got a valid response code. + */ + +static int +evrtsp_valid_response_code(int code) +{ + if (code == 0) + return (0); + + return (1); +} + +/* Parses the status line of an RTSP server */ + +static int +evrtsp_parse_response_line(struct evrtsp_request *req, char *line) +{ + char *protocol; + char *number; + const char *readable = ""; + + protocol = strsep(&line, " "); + if (line == NULL) + return (-1); + number = strsep(&line, " "); + if (line != NULL) + readable = line; + + if (strcmp(protocol, "RTSP/1.0") == 0) { + req->major = 1; + req->minor = 0; + } else if (strcmp(protocol, "RTSP/1.1") == 0) { + req->major = 1; + req->minor = 1; + } else { + event_warnx("%s: bad protocol \"%s\"", + __func__, protocol); + return (-1); + } + + req->response_code = atoi(number); + if (!evrtsp_valid_response_code(req->response_code)) { + event_warnx("%s: bad response code \"%s\"", + __func__, number); + return (-1); + } + + if ((req->response_code_line = strdup(readable)) == NULL) + event_err(1, "%s: strdup", __func__); + + return (0); +} + +const char * +evrtsp_find_header(const struct evkeyvalq *headers, const char *key) +{ + struct evkeyval *header; + + TAILQ_FOREACH(header, headers, next) { + if (strcasecmp(header->key, key) == 0) + return (header->value); + } + + return (NULL); +} + +void +evrtsp_clear_headers(struct evkeyvalq *headers) +{ + struct evkeyval *header; + + for (header = TAILQ_FIRST(headers); + header != NULL; + header = TAILQ_FIRST(headers)) { + TAILQ_REMOVE(headers, header, next); + free(header->key); + free(header->value); + free(header); + } +} + +/* + * Returns 0, if the header was successfully removed. + * Returns -1, if the header could not be found. + */ + +int +evrtsp_remove_header(struct evkeyvalq *headers, const char *key) +{ + struct evkeyval *header; + + TAILQ_FOREACH(header, headers, next) { + if (strcasecmp(header->key, key) == 0) + break; + } + + if (header == NULL) + return (-1); + + /* Free and remove the header that we found */ + TAILQ_REMOVE(headers, header, next); + free(header->key); + free(header->value); + free(header); + + return (0); +} + +static int +evrtsp_header_is_valid_value(const char *value) +{ + const char *p = value; + + while ((p = strpbrk(p, "\r\n")) != NULL) { + /* we really expect only one new line */ + p += strspn(p, "\r\n"); + /* we expect a space or tab for continuation */ + if (*p != ' ' && *p != '\t') + return (0); + } + return (1); +} + +int +evrtsp_add_header(struct evkeyvalq *headers, + const char *key, const char *value) +{ + event_debug(("%s: key: %s val: %s", __func__, key, value)); + + if (strchr(key, '\r') != NULL || strchr(key, '\n') != NULL) { + /* drop illegal headers */ + event_warn("%s: dropping illegal header key", __func__); + return (-1); + } + + if (!evrtsp_header_is_valid_value(value)) { + event_warn("%s: dropping illegal header value", __func__); + return (-1); + } + + return (evrtsp_add_header_internal(headers, key, value)); +} + +static int +evrtsp_add_header_internal(struct evkeyvalq *headers, + const char *key, const char *value) +{ + struct evkeyval *header = calloc(1, sizeof(struct evkeyval)); + + if (header == NULL) { + event_warn("%s: calloc", __func__); + return (-1); + } + if ((header->key = strdup(key)) == NULL) { + free(header); + event_warn("%s: strdup", __func__); + return (-1); + } + if ((header->value = strdup(value)) == NULL) { + free(header->key); + free(header); + event_warn("%s: strdup", __func__); + return (-1); + } + + TAILQ_INSERT_TAIL(headers, header, next); + + return (0); +} + +/* + * Parses header lines from a request or a response into the specified + * request object given an event buffer. + * + * Returns + * DATA_CORRUPTED on error + * MORE_DATA_EXPECTED when we need to read more headers + * ALL_DATA_READ when all headers have been read. + */ + +enum message_read_status +evrtsp_parse_firstline(struct evrtsp_request *req, struct evbuffer *buffer) +{ + char *line; + enum message_read_status status = ALL_DATA_READ; + + line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_ANY); + if (line == NULL) + return (MORE_DATA_EXPECTED); + + switch (req->kind) { + case EVRTSP_RESPONSE: + if (evrtsp_parse_response_line(req, line) == -1) + status = DATA_CORRUPTED; + break; + default: + status = DATA_CORRUPTED; + } + + free(line); + return (status); +} + +static int +evrtsp_append_to_last_header(struct evkeyvalq *headers, const char *line) +{ + struct evkeyval *header = TAILQ_LAST(headers, evkeyvalq); + char *newval; + size_t old_len, line_len; + + if (header == NULL) + return (-1); + + old_len = strlen(header->value); + line_len = strlen(line); + + newval = realloc(header->value, old_len + line_len + 1); + if (newval == NULL) + return (-1); + + memcpy(newval + old_len, line, line_len + 1); + header->value = newval; + + return (0); +} + +enum message_read_status +evrtsp_parse_headers(struct evrtsp_request *req, struct evbuffer *buffer) +{ + char *line; + enum message_read_status status = MORE_DATA_EXPECTED; + + struct evkeyvalq *headers = req->input_headers; + while ((line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_CRLF)) + != NULL) { + char *skey, *svalue; + + if (*line == '\0') { /* Last header - Done */ + status = ALL_DATA_READ; + free(line); + break; + } + + /* Check if this is a continuation line */ + if (*line == ' ' || *line == '\t') { + if (evrtsp_append_to_last_header(headers, line) == -1) + goto error; + free(line); + continue; + } + + /* Processing of header lines */ + svalue = line; + skey = strsep(&svalue, ":"); + if (svalue == NULL) + goto error; + + svalue += strspn(svalue, " "); + + if (evrtsp_add_header(headers, skey, svalue) == -1) + goto error; + + free(line); + } + + return (status); + + error: + free(line); + return (DATA_CORRUPTED); +} + +static int +evrtsp_get_body_length(struct evrtsp_request *req) +{ + struct evkeyvalq *headers = req->input_headers; + const char *content_length; + + content_length = evrtsp_find_header(headers, "Content-Length"); + + if (content_length == NULL) { + /* If there is no Content-Length: header, a value of 0 is assumed, per spec. */ + req->ntoread = 0; + } else { + ev_int64_t ntoread = evutil_strtoll(content_length, NULL, 10); + if (*content_length == '\0' || ntoread < 0) { + event_warnx("%s: illegal content length: %s", + __func__, content_length); + return (-1); + } + req->ntoread = ntoread; + } + + event_debug(("%s: bytes to read: %lld (in buffer %zu)", + __func__, req->ntoread, + evbuffer_get_length(req->evcon->input_buffer))); + + return (0); +} + +static void +evrtsp_get_body(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + evcon->state = EVCON_READING_BODY; + if (evrtsp_get_body_length(req) == -1) { + event_warn("%s: invalid body", __func__); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + return; + } + + evrtsp_read_body(evcon, req); +} + +static void +evrtsp_read_firstline(struct evrtsp_connection *evcon, + struct evrtsp_request *req) +{ + enum message_read_status res; + + res = evrtsp_parse_firstline(req, evcon->input_buffer); + if (res == DATA_CORRUPTED) { + /* Error while reading, terminate */ + event_warnx("%s: bad header lines on %d", + __func__, evcon->fd); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + return; + } else if (res == MORE_DATA_EXPECTED) { + /* Need more header lines */ + evrtsp_add_event(&evcon->ev, + evcon->timeout, RTSP_READ_TIMEOUT); + return; + } + + evcon->state = EVCON_READING_HEADERS; + evrtsp_read_header(evcon, req); +} + +static void +evrtsp_read_header(struct evrtsp_connection *evcon, struct evrtsp_request *req) +{ + enum message_read_status res; + int fd = evcon->fd; + + res = evrtsp_parse_headers(req, evcon->input_buffer); + if (res == DATA_CORRUPTED) { + /* Error while reading, terminate */ + event_warnx("%s: bad header lines on %d", __func__, fd); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + return; + } else if (res == MORE_DATA_EXPECTED) { + /* Need more header lines */ + evrtsp_add_event(&evcon->ev, + evcon->timeout, RTSP_READ_TIMEOUT); + return; + } + + /* Done reading headers, do the real work */ + switch (req->kind) { + case EVRTSP_RESPONSE: + event_debug(("%s: start of read body on %d", + __func__, fd)); + evrtsp_get_body(evcon, req); + break; + + default: + event_warnx("%s: bad header on %d", __func__, fd); + evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); + break; + } +} + +/* + * Creates a TCP connection to the specified port and executes a callback + * when finished. Failure or sucess is indicate by the passed connection + * object. + * + * Although this interface accepts a hostname, it is intended to take + * only numeric hostnames so that non-blocking DNS resolution can + * happen elsewhere. + */ + +struct evrtsp_connection * +evrtsp_connection_new(const char *address, unsigned short port) +{ + struct evrtsp_connection *evcon = NULL; + char *intf; + char *addr; + unsigned char scratch[16]; + int family; + + if ((addr = strdup(address)) == NULL) { + event_warn("%s: strdup failed", __func__); + goto error; + } + + intf = strchr(addr, '%'); + if (intf) + *intf = '\0'; + + if (inet_pton(AF_INET6, addr, scratch) == 1) + family = AF_INET6; + else if (inet_pton(AF_INET, addr, scratch) == 1) + family = AF_INET; + else { + free(addr); + event_warn("%s: address is neither IPv6 nor IPv4", __func__); + return NULL; + } + + if (intf) + *intf = '%'; + + event_debug(("Attempting connection to %s:%d", address, port)); + + if ((evcon = calloc(1, sizeof(struct evrtsp_connection))) == NULL) { + free(addr); + event_warn("%s: calloc failed", __func__); + goto error; + } + + evcon->fd = -1; + evcon->port = port; + + evcon->timeout = -1; + + evcon->cseq = 1; + + evcon->family = family; + evcon->address = addr; + + if ((evcon->input_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new failed", __func__); + goto error; + } + + if ((evcon->output_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new failed", __func__); + goto error; + } + + evcon->state = EVCON_DISCONNECTED; + TAILQ_INIT(&evcon->requests); + + return (evcon); + + error: + if (evcon != NULL) + evrtsp_connection_free(evcon); + return (NULL); +} + +void evrtsp_connection_set_base(struct evrtsp_connection *evcon, + struct event_base *base) +{ + assert(evcon->base == NULL); + assert(evcon->state == EVCON_DISCONNECTED); + evcon->base = base; +} + +void +evrtsp_connection_set_timeout(struct evrtsp_connection *evcon, + int timeout_in_secs) +{ + evcon->timeout = timeout_in_secs; +} + +void +evrtsp_connection_set_closecb(struct evrtsp_connection *evcon, + void (*cb)(struct evrtsp_connection *, void *), void *cbarg) +{ + evcon->closecb = cb; + evcon->closecb_arg = cbarg; +} + +void +evrtsp_connection_set_ciphercb(struct evrtsp_connection *evcon, + void (*cb)(struct evbuffer *, void *, int encrypt), void *cbarg) +{ + evcon->ciphercb = cb; + evcon->ciphercb_arg = cbarg; +} + +void +evrtsp_connection_get_local_address(struct evrtsp_connection *evcon, + char **address, u_short *port, int *family) +{ + union { + struct sockaddr_storage ss; + struct sockaddr sa; + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + } addr; + socklen_t slen; + int ret; + + *address = NULL; + *port = 0; + memset(&addr, 0, sizeof(addr)); + + if (!evrtsp_connected(evcon)) + return; + + slen = sizeof(struct sockaddr_storage); + ret = getsockname(evcon->fd, &addr.sa, &slen); + if (ret < 0) + return; + + name_from_addr(&addr.sa, slen, address, NULL); + + if (!*address) + return; + + *family = addr.sa.sa_family; + + switch (*family) + { + case AF_INET: + *port = ntohs(addr.sin.sin_port); + break; + +#ifdef AF_INET6 + case AF_INET6: + *port = ntohs(addr.sin6.sin6_port); + break; +#endif + + default: + free(*address); + *address = NULL; + + event_err(1, "%s: unhandled address family\n", __func__); + return; + } +} + +void +evrtsp_connection_get_peer(struct evrtsp_connection *evcon, + char **address, u_short *port) +{ + *address = evcon->address; + *port = evcon->port; +} + +int +evrtsp_connection_connect(struct evrtsp_connection *evcon) +{ + if (evcon->state == EVCON_CONNECTING) + return (0); + + evrtsp_connection_reset(evcon); + + evcon->fd = bind_socket(evcon->family, + evcon->bind_address, evcon->bind_port, 0 /*reuse*/); + if (evcon->fd == -1) { + event_warnx("%s: failed to bind to \"%s\"", + __func__, evcon->bind_address); + return (-1); + } + + if (socket_connect(evcon->fd, evcon->address, evcon->port) == -1) { + EVUTIL_CLOSESOCKET(evcon->fd); evcon->fd = -1; + return (-1); + } + + /* Set up a callback for successful connection setup */ + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_WRITE, evrtsp_connectioncb, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_CONNECT_TIMEOUT); + + evcon->state = EVCON_CONNECTING; + + return (0); +} + +/* + * Starts an RTSP request on the provided evrtsp_connection object. + * If the connection object is not connected to the server already, + * this will start the connection. + */ + +int +evrtsp_make_request(struct evrtsp_connection *evcon, + struct evrtsp_request *req, + enum evrtsp_cmd_type type, const char *uri) +{ +event_debug(("%s: TEST", __func__)); + + /* We are making a request */ + req->kind = EVRTSP_REQUEST; + req->type = type; + if (req->uri != NULL) + free(req->uri); + if ((req->uri = strdup(uri)) == NULL) + event_err(1, "%s: strdup", __func__); + + /* Set the protocol version if it is not supplied */ + if (!req->major && !req->minor) { + req->major = 1; + req->minor = 0; + } + + assert(req->evcon == NULL); + req->evcon = evcon; + assert(!(req->flags & EVRTSP_REQ_OWN_CONNECTION)); + + TAILQ_INSERT_TAIL(&evcon->requests, req, next); + + /* If the connection object is not connected; make it so */ + if (!evrtsp_connected(evcon)) + return (evrtsp_connection_connect(evcon)); + + /* + * If it's connected already and we are the first in the queue, + * then we can dispatch this request immediately. Otherwise, it + * will be dispatched once the pending requests are completed. + */ + if (TAILQ_FIRST(&evcon->requests) == req) + evrtsp_request_dispatch(evcon); + + return (0); +} + +/* + * Reads data from file descriptor into request structure + * Request structure needs to be set up correctly. + */ + +void +evrtsp_start_read(struct evrtsp_connection *evcon) +{ + /* Set up an event to read the headers */ + if (event_initialized(&evcon->ev)) + event_del(&evcon->ev); + event_assign(&evcon->ev, evcon->base, evcon->fd, EV_READ, evrtsp_read, evcon); + evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); + evcon->state = EVCON_READING_FIRSTLINE; +} + +static void +evrtsp_send_done(struct evrtsp_connection *evcon, void *arg) +{ + struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); + TAILQ_REMOVE(&evcon->requests, req, next); + + /* delete possible close detection events */ + evrtsp_connection_stop_detectclose(evcon); + + assert(req->flags & EVRTSP_REQ_OWN_CONNECTION); + evrtsp_request_free(req); +} + +/* Requires that headers and response code are already set up */ + +static inline void +evrtsp_send(struct evrtsp_request *req, struct evbuffer *databuf) +{ + struct evrtsp_connection *evcon = req->evcon; + + if (evcon == NULL) { + evrtsp_request_free(req); + return; + } + + assert(TAILQ_FIRST(&evcon->requests) == req); + + /* xxx: not sure if we really should expose the data buffer this way */ + if (databuf != NULL) + evbuffer_add_buffer(req->output_buffer, databuf); + + /* Adds headers to the response */ + evrtsp_make_header(evcon, req); + + evrtsp_write_buffer(evcon, evrtsp_send_done, NULL); +} + +/* + * Request related functions + */ + +struct evrtsp_request * +evrtsp_request_new(void (*cb)(struct evrtsp_request *, void *), void *arg) +{ + struct evrtsp_request *req = NULL; + + /* Allocate request structure */ + if ((req = calloc(1, sizeof(struct evrtsp_request))) == NULL) { + event_warn("%s: calloc", __func__); + goto error; + } + + req->kind = EVRTSP_RESPONSE; + req->input_headers = calloc(1, sizeof(struct evkeyvalq)); + if (req->input_headers == NULL) { + event_warn("%s: calloc", __func__); + goto error; + } + TAILQ_INIT(req->input_headers); + + req->output_headers = calloc(1, sizeof(struct evkeyvalq)); + if (req->output_headers == NULL) { + event_warn("%s: calloc", __func__); + goto error; + } + TAILQ_INIT(req->output_headers); + + if ((req->input_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new", __func__); + goto error; + } + + if ((req->output_buffer = evbuffer_new()) == NULL) { + event_warn("%s: evbuffer_new", __func__); + goto error; + } + + req->cb = cb; + req->cb_arg = arg; + + return (req); + + error: + if (req != NULL) + evrtsp_request_free(req); + return (NULL); +} + +void +evrtsp_request_free(struct evrtsp_request *req) +{ + if (req->uri != NULL) + free(req->uri); + if (req->response_code_line != NULL) + free(req->response_code_line); + + evrtsp_clear_headers(req->input_headers); + free(req->input_headers); + + evrtsp_clear_headers(req->output_headers); + free(req->output_headers); + + if (req->input_buffer != NULL) + evbuffer_free(req->input_buffer); + + if (req->output_buffer != NULL) + evbuffer_free(req->output_buffer); + + free(req); +} + +/* + * Allows for inspection of the request URI + */ + +const char * +evrtsp_request_uri(struct evrtsp_request *req) { + if (req->uri == NULL) + event_warn("%s: request has no uri", __func__); + return (req->uri); +} + +/* + * Network helper functions that we do not want to export to the rest of + * the world. + */ +#if 0 /* Unused */ +static struct addrinfo * +addr_from_name(char *address) +{ +#ifdef EVENT__HAVE_GETADDRINFO + struct addrinfo ai, *aitop; + int ai_result; + + memset(&ai, 0, sizeof(ai)); + ai.ai_family = AF_INET; + ai.ai_socktype = SOCK_RAW; + ai.ai_flags = 0; + if ((ai_result = getaddrinfo(address, NULL, &ai, &aitop)) != 0) { + if ( ai_result == EAI_SYSTEM ) + event_warn("getaddrinfo"); + else + event_warnx("getaddrinfo: %s", gai_strerror(ai_result)); + } + + return (aitop); +#else + assert(0); + return NULL; /* XXXXX Use gethostbyname, if this function is ever used. */ +#endif +} +#endif + +static void +name_from_addr(struct sockaddr *sa, socklen_t salen, + char **phost, char **pport) +{ + char ntop[NI_MAXHOST]; + char strport[NI_MAXSERV]; + int ni_result; + +#ifdef EVENT__HAVE_GETNAMEINFO + ni_result = getnameinfo(sa, salen, + ntop, sizeof(ntop), strport, sizeof(strport), + NI_NUMERICHOST|NI_NUMERICSERV); + + if (ni_result != 0) { + if (ni_result == EAI_SYSTEM) + event_err(1, "getnameinfo failed"); + else + event_errx(1, "getnameinfo failed: %s", gai_strerror(ni_result)); + return; + } +#else + ni_result = fake_getnameinfo(sa, salen, + ntop, sizeof(ntop), strport, sizeof(strport), + NI_NUMERICHOST|NI_NUMERICSERV); + if (ni_result != 0) + return; +#endif + if (phost) + *phost = strdup(ntop); + if (pport) + *pport = strdup(strport); +} + +/* Create a non-blocking socket and bind it */ +/* todo: rename this function */ +static int +bind_socket_ai(int family, struct addrinfo *ai, int reuse) +{ + int fd, on = 1, r; + int serrno; + + if (ai) + family = ai->ai_family; + + /* Create listen socket */ + fd = socket(family, SOCK_STREAM, 0); + if (fd == -1) { + event_warn("socket"); + return (-1); + } + + if (evutil_make_socket_nonblocking(fd) < 0) + goto out; + +#ifndef WIN32 + if (fcntl(fd, F_SETFD, 1) == -1) { + event_warn("fcntl(F_SETFD)"); + goto out; + } +#endif + + if (family == AF_INET6) + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)); + + setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, sizeof(on)); + if (reuse) { + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, + (void *)&on, sizeof(on)); + } + + if (ai != NULL) { + r = bind(fd, ai->ai_addr, ai->ai_addrlen); + if (r == -1) + goto out; + } + + return (fd); + + out: + serrno = EVUTIL_SOCKET_ERROR(); + EVUTIL_CLOSESOCKET(fd); + EVUTIL_SET_SOCKET_ERROR(serrno); + return (-1); +} + +static struct addrinfo * +make_addrinfo(const char *address, u_short port) +{ + struct addrinfo *aitop = NULL; + +#ifdef EVENT__HAVE_GETADDRINFO + struct addrinfo ai; + char strport[NI_MAXSERV]; + int ai_result; + + memset(&ai, 0, sizeof(ai)); + ai.ai_family = AF_UNSPEC; + ai.ai_socktype = SOCK_STREAM; + ai.ai_flags = AI_PASSIVE; /* turn NULL host name into INADDR_ANY */ + evutil_snprintf(strport, sizeof(strport), "%d", port); + if ((ai_result = getaddrinfo(address, strport, &ai, &aitop)) != 0) { + if ( ai_result == EAI_SYSTEM ) + event_warn("getaddrinfo"); + else + event_warnx("getaddrinfo: %s", gai_strerror(ai_result)); + return (NULL); + } +#else + static int cur; + static struct addrinfo ai[2]; /* We will be returning the address of some of this memory so it has to last even after this call. */ + if (++cur == 2) cur = 0; /* allow calling this function twice */ + + if (fake_getaddrinfo(address, &ai[cur]) < 0) { + event_warn("fake_getaddrinfo"); + return (NULL); + } + aitop = &ai[cur]; + ((struct sockaddr_in *) aitop->ai_addr)->sin_port = htons(port); +#endif + + return (aitop); +} + +static int +bind_socket(int family, const char *address, u_short port, int reuse) +{ + int fd; + struct addrinfo *aitop = NULL; + + /* just create an unbound socket */ + if (address == NULL && port == 0) + return bind_socket_ai(family, NULL, 0); + + aitop = make_addrinfo(address, port); + + if (aitop == NULL) + return (-1); + + fd = bind_socket_ai(family, aitop, reuse); + +#ifdef EVENT__HAVE_GETADDRINFO + freeaddrinfo(aitop); +#else + fake_freeaddrinfo(aitop); +#endif + + return (fd); +} + +static int +socket_connect(int fd, const char *address, unsigned short port) +{ + struct addrinfo *ai = make_addrinfo(address, port); + int res = -1; + + if (ai == NULL) { + event_warnx("%s: make_addrinfo: \"%s:%d\"", + __func__, address, port); + return (-1); + } + + if (connect(fd, ai->ai_addr, ai->ai_addrlen) == -1) { +#ifdef WIN32 + int tmp_error = WSAGetLastError(); + if (tmp_error != WSAEWOULDBLOCK && tmp_error != WSAEINVAL && + tmp_error != WSAEINPROGRESS) { + goto out; + } +#else + if (errno != EINPROGRESS) { + goto out; + } +#endif + } + + /* everything is fine */ + res = 0; + +out: +#ifdef EVENT__HAVE_GETADDRINFO + freeaddrinfo(ai); +#else + fake_freeaddrinfo(ai); +#endif + + return (res); +} diff --git a/pair_ap/pair_fruit.c b/pair_ap/pair_fruit.c new file mode 100644 index 00000000..42e0b066 --- /dev/null +++ b/pair_ap/pair_fruit.c @@ -0,0 +1,1063 @@ +/* + * + * The Secure Remote Password 6a implementation is adapted from: + * - Tom Cocagne + * + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include +#include + +#include +#include + +#include + +#include "pair-internal.h" + +/* ----------------------------- DEFINES ETC ------------------------------- */ + +#define USERNAME "12:34:56:78:90:AB" +#define EPK_LENGTH 32 +#define AUTHTAG_LENGTH 16 +#define AES_SETUP_KEY "Pair-Setup-AES-Key" +#define AES_SETUP_IV "Pair-Setup-AES-IV" +#define AES_VERIFY_KEY "Pair-Verify-AES-Key" +#define AES_VERIFY_IV "Pair-Verify-AES-IV" + + +/* ---------------------------------- SRP ---------------------------------- */ + +typedef enum +{ + SRP_NG_2048, + SRP_NG_CUSTOM +} SRP_NGType; + +typedef struct +{ + bnum N; + bnum g; +} NGConstant; + +struct SRPUser +{ + enum hash_alg alg; + NGConstant *ng; + + bnum a; + bnum A; + bnum S; + + const unsigned char *bytes_A; + int authenticated; + + char *username; + unsigned char *password; + int password_len; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [2 * SHA512_DIGEST_LENGTH]; // See hash_session_key() + int session_key_len; +}; + +struct NGHex +{ + const char *n_hex; + const char *g_hex; +}; + +// We only need 2048 right now, but keep the array in case we want to add others later +// All constants here were pulled from Appendix A of RFC 5054 +static struct NGHex global_Ng_constants[] = +{ + { /* 2048 */ + "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" + "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" + "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" + "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" + "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" + "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" + "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", + "2" + }, + {0,0} /* null sentinel */ +}; + + +static NGConstant * +new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) +{ + NGConstant *ng = calloc(1, sizeof(NGConstant)); + + if ( ng_type != SRP_NG_CUSTOM ) + { + n_hex = global_Ng_constants[ ng_type ].n_hex; + g_hex = global_Ng_constants[ ng_type ].g_hex; + } + + bnum_hex2bn(ng->N, n_hex); + bnum_hex2bn(ng->g, g_hex); + + return ng; +} + +static void +free_ng(NGConstant * ng) +{ + if (!ng) + return; + + bnum_free(ng->N); + bnum_free(ng->g); + free(ng); +} + +static bnum +calculate_x(enum hash_alg alg, const bnum salt, const char *username, const unsigned char *password, int password_len) +{ + unsigned char ucp_hash[SHA512_DIGEST_LENGTH]; + HashCTX ctx; + + hash_init( alg, &ctx ); + hash_update( alg, &ctx, username, strlen(username) ); + hash_update( alg, &ctx, ":", 1 ); + hash_update( alg, &ctx, password, password_len ); + hash_final( alg, &ctx, ucp_hash ); + + return H_ns( alg, salt, ucp_hash, hash_length(alg) ); +} + +static int +hash_session_key(enum hash_alg alg, const bnum n, unsigned char *dest) +{ + int nbytes = bnum_num_bytes(n); + unsigned char *bin = malloc(nbytes); + unsigned char fourbytes[4] = { 0 }; // Only God knows the reason for this, and perhaps some poor soul at Apple + + bnum_bn2bin(n, bin, nbytes); + + hash_ab(alg, dest, bin, nbytes, fourbytes, sizeof(fourbytes)); + + fourbytes[3] = 1; // Again, only ... + + hash_ab(alg, dest + hash_length(alg), bin, nbytes, fourbytes, sizeof(fourbytes)); + + free(bin); + + return (2 * hash_length(alg)); +} + +static void +calculate_M(enum hash_alg alg, NGConstant *ng, unsigned char *dest, const char *I, const bnum s, + const bnum A, const bnum B, const unsigned char *K, int K_len) +{ + unsigned char H_N[ SHA512_DIGEST_LENGTH ]; + unsigned char H_g[ SHA512_DIGEST_LENGTH ]; + unsigned char H_I[ SHA512_DIGEST_LENGTH ]; + unsigned char H_xor[ SHA512_DIGEST_LENGTH ]; + HashCTX ctx; + int i = 0; + int hash_len = hash_length(alg); + + hash_num( alg, ng->N, H_N ); + hash_num( alg, ng->g, H_g ); + + hash(alg, (const unsigned char *)I, strlen(I), H_I); + + for (i=0; i < hash_len; i++ ) + H_xor[i] = H_N[i] ^ H_g[i]; + + hash_init( alg, &ctx ); + + hash_update( alg, &ctx, H_xor, hash_len ); + hash_update( alg, &ctx, H_I, hash_len ); + update_hash_n( alg, &ctx, s ); + update_hash_n( alg, &ctx, A ); + update_hash_n( alg, &ctx, B ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +static void +calculate_H_AMK(enum hash_alg alg, unsigned char *dest, const bnum A, const unsigned char * M, const unsigned char * K, int K_len) +{ + HashCTX ctx; + + hash_init( alg, &ctx ); + + update_hash_n( alg, &ctx, A ); + hash_update( alg, &ctx, M, hash_length(alg) ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +static struct SRPUser * +srp_user_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_password, int len_password, + const char *n_hex, const char *g_hex) +{ + struct SRPUser *usr = calloc(1, sizeof(struct SRPUser)); + int ulen = strlen(username) + 1; + + if (!usr) + goto err_exit; + + usr->alg = alg; + usr->ng = new_ng( ng_type, n_hex, g_hex ); + + bnum_new(usr->a); + bnum_new(usr->A); + bnum_new(usr->S); + + if (!usr->ng || !usr->a || !usr->A || !usr->S) + goto err_exit; + + usr->username = malloc(ulen); + usr->password = malloc(len_password); + usr->password_len = len_password; + + if (!usr->username || !usr->password) + goto err_exit; + + memcpy(usr->username, username, ulen); + memcpy(usr->password, bytes_password, len_password); + + usr->authenticated = 0; + usr->bytes_A = 0; + + return usr; + + err_exit: + if (!usr) + return NULL; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free(usr->username); + if (usr->password) + { + memset(usr->password, 0, usr->password_len); + free(usr->password); + } + free(usr); + + return NULL; +} + +static void +srp_user_free(struct SRPUser *usr) +{ + if(!usr) + return; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free_ng(usr->ng); + + memset(usr->password, 0, usr->password_len); + + free(usr->username); + free(usr->password); + free((char *)usr->bytes_A); + + memset(usr, 0, sizeof(*usr)); + free(usr); +} + +static int +srp_user_is_authenticated(struct SRPUser *usr) +{ + return usr->authenticated; +} + +static const unsigned char * +srp_user_get_session_key(struct SRPUser *usr, int *key_length) +{ + if (key_length) + *key_length = usr->session_key_len; + return usr->session_key; +} + +/* Output: username, bytes_A, len_A */ +static void +srp_user_start_authentication(struct SRPUser *usr, const char **username, + const unsigned char **bytes_A, int *len_A) +{ + bnum_random(usr->a, 256); + bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); + + *len_A = bnum_num_bytes(usr->A); + *bytes_A = malloc(*len_A); + + if (!*bytes_A) + { + *len_A = 0; + *bytes_A = 0; + *username = 0; + return; + } + + bnum_bn2bin(usr->A, (unsigned char *) *bytes_A, *len_A); + + usr->bytes_A = *bytes_A; + *username = usr->username; +} + +/* Output: bytes_M. Buffer length is SHA512_DIGEST_LENGTH */ +static void +srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_B, int len_B, + const unsigned char **bytes_M, int *len_M ) +{ + bnum s, B, k, v; + bnum tmp1, tmp2, tmp3; + bnum u, x; + + *len_M = 0; + *bytes_M = 0; + + bnum_bin2bn(s, bytes_s, len_s); + bnum_bin2bn(B, bytes_B, len_B); + k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g); + bnum_new(v); + bnum_new(tmp1); + bnum_new(tmp2); + bnum_new(tmp3); + + if (!s || !B || !k || !v || !tmp1 || !tmp2 || !tmp3) + goto cleanup1; + + u = H_nn_pad(usr->alg, usr->A, B); + x = calculate_x(usr->alg, s, usr->username, usr->password, usr->password_len); + if (!u || !x) + goto cleanup2; + + // SRP-6a safety check + if (!bnum_is_zero(B) && !bnum_is_zero(u)) + { + bnum_modexp(v, usr->ng->g, x, usr->ng->N); + + // S = (B - k*(g^x)) ^ (a + ux) + bnum_mul(tmp1, u, x); + bnum_add(tmp2, usr->a, tmp1); // tmp2 = (a + ux) + bnum_modexp(tmp1, usr->ng->g, x, usr->ng->N); + bnum_mul(tmp3, k, tmp1); // tmp3 = k*(g^x) + bnum_sub(tmp1, B, tmp3); // tmp1 = (B - K*(g^x)) + bnum_modexp(usr->S, tmp1, tmp2, usr->ng->N); + + usr->session_key_len = hash_session_key(usr->alg, usr->S, usr->session_key); + + calculate_M(usr->alg, usr->ng, usr->M, usr->username, s, usr->A, B, usr->session_key, usr->session_key_len); + calculate_H_AMK(usr->alg, usr->H_AMK, usr->A, usr->M, usr->session_key, usr->session_key_len); + + *bytes_M = usr->M; + if (len_M) + *len_M = hash_length(usr->alg); + } + else + { + *bytes_M = NULL; + if (len_M) + *len_M = 0; + } + + cleanup2: + bnum_free(x); + bnum_free(u); + cleanup1: + bnum_free(tmp3); + bnum_free(tmp2); + bnum_free(tmp1); + bnum_free(v); + bnum_free(k); + bnum_free(B); + bnum_free(s); +} + +static void +srp_user_verify_session(struct SRPUser *usr, const unsigned char *bytes_HAMK) +{ + if (memcmp(usr->H_AMK, bytes_HAMK, hash_length(usr->alg)) == 0) + usr->authenticated = 1; +} + + +/* -------------------------------- HELPERS -------------------------------- */ + +static int +encrypt_gcm(unsigned char *ciphertext, int ciphertext_len, unsigned char *tag, unsigned char *plaintext, int plaintext_len, unsigned char *key, unsigned char *iv, const char **errmsg) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + *errmsg = NULL; + + if ( !(ctx = EVP_CIPHER_CTX_new()) || + (EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) || + (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL) != 1) || + (EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv) != 1) ) + { + *errmsg = "Error initialising AES 128 GCM encryption"; + goto error; + } + + if (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len) != 1) + { + *errmsg = "Error GCM encrypting"; + goto error; + } + + if (len > ciphertext_len) + { + *errmsg = "Bug! Buffer overflow"; + goto error; + } + + if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1) + { + *errmsg = "Error finalising GCM encryption"; + goto error; + } + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, AUTHTAG_LENGTH, tag) != 1) + { + *errmsg = "Error getting authtag"; + goto error; + } + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + gcry_error_t err; + + err = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_GCM, 0); + if (err) + { + *errmsg = "Error initialising AES 128 GCM encryption"; + return -1; + } + + err = gcry_cipher_setkey(hd, key, gcry_cipher_get_algo_keylen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set key for AES 128 GCM"; + goto error; + } + + err = gcry_cipher_setiv(hd, iv, gcry_cipher_get_algo_blklen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set iv for AES 128 GCM"; + goto error; + } + + err = gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext, plaintext_len); + if (err) + { + *errmsg = "Error GCM encrypting"; + goto error; + } + + err = gcry_cipher_gettag(hd, tag, AUTHTAG_LENGTH); + if (err) + { + *errmsg = "Error getting authtag"; + goto error; + } + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#endif +} + +static int +encrypt_ctr(unsigned char *ciphertext, int ciphertext_len, + unsigned char *plaintext1, int plaintext1_len, unsigned char *plaintext2, int plaintext2_len, + unsigned char *key, unsigned char *iv, const char **errmsg) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + *errmsg = NULL; + + if ( !(ctx = EVP_CIPHER_CTX_new()) || (EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), NULL, key, iv) != 1) ) + { + *errmsg = "Error initialising AES 128 CTR encryption"; + goto error; + } + + if ( (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext1, plaintext1_len) != 1) || + (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext2, plaintext2_len) != 1) ) + { + *errmsg = "Error CTR encrypting"; + goto error; + } + + if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1) + { + *errmsg = "Error finalising encryption"; + goto error; + } + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + gcry_error_t err; + + err = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_CTR, 0); + if (err) + { + *errmsg = "Error initialising AES 128 CTR encryption"; + return -1; + } + err = gcry_cipher_setkey(hd, key, gcry_cipher_get_algo_keylen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set key for AES 128 CTR"; + goto error; + } + + err = gcry_cipher_setctr(hd, iv, gcry_cipher_get_algo_blklen(GCRY_CIPHER_AES128)); + if (err) + { + *errmsg = "Could not set iv for AES 128 CTR"; + goto error; + } + + err = gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext1, plaintext1_len); + if (err) + { + *errmsg = "Error CTR encrypting plaintext 1"; + goto error; + } + + err = gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext2, plaintext2_len); + if (err) + { + *errmsg = "Error CTR encrypting plaintext 2"; + goto error; + } + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#endif +} + + +/* -------------------------- IMPLEMENTATION -------------------------------- */ + +static int +client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + if (sodium_init() == -1) + return -1; + + if (!pin || strlen(pin) < 4) + return -1; + + memcpy(sctx->pin, pin, sizeof(sctx->pin)); + + return 0; +} + +static void +client_setup_free(struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + srp_user_free(sctx->user); + + free(sctx->pkB); + free(sctx->M2); + free(sctx->salt); + free(sctx->epk); + free(sctx->authtag); +} + +static uint8_t * +client_setup_request1(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t method; + plist_t user; + uint32_t uint32; + char *data = NULL; // Necessary to initialize because plist_to_bin() uses value + + sctx->user = srp_user_new(HASH_SHA1, SRP_NG_2048, USERNAME, (unsigned char *)sctx->pin, sizeof(sctx->pin), 0, 0); + + dict = plist_new_dict(); + + method = plist_new_string("pin"); + user = plist_new_string(USERNAME); + + plist_dict_set_item(dict, "method", method); + plist_dict_set_item(dict, "user", user); + plist_to_bin(dict, &data, &uint32); + plist_free(dict); + + *len = (size_t)uint32; + return (uint8_t *)data; +} + +static uint8_t * +client_setup_request2(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t pk; + plist_t proof; + const char *auth_username = NULL; + uint32_t uint32; + char *data = NULL; + + // Calculate A + srp_user_start_authentication(sctx->user, &auth_username, &sctx->pkA, &sctx->pkA_len); + + // Calculate M1 (client proof) + srp_user_process_challenge(sctx->user, (const unsigned char *)sctx->salt, sctx->salt_len, (const unsigned char *)sctx->pkB, sctx->pkB_len, &sctx->M1, &sctx->M1_len); + + pk = plist_new_data((char *)sctx->pkA, sctx->pkA_len); + proof = plist_new_data((char *)sctx->M1, sctx->M1_len); + + dict = plist_new_dict(); + plist_dict_set_item(dict, "pk", pk); + plist_dict_set_item(dict, "proof", proof); + plist_to_bin(dict, &data, &uint32); + plist_free(dict); + + *len = (size_t)uint32; + return (uint8_t *)data; +} + +static uint8_t * +client_setup_request3(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t epk; + plist_t authtag; + uint32_t uint32; + char *data = NULL; + const unsigned char *session_key; + int session_key_len; + unsigned char key[SHA512_DIGEST_LENGTH]; + unsigned char iv[SHA512_DIGEST_LENGTH]; + unsigned char encrypted[128]; // Alloc a bit extra - should only need 2*16 + unsigned char tag[16]; + const char *errmsg; + int ret; + + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup request 3: No valid session key"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, key, (unsigned char *)AES_SETUP_KEY, strlen(AES_SETUP_KEY), session_key, session_key_len); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Hashing of key string and shared secret failed"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, iv, (unsigned char *)AES_SETUP_IV, strlen(AES_SETUP_IV), session_key, session_key_len); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Hashing of iv string and shared secret failed"; + return NULL; + } + + iv[15]++; // Nonce? +/* + if (iv[15] == 0x00 || iv[15] == 0xff) + printf("- note that value of last byte is %d!\n", iv[15]); +*/ + crypto_sign_keypair(sctx->public_key, sctx->private_key); + + ret = encrypt_gcm(encrypted, sizeof(encrypted), tag, sctx->public_key, sizeof(sctx->public_key), key, iv, &errmsg); + if (ret < 0) + { + handle->errmsg = errmsg; + return NULL; + } + + epk = plist_new_data((char *)encrypted, EPK_LENGTH); + authtag = plist_new_data((char *)tag, AUTHTAG_LENGTH); + + dict = plist_new_dict(); + plist_dict_set_item(dict, "epk", epk); + plist_dict_set_item(dict, "authTag", authtag); + plist_to_bin(dict, &data, &uint32); + plist_free(dict); + + *len = (size_t)uint32; + return (uint8_t *)data; +} + +static int +client_setup_response1(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t pk; + plist_t salt; + + plist_from_bin((const char *)data, data_len, &dict); + + pk = plist_dict_get_item(dict, "pk"); + salt = plist_dict_get_item(dict, "salt"); + if (!pk || !salt) + { + handle->errmsg = "Setup response 1: Missing pk or salt"; + plist_free(dict); + return -1; + } + + plist_get_data_val(pk, (char **)&sctx->pkB, &sctx->pkB_len); // B + plist_get_data_val(salt, (char **)&sctx->salt, &sctx->salt_len); + + plist_free(dict); + + return 0; +} + +static int +client_setup_response2(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t proof; + + plist_from_bin((const char *)data, data_len, &dict); + + proof = plist_dict_get_item(dict, "proof"); + if (!proof) + { + handle->errmsg = "Setup response 2: Missing proof"; + plist_free(dict); + return -1; + } + + plist_get_data_val(proof, (char **)&sctx->M2, &sctx->M2_len); // M2 + + plist_free(dict); + + // Check M2 + srp_user_verify_session(sctx->user, (const unsigned char *)sctx->M2); + if (!srp_user_is_authenticated(sctx->user)) + { + handle->errmsg = "Setup response 2: Server authentication failed"; + return -1; + } + + return 0; +} + +static int +client_setup_response3(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + plist_t dict; + plist_t epk; + plist_t authtag; + + plist_from_bin((const char *)data, data_len, &dict); + + epk = plist_dict_get_item(dict, "epk"); + if (!epk) + { + handle->errmsg = "Setup response 3: Missing epk"; + plist_free(dict); + return -1; + } + + plist_get_data_val(epk, (char **)&sctx->epk, &sctx->epk_len); + + authtag = plist_dict_get_item(dict, "authTag"); + if (!authtag) + { + handle->errmsg = "Setup response 3: Missing authTag"; + plist_free(dict); + return -1; + } + + plist_get_data_val(authtag, (char **)&sctx->authtag, &sctx->authtag_len); + + plist_free(dict); + + assert(sizeof(handle->result.client_private_key) == sizeof(sctx->private_key)); + assert(sizeof(handle->result.client_public_key) == sizeof(sctx->public_key)); + + memcpy(handle->result.client_private_key, sctx->private_key, sizeof(sctx->private_key)); + memcpy(handle->result.client_public_key, sctx->public_key, sizeof(sctx->public_key)); + + handle->status = PAIR_STATUS_COMPLETED; + return 0; +} + +static int +client_setup_result(struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + char *ptr; + int i; + + // Last 32 bytes of the private key is the public key, so we don't need to + // explicitly export that + ptr = handle->result_str; + for (i = 0; i < sizeof(sctx->private_key); i++) + ptr += sprintf(ptr, "%02x", sctx->private_key[i]); // 2 x 64 bytes + *ptr = '\0'; + + return 0; +} + + +static int +client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + char hex[] = { 0, 0, 0 }; + size_t hexkey_len; + const char *ptr; + int i; + + if (sodium_init() == -1) + return -1; + + if (!client_setup_keys) + return -1; + + hexkey_len = strlen(client_setup_keys); + + if (hexkey_len != 2 * sizeof(vctx->client_private_key)) + return -1; + + if (device_id && strlen(device_id) != 16) + return -1; + + if (device_id) + memcpy(vctx->device_id, device_id, strlen(device_id)); + + ptr = client_setup_keys; + for (i = 0; i < sizeof(vctx->client_private_key); i++, ptr+=2) + { + hex[0] = ptr[0]; + hex[1] = ptr[1]; + vctx->client_private_key[i] = strtol(hex, NULL, 16); + } + + crypto_sign_ed25519_sk_to_pk(vctx->client_public_key, vctx->client_private_key); + + return 0; +} + +static uint8_t * +client_verify_request1(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + const uint8_t basepoint[32] = {9}; + uint8_t *data; + int ret; + + ret = crypto_scalarmult(vctx->client_eph_public_key, vctx->client_eph_private_key, basepoint); + if (ret < 0) + { + handle->errmsg = "Verify request 1: Curve 25519 returned an error"; + return NULL; + } + + *len = 4 + sizeof(vctx->client_eph_public_key) + sizeof(vctx->client_public_key); + data = calloc(1, *len); + if (!data) + { + handle->errmsg = "Verify request 1: Out of memory"; + return NULL; + } + + data[0] = 1; // Magic + memcpy(data + 4, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + memcpy(data + 4 + sizeof(vctx->client_eph_public_key), vctx->client_public_key, sizeof(vctx->client_public_key)); + + return data; +} + +static uint8_t * +client_verify_request2(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + uint8_t shared_secret[crypto_scalarmult_BYTES]; + uint8_t key[SHA512_DIGEST_LENGTH]; + uint8_t iv[SHA512_DIGEST_LENGTH]; + uint8_t encrypted[128]; // Alloc a bit extra, should only really need size of public key len + uint8_t signature[crypto_sign_BYTES]; + uint8_t *data; + int ret; + const char *errmsg; + + *len = sizeof(vctx->client_eph_public_key) + sizeof(vctx->server_eph_public_key); + data = calloc(1, *len); + if (!data) + { + handle->errmsg = "Verify request 2: Out of memory"; + return NULL; + } + + memcpy(data, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + memcpy(data + sizeof(vctx->client_eph_public_key), vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key)); + + crypto_sign_detached(signature, NULL, data, *len, vctx->client_private_key); + + free(data); + + ret = crypto_scalarmult(shared_secret, vctx->client_eph_private_key, vctx->server_eph_public_key); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Curve 25519 returned an error"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, key, (unsigned char *)AES_VERIFY_KEY, strlen(AES_VERIFY_KEY), shared_secret, sizeof(shared_secret)); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Hashing of key string and shared secret failed"; + return NULL; + } + + ret = hash_ab(HASH_SHA512, iv, (unsigned char *)AES_VERIFY_IV, strlen(AES_VERIFY_IV), shared_secret, sizeof(shared_secret)); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Hashing of iv string and shared secret failed"; + return NULL; + } + + ret = encrypt_ctr(encrypted, sizeof(encrypted), vctx->server_fruit_public_key, sizeof(vctx->server_fruit_public_key), signature, sizeof(signature), key, iv, &errmsg); + if (ret < 0) + { + handle->errmsg = errmsg; + return NULL; + } + + *len = 4 + sizeof(vctx->server_fruit_public_key); + data = calloc(1, *len); + if (!data) + { + handle->errmsg = "Verify request 2: Out of memory"; + return NULL; + } + + memcpy(data + 4, encrypted, sizeof(vctx->server_fruit_public_key)); + + return data; +} + +static int +client_verify_response1(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + size_t wanted; + + wanted = sizeof(vctx->server_eph_public_key) + sizeof(vctx->server_fruit_public_key); + if (data_len < wanted) + { + handle->errmsg = "Verify response 2: Unexpected response (too short)"; + return -1; + } + + memcpy(vctx->server_eph_public_key, data, sizeof(vctx->server_eph_public_key)); + memcpy(vctx->server_fruit_public_key, data + sizeof(vctx->server_eph_public_key), sizeof(vctx->server_fruit_public_key)); + + return 0; +} + +static int +client_verify_response2(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + // TODO actually check response + + memcpy(handle->result.shared_secret, vctx->shared_secret, sizeof(vctx->shared_secret)); + handle->result.shared_secret_len = sizeof(vctx->shared_secret); + + handle->status = PAIR_STATUS_COMPLETED; + + return 0; +} + + +struct pair_definition pair_client_fruit = +{ + .pair_setup_new = client_setup_new, + .pair_setup_free = client_setup_free, + .pair_setup_result = client_setup_result, + + .pair_setup_request1 = client_setup_request1, + .pair_setup_request2 = client_setup_request2, + .pair_setup_request3 = client_setup_request3, + + .pair_setup_response1 = client_setup_response1, + .pair_setup_response2 = client_setup_response2, + .pair_setup_response3 = client_setup_response3, + + .pair_verify_new = client_verify_new, + + .pair_verify_request1 = client_verify_request1, + .pair_verify_request2 = client_verify_request2, + + .pair_verify_response1 = client_verify_response1, + .pair_verify_response2 = client_verify_response2, +}; diff --git a/pair_ap/pair_homekit.c b/pair_ap/pair_homekit.c new file mode 100644 index 00000000..076ea14a --- /dev/null +++ b/pair_ap/pair_homekit.c @@ -0,0 +1,3123 @@ +/* + * Adaption of ap2-sender by ViktoriiaKh: + * + * + * To test, it is useful to try with this receiver: + * + * + * The Secure Remote Password 6a implementation is adapted from: + * - Tom Cocagne + * + * + * TLV helpers are adapted from ESP homekit: + * + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include +#include +#include + +#include + +#include + +#include "pair-internal.h" +#include "pair-tlv.h" + + +/* ----------------------------- DEFINES ETC ------------------------------- */ + +#define USERNAME "Pair-Setup" +#define AUTHTAG_LENGTH 16 +#define NONCE_LENGTH 12 // 96 bits according to chacha poly1305 +#define REQUEST_BUFSIZE 4096 +#define ENCRYPTED_LEN_MAX 0x400 + +enum pair_keys +{ + PAIR_SETUP_MSG01 = 0, + PAIR_SETUP_MSG02, + PAIR_SETUP_MSG03, + PAIR_SETUP_MSG04, + PAIR_SETUP_MSG05, + PAIR_SETUP_MSG06, + PAIR_SETUP_CONTROLLER_SIGN, + PAIR_SETUP_ACCESSORY_SIGN, + PAIR_VERIFY_MSG01, + PAIR_VERIFY_MSG02, + PAIR_VERIFY_MSG03, + PAIR_VERIFY_MSG04, + PAIR_CONTROL_WRITE, + PAIR_CONTROL_READ, + PAIR_EVENTS_WRITE, + PAIR_EVENTS_READ, +}; + +struct pair_keys_map +{ + uint8_t state; + const char *salt; + const char *info; + const char nonce[8]; +}; + +static struct pair_keys_map pair_keys_map[] = +{ + // Used for /pair-setup + { 0x01, NULL, NULL, "" }, + { 0x02, NULL, NULL, "" }, + { 0x03, NULL, NULL, "" }, + { 0x04, NULL, NULL, "" }, + { 0x05, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg05" }, + { 0x06, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info", "PS-Msg06" }, + { 0, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", "" }, + { 0, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", "" }, + + // Used for /pair-verify + { 0x01, NULL, NULL, "" }, + { 0x02, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg02" }, + { 0x03, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", "PV-Msg03" }, + { 0x04, NULL, NULL, "" }, + + // Encryption/decryption of control channel + { 0, "Control-Salt", "Control-Write-Encryption-Key", "" }, + { 0, "Control-Salt", "Control-Read-Encryption-Key", "" }, + + // Encryption/decryption of event channel + { 0, "Events-Salt", "Events-Write-Encryption-Key", "" }, + { 0, "Events-Salt", "Events-Read-Encryption-Key", "" }, +}; + +enum pair_method { + PairingMethodPairSetup = 0x00, + PairingMethodPairSetupWithAuth = 0x01, + PairingMethodPairVerify = 0x02, + PairingMethodAddPairing = 0x03, + PairingMethodRemovePairing = 0x04, + PairingMethodListPairings = 0x05 +}; + +enum pair_flags { + PairingFlagsTransient = 0x10, +}; + +// Forwards +const struct pair_definition pair_client_homekit_normal; +const struct pair_definition pair_client_homekit_transient; +const struct pair_definition pair_server_homekit; + + +/* ---------------------------------- SRP ----------------------------------- */ + +typedef enum +{ + SRP_NG_2048, + SRP_NG_3072, + SRP_NG_CUSTOM +} SRP_NGType; + +typedef struct +{ + bnum N; + bnum g; +} NGConstant; + +struct SRPUser +{ + enum hash_alg alg; + NGConstant *ng; + + bnum a; + bnum A; + bnum S; + + const unsigned char *bytes_A; + int authenticated; + + char *username; + unsigned char *password; + int password_len; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [SHA512_DIGEST_LENGTH]; + int session_key_len; +}; + +struct SRPVerifier +{ + enum hash_alg alg; + NGConstant *ng; + + const unsigned char *bytes_B; + int authenticated; + + char *username; + + unsigned char M [SHA512_DIGEST_LENGTH]; + unsigned char H_AMK [SHA512_DIGEST_LENGTH]; + unsigned char session_key [SHA512_DIGEST_LENGTH]; + int session_key_len; +}; + +struct NGHex +{ + const char *n_hex; + const char *g_hex; +}; + +// These constants here were pulled from Appendix A of RFC 5054 +static struct NGHex global_Ng_constants[] = +{ + { /* 2048 */ + "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" + "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" + "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" + "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" + "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" + "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" + "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", + "2" + }, + { /* 3072 */ + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B" + "139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485" + "B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1F" + "E649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23" + "DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32" + "905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF69558" + "17183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521" + "ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D7" + "1E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B1817" + "7B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82" + "D120A93AD2CAFFFFFFFFFFFFFFFF", + "5" + }, + {0,0} /* null sentinel */ +}; + + +static NGConstant * +new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) +{ + NGConstant *ng = calloc(1, sizeof(NGConstant)); + + if ( ng_type != SRP_NG_CUSTOM ) + { + n_hex = global_Ng_constants[ ng_type ].n_hex; + g_hex = global_Ng_constants[ ng_type ].g_hex; + } + + bnum_hex2bn(ng->N, n_hex); + bnum_hex2bn(ng->g, g_hex); + + return ng; +} + +static void +free_ng(NGConstant * ng) +{ + if (!ng) + return; + + bnum_free(ng->N); + bnum_free(ng->g); + free(ng); +} + +static bnum +calculate_x(enum hash_alg alg, const bnum salt, const char *username, const unsigned char *password, int password_len) +{ + unsigned char ucp_hash[SHA512_DIGEST_LENGTH]; + HashCTX ctx; + + hash_init( alg, &ctx ); + hash_update( alg, &ctx, username, strlen(username) ); + hash_update( alg, &ctx, ":", 1 ); + hash_update( alg, &ctx, password, password_len ); + hash_final( alg, &ctx, ucp_hash ); + + return H_ns( alg, salt, ucp_hash, hash_length(alg) ); +} + +static void +calculate_M(enum hash_alg alg, NGConstant *ng, unsigned char *dest, const char *I, const bnum s, + const bnum A, const bnum B, const unsigned char *K, int K_len) +{ + unsigned char H_N[ SHA512_DIGEST_LENGTH ]; + unsigned char H_g[ SHA512_DIGEST_LENGTH ]; + unsigned char H_I[ SHA512_DIGEST_LENGTH ]; + unsigned char H_xor[ SHA512_DIGEST_LENGTH ]; + HashCTX ctx; + int i = 0; + int hash_len = hash_length(alg); + + hash_num( alg, ng->N, H_N ); + hash_num( alg, ng->g, H_g ); + + hash(alg, (const unsigned char *)I, strlen(I), H_I); + + for (i=0; i < hash_len; i++ ) + H_xor[i] = H_N[i] ^ H_g[i]; + + hash_init( alg, &ctx ); + + hash_update( alg, &ctx, H_xor, hash_len ); + hash_update( alg, &ctx, H_I, hash_len ); + update_hash_n( alg, &ctx, s ); + update_hash_n( alg, &ctx, A ); + update_hash_n( alg, &ctx, B ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +static void +calculate_H_AMK(enum hash_alg alg, unsigned char *dest, const bnum A, const unsigned char * M, const unsigned char * K, int K_len) +{ + HashCTX ctx; + + hash_init( alg, &ctx ); + + update_hash_n( alg, &ctx, A ); + hash_update( alg, &ctx, M, hash_length(alg) ); + hash_update( alg, &ctx, K, K_len ); + + hash_final( alg, &ctx, dest ); +} + +/* ----------------------- SRP for the client side -------------------------- */ + +static struct SRPUser * +srp_user_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_password, int len_password, + const char *n_hex, const char *g_hex) +{ + struct SRPUser *usr = calloc(1, sizeof(struct SRPUser)); + int ulen = strlen(username) + 1; + + if (!usr) + goto err_exit; + + usr->alg = alg; + usr->ng = new_ng( ng_type, n_hex, g_hex ); + + bnum_new(usr->a); + bnum_new(usr->A); + bnum_new(usr->S); + + if (!usr->ng || !usr->a || !usr->A || !usr->S) + goto err_exit; + + usr->username = malloc(ulen); + usr->password = malloc(len_password); + usr->password_len = len_password; + + if (!usr->username || !usr->password) + goto err_exit; + + memcpy(usr->username, username, ulen); + memcpy(usr->password, bytes_password, len_password); + + usr->authenticated = 0; + usr->bytes_A = 0; + + return usr; + + err_exit: + if (!usr) + return NULL; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free(usr->username); + if (usr->password) + { + memset(usr->password, 0, usr->password_len); + free(usr->password); + } + free(usr); + + return NULL; +} + +static void +srp_user_free(struct SRPUser *usr) +{ + if(!usr) + return; + + bnum_free(usr->a); + bnum_free(usr->A); + bnum_free(usr->S); + + free_ng(usr->ng); + + memset(usr->password, 0, usr->password_len); + + free(usr->username); + free(usr->password); + free((char *)usr->bytes_A); + + memset(usr, 0, sizeof(*usr)); + free(usr); +} + +static int +srp_user_is_authenticated(struct SRPUser *usr) +{ + return usr->authenticated; +} + +static const unsigned char * +srp_user_get_session_key(struct SRPUser *usr, int *key_length) +{ + if (key_length) + *key_length = usr->session_key_len; + return usr->session_key; +} + +/* Output: username, bytes_A, len_A */ +static void +srp_user_start_authentication(struct SRPUser *usr, const char **username, + const unsigned char **bytes_A, int *len_A) +{ + bnum_random(usr->a, 256); +// BN_hex2bn(&(usr->a), "D929DFB605687233C9E9030C2280156D03BDB9FDCF3CCE3BC27D9CCFCB5FF6A1"); + + bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); + + *len_A = bnum_num_bytes(usr->A); + *bytes_A = malloc(*len_A); + + if (!*bytes_A) + { + *len_A = 0; + *bytes_A = 0; + *username = 0; + return; + } + + bnum_bn2bin(usr->A, (unsigned char *) *bytes_A, *len_A); + + usr->bytes_A = *bytes_A; + *username = usr->username; +} + +/* Output: bytes_M. Buffer length is SHA512_DIGEST_LENGTH */ +static void +srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_B, int len_B, + const unsigned char **bytes_M, int *len_M ) +{ + bnum s, B, k, v; + bnum tmp1, tmp2, tmp3; + bnum u, x; + + *len_M = 0; + *bytes_M = 0; + + bnum_bin2bn(s, bytes_s, len_s); + bnum_bin2bn(B, bytes_B, len_B); + k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g); + + bnum_new(v); + bnum_new(tmp1); + bnum_new(tmp2); + bnum_new(tmp3); + + if (!s || !B || !k || !v || !tmp1 || !tmp2 || !tmp3) + goto cleanup1; + + u = H_nn_pad(usr->alg, usr->A, B); + x = calculate_x(usr->alg, s, usr->username, usr->password, usr->password_len); + if (!u || !x) + goto cleanup2; + + // SRP-6a safety check + if (!bnum_is_zero(B) && !bnum_is_zero(u)) + { + bnum_modexp(v, usr->ng->g, x, usr->ng->N); + + // S = (B - k*(g^x)) ^ (a + ux) + bnum_mul(tmp1, u, x); + bnum_add(tmp2, usr->a, tmp1); // tmp2 = (a + ux) + bnum_modexp(tmp1, usr->ng->g, x, usr->ng->N); + bnum_mul(tmp3, k, tmp1); // tmp3 = k*(g^x) + bnum_sub(tmp1, B, tmp3); // tmp1 = (B - K*(g^x)) + bnum_modexp(usr->S, tmp1, tmp2, usr->ng->N); + + hash_num(usr->alg, usr->S, usr->session_key); + usr->session_key_len = hash_length(usr->alg); + + calculate_M(usr->alg, usr->ng, usr->M, usr->username, s, usr->A, B, usr->session_key, usr->session_key_len); + calculate_H_AMK(usr->alg, usr->H_AMK, usr->A, usr->M, usr->session_key, usr->session_key_len); + + *bytes_M = usr->M; + if (len_M) + *len_M = hash_length(usr->alg); + } + else + { + *bytes_M = NULL; + if (len_M) + *len_M = 0; + } + + cleanup2: + bnum_free(x); + bnum_free(u); + cleanup1: + bnum_free(tmp3); + bnum_free(tmp2); + bnum_free(tmp1); + bnum_free(v); + bnum_free(k); + bnum_free(B); + bnum_free(s); +} + +static void +srp_user_verify_session(struct SRPUser *usr, const unsigned char *bytes_HAMK) +{ + if (memcmp(usr->H_AMK, bytes_HAMK, hash_length(usr->alg)) == 0) + usr->authenticated = 1; +} + + +/* ----------------------- SRP for the server side -------------------------- */ + +static int +srp_create_salted_verification_key(enum hash_alg alg, + SRP_NGType ng_type, const char *username, + const unsigned char *password, int len_password, + unsigned char **bytes_s, int *len_s, + unsigned char **bytes_v, int *len_v, + const char *n_hex, const char *g_hex ) +{ + bnum s, v, x; + NGConstant *ng; + + bnum_new(s); + bnum_new(v); + x = NULL; + + ng = new_ng(ng_type, n_hex, g_hex); + + *bytes_s = NULL; + *bytes_v = NULL; + + if (!s || !v || !ng) + goto error; + + bnum_random(s, 128); // MODIFIED from csrp's BN_rand(s, 32, -1, 0) + + x = calculate_x(alg, s, username, password, len_password); + if (!x) + goto error; + + bnum_modexp(v, ng->g, x, ng->N); + + *len_s = bnum_num_bytes(s); + *len_v = bnum_num_bytes(v); + + *bytes_s = malloc(*len_s); + *bytes_v = malloc(*len_v); + if (!(*bytes_s) || !(*bytes_v)) + goto error; + + bnum_bn2bin(s, (unsigned char *) *bytes_s, *len_s); + bnum_bn2bin(v, (unsigned char *) *bytes_v, *len_v); + + free_ng(ng); + bnum_free(s); + bnum_free(v); + bnum_free(x); + return 0; + + error: + free(*bytes_s); + free(*bytes_v); + free_ng(ng); + bnum_free(s); + bnum_free(v); + bnum_free(x); + return -1; +} + +static int +srp_verifier_start_authentication(enum hash_alg alg, SRP_NGType ng_type, + const unsigned char *bytes_v, int len_v, + unsigned char **bytes_b, int *len_b, + unsigned char **bytes_B, int *len_B, + const char *n_hex, const char *g_hex) +{ + bnum v, k, b, B, tmp1, tmp2; + NGConstant *ng; + + v = NULL; + k = NULL; + bnum_new(b); + bnum_new(B); + bnum_new(tmp1); + bnum_new(tmp2); + + *len_b = 0; + *bytes_b = NULL; + *len_B = 0; + *bytes_B = NULL; + + ng = new_ng(ng_type, n_hex, g_hex); + + if (!b || !B || !tmp1 || !tmp2 || !ng) + goto error; + + bnum_bin2bn(v, bytes_v, len_v); + + bnum_random(b, 256); // MODIFIED from BN_rand(b, 256, -1, 0) + + k = H_nn_pad(alg, ng->N, ng->g); // MODIFIED from H_nn(alg, ng->N, ng->g) + if (!k) + goto error; + + // B = kv + g^b + bnum_mul(tmp1, k, v); + bnum_modexp(tmp2, ng->g, b, ng->N); + bnum_modadd(B, tmp1, tmp2, ng->N); + + *len_B = bnum_num_bytes(B); + *len_b = bnum_num_bytes(b); + + *bytes_B = malloc(*len_B); + *bytes_b = malloc(*len_b); + if (!(*bytes_B) || !(*bytes_b)) + goto error; + + bnum_bn2bin(B, (unsigned char *) *bytes_B, *len_B); + bnum_bn2bin(b, (unsigned char *) *bytes_b, *len_b); + + bnum_free(b); + bnum_free(B); + bnum_free(v); + bnum_free(k); + bnum_free(tmp1); + bnum_free(tmp2); + free_ng(ng); + return 0; + + error: + free(*bytes_B); + free(*bytes_b); + bnum_free(b); + bnum_free(B); + bnum_free(v); + bnum_free(k); + bnum_free(tmp1); + bnum_free(tmp2); + free_ng(ng); + return -1; +} + +static void +srp_verifier_free(struct SRPVerifier *ver) +{ + if (!ver) + return; + + free_ng(ver->ng); + + free(ver->username); + + memset(ver, 0, sizeof(struct SRPVerifier)); + free(ver); +} + +static struct SRPVerifier * +srp_verifier_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_v, int len_v, + const unsigned char *bytes_A, int len_A, + const unsigned char *bytes_b, int len_b, + const unsigned char *bytes_B, int len_B, + const char *n_hex, const char *g_hex ) +{ + struct SRPVerifier *ver = NULL; + bnum s, v, A, b, B, S, tmp1, tmp2, u, k; + NGConstant *ng; + size_t ulen; + + bnum_bin2bn(s, bytes_s, len_s); + bnum_bin2bn(v, bytes_v, len_v); + bnum_bin2bn(A, bytes_A, len_A); + bnum_bin2bn(b, bytes_b, len_b); + bnum_bin2bn(B, bytes_B, len_B); + bnum_new(S); + bnum_new(tmp1); + bnum_new(tmp2); + u = NULL; + k = NULL; + + ng = new_ng(ng_type, n_hex, g_hex); + + if (!s || !v || !A || !B || !S || !b || !tmp1 || !tmp2 || !ng) + goto error; + + ver = calloc(1, sizeof(struct SRPVerifier)); + if (!ver) + goto error; + + ulen = strlen(username) + 1; + + ver->alg = alg; + ver->ng = ng; + + ver->username = malloc(ulen); + if (!ver->username) + goto error; + + memcpy(ver->username, username, ulen); + + ver->authenticated = 0; + + // SRP-6a safety check + bnum_mod(tmp1, A, ng->N); + if (bnum_is_zero(tmp1)) + goto error; + + k = H_nn_pad(alg, ng->N, ng->g); // MODIFIED from H_nn(alg, ng->N, ng->g) + u = H_nn_pad(alg, A, B); // MODIFIED from H_nn(alg, A, B) + + // S = (A *(v^u)) ^ b + bnum_modexp(tmp1, v, u, ng->N); + bnum_mul(tmp2, A, tmp1); + bnum_modexp(S, tmp2, b, ng->N); + + hash_num(alg, S, ver->session_key); + ver->session_key_len = hash_length(ver->alg); + + calculate_M(alg, ng, ver->M, username, s, A, B, ver->session_key, ver->session_key_len); + calculate_H_AMK(alg, ver->H_AMK, A, ver->M, ver->session_key, ver->session_key_len); + + ver->bytes_B = bytes_B; + + bnum_free(s); + bnum_free(v); + bnum_free(A); + bnum_free(u); + bnum_free(k); + bnum_free(B); + bnum_free(S); + bnum_free(b); + bnum_free(tmp1); + bnum_free(tmp2); + return ver; + + error: + srp_verifier_free(ver); + bnum_free(s); + bnum_free(v); + bnum_free(A); + bnum_free(u); + bnum_free(k); + bnum_free(B); + bnum_free(S); + bnum_free(b); + bnum_free(tmp1); + bnum_free(tmp2); + return NULL; +} + +// user_M must be exactly SHA512_DIGEST_LENGTH bytes in size +static void +srp_verifier_verify_session(struct SRPVerifier *ver, const unsigned char *user_M, const unsigned char **bytes_HAMK) +{ + if (memcmp(ver->M, user_M, hash_length(ver->alg)) == 0) + { + ver->authenticated = 1; + *bytes_HAMK = ver->H_AMK; + } + else + *bytes_HAMK = NULL; +} + +static const unsigned char * +srp_verifier_get_session_key(struct SRPVerifier *ver, int *key_length) +{ + if (key_length) + *key_length = hash_length(ver->alg); + + return ver->session_key; +} + +/* -------------------------------- HELPERS --------------------------------- */ + +static void +hexread(uint8_t *out, size_t out_len, const char *in) +{ + char hex[] = { 0, 0, 0 }; + int i; + + for (i = 0; i < out_len; i++, in+=2) + { + hex[0] = in[0]; + hex[1] = in[1]; + out[i] = strtol(hex, NULL, 16); + } +} + +static pair_tlv_values_t * +message_process(const uint8_t *data, size_t data_len, const char **errmsg) +{ + pair_tlv_values_t *response; + pair_tlv_t *error; + int ret; + + response = pair_tlv_new(); + if (!response) + { + *errmsg = "Out of memory\n"; + return NULL; + } + + ret = pair_tlv_parse(data, data_len, response); + if (ret < 0) + { + *errmsg = "Could not parse TLV\n"; + goto error; + } + +#ifdef DEBUG_PAIR + pair_tlv_debug(response); +#endif + + error = pair_tlv_get_value(response, TLVType_Error); + if (error) + { + if (error->value[0] == TLVError_Authentication) + *errmsg = "Device returned an authentication failure"; + else if (error->value[0] == TLVError_Backoff) + *errmsg = "Device told us to back off pairing attempts\n"; + else if (error->value[0] == TLVError_MaxPeers) + *errmsg = "Max peers trying to connect to device\n"; + else if (error->value[0] == TLVError_MaxTries) + *errmsg = "Max pairing attemps reached\n"; + else if (error->value[0] == TLVError_Unavailable) + *errmsg = "Device is unuavailble at this time\n"; + else + *errmsg = "Device is busy/returned unknown error\n"; + + goto error; + } + + return response; + + error: + pair_tlv_free(response); + return NULL; +} + +/* Executes SHA512 RFC 5869 extract + expand, writing a derived key to okm + + hkdfExtract(SHA512, salt, salt_len, ikm, ikm_len, prk); + hkdfExpand(SHA512, prk, SHA512_LEN, info, info_len, okm, okm_len); +*/ +static int +hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm_len, enum pair_keys pair_key) +{ +#ifdef CONFIG_OPENSSL +#include + EVP_PKEY_CTX *pctx; + + if (okm_len > SHA512_DIGEST_LENGTH) + return -1; + if (! (pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL))) + return -1; + if (EVP_PKEY_derive_init(pctx) <= 0) + goto error; + if (EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha512()) <= 0) + goto error; + if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, (const unsigned char *)pair_keys_map[pair_key].salt, strlen(pair_keys_map[pair_key].salt)) <= 0) + goto error; + if (EVP_PKEY_CTX_set1_hkdf_key(pctx, ikm, ikm_len) <= 0) + goto error; + if (EVP_PKEY_CTX_add1_hkdf_info(pctx, (const unsigned char *)pair_keys_map[pair_key].info, strlen(pair_keys_map[pair_key].info)) <= 0) + goto error; + if (EVP_PKEY_derive(pctx, okm, &okm_len) <= 0) + goto error; + + EVP_PKEY_CTX_free(pctx); + return 0; + + error: + EVP_PKEY_CTX_free(pctx); + return -1; +#elif CONFIG_GCRYPT + uint8_t prk[SHA512_DIGEST_LENGTH]; + gcry_md_hd_t hmac_handle; + + if (okm_len > SHA512_DIGEST_LENGTH) + return -1; // Below calculation not valid if output is larger than hash size + if (gcry_md_open(&hmac_handle, GCRY_MD_SHA512, GCRY_MD_FLAG_HMAC) != GPG_ERR_NO_ERROR) + return -1; + if (gcry_md_setkey(hmac_handle, (const unsigned char *)pair_keys_map[pair_key].salt, strlen(pair_keys_map[pair_key].salt)) != GPG_ERR_NO_ERROR) + goto error; + gcry_md_write(hmac_handle, ikm, ikm_len); + memcpy(prk, gcry_md_read(hmac_handle, 0), sizeof(prk)); + + gcry_md_reset(hmac_handle); + + if (gcry_md_setkey(hmac_handle, prk, sizeof(prk)) != GPG_ERR_NO_ERROR) + goto error; + gcry_md_write(hmac_handle, (const unsigned char *)pair_keys_map[pair_key].info, strlen(pair_keys_map[pair_key].info)); + gcry_md_putc(hmac_handle, 1); + + memcpy(okm, gcry_md_read(hmac_handle, 0), okm_len); + + gcry_md_close(hmac_handle); + return 0; + + error: + gcry_md_close(hmac_handle); + return -1; +#else + return -1; +#endif +} + +static int +encrypt_chacha(uint8_t *cipher, const uint8_t *plain, size_t plain_len, const uint8_t *key, size_t key_len, const void *ad, size_t ad_len, uint8_t *tag, size_t tag_len, const uint8_t nonce[NONCE_LENGTH]) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + if (! (ctx = EVP_CIPHER_CTX_new())) + return -1; + + if (EVP_EncryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1) + goto error; + + if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary + goto error; + + if (ad_len > 0 && EVP_EncryptUpdate(ctx, NULL, &len, ad, ad_len) != 1) + goto error; + + if (EVP_EncryptUpdate(ctx, cipher, &len, plain, plain_len) != 1) + goto error; + + assert(len == plain_len); + + if (EVP_EncryptFinal_ex(ctx, NULL, &len) != 1) + goto error; + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, tag_len, tag) != 1) + goto error; + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + + if (gcry_cipher_open(&hd, GCRY_CIPHER_CHACHA20, GCRY_CIPHER_MODE_POLY1305, 0) != GPG_ERR_NO_ERROR) + return -1; + + if (gcry_cipher_setkey(hd, key, key_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_setiv(hd, nonce, NONCE_LENGTH) != GPG_ERR_NO_ERROR) + goto error; + + if (ad_len > 0 && gcry_cipher_authenticate(hd, ad, ad_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_encrypt(hd, cipher, plain_len, plain, plain_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_gettag(hd, tag, tag_len) != GPG_ERR_NO_ERROR) + goto error; + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#else + return -1; +#endif +} + +static int +decrypt_chacha(uint8_t *plain, const uint8_t *cipher, size_t cipher_len, const uint8_t *key, size_t key_len, const void *ad, size_t ad_len, uint8_t *tag, size_t tag_len, const uint8_t nonce[NONCE_LENGTH]) +{ +#ifdef CONFIG_OPENSSL + EVP_CIPHER_CTX *ctx; + int len; + + if (! (ctx = EVP_CIPHER_CTX_new())) + return -1; + + if (EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1) + goto error; + + if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary + goto error; + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag) != 1) + goto error; + + if (ad_len > 0 && EVP_DecryptUpdate(ctx, NULL, &len, ad, ad_len) != 1) + goto error; + + if (EVP_DecryptUpdate(ctx, plain, &len, cipher, cipher_len) != 1) + goto error; + + if (EVP_DecryptFinal_ex(ctx, NULL, &len) != 1) + goto error; + + EVP_CIPHER_CTX_free(ctx); + return 0; + + error: + EVP_CIPHER_CTX_free(ctx); + return -1; +#elif CONFIG_GCRYPT + gcry_cipher_hd_t hd; + + if (gcry_cipher_open(&hd, GCRY_CIPHER_CHACHA20, GCRY_CIPHER_MODE_POLY1305, 0) != GPG_ERR_NO_ERROR) + return -1; + + if (gcry_cipher_setkey(hd, key, key_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_setiv(hd, nonce, NONCE_LENGTH) != GPG_ERR_NO_ERROR) + goto error; + + if (ad_len > 0 && gcry_cipher_authenticate(hd, ad, ad_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_decrypt(hd, plain, cipher_len, cipher, cipher_len) != GPG_ERR_NO_ERROR) + goto error; + + if (gcry_cipher_checktag(hd, tag, tag_len) != GPG_ERR_NO_ERROR) + goto error; + + gcry_cipher_close(hd); + return 0; + + error: + gcry_cipher_close(hd); + return -1; +#else + return -1; +#endif +} + +static int +create_info(uint8_t *info, size_t *info_len, uint8_t *a, size_t a_len, uint8_t *b, size_t b_len, uint8_t *c, size_t c_len) +{ + if (a_len + b_len + c_len > *info_len) + return -1; + + *info_len = a_len + b_len + c_len; + memcpy(info, a, a_len); + memcpy(info + a_len, b, b_len); + memcpy(info + a_len + b_len, c, c_len); + + return 0; +} + +static int +create_and_sign_device_info(uint8_t *msg, size_t *msg_len, const char *device_id, uint8_t *device_x, size_t device_x_len, uint8_t *pk, size_t pk_len, uint8_t *sk) +{ + pair_tlv_values_t *tlv; + uint8_t device_info[256]; + size_t device_info_len; + size_t device_id_len; + uint8_t signature[crypto_sign_BYTES]; + int ret; + + device_id_len = strlen(device_id); + device_info_len = sizeof(device_info); + + ret = create_info(device_info, &device_info_len, device_x, device_x_len, (uint8_t *)device_id, device_id_len, pk, pk_len); + if (ret < 0) + return -1; + + crypto_sign_detached(signature, NULL, device_info, device_info_len, sk); + + tlv = pair_tlv_new(); + pair_tlv_add_value(tlv, TLVType_Identifier, (unsigned char *)device_id, device_id_len); + pair_tlv_add_value(tlv, TLVType_Signature, signature, sizeof(signature)); + + ret = pair_tlv_format(tlv, msg, msg_len); + + pair_tlv_free(tlv); + return ret; +} + +static int +create_and_sign_accessory_info(uint8_t *msg, size_t *msg_len, uint8_t *server_pk, size_t server_pk_len, const char *accessory_id, uint8_t *client_pk, size_t client_pk_len, uint8_t *sk) +{ + pair_tlv_values_t *tlv; + uint8_t accessory_info[256]; + size_t accessory_info_len; + size_t accessory_id_len; + uint8_t signature[crypto_sign_BYTES]; + int ret; + + accessory_id_len = strlen(accessory_id); + accessory_info_len = sizeof(accessory_info); + + ret = create_info(accessory_info, &accessory_info_len, server_pk, server_pk_len, (uint8_t *)accessory_id, accessory_id_len, client_pk, client_pk_len); + if (ret < 0) + return -1; + + crypto_sign_detached(signature, NULL, accessory_info, accessory_info_len, sk); + + tlv = pair_tlv_new(); + pair_tlv_add_value(tlv, TLVType_Identifier, (unsigned char *)accessory_id, accessory_id_len); + pair_tlv_add_value(tlv, TLVType_Signature, signature, sizeof(signature)); + + ret = pair_tlv_format(tlv, msg, msg_len); + + pair_tlv_free(tlv); + return ret; +} + +static int +verify_info(uint8_t *signature, uint8_t *pk, uint8_t *a, size_t a_len, uint8_t *b, size_t b_len, uint8_t *c, size_t c_len) +{ + uint8_t info[256]; + size_t info_len; + int ret; + + info_len = sizeof(info); + ret = create_info(info, &info_len, a, a_len, b, b_len, c, c_len); + if (ret < 0) + return -1; + + return crypto_sign_verify_detached(signature, info, info_len, pk); +} + + +/* ------------------------- CLIENT IMPLEMENTATION -------------------------- */ + +static int +client_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + if (sodium_init() == -1) + return -1; + + if (handle->type == &pair_client_homekit_normal) + { + if (!pin || strlen(pin) < 4) + return -1; + } + else if (handle->type == &pair_client_homekit_transient && !pin) + { + pin = "3939"; + } + + if (device_id && strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + memcpy(sctx->pin, pin, sizeof(sctx->pin)); + + sctx->add_cb = add_cb; + sctx->add_cb_arg = cb_arg; + + if (device_id) + memcpy(sctx->device_id, device_id, strlen(device_id)); + + crypto_sign_keypair(sctx->public_key, sctx->private_key); + + return 0; +} + +static void +client_setup_free(struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + + srp_user_free(sctx->user); + + free(sctx->pkB); + free(sctx->M2); + free(sctx->salt); + free(sctx->epk); + free(sctx->authtag); +} + +static uint8_t * +client_setup_request1(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + uint8_t method; + uint8_t flags; + int endian_test = 1; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + // Test here instead of setup_new() so we can give an error message + if(*(char *)&endian_test != 1) + { + handle->errmsg = "Setup request 1: No support for big endian architechture"; + goto error; + } + + sctx->user = srp_user_new(HASH_SHA512, SRP_NG_3072, USERNAME, (unsigned char *)sctx->pin, sizeof(sctx->pin), 0, 0); + if (!sctx->user) + { + handle->errmsg = "Setup request 1: Create SRP user failed"; + goto error; + } + + method = PairingMethodPairSetup; + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG01].state, sizeof(pair_keys_map[PAIR_SETUP_MSG01].state)); + pair_tlv_add_value(request, TLVType_Method, &method, sizeof(method)); + + if (handle->type == &pair_client_homekit_transient) + { + flags = PairingFlagsTransient; + pair_tlv_add_value(request, TLVType_Flags, &flags, sizeof(flags)); + } + + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Setup request 1: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + pair_tlv_free(request); + return data; + + error: + pair_tlv_free(request); + free(data); + return NULL; +} + +static uint8_t * +client_setup_request2(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + const char *auth_username = NULL; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + // Calculate A + srp_user_start_authentication(sctx->user, &auth_username, &sctx->pkA, &sctx->pkA_len); + + // Calculate M1 (client proof) + srp_user_process_challenge(sctx->user, (const unsigned char *)sctx->salt, sctx->salt_len, (const unsigned char *)sctx->pkB, sctx->pkB_len, &sctx->M1, &sctx->M1_len); + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG03].state, sizeof(pair_keys_map[PAIR_SETUP_MSG03].state)); + pair_tlv_add_value(request, TLVType_PublicKey, sctx->pkA, sctx->pkA_len); + pair_tlv_add_value(request, TLVType_Proof, sctx->M1, sctx->M1_len); + + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Setup request 2: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + pair_tlv_free(request); + return data; + + error: + pair_tlv_free(request); + free(data); + return NULL; +} + +static uint8_t * +client_setup_request3(size_t *len, struct pair_setup_context *handle) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + const unsigned char *session_key; + int session_key_len; + uint8_t device_x[32]; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + pair_tlv_values_t *append; + size_t append_len; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup request 3: No valid session key"; + goto error; + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_CONTROLLER_SIGN); + if (ret < 0) + { + handle->errmsg = "Setup request 3: hkdf error getting device_x"; + goto error; + } + + ret = create_and_sign_device_info(data, &data_len, sctx->device_id, device_x, sizeof(device_x), sctx->public_key, sizeof(sctx->public_key), sctx->private_key); + if (ret < 0) + { + handle->errmsg = "Setup request 3: error creating signed device info"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, PAIR_SETUP_MSG05); + if (ret < 0) + { + handle->errmsg = "Setup request 3: hkdf error getting derived_key"; + goto error; + } + + // Append TLV-encoded public key to *data, which already has identifier and signature + append = pair_tlv_new(); + append_len = REQUEST_BUFSIZE - data_len; + pair_tlv_add_value(append, TLVType_PublicKey, sctx->public_key, sizeof(sctx->public_key)); + ret = pair_tlv_format(append, data + data_len, &append_len); + pair_tlv_free(append); + if (ret < 0) + { + handle->errmsg = "Setup request 3: error appending public key to TLV"; + goto error; + } + data_len += append_len; + + memcpy(nonce + 4, pair_keys_map[PAIR_SETUP_MSG05].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Could not encrypt"; + goto error; + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_SETUP_MSG05].state, sizeof(pair_keys_map[PAIR_SETUP_MSG05].state)); + pair_tlv_add_value(request, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Setup request 3: error appending public key to TLV"; + goto error; + } + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(request); + return data; + + error: + free(encrypted_data); + pair_tlv_free(request); + free(data); + return NULL; +} + +static int +client_setup_response1(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *response; + pair_tlv_t *pk; + pair_tlv_t *salt; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + pk = pair_tlv_get_value(response, TLVType_PublicKey); + salt = pair_tlv_get_value(response, TLVType_Salt); + if (!pk || !salt) + { + handle->errmsg = "Setup response 1: Missing or invalid pk/salt"; + goto error; + } + + sctx->pkB_len = pk->size; // 384 + sctx->pkB = malloc(sctx->pkB_len); + memcpy(sctx->pkB, pk->value, sctx->pkB_len); + + sctx->salt_len = salt->size; // 16 + sctx->salt = malloc(sctx->salt_len); + memcpy(sctx->salt, salt->value, sctx->salt_len); + + pair_tlv_free(response); + return 0; + + error: + pair_tlv_free(response); + return -1; +} + +static int +client_setup_response2(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *response; + pair_tlv_t *proof; + const uint8_t *session_key; + int session_key_len; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + proof = pair_tlv_get_value(response, TLVType_Proof); + if (!proof) + { + handle->errmsg = "Setup response 2: Missing proof"; + goto error; + } + + sctx->M2_len = proof->size; // 64 + sctx->M2 = malloc(sctx->M2_len); + memcpy(sctx->M2, proof->value, sctx->M2_len); + + // Check M2 + srp_user_verify_session(sctx->user, (const unsigned char *)sctx->M2); + if (!srp_user_is_authenticated(sctx->user)) + { + handle->errmsg = "Setup response 2: Server authentication failed"; + goto error; + } + + if (handle->type == &pair_client_homekit_transient) + { + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup response 2: Could not compute session key"; + goto error; + } + + assert(sizeof(handle->result.shared_secret) >= session_key_len); + + memcpy(handle->result.shared_secret, session_key, session_key_len); + handle->result.shared_secret_len = session_key_len; + + handle->status = PAIR_STATUS_COMPLETED; + } + + pair_tlv_free(response); + return 0; + + error: + pair_tlv_free(response); + return -1; +} + +static int +client_setup_response3(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_setup_context *sctx = &handle->sctx.client; + pair_tlv_values_t *response; + pair_tlv_t *encrypted_data; + pair_tlv_t *device_id; + pair_tlv_t *pk; + pair_tlv_t *signature; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + const uint8_t *session_key; + int session_key_len; + uint8_t device_x[32]; + int ret; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + encrypted_data = pair_tlv_get_value(response, TLVType_EncryptedData); + if (!encrypted_data) + { + handle->errmsg = "Setup response 3: Missing encrypted_data"; + goto error; + } + + session_key = srp_user_get_session_key(sctx->user, &session_key_len); + if (!session_key) + { + handle->errmsg = "Setup response 3: No valid session key"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, PAIR_SETUP_MSG06); + if (ret < 0) + { + handle->errmsg = "Setup response 3: hkdf error getting derived_key"; + goto error; + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + handle->errmsg = "Setup response 3: Invalid encrypted data"; + goto error; + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[PAIR_SETUP_MSG06].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Setup response 3: Decryption error"; + goto error; + } + + pair_tlv_free(response); + response = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!response) + { + goto error; + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_ACCESSORY_SIGN); + if (ret < 0) + { + handle->errmsg = "Setup response 3: hkdf error getting device_x"; + goto error; + } + + device_id = pair_tlv_get_value(response, TLVType_Identifier); + pk = pair_tlv_get_value(response, TLVType_PublicKey); + signature = pair_tlv_get_value(response, TLVType_Signature); + if (!device_id || device_id->size >= sizeof(handle->result.device_id) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES || !signature || signature->size != crypto_sign_BYTES) + { + handle->errmsg = "Setup response 3: Missing/invalid device ID, public key or signature"; + goto error; + } + + ret = verify_info(signature->value, pk->value, device_x, sizeof(device_x), device_id->value, device_id->size, pk->value, pk->size); + if (ret < 0) + { + handle->errmsg = "Setup request 3: Invalid signature"; + goto error; + } + + assert(sizeof(handle->result.client_private_key) == sizeof(sctx->private_key)); + assert(sizeof(handle->result.client_public_key) == sizeof(sctx->public_key)); + assert(sizeof(handle->result.server_public_key) == pk->size); + + memcpy(handle->result.client_private_key, sctx->private_key, sizeof(sctx->private_key)); + memcpy(handle->result.client_public_key, sctx->public_key, sizeof(sctx->public_key)); + memcpy(handle->result.server_public_key, pk->value, pk->size); + memcpy(handle->result.device_id, device_id->value, device_id->size); + + if (sctx->add_cb) + sctx->add_cb(handle->result.server_public_key, handle->result.device_id, sctx->add_cb_arg); + + handle->status = PAIR_STATUS_COMPLETED; + + free(decrypted_data); + pair_tlv_free(response); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(response); + return -1; +} + +static int +client_setup_result(struct pair_setup_context *handle) +{ + char *ptr; + int i; + + assert(sizeof(handle->result_str) >= 2 * sizeof(handle->result.client_private_key) + 2 * sizeof(handle->result.server_public_key) + 1); + + // It is enough to export the private key, since the public key can be + // extracted from that with crypto_sign_ed25519_sk_to_pk (it is the last 32 + // bytes) + ptr = handle->result_str; + for (i = 0; i < sizeof(handle->result.client_private_key); i++) + ptr += sprintf(ptr, "%02x", handle->result.client_private_key[i]); // 2 x 64 bytes + for (i = 0; i < sizeof(handle->result.server_public_key); i++) + ptr += sprintf(ptr, "%02x", handle->result.server_public_key[i]); // 2 x 32 bytes + *ptr = '\0'; + + return 0; +} + +static int +client_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + size_t hexkey_len; + + if (sodium_init() == -1) + return -1; + + if (!device_id || strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + if (!client_setup_keys) + return -1; + + hexkey_len = strlen(client_setup_keys); + if (hexkey_len == 2 * sizeof(vctx->client_private_key) + 2 * sizeof(vctx->server_public_key)) + { + hexread(vctx->client_private_key, sizeof(vctx->client_private_key), client_setup_keys); + hexread(vctx->server_public_key, sizeof(vctx->server_public_key), client_setup_keys + 2 * sizeof(vctx->client_private_key)); + vctx->verify_server_signature = true; + } + else if (hexkey_len == 2 * sizeof(vctx->client_private_key)) // No server public key known, so signature validation will be skipped + { + hexread(vctx->client_private_key, sizeof(vctx->client_private_key), client_setup_keys); + } + else + return -1; + + crypto_sign_ed25519_sk_to_pk(vctx->client_public_key, vctx->client_private_key); + + snprintf(vctx->device_id, sizeof(vctx->device_id), "%s", device_id); + + return 0; +} + +static uint8_t * +client_verify_request1(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; +// const uint8_t basepoint[crypto_scalarmult_BYTES] = {9}; // 32 bytes + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + crypto_box_keypair(vctx->client_eph_public_key, vctx->client_eph_private_key); + +/* + // TODO keep around in case box_keypair doesn't work + ret = crypto_scalarmult(vctx->client_eph_public_key, vctx->client_eph_private_key, basepoint); + if (ret < 0) + { + handle->errmsg = "Verify request 1: Curve 25519 returned an error"; + goto error; + } +*/ + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_VERIFY_MSG01].state, sizeof(pair_keys_map[PAIR_VERIFY_MSG01].state)); + pair_tlv_add_value(request, TLVType_PublicKey, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Verify request 1: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + pair_tlv_free(request); + return data; + + error: + pair_tlv_free(request); + free(data); + return NULL; +} + +static uint8_t * +client_verify_request2(size_t *len, struct pair_verify_context *handle) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + pair_tlv_values_t *request; + uint8_t *data; + size_t data_len; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + request = pair_tlv_new(); + + ret = create_and_sign_device_info(data, &data_len, vctx->device_id, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key), + vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key), vctx->client_private_key); + if (ret < 0) + { + handle->errmsg = "Verify request 2: error creating signed device info"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), PAIR_VERIFY_MSG03); + if (ret < 0) + { + handle->errmsg = "Verify request 2: hkdf error getting derived_key"; + goto error; + } + + memcpy(nonce + 4, pair_keys_map[PAIR_VERIFY_MSG03].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Verify request 2: Could not encrypt"; + goto error; + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(request, TLVType_State, &pair_keys_map[PAIR_VERIFY_MSG03].state, sizeof(pair_keys_map[PAIR_VERIFY_MSG03].state)); + pair_tlv_add_value(request, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(request, data, &data_len); + if (ret < 0) + { + handle->errmsg = "Verify request 2: pair_tlv_format returned an error"; + goto error; + } + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(request); + return data; + + error: + free(encrypted_data); + pair_tlv_free(request); + free(data); + return NULL; +} + +static int +client_verify_response1(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + pair_tlv_values_t *response; + pair_tlv_t *encrypted_data; + pair_tlv_t *public_key; + pair_tlv_t *device_id; + pair_tlv_t *signature; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + int ret; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + encrypted_data = pair_tlv_get_value(response, TLVType_EncryptedData); + if (!encrypted_data) + { + handle->errmsg = "Verify response 1: Missing encrypted_data"; + goto error; + } + + public_key = pair_tlv_get_value(response, TLVType_PublicKey); + if (!public_key || public_key->size != sizeof(vctx->server_eph_public_key)) + { + handle->errmsg = "Verify response 1: Missing or invalid public_key"; + goto error; + } + + memcpy(vctx->server_eph_public_key, public_key->value, sizeof(vctx->server_eph_public_key)); + ret = crypto_scalarmult(vctx->shared_secret, vctx->client_eph_private_key, vctx->server_eph_public_key); + if (ret < 0) + { + handle->errmsg = "Verify response 1: Curve 25519 returned an error"; + goto error; + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), PAIR_VERIFY_MSG02); + if (ret < 0) + { + handle->errmsg = "Verify response 1: hkdf error getting derived_key"; + goto error; + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + handle->errmsg = "Verify response 1: Invalid encrypted data"; + goto error; + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[PAIR_VERIFY_MSG02].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + handle->errmsg = "Verify response 1: Decryption error"; + goto error; + } + + pair_tlv_free(response); + response = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!response) + { + goto error; + } + + device_id = pair_tlv_get_value(response, TLVType_Identifier); + signature = pair_tlv_get_value(response, TLVType_Signature); + if (!device_id || !signature || signature->size != crypto_sign_BYTES) + { + handle->errmsg = "Verify response 1: Missing device ID or signature"; + goto error; + } + + if (vctx->verify_server_signature) + { + ret = verify_info(signature->value, vctx->server_public_key, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key), + device_id->value, device_id->size, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); + if (ret < 0) + { + handle->errmsg = "Verify response 1: Invalid signature"; + goto error; + } + } + + free(decrypted_data); + pair_tlv_free(response); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(response); + return -1; +} + +static int +client_verify_response2(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_client_verify_context *vctx = &handle->vctx.client; + pair_tlv_values_t *response; + + response = message_process(data, data_len, &handle->errmsg); + if (!response) + { + return -1; + } + + memcpy(handle->result.shared_secret, vctx->shared_secret, sizeof(vctx->shared_secret)); + handle->result.shared_secret_len = sizeof(vctx->shared_secret); + + handle->status = PAIR_STATUS_COMPLETED; + + return 0; +} + + +/* ------------------------- SERVER IMPLEMENTATION -------------------------- */ + +// Use (unsecure) keys seeded from device_id. We need the keys to always be the +// same during pair setup and pair verify, since the client saves them after +// pair-setup 3, so that the signature in pair-verify 1 can be checked. +static void +server_keypair(uint8_t *public_key, uint8_t *private_key, const char *device_id) +{ + uint8_t seed[crypto_sign_SEEDBYTES] = { 0 }; + size_t len; + + len = strlen(device_id); + if (len > sizeof(seed)) + len = sizeof(seed); + + memcpy(seed, device_id, len); + + crypto_sign_seed_keypair(public_key, private_key, seed); +} + +static uint8_t * +server_auth_failed_response(size_t *len, enum pair_keys msg_state) +{ + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + uint8_t error = TLVError_Authentication; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_Error, &error, sizeof(error)); + + pair_tlv_format(response, data, &data_len); + pair_tlv_free(response); + + *len = data_len; + + return data; +} + +static int +server_setup_new(struct pair_setup_context *handle, const char *pin, pair_cb add_cb, void *cb_arg, const char *device_id) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + + if (sodium_init() == -1) + return -1; + + if (!pin) + pin = "3939"; + + if (!device_id || strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + memcpy(sctx->pin, pin, sizeof(sctx->pin)); + + sctx->add_cb = add_cb; + sctx->add_cb_arg = cb_arg; + + snprintf(sctx->device_id, sizeof(sctx->device_id), "%s", device_id); + + server_keypair(sctx->public_key, sctx->private_key, sctx->device_id); + + return 0; +} + +static void +server_setup_free(struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + + srp_verifier_free(sctx->verifier); + + free(sctx->pkA); + free(sctx->pkB); + free(sctx->b); + free(sctx->M1); + free(sctx->v); + free(sctx->salt); +} + +static int +server_setup_request1(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; +// enum pair_keys msg_state = PAIR_SETUP_MSG01; + pair_tlv_values_t *request; + pair_tlv_t *method; + pair_tlv_t *type; + int ret; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + method = pair_tlv_get_value(request, TLVType_Method); + if (!method || method->size != 1 || method->value[0] != 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 1: Missing or unexpected pairing method in TLV"); + } + + type = pair_tlv_get_value(request, TLVType_Flags); + sctx->is_transient = (type && type->size == 1 && type->value[0] == PairingFlagsTransient); + + // Note this is modified to return a 16 byte salt + ret = srp_create_salted_verification_key(HASH_SHA512, SRP_NG_3072, USERNAME, (unsigned char *)sctx->pin, sizeof(sctx->pin), + &sctx->salt, &sctx->salt_len, &sctx->v, &sctx->v_len, NULL, NULL); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 1: Could not create verification key"); + } + + ret = srp_verifier_start_authentication(HASH_SHA512, SRP_NG_3072, sctx->v, sctx->v_len, + &sctx->b, &sctx->b_len, &sctx->pkB, &sctx->pkB_len, NULL, NULL); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 1: Could not compute B"); + } + + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static int +server_setup_request2(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; +// enum pair_keys msg_state = PAIR_SETUP_MSG03; + pair_tlv_values_t *request; + pair_tlv_t *pk; + pair_tlv_t *proof; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + pk = pair_tlv_get_value(request, TLVType_PublicKey); + proof = pair_tlv_get_value(request, TLVType_Proof); + if (!pk || !proof) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 2: Missing pkA or proof"); + } + + sctx->pkA_len = pk->size; // 384 + sctx->pkA = malloc(sctx->pkA_len); + memcpy(sctx->pkA, pk->value, sctx->pkA_len); + + sctx->M1_len = proof->size; // 64 + sctx->M1 = malloc(sctx->M1_len); + memcpy(sctx->M1, proof->value, sctx->M1_len); + + sctx->verifier = srp_verifier_new(HASH_SHA512, SRP_NG_3072, USERNAME, sctx->salt, sctx->salt_len, sctx->v, sctx->v_len, + sctx->pkA, sctx->pkA_len, sctx->b, sctx->b_len, sctx->pkB, sctx->pkB_len, NULL, NULL); + if (!sctx->verifier) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + + sctx->M2_len = 64; // 512 bit hash + srp_verifier_verify_session(sctx->verifier, sctx->M1, &sctx->M2); + if (!sctx->M2) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; // Not an error, server should give proper TLV-formatet reply + } + + out: + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static int +server_setup_request3(struct pair_setup_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG05; + pair_tlv_values_t *request; + pair_tlv_t *encrypted_data; + pair_tlv_t *device_id; + pair_tlv_t *pk; + pair_tlv_t *signature; + const uint8_t *session_key; + int session_key_len; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + uint8_t device_x[32]; + int ret; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + session_key = srp_verifier_get_session_key(sctx->verifier, &session_key_len); + if (!session_key) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: No valid session key"); + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: hkdf error getting derived_key"); + } + + encrypted_data = pair_tlv_get_value(request, TLVType_EncryptedData); + if (!encrypted_data) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Missing encrypted_data"); + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Invalid encrypted data"); + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Decryption error"); + } + + pair_tlv_free(request); + request = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_CONTROLLER_SIGN); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: hkdf error getting device_x"); + } + + device_id = pair_tlv_get_value(request, TLVType_Identifier); + pk = pair_tlv_get_value(request, TLVType_PublicKey); + signature = pair_tlv_get_value(request, TLVType_Signature); + if (!device_id || device_id->size >= sizeof(handle->result.device_id) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES || !signature || signature->size != crypto_sign_BYTES) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 3: Missing/invalid device ID, public key or signature"); + } + + ret = verify_info(signature->value, pk->value, device_x, sizeof(device_x), device_id->value, device_id->size, pk->value, pk->size); + if (ret < 0) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + + memcpy(handle->result.device_id, device_id->value, device_id->size); + memcpy(handle->result.client_public_key, pk->value, pk->size); + + out: + free(decrypted_data); + pair_tlv_free(request); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(request); + return -1; +} + +static uint8_t * +server_setup_response1(size_t *len, struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG02; + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_Salt, sctx->salt, sctx->salt_len); // 16 + pair_tlv_add_value(response, TLVType_PublicKey, sctx->pkB, sctx->pkB_len); // 384 + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 1: pair_tlv_format returned an error"); + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static uint8_t * +server_setup_response2(size_t *len, struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG04; + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + const uint8_t *session_key; + int session_key_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_Proof, sctx->M2, sctx->M2_len); // 384 + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 2: pair_tlv_format returned an error"); + } + + if (sctx->is_transient) + { + session_key = srp_verifier_get_session_key(sctx->verifier, &session_key_len); + if (!session_key) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup request 2: Could not compute session key"); + } + + assert(sizeof(handle->result.shared_secret) >= session_key_len); + + memcpy(handle->result.shared_secret, session_key, session_key_len); + handle->result.shared_secret_len = session_key_len; + + handle->status = PAIR_STATUS_COMPLETED; + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static uint8_t * +server_setup_response3(size_t *len, struct pair_setup_context *handle) +{ + struct pair_server_setup_context *sctx = &handle->sctx.server; + enum pair_keys msg_state = PAIR_SETUP_MSG06; + const uint8_t *session_key; + int session_key_len; + pair_tlv_values_t *response; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + pair_tlv_values_t *append; + size_t append_len; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + uint8_t *data; + size_t data_len; + uint8_t device_x[32]; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + session_key = srp_verifier_get_session_key(sctx->verifier, &session_key_len); + if (!session_key) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: No valid session key"); + } + + ret = hkdf_extract_expand(device_x, sizeof(device_x), session_key, session_key_len, PAIR_SETUP_ACCESSORY_SIGN); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: hkdf error getting device_x"); + } + + ret = create_and_sign_device_info(data, &data_len, sctx->device_id, device_x, sizeof(device_x), sctx->public_key, sizeof(sctx->public_key), sctx->private_key); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: create device info returned an error"); + } + + // Append TLV-encoded public key to *data, which already has identifier and signature + append = pair_tlv_new(); + append_len = REQUEST_BUFSIZE - data_len; + pair_tlv_add_value(append, TLVType_PublicKey, sctx->public_key, sizeof(sctx->public_key)); + ret = pair_tlv_format(append, data + data_len, &append_len); + pair_tlv_free(append); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: error appending public key to TLV"); + } + data_len += append_len; + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), session_key, session_key_len, msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: hkdf error getting derived_key"); + } + + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: Could not encrypt"); + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Setup response 3: error appending public key to TLV"); + } + + if (sctx->add_cb) + sctx->add_cb(handle->result.client_public_key, handle->result.device_id, sctx->add_cb_arg); + + handle->status = PAIR_STATUS_COMPLETED; + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(response); + return data; + + error: + free(encrypted_data); + free(data); + pair_tlv_free(response); + return NULL; +} + + +static int +server_verify_new(struct pair_verify_context *handle, const char *client_setup_keys, pair_cb cb, void *cb_arg, const char *device_id) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + + if (sodium_init() == -1) + return -1; + + if (client_setup_keys) + return -1; + + if (!device_id || strlen(device_id) >= PAIR_AP_DEVICE_ID_LEN_MAX) + return -1; + + snprintf(vctx->device_id, sizeof(vctx->device_id), "%s", device_id); + + vctx->get_cb = cb; + vctx->get_cb_arg = cb_arg; + vctx->verify_client_signature = cb; + + server_keypair(vctx->server_public_key, vctx->server_private_key, vctx->device_id); + + return 0; +} + +static int +server_verify_request1(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; +// enum pair_keys msg_state = PAIR_VERIFY_MSG01; + pair_tlv_values_t *request; + pair_tlv_t *pk; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + pk = pair_tlv_get_value(request, TLVType_PublicKey); + if (!pk || pk->size != sizeof(vctx->client_eph_public_key)) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 1: Missing or invalid public_key"); + } + + memcpy(vctx->client_eph_public_key, pk->value, sizeof(vctx->client_eph_public_key)); + + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static int +server_verify_request2(struct pair_verify_context *handle, const uint8_t *data, size_t data_len) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + enum pair_keys msg_state = PAIR_VERIFY_MSG03; + pair_tlv_values_t *request; + pair_tlv_t *encrypted_data; + pair_tlv_t *device_id; + pair_tlv_t *signature; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + size_t encrypted_len; + uint8_t *decrypted_data = NULL; + char id_str[PAIR_AP_DEVICE_ID_LEN_MAX] = { 0 }; + uint8_t client_public_key[crypto_sign_PUBLICKEYBYTES]; + int ret; + + request = message_process(data, data_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: hkdf error getting derived_key"); + } + + encrypted_data = pair_tlv_get_value(request, TLVType_EncryptedData); + if (!encrypted_data) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Missing encrypted_data"); + } + + // encrypted_data->value consists of the encrypted payload + the auth tag + if (encrypted_data->size < AUTHTAG_LENGTH) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Invalid encrypted data"); + } + + encrypted_len = encrypted_data->size - AUTHTAG_LENGTH; + memcpy(tag, encrypted_data->value + encrypted_len, AUTHTAG_LENGTH); + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + decrypted_data = malloc(encrypted_len); + + ret = decrypt_chacha(decrypted_data, encrypted_data->value, encrypted_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Decryption error"); + } + + pair_tlv_free(request); + request = message_process(decrypted_data, encrypted_len, &handle->errmsg); + if (!request) + { + RETURN_ERROR(PAIR_STATUS_INVALID, handle->errmsg); + } + + device_id = pair_tlv_get_value(request, TLVType_Identifier); + signature = pair_tlv_get_value(request, TLVType_Signature); + if (!device_id || !signature || signature->size != crypto_sign_BYTES) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Missing identifier or signature"); + } + + if (vctx->verify_client_signature) + { + if (device_id->size >= sizeof(id_str)) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify request 2: Device ID from peer is too long"); + } + + memcpy(id_str, device_id->value, device_id->size); + + ret = vctx->get_cb(client_public_key, id_str, vctx->get_cb_arg); + if (ret < 0) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + + ret = verify_info(signature->value, client_public_key, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key), + device_id->value, device_id->size, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key)); + if (ret < 0) + { + handle->status = PAIR_STATUS_AUTH_FAILED; + goto out; + } + } + + out: + free(decrypted_data); + pair_tlv_free(request); + return 0; + + error: + free(decrypted_data); + pair_tlv_free(request); + return -1; +} + +static uint8_t * +server_verify_response1(size_t *len, struct pair_verify_context *handle) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + enum pair_keys msg_state = PAIR_VERIFY_MSG02; + pair_tlv_values_t *response; + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t derived_key[32]; + uint8_t *encrypted_data = NULL; + size_t encrypted_data_len; + uint8_t *data; + size_t data_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + crypto_box_keypair(vctx->server_eph_public_key, vctx->server_eph_private_key); + + ret = crypto_scalarmult(vctx->shared_secret, vctx->server_eph_private_key, vctx->client_eph_public_key); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: Error generating shared secret"); + } + + ret = create_and_sign_accessory_info(data, &data_len, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key), vctx->device_id, + vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key), vctx->server_private_key); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: Error creating device info"); + } + + ret = hkdf_extract_expand(derived_key, sizeof(derived_key), vctx->shared_secret, sizeof(vctx->shared_secret), msg_state); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: hkdf error getting derived_key"); + } + + memcpy(nonce + 4, pair_keys_map[msg_state].nonce, NONCE_LENGTH - 4); + + encrypted_data_len = data_len + sizeof(tag); // Space for ciphered payload and authtag + encrypted_data = malloc(encrypted_data_len); + + ret = encrypt_chacha(encrypted_data, data, data_len, derived_key, sizeof(derived_key), NULL, 0, tag, sizeof(tag), nonce); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: Could not encrypt"); + } + + memcpy(encrypted_data + data_len, tag, sizeof(tag)); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + pair_tlv_add_value(response, TLVType_PublicKey, vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key)); + pair_tlv_add_value(response, TLVType_EncryptedData, encrypted_data, encrypted_data_len); + + data_len = REQUEST_BUFSIZE; // Re-using *data, so pass original length to pair_tlv_format + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 1: pair_tlv_format returned an error"); + } + + *len = data_len; + + free(encrypted_data); + pair_tlv_free(response); + return data; + + error: + free(encrypted_data); + free(data); + pair_tlv_free(response); + return NULL; +} + +static uint8_t * +server_verify_response2(size_t *len, struct pair_verify_context *handle) +{ + struct pair_server_verify_context *vctx = &handle->vctx.server; + enum pair_keys msg_state = PAIR_VERIFY_MSG04; + pair_tlv_values_t *response; + uint8_t *data; + size_t data_len; + int ret; + + if (handle->status == PAIR_STATUS_AUTH_FAILED) + return server_auth_failed_response(len, msg_state); + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &pair_keys_map[msg_state].state, sizeof(pair_keys_map[msg_state].state)); + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + RETURN_ERROR(PAIR_STATUS_INVALID, "Verify response 2: pair_tlv_format returned an error"); + } + + *len = data_len; + + memcpy(handle->result.shared_secret, vctx->shared_secret, sizeof(vctx->shared_secret)); + handle->result.shared_secret_len = sizeof(vctx->shared_secret); + + handle->status = PAIR_STATUS_COMPLETED; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static int +server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + const char *errmsg; + pair_tlv_values_t *request; + pair_tlv_t *device_id; + pair_tlv_t *pk; + char id_str[PAIR_AP_DEVICE_ID_LEN_MAX] = { 0 }; + + request = message_process(in, in_len, &errmsg); + if (!request) + { + goto error; + } + + device_id = pair_tlv_get_value(request, TLVType_Identifier); + pk = pair_tlv_get_value(request, TLVType_PublicKey); + if (!device_id || device_id->size >= sizeof(id_str) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES) + { + goto error; + } + + memcpy(id_str, device_id->value, device_id->size); + + cb(pk->value, id_str, cb_arg); + + pair_tlv_free(request); + return 0; + + error: + pair_tlv_free(request); + return -1; +} + +static uint8_t * +server_add_remove_response(size_t *len) +{ + pair_tlv_values_t *response; + uint8_t *data; + uint8_t state = 2; + size_t data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &state, sizeof(state)); + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + goto error; + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static int +server_add_remove(uint8_t **out, size_t *out_len, pair_cb cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + int ret; + + ret = server_add_remove_request(cb, cb_arg, in, in_len); + if (ret < 0) + return -1; + + *out = server_add_remove_response(out_len); + if (!*out) + return -1; + + return 0; +} + +static int +server_list_cb(uint8_t public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id, void *cb_arg) +{ + pair_tlv_values_t *response = cb_arg; + + pair_tlv_add_value(response, TLVType_Identifier, (unsigned char *)device_id, strlen(device_id)); + pair_tlv_add_value(response, TLVType_PublicKey, public_key, crypto_sign_PUBLICKEYBYTES); + return 0; +} + +static uint8_t * +server_list_response(size_t *len, pair_list_cb cb, void *cb_arg) +{ + pair_tlv_values_t *response; + uint8_t *data; + uint8_t state = 2; + size_t data_len; + int ret; + + data_len = REQUEST_BUFSIZE; + data = malloc(data_len); + response = pair_tlv_new(); + + pair_tlv_add_value(response, TLVType_State, &state, sizeof(state)); + + cb(server_list_cb, response, cb_arg); + + ret = pair_tlv_format(response, data, &data_len); + if (ret < 0) + { + goto error; + } + + *len = data_len; + + pair_tlv_free(response); + return data; + + error: + free(data); + pair_tlv_free(response); + return NULL; +} + +static int +server_list(uint8_t **out, size_t *out_len, pair_list_cb cb, void *cb_arg, const uint8_t *in, size_t in_len) +{ + // Skip reading the request, it just has state = 1 and pair method = + // PairingMethodListPairings + + *out = server_list_response(out_len, cb, cb_arg); + if (!*out) + return -1; + + return 0; +} + + +/* ----------------------- CIPHERING IMPLEMENTATION ------------------------- */ + +static void +cipher_free(struct pair_cipher_context *cctx) +{ + if (!cctx) + return; + + free(cctx); +} + +static struct pair_cipher_context * +cipher_new(struct pair_definition *type, int channel, const uint8_t *shared_secret, size_t shared_secret_len) +{ + struct pair_cipher_context *cctx; + enum pair_keys write_key; + enum pair_keys read_key; + int ret; + + // Note that events is opposite, probably because it is a reverse connection + switch (channel) + { + case 0: + write_key = PAIR_CONTROL_WRITE; + read_key = PAIR_CONTROL_READ; + break; + case 1: + write_key = PAIR_EVENTS_READ; + read_key = PAIR_EVENTS_WRITE; + break; + case 2: + write_key = PAIR_CONTROL_READ; + read_key = PAIR_CONTROL_WRITE; + break; + case 3: + write_key = PAIR_EVENTS_WRITE; + read_key = PAIR_EVENTS_READ; + break; + default: + return NULL; + } + + cctx = calloc(1, sizeof(struct pair_cipher_context)); + if (!cctx) + goto error; + + cctx->type = type; + + ret = hkdf_extract_expand(cctx->encryption_key, sizeof(cctx->encryption_key), shared_secret, shared_secret_len, write_key); + if (ret < 0) + goto error; + + ret = hkdf_extract_expand(cctx->decryption_key, sizeof(cctx->decryption_key), shared_secret, shared_secret_len, read_key); + if (ret < 0) + goto error; + + return cctx; + + error: + pair_cipher_free(cctx); + return NULL; +} + +static ssize_t +encrypt(uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pair_cipher_context *cctx) +{ + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + const uint8_t *plain_block; + uint8_t *cipher_block; + uint16_t block_len; + int nblocks; + int ret; + int i; + + if (plaintext_len == 0 || !plaintext) + return -1; + + // Encryption is done in blocks, where each block consists of a short, the + // encrypted data and an auth tag. The short is the size of the encrypted + // data. The encrypted data in the block cannot exceed ENCRYPTED_LEN_MAX. + nblocks = 1 + ((plaintext_len - 1) / ENCRYPTED_LEN_MAX); // Ceiling of division + + *ciphertext_len = nblocks * (sizeof(block_len) + AUTHTAG_LENGTH) + plaintext_len; + *ciphertext = malloc(*ciphertext_len); + + cctx->encryption_counter_prev = cctx->encryption_counter; + + for (i = 0, plain_block = plaintext, cipher_block = *ciphertext; i < nblocks; i++) + { + // If it is the last block we will encrypt only the remaining data + block_len = (i + 1 == nblocks) ? (plaintext + plaintext_len - plain_block) : ENCRYPTED_LEN_MAX; + + memcpy(nonce + 4, &(cctx->encryption_counter), sizeof(cctx->encryption_counter));// TODO BE or LE? + + // Write the ciphered block + memcpy(cipher_block, &block_len, sizeof(block_len)); // TODO BE or LE? + ret = encrypt_chacha(cipher_block + sizeof(block_len), plain_block, block_len, cctx->encryption_key, sizeof(cctx->encryption_key), &block_len, sizeof(block_len), tag, sizeof(tag), nonce); + if (ret < 0) + { + cctx->errmsg = "Encryption with chacha poly1305 failed"; + cctx->encryption_counter = cctx->encryption_counter_prev; + free(*ciphertext); + return -1; + } + memcpy(cipher_block + sizeof(block_len) + block_len, tag, AUTHTAG_LENGTH); + + plain_block += block_len; + cipher_block += block_len + sizeof(block_len) + AUTHTAG_LENGTH; + cctx->encryption_counter++; + } + +#ifdef DEBUG_PAIR + hexdump("Encrypted:\n", *ciphertext, *ciphertext_len); +#endif + + return plain_block - plaintext; +} + +static ssize_t +decrypt(uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pair_cipher_context *cctx) +{ + uint8_t nonce[NONCE_LENGTH] = { 0 }; + uint8_t tag[AUTHTAG_LENGTH]; + uint8_t *plain_block; + const uint8_t *cipher_block; + uint16_t block_len; + int ret; + + if (ciphertext_len < sizeof(block_len) || !ciphertext) + return -1; + + // This will allocate more than we need. Since we don't know the number of + // blocks in the ciphertext yet we can't calculate the exact required length. + *plaintext = malloc(ciphertext_len); + + cctx->decryption_counter_prev = cctx->decryption_counter; + + for (plain_block = *plaintext, cipher_block = ciphertext; cipher_block < ciphertext + ciphertext_len; ) + { + memcpy(&block_len, cipher_block, sizeof(block_len)); // TODO BE or LE? + if (cipher_block + block_len + sizeof(block_len) + AUTHTAG_LENGTH > ciphertext + ciphertext_len) + { + // The remaining ciphertext doesn't contain an entire block, so stop + break; + } + + memcpy(tag, cipher_block + sizeof(block_len) + block_len, sizeof(tag)); + memcpy(nonce + 4, &(cctx->decryption_counter), sizeof(cctx->decryption_counter));// TODO BE or LE? + + ret = decrypt_chacha(plain_block, cipher_block + sizeof(block_len), block_len, cctx->decryption_key, sizeof(cctx->decryption_key), &block_len, sizeof(block_len), tag, sizeof(tag), nonce); + if (ret < 0) + { + cctx->errmsg = "Decryption with chacha poly1305 failed"; + cctx->decryption_counter = cctx->decryption_counter_prev; + free(*plaintext); + return -1; + } + + plain_block += block_len; + cipher_block += block_len + sizeof(block_len) + AUTHTAG_LENGTH; + cctx->decryption_counter++; + } + + *plaintext_len = plain_block - *plaintext; + +#ifdef DEBUG_PAIR + hexdump("Decrypted:\n", *plaintext, *plaintext_len); +#endif + + return cipher_block - ciphertext; +} + +static int +state_get(const char **errmsg, const uint8_t *data, size_t data_len) +{ + pair_tlv_values_t *message; + pair_tlv_t *state; + int ret; + + if (!data || data_len == 0) + { + return 0; // state 0 = no incoming data yet -> first request + } + + message = message_process(data, data_len, errmsg); + if (!message) + { + goto error; + } + + state = pair_tlv_get_value(message, TLVType_State); + if (!state || state->size != 1) + { + *errmsg = "Could not get message state"; + goto error; + } + + ret = state->value[0]; + + pair_tlv_free(message); + return ret; + + error: + pair_tlv_free(message); + return -1; +} + +static void +public_key_get(uint8_t server_public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id) +{ + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + server_keypair(server_public_key, private_key, device_id); +} + +const struct pair_definition pair_client_homekit_normal = +{ + .pair_setup_new = client_setup_new, + .pair_setup_free = client_setup_free, + .pair_setup_result = client_setup_result, + + .pair_setup_request1 = client_setup_request1, + .pair_setup_request2 = client_setup_request2, + .pair_setup_request3 = client_setup_request3, + + .pair_setup_response1 = client_setup_response1, + .pair_setup_response2 = client_setup_response2, + .pair_setup_response3 = client_setup_response3, + + .pair_verify_new = client_verify_new, + + .pair_verify_request1 = client_verify_request1, + .pair_verify_request2 = client_verify_request2, + + .pair_verify_response1 = client_verify_response1, + .pair_verify_response2 = client_verify_response2, + + .pair_cipher_new = cipher_new, + .pair_cipher_free = cipher_free, + + .pair_encrypt = encrypt, + .pair_decrypt = decrypt, + + .pair_state_get = state_get, +}; + +const struct pair_definition pair_client_homekit_transient = +{ + .pair_setup_new = client_setup_new, + .pair_setup_free = client_setup_free, + + .pair_setup_request1 = client_setup_request1, + .pair_setup_request2 = client_setup_request2, + .pair_setup_request3 = client_setup_request3, + + .pair_setup_response1 = client_setup_response1, + .pair_setup_response2 = client_setup_response2, + .pair_setup_response3 = client_setup_response3, + + .pair_verify_new = client_verify_new, + + .pair_verify_request1 = client_verify_request1, + .pair_verify_request2 = client_verify_request2, + + .pair_verify_response1 = client_verify_response1, + .pair_verify_response2 = client_verify_response2, + + .pair_cipher_new = cipher_new, + .pair_cipher_free = cipher_free, + + .pair_encrypt = encrypt, + .pair_decrypt = decrypt, + + .pair_state_get = state_get, +}; + +const struct pair_definition pair_server_homekit = +{ + .pair_setup_new = server_setup_new, + .pair_setup_free = server_setup_free, + + .pair_setup_request1 = server_setup_response1, + .pair_setup_request2 = server_setup_response2, + .pair_setup_request3 = server_setup_response3, + + .pair_setup_response1 = server_setup_request1, + .pair_setup_response2 = server_setup_request2, + .pair_setup_response3 = server_setup_request3, + + .pair_verify_new = server_verify_new, + + .pair_verify_request1 = server_verify_response1, + .pair_verify_request2 = server_verify_response2, + + .pair_verify_response1 = server_verify_request1, + .pair_verify_response2 = server_verify_request2, + + .pair_add = server_add_remove, + .pair_remove = server_add_remove, + .pair_list = server_list, + + .pair_cipher_new = cipher_new, + .pair_cipher_free = cipher_free, + + .pair_encrypt = encrypt, + .pair_decrypt = decrypt, + + .pair_state_get = state_get, + .pair_public_key_get = public_key_get, +}; diff --git a/pair_ap/server-example.c b/pair_ap/server-example.c new file mode 100644 index 00000000..73fe4bda --- /dev/null +++ b/pair_ap/server-example.c @@ -0,0 +1,685 @@ +/* + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "pair.h" + +#define DEVICE_ID "FFEEDDCCBBAA9988" +#define LISTEN_PORT 7000 +#define CONTENT_TYPE_OCTET "application/octet-stream" +#define RTSP_VERSION "RTSP/1.0" +#define OPTIONS "OPTIONS *" + +struct connection_ctx +{ + struct evbuffer *pending; + struct pair_setup_context *setup_ctx; + struct pair_verify_context *verify_ctx; + struct pair_cipher_context *cipher_ctx; + + int pair_completed; +}; + +struct rtsp_msg +{ + int content_length; + char *content_type; + char *first_line; + int cseq; + + const uint8_t *body; + size_t bodylen; + + const uint8_t *data; + size_t datalen; +}; + +struct pairings +{ + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + uint8_t public_key[32]; + + struct pairings *next; +} *pairings; + + +static void +connection_free(struct connection_ctx *conn_ctx) +{ + if (!conn_ctx) + return; + + evbuffer_free(conn_ctx->pending); + pair_setup_free(conn_ctx->setup_ctx); + pair_cipher_free(conn_ctx->cipher_ctx); + + free(conn_ctx); +} + +static void +response_headers_add(struct evbuffer *response, int cseq, size_t content_length, const char *content_type) +{ + evbuffer_add_printf(response, "%s 200 OK\r\n", RTSP_VERSION); + evbuffer_add_printf(response, "Server: MyServer/1.0\r\n"); + if (content_length) + evbuffer_add_printf(response, "Content-Length: %zu\r\n", content_length); + if (content_type) + evbuffer_add_printf(response, "Content-Type: %s\r\n", content_type); + evbuffer_add_printf(response, "CSeq: %d\r\n", cseq); + evbuffer_add_printf(response, "\r\n"); +} + +static void +response_create_from_raw(struct evbuffer *response, uint8_t *body, size_t body_len, int cseq, const char *content_type) +{ + response_headers_add(response, cseq, body_len, content_type); + + if (body) + evbuffer_add(response, body, body_len); +} + +static int +encryption_enable(struct connection_ctx *conn_ctx, const uint8_t *shared_secret, size_t shared_secret_len) +{ + conn_ctx->cipher_ctx = pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, shared_secret, shared_secret_len); + if (!conn_ctx->cipher_ctx) + { + printf("Error setting up ciphering\n"); + return -1; + } + + return 0; +} + +static int +buffer_encrypt(struct evbuffer *output, uint8_t *in, size_t in_len, struct connection_ctx *conn_ctx) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_encrypt(&out, &out_len, in, in_len, conn_ctx->cipher_ctx); + if (ret < 0) + { + printf("Error encrypting: %s\n", pair_cipher_errmsg(conn_ctx->cipher_ctx)); + return -1; + } + + evbuffer_add(output, out, out_len); + free(out); + return 0; +} + +static int +buffer_decrypt(struct evbuffer *output, struct evbuffer *input, struct connection_ctx *conn_ctx) +{ + uint8_t *in; + size_t in_len; + ssize_t bytes_decrypted; + uint8_t *plain; + size_t plain_len; + + in = evbuffer_pullup(input, -1); + in_len = evbuffer_get_length(input); + + // Note that bytes_decrypted is not necessarily equal to plain_len + bytes_decrypted = pair_decrypt(&plain, &plain_len, in, in_len, conn_ctx->cipher_ctx); + if (bytes_decrypted < 0) + { + printf("Error decrypting: %s\n", pair_cipher_errmsg(conn_ctx->cipher_ctx)); + return -1; + } + + evbuffer_add(output, plain, plain_len); + evbuffer_drain(input, bytes_decrypted); + free(plain); + return 0; +} + + +/* ---------------------------- Pairing callbacks --------------------------- */ +/* Note that none of these callbacks are required if you don't care about */ +/* securely verifying the client + don't require support for the pair-add, */ +/* pair-remove and pair-list methods. */ + +static struct pairings * +pairing_find(const char *device_id) +{ + struct pairings *pairing; + + for (pairing = pairings; pairing; pairing = pairing->next) + { + if (strcmp(device_id, pairing->device_id) == 0) + break; + } + + return pairing; +} + +static int +pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg) +{ + struct pairings *pairing; + + printf("Adding paired device %s\n", device_id); + + pairing = pairing_find(device_id); + if (pairing) + { + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + return 0; + } + + pairing = calloc(1, sizeof(struct pairings)); + snprintf(pairing->device_id, sizeof(pairing->device_id), "%s", device_id); + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + + pairing->next = pairings; + pairings = pairing; + + return 0; +} + +static int +pairing_remove_cb(uint8_t public_key[32], const char *device_id, void *cb_arg) +{ + struct pairings *pairing; + struct pairings *iter; + + printf("Removing paired device %s\n", device_id); + + pairing = pairing_find(device_id); + if (!pairing) + { + printf("Remove callback for unknown device\n"); + return -1; + } + + if (pairing == pairings) + pairings = pairing->next; + else + { + for (iter = pairings; iter && (iter->next != pairing); iter = iter->next) + ; /* EMPTY */ + + if (iter) + iter->next = pairing->next; + } + + free(pairing); + return 0; +} + +static void +pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg) +{ + struct pairings *pairing; + + printf("Listing paired devices\n"); + + for (pairing = pairings; pairing; pairing = pairing->next) + { + enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); + } +} + +static int +pairing_get_cb(uint8_t public_key[32], const char *device_id, void *cb_arg) +{ + struct pairings *pairing; + + printf("Returning public key for paired device %s\n", device_id); + + pairing = pairing_find(device_id); + if (!pairing) + return -1; + + memcpy(public_key, pairing->public_key, sizeof(pairing->public_key)); + + return 0; +} + + +/* -------------------------- Pair request handlers ------------------------- */ + +static int +handle_pin_start(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + printf("Please pair with code 3939\n"); + + response_create_from_raw(output, NULL, 0, msg->cseq, NULL); + + return 0; +} + +static int +handle_pair_setup(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + struct pair_result *result; + int ret; + + if (!conn_ctx->setup_ctx) + { + conn_ctx->setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, NULL, pairing_add_cb, NULL, DEVICE_ID); + if (!conn_ctx->setup_ctx) + { + printf("Error creating setup context\n"); + return -1; + } + } + + ret = pair_setup(&out, &out_len, conn_ctx->setup_ctx, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Pair setup error: %s\n", pair_setup_errmsg(conn_ctx->setup_ctx)); + return -1; + } + + ret = pair_setup_result(NULL, &result, conn_ctx->setup_ctx); + if (ret == 0 && result->shared_secret_len > 0) // Transient pairing completed (step 2) + { + encryption_enable(conn_ctx, result->shared_secret, result->shared_secret_len); + conn_ctx->pair_completed = 1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_verify(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + struct pair_result *result; + int ret; + + if (!conn_ctx->verify_ctx) + { + conn_ctx->verify_ctx = pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, pairing_get_cb, NULL, DEVICE_ID); + if (!conn_ctx->verify_ctx) + { + printf("Error creating verify context\n"); + return -1; + } + } + + ret = pair_verify(&out, &out_len, conn_ctx->verify_ctx, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Pair verify error: %s\n", pair_verify_errmsg(conn_ctx->verify_ctx)); + return -1; + } + + ret = pair_verify_result(&result, conn_ctx->verify_ctx); + if (ret == 0) + { + encryption_enable(conn_ctx, result->shared_secret, result->shared_secret_len); + conn_ctx->pair_completed = 1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_add(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_add(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_add_cb, NULL, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Error adding device to list\n"); + return -1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_remove(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_remove(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_remove_cb, NULL, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Error removing device from list\n"); + return -1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_pair_list(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + uint8_t *out; + size_t out_len; + int ret; + + ret = pair_list(PAIR_SERVER_HOMEKIT, &out, &out_len, pairing_list_cb, NULL, msg->body, msg->bodylen); + if (ret < 0) + { + printf("Error creating list of paired devices\n"); + return -1; + } + + response_create_from_raw(output, out, out_len, msg->cseq, CONTENT_TYPE_OCTET); + free(out); + + return 0; +} + +static int +handle_options(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + struct evbuffer *response; + uint8_t *plain; + size_t plain_len; + int ret; + + response = evbuffer_new(); + + response_create_from_raw(response, NULL, 0, msg->cseq, NULL); + + if (!conn_ctx->cipher_ctx) + { + evbuffer_add_buffer(output, response); + evbuffer_free(response); + return 0; + } + + plain = evbuffer_pullup(response, -1); + plain_len = evbuffer_get_length(response); + + ret = buffer_encrypt(output, plain, plain_len, conn_ctx); + + evbuffer_free(response); + return ret; +} + +static int +response_send(struct evbuffer *output, struct connection_ctx *conn_ctx, struct rtsp_msg *msg) +{ + if (!msg->first_line) + return -1; + + if (strncmp(msg->first_line, PAIR_AP_POST_PIN_START, strlen(PAIR_AP_POST_PIN_START)) == 0) + return handle_pin_start(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_SETUP, strlen(PAIR_AP_POST_SETUP)) == 0) + return handle_pair_setup(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_VERIFY, strlen(PAIR_AP_POST_VERIFY)) == 0) + return handle_pair_verify(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_ADD, strlen(PAIR_AP_POST_ADD)) == 0) + return handle_pair_add(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_LIST, strlen(PAIR_AP_POST_LIST)) == 0) + return handle_pair_list(output, conn_ctx, msg); + else if (strncmp(msg->first_line, PAIR_AP_POST_REMOVE, strlen(PAIR_AP_POST_REMOVE)) == 0) + return handle_pair_remove(output, conn_ctx, msg); + else if (strncmp(msg->first_line, OPTIONS, strlen(OPTIONS)) == 0) + return handle_options(output, conn_ctx, msg); + + printf("Unknown method: %s\n", msg->first_line); + return -1; +} + + +/* --------------------- A basic RTSP server implementation ----------------- */ + +static void +rtsp_clear(struct rtsp_msg *msg) +{ + free(msg->first_line); + free(msg->content_type); +} + +// Very primitive RTSP message parser, hope you have a better one +static int +rtsp_parse(struct rtsp_msg *msg, uint8_t *in, size_t in_len) +{ + char *line; + int i; + + line = (char *)in; + for (i = 0; i < in_len; i++) + { + if (in[i] != '\n' && in[i - 1] != '\r') + continue; + + if (in[i - 2] == '\n' && in[i - 3] == '\r') + { + msg->bodylen = in_len - (i + 1); + if (msg->bodylen != msg->content_length) + { + printf("Incomplete read (have %zu, content-length %d), waiting for more data\n\n", msg->bodylen, msg->content_length); + rtsp_clear(msg); + return 1; + } + else if (msg->bodylen > 0) + msg->body = in + i + 1; + + break; + } + + in[i - 1] = '\0'; + + if (!msg->first_line) + msg->first_line = strdup(line); + + if (strncmp(line, "CSeq: ", strlen("CSeq: ")) == 0) + msg->cseq = atoi(line + strlen("CSeq: ")); + + if (strncmp(line, "Content-Length: ", strlen("Content-Length: ")) == 0) + msg->content_length = atoi(line + strlen("Content-Length: ")); + + if (strncmp(line, "Content-Type: ", strlen("Content-Type: ")) == 0 && !msg->content_type) + msg->content_type = strdup(line + strlen("Content-Type: ")); + + in[i - 1] = '\r'; + + line = (char *)in + i + 1; + } + + msg->data = in; + msg->datalen = in_len; + + return 0; +} + +static void +in_read_cb(struct bufferevent *bev, void *arg) +{ + struct connection_ctx *conn_ctx = arg; + struct evbuffer *input; + struct evbuffer *output; + uint8_t *plain; + size_t plain_len; + struct rtsp_msg msg = { 0 }; + int ret; + + input = bufferevent_get_input(bev); + output = bufferevent_get_output(bev); + + printf("\n--------------------------------------------------------------------------\n"); + + if (conn_ctx->pair_completed) + { + buffer_decrypt(conn_ctx->pending, input, conn_ctx); + } + else + { + evbuffer_add_buffer(conn_ctx->pending, input); + } + + // Pending holds all the message we have received so far, incl. what parts we + // might have received in previous callbacks + plain = evbuffer_pullup(conn_ctx->pending, -1); + plain_len = evbuffer_get_length(conn_ctx->pending); + + ret = rtsp_parse(&msg, plain, plain_len); + if (ret < 0) + { + printf("Could not parse RTSP message\n"); + goto error; + } + else if (ret == 1) + return; // Message incomplete, wait for more data + + ret = response_send(output, conn_ctx, &msg); + if (ret < 0) + { + goto error; + } + + error: + rtsp_clear(&msg); + evbuffer_drain(conn_ctx->pending, evbuffer_get_length(conn_ctx->pending)); + return; +} + +static void +in_event_cb(struct bufferevent *bev, short events, void *arg) +{ + struct connection_ctx *conn_ctx = arg; + + if (events & BEV_EVENT_ERROR) + printf("Error from bufferevent: %s\n", evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR())); + + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) + bufferevent_free(bev); + + connection_free(conn_ctx); +} + + +/*------------------------- General server stuff ----------------------------*/ + +static void +in_accept_cb(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *address, int socklen, void *ctx) +{ + struct event_base *base = evconnlistener_get_base(listener); + struct bufferevent *bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE); + struct connection_ctx *conn_ctx; + + conn_ctx = calloc(1, sizeof(struct connection_ctx)); + conn_ctx->pending = evbuffer_new(); + + bufferevent_setcb(bev, in_read_cb, NULL, in_event_cb, conn_ctx); + bufferevent_enable(bev, EV_READ | EV_WRITE); + + printf("New connection accepted\n"); +} + +static void +in_error_cb(struct evconnlistener *listener, void *ctx) +{ + int err = EVUTIL_SOCKET_ERROR(); + printf("Error occured %d (%s) on the listener\n", err, evutil_socket_error_to_string(err)); +} + +static struct evconnlistener * +listen_add(struct event_base *evbase, evconnlistener_cb req_cb, evconnlistener_errorcb err_cb, unsigned short port) +{ + struct evconnlistener *listener; + struct addrinfo hints = { 0 }; + struct addrinfo *servinfo; + char strport[8]; + int ret; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + hints.ai_flags = AI_PASSIVE; + + snprintf(strport, sizeof(strport), "%hu", port); + ret = getaddrinfo(NULL, strport, &hints, &servinfo); + if (ret < 0) + { + printf("getaddrinf() failed: %s\n", gai_strerror(ret)); + return NULL; + } + + listener = evconnlistener_new_bind(evbase, req_cb, NULL, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, servinfo->ai_addr, servinfo->ai_addrlen); + freeaddrinfo(servinfo); + if (!listener) + { + printf("Could not create listener for port %hu\n", port); + return NULL; + } + + evconnlistener_set_error_cb(listener, err_cb); + + return listener; +} + +int +main(int argc, char * argv[]) +{ + struct event_base *evbase; + struct evconnlistener *listener; + + evbase = event_base_new(); + + listener = listen_add(evbase, in_accept_cb, in_error_cb, LISTEN_PORT); + if (!listener) + return -1; + + printf("Listening for pairing requests on port %d\n", LISTEN_PORT); + + event_base_dispatch(evbase); + + evconnlistener_free(listener); + event_base_free(evbase); + + return 0; +}