[airplay] Add WWW authentication for Airplay 2

Some speakers (e.g. Homepod) that are configured to require password want that
this is used both for pairing authentication and classic WWW Authentication.

This commit adds the WWW Authentication that is also used for Airplay 1.

Closes #1545.
This commit is contained in:
ejurgensen 2022-10-15 00:30:31 +02:00
parent 62c72d72d6
commit e437e41870

View File

@ -650,11 +650,220 @@ chacha_encrypt(uint8_t *cipher, uint8_t *plain, size_t plain_len, const void *ad
/* --------------------- Helpers for sending RTSP requests ------------------ */
static int
auth_header_add(struct evrtsp_request *req, const char *password, const char *realm, const char *nonce, const char *method, const char *uri)
{
const char *hash_fmt = "%02x";
const char *username = "";
char ha1[33] = { 0 };
char ha2[33] = { 0 };
char ebuf[64];
char auth[256];
uint8_t *hash_bytes;
size_t hashlen;
gcry_md_hd_t hd;
gpg_error_t gc_err;
int i;
int ret;
if (!password)
password = "";
gc_err = gcry_md_open(&hd, GCRY_MD_MD5, 0);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_AIRPLAY, "Could not open MD5: %s\n", ebuf);
return -1;
}
hashlen = gcry_md_get_algo_dlen(GCRY_MD_MD5);
/* HA 1 */
gcry_md_write(hd, username, strlen(username));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, realm, strlen(realm));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, password, strlen(password));
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
if (!hash_bytes)
{
DPRINTF(E_LOG, L_AIRPLAY, "Could not read MD5 hash\n");
return -1;
}
for (i = 0; i < hashlen; i++)
sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]);
/* RESET */
gcry_md_reset(hd);
/* HA 2 */
gcry_md_write(hd, method, strlen(method));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, uri, strlen(uri));
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
if (!hash_bytes)
{
DPRINTF(E_LOG, L_AIRPLAY, "Could not read MD5 hash\n");
return -1;
}
for (i = 0; i < hashlen; i++)
sprintf(ha2 + (2 * i), hash_fmt, hash_bytes[i]);
/* RESET */
gcry_md_reset(hd);
/* Final value */
gcry_md_write(hd, ha1, 32);
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, nonce, strlen(nonce));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, ha2, 32);
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
if (!hash_bytes)
{
DPRINTF(E_LOG, L_AIRPLAY, "Could not read MD5 hash\n");
return -1;
}
for (i = 0; i < hashlen; i++)
sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]);
gcry_md_close(hd);
/* Build header */
ret = snprintf(auth, sizeof(auth), "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"",
username, realm, nonce, uri, ha1);
if ((ret < 0) || (ret >= sizeof(auth)))
{
DPRINTF(E_LOG, L_AIRPLAY, "Authorization value header exceeds buffer size\n");
return -1;
}
evrtsp_add_header(req->output_headers, "Authorization", auth);
DPRINTF(E_SPAM, L_AIRPLAY, "Authorization header: %s\n", auth);
return 0;
}
static int
auth_header_parse(struct airplay_session *rs, struct evrtsp_request *req)
{
const char *param;
char *auth = NULL;
char *token;
char *ptr;
if (rs->realm)
{
free(rs->realm);
rs->realm = NULL;
}
if (rs->nonce)
{
free(rs->nonce);
rs->nonce = NULL;
}
param = evrtsp_find_header(req->input_headers, "WWW-Authenticate");
if (!param)
{
DPRINTF(E_LOG, L_AIRPLAY, "WWW-Authenticate header not found\n");
goto error;
}
DPRINTF(E_DBG, L_AIRPLAY, "WWW-Authenticate: %s\n", param);
if (strncmp(param, "Digest ", strlen("Digest ")) != 0)
{
DPRINTF(E_LOG, L_AIRPLAY, "Unsupported authentication method: %s\n", param);
goto error;
}
auth = strdup(param);
if (!auth)
{
DPRINTF(E_LOG, L_AIRPLAY, "Out of memory for WWW-Authenticate header copy\n");
goto error;
}
token = strchr(auth, ' ');
if (!token)
{
DPRINTF(E_LOG, L_AIRPLAY, "Unexpected WWW-Authenticate auth\n");
goto error;
}
token++;
token = strtok_r(token, " =", &ptr);
while (token)
{
if (strcmp(token, "realm") == 0)
{
token = strtok_r(NULL, "=\"", &ptr);
if (!token)
break;
rs->realm = strdup(token);
}
else if (strcmp(token, "nonce") == 0)
{
token = strtok_r(NULL, "=\"", &ptr);
if (!token)
break;
rs->nonce = strdup(token);
}
token = strtok_r(NULL, " =", &ptr);
}
if (!rs->realm || !rs->nonce)
{
DPRINTF(E_LOG, L_AIRPLAY, "Could not find realm/nonce in WWW-Authenticate header\n");
if (rs->realm)
{
free(rs->realm);
rs->realm = NULL;
}
if (rs->nonce)
{
free(rs->nonce);
rs->nonce = NULL;
}
goto error;
}
DPRINTF(E_SPAM, L_AIRPLAY, "Found realm: [%s], nonce: [%s]\n", rs->realm, rs->nonce);
free(auth);
return 0;
error:
free(auth);
return -1;
}
static int
request_headers_add(struct evrtsp_request *req, struct airplay_session *rs, enum evrtsp_cmd_type req_method)
{
char buf[64];
const char *user_agent;
const char *method;
const char *url;
int ret;
snprintf(buf, sizeof(buf), "%d", rs->cseq);
evrtsp_add_header(req->output_headers, "CSeq", buf);
@ -664,6 +873,20 @@ request_headers_add(struct evrtsp_request *req, struct airplay_session *rs, enum
user_agent = cfg_getstr(cfg_getsec(cfg, "general"), "user_agent");
evrtsp_add_header(req->output_headers, "User-Agent", user_agent);
// If we have a realm + nonce it means that the device told us in the reply to
// SETUP that www authentication with password is required
if (rs->realm && rs->nonce)
{
method = evrtsp_method(req_method);
url = (req_method == EVRTSP_REQ_OPTIONS) ? "*" : rs->session_url;
ret = auth_header_add(req, rs->password, rs->realm, rs->nonce, method, url);
if (ret < 0)
return -1;
rs->req_has_auth = 1;
}
snprintf(buf, sizeof(buf), "%" PRIX64, libhash);
evrtsp_add_header(req->output_headers, "Client-Instance", buf);
evrtsp_add_header(req->output_headers, "DACP-ID", buf);
@ -2714,6 +2937,28 @@ response_handler_setup_session(struct evrtsp_request *req, struct airplay_sessio
uint64_t uintval;
int ret;
if (req->response_code == RTSP_UNAUTHORIZED)
{
if (rs->req_has_auth)
{
DPRINTF(E_LOG, L_AIRPLAY, "Bad or missing password for device '%s' (%s)\n", rs->devname, rs->address);
return AIRPLAY_SEQ_ABORT;
}
// We haven't tried authenticating yet, so save realm and nonce from the
// received WWW-Authenticate header and trigger a re-run with auth header
ret = auth_header_parse(rs, req);
if (ret < 0)
return AIRPLAY_SEQ_ABORT;
return AIRPLAY_SEQ_START_PLAYBACK;
}
else if (req->response_code != RTSP_OK)
{
DPRINTF(E_WARN, L_AIRPLAY, "Unexpected reply to SETUP (session) from '%s'\n", rs->devname);
return AIRPLAY_SEQ_ABORT;
}
ret = wplist_from_evbuf(&response, req->input_buffer);
if (ret < 0)
{
@ -3147,7 +3392,9 @@ static struct airplay_seq_request airplay_seq_request[][7] =
#if AIRPLAY_USE_AUTH_SETUP
{ AIRPLAY_SEQ_START_PLAYBACK, "auth-setup", EVRTSP_REQ_POST, payload_make_auth_setup, NULL, "application/octet-stream", "/auth-setup", true },
#endif
{ AIRPLAY_SEQ_START_PLAYBACK, "SETUP (session)", EVRTSP_REQ_SETUP, payload_make_setup_session, response_handler_setup_session, "application/x-apple-binary-plist", NULL, false },
// proceed_on_rtsp_not_ok is true because a device may reply with 401 Unauthorized
// and a WWW-Authenticate header, and then we may need re-run with password auth
{ AIRPLAY_SEQ_START_PLAYBACK, "SETUP (session)", EVRTSP_REQ_SETUP, payload_make_setup_session, response_handler_setup_session, "application/x-apple-binary-plist", NULL, true },
{ AIRPLAY_SEQ_START_PLAYBACK, "SETPEERS", EVRTSP_REQ_SETPEERS, payload_make_setpeers, NULL, "/peer-list-changed", NULL, false },
{ AIRPLAY_SEQ_START_PLAYBACK, "SETUP (stream)", EVRTSP_REQ_SETUP, payload_make_setup_stream, response_handler_setup_stream, "application/x-apple-binary-plist", NULL, false },
{ AIRPLAY_SEQ_START_PLAYBACK, "RECORD", EVRTSP_REQ_RECORD, payload_make_record, response_handler_record, NULL, NULL, false },