diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c index b157e571e..d87220e50 100644 --- a/src/modules/module-raop-discover.c +++ b/src/modules/module-raop-discover.c @@ -64,7 +64,7 @@ * #raop.domain = "" * #raop.device = "" * #raop.transport = "udp" | "tcp" - * #raop.encryption.type = "RSA" | "auth_setup" | "none" + * #raop.encryption.type = "RSA" | "auth_setup" | "pair_setup" | "none" * #raop.audio.codec = "PCM" | "ALAC" | "AAC" | "AAC-ELD" * #audio.channels = 2 * #audio.format = "S16" | "S24" | "S32" @@ -105,12 +105,14 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); static const struct spa_dict_item module_props[] = { { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, - { PW_KEY_MODULE_DESCRIPTION, "Discover remote streams" }, + { PW_KEY_MODULE_DESCRIPTION, "Discover remote speakers" }, { PW_KEY_MODULE_USAGE, MODULE_USAGE }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; -#define SERVICE_TYPE_SINK "_raop._tcp" +#define SERVICE_TYPE_RAOP "_raop._tcp" +#define SERVICE_TYPE_AP "_airplay._tcp" + struct impl { struct pw_context *context; @@ -122,7 +124,8 @@ struct impl { AvahiPoll *avahi_poll; AvahiClient *client; - AvahiServiceBrowser *sink_browser; + AvahiServiceBrowser *sink_browser_raop; + AvahiServiceBrowser *sink_browser_ap; struct spa_list tunnel_list; }; @@ -182,8 +185,10 @@ static void impl_free(struct impl *impl) spa_list_consume(t, &impl->tunnel_list, link) free_tunnel(t); - if (impl->sink_browser) - avahi_service_browser_free(impl->sink_browser); + if (impl->sink_browser_raop) + avahi_service_browser_free(impl->sink_browser_raop); + if (impl->sink_browser_ap) + avahi_service_browser_free(impl->sink_browser_ap); if (impl->client) avahi_client_free(impl->client); if (impl->avahi_poll) @@ -215,7 +220,30 @@ static bool str_in_list(const char *haystack, const char *delimiters, const char return false; } -static void pw_properties_from_avahi_string(const char *key, const char *value, +static void pw_properties_from_ap_txt(const char *key, const char *value, + struct pw_properties *props) +{ + if (spa_streq(key, "deviceid")) { + pw_properties_set(props, "raop.device", value); + } + else if (spa_streq(key, "features")) { + pw_properties_set(props, "raop.features", value); + } + else if (spa_streq(key, "flags")) { + pw_properties_set(props, "raop.flags", value); + } + else if (spa_streq(key, "pk")) { + pw_properties_set(props, "raop.pk", value); + } + else if (spa_streq(key, "pi")) { + pw_properties_set(props, "raop.pi", value); + } + else if (spa_streq(key, "psi")) { + pw_properties_set(props, "raop.psi", value); + } +} + +static void pw_properties_from_raop_txt(const char *key, const char *value, struct pw_properties *props) { if (spa_streq(key, "device")) { @@ -393,6 +421,37 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr goto done; } + pw_log_info("mdns service type: %s", type); + if (strcmp(type, SERVICE_TYPE_AP) == 0) { + for (l = txt; l; l = l->next) { + char *key, *value; + + if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) + break; + + pw_properties_from_ap_txt(key, value, props); + avahi_free(key); + avahi_free(value); + } + + // TODO lorbus + // if !feature audio goto done + pw_properties_set(props, "raop.encryption.type", "pair_setup"); + pw_properties_set(props, "raop.transport", "udp"); + + } else { + for (l = txt; l; l = l->next) { + char *key, *value; + + if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) + break; + + pw_properties_from_raop_txt(key, value, props); + avahi_free(key); + avahi_free(value); + } + } + avahi_address_snprint(at, sizeof(at), a); ipv = protocol == AVAHI_PROTO_INET ? 4 : 6; pw_properties_setf(props, "raop.ip", "%s", at); @@ -402,17 +461,6 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr pw_properties_setf(props, "raop.hostname", "%s", host_name); pw_properties_setf(props, "raop.domain", "%s", domain); - for (l = txt; l; l = l->next) { - char *key, *value; - - if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) - break; - - pw_properties_from_avahi_string(key, value, props); - avahi_free(key); - avahi_free(value); - } - if ((str = pw_properties_get(impl->properties, "raop.latency.ms")) != NULL) pw_properties_set(props, "raop.latency.ms", str); @@ -502,9 +550,13 @@ static void client_callback(AvahiClient *c, AvahiClientState state, void *userda case AVAHI_CLIENT_S_REGISTERING: case AVAHI_CLIENT_S_RUNNING: case AVAHI_CLIENT_S_COLLISION: - if (impl->sink_browser == NULL) - impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK); - if (impl->sink_browser == NULL) + if (impl->sink_browser_raop == NULL) + impl->sink_browser_raop = make_browser(impl, SERVICE_TYPE_RAOP); + if (impl->sink_browser_raop == NULL) + goto error; + if (impl->sink_browser_ap == NULL) + impl->sink_browser_ap = make_browser(impl, SERVICE_TYPE_AP); + if (impl->sink_browser_ap == NULL) goto error; break; case AVAHI_CLIENT_FAILURE: @@ -513,9 +565,13 @@ static void client_callback(AvahiClient *c, AvahiClientState state, void *userda SPA_FALLTHROUGH; case AVAHI_CLIENT_CONNECTING: - if (impl->sink_browser) { - avahi_service_browser_free(impl->sink_browser); - impl->sink_browser = NULL; + if (impl->sink_browser_raop) { + avahi_service_browser_free(impl->sink_browser_raop); + impl->sink_browser_raop = NULL; + } + if (impl->sink_browser_ap) { + avahi_service_browser_free(impl->sink_browser_ap); + impl->sink_browser_ap = NULL; } break; default: diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c index 6d732fc0f..93c17a705 100644 --- a/src/modules/module-raop-sink.c +++ b/src/modules/module-raop-sink.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include +#include #include #include #include @@ -26,6 +28,7 @@ #include #include #include +#include #include "config.h" @@ -44,6 +47,8 @@ #include #include "module-raop/rtsp-client.h" +#include "module-raop/srp.h" +#include "module-raop/tlv.h" /** \page page_module_raop_sink PipeWire Module: AirPlay Sink * @@ -136,9 +141,16 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define MD5_DIGEST_LENGTH 16 #endif #define MD5_HASH_LENGTH (2*MD5_DIGEST_LENGTH) +#ifndef SHA512_DIGEST_LENGTH +#define SHA512_DIGEST_LENGTH 64 +#endif #define DEFAULT_USER_AGENT "iTunes/11.0.4 (Windows; N)" #define DEFAULT_USER_NAME "iTunes" +#define AP_USER_AGENT "AirPlay/381.13" +#define AP_USER_NAME "AirPlay" +#define AP_REQUEST_BUFSIZE 4096 +#define AP_SRP_USER_NAME "Pair-Setup" #define MAX_PORT_RETRY 128 @@ -171,7 +183,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); static const struct spa_dict_item module_props[] = { { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, - { PW_KEY_MODULE_DESCRIPTION, "An RAOP audio sink" }, + { PW_KEY_MODULE_DESCRIPTION, "An AirPlay audio sink" }, { PW_KEY_MODULE_USAGE, MODULE_USAGE }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; @@ -183,7 +195,8 @@ enum { enum { CRYPTO_NONE, CRYPTO_RSA, - CRYPTO_AUTH_SETUP, + CRYPTO_AUTH_SETUP = 4, + CRYPTO_PAIR_TRANSIENT = 8, }; enum { CODEC_PCM, @@ -232,6 +245,22 @@ struct impl { uint8_t aes_iv[AES_CHUNK_SIZE]; /* Initialization vector for cbc */ EVP_CIPHER_CTX *ctx; + struct SRPUser *srp_user; + uint8_t public_key[32]; + uint8_t private_key[64]; + const uint8_t *pkA; + int pkA_len; + uint8_t *pkB; + uint64_t pkB_len; + const uint8_t *M1; + int M1_len; + uint8_t *M2; + int M2_len; + uint8_t *salt; + uint64_t salt_len; + uint8_t shared_secret[64]; + size_t shared_secret_len; // Will be 32 (normal) or 64 (transient) + uint16_t control_port; int control_fd; struct spa_source *control_source; @@ -1384,6 +1413,290 @@ static int rtsp_do_raop_auth(struct impl *impl, const struct spa_dict *headers) return rtsp_send(impl, "OPTIONS", NULL, NULL, rtsp_raop_auth_reply); } +static tlv_values_t * tlv_message_process(const uint8_t *data, size_t data_len) +{ + tlv_values_t *response; + tlv_t *error; + int ret; + + pw_log_debug("tlv: processing tlv message"); + + response = tlv_new(); + if (!response) { + pw_log_error("tlv: Out of memory"); + return NULL; + } + + ret = tlv_parse(data, data_len, response); + if (ret < 0) { + pw_log_error("tlv: Could not parse TLV"); + goto error; + } + + for (tlv_t *t=response->head; t; t=t->next) { + pw_log_info("tlv: parsed type %d value (%zu bytes)", t->type, t->size); + } + + error = tlv_get_value(response, AP2_TLVType_Error); + if (error) { + if (error->value[0] == AP2_TLVError_Authentication) + pw_log_error("tlv: Device returned an authentication failure"); + else if (error->value[0] == AP2_TLVError_Backoff) + pw_log_error("tlv: Device told us to back off pairing attempts"); + else if (error->value[0] == AP2_TLVError_MaxPeers) + pw_log_error("tlv: Max peers trying to connect to device"); + else if (error->value[0] == AP2_TLVError_MaxTries) + pw_log_error("tlv: Max pairing attempts reached"); + else if (error->value[0] == AP2_TLVError_Unavailable) + pw_log_error("tlv: Device is unavailable at this time"); + else + pw_log_error("tlv: Device is busy/returned unknown error"); + + goto error; + } + + return response; + +error: + tlv_free(response); + return NULL; +} + +static int rtsp_ap2_pair_setup2_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) +{ + struct impl *impl = data; + tlv_values_t *response; + tlv_t *proof; + const uint8_t *session_key; + int session_key_len; + uint32_t content_length; + const char *str; + + pw_log_info("pair-setup (req 2) status: %d", status); + + if ((str = spa_dict_lookup(headers, "Content-Length")) == NULL) { + pw_log_error("pair-setup (req 2) res: Missing Content-Length"); + return -1; + } + + if (!spa_atou32(str, &content_length, 0)) { + pw_log_error("pair-setup (req 2) res: Could not read Content-Length"); + return -1; + } + pw_log_debug("pair-setup (req 2) res: Content-Length: %s", str); + + response = tlv_message_process((const uint8_t *)content->data, (size_t)content_length); + if (!response) { + pw_log_error("pair-setup (req 2) res: Received an error"); + return -1; + } + + proof = tlv_get_value(response, AP2_TLVType_Proof); + if (!proof || proof->size != SHA512_DIGEST_LENGTH) { + pw_log_error("pair-setup (req 2) res: Missing or invalid proof"); + goto error; + } + + impl->M2_len = proof->size; + impl->M2 = malloc(impl->M2_len); + memcpy(impl->M2, proof->value, impl->M2_len); + + // Check M2 + srp_user_verify_session(impl->srp_user, (const unsigned char *)impl->M2); + if (!srp_user_is_authenticated(impl->srp_user)) { + pw_log_error("pair-setup (req 2) res: Server authentication failed"); + goto error; + } + + session_key = srp_user_get_session_key(impl->srp_user, &session_key_len); + if (!session_key) { + pw_log_error("pair-setup (req 2) res: Could not compute session key"); + goto error; + } + + if (sizeof(impl->shared_secret) < (unsigned long int) session_key_len) { + pw_log_error("pair-setup (req 2) res: invalid session key"); + goto error; + } + + memcpy(impl->shared_secret, session_key, session_key_len); + impl->shared_secret_len = session_key_len; + + pw_log_info("received shared secret"); + + tlv_free(response); + // TODO + pw_properties_set(impl->headers, "X-Apple-HKP", NULL); + + //return rtsp_do_setup(impl); + return 0; + +error: + tlv_free(response); + return -1; +} + +static int rtsp_do_ap2_pair_setup2(struct impl *impl) +{ + tlv_values_t *request; + uint8_t *data; + size_t data_len = AP_REQUEST_BUFSIZE; + const char *auth_username = NULL; + int ret; + // TLV Pairing Message State + uint8_t state = 0x03; + + data = malloc(data_len); + request = tlv_new(); + + // Calculate A + srp_user_start_authentication(impl->srp_user, &auth_username, &impl->pkA, &impl->pkA_len); + + // Calculate M1 (client proof) + srp_user_process_challenge(impl->srp_user, impl->salt, impl->salt_len, impl->pkB, impl->pkB_len, &impl->M1, &impl->M1_len); + + tlv_add_value(request, AP2_TLVType_State, &state, sizeof(state)); + tlv_add_value(request, AP2_TLVType_PublicKey, impl->pkA, impl->pkA_len); + tlv_add_value(request, AP2_TLVType_Proof, impl->M1, impl->M1_len); + + ret = tlv_format(request, data, &data_len); + if (ret < 0) { + pw_log_error("pair-setup 2: tlv_format returned an error"); + goto error; + } + + for (tlv_t *t=request->head; t; t=t->next) { + pw_log_info("pair-setup 2: sending TLV type %d (bytes: %zu)", t->type, t->size); + } + + tlv_free(request); + + return pw_rtsp_client_url_send(impl->rtsp, "/pair-setup", "POST", &impl->headers->dict, + "application/octet-stream", data, data_len, + rtsp_ap2_pair_setup2_reply, impl); + +error: + tlv_free(request); + free(data); + return -1; +} + +static int rtsp_ap2_pair_setup1_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) +{ + struct impl *impl = data; + uint32_t content_length; + const char *len_str; + tlv_values_t *response; + tlv_t *pk; + tlv_t *salt; + + pw_log_info("pair-setup (req 1) status: %d", status); + + if ((len_str = spa_dict_lookup(headers, "Content-Length")) == NULL) { + pw_log_error("pair-setup (req 1) res: Missing Content-Length header"); + return -1; + } + + if (!spa_atou32(len_str, &content_length, 0)) { + pw_log_error("pair-setup (req 1) res: Could not read Content-Length header"); + return -1; + } + pw_log_info("pair-setup (req 1) content len: %d", content_length); + + response = tlv_message_process((const uint8_t *)content->data, (size_t)content_length); + if (!response) { + pw_log_error("pair-setup (req 1) res: message process error"); + return -1; + } + + pk = tlv_get_value(response, AP2_TLVType_PublicKey); + if (!pk) { + pw_log_error("pair-setup (req 1) res: missing public key"); + goto error; + } + if (pk->size > (long unsigned int) get_N_len(SRP_NG_3072)) { + pw_log_error("pair-setup (req 1) res: invalid public key"); + goto error; + } + + salt = tlv_get_value(response, AP2_TLVType_Salt); + if (!salt) { + pw_log_error("pair-setup (req 1) res: missing salt"); + goto error; + } + if (salt->size != 16) { + pw_log_error("pair-setup (req 1) res: invalid salt"); + goto error; + } + + impl->pkB_len = pk->size; + impl->pkB = malloc(impl->pkB_len); + memcpy(impl->pkB, pk->value, impl->pkB_len); + + impl->salt_len = salt->size; + impl->salt = malloc(impl->salt_len); + memcpy(impl->salt, salt->value, impl->salt_len); + + tlv_free(response); + + return rtsp_do_ap2_pair_setup2(impl); + +error: + tlv_free(response); + return -1; +} + +static int rtsp_do_ap2_pair_setup1(struct impl *impl) +{ + tlv_values_t *request; + uint8_t *data; + size_t data_len; + // TLV Pairing Method: Pair Setup + uint8_t method = 0x00; + // TLV Pairing Flags: Transient + uint8_t flags = 0x10; + // TLV Pairing Message State + uint8_t state = 0x01; + char *pin = "3939"; + int ret; + + data_len = AP_REQUEST_BUFSIZE; + data = malloc(data_len); + request = tlv_new(); + + impl->srp_user = srp_user_new(SRP_SHA512, SRP_NG_3072, AP_SRP_USER_NAME, (unsigned char *)pin, strlen(pin), 0, 0); + if (!impl->srp_user) { + pw_log_error("pair-setup 1: SRP user creation failed"); + goto error; + } + + tlv_add_value(request, AP2_TLVType_State, &state, sizeof(state)); + tlv_add_value(request, AP2_TLVType_Method, &method, sizeof(method)); + tlv_add_value(request, AP2_TLVType_Flags, &flags, sizeof(flags)); + + ret = tlv_format(request, data, &data_len); + if (ret < 0) { + pw_log_error("pair-setup 1: tlv_format returned an error"); + goto error; + } + + for (tlv_t *t=request->head; t; t=t->next) { + pw_log_info("pair-setup 1: sending TLV type %d value (%zu bytes)", t->type, t->size); + } + + tlv_free(request); + + pw_properties_set(impl->headers, "X-Apple-HKP", "4"); + + return pw_rtsp_client_url_send(impl->rtsp, "/pair-setup", "POST", &impl->headers->dict, + "application/octet-stream", (unsigned char *)data, data_len, + rtsp_ap2_pair_setup1_reply, impl); +error: + tlv_free(request); + free(data); + return -1; +} + static int rtsp_raop_options_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) { struct impl *impl = data; @@ -1409,30 +1722,51 @@ static void rtsp_connected(void *data) { struct impl *impl = data; uint32_t sci[2]; - uint8_t rac[16]; - char sac[16*4]; - int res; pw_log_info("connected"); impl->connected = true; - if ((res = pw_getrandom(sci, sizeof(sci), 0)) < 0 || - (res = pw_getrandom(rac, sizeof(rac), 0)) < 0) { - pw_log_error("error generating random data: %s", spa_strerror(res)); + // TODO: Do a GET /info first + + if (pw_getrandom(sci, sizeof(sci), 0) < 0) { + pw_log_error("error generating random data: %m"); return; } pw_properties_setf(impl->headers, "Client-Instance", - "%08x%08x", sci[0], sci[1]); + "%08X%08X", sci[0], sci[1]); - base64_encode(rac, sizeof(rac), sac, '\0'); - pw_properties_set(impl->headers, "Apple-Challenge", sac); + pw_properties_setf(impl->headers, "DACP-ID", + "%08X%08X", sci[0], sci[1]); - pw_properties_set(impl->headers, "User-Agent", DEFAULT_USER_AGENT); + switch (impl->encryption) { + case CRYPTO_PAIR_TRANSIENT: + pw_properties_set(impl->headers, "User-Agent", AP_USER_AGENT); - pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict, + rtsp_do_ap2_pair_setup1(impl); + break; + default: + uint8_t rac[16]; + char sac[16*4]; + + if (pw_getrandom(rac, sizeof(rac), 0) < 0) { + pw_log_error("error generating random data: %m"); + return; + } + + base64_encode(rac, sizeof(rac), sac, '\0'); + pw_properties_set(impl->headers, "Apple-Challenge", sac); + + pw_properties_set(impl->headers, "User-Agent", DEFAULT_USER_AGENT); + + pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict, NULL, NULL, rtsp_raop_options_reply, impl); + + break; + } + + return; } static void connection_cleanup(struct impl *impl) @@ -1888,6 +2222,9 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_log_error( "can't create cipher context: %m"); goto error; } + + impl->srp_user = NULL; + if (args == NULL) args = ""; @@ -1992,6 +2329,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->encryption = CRYPTO_RSA; else if (spa_streq(str, "auth_setup")) impl->encryption = CRYPTO_AUTH_SETUP; + else if (spa_streq(str, "pair_setup")) + impl->encryption = CRYPTO_PAIR_TRANSIENT; else { pw_log_error( "can't handle encryption type %s", str); res = -EINVAL;