diff --git a/meson.build b/meson.build index 9ef732b32..24de87e1e 100644 --- a/meson.build +++ b/meson.build @@ -275,6 +275,7 @@ summary({'dbus (Bluetooth, rt, portal, pw-reserve)': dbus_dep.found()}, bool_yn: cdata.set('HAVE_DBUS', dbus_dep.found()) sdl_dep = dependency('sdl2', required : get_option('sdl2')) summary({'SDL2 (video examples)': sdl_dep.found()}, bool_yn: true, section: 'Misc dependencies') +plist_lib = dependency('libplist-2.0', required: get_option('raop')) drm_dep = dependency('libdrm', required : false) readline_dep = dependency('readline', required : get_option('readline')) diff --git a/src/modules/meson.build b/src/modules/meson.build index a48ac3cbd..669ea83b2 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -612,7 +612,7 @@ if build_module_raop install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, openssl_lib], + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, openssl_lib, plist_lib], ) endif summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules') diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c index 93c17a705..ac53d5458 100644 --- a/src/modules/module-raop-sink.c +++ b/src/modules/module-raop-sink.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include #if OPENSSL_API_LEVEL >= 30000 @@ -29,6 +31,7 @@ #include #include #include +#include #include "config.h" @@ -145,12 +148,18 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #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 DEFAULT_USER_NAME "PipeWire" #define AP_SRP_USER_NAME "Pair-Setup" +#define AP_CONTROL_SALT "Control-Salt" +#define AP_CONTROL_ENC_INFO "Control-Write-Encryption-Key" +#define AP_CONTROL_DEC_INFO "Control-Read-Encryption-Key" +#define AP_NONCE_LENGTH 12 +#define AP_AUTHTAG_LENGTH 16 +#define AP_ENCRYPTED_BLOCK_LENGTH_MAX 1024 +#define AP_REQUEST_BUFSIZE 4096 +// For transient pairing the key_len will be 64 bytes, but only 32 are used for +// audio payload encryption. For normal pairing the key is 32 bytes. +#define AP_AUDIO_KEY_LEN 32 #define MAX_PORT_RETRY 128 @@ -261,6 +270,8 @@ struct impl { uint8_t shared_secret[64]; size_t shared_secret_len; // Will be 32 (normal) or 64 (transient) + //struct pw_rtsp_cipher_context *control_cipher_ctx; + uint16_t control_port; int control_fd; struct spa_source *control_source; @@ -445,6 +456,9 @@ static int flush_to_udp_packet(struct impl *impl) } if (impl->encryption == CRYPTO_RSA) aes_encrypt(impl, dst, len); + // TODO lorbus + //else if (impl->encryption == CRYPTO_PAIR_TRANSIENT) + // rtp_ap_encrypt(impl, dst, len, impl->shared_secret, AP_AUDIO_KEY_LEN, NULL); impl->rtptime += n_frames; impl->seq = (impl->seq + 1) & 0xffff; @@ -1087,9 +1101,273 @@ static int rtsp_setup_reply(void *data, int status, const struct spa_dict *heade return 0; } +static void wplist_dict_add_uint(plist_t node, const char *key, uint64_t val) +{ + plist_t add = plist_new_uint(val); + plist_dict_set_item(node, key, add); +} + +static void wplist_dict_add_string(plist_t node, const char *key, const char *val) +{ + plist_t add = plist_new_string(val); + plist_dict_set_item(node, key, add); +} + +static void wplist_dict_add_bool(plist_t node, const char *key, bool val) +{ + plist_t add = plist_new_bool(val); + plist_dict_set_item(node, key, add); +} + +static void wplist_dict_add_data(plist_t node, const char *key, uint8_t *data, size_t len) +{ + plist_t add = plist_new_data((const char *)data, len); + plist_dict_set_item(node, key, add); +} + +static int +wplist_to_bin(uint8_t **data, size_t *len, plist_t node) +{ + char *out = NULL; + uint32_t out_len = 0; + + plist_to_bin(node, &out, &out_len); + if (!out) + return -1; + + *data = (uint8_t *)out; + *len = out_len; + + return 0; +} + +static int +wplist_from_bin(plist_t *node, const struct pw_array *buf) +{ + plist_t out = NULL; + + plist_from_bin((char *)buf->data, (uint32_t)buf->size, &out); + if (!out) + return -1; + + *node = out; + + return 0; +} + +// Executes SHA512 RFC 5869 extract + expand, writing a derived key to okm +static int +hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm_len, const char *salt, const char *info) +{ + EVP_PKEY_CTX *pctx; + + if (okm_len > SHA512_DIGEST_LENGTH) + return -1; + if (! (pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL))) + return -1; + if (EVP_PKEY_derive_init(pctx) <= 0) + goto error; + if (EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha512()) <= 0) + goto error; + if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, (const unsigned char *)salt, strlen(salt)) <= 0) + goto error; + if (EVP_PKEY_CTX_set1_hkdf_key(pctx, ikm, ikm_len) <= 0) + goto error; + if (EVP_PKEY_CTX_add1_hkdf_info(pctx, (const unsigned char *)info, strlen(info)) <= 0) + goto error; + if (EVP_PKEY_derive(pctx, okm, &okm_len) <= 0) + goto error; + EVP_PKEY_CTX_free(pctx); + return 0; + error: + EVP_PKEY_CTX_free(pctx); + return -1; +} +/* +static pw_rtsp_cipher_context *ap_cipher_context_new(const uint8_t *shared_secret, size_t shared_secret_len, const char *enc_salt, const char *enc_info, const char *dec_salt, const char *dec_info) +{ + struct pw_rtsp_cipher_context *cctx; + int ret; + + cctx = calloc(1, sizeof(struct pw_rtsp_cipher_context)); + if (!cctx) + goto error; + ret = hkdf_extract_expand(cctx->encryption_key, sizeof(cctx->encryption_key), shared_secret, shared_secret_len, enc_salt, enc_info); + if (ret < 0) + goto error; + ret = hkdf_extract_expand(cctx->decryption_key, sizeof(cctx->decryption_key), shared_secret, shared_secret_len, dec_salt, dec_info); + if (ret < 0) + goto error; + return cctx; +error: + free(cctx); + return NULL; +} + +static int chacha20_poly1305_encrypt(uint8_t *cipher, const uint8_t *plain, size_t plain_len, + const uint8_t *key, size_t key_len, const void *ad, size_t ad_len, + uint8_t *tag, size_t tag_len, const uint8_t nonce[AP_NONCE_LENGTH]) { + EVP_CIPHER_CTX *ctx; + int len; + + if (!(ctx = EVP_CIPHER_CTX_new())) + return -1; + if (EVP_EncryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1) + goto error; + if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary + goto error; + if (ad_len > 0 && EVP_EncryptUpdate(ctx, NULL, &len, ad, ad_len) != 1) + goto error; + if (EVP_EncryptUpdate(ctx, cipher, &len, plain, plain_len) != 1) + goto error; + assert((size_t)len == plain_len); + if (EVP_EncryptFinal_ex(ctx, NULL, &len) != 1) + goto error; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, tag_len, tag) != 1) + goto error; + EVP_CIPHER_CTX_free(ctx); + return 0; +error: + EVP_CIPHER_CTX_free(ctx); + return -1; +} + +static ssize_t ap_encrypt(uint8_t **ciphertext, size_t *ciphertext_len, + const uint8_t *plaintext, size_t plaintext_len, + struct pw_rtsp_cipher_context *cctx) +{ + uint8_t nonce[AP_NONCE_LENGTH] = { 0 }; + uint8_t tag[AP_AUTHTAG_LENGTH]; + const uint8_t *plain_block; + uint8_t *cipher_block; + uint16_t block_len; + int nblocks; + int ret; + int i; + + if (plaintext_len == 0 || !plaintext) + return -1; + + // Encryption is done in blocks, where each block consists of a short, the + // encrypted data and an auth tag. The short is the size of the encrypted + // data. The encrypted data in the block cannot exceed AP_ENCRYPTED_BLOCK_LENGTH_MAX == 1024. + nblocks = 1 + ((plaintext_len - 1) / AP_ENCRYPTED_BLOCK_LENGTH_MAX); // Ceiling of division + + *ciphertext_len = nblocks * (sizeof(block_len) + AP_AUTHTAG_LENGTH) + plaintext_len; + *ciphertext = malloc(*ciphertext_len); + + cctx->encryption_counter_prev = cctx->encryption_counter; + + for (i = 0, plain_block = plaintext, cipher_block = *ciphertext; i < nblocks; i++) { + // If it is the last block we will encrypt only the remaining data + block_len = (i + 1 == nblocks) ? (plaintext + plaintext_len - plain_block) : AP_ENCRYPTED_BLOCK_LENGTH_MAX; + + memcpy(nonce + 4, &(cctx->encryption_counter), sizeof(cctx->encryption_counter));// TODO BE or LE? + + // Write the ciphered block + memcpy(cipher_block, &block_len, sizeof(block_len)); // TODO BE or LE? + ret = chacha20_poly1305_encrypt(cipher_block + sizeof(block_len), + plain_block, block_len, cctx->encryption_key, sizeof(cctx->encryption_key), + &block_len, sizeof(block_len), tag, sizeof(tag), nonce); + if (ret < 0) { + pw_log_error("Encryption with chacha poly1305 failed"); + cctx->encryption_counter = cctx->encryption_counter_prev; + free(*ciphertext); + return -1; + } + memcpy(cipher_block + sizeof(block_len) + block_len, tag, AP_AUTHTAG_LENGTH); + + plain_block += block_len; + cipher_block += block_len + sizeof(block_len) + AP_AUTHTAG_LENGTH; + cctx->encryption_counter++; + } + + return plain_block - plaintext; +} + + +static int chacha20_poly1305_decrypt(uint8_t *plain, const uint8_t *cipher, size_t cipher_len, + const uint8_t *key, size_t key_len, const void *ad, size_t ad_len, + uint8_t *tag, size_t tag_len, const uint8_t nonce[AP_NONCE_LENGTH]) +{ + EVP_CIPHER_CTX *ctx; + int len; + + if (! (ctx = EVP_CIPHER_CTX_new())) + return -1; + if (EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1) + goto error; + if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary + goto error; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag) != 1) + goto error; + if (ad_len > 0 && EVP_DecryptUpdate(ctx, NULL, &len, ad, ad_len) != 1) + goto error; + if (EVP_DecryptUpdate(ctx, plain, &len, cipher, cipher_len) != 1) + goto error; + if (EVP_DecryptFinal_ex(ctx, NULL, &len) != 1) + goto error; + EVP_CIPHER_CTX_free(ctx); + return 0; +error: + EVP_CIPHER_CTX_free(ctx); + return -1; +} + +static ssize_t ap_decrypt(uint8_t **plaintext, size_t *plaintext_len, + const uint8_t *ciphertext, size_t ciphertext_len, + struct pw_rtsp_cipher_context *cctx) +{ + uint8_t nonce[AP_NONCE_LENGTH] = { 0 }; + uint8_t tag[AP_AUTHTAG_LENGTH]; + uint8_t *plain_block; + const uint8_t *cipher_block; + uint16_t block_len; + int ret; + + if (ciphertext_len < sizeof(block_len) || !ciphertext) + return -1; + + // This will allocate more than we need. Since we don't know the number of + // blocks in the ciphertext yet we can't calculate the exact required length. + *plaintext = malloc(ciphertext_len); + + cctx->decryption_counter_prev = cctx->decryption_counter; + + for (plain_block = *plaintext, cipher_block = ciphertext; cipher_block < ciphertext + ciphertext_len; ) { + memcpy(&block_len, cipher_block, sizeof(block_len)); // TODO BE or LE? + if (cipher_block + block_len + sizeof(block_len) + AP_AUTHTAG_LENGTH > ciphertext + ciphertext_len) { + // The remaining ciphertext doesn't contain an entire block, so stop + break; + } + + memcpy(tag, cipher_block + sizeof(block_len) + block_len, sizeof(tag)); + memcpy(nonce + 4, &(cctx->decryption_counter), sizeof(cctx->decryption_counter));// TODO BE or LE? + + ret = chacha20_poly1305_decrypt(plain_block, cipher_block + sizeof(block_len), block_len, cctx->decryption_key, sizeof(cctx->decryption_key), &block_len, sizeof(block_len), tag, sizeof(tag), nonce); + if (ret < 0) { + pw_log_error("Decryption with chacha poly1305 failed"); + cctx->decryption_counter = cctx->decryption_counter_prev; + free(*plaintext); + return -1; + } + + plain_block += block_len; + cipher_block += block_len + sizeof(block_len) + AP_AUTHTAG_LENGTH; + cctx->decryption_counter++; + } + + *plaintext_len = plain_block - *plaintext; + + return cipher_block - ciphertext; +} +*/ static int rtsp_do_setup(struct impl *impl) { int res; + uint8_t *content = NULL; + size_t content_len = 0; + plist_t root_plist = NULL; switch (impl->protocol) { case PROTO_TCP: @@ -1109,10 +1387,59 @@ static int rtsp_do_setup(struct impl *impl) impl->timing_source = pw_loop_add_io(impl->loop, impl->timing_fd, SPA_IO_IN, false, on_timing_source_io, impl); - pw_properties_setf(impl->headers, "Transport", - "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;" - "control_port=%u;timing_port=%u", - impl->control_port, impl->timing_port); + switch (impl->protocol) { + case CRYPTO_PAIR_TRANSIENT: + // Encryption/decryption of control channel + const char *control_salt = AP_CONTROL_SALT; + const char *enc_info = AP_CONTROL_ENC_INFO; + const char *dec_info = AP_CONTROL_DEC_INFO; + + plist_t stream = plist_new_dict(); + wplist_dict_add_uint(stream, "audioFormat", 0x40000); // 0x40000 ALAC/44100/16/2 + wplist_dict_add_string(stream, "audioMode", "default"); + wplist_dict_add_uint(stream, "controlPort", impl->control_port); + wplist_dict_add_uint(stream, "ct", impl->codec); // Compression type, 1 LPCM, 2 ALAC, 3 AAC, 4 AAC ELD, 32 OPUS + wplist_dict_add_bool(stream, "isMedia", true); // ? + //wplist_dict_add_uint(stream, "latencyMax", 88200); // TODO how do these latencys work? + wplist_dict_add_uint(stream, "latencyMin", RAOP_LATENCY_MIN); + wplist_dict_add_data(stream, "shk", impl->shared_secret, impl->shared_secret_len); + wplist_dict_add_uint(stream, "spf", FRAMES_PER_UDP_PACKET); + wplist_dict_add_uint(stream, "sr", impl->info.rate); + wplist_dict_add_uint(stream, "type", 0x60); // RTP type: 0x60 = 96 realtime, 103 buffered + wplist_dict_add_bool(stream, "supportsDynamicStreamID", false); + wplist_dict_add_uint(stream, "streamConnectionID", (uint64_t)impl->session_id); + plist_t streams = plist_new_array(); + plist_array_append_item(streams, stream); + + root_plist = plist_new_dict(); + plist_dict_set_item(root_plist, "streams", streams); + content = malloc(content_len); + wplist_to_bin(&content, &content_len, root_plist); + + const char *url = pw_rtsp_client_get_url(impl->rtsp); + + //impl->control_cipher_ctx = ap_cipher_context_new(impl->shared_secret, impl->shared_secret_len, control_salt, enc_info, control_salt, dec_info); + //if (!impl->control_cipher_ctx) { + // pw_log_error("Could not create control cipher"); + // goto error; + //} + //struct pw_rtsp_cipher *cipher = ap_cipher_new(ap_decrypt, ap_encrypt); + + res = pw_rtsp_client_url_send(impl->rtsp, url, "SETUP", &impl->headers->dict, + "application/x-apple-binary-plist", content, content_len, + rtsp_setup_reply, impl); + + plist_free(root_plist); + free(content); + + return res; + default: + pw_properties_setf(impl->headers, "Transport", + "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;" + "control_port=%u;timing_port=%u", + impl->control_port, impl->timing_port); + } + break; default: @@ -1413,7 +1740,7 @@ 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) +static tlv_values_t *tlv_message_process(const uint8_t *data, size_t data_len) { tlv_values_t *response; tlv_t *error; @@ -1718,53 +2045,117 @@ static int rtsp_raop_options_reply(void *data, int status, const struct spa_dict return res; } +static void rtsp_do_raop_options(void *data) +{ + struct impl *impl = data; + 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_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict, + NULL, NULL, rtsp_raop_options_reply, impl); + + return; +} + +static int rtsp_get_info_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) +{ + struct impl *impl = data; + int res; + + pw_log_info("info status: %d", status); + plist_t info; + res = wplist_from_bin(&info, content); + if (res < 0) + return res; + + plist_t features_plist = plist_dict_get_item(info, "features"); + if (!features_plist) + return -1; + + uint64_t features; + plist_get_uint_val(features_plist, &features); + pw_log_info("features value: 0x%" PRIx64 "\n", features); + + plist_t status_flags_plist = plist_dict_get_item(info, "statusFlags"); + if (!status_flags_plist) + return -1; + + uint64_t status_flags; + plist_get_uint_val(status_flags_plist, &status_flags); + pw_log_info("statusFlags value: 0x%" PRIx64 "\n", status_flags); + + switch (impl->encryption) { + case CRYPTO_PAIR_TRANSIENT: + rtsp_do_ap2_pair_setup1(impl); + break; + default: + rtsp_do_raop_options(impl); + break; + } + return 0; +} + static void rtsp_connected(void *data) { struct impl *impl = data; uint32_t sci[2]; + //plist_t txt, qualifier = plist_new_array(), root_plist = plist_new_dict(); pw_log_info("connected"); impl->connected = true; - // 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]); - - pw_properties_setf(impl->headers, "DACP-ID", - "%08X%08X", sci[0], sci[1]); - switch (impl->encryption) { case CRYPTO_PAIR_TRANSIENT: - pw_properties_set(impl->headers, "User-Agent", AP_USER_AGENT); + pw_properties_setf(impl->headers, "DACP-ID", + "%08X%08X", sci[0], sci[1]); + pw_properties_setf(impl->headers, "User-Agent", "PipeWire/%s", pw_get_headers_version()); + + //txt = plist_new_string("txtAirPlay"); - 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; - } + pw_properties_setf(impl->headers, "Client-Instance", + "%08X%08X", sci[0], sci[1]); - 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); + //txt = plist_new_string("txtRAOP"); break; } + /* + plist_array_append_item(qualifier, txt); + plist_dict_set_item(root_plist, "qualifier", qualifier); + size_t content_len = 0; + uint8_t *content = malloc(content_len); + wplist_to_bin(&content, &content_len, root_plist); + + pw_rtsp_client_url_send(impl->rtsp, "/info", "GET", &impl->headers->dict, + "application/x-apple-binary-plist", content, content_len, + rtsp_get_info_reply, impl); + + plist_free(root_plist); + free(content); + */ + + pw_properties_setf(impl->headers, "User-Agent", "PipeWire/%s", pw_get_headers_version()); + + pw_rtsp_client_url_send(impl->rtsp, "/info", "GET", &impl->headers->dict, + NULL, NULL, 0, + rtsp_get_info_reply, impl); return; } diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c index 7e4b061c1..c8e81e0a0 100644 --- a/src/modules/module-raop/rtsp-client.c +++ b/src/modules/module-raop/rtsp-client.c @@ -243,7 +243,7 @@ static int process_status(struct pw_rtsp_client *client, char *buf) const char *state = NULL, *s; size_t len; - pw_log_info("status: %s", buf); + pw_log_info("processing status: %s", buf); s = pw_split_walk(buf, " ", &len, &state); if (!spa_strstartswith(s, "RTSP/")) @@ -355,6 +355,7 @@ static int process_content(struct pw_rtsp_client *client) spa_assert((size_t) res <= client->content_length); client->content_length -= res; + pw_log_info("processing content (%ld bytes):\n%s", client->content.size, client->content.data); } if (client->content_length == 0)