Merge branch 'development' of github.com:aillwee/shairport-sync into charles-dev
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
[submodule "pair_ap"]
|
||||
path = pair_ap
|
||||
url = https://github.com/ejurgensen/pair_ap
|
||||
|
||||
@@ -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=<pathname>` | Specify a pathname to a directory in which to write the PID file.
|
||||
| `--with-ssl=<library>` | 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 |
|
||||
|
||||
@@ -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.
|
||||
+68
@@ -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?
|
||||
+21
-9
@@ -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:
|
||||
```
|
||||
|
||||
+40
-20
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Fedora Installation Guide
|
||||
Fedora Installation Guide [Needs updating for Airplay 2]
|
||||
-----
|
||||
|
||||
Install the toolchain and pre-requisites, if necessary:
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||
|
||||
+9
-327
@@ -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
|
||||
-------------
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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**
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
-1
Submodule pair_ap deleted from d955c447da
@@ -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)
|
||||
@@ -0,0 +1,578 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <sodium.h>
|
||||
|
||||
#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 <gcrypt.h>
|
||||
#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 <openssl/crypto.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/evp.h>
|
||||
#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
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* TLV helpers are adapted from ESP homekit:
|
||||
* <https://github.com/maximkulkin/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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#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
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#ifndef __PAIR_AP_TLV_H__
|
||||
#define __PAIR_AP_TLV_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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__ */
|
||||
+771
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h> // for isprint()
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
#ifndef __PAIR_AP_H__
|
||||
#define __PAIR_AP_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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__ */
|
||||
@@ -0,0 +1,10 @@
|
||||
*.o
|
||||
*.lo
|
||||
*.a
|
||||
*.la
|
||||
.dirstamp
|
||||
.deps/
|
||||
.libs/
|
||||
|
||||
client-example
|
||||
server-example
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Julien BLACHE <jb@jblache.org>
|
||||
* Based on evhttp from libevent 1.4.x
|
||||
*
|
||||
* Copyright (c) 2000-2004 Niels Provos <provos@citi.umich.edu>
|
||||
* 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 <event2/event.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#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_ */
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2000-2004 Niels Provos <provos@citi.umich.edu>
|
||||
* 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
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Julien BLACHE <jb@jblache.org>
|
||||
* Based on evhttp from libevent 1.4.x
|
||||
*
|
||||
* Copyright 2001 Niels Provos <provos@citi.umich.edu>
|
||||
* 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 <event2/buffer.h>
|
||||
#include <event2/event_struct.h>
|
||||
|
||||
#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 */
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/listener.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user