Merge pull request #1244 from ejurgensen/pair_home5

Home app pairing (support for pair-add etc).

Super stuff, @ejurgensen. Thanks, as ever.
This commit is contained in:
Mike Brady
2021-07-16 10:55:29 +01:00
committed by GitHub
5 changed files with 193 additions and 18 deletions
+7 -6
View File
@@ -2,9 +2,10 @@
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)
* Homekit pairing (also for AirPlay 2)
Credit goes to @funtax and @ViktoriiaKh for doing some of the heavy lifting.
## Requirements
- libsodium
- libgcrypt or libopenssl
@@ -33,7 +34,7 @@ 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
### Normal pairing with one-time code
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
@@ -42,12 +43,12 @@ 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
the parties check eachothers 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.
encryption of the following communication between the parties.
### Transient pairing
Some devices don't require a code from the user for pairing (e.g. an Airport
@@ -55,8 +56,7 @@ 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.
The controller can still use `/pair-add` etc. towards such devices.
## "fruit" pairing
Like normal Homekit pairing, this consists of first requesting a code with
@@ -69,4 +69,5 @@ shared secret.
- [AirPlayAuth](https://github.com/funtax/AirPlayAuth)
- [AirPlayAuth-ObjC](https://github.com/ViktoriiaKh/AirPlayAuth-ObjC)
- [ap2-sender](https://github.com/ViktoriiaKh/ap2-sender)
- [airplay2-receiver](https://github.com/ckdo/airplay2-receiver)
- [csrp](https://github.com/cocagne/csrp)
+1 -1
View File
@@ -4,7 +4,7 @@
#include <stdint.h>
#define PAIR_AP_VERSION_MAJOR 0
#define PAIR_AP_VERSION_MINOR 5
#define PAIR_AP_VERSION_MINOR 8
#define PAIR_AP_DEVICE_ID_LEN_MAX 64
+19 -3
View File
@@ -2679,6 +2679,7 @@ server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in
pair_tlv_t *device_id;
pair_tlv_t *pk;
char id_str[PAIR_AP_DEVICE_ID_LEN_MAX] = { 0 };
uint8_t *public_key = NULL;
request = message_process(in, in_len, &errmsg);
if (!request)
@@ -2687,15 +2688,21 @@ server_add_remove_request(pair_cb cb, void *cb_arg, const uint8_t *in, size_t in
}
device_id = pair_tlv_get_value(request, TLVType_Identifier);
pk = pair_tlv_get_value(request, TLVType_PublicKey);
if (!device_id || device_id->size >= sizeof(id_str) || !pk || pk->size != crypto_sign_PUBLICKEYBYTES)
if (!device_id || device_id->size >= sizeof(id_str))
{
goto error;
}
// Only present when adding
pk = pair_tlv_get_value(request, TLVType_PublicKey);
if (pk && pk->size == crypto_sign_PUBLICKEYBYTES)
{
public_key = pk->value;
}
memcpy(id_str, device_id->value, device_id->size);
cb(pk->value, id_str, cb_arg);
cb(public_key, id_str, cb_arg);
pair_tlv_free(request);
return 0;
@@ -2757,9 +2764,18 @@ static int
server_list_cb(uint8_t public_key[crypto_sign_PUBLICKEYBYTES], const char *device_id, void *cb_arg)
{
pair_tlv_values_t *response = cb_arg;
pair_tlv_t *previous_id;
uint8_t permissions = 1; // Means admin (TODO don't hardcode - let caller set)
// If this isn't the first iteration (item) then we must add a separator
previous_id = pair_tlv_get_value(response, TLVType_Identifier);
if (previous_id)
pair_tlv_add_value(response, TLVType_Separator, NULL, 0);
pair_tlv_add_value(response, TLVType_Identifier, (unsigned char *)device_id, strlen(device_id));
pair_tlv_add_value(response, TLVType_PublicKey, public_key, crypto_sign_PUBLICKEYBYTES);
pair_tlv_add_value(response, TLVType_Permissions, &permissions, sizeof(permissions));
return 0;
}
+161
View File
@@ -1555,6 +1555,125 @@ void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)
#endif
#ifdef CONFIG_AIRPLAY_2
struct pairings {
char device_id[PAIR_AP_DEVICE_ID_LEN_MAX];
uint8_t public_key[32];
struct pairings *next;
} *pairings;
static struct pairings * pairing_find(const char *device_id)
{
for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) {
if (strcmp(device_id, pairing->device_id) == 0)
return pairing;
}
return NULL;
}
static void pairing_add(uint8_t public_key[32], const char *device_id) {
struct pairings *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;
}
static void pairing_remove(struct pairings *pairing) {
if (pairing == pairings) {
pairings = pairing->next;
} else {
struct pairings *iter;
for (iter = pairings; iter && (iter->next != pairing); iter = iter->next)
; /* EMPTY */
if (iter)
iter->next = pairing->next;
}
free(pairing);
}
static int pairing_add_cb(uint8_t public_key[32], const char *device_id, void *cb_arg __attribute__((unused))) {
debug(1, "pair-add cb for %s", device_id);
struct pairings *pairing = pairing_find(device_id);
if (pairing) {
memcpy(pairing->public_key, public_key, sizeof(pairing->public_key));
return 0;
}
pairing_add(public_key, device_id);
return 0;
}
static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, void *cb_arg __attribute__((unused))) {
debug(1, "pair-remove cb for %s", device_id);
struct pairings *pairing = pairing_find(device_id);
if (!pairing) {
debug(1, "pair-remove callback for unknown device");
return -1;
}
pairing_remove(pairing);
return 0;
}
static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, void *cb_arg __attribute__((unused))) {
debug(1, "pair-list cb");
for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) {
enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg);
}
}
void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) {
uint8_t *body = NULL;
size_t body_len = 0;
int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, (const uint8_t *)req->content, req->contentlength);
if (ret < 0) {
debug(1, "pair-add returned an error");
resp->respcode = 451;
return;
}
resp->content = (char *)body; // these will be freed when the data is sent
resp->contentlength = body_len;
msg_add_header(resp, "Content-Type", "application/octet-stream");
debug_log_rtsp_message(2, "pair-add response", resp);
}
void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) {
uint8_t *body = NULL;
size_t body_len = 0;
int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, (const uint8_t *)req->content, req->contentlength);
if (ret < 0) {
debug(1, "pair-list returned an error");
resp->respcode = 451;
return;
}
resp->content = (char *)body; // these will be freed when the data is sent
resp->contentlength = body_len;
msg_add_header(resp, "Content-Type", "application/octet-stream");
debug_log_rtsp_message(2, "pair-list response", resp);
}
void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, rtsp_message *resp) {
uint8_t *body = NULL;
size_t body_len = 0;
int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, (const uint8_t *)req->content, req->contentlength);
if (ret < 0) {
debug(1, "pair-remove returned an error");
resp->respcode = 451;
return;
}
resp->content = (char *)body; // these will be freed when the data is sent
resp->contentlength = body_len;
msg_add_header(resp, "Content-Type", "application/octet-stream");
debug_log_rtsp_message(2, "pair-remove response", resp);
}
void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
int ret;
uint8_t *body = NULL;
@@ -1747,14 +1866,56 @@ void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message
msg_add_header(resp, "Content-Type", "application/octet-stream");
}
/*
<key>Identifier</key>
<string>21cc689d-d5de-4814-872c-71d1426b57e0</string>
<key>Enable_HK_Access_Control</key>
<true/>
<key>PublicKey</key>
<data>
qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ=
</data>
<key>Device_Name</key>
<string>MyDevice</string>
<key>Access_Control_Level</key>
<integer>0</integer>
*/
void handle_configure(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req __attribute__((unused)), rtsp_message *resp) {
uint8_t public_key[32];
pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, config.airplay_device_id);
plist_t response_plist = plist_new_dict();
plist_dict_set_item(response_plist, "Identifier", plist_new_string(config.airplay_pi));
plist_dict_set_item(response_plist, "Enable_HK_Access_Control", plist_new_bool(1));
plist_dict_set_item(response_plist, "PublicKey", plist_new_data((const char *)public_key, sizeof(public_key)));
plist_dict_set_item(response_plist, "Device_Name", plist_new_string(config.service_name));
plist_dict_set_item(response_plist, "Access_Control_Level", plist_new_uint(0));
plist_to_bin(response_plist, &resp->content, &resp->contentlength);
plist_free(response_plist);
msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist");
debug_log_rtsp_message(2, "POST /configure response:", resp);
}
void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) {
resp->respcode = 200;
if (strcmp(req->path, "/pair-setup") == 0) {
handle_pair_setup(conn, req, resp);
} else if (strcmp(req->path, "/pair-verify") == 0) {
handle_pair_verify(conn, req, resp);
} else if (strcmp(req->path, "/pair-add") == 0) {
handle_pair_add(conn, req, resp);
} else if (strcmp(req->path, "/pair-remove") == 0) {
handle_pair_remove(conn, req, resp);
} else if (strcmp(req->path, "/pair-list") == 0) {
handle_pair_list(conn, req, resp);
} else if (strcmp(req->path, "/fp-setup") == 0) {
handle_fp_setup(conn, req, resp);
} else if (strcmp(req->path, "/configure") == 0) {
handle_configure(conn, req, resp);
} else {
debug(3, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path,
req->contentlength);
+5 -8
View File
@@ -1587,15 +1587,12 @@ int main(int argc, char **argv) {
// features=0x405F4A00,0x1C340 in the mDNS string, and in a signed decimal number in the plist:
// 496155702020608 this setting here is the source of both the plist features response and the
// mDNS string.
config.airplay_features = 0x1C340405F4A00;
// note: 0x300401F4A00 works but with weird delays and stuff
config.airplay_features = 0x1C340405FCA00;
// Advertised with mDNS and returned with GET /info, see
// https://openairplay.github.io/airplay-spec/status_flags.html 0x4: Audio cable attached, no PIN
// required (transient pairing), no Homekit access control 0x204: Audio cable attached,
// OneTimePairingRequired 0x604: Audio cable attached, OneTimePairingRequired, allow Homekit
// access control
// note: 0x300401F4A00 works but with weird delays and stuff
// required (transient pairing), 0x204: Audio cable attached, OneTimePairingRequired 0x604: Audio
// cable attached, OneTimePairingRequired, device was setup for Homekit access control
config.airplay_statusflags = 0x4;
// Set to NULL to work with transient pairing
config.airplay_pin = NULL;
@@ -1622,7 +1619,7 @@ int main(int argc, char **argv) {
uuid_generate_random(binuuid);
char *uuid = malloc(UUID_STR_LEN);
/* Produces a UUID string at uuid consisting of lower-case letters. */
// Produces a UUID string at uuid consisting of lower-case letters
uuid_unparse_lower(binuuid, uuid);
config.airplay_pi = strdup(uuid);
config.airplay_gid = strdup(uuid); // initially the gid is the same as the pi