From a9e3459c71f34c536247baa086d5a58277d7b94a Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sun, 30 Jul 2023 00:41:15 +0200 Subject: [PATCH 1/8] module-raop-sink: Rename a bunch of things --- src/modules/module-raop-sink.c | 67 +++++++++++++++++----------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c index 6aeace5d6..6d732fc0f 100644 --- a/src/modules/module-raop-sink.c +++ b/src/modules/module-raop-sink.c @@ -228,8 +228,8 @@ struct impl { unsigned int do_disconnect:1; - uint8_t key[AES_CHUNK_SIZE]; /* Key for aes-cbc */ - uint8_t iv[AES_CHUNK_SIZE]; /* Initialization vector for cbc */ + uint8_t aes_key[AES_CHUNK_SIZE]; /* Key for aes-cbc */ + uint8_t aes_iv[AES_CHUNK_SIZE]; /* Initialization vector for cbc */ EVP_CIPHER_CTX *ctx; uint16_t control_port; @@ -287,7 +287,7 @@ static inline void bit_writer(uint8_t **p, int *pos, uint8_t data, int len) static int aes_encrypt(struct impl *impl, uint8_t *data, int len) { int i = len & ~0xf, clen = i; - EVP_EncryptInit(impl->ctx, EVP_aes_128_cbc(), impl->key, impl->iv); + EVP_EncryptInit(impl->ctx, EVP_aes_128_cbc(), impl->aes_key, impl->aes_iv); EVP_EncryptUpdate(impl->ctx, data, &clen, data, i); return i; } @@ -783,7 +783,7 @@ static int MD5_hash(char hash[MD5_HASH_LENGTH+1], const char *fmt, ...) return 0; } -static int rtsp_add_auth(struct impl *impl, const char *method) +static int rtsp_add_raop_auth_header(struct impl *impl, const char *method) { char auth[1024]; @@ -831,7 +831,7 @@ static int rtsp_send(struct impl *impl, const char *method, { int res; - rtsp_add_auth(impl, method); + rtsp_add_raop_auth_header(impl, method); res = pw_rtsp_client_send(impl->rtsp, method, &impl->headers->dict, content_type, content, reply, impl); @@ -1105,7 +1105,7 @@ error: return -EIO; } -static int rtsp_announce_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) +static int rtsp_raop_announce_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) { struct impl *impl = data; @@ -1205,12 +1205,12 @@ error: goto done; } -static int rtsp_do_announce(struct impl *impl) +static int rtsp_do_raop_announce(struct impl *impl) { const char *host; - uint8_t rsakey[512]; - char key[512*2]; - char iv[16*2]; + uint8_t rsa_key[512]; + char rsa_key_b64enc[512*2]; + char rsa_iv_b64enc[16*2]; int res, frames, rsa_len, ip_version; spa_autofree char *sdp = NULL; char local_ip[256]; @@ -1241,7 +1241,6 @@ static int rtsp_do_announce(struct impl *impl) if (!sdp) return -errno; break; - case CRYPTO_AUTH_SETUP: sdp = spa_aprintf("v=0\r\n" "o=iTunes %s 0 IN IP%d %s\r\n" @@ -1260,16 +1259,16 @@ static int rtsp_do_announce(struct impl *impl) break; case CRYPTO_RSA: - if ((res = pw_getrandom(impl->key, sizeof(impl->key), 0)) < 0 || - (res = pw_getrandom(impl->iv, sizeof(impl->iv), 0)) < 0) + if ((res = pw_getrandom(impl->aes_key, sizeof(impl->aes_key), 0)) < 0 || + (res = pw_getrandom(impl->aes_iv, sizeof(impl->aes_iv), 0)) < 0) return res; - rsa_len = rsa_encrypt(impl->key, 16, rsakey); + rsa_len = rsa_encrypt(impl->aes_key, 16, rsa_key); if (rsa_len < 0) return -rsa_len; - base64_encode(rsakey, rsa_len, key, '='); - base64_encode(impl->iv, 16, iv, '='); + base64_encode(rsa_key, rsa_len, rsa_key_b64enc, '='); + base64_encode(impl->aes_iv, 16, rsa_iv_b64enc, '='); sdp = spa_aprintf("v=0\r\n" "o=iTunes %s 0 IN IP%d %s\r\n" @@ -1283,7 +1282,7 @@ static int rtsp_do_announce(struct impl *impl) "a=aesiv:%s\r\n", impl->session_id, ip_version, local_ip, ip_version, host, frames, impl->info.rate, - key, iv); + rsa_key_b64enc, rsa_iv_b64enc); if (!sdp) return -errno; break; @@ -1291,19 +1290,19 @@ static int rtsp_do_announce(struct impl *impl) return -ENOTSUP; } - return rtsp_send(impl, "ANNOUNCE", "application/sdp", sdp, rtsp_announce_reply); + return rtsp_send(impl, "ANNOUNCE", "application/sdp", sdp, rtsp_raop_announce_reply); } -static int rtsp_auth_setup_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) +static int rtsp_raop_auth_setup_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) { struct impl *impl = data; pw_log_info("auth-setup status: %d", status); - return rtsp_do_announce(impl); + return rtsp_do_raop_announce(impl); } -static int rtsp_do_auth_setup(struct impl *impl) +static int rtsp_do_raop_auth_setup(struct impl *impl) { static const unsigned char content[33] = "\x01" @@ -1312,22 +1311,22 @@ static int rtsp_do_auth_setup(struct impl *impl) return pw_rtsp_client_url_send(impl->rtsp, "/auth-setup", "POST", &impl->headers->dict, "application/octet-stream", content, sizeof(content), - rtsp_auth_setup_reply, impl); + rtsp_raop_auth_setup_reply, impl); } -static int rtsp_auth_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) +static int rtsp_raop_auth_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) { struct impl *impl = data; int res = 0; - pw_log_info("auth status: %d", status); + pw_log_info("raop auth status: %d", status); switch (status) { case 200: if (impl->encryption == CRYPTO_AUTH_SETUP) - res = rtsp_do_auth_setup(impl); + res = rtsp_do_raop_auth_setup(impl); else - res = rtsp_do_announce(impl); + res = rtsp_do_raop_announce(impl); break; } return res; @@ -1351,7 +1350,7 @@ static const char *find_attr(char **tokens, const char *key) return NULL; } -static int rtsp_do_auth(struct impl *impl, const struct spa_dict *headers) +static int rtsp_do_raop_auth(struct impl *impl, const struct spa_dict *headers) { const char *str, *realm, *nonce; int n_tokens; @@ -1382,10 +1381,10 @@ static int rtsp_do_auth(struct impl *impl, const struct spa_dict *headers) impl->nonce = strdup(nonce); } - return rtsp_send(impl, "OPTIONS", NULL, NULL, rtsp_auth_reply); + return rtsp_send(impl, "OPTIONS", NULL, NULL, rtsp_raop_auth_reply); } -static int rtsp_options_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) +static int rtsp_raop_options_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) { struct impl *impl = data; int res = 0; @@ -1394,13 +1393,13 @@ static int rtsp_options_reply(void *data, int status, const struct spa_dict *hea switch (status) { case 401: - res = rtsp_do_auth(impl, headers); + res = rtsp_do_raop_auth(impl, headers); break; case 200: if (impl->encryption == CRYPTO_AUTH_SETUP) - res = rtsp_do_auth_setup(impl); + res = rtsp_do_raop_auth_setup(impl); else - res = rtsp_do_announce(impl); + res = rtsp_do_raop_announce(impl); break; } return res; @@ -1433,7 +1432,7 @@ static void rtsp_connected(void *data) pw_properties_set(impl->headers, "User-Agent", DEFAULT_USER_AGENT); pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict, - NULL, NULL, rtsp_options_reply, impl); + NULL, NULL, rtsp_raop_options_reply, impl); } static void connection_cleanup(struct impl *impl) @@ -1530,7 +1529,7 @@ static int rtsp_do_connect(struct impl *impl) if (impl->connected) { if (!impl->ready) - return rtsp_do_announce(impl); + return rtsp_do_raop_announce(impl); return 0; } From 87fcd5454a1183a51e2b9b2060aa991f1cdd19f0 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Wed, 12 Jul 2023 20:14:40 +0200 Subject: [PATCH 2/8] DNM: devel: Enable module-raop-discover --- src/daemon/pipewire.conf.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in index 27051d66e..690e4a810 100644 --- a/src/daemon/pipewire.conf.in +++ b/src/daemon/pipewire.conf.in @@ -168,6 +168,9 @@ context.modules = [ # Provides factories to make session manager objects. { name = libpipewire-module-session-manager } + # Use zeroconf to detect and load module-raop-sink + { name = libpipewire-module-raop-discover } + # Use libcanberra to play X11 Bell { name = libpipewire-module-x11-bell args = { From cc9a4c8ccafc035f185afc5df496041bfe05a6d0 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Wed, 21 Jun 2023 06:03:04 +0200 Subject: [PATCH 3/8] module-raop: Add TLV utils Adds MIT-licensed TLV helper utilities, taken from https://github.com/maximkulkin/esp-homekit and adapted for usage in PipeWire. --- src/modules/meson.build | 3 +- src/modules/module-raop/tlv.c | 183 ++++++++++++++++++++++++++++++++++ src/modules/module-raop/tlv.h | 69 +++++++++++++ 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 src/modules/module-raop/tlv.c create mode 100644 src/modules/module-raop/tlv.h diff --git a/src/modules/meson.build b/src/modules/meson.build index 9e38742cd..22f1cce85 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -605,7 +605,8 @@ build_module_raop = openssl_lib.found() if build_module_raop pipewire_module_raop_sink = shared_library('pipewire-module-raop-sink', [ 'module-raop-sink.c', - 'module-raop/rtsp-client.c' ], + 'module-raop/rtsp-client.c', + 'module-raop/tlv.c' ], include_directories : [configinc], install : true, install_dir : modules_install_dir, diff --git a/src/modules/module-raop/tlv.c b/src/modules/module-raop/tlv.c new file mode 100644 index 000000000..9d3990d5b --- /dev/null +++ b/src/modules/module-raop/tlv.c @@ -0,0 +1,183 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Maxim Kulkin */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include "tlv.h" + +tlv_values_t * tlv_new(void) +{ + tlv_values_t *values = malloc(sizeof(tlv_values_t)); + if (!values) + return NULL; + + values->head = NULL; + return values; +} + +void tlv_free(tlv_values_t *values) +{ + if (!values) + return; + + tlv_t *t = values->head; + while (t) { + tlv_t *t2 = t; + t = t->next; + if (t2->value) + free(t2->value); + free(t2); + } + free(values); +} + +static int tlv_add_value_(tlv_values_t *values, uint8_t type, uint8_t *value, size_t size) +{ + tlv_t *tlv = malloc(sizeof(tlv_t)); + if (!tlv) { + return TLV_ERROR_MEMORY; + } + tlv->type = type; + tlv->size = size; + tlv->value = value; + tlv->next = NULL; + + if (!values->head) { + values->head = tlv; + } else { + tlv_t *t = values->head; + while (t->next) { + t = t->next; + } + t->next = tlv; + } + + return 0; +} + +int tlv_add_value(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 TLV_ERROR_MEMORY; + } + memcpy(data, value, size); + } + ret = tlv_add_value_(values, type, data, size); + if (ret < 0) + free(data); + return ret; +} + +tlv_t * tlv_get_value(const tlv_values_t *values, uint8_t type) +{ + tlv_t *t = values->head; + while (t) { + if (t->type == type) + return t; + t = t->next; + } + return NULL; +} + +int tlv_format(const tlv_values_t *values, uint8_t *buffer, size_t *size) +{ + size_t required_size = 0; + 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 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 tlv_parse(const uint8_t *buffer, size_t length, 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) + return -2; + + data = malloc(size); + if (!data) + return 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; +} diff --git a/src/modules/module-raop/tlv.h b/src/modules/module-raop/tlv.h new file mode 100644 index 000000000..0d7129e11 --- /dev/null +++ b/src/modules/module-raop/tlv.h @@ -0,0 +1,69 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Maxim Kulkin */ +/* SPDX-License-Identifier: MIT */ + +#include + +#define TLV_ERROR_MEMORY -1 +#define TLV_ERROR_INSUFFICIENT_SIZE -2 + +typedef enum { + AP2_TLVType_Method = 0, // (integer) Method to use for pairing. See PairMethod + AP2_TLVType_Identifier = 1, // (UTF-8) Identifier for authentication + AP2_TLVType_Salt = 2, // (bytes) 16+ bytes of random salt + AP2_TLVType_PublicKey = 3, // (bytes) Curve25519, SRP public key or signed Ed25519 key + AP2_TLVType_Proof = 4, // (bytes) Ed25519 or SRP proof + AP2_TLVType_EncryptedData = 5, // (bytes) Encrypted data with auth tag at end + AP2_TLVType_State = 6, // (integer) State of the pairing process. 1=M1, 2=M2, etc. + AP2_TLVType_Error = 7, // (integer) Error code. Must only be present if error code is + // not 0. See AP2_TLVError + AP2_TLVType_RetryDelay = 8, // (integer) Seconds to delay until retrying a setup code + AP2_TLVType_Certificate = 9, // (bytes) X.509 Certificate + AP2_TLVType_Signature = 10, // (bytes) Ed25519 + AP2_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 + AP2_TLVType_FragmentData = 13, // (bytes) Non-last fragment of data. If length is 0, + // it's an ACK. + AP2_TLVType_FragmentLast = 14, // (bytes) Last fragment of data + AP2_TLVType_Flags = 19, // Added from airplay2_receiver + AP2_TLVType_Separator = 0xff, +} AP2_TLVType; + +typedef enum { + AP2_TLVError_Unknown = 1, // Generic error to handle unexpected errors + AP2_TLVError_Authentication = 2, // Setup code or signature verification failed + AP2_TLVError_Backoff = 3, // Client must look at the retry delay TLV item and + // wait that many seconds before retrying + AP2_TLVError_MaxPeers = 4, // Server cannot accept any more pairings + AP2_TLVError_MaxTries = 5, // Server reached its maximum number of + // authentication attempts + AP2_TLVError_Unavailable = 6, // Server pairing method is unavailable + AP2_TLVError_Busy = 7, // Server is busy and cannot accept a pairing + // request at this time +} AP2_TLVError; + +typedef struct _tlv { + struct _tlv *next; + uint8_t type; + uint8_t *value; + size_t size; +} tlv_t; + +typedef struct { + tlv_t *head; +} tlv_values_t; + +tlv_values_t *tlv_new(void); + +void tlv_free(tlv_values_t *values); + +int tlv_add_value(tlv_values_t *values, uint8_t type, const uint8_t *value, size_t size); + +tlv_t *tlv_get_value(const tlv_values_t *values, uint8_t type); + +int tlv_format(const tlv_values_t *values, uint8_t *buffer, size_t *size); + +int tlv_parse(const uint8_t *buffer, size_t length, tlv_values_t *values); From 75eb403d15026521c0878c7675a22c59783da91b Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Wed, 21 Jun 2023 06:15:39 +0200 Subject: [PATCH 4/8] module-raop: Add SRP utils Adds MIT-licensed SRP helper utilities, taken from https://github.com/cocagne/csrp. Extensively adapted for usage in PipeWire. Upgraded to OpenSSL 3. --- src/modules/meson.build | 1 + src/modules/module-raop/srp.c | 437 ++++++++++++++++++++++++++++++++++ src/modules/module-raop/srp.h | 31 +++ 3 files changed, 469 insertions(+) create mode 100644 src/modules/module-raop/srp.c create mode 100644 src/modules/module-raop/srp.h diff --git a/src/modules/meson.build b/src/modules/meson.build index 22f1cce85..a48ac3cbd 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -606,6 +606,7 @@ if build_module_raop pipewire_module_raop_sink = shared_library('pipewire-module-raop-sink', [ 'module-raop-sink.c', 'module-raop/rtsp-client.c', + 'module-raop/srp.c', 'module-raop/tlv.c' ], include_directories : [configinc], install : true, diff --git a/src/modules/module-raop/srp.c b/src/modules/module-raop/srp.c new file mode 100644 index 000000000..39ce9145f --- /dev/null +++ b/src/modules/module-raop/srp.c @@ -0,0 +1,437 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2013 Tom Cocagne */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include + +#include + +#include "srp.h" + +#ifndef SHA512_DIGEST_LENGTH +#define SHA512_DIGEST_LENGTH 64 +#endif + +#ifndef SHA_DIGEST_LENGTH +#define SHA_DIGEST_LENGTH 20 +#endif + +#define SRP_SALT_BYTES 16 + +typedef struct { + int N_len; + BIGNUM *N; + BIGNUM *g; +} NGConstant; + +struct NGHex { + int N_len; + const char *n_hex; + const char *g_hex; +}; + +// These constants here were pulled from Appendix A of RFC 5054 +static struct NGHex Ng_constants[] = { + { /* 2048 */ + 256, + "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" + "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" + "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" + "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" + "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" + "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" + "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", + "2" + }, + { /* 3072 */ + 384, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B" + "139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485" + "B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1F" + "E649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23" + "DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32" + "905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF69558" + "17183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521" + "ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D7" + "1E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B1817" + "7B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82" + "D120A93AD2CAFFFFFFFFFFFFFFFF", + "5" + }, +}; + +static NGConstant *new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) +{ + NGConstant *ng = calloc(1, sizeof(NGConstant)); + + n_hex = Ng_constants[ng_type].n_hex; + g_hex = Ng_constants[ng_type].g_hex; + + BN_hex2bn(&ng->N, n_hex); + BN_hex2bn(&ng->g, g_hex); + + ng->N_len = BN_num_bytes(ng->N); + + assert(ng->N_len == Ng_constants[ng_type].N_len); + + return ng; +} + +int get_N_len(SRP_NGType ng_type) +{ + return Ng_constants[ng_type].N_len; +} + +struct SRPUser { + SRP_HashAlgorithm hash_alg; + NGConstant *ng; + + BIGNUM *a; + BIGNUM *A; + BIGNUM *S; + + const unsigned char *bytes_A; + int len_A; + int authenticated; + + char *username; + unsigned char *password; + int password_len; + + unsigned char M[SHA512_DIGEST_LENGTH]; + unsigned char H_AMK[SHA512_DIGEST_LENGTH]; + unsigned char session_key[SHA512_DIGEST_LENGTH]; + int session_key_len; +}; + +const EVP_MD *hash_func(SRP_HashAlgorithm alg) +{ + switch (alg) { + case SRP_SHA1: + return EVP_sha1(); + case SRP_SHA512: + return EVP_sha512(); + default: + return NULL; + }; +} + +static int hash_length(SRP_HashAlgorithm alg) +{ + switch (alg) { + case SRP_SHA1: + return SHA_DIGEST_LENGTH; + case SRP_SHA512: + return SHA512_DIGEST_LENGTH; + default: + return -1; + }; +} + +static BIGNUM *H_nn_pad(SRP_HashAlgorithm alg, const BIGNUM *n1, const BIGNUM *n2, int padded_len) +{ + unsigned char buff[SHA512_DIGEST_LENGTH]; + int len_n1 = BN_num_bytes(n1); + int len_n2 = BN_num_bytes(n2); + int nbytes = 2 * padded_len; + int offset_n1 = padded_len - len_n1; + int offset_n2 = nbytes - len_n2; + unsigned char *bin = (unsigned char *) malloc(nbytes); + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + pw_log_debug("srp: H_nn_pad"); + + if (!bin) + return 0; + + assert(len_n1 <= padded_len); + assert(len_n2 <= padded_len); + + BN_bn2bin(n1, bin + offset_n1); + BN_bn2bin(n2, bin + offset_n2); + EVP_DigestInit_ex(ctx, hash_func(alg), NULL); + EVP_DigestUpdate(ctx, bin, nbytes); + EVP_DigestFinal_ex(ctx, buff, NULL); + EVP_MD_CTX_free(ctx); + free(bin); + + return BN_bin2bn(buff, hash_length(alg), NULL); +} + +static BIGNUM *calculate_x(SRP_HashAlgorithm alg, const uint8_t *salt, int salt_len, + const char *username, const unsigned char *password, int password_len) +{ + unsigned char x[SHA512_DIGEST_LENGTH]; + EVP_MD_CTX *ctx; + + assert(salt_len == SRP_SALT_BYTES); + + pw_log_debug("srp: calculate_x"); + + ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, hash_func(alg), NULL); + EVP_DigestUpdate(ctx, username, strlen(username)); + EVP_DigestUpdate(ctx, (const uint8_t *) ":", 1); + EVP_DigestUpdate(ctx, password, password_len); + EVP_DigestFinal_ex(ctx, x, NULL); + EVP_MD_CTX_free(ctx); + + ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, hash_func(alg), NULL); + EVP_DigestUpdate(ctx, (const uint8_t*) salt, salt_len); + EVP_DigestUpdate(ctx, x, hash_length(alg)); + EVP_DigestFinal_ex(ctx, x, NULL); + EVP_MD_CTX_free(ctx); + + return BN_bin2bn(x, hash_length(alg), NULL); +} + +static void hash_num(SRP_HashAlgorithm alg, const BIGNUM *n, unsigned char *dest) +{ + int nbytes = BN_num_bytes(n); + unsigned char *bin = (unsigned char *) malloc(nbytes); + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + if (!bin) + return; + + BN_bn2bin(n, bin); + EVP_DigestInit_ex(ctx, hash_func(alg), NULL); + EVP_DigestUpdate(ctx, bin, nbytes); + EVP_DigestFinal_ex(ctx, dest, NULL); + EVP_MD_CTX_free(ctx); + free(bin); +} + +static void calculate_M(SRP_HashAlgorithm alg, NGConstant *ng, unsigned char *dest, const char *I, + const uint8_t *salt, int salt_len, const uint8_t *A, int A_len, const uint8_t *B, int B_len, + const unsigned char *K) +{ + unsigned char H_N[SHA512_DIGEST_LENGTH]; + unsigned char H_g[SHA512_DIGEST_LENGTH]; + unsigned char H_I[SHA512_DIGEST_LENGTH]; + unsigned char H_xor[SHA512_DIGEST_LENGTH]; + EVP_MD_CTX *ctx; + int i = 0; + int hash_len = hash_length(alg); + + pw_log_debug("srp: calculate_M"); + + hash_num(alg, ng->N, H_N); + hash_num(alg, ng->g, H_g); + + ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, hash_func(alg), NULL); + EVP_DigestUpdate(ctx, I, strlen(I)); + EVP_DigestFinal_ex(ctx, H_I, NULL); + EVP_MD_CTX_free(ctx); + + for (i=0; i < hash_len; i++) + H_xor[i] = H_N[i] ^ H_g[i]; + + ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, hash_func(alg), NULL); + + EVP_DigestUpdate(ctx, H_xor, hash_len); + EVP_DigestUpdate(ctx, H_I, hash_len); + EVP_DigestUpdate(ctx, salt, salt_len); + EVP_DigestUpdate(ctx, A, A_len); + EVP_DigestUpdate(ctx, B, B_len); + EVP_DigestUpdate(ctx, K, hash_len); + + EVP_DigestFinal_ex(ctx, dest, NULL); + EVP_MD_CTX_free(ctx); +} + +static void calculate_H_AMK(SRP_HashAlgorithm alg, unsigned char *dest, const uint8_t *A, int A_len, + const unsigned char *M, const unsigned char *K) +{ + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + EVP_DigestInit_ex(ctx, hash_func(alg), NULL); + + EVP_DigestUpdate(ctx, A, A_len); + EVP_DigestUpdate(ctx, M, hash_length(alg)); + EVP_DigestUpdate(ctx, K, hash_length(alg)); + + EVP_DigestFinal_ex(ctx, dest, NULL); + EVP_MD_CTX_free(ctx); +} + +struct SRPUser *srp_user_new(SRP_HashAlgorithm alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_password, int len_password, const char *n_hex, const char *g_hex) +{ + struct SRPUser *usr = (struct SRPUser *) malloc(sizeof(struct SRPUser)); + int ulen = strlen(username) + 1; + + if (!usr) + goto err_exit; + + usr->hash_alg = alg; + usr->ng = new_ng(ng_type, n_hex, g_hex); + + usr->a = BN_new(); + usr->A = BN_new(); + usr->S = BN_new(); + + if (!usr->ng || !usr->a || !usr->A || !usr->S) + goto err_exit; + + usr->username = (char *) malloc(ulen); + usr->password = (unsigned char *) malloc(len_password); + usr->password_len = len_password; + + if (!usr->username || !usr->password) + goto err_exit; + + memcpy(usr->username, username, ulen); + memcpy(usr->password, bytes_password, len_password); + + usr->authenticated = 0; + usr->bytes_A = 0; + usr->len_A = 0; + + return usr; + +err_exit: + if (!usr) + return NULL; + + BN_free(usr->a); + BN_free(usr->A); + BN_free(usr->S); + if (usr->username) + free(usr->username); + if (usr->password) { + memset(usr->password, 0, usr->password_len); + free(usr->password); + } + free(usr); + + return 0; +} + +void srp_user_start_authentication(struct SRPUser *usr, const char **username, const unsigned char **bytes_A, int *len_A) +{ + BN_CTX *bn_ctx = BN_CTX_new(); + + pw_log_debug("srp: srp_user_start_authentication"); + + BN_rand(usr->a, 256, -1, 0); + BN_mod_exp(usr->A, usr->ng->g, usr->a, usr->ng->N, bn_ctx); + + BN_CTX_free(bn_ctx); + + *len_A = BN_num_bytes(usr->A); + *bytes_A = (const unsigned char *) malloc(*len_A); + + if (!*bytes_A) { + pw_log_debug("srp: srp_user_start_authentication something went wrong"); + *len_A = 0; + *bytes_A = 0; + *username = 0; + return; + } + + BN_bn2bin(usr->A, (unsigned char *) *bytes_A); + + usr->bytes_A = *bytes_A; + usr->len_A = *len_A; + *username = usr->username; +} + +void srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_B, int len_B, const unsigned char **bytes_M, int *len_M) +{ + BIGNUM *B = BN_bin2bn(bytes_B, len_B, NULL); + BIGNUM *u = 0; + BIGNUM *x = 0; + BIGNUM *k = 0; + BIGNUM *v = BN_new(); + BIGNUM *tmp1 = BN_new(); + BIGNUM *tmp2 = BN_new(); + BIGNUM *tmp3 = BN_new(); + BN_CTX *bn_ctx = BN_CTX_new(); + + pw_log_debug("srp: srp_user_process_challenge"); + + *len_M = 0; + *bytes_M = 0; + + if (!bytes_s || !B || !v || !tmp1 || !tmp2 || !tmp3 || !bn_ctx) { + pw_log_debug("srp: BIGNUM init failed"); + goto cleanup_and_exit; + } + k = H_nn_pad(usr->hash_alg, usr->ng->N, usr->ng->g, usr->ng->N_len); + if (!k) { + pw_log_debug("srp: couldn't calc k"); + goto cleanup_and_exit; + } + u = H_nn_pad(usr->hash_alg, usr->A, B, usr->ng->N_len); + if (!u) { + pw_log_debug("srp: couldn't calc u"); + goto cleanup_and_exit; + } + x = calculate_x(usr->hash_alg, bytes_s, len_s, usr->username, usr->password, usr->password_len); + if (!x) { + pw_log_debug("srp: couldn't calc x"); + goto cleanup_and_exit; + } + /* SRP-6a safety check */ + if (!BN_is_zero(B) && !BN_is_zero(u)) { + BN_mod_exp(v, usr->ng->g, x, usr->ng->N, bn_ctx); + + /* S = (B - k*(g^x)) ^ (a + ux) */ + BN_mul(tmp1, u, x, bn_ctx); + BN_add(tmp2, usr->a, tmp1); /* tmp2 = (a + ux) */ + BN_mod_exp(tmp1, usr->ng->g, x, usr->ng->N, bn_ctx); + BN_mul(tmp3, k, tmp1, bn_ctx); /* tmp3 = k*(g^x) */ + BN_sub(tmp1, B, tmp3); /* tmp1 = (B - K*(g^x)) */ + BN_mod_exp(usr->S, tmp1, tmp2, usr->ng->N, bn_ctx); + + hash_num(usr->hash_alg, usr->S, usr->session_key); + + calculate_M(usr->hash_alg, usr->ng, usr->M, usr->username, bytes_s, len_s, usr->bytes_A, usr->len_A, bytes_B, len_B, usr->session_key); + calculate_H_AMK(usr->hash_alg, usr->H_AMK, usr->bytes_A, usr->len_A, usr->M, usr->session_key); + + *bytes_M = usr->M; + if (len_M) + *len_M = hash_length(usr->hash_alg); + } + +cleanup_and_exit: + BN_free(B); + BN_free(u); + BN_free(x); + BN_free(k); + BN_free(v); + BN_free(tmp1); + BN_free(tmp2); + BN_free(tmp3); + BN_CTX_free(bn_ctx); +} + +int srp_user_is_authenticated(struct SRPUser *usr) +{ + return usr->authenticated; +} + +const unsigned char *srp_user_get_session_key(struct SRPUser *usr, int *key_length) +{ + if (key_length) + *key_length = hash_length(usr->hash_alg); + + return usr->session_key; +} + +void srp_user_verify_session(struct SRPUser *usr, const unsigned char *bytes_HAMK) +{ + if (memcmp(usr->H_AMK, bytes_HAMK, hash_length(usr->hash_alg)) == 0 ) + usr->authenticated = 1; +} diff --git a/src/modules/module-raop/srp.h b/src/modules/module-raop/srp.h new file mode 100644 index 000000000..9671252b4 --- /dev/null +++ b/src/modules/module-raop/srp.h @@ -0,0 +1,31 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2013 Tom Cocagne */ +/* SPDX-License-Identifier: MIT */ + +struct SRPUser; + +typedef enum { + SRP_NG_2048, + SRP_NG_3072 +} SRP_NGType; + +typedef enum { + SRP_SHA1, + SRP_SHA512 +} SRP_HashAlgorithm; + +struct SRPUser *srp_user_new(SRP_HashAlgorithm alg, SRP_NGType ng_type, const char *username, + const unsigned char *bytes_password, int len_password, const char *n_hex, const char *g_hex); + +int get_N_len(SRP_NGType ng_type); + +void srp_user_start_authentication(struct SRPUser *usr, const char **username, const unsigned char **bytes_A, int *len_A); + +void srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, int len_s, + const unsigned char *bytes_B, int len_B, const unsigned char **bytes_M, int *len_M); + +int srp_user_is_authenticated(struct SRPUser *usr); + +const unsigned char *srp_user_get_session_key(struct SRPUser *usr, int *key_length); + +void srp_user_verify_session(struct SRPUser *usr, const unsigned char *bytes_HAMK); From 45a0460722fec334f2e0ab83e1fba7673cef9f35 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sun, 30 Jul 2023 00:54:56 +0200 Subject: [PATCH 5/8] module-raop: Add transient HomeKit pairing --- src/modules/module-raop-discover.c | 104 ++++++-- src/modules/module-raop-sink.c | 365 ++++++++++++++++++++++++++++- 2 files changed, 432 insertions(+), 37 deletions(-) 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; From cf2aac7039ae7653ffc03cd0a9220e267e2b3e31 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Thu, 13 Jul 2023 02:10:17 +0200 Subject: [PATCH 6/8] DNM: devel: Disable RAOP Avahi browser --- src/modules/module-raop-discover.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c index d87220e50..b7edd2226 100644 --- a/src/modules/module-raop-discover.c +++ b/src/modules/module-raop-discover.c @@ -550,10 +550,10 @@ 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_raop == NULL) - impl->sink_browser_raop = make_browser(impl, SERVICE_TYPE_RAOP); - if (impl->sink_browser_raop == NULL) - goto error; +// 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) From b4b3a5338471064c4c67096bb9e691a087016d2f Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sat, 5 Aug 2023 06:37:12 +0200 Subject: [PATCH 7/8] GET /info --- meson.build | 1 + src/modules/meson.build | 2 +- src/modules/module-raop-sink.c | 457 ++++++++++++++++++++++++-- src/modules/module-raop/rtsp-client.c | 3 +- 4 files changed, 428 insertions(+), 35 deletions(-) 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) From 77437f5a79b667d271eb9ea198becf16f184e729 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Tue, 1 Aug 2023 21:07:19 +0200 Subject: [PATCH 8/8] module-raop/rtsp-client: Add cipher support --- src/modules/module-raop-sink.c | 37 ++++++------ src/modules/module-raop/rtsp-client.c | 83 +++++++++++++++++++++++---- src/modules/module-raop/rtsp-client.h | 17 ++++++ 3 files changed, 109 insertions(+), 28 deletions(-) diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c index ac53d5458..00162f796 100644 --- a/src/modules/module-raop-sink.c +++ b/src/modules/module-raop-sink.c @@ -269,8 +269,7 @@ struct impl { uint64_t salt_len; 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; + struct pw_rtsp_cipher_context *cipher_ctx; uint16_t control_port; int control_fd; @@ -1019,7 +1018,7 @@ static int rtsp_setup_reply(void *data, int status, const struct spa_dict *heade int res; pw_log_info("setup status: %d", status); - + // TODO if ((str = spa_dict_lookup(headers, "Session")) == NULL) { pw_log_error("missing Session header"); return 0; @@ -1183,13 +1182,13 @@ hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm 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) + +static struct 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)); + cctx = calloc(1, sizeof(*cctx)); if (!cctx) goto error; ret = hkdf_extract_expand(cctx->encryption_key, sizeof(cctx->encryption_key), shared_secret, shared_secret_len, enc_salt, enc_info); @@ -1361,7 +1360,7 @@ static ssize_t ap_decrypt(uint8_t **plaintext, size_t *plaintext_len, return cipher_block - ciphertext; } -*/ + static int rtsp_do_setup(struct impl *impl) { int res; @@ -1387,7 +1386,7 @@ 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); - switch (impl->protocol) { + switch (impl->encryption) { case CRYPTO_PAIR_TRANSIENT: // Encryption/decryption of control channel const char *control_salt = AP_CONTROL_SALT; @@ -1400,7 +1399,7 @@ static int rtsp_do_setup(struct impl *impl) 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, "latencyMax", 88200); // ? 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); @@ -1418,12 +1417,16 @@ static int rtsp_do_setup(struct impl *impl) 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); + impl->cipher_ctx = ap_cipher_context_new(impl->shared_secret, impl->shared_secret_len, control_salt, enc_info, control_salt, dec_info); + if (!impl->cipher_ctx) { + pw_log_error("Could not create control cipher"); + goto error; + } + + pw_rtsp_client_set_cipher_ctx(impl->rtsp, impl->cipher_ctx); + + pw_rtsp_client_set_encryption(impl->rtsp, ap_encrypt); + pw_rtsp_client_set_decryption(impl->rtsp, ap_decrypt); res = pw_rtsp_client_url_send(impl->rtsp, url, "SETUP", &impl->headers->dict, "application/x-apple-binary-plist", content, content_len, @@ -1855,8 +1858,8 @@ static int rtsp_ap2_pair_setup2_reply(void *data, int status, const struct spa_d // TODO pw_properties_set(impl->headers, "X-Apple-HKP", NULL); - //return rtsp_do_setup(impl); - return 0; + return rtsp_do_setup(impl); + //return 0; error: tlv_free(response); diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c index c8e81e0a0..1c0a5f1a8 100644 --- a/src/modules/module-raop/rtsp-client.c +++ b/src/modules/module-raop/rtsp-client.c @@ -56,6 +56,9 @@ struct pw_rtsp_client { unsigned int need_flush:1; enum client_recv_state recv_state; + struct pw_rtsp_cipher_context *cipher_ctx; + ssize_t (*decrypt) (uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pw_rtsp_cipher_context *cctx); + ssize_t (*encrypt) (uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pw_rtsp_cipher_context *cctx); int status; char line_buf[1024]; size_t line_pos; @@ -92,12 +95,33 @@ struct pw_rtsp_client *pw_rtsp_client_new(struct pw_loop *main_loop, client->headers = pw_properties_new(NULL, NULL); pw_array_init(&client->content, 4096); client->recv_state = CLIENT_RECV_NONE; + client->cipher_ctx = NULL; + client->decrypt = NULL; + client->encrypt = NULL; pw_log_info("new client %p", client); return client; } +void pw_rtsp_client_set_decryption(struct pw_rtsp_client *client, + ssize_t (*decrypt) (uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pw_rtsp_cipher_context *cctx)) +{ + client->decrypt = decrypt; +} + +void pw_rtsp_client_set_encryption(struct pw_rtsp_client *client, + ssize_t (*encrypt) (uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pw_rtsp_cipher_context *cctx)) +{ + client->encrypt = encrypt; +} + +void pw_rtsp_client_set_cipher_ctx(struct pw_rtsp_client *client, + struct pw_rtsp_cipher_context *cipher_ctx) +{ + client->cipher_ctx = cipher_ctx; +} + void pw_rtsp_client_destroy(struct pw_rtsp_client *client) { pw_log_info("destroy client %p", client); @@ -154,6 +178,8 @@ int pw_rtsp_client_get_local_ip(struct pw_rtsp_client *client, static int handle_connect(struct pw_rtsp_client *client, int fd) { + pw_log_info("enter handle connect"); + int res, ip_version; socklen_t len; char local_ip[INET6_ADDRSTRLEN]; @@ -185,9 +211,12 @@ static int handle_connect(struct pw_rtsp_client *client, int fd) client->recv_state = CLIENT_RECV_STATUS; pw_properties_clear(client->headers); + pw_array_reset(&client->content); client->status = 0; client->line_pos = 0; client->content_length = 0; + client->decrypt = NULL; + client->encrypt = NULL; pw_rtsp_client_emit_connected(client); @@ -196,7 +225,7 @@ static int handle_connect(struct pw_rtsp_client *client, int fd) static int read_line(struct pw_rtsp_client *client, char **buf) { - int res; + int res, idx = 0; while (true) { uint8_t c; @@ -217,15 +246,17 @@ static int read_line(struct pw_rtsp_client *client, char **buf) client->line_pos = 0; if (buf) *buf = client->line_buf; - return 1; + return idx; } if (c == '\r') continue; - if (client->line_pos < sizeof(client->line_buf) - 1) + if (client->line_pos < sizeof(client->line_buf) - 1) { client->line_buf[client->line_pos++] = c; + idx++; + } client->line_buf[client->line_pos] = '\0'; } - return 0; + return idx; } static struct message *find_pending(struct pw_rtsp_client *client, uint32_t cseq) @@ -355,7 +386,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); + pw_log_info("processing content (%ld bytes):\n%s", client->content.size, (char *)client->content.data); } if (client->content_length == 0) @@ -370,11 +401,24 @@ static int process_input(struct pw_rtsp_client *client) char *buf = NULL; int res; - if ((res = read_line(client, &buf)) <= 0) + if ((res = read_line(client, &buf)) < 0) { + pw_log_debug("ret 1: %d", res); return res; - - pw_log_debug("received line: %s", buf); - + } + pw_log_debug("received line (%ld bytes): %s", res, buf); + if (res > 0 && client->decrypt != NULL && client->cipher_ctx != NULL) { + pw_log_debug("decrypting"); + uint8_t *dec_buf = malloc(1024); + size_t dec_len; + ssize_t dec; + dec = client->decrypt(dec_buf, &dec_len, (const uint8_t *)buf, res, client->cipher_ctx); + if (dec < 0) { + pw_log_debug("ret 2"); + return dec; + } + buf = dec_buf; + pw_log_debug("plain line: %s", buf); + } switch (client->recv_state) { case CLIENT_RECV_STATUS: return process_status(client, buf); @@ -446,14 +490,17 @@ on_source_io(void *data, int fd, uint32_t mask) int res; if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { + pw_log_info("ERR"); res = -EPIPE; goto error; } if (mask & SPA_IO_IN) { + pw_log_info("IN"); if ((res = process_input(client)) < 0) goto error; } if (mask & SPA_IO_OUT || client->need_flush) { + pw_log_info("OUT"); if (client->connecting) { if ((res = handle_connect(client, fd)) < 0) goto error; @@ -553,6 +600,9 @@ int pw_rtsp_client_disconnect(struct pw_rtsp_client *client) client->url = NULL; free(client->session_id); client->session_id = NULL; + if (client->cipher_ctx != NULL) + free(client->cipher_ctx); + client->cipher_ctx = NULL; spa_list_consume(msg, &client->messages, link) { spa_list_remove(&msg->link); @@ -599,8 +649,19 @@ int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url, fclose(f); - msg->data = SPA_PTROFF(msg, sizeof(*msg), void); - msg->len = len - sizeof(*msg); + if (client->encrypt != NULL && client->cipher_ctx != NULL) { + uint8_t *enc_buf; + size_t enc_len; + ssize_t enc = client->encrypt(&enc_buf, &enc_len, SPA_PTROFF(msg, sizeof(*msg), void), len - sizeof(*msg), client->cipher_ctx); + if (enc < 0) + return enc; + msg->data = enc_buf; + msg->len = enc_len; + } + else { + msg->data = SPA_PTROFF(msg, sizeof(*msg), void); + msg->len = len - sizeof(*msg); + } msg->offset = 0; msg->reply = reply; msg->user_data = user_data; diff --git a/src/modules/module-raop/rtsp-client.h b/src/modules/module-raop/rtsp-client.h index 1b832cbcc..a407f0030 100644 --- a/src/modules/module-raop/rtsp-client.h +++ b/src/modules/module-raop/rtsp-client.h @@ -30,6 +30,15 @@ struct pw_rtsp_client_events { }; +struct pw_rtsp_cipher_context { + uint8_t encryption_key[32]; + uint8_t decryption_key[32]; + uint64_t encryption_counter; + uint64_t decryption_counter; + uint64_t encryption_counter_prev; + uint64_t decryption_counter_prev; +}; + struct pw_rtsp_client * pw_rtsp_client_new(struct pw_loop *main_loop, struct pw_properties *props, size_t user_data_size); @@ -64,6 +73,14 @@ int pw_rtsp_client_send(struct pw_rtsp_client *client, int (*reply) (void *user_data, int status, const struct spa_dict *headers, const struct pw_array *content), void *user_data); +void pw_rtsp_client_set_decryption(struct pw_rtsp_client *client, + ssize_t (*decrypt) (uint8_t **plaintext, size_t *plaintext_len, const uint8_t *ciphertext, size_t ciphertext_len, struct pw_rtsp_cipher_context *cctx)); + +void pw_rtsp_client_set_encryption(struct pw_rtsp_client *client, + ssize_t (*encrypt) (uint8_t **ciphertext, size_t *ciphertext_len, const uint8_t *plaintext, size_t plaintext_len, struct pw_rtsp_cipher_context *cctx)); + +void pw_rtsp_client_set_cipher_ctx(struct pw_rtsp_client *client, + struct pw_rtsp_cipher_context *cipher_ctx); #ifdef __cplusplus }