From 2d29b6b6d1912eb3496160c76943a1a39927c0c5 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 9 Dec 2023 18:34:05 -0500
Subject: [PATCH 1/3] use base 64 to represent public key as string
---
lib/crypto.c | 24 ++++++++++++++++++++++++
lib/crypto.h | 1 +
lib/pairing.c | 12 ++++++++++--
lib/pairing.h | 3 ++-
lib/raop_handlers.h | 18 ++++++++++--------
5 files changed, 47 insertions(+), 11 deletions(-)
diff --git a/lib/crypto.c b/lib/crypto.c
index c1167d5..a3da644 100644
--- a/lib/crypto.c
+++ b/lib/crypto.c
@@ -546,3 +546,27 @@ void sha_destroy(sha_ctx_t *ctx) {
int get_random_bytes(unsigned char *buf, int num) {
return RAND_bytes(buf, num);
}
+#include
+void pk_to_base64(const unsigned char *pk, int pk_len, char *pk_base64, int len) {
+ memset(pk_base64, 0, len);
+ int len64 = (4 * (pk_len /3)) + (pk_len % 3 ? 4 : 0);
+
+ assert (len > len64);
+
+ BIO *b64 = BIO_new(BIO_f_base64());
+ BIO *bio = BIO_new(BIO_s_mem());
+ BUF_MEM *bufferPtr;
+
+
+ bio = BIO_push(b64, bio);
+
+ BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
+ BIO_write(bio, pk, pk_len);
+ BIO_flush(bio);
+
+ BIO_get_mem_ptr(bio, &bufferPtr);
+ BIO_set_close(bio, BIO_NOCLOSE);
+ BIO_free_all(bio);
+ memcpy(pk_base64,(*bufferPtr).data, len64);
+}
+
diff --git a/lib/crypto.h b/lib/crypto.h
index 47ac30a..7f3f758 100644
--- a/lib/crypto.h
+++ b/lib/crypto.h
@@ -67,6 +67,7 @@ x25519_key_t *x25519_key_from_raw(const unsigned char data[X25519_KEY_SIZE]);
void x25519_key_get_raw(unsigned char data[X25519_KEY_SIZE], const x25519_key_t *key);
void x25519_key_destroy(x25519_key_t *key);
int get_random_bytes(unsigned char *buf, int num);
+void pk_to_base64(const unsigned char *pk, int pk_len, char *pk_base64, int len);
void x25519_derive_secret(unsigned char secret[X25519_KEY_SIZE], const x25519_key_t *ours, const x25519_key_t *theirs);
diff --git a/lib/pairing.c b/lib/pairing.c
index 3df0b58..590d0dc 100644
--- a/lib/pairing.c
+++ b/lib/pairing.c
@@ -447,8 +447,16 @@ srp_confirm_pair_setup(pairing_session_t *session, pairing_t *pairing,
return epk_len;
}
-void access_client_session_data(pairing_session_t *session, char **username, unsigned char **client_pk, bool *setup) {
+void access_client_session_data(pairing_session_t *session, char **username, char **client_pk64, bool *setup) {
+ int len64 = 4 * (1 + (ED25519_KEY_SIZE / 3)) + 1;
+ *client_pk64 = (char *) malloc(len64);
*username = session->username;
- *client_pk = session->client_pk;
+ pk_to_base64(session->client_pk, ED25519_KEY_SIZE, *client_pk64, len64);
setup = &(session->pair_setup);
}
+
+void ed25519_pk_to_base64(const unsigned char *pk, char **pk64) {
+ int len64 = 4 * (1 + (ED25519_KEY_SIZE / 3)) + 1;
+ *pk64 = (char *) malloc(len64);
+ pk_to_base64(pk, ED25519_KEY_SIZE, *pk64, len64);
+}
diff --git a/lib/pairing.h b/lib/pairing.h
index eec1231..84f2077 100644
--- a/lib/pairing.h
+++ b/lib/pairing.h
@@ -60,5 +60,6 @@ int srp_validate_proof(pairing_session_t *session, pairing_t *pairing, const uns
int len_A, unsigned char *proof, int client_proof_len, int proof_len);
int srp_confirm_pair_setup(pairing_session_t *session, pairing_t *pairing, unsigned char *epk,
unsigned char *auth_tag);
-void access_client_session_data(pairing_session_t *session, char **username, unsigned char **client_pk, bool *setup);
+void access_client_session_data(pairing_session_t *session, char **username, char **client_pk, bool *setup);
+void ed25519_pk_to_base64(const unsigned char *pk, char **pk64);
#endif
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 679cb72..4bb21d5 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -343,13 +343,12 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
} else {
bool client_pair_setup;
char *client_device_id;
- unsigned char *client_pk;
+ char *client_pk; /* encoded as null-terminated base64 string*/
access_client_session_data(conn->session, &client_device_id, &client_pk, &client_pair_setup);
- char * client_pk_str = utils_pk_to_string(client_pk, ED25519_KEY_SIZE);
if (conn->raop->callbacks.register_client) {
- conn->raop->callbacks.register_client(conn->raop->callbacks.cls, client_device_id, client_pk_str);
+ conn->raop->callbacks.register_client(conn->raop->callbacks.cls, client_device_id, client_pk);
}
- free (client_pk_str);
+ free (client_pk);
logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-pin-setup success\n");
}
pairing_session_set_setup_status(conn->session);
@@ -442,12 +441,15 @@ raop_handler_pairverify(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ED25519 signature");
}
if (register_check) {
- char *pk_str = utils_pk_to_string((const unsigned char *)(data + 4 + X25519_KEY_SIZE), ED25519_KEY_SIZE);
bool registered_client = true;
- if (conn->raop->callbacks.check_register) {
- registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk_str);
+ if (conn->raop->callbacks.check_register) {
+ const unsigned char *pk = data + 4 + X25519_KEY_SIZE;
+ char *pk64;
+ ed25519_pk_to_base64(pk, &pk64);
+ registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk64);
+ free (pk64);
}
- free (pk_str);
+
if (!registered_client) {
return;
}
From 7406b000b171a41ee0b5b699b0486993ff97a900 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 12 Dec 2023 00:14:10 -0500
Subject: [PATCH 2/3] client pin-paring register is working now
---
uxplay.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 62 insertions(+), 7 deletions(-)
diff --git a/uxplay.cpp b/uxplay.cpp
index aafaa78..1af4284 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -132,6 +133,10 @@ static unsigned short pin = 0;
static std::string keyfile = "";
static std::string mac_address = "";
static std::string dacpfile = "";
+static bool registration_list = true;
+static std::string pairing_register = "";
+static std::vector registered_keys;
+
/* logging */
static void log(int level, const char* format, ...) {
@@ -1541,15 +1546,38 @@ extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
}
}
-extern "C" void register_client(void *cls, const char *device_id, const char *client_pk_str) {
- /* pair-setup-pin client registration by the server is not implemented here, do nothing*/
- LOGD("registered new client: DeviceID = %s\nPK = \"%s\"", device_id, client_pk_str);
+extern "C" void register_client(void *cls, const char *device_id, const char *client_pk) {
+ if (!registration_list) {
+ /* we are not maintaining a list of registered clients */
+ return;
+ }
+ LOGI("registered new client: DeviceID = %s PK = \n%s", device_id, client_pk);
+ registered_keys.push_back(client_pk);
+ if (strlen(pairing_register.c_str())) {
+ FILE *fp = fopen(pairing_register.c_str(), "a");
+ if (fp) {
+ fprintf(fp, "%s,%s\n", client_pk, device_id);
+ fclose(fp);
+ }
+ }
}
-extern "C" bool check_register(void *cls, const char *client_pk_str) {
- /* pair-setup-pin client registration by the server is not implemented here, return "true"*/
- LOGD("register check returning client:\nPK = \"%s\"", client_pk_str);
- return true;
+extern "C" bool check_register(void *cls, const char *client_pk) {
+ if (!registration_list) {
+ /* we are not maintaining a list of registered clients */
+ return true;
+ }
+ LOGD("check returning client registration:\n PK:%s", client_pk);
+ if (std::find(registered_keys.rbegin(), registered_keys.rend(), client_pk) != registered_keys.rend()) {
+ LOGD("client registration found");
+ return true;
+ } else {
+ LOGE("returning client's pairing registration not found,\n PK: %s", client_pk);
+ for (int i = 0; i < registered_keys.size(); i++) {
+ printf("%s\n", (registered_keys[i]).c_str());
+ }
+ return false;
+ }
}
extern "C" void log_callback (void *cls, int level, const char *msg) {
@@ -1825,6 +1853,33 @@ int main (int argc, char *argv[]) {
video_parser.append(BT709_FIX);
}
+ if (require_password && registration_list) {
+ if (pairing_register == "") {
+ const char * homedir = get_homedir();
+ if (homedir) {
+ pairing_register = homedir;
+ pairing_register.append("/.uxplay.pair_register");
+ }
+ }
+ }
+
+ /* read in public keys that were previously registered with pair-setup-pin */
+ if (require_password && registration_list && strlen(pairing_register.c_str())) {
+ char * line = NULL;
+ size_t len = 0;
+ std::string key;
+ FILE *fp = fopen(pairing_register.c_str(), "r");
+ if (fp) {
+ while ((getline(&line, &len, fp)) != -1) {
+ /*32 bytes pk -> base64 -> strlen(pk64) = 44 chars = line[0:43]; remove \n at line[44] */
+ line[44] = '\0';
+ registered_keys.push_back(key.assign(line));
+ }
+ fclose(fp);
+ free (line);
+ }
+ }
+
if (require_password && keyfile == "") {
const char * homedir = get_homedir();
if (homedir) {
From 471af2133204e6035ed396d7f0b4138da72c774b Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 26 Dec 2023 04:57:37 -0500
Subject: [PATCH 3/3] v1.68: improvements to pin-pairing
---
README.html | 72 +++++++++++++++++++++++++++++++++------------
README.md | 41 +++++++++++++++++++++-----
README.txt | 70 +++++++++++++++++++++++++++++++++----------
lib/crypto.c | 14 +++++++--
lib/crypto.h | 2 +-
lib/pairing.c | 16 ++++++----
lib/pairing.h | 2 +-
lib/raop.c | 4 +--
lib/raop.h | 4 +--
lib/raop_handlers.h | 40 ++++++++++++++++---------
uxplay.1 | 16 +++++++---
uxplay.cpp | 51 ++++++++++++++++++++++----------
12 files changed, 244 insertions(+), 88 deletions(-)
diff --git a/README.html b/README.html
index bfe24c3..216b869 100644
--- a/README.html
+++ b/README.html
@@ -1,6 +1,6 @@
UxPlay
-1.67: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix
+id="uxplay-1.68-airplay-mirror-and-airplay-audio-server-for-linux-macos-and-unix-now-also-runs-on-windows.">UxPlay
+1.68: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix
(now also runs on Windows).
Now
@@ -9,9 +9,13 @@ href="https://github.com/FDH2/UxPlay">https://github.com/FDH2/UxPlay
(where ALL user issues should be posted, and latest versions can be
found).
-- NEW in v1.67: support for one-time Apple-style
-“pin” code client authentication (“client-server pairing”) when the
-option “-pin” is used.
+- NEW in v1.68: improved support for one-time
+Apple-style “pin” codes introduced in 1.67: a register of pin-registered
+clients is now optionally maintained to check returning clients; a
+simpler method for generating a persistent public key (based on the MAC
+address, which now can be set in the UxPlay startup file) is now the
+default. (The pem-file method introduced in 1.67 is still available with
+the ’-key” option.)
Highlights:
@@ -461,12 +465,17 @@ clients to “pair” with the UxPlay server the first time they connect to
it, by entering a 4-digit pin code that is displayed on the UxPlay
terminal. (This is optional, but sometimes required if the client is a
corporately-owned and -managed device with MDM Mobile Device
-Management.) Pairing occurs just once, is curently only recorded in the
-client, and persists unless the UxPlay public key (stored in
-$HOME/.uxplay.pem, or elsewhere if option
--key <filename> is used) is moved or deleted, after
-which a new key is generated. (Non-Apple clients might not implement the
-persistence feature.)
+Management.) Pairing occurs just once, is currently only recorded in the
+client, and persists unless the UxPlay public key is changed. By default
+(since v1.68) the public key is now generated using the “Device ID”,
+which is either the server’s hardware MAC address, or can be set with
+the -m option (most conveniently using the startup option file).
+(Storage of a more securely-generated persistent key as an OpenSSL “pem”
+file is still available with the -key option). For use of uxplay in a
+more public environment, a list of previously-registered clients can
+(since v1.68) be optionally-maintained using the -reg option: without
+this option, returning clients claiming to be registered are just
+trusted and not checked.
By default, UxPlay is locked to its current client until that
client drops the connection; since UxPlay-1.58, the option
-nohold modifies this behavior so that when a new client
@@ -895,6 +904,14 @@ UxPlay startups. As long as this file is not deleted or moved, a client
will not have to re-authenticate after an initial authentication.
(Add a “pin” entry in the UxPlay startup file if you wish the UxPlay
server to use this protocol).
+-reg [filename]: (since v1.68). This option
+maintains a list of previously-pin-registered clients in
+$HOME/.uxplay.register (or optionally, in filename). Without
+this option, returning clients claiming to be already pin-registered are
+trusted and not checked. (This option may be useful if UxPlay is used in
+a more public environment, to record client details; the register is
+text, one line per client, with client’s public key (base-64 format),
+Device ID, and Device name.)
-vsync [x] (In Mirror mode:) this option
(now the default) uses timestamps to synchronize audio
with video on the server, with an optional audio delay in (decimal)
@@ -1107,13 +1124,27 @@ card, (more specifically, the MAC address used by the first active
network interface detected) a random MAC address will be used even if
option -m was not specified. (Note that a random MAC
address will be different each time UxPlay is started).
--key [filename]: By default, the storage of
-the Server private key is in the file $HOME/.uxplay.pem. Use the “-key
-filename” option to change this location. This option should be
-set in the UxPlay startup file as a line “key filename” (no
-initial “-”), where filename is a full path. The filename
-may be enclosed in quotes ("...."), (and must be, if the
-filename has any blank spaces).
+-key [filename]: This (more secure) option
+for generating and storing a persistant public key (needed for the -pin
+option) has been replaced by default with a (less secure) method which
+generates a key from the server’s “device ID” (MAC address, which can be
+changed with the -m option, conveniently as a startup file option). When
+the -key option is used, a securely generated keypair is generated and
+stored in $HOME/.uxplay.pem, if that file does not exist,
+or read from it, if it exists. (Optionally, the key can be stored in
+filename.) This method is more secure than the new default
+method, (because the Device ID is broadcast in the DNS_SD announcement)
+but still leaves the private key exposed to anyone who can access the
+pem file. Because the default (but “less-secure”) “Device ID” method is
+simpler, and security of client access to uxplay is unlikely to be an
+important issue, the -key option is no longer recommended.
+By default, the storage of the Server private key is in the file
+$HOME/.uxplay.pem. Use the “-key filename” option to change
+this location. This option should be set in the UxPlay startup file as a
+line “key filename” (no initial “-”), where
+filename is a full path. The filename may be enclosed in
+quotes ("...."), (and must be, if the filename has any
+blank spaces).
-dacp [filename]: Export current client
DACP-ID and Active-Remote key to file: default is $HOME/.uxplay.dacp.
(optionally can be changed to filename). Can be used by remote
@@ -1463,6 +1494,11 @@ an AppleTV6,2 with sourceVersion 380.20.1 (an AppleTV 4K 1st gen,
introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
Changelog
+1.68 2023-12-25 Introduced a simpler (default) method for generating
+a persistent public key from the server MAC address (which can now be
+set with the -m option). (The previous pem-file method is still
+available with -key option). New option -reg to maintain a register of
+pin-authenticated clients.
1.67 2023-11-30 Add support for Apple-style one-time pin
authentication of clients with option “-pin”: (uses SRP6a authentication
protocol and public key persistence). Detection with error message of
diff --git a/README.md b/README.md
index 7827ae7..04db3aa 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,12 @@
-# UxPlay 1.67: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
+# UxPlay 1.68: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
### Now developed at the GitHub site [https://github.com/FDH2/UxPlay](https://github.com/FDH2/UxPlay) (where ALL user issues should be posted, and latest versions can be found).
- * _**NEW in v1.67**: support for one-time Apple-style "pin" code client authentication ("client-server
- pairing") when the option "-pin" is used._
+ * _**NEW in v1.68**: improved support for one-time Apple-style "pin" codes introduced in 1.67: a register of pin-registered clients is
+ now optionally maintained to check returning clients; a simpler method for generating a persistent public key (based on
+ the MAC address, which now can be set in the UxPlay startup file) is now the default. (The pem-file method introduced in 1.67 is
+ still available with the '-key" option.)_
+
## Highlights:
@@ -375,10 +378,14 @@ help with this or other problems.
* Since v 1.67, the UxPlay option "`-pin`" allows clients to "pair" with the UxPlay server
the first time they connect to it, by entering
a 4-digit pin code that is displayed on the UxPlay terminal. (This is optional, but sometimes required if the client is a
-corporately-owned and -managed device with MDM Mobile Device Management.) Pairing occurs just once, is curently only
+corporately-owned and -managed device with MDM Mobile Device Management.) Pairing occurs just once, is currently only
recorded in the client, and persists unless the
-UxPlay public key (stored in $HOME/.uxplay.pem, or elsewhere if option `-key ` is used) is moved or deleted, after
-which a new key is generated. (Non-Apple clients might not implement the persistence feature.)
+UxPlay public key is changed. By default (since v1.68) the public key is now generated using the "Device ID", which is either the server's
+hardware MAC address, or
+can be set with the -m option (most conveniently using the startup option file). (Storage of a more securely-generated
+persistent key as an OpenSSL "pem" file is still available with the -key option). For use of uxplay in a more public environment, a
+list of previously-registered clients can (since v1.68) be optionally-maintained using the -reg option: without this
+option, returning clients claiming to be registered are just trusted and not checked.
* By default, UxPlay is locked to
its current client until that client drops the connection; since UxPlay-1.58, the option `-nohold` modifies this
@@ -708,6 +715,12 @@ with "`#`" are treated as comments, and ignored. Command line options supersede
client will not have to re-authenticate after an initial authentication. _(Add a "pin" entry in the UxPlay startup file if you wish the
UxPlay server to use this protocol)._
+**-reg [_filename_]**: (since v1.68). This option maintains a list of previously-pin-registered clients in $HOME/.uxplay.register (or optionally, in _filename_).
+ Without this option, returning clients claiming to be already pin-registered are trusted and not checked. (This option may be useful if UxPlay is used
+ in a more public environment, to record client details; the register is text, one line per client, with client's public
+ key (base-64 format), Device ID, and Device name.)
+
+
**-vsync [x]** (In Mirror mode:) this option (**now the default**) uses timestamps to synchronize audio with video on the server,
with an optional audio delay in (decimal) milliseconds (_x_ = "20.5" means 0.0205 seconds delay: positive or
negative delays less than a second are allowed.) It is needed on low-power systems such as Raspberry Pi without hardware
@@ -891,7 +904,16 @@ which will not work if a firewall is running.
a random MAC address will be used even if option **-m** was not specified.
(Note that a random MAC address will be different each time UxPlay is started).
-**-key [_filename_]**: By default, the storage of the Server private key is in the file $HOME/.uxplay.pem. Use
+**-key [_filename_]**: This (more secure) option for generating and storing a persistant public key (needed for
+ the -pin option) has been replaced by default with a (less secure) method which generates a key from the server's "device ID"
+ (MAC address, which can be changed with the -m option, conveniently as a startup file option).
+ When the -key option is used, a securely generated keypair is generated and stored in `$HOME/.uxplay.pem`, if that file does not exist,
+ or read from it, if it exists. (Optionally, the key can be stored in _filename_.) This method is more secure than the new default method,
+ (because the Device ID is broadcast in the DNS_SD announcement) but still leaves the private key exposed to anyone who can access the pem file.
+ Because the default (but "less-secure") "Device ID" method is simpler, and security of client access to uxplay is unlikely to be an important issue,
+ the -key option is no longer recommended.
+
+By default, the storage of the Server private key is in the file $HOME/.uxplay.pem. Use
the "-key _filename_" option to change this location. This option should be set in the UxPlay startup file
as a line "`key filename`" (no initial "-"), where ``filename`` is a full path. The filename may be enclosed
in quotes (`"...."`), (and must be, if the filename has any blank spaces).
@@ -1156,6 +1178,11 @@ tvOS 12.2.1), so it does not seem to matter what version UxPlay claims to be.
# Changelog
+1.68 2023-12-25 Introduced a simpler (default) method for generating a persistent public key from the server MAC
+ address (which can now be set with the -m option).
+ (The previous pem-file method is still available with -key option).
+ New option -reg to maintain a register of pin-authenticated clients.
+
1.67 2023-11-30 Add support for Apple-style one-time pin authentication of clients with option "-pin":
(uses SRP6a authentication protocol and public key persistence). Detection with error message
of (currently) unsupported H265 video when requesting high resolution over wired ethernet.
diff --git a/README.txt b/README.txt
index 1035f1e..6453826 100644
--- a/README.txt
+++ b/README.txt
@@ -1,10 +1,14 @@
-# UxPlay 1.67: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
+# UxPlay 1.68: AirPlay-Mirror and AirPlay-Audio server for Linux, macOS, and Unix (now also runs on Windows).
### Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).
-- ***NEW in v1.67**: support for one-time Apple-style "pin" code
- client authentication ("client-server pairing") when the option
- "-pin" is used.*
+- ***NEW in v1.68**: improved support for one-time Apple-style "pin"
+ codes introduced in 1.67: a register of pin-registered clients is
+ now optionally maintained to check returning clients; a simpler
+ method for generating a persistent public key (based on the MAC
+ address, which now can be set in the UxPlay startup file) is now the
+ default. (The pem-file method introduced in 1.67 is still available
+ with the '-key" option.)*
## Highlights:
@@ -451,11 +455,18 @@ below for help with this or other problems.
entering a 4-digit pin code that is displayed on the UxPlay
terminal. (This is optional, but sometimes required if the client is
a corporately-owned and -managed device with MDM Mobile Device
- Management.) Pairing occurs just once, is curently only recorded in
- the client, and persists unless the UxPlay public key (stored in
- \$HOME/.uxplay.pem, or elsewhere if option `-key ` is
- used) is moved or deleted, after which a new key is generated.
- (Non-Apple clients might not implement the persistence feature.)
+ Management.) Pairing occurs just once, is currently only recorded in
+ the client, and persists unless the UxPlay public key is changed. By
+ default (since v1.68) the public key is now generated using the
+ "Device ID", which is either the server's hardware MAC address, or
+ can be set with the -m option (most conveniently using the startup
+ option file). (Storage of a more securely-generated persistent key
+ as an OpenSSL "pem" file is still available with the -key option).
+ For use of uxplay in a more public environment, a list of
+ previously-registered clients can (since v1.68) be
+ optionally-maintained using the -reg option: without this option,
+ returning clients claiming to be registered are just trusted and not
+ checked.
- By default, UxPlay is locked to its current client until that client
drops the connection; since UxPlay-1.58, the option `-nohold`
@@ -895,6 +906,14 @@ re-authenticate after an initial authentication. *(Add a "pin" entry in
the UxPlay startup file if you wish the UxPlay server to use this
protocol).*
+**-reg \[*filename*\]**: (since v1.68). This option maintains a list of
+previously-pin-registered clients in \$HOME/.uxplay.register (or
+optionally, in *filename*). Without this option, returning clients
+claiming to be already pin-registered are trusted and not checked. (This
+option may be useful if UxPlay is used in a more public environment, to
+record client details; the register is text, one line per client, with
+client's public key (base-64 format), Device ID, and Device name.)
+
**-vsync \[x\]** (In Mirror mode:) this option (**now the default**)
uses timestamps to synchronize audio with video on the server, with an
optional audio delay in (decimal) milliseconds (*x* = "20.5" means
@@ -1125,12 +1144,27 @@ network interface detected) a random MAC address will be used even if
option **-m** was not specified. (Note that a random MAC address will be
different each time UxPlay is started).
-**-key \[*filename*\]**: By default, the storage of the Server private
-key is in the file \$HOME/.uxplay.pem. Use the "-key *filename*" option
-to change this location. This option should be set in the UxPlay startup
-file as a line "`key filename`" (no initial "-"), where `filename` is a
-full path. The filename may be enclosed in quotes (`"...."`), (and must
-be, if the filename has any blank spaces).
+**-key \[*filename*\]**: This (more secure) option for generating and
+storing a persistant public key (needed for the -pin option) has been
+replaced by default with a (less secure) method which generates a key
+from the server's "device ID" (MAC address, which can be changed with
+the -m option, conveniently as a startup file option). When the -key
+option is used, a securely generated keypair is generated and stored in
+`$HOME/.uxplay.pem`, if that file does not exist, or read from it, if it
+exists. (Optionally, the key can be stored in *filename*.) This method
+is more secure than the new default method, (because the Device ID is
+broadcast in the DNS_SD announcement) but still leaves the private key
+exposed to anyone who can access the pem file. Because the default (but
+"less-secure") "Device ID" method is simpler, and security of client
+access to uxplay is unlikely to be an important issue, the -key option
+is no longer recommended.
+
+By default, the storage of the Server private key is in the file
+\$HOME/.uxplay.pem. Use the "-key *filename*" option to change this
+location. This option should be set in the UxPlay startup file as a line
+"`key filename`" (no initial "-"), where `filename` is a full path. The
+filename may be enclosed in quotes (`"...."`), (and must be, if the
+filename has any blank spaces).
**-dacp \[*filename*\]**: Export current client DACP-ID and
Active-Remote key to file: default is \$HOME/.uxplay.dacp. (optionally
@@ -1497,6 +1531,12 @@ what version UxPlay claims to be.
# Changelog
+1.68 2023-12-25 Introduced a simpler (default) method for generating a
+persistent public key from the server MAC address (which can now be set
+with the -m option). (The previous pem-file method is still available
+with -key option). New option -reg to maintain a register of
+pin-authenticated clients.
+
1.67 2023-11-30 Add support for Apple-style one-time pin authentication
of clients with option "-pin": (uses SRP6a authentication protocol and
public key persistence). Detection with error message of (currently)
diff --git a/lib/crypto.c b/lib/crypto.c
index a3da644..906a2fe 100644
--- a/lib/crypto.c
+++ b/lib/crypto.c
@@ -35,6 +35,8 @@
#include "utils.h"
+#define SALT_PK "UxPlay-Persistent-Not-Secure-Public-Key"
+
struct aes_ctx_s {
EVP_CIPHER_CTX *cipher_ctx;
uint8_t key[AES_128_BLOCK_SIZE];
@@ -352,7 +354,7 @@ struct ed25519_key_s {
EVP_PKEY *pkey;
};
-ed25519_key_t *ed25519_key_generate(const char *keyfile, int *result) {
+ed25519_key_t *ed25519_key_generate(const char *device_id, const char *keyfile, int *result) {
ed25519_key_t *key;
EVP_PKEY_CTX *pctx;
BIO *bp;
@@ -379,7 +381,15 @@ ed25519_key_t *ed25519_key_generate(const char *keyfile, int *result) {
new_pk = true;
}
} else {
- new_pk = true;
+ /* generate (insecure) persistent keypair using device_id */
+ unsigned char hash[SHA512_DIGEST_LENGTH];
+ char salt[] = SALT_PK;
+ sha_ctx_t *ctx = sha_init();
+ sha_update(ctx, (const unsigned char *) salt, (unsigned int) strlen(salt));
+ sha_update(ctx, (const unsigned char *) device_id, (unsigned int) strlen(device_id));
+ sha_final(ctx, hash, NULL);
+ sha_destroy(ctx);
+ key->pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, (const unsigned char *) hash, ED25519_KEY_SIZE);
}
if (new_pk) {
diff --git a/lib/crypto.h b/lib/crypto.h
index 7f3f758..7bd2d5e 100644
--- a/lib/crypto.h
+++ b/lib/crypto.h
@@ -83,7 +83,7 @@ int gcm_decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *pl
typedef struct ed25519_key_s ed25519_key_t;
-ed25519_key_t *ed25519_key_generate(const char * keyfile, int * result);
+ed25519_key_t *ed25519_key_generate(const char *device_id, const char * keyfile, int * result);
ed25519_key_t *ed25519_key_from_raw(const unsigned char data[ED25519_KEY_SIZE]);
void ed25519_key_get_raw(unsigned char data[ED25519_KEY_SIZE], const ed25519_key_t *key);
/*
diff --git a/lib/pairing.c b/lib/pairing.c
index 590d0dc..e60273c 100644
--- a/lib/pairing.c
+++ b/lib/pairing.c
@@ -84,7 +84,7 @@ derive_key_internal(pairing_session_t *session, const unsigned char *salt, unsig
}
pairing_t *
-pairing_init_generate(const char * keyfile, int *result)
+pairing_init_generate(const char *device_id, const char *keyfile, int *result)
{
pairing_t *pairing;
*result = 0;
@@ -93,7 +93,7 @@ pairing_init_generate(const char * keyfile, int *result)
return NULL;
}
- pairing->ed = ed25519_key_generate(keyfile, result);
+ pairing->ed = ed25519_key_generate(device_id, keyfile, result);
return pairing;
}
@@ -136,7 +136,7 @@ pairing_session_init(pairing_t *pairing)
session->status = STATUS_INITIAL;
session->srp = NULL;
-
+ session->pair_setup = false;
return session;
}
@@ -449,10 +449,14 @@ srp_confirm_pair_setup(pairing_session_t *session, pairing_t *pairing,
void access_client_session_data(pairing_session_t *session, char **username, char **client_pk64, bool *setup) {
int len64 = 4 * (1 + (ED25519_KEY_SIZE / 3)) + 1;
- *client_pk64 = (char *) malloc(len64);
- *username = session->username;
- pk_to_base64(session->client_pk, ED25519_KEY_SIZE, *client_pk64, len64);
setup = &(session->pair_setup);
+ *username = session->username;
+ if (setup) {
+ *client_pk64 = (char *) malloc(len64);
+ pk_to_base64(session->client_pk, ED25519_KEY_SIZE, *client_pk64, len64);
+ } else {
+ *client_pk64 = NULL;
+ }
}
void ed25519_pk_to_base64(const unsigned char *pk, char **pk64) {
diff --git a/lib/pairing.h b/lib/pairing.h
index 84f2077..54db62b 100644
--- a/lib/pairing.h
+++ b/lib/pairing.h
@@ -36,7 +36,7 @@
typedef struct pairing_s pairing_t;
typedef struct pairing_session_s pairing_session_t;
-pairing_t *pairing_init_generate(const char * keyfile, int *result);
+pairing_t *pairing_init_generate(const char *device_id, const char *keyfile, int *result);
void pairing_get_public_key(pairing_t *pairing, unsigned char public_key[ED25519_KEY_SIZE]);
pairing_session_t *pairing_session_init(pairing_t *pairing);
diff --git a/lib/raop.c b/lib/raop.c
index c42f177..ef9b009 100644
--- a/lib/raop.c
+++ b/lib/raop.c
@@ -411,7 +411,7 @@ conn_destroy(void *ptr) {
}
raop_t *
-raop_init(int max_clients, raop_callbacks_t *callbacks, const char* keyfile) {
+raop_init(int max_clients, raop_callbacks_t *callbacks, const char *device_id, const char *keyfile) {
raop_t *raop;
pairing_t *pairing;
httpd_t *httpd;
@@ -443,7 +443,7 @@ raop_init(int max_clients, raop_callbacks_t *callbacks, const char* keyfile) {
/* create a new public key for pairing */
int new_key;
- pairing = pairing_init_generate(keyfile, &new_key);
+ pairing = pairing_init_generate(device_id, keyfile, &new_key);
if (!pairing) {
free(raop);
return NULL;
diff --git a/lib/raop.h b/lib/raop.h
index ffbeab3..2888d3d 100644
--- a/lib/raop.h
+++ b/lib/raop.h
@@ -60,7 +60,7 @@ struct raop_callbacks_s {
void (*video_report_size)(void *cls, float *width_source, float *height_source, float *width, float *height);
void (*report_client_request) (void *cls, char *deviceid, char *model, char *name, bool *admit);
void (*display_pin) (void *cls, char * pin);
- void (*register_client) (void *cls, const char *device_id, const char *pk_str);
+ void (*register_client) (void *cls, const char *device_id, const char *pk_str, const char *name);
bool (*check_register) (void *cls, const char *pk_str);
void (*export_dacp) (void *cls, const char *active_remote, const char *dacp_id);
};
@@ -68,7 +68,7 @@ typedef struct raop_callbacks_s raop_callbacks_t;
raop_ntp_t *raop_ntp_init(logger_t *logger, raop_callbacks_t *callbacks, const char *remote, int remote_addr_len,
unsigned short timing_rport, timing_protocol_t *time_protocol);
- RAOP_API raop_t *raop_init(int max_clients, raop_callbacks_t *callbacks, const char* keyfile);
+RAOP_API raop_t *raop_init(int max_clients, raop_callbacks_t *callbacks, const char *device_id, const char *keyfile);
RAOP_API void raop_set_log_level(raop_t *raop, int level);
RAOP_API void raop_set_log_callback(raop_t *raop, raop_log_callback_t callback, void *cls);
RAOP_API int raop_set_plist(raop_t *raop, const char *plist_item, const int value);
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 4bb21d5..fff801b 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -341,14 +341,6 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "pair-pin-setup (step 3): client authentication failed\n");
goto authentication_failed;
} else {
- bool client_pair_setup;
- char *client_device_id;
- char *client_pk; /* encoded as null-terminated base64 string*/
- access_client_session_data(conn->session, &client_device_id, &client_pk, &client_pair_setup);
- if (conn->raop->callbacks.register_client) {
- conn->raop->callbacks.register_client(conn->raop->callbacks.cls, client_device_id, client_pk);
- }
- free (client_pk);
logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-pin-setup success\n");
}
pairing_session_set_setup_status(conn->session);
@@ -586,12 +578,32 @@ raop_handler_setup(raop_conn_t *conn,
if (conn->raop->callbacks.report_client_request) {
conn->raop->callbacks.report_client_request(conn->raop->callbacks.cls, deviceID, model, name, &admit_client);
}
- free (deviceID);
- deviceID = NULL;
- free (model);
- model = NULL;
- free (name);
- name = NULL;
+ if (admit_client && deviceID && name && conn->raop->callbacks.register_client) {
+ bool pending_registration;
+ char *client_device_id;
+ char *client_pk; /* encoded as null-terminated base64 string*/
+ access_client_session_data(conn->session, &client_device_id, &client_pk, &pending_registration);
+ if (pending_registration) {
+ if (client_pk && !strcmp(deviceID, client_device_id)) {
+ conn->raop->callbacks.register_client(conn->raop->callbacks.cls, client_device_id, client_pk, name);
+ }
+ }
+ if (client_pk) {
+ free (client_pk);
+ }
+ }
+ if (deviceID) {
+ free (deviceID);
+ deviceID = NULL;
+ }
+ if (model) {
+ free (model);
+ model = NULL;
+ }
+ if (name) {
+ free (name);
+ name = NULL;
+ }
if (admit_client == false) {
/* client is not authorized to connect */
plist_free(res_root_node);
diff --git a/uxplay.1 b/uxplay.1
index 200e052..99f3a79 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -1,11 +1,11 @@
-.TH UXPLAY "1" "November 2023" "1.67" "User Commands"
+.TH UXPLAY "1" "December 2023" "1.68" "User Commands"
.SH NAME
uxplay \- start AirPlay server
.SH SYNOPSIS
.B uxplay
[\fI\,-n name\/\fR] [\fI\,-s wxh\/\fR] [\fI\,-p \/\fR[\fI\,n\/\fR]] [more \fI OPTIONS \/\fR ...]
.SH DESCRIPTION
-UxPlay 1.67: An open\-source AirPlay mirroring (+ audio streaming) server:
+UxPlay 1.68: An open\-source AirPlay mirroring (+ audio streaming) server:
.SH OPTIONS
.TP
.B
@@ -17,6 +17,10 @@ UxPlay 1.67: An open\-source AirPlay mirroring (+ audio streaming) server:
.IP
without option, pin is random: optionally use fixed pin xxxx.
.TP
+\fB\-reg\fI [fn]\fR Keep a register in $HOME/.uxplay.register to verify returning
+.IP
+ client pin-registration; (option: use file "fn" for this)
+.TP
\fB\-vsync\fI[x]\fR Mirror mode: sync audio to video using timestamps (default)
.IP
\fIx\fR is optional audio delay: millisecs, decimal, can be neg.
@@ -106,9 +110,13 @@ UxPlay 1.67: An open\-source AirPlay mirroring (+ audio streaming) server:
.TP
\fB\-r\fR {R|L} Rotate 90 degrees Right (cw) or Left (ccw)
.TP
-\fB\-m\fR Use random MAC address (use for concurrent UxPlay's)
+\fB\-m\fI [mac]\fR Set MAC address (also Device ID); use for concurrent UxPlays
+.IP
+ if mac xx:xx:xx:xx:xx:xx is not given, a random MAC is used.
+.PP
.TP
-\fB\-key\fI fn \fR Store private key in file fn (Default: $HOME/.uxplay.pem)
+\fB\-key\fI [fn]\fR Store private key in $HOME/.uxplay.pem (or in file "fn")
+.PP
.TP
\fB\-dacp\fI [fn]\fRExport client DACP information to file $HOME/.uxplay.dacp
.IP
diff --git a/uxplay.cpp b/uxplay.cpp
index 1af4284..623d220 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -61,7 +61,7 @@
#include "renderers/video_renderer.h"
#include "renderers/audio_renderer.h"
-#define VERSION "1.67"
+#define VERSION "1.68"
#define SECOND_IN_USECS 1000000
#define SECOND_IN_NSECS 1000000000UL
@@ -133,7 +133,7 @@ static unsigned short pin = 0;
static std::string keyfile = "";
static std::string mac_address = "";
static std::string dacpfile = "";
-static bool registration_list = true;
+static bool registration_list = false;
static std::string pairing_register = "";
static std::vector registered_keys;
@@ -566,7 +566,9 @@ static void print_info (char *name) {
printf("-n name Specify the network name of the AirPlay server\n");
printf("-nh Do not add \"@hostname\" at the end of AirPlay server name\n");
printf("-pin[xxxx]Use a 4-digit pin code to control client access (default: no)\n");
- printf(" without option, pin is random: optionally use fixed pin xxxx\n");
+ printf(" default pin is random: optionally use fixed pin xxxx\n");
+ printf("-reg [fn] Keep a register in $HOME/.uxplay.register to verify returning\n");
+ printf(" client pin-registration; (option: use file \"fn\" for this)\n");
printf("-vsync [x]Mirror mode: sync audio to video using timestamps (default)\n");
printf(" x is optional audio delay: millisecs, decimal, can be neg.\n");
printf("-vsync no Switch off audio/(server)video timestamp synchronization \n");
@@ -612,8 +614,8 @@ static void print_info (char *name) {
printf("-f {H|V|I}Horizontal|Vertical flip, or both=Inversion=rotate 180 deg\n");
printf("-r {R|L} Rotate 90 degrees Right (cw) or Left (ccw)\n");
printf("-m [mac] Set MAC address (also Device ID);use for concurrent UxPlays\n");
- printf(" if mac xx:xx:xx:xx:xx:xx is not given, a random mac is used\n");
- printf("-key Store private key in file (default:$HOME/.uxplay.pem)\n");
+ printf(" if mac xx:xx:xx:xx:xx:xx is not given, a random MAC is used\n");
+ printf("-key [fn] Store private key in $HOME/.uxplay.pem (or in file \"fn\")\n");
printf("-dacp [fn]Export client DACP information to file $HOME/.uxplay.dacp\n");
printf(" (option to use file \"fn\" instead); used for client remote\n");
printf("-vdmp [n] Dump h264 video output to \"fn.h264\"; fn=\"videodump\",change\n");
@@ -1049,6 +1051,17 @@ static void parse_arguments (int argc, char *argv[]) {
}
pin = n + 10000;
}
+ } else if (arg == "-reg") {
+ registration_list = true;
+ pairing_register.erase();
+ if (i < argc - 1 && *argv[i+1] != '-') {
+ pairing_register.append(argv[++i]);
+ const char * fn = pairing_register.c_str();
+ if (!file_has_write_access(fn)) {
+ fprintf(stderr, "%s cannot be written to:\noption \"-key \" must be to a file with write access\n", fn);
+ exit(1);
+ }
+ }
} else if (arg == "-key") {
keyfile.erase();
if (i < argc - 1 && *argv[i+1] != '-') {
@@ -1059,8 +1072,10 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
}
} else {
- fprintf(stderr, "option \"-key \" requires a path to a file for persistent key storage\n");
- exit(1);
+ // fprintf(stderr, "option \"-key \" requires a path to a file for persistent key storage\n");
+ // exit(1);
+ keyfile.erase();
+ keyfile.append("0");
}
} else if (arg == "-dacp") {
dacpfile.erase();
@@ -1546,17 +1561,17 @@ extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
}
}
-extern "C" void register_client(void *cls, const char *device_id, const char *client_pk) {
+extern "C" void register_client(void *cls, const char *device_id, const char *client_pk, const char *client_name) {
if (!registration_list) {
/* we are not maintaining a list of registered clients */
return;
}
- LOGI("registered new client: DeviceID = %s PK = \n%s", device_id, client_pk);
+ LOGI("registered new client: %s DeviceID = %s PK = \n%s", client_name, device_id, client_pk);
registered_keys.push_back(client_pk);
if (strlen(pairing_register.c_str())) {
FILE *fp = fopen(pairing_register.c_str(), "a");
if (fp) {
- fprintf(fp, "%s,%s\n", client_pk, device_id);
+ fprintf(fp, "%s,%s,%s\n", client_pk, device_id, client_name);
fclose(fp);
}
}
@@ -1628,7 +1643,7 @@ static int start_raop_server (unsigned short display[5], unsigned short tcp[3],
raop_cbs.export_dacp = export_dacp;
/* set max number of connections = 2 to protect against capture by new client */
- raop = raop_init(max_connections, &raop_cbs, keyfile.c_str());
+ raop = raop_init(max_connections, &raop_cbs, mac_address.c_str(), keyfile.c_str());
if (raop == NULL) {
LOGE("Error initializing raop!");
return -1;
@@ -1858,7 +1873,7 @@ int main (int argc, char *argv[]) {
const char * homedir = get_homedir();
if (homedir) {
pairing_register = homedir;
- pairing_register.append("/.uxplay.pair_register");
+ pairing_register.append("/.uxplay.register");
}
}
}
@@ -1869,25 +1884,30 @@ int main (int argc, char *argv[]) {
size_t len = 0;
std::string key;
FILE *fp = fopen(pairing_register.c_str(), "r");
+ int clients = 0;
if (fp) {
- while ((getline(&line, &len, fp)) != -1) {
+ while ((getline(&line, &len, fp)) != -1) {
/*32 bytes pk -> base64 -> strlen(pk64) = 44 chars = line[0:43]; remove \n at line[44] */
line[44] = '\0';
registered_keys.push_back(key.assign(line));
+ clients ++;
+ }
+ if (clients) {
+ LOGI("Register %s lists %d pin-registered clients", pairing_register.c_str(), clients);
}
fclose(fp);
free (line);
}
}
- if (require_password && keyfile == "") {
+ if (require_password && keyfile == "0") {
const char * homedir = get_homedir();
if (homedir) {
keyfile.erase();
keyfile = homedir;
keyfile.append("/.uxplay.pem");
} else {
- LOGE("could not determine $HOME: public key wiil no be saved, and so will not be persistent");
+ LOGE("could not determine $HOME: public key wiil not be saved, and so will not be persistent");
}
}
@@ -1938,7 +1958,6 @@ int main (int argc, char *argv[]) {
LOGI("using randomly-generated MAC address %s",mac_address.c_str());
}
parse_hw_addr(mac_address, server_hw_addr);
- mac_address.clear();
if (coverart_filename.length()) {
LOGI("any AirPlay audio cover-art will be written to file %s",coverart_filename.c_str());